feat: add the inline tag and json-style omitempty
Test / test (push) Successful in 1m30s

Assisted-by: GLM 5.3 Flash
This commit is contained in:
2026-09-22 00:28:52 +02:00
parent 13ac6dd521
commit a8a2fcf8c3
4 changed files with 227 additions and 43 deletions
+97 -38
View File
@@ -275,7 +275,7 @@ func buildOrderedDoc(om *OrderedMap, doc *tomlDoc, path encPath) error {
// A nil value has no TOML form, the rule nil pointer fields follow.
continue
}
if err := addField(doc, key, rv, path); err != nil {
if err := addField(doc, key, rv, path, false); err != nil {
return err
}
}
@@ -304,6 +304,10 @@ type entry struct {
doc *tomlDoc // entryTable
docs []*tomlDoc
// inline forces a table entry to emit as `key = {…}`; it is set by the
// `,inline` tag option. An array of tables keeps the header form.
inline bool
// emitted records that the grouped emission wrote this table inline, so
// the header pass that follows skips it. The representation is built
// fresh per Marshal call.
@@ -347,8 +351,10 @@ func (d *tomlDoc) addScalar(key string, val any) {
d.entries = append(d.entries, entry{kind: entryScalar, key: key, val: val})
}
func (d *tomlDoc) addTable(key string, sub *tomlDoc) {
d.entries = append(d.entries, entry{kind: entryTable, key: key, doc: sub})
func (d *tomlDoc) addTable(key string, sub *tomlDoc) *entry {
e := entry{kind: entryTable, key: key, doc: sub}
d.entries = append(d.entries, e)
return &d.entries[len(d.entries)-1]
}
func (d *tomlDoc) addArray(key string, subs []*tomlDoc) {
@@ -482,7 +488,7 @@ func walkStructDoc(v reflect.Value, doc *tomlDoc, path encPath, prefix []int, sc
if fieldOmitted(f, v.Field(i)) {
continue
}
if err := addField(doc, name, v.Field(i), path); err != nil {
if err := addField(doc, name, v.Field(i), path, tagHasOption(f.Tag.Get("toml"), "inline")); err != nil {
return err
}
}
@@ -493,30 +499,69 @@ func walkStructDoc(v reflect.Value, doc *tomlDoc, path encPath, prefix []int, sc
// state decides through that method before reflection is consulted.
type isZeroer interface{ IsZero() bool }
// tagOptions returns the option part of a `toml` tag, the part after the
// first comma.
func tagOptions(tag string) string {
_, opts, _ := strings.Cut(tag, ",")
return opts
}
// tagHasOption reports whether want is one of the tag's comma-separated
// options.
func tagHasOption(tag, want string) bool {
opts := tagOptions(tag)
for opts != "" {
var opt string
opt, opts, _ = strings.Cut(opts, ",")
if opt == want {
return true
}
}
return false
}
// fieldOmitted reports whether the field's tag options drop it from the
// output: omitzero skips the zero value of the field's type, omitempty skips
// an empty collection (slice, array, or map). The decoder ignores both
// options; they shape emission only.
// an empty value in the encoding/json sense, an empty string, a zero number,
// false, a nil pointer or interface, and an empty slice, array or map. The
// decoder ignores both options; they shape emission only.
func fieldOmitted(f reflect.StructField, v reflect.Value) bool {
tag, ok := f.Tag.Lookup("toml")
if !ok {
return false
}
_, opts, _ := strings.Cut(tag, ",")
for opts != "" {
var opt string
opt, opts, _ = strings.Cut(opts, ",")
switch opt {
case "omitzero":
if isZeroValue(v) {
if tagHasOption(tag, "omitzero") && isZeroValue(v) {
return true
}
if tagHasOption(tag, "omitempty") {
switch v.Kind() {
case reflect.Slice, reflect.Array, reflect.Map:
if v.Len() == 0 {
return true
}
case "omitempty":
switch v.Kind() {
case reflect.Slice, reflect.Array, reflect.Map:
if v.Len() == 0 {
return true
}
case reflect.String:
if v.Len() == 0 {
return true
}
case reflect.Bool:
if !v.Bool() {
return true
}
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
if v.Int() == 0 {
return true
}
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
if v.Uint() == 0 {
return true
}
case reflect.Float32, reflect.Float64:
if v.Float() == 0 {
return true
}
case reflect.Pointer, reflect.Interface:
if v.IsNil() {
return true
}
}
}
@@ -569,7 +614,7 @@ func buildMapDoc(v reflect.Value, doc *tomlDoc, path encPath) error {
return err
}
}
if err := addField(doc, k.String(), v.MapIndex(k), path); err != nil {
if err := addField(doc, k.String(), v.MapIndex(k), path, false); err != nil {
return err
}
}
@@ -584,7 +629,11 @@ func buildMapDoc(v reflect.Value, doc *tomlDoc, path encPath) error {
// contract violation.
var errNilMarshalTOML = errors.New("MarshalTOML returned a nil value")
func addField(doc *tomlDoc, name string, v reflect.Value, path encPath) error {
// addField adds one value under name, the shape addField picks deciding
// whether it is a scalar line, a sub-table or an array. forceInline marks a
// table-valued field carrying the `,inline` tag option: it emits as
// `key = {…}` instead of a header section.
func addField(doc *tomlDoc, name string, v reflect.Value, path encPath, forceInline bool) error {
if m, ok := marshalerOf(v); ok {
mv, err := m.MarshalTOML()
if err != nil {
@@ -624,7 +673,7 @@ func addField(doc *tomlDoc, name string, v reflect.Value, path encPath) error {
if err := buildOrderedDoc(&om, sub, path.key(name)); err != nil {
return err
}
doc.addTable(name, sub)
doc.addTable(name, sub).inline = forceInline
return nil
}
switch v.Kind() {
@@ -633,11 +682,11 @@ func addField(doc *tomlDoc, name string, v reflect.Value, path encPath) error {
doc.addScalar(name, v.Interface())
return nil
}
return addSubTable(doc, name, v, path)
return addSubTable(doc, name, v, path, forceInline)
case reflect.Map:
return addSubTable(doc, name, v, path)
return addSubTable(doc, name, v, path, forceInline)
case reflect.Slice, reflect.Array:
return addArrayValue(doc, name, v, path)
return addArrayValue(doc, name, v, path, forceInline)
default:
val, err := normaliseValue(v)
if err != nil {
@@ -648,7 +697,7 @@ func addField(doc *tomlDoc, name string, v reflect.Value, path encPath) error {
}
}
func addSubTable(doc *tomlDoc, name string, v reflect.Value, path encPath) error {
func addSubTable(doc *tomlDoc, name string, v reflect.Value, path encPath, forceInline bool) error {
sub := &tomlDoc{ctx: doc.ctx, opts: doc.opts, depth: doc.depth + 1}
if atDepthLimit(sub.depth) {
return &EncodeError{Path: path.key(name).String(), Err: errDepthLimit()}
@@ -663,11 +712,11 @@ func addSubTable(doc *tomlDoc, name string, v reflect.Value, path encPath) error
return err
}
}
doc.addTable(name, sub)
doc.addTable(name, sub).inline = forceInline
return nil
}
func addArrayValue(doc *tomlDoc, name string, v reflect.Value, path encPath) error {
func addArrayValue(doc *tomlDoc, name string, v reflect.Value, path encPath, forceInline bool) error {
if v.Kind() == reflect.Slice && v.IsNil() {
// A nil slice has no explicit representation in TOML, so it is skipped.
return nil
@@ -723,6 +772,11 @@ func addArrayValue(doc *tomlDoc, name string, v reflect.Value, path encPath) err
if v.Type().Elem().Kind() == reflect.Interface {
allTables = false
}
// A `,inline` tag on an array of tables asks for a form that would change
// the value's Go type on re-parse, so the error is the honest answer.
if forceInline && allTables {
return &EncodeError{Path: path.key(name).String(), Err: errors.New("an array of tables has no inline form")}
}
if allTables {
subs := make([]*tomlDoc, n)
for i, ev := range elems {
@@ -1104,7 +1158,7 @@ func (e *encoder) emitDoc(doc *tomlDoc, prefix []string) error {
if t.kind != entryTable {
continue
}
inlined, err := e.writeInlineSubTableIfSmall(t.key, t.doc)
inlined, err := e.writeInlineSubTableIfSmall(t)
if err != nil {
return err
}
@@ -1159,7 +1213,7 @@ func (e *encoder) emitDoc(doc *tomlDoc, prefix []string) error {
return err
}
case entryTable:
inlined, err := e.writeInlineSubTableIfSmall(ent.key, ent.doc)
inlined, err := e.writeInlineSubTableIfSmall(&ent)
if err != nil {
return err
}
@@ -1516,29 +1570,34 @@ func (e *encoder) writeInlineDocMultiline(doc *tomlDoc) error {
// writeInlineSubTableIfSmall writes "key = {…}" for a sub-table whose
// single-line rendering fits the compact threshold, and reports whether it did
// so. An array of tables is never inlined, because its inline form would
// re-parse as a value array and change the value's Go type.
func (e *encoder) writeInlineSubTableIfSmall(name string, doc *tomlDoc) (bool, error) {
if e.opts.inlineTablesAt <= 0 || !inlinableDoc(doc) {
// so. An entry the `,inline` tag option marks is written regardless of the
// threshold. An array of tables is never inlined, because its inline form
// would re-parse as a value array and change the value's Go type; a forced
// inline of one is an error rather than a silent form change.
func (e *encoder) writeInlineSubTableIfSmall(t *entry) (bool, error) {
if !t.inline && (e.opts.inlineTablesAt <= 0 || !inlinableDoc(t.doc)) {
return false, nil
}
if !inlinableDoc(t.doc) {
return false, fmt.Errorf("interpres: field %q holds an array of tables and has no inline form", t.key)
}
flat := e.flat()
if err := flat.writeInlineDoc(doc); err != nil {
if err := flat.writeInlineDoc(t.doc); err != nil {
flat.release()
return false, err
}
if flat.buf.Len() > e.opts.inlineTablesAt {
if !t.inline && flat.buf.Len() > e.opts.inlineTablesAt {
flat.release()
return false, nil
}
if err := e.writeKey(name); err != nil {
if err := e.writeKey(t.key); err != nil {
flat.release()
return false, err
}
e.buf.WriteString(" = ")
if e.column()+flat.buf.Len() <= e.limit {
e.buf.Write(flat.buf.Bytes())
} else if err := e.writeInlineDocMultiline(doc); err != nil {
} else if err := e.writeInlineDocMultiline(t.doc); err != nil {
flat.release()
return false, err
}