Assisted-by: GLM 5.3 Flash
This commit is contained in:
@@ -94,6 +94,15 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
|
||||
### Changed
|
||||
|
||||
- `omitempty` follows the encoding/json semantics: the field is skipped when
|
||||
it holds an empty string, a zero number, `false`, a nil pointer or
|
||||
interface, or a nil or empty slice, array or map. In 1.x the option covered
|
||||
only the collections.
|
||||
- The `toml` tag gained the `inline` option: a struct or map field tagged
|
||||
`toml:"retry,inline"` emits as `retry = {…}` instead of a header section,
|
||||
whatever its size, a named embedded struct included. Forcing it on an array
|
||||
of tables is an error, because the inline form would re-parse as a value
|
||||
array and change the value's Go type.
|
||||
- The output takes the TOML 1.1 form. A date-time writes its seconds only when
|
||||
the value carries them and drops the trailing zeros of a fractional second,
|
||||
so `07:32:00` is written `07:32` and half a second as `00.5`. Both are the
|
||||
|
||||
+13
-5
@@ -475,22 +475,30 @@ nil map emits nothing.
|
||||
|
||||
### Tag options
|
||||
|
||||
The part of a `toml` tag after the first comma carries options. Both options
|
||||
shape emission only; the decoder ignores them.
|
||||
The part of a `toml` tag after the first comma carries options. They shape
|
||||
emission only; the decoder ignores them, so a value that round-trips keeps
|
||||
its key whether the table it came from was written inline or under a header.
|
||||
|
||||
- `omitzero` skips the field when its value is the zero value of its type. A
|
||||
type with an `IsZero() bool` method (time.Time among them) decides through
|
||||
that method, so a zero `time.Time` or an all-zero struct disappears from
|
||||
the output.
|
||||
- `omitempty` skips the field when it holds an empty collection: a nil or
|
||||
empty slice or array, or a nil or empty map. Strings and other scalars are
|
||||
not covered by `omitempty`; use `omitzero` for those.
|
||||
- `omitempty` skips the field when it holds an empty value in the
|
||||
encoding/json sense: an empty string, a zero number, `false`, a nil pointer
|
||||
or interface, and a nil or empty slice, array or map. This is a change of
|
||||
semantics against 1.x, where only collections were covered.
|
||||
- `inline` forces a struct or map field to emit as `name = {…}`, the inline
|
||||
table form, instead of a header section, whatever its size; a named
|
||||
embedded struct tagged this way does the same. A field holding an array of
|
||||
tables is an error under `inline`, because the inline form would re-parse
|
||||
as a value array and change the value's Go type.
|
||||
|
||||
```go
|
||||
type Config struct {
|
||||
Host string `toml:"host,omitzero"`
|
||||
Started time.Time `toml:"started,omitzero"`
|
||||
Tags []string `toml:"tags,omitempty"`
|
||||
Retry Retry `toml:"retry,inline"`
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
case "omitempty":
|
||||
if tagHasOption(tag, "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
|
||||
}
|
||||
|
||||
+108
@@ -2126,3 +2126,111 @@ func TestUnmarshalWithOptions(t *testing.T) {
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestInlineTag(t *testing.T) {
|
||||
type Inner struct {
|
||||
A int `toml:"a"`
|
||||
B int `toml:"b"`
|
||||
}
|
||||
t.Run("a struct field writes inline", func(t *testing.T) {
|
||||
type Cfg struct {
|
||||
Inner Inner `toml:"inner,inline"`
|
||||
}
|
||||
out, err := Marshal(Cfg{Inner: Inner{1, 2}})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if string(out) != "inner = {a = 1, b = 2}\n" {
|
||||
t.Errorf("output %q", out)
|
||||
}
|
||||
})
|
||||
t.Run("a map field writes inline", func(t *testing.T) {
|
||||
type Cfg struct {
|
||||
Opts map[string]int `toml:"opts,inline"`
|
||||
}
|
||||
out, err := Marshal(Cfg{Opts: map[string]int{"x": 1}})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if string(out) != "opts = {x = 1}\n" {
|
||||
t.Errorf("output %q", out)
|
||||
}
|
||||
})
|
||||
t.Run("a named embedded struct writes inline", func(t *testing.T) {
|
||||
type Cfg struct {
|
||||
Inner `toml:"inner,inline"`
|
||||
}
|
||||
out, err := Marshal(Cfg{Inner: Inner{1, 2}})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if string(out) != "inner = {a = 1, b = 2}\n" {
|
||||
t.Errorf("output %q", out)
|
||||
}
|
||||
})
|
||||
t.Run("an inline field decodes back", func(t *testing.T) {
|
||||
type Cfg struct {
|
||||
Inner Inner `toml:"inner,inline"`
|
||||
}
|
||||
var cfg Cfg
|
||||
if err := Unmarshal([]byte("inner = {a = 3, b = 4}\n"), &cfg); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if cfg.Inner != (Inner{3, 4}) {
|
||||
t.Errorf("decoded %+v", cfg.Inner)
|
||||
}
|
||||
})
|
||||
t.Run("a forced inline of an array of tables is an error", func(t *testing.T) {
|
||||
type Item struct {
|
||||
N int `toml:"n"`
|
||||
}
|
||||
type Cfg struct {
|
||||
Items []Item `toml:"items,inline"`
|
||||
}
|
||||
if _, err := Marshal(Cfg{Items: []Item{{1}}}); err == nil {
|
||||
t.Error("forced inline of an array of tables succeeded, want an error")
|
||||
}
|
||||
})
|
||||
t.Run("without the tag the header form stands", func(t *testing.T) {
|
||||
type Cfg struct {
|
||||
Inner Inner `toml:"inner"`
|
||||
}
|
||||
out, err := Marshal(Cfg{Inner: Inner{1, 2}})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if string(out) != "[inner]\na = 1\nb = 2\n" {
|
||||
t.Errorf("output %q", out)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestOmitEmptyJSONSemantics(t *testing.T) {
|
||||
type Cfg struct {
|
||||
Empty string `toml:"empty,omitempty"`
|
||||
Full string `toml:"full,omitempty"`
|
||||
Zero int `toml:"zero,omitempty"`
|
||||
One int `toml:"one,omitempty"`
|
||||
Off bool `toml:"off,omitempty"`
|
||||
On bool `toml:"on,omitempty"`
|
||||
Nil *string `toml:"nil,omitempty"`
|
||||
Set *string `toml:"set,omitempty"`
|
||||
Nothing map[string]string `toml:"nothing,omitempty"`
|
||||
Somethg map[string]string `toml:"somethg,omitempty"`
|
||||
}
|
||||
s := "x"
|
||||
out, err := Marshal(Cfg{
|
||||
Full: "y",
|
||||
One: 1,
|
||||
On: true,
|
||||
Set: &s,
|
||||
Somethg: map[string]string{"k": "v"},
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
want := "full = \"y\"\none = 1\non = true\nset = \"x\"\n\n[somethg]\nk = \"v\"\n"
|
||||
if string(out) != want {
|
||||
t.Errorf("output:\n%q\nwant:\n%q", out, want)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user