diff --git a/CHANGELOG.md b/CHANGELOG.md index 5474309..c24fb44 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -125,6 +125,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed +- `Encoder.GroupByKind(bool)` is renamed to `Encoder.Layout(kind)` and takes + a `LayoutKind`: `LayoutKindGrouped`, the default, or + `LayoutKindDeclaration` for the declaration order. `UseLiteralMultiline` + is renamed to `LiteralMultiline`. The behaviour is unchanged; 2.0 is the + only chance a rename has, and the migrator updates the calls mechanically. - `DecodeError` and `EncodeError` carry one `Path` type, a list of segments (`"items"`, `"[0]"`, `"weight"`) with a `String()` rendering the TOML notation, `items[0].weight`. The decode error used to hold a bare diff --git a/README.md b/README.md index d4a2818..1e091b6 100644 --- a/README.md +++ b/README.md @@ -131,9 +131,9 @@ The value `MarshalTOML` returns is encoded in place of the receiver; ```go out, err := interpres.NewEncoder(). - GroupByKind(false). // preserve declaration order + Layout(interpres.LayoutKindDeclaration), // preserve declaration order OmitEmptyArrays(). // skip empty scalar arrays - UseLiteralMultiline(80). // long multi-line strings as literal blocks + LiteralMultiline(80), // long multi-line strings as literal blocks Marshal(cfg) ``` diff --git a/docs/API.md b/docs/API.md index 52e43c2..a73b2c8 100644 --- a/docs/API.md +++ b/docs/API.md @@ -555,11 +555,11 @@ parsed as keys of the sub-table. ### Preserving declaration order -`GroupByKind(false)` on an `Encoder` walks the entries in declaration order +`LayoutKindDeclaration` on an `Encoder` walks the entries in declaration order instead, emitting each header immediately before its content: ```go -out, err := interpres.NewEncoder().GroupByKind(false).Marshal(cfg) +out, err := interpres.NewEncoder().Layout(interpres.LayoutKindDeclaration).Marshal(cfg) ``` The output remains parseable, but a scalar declared after a sub-table lands @@ -650,12 +650,12 @@ omitted, because TOML forbids an empty `[[a]]`. Other empty arrays emit as ### Long strings By default every string is emitted as a basic `"..."` string with the escapes -TOML requires, a newline among them as `\n`. `UseLiteralMultiline(threshold)` +TOML requires, a newline among them as `\n`. `LiteralMultiline(threshold)` switches strings that contain a newline and are at least `threshold` bytes long to the literal `'''...'''` form, which carries the newlines verbatim: ```go -out, err := interpres.NewEncoder().UseLiteralMultiline(80).Marshal(cfg) +out, err := interpres.NewEncoder().LiteralMultiline(80).Marshal(cfg) ``` Single-line strings keep the basic form regardless of the threshold, and a @@ -826,17 +826,17 @@ encoder: | Method | Default | Effect | |---|---|---| -| `GroupByKind(v bool)` | `true` | group entries as scalars, then sub-tables, then arrays of tables; `false` preserves declaration order | +| `Layout(kind LayoutKind)` | `LayoutKindGrouped` | group entries as scalars, then sub-tables, then arrays of tables; `LayoutKindDeclaration` preserves declaration order | | `OmitEmptyArrays()` | off | skip `key = []` for empty scalar arrays | -| `UseLiteralMultiline(threshold int)` | `0` | emit multi-line strings of at least `threshold` bytes as literal `'''...'''` | +| `LiteralMultiline(threshold int)` | `0` | emit multi-line strings of at least `threshold` bytes as literal `'''...'''` | | `InlineTables(threshold int)` | `0` | write a sub-table inline when its single-line form is at most `threshold` bytes | | `EmitFieldComments()` | off | print the `comment=` tag option of a field above its line or header | ```go out, err := interpres.NewEncoder(). - GroupByKind(false). + Layout(interpres.LayoutKindDeclaration). OmitEmptyArrays(). - UseLiteralMultiline(80). + LiteralMultiline(80). InlineTables(60). MarshalContext(ctx, cfg) ``` diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index 92b3dcd..035f6eb 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -77,7 +77,7 @@ sequenceDiagram ``` Encoding walks the other way. `encode.go` first builds a `tomlDoc` from the -value, then emits it; the two phases are why `GroupByKind` can reorder entries +value, then emits it; the two phases are why `Layout` can reorder entries without a second reflection pass, and why cancellation is checked during both. ```mermaid diff --git a/encode.go b/encode.go index 3aaf302..5518b64 100644 --- a/encode.go +++ b/encode.go @@ -295,7 +295,7 @@ const ( // entry is one binding in a tomlDoc. entries live in a single slice in the // order they were added; emission walks that order directly, either as it is -// (Encoder with GroupByKind(false)) or in kind-grouped passes over the same +// (Encoder with LayoutKindDeclaration) or in kind-grouped passes over the same // slice (the default). type entry struct { kind entryKind @@ -1157,7 +1157,7 @@ func (e *encoder) writeBlankLine() { } func (e *encoder) emitDoc(doc *tomlDoc, prefix []string) error { - if e.opts.groupByKind { + if e.opts.layout == LayoutKindGrouped { // Scalars first, then inline sub-tables as value lines, then the // remaining tables as headers, then arrays of tables. Each pass walks // the entries in place; grouping copies of them cost the encoder a diff --git a/encode_test.go b/encode_test.go index 52664b2..06250cf 100644 --- a/encode_test.go +++ b/encode_test.go @@ -96,8 +96,8 @@ func TestMarshalContextHonoursCancellation(t *testing.T) { } } -func TestEncoderGroupByKindDefault(t *testing.T) { - // NewEncoder must default to GroupByKind=true so legacy callers keep the +func TestEncoderLayoutGroupedDefault(t *testing.T) { + // NewEncoder must default to LayoutKindGrouped so legacy callers keep the // scalars-first ordering. type Cfg struct { Name string `toml:"name"` @@ -117,7 +117,7 @@ func TestEncoderGroupByKindDefault(t *testing.T) { } } -func TestEncoderGroupByKindFalsePreservesOrder(t *testing.T) { +func TestEncoderLayoutDeclarationPreservesOrder(t *testing.T) { type Inner struct { Host string `toml:"host"` } @@ -131,11 +131,11 @@ func TestEncoderGroupByKindFalsePreservesOrder(t *testing.T) { Server: Inner{Host: "h"}, Debug: true, } - out, err := NewEncoder().GroupByKind(false).Marshal(in) + out, err := NewEncoder().Layout(LayoutKindDeclaration).Marshal(in) if err != nil { t.Fatalf("marshal: %v", err) } - // With GroupByKind(false) the encoder walks entries in declaration order. + // With Layout(LayoutKindDeclaration) the encoder walks entries in declaration order. // The output is still parseable, but a scalar that follows a header is // parsed as a sub-table key. That is the user's trade-off; see // docs/API.md. @@ -145,8 +145,8 @@ func TestEncoderGroupByKindFalsePreservesOrder(t *testing.T) { } } -func TestEncoderGroupByKindTrueDefaultOrder(t *testing.T) { - // The default (GroupByKind=true) must lift the trailing scalar ahead of +func TestEncoderLayoutGroupedDefaultOrder(t *testing.T) { + // The default (LayoutKindGrouped) must lift the trailing scalar ahead of // the [server] block so the document round-trips losslessly. type Inner struct { Host string `toml:"host"` @@ -224,12 +224,12 @@ func TestEncoderOmitEmptyArrayOfTablesStillSkipped(t *testing.T) { } } -func TestEncoderUseLiteralMultiline(t *testing.T) { +func TestEncoderLiteralMultiline(t *testing.T) { type Cfg struct { Long string `toml:"long"` } long := strings.Repeat("a", 50) + "\nline two\nline three" - out, err := NewEncoder().UseLiteralMultiline(20).Marshal(Cfg{Long: long}) + out, err := NewEncoder().LiteralMultiline(20).Marshal(Cfg{Long: long}) if err != nil { t.Fatalf("marshal: %v", err) } @@ -239,12 +239,12 @@ func TestEncoderUseLiteralMultiline(t *testing.T) { } } -func TestEncoderUseLiteralMultilineBelowThreshold(t *testing.T) { +func TestEncoderLiteralMultilineBelowThreshold(t *testing.T) { // A multi-line value shorter than the threshold must remain escaped. type Cfg struct { Short string `toml:"short"` } - out, err := NewEncoder().UseLiteralMultiline(1000).Marshal(Cfg{Short: "one\ntwo"}) + out, err := NewEncoder().LiteralMultiline(1000).Marshal(Cfg{Short: "one\ntwo"}) if err != nil { t.Fatalf("marshal: %v", err) } @@ -254,12 +254,12 @@ func TestEncoderUseLiteralMultilineBelowThreshold(t *testing.T) { } } -func TestEncoderUseLiteralMultilineThresholdZero(t *testing.T) { - // UseLiteralMultiline(0) disables the literal form entirely. +func TestEncoderLiteralMultilineThresholdZero(t *testing.T) { + // LiteralMultiline(0) disables the literal form entirely. type Cfg struct { S string `toml:"s"` } - out, err := NewEncoder().UseLiteralMultiline(0).Marshal(Cfg{S: "a\nb\nc\nd"}) + out, err := NewEncoder().LiteralMultiline(0).Marshal(Cfg{S: "a\nb\nc\nd"}) if err != nil { t.Fatalf("marshal: %v", err) } @@ -282,7 +282,7 @@ func TestEncoderLiteralMultilineFallsBackWhenUnsafe(t *testing.T) { {"lone carriage return", "first\rsecond\nthird"}, } for _, c := range cases { - out, err := NewEncoder().UseLiteralMultiline(5).Marshal(map[string]any{"s": c.in}) + out, err := NewEncoder().LiteralMultiline(5).Marshal(map[string]any{"s": c.in}) if err != nil { t.Fatalf("%s: marshal: %v", c.name, err) } @@ -483,9 +483,9 @@ func TestEncoderChainedOptions(t *testing.T) { } long := strings.Repeat("x", 200) out, err := NewEncoder(). - GroupByKind(false). + Layout(LayoutKindDeclaration). OmitEmptyArrays(). - UseLiteralMultiline(50). + LiteralMultiline(50). Marshal(Cfg{S: "short", I: Inner{V: long}}) if err != nil { t.Fatalf("marshal: %v", err) diff --git a/examples/basic/main.go b/examples/basic/main.go index 53a6389..5c457fb 100644 --- a/examples/basic/main.go +++ b/examples/basic/main.go @@ -130,12 +130,12 @@ func Run(stdout, stderr io.Writer) int { } fmt.Fprintf(stdout, "\n--- marshal (group by kind, default) ---\n%s", out) - out2, err := interpres.NewEncoder().GroupByKind(false).Marshal(cfg) + out2, err := interpres.NewEncoder().Layout(interpres.LayoutKindDeclaration).Marshal(cfg) if err != nil { fmt.Fprintln(stderr, "marshal:", err) return 1 } - fmt.Fprintf(stdout, "\n--- marshal (GroupByKind=false) ---\n%s", out2) + fmt.Fprintf(stdout, "\n--- marshal (LayoutKindDeclaration) ---\n%s", out2) // Demonstrate Unmarshaler-style mutation: re-decode the second output to // prove it round-trips back into the same Go value. diff --git a/examples/basic/main_test.go b/examples/basic/main_test.go index 3519cca..2fcee15 100644 --- a/examples/basic/main_test.go +++ b/examples/basic/main_test.go @@ -25,7 +25,7 @@ func TestRunPrintsConfigAndMarshal(t *testing.T) { "admin=false", "--- marshal (group by kind, default) ---", `title = "interpres demo"`, - "--- marshal (GroupByKind=false) ---", + "--- marshal (LayoutKindDeclaration) ---", "[server]", "port = 9090", "[[users]]", diff --git a/interpres.go b/interpres.go index 8933fce..09b9f8e 100644 --- a/interpres.go +++ b/interpres.go @@ -545,12 +545,25 @@ func MarshalContext(ctx context.Context, v any) ([]byte, error) { return NewEncoder().MarshalContext(ctx, v) } +// A LayoutKind names the layout the encoder writes a document's entries in. +type LayoutKind int + +const ( + // LayoutKindGrouped reorders entries at one level: scalars first, then + // sub-tables, then arrays of tables. The default. + LayoutKindGrouped LayoutKind = iota + // LayoutKindDeclaration preserves the declaration order: struct field + // order, or sorted key order for maps. + LayoutKindDeclaration +) + // An Encoder encodes Go values into TOML. // // All options default to the behaviour that passes the toml-test compliance // suite in both directions: // -// GroupByKind: true (scalars first, then tables, then arrays of tables) +// Layout: LayoutKindGrouped (scalars first, then tables, then +// arrays of tables) // OmitEmptyArrays: false (a nil/empty []string slice emits [] as a value; // a nil/empty []Item struct slice is still skipped) // LiteralMultilineAt: 0 (always emit the escaped basic form, never a @@ -562,22 +575,21 @@ func MarshalContext(ctx context.Context, v any) ([]byte, error) { // callers that need the underlying knobs reach for the methods rather than // reading or mutating fields. type Encoder struct { - groupByKind bool // default true; set via (*Encoder).GroupByKind - omitEmptyArrays bool // default false; set via (*Encoder).OmitEmptyArrays - literalMultilineAt int // default 0; set via (*Encoder).UseLiteralMultiline - inlineTablesAt int // default 0; set via (*Encoder).InlineTables - emitFieldComments bool // default false; set via (*Encoder).EmitFieldComments + layout LayoutKind // default LayoutKindGrouped; set via (*Encoder).Layout + omitEmptyArrays bool // default false; set via (*Encoder).OmitEmptyArrays + literalMultilineAt int // default 0; set via (*Encoder).LiteralMultiline + inlineTablesAt int // default 0; set via (*Encoder).InlineTables + emitFieldComments bool // default false; set via (*Encoder).EmitFieldComments } // NewEncoder returns an Encoder with default options. -func NewEncoder() *Encoder { return &Encoder{groupByKind: true} } +func NewEncoder() *Encoder { return &Encoder{layout: LayoutKindGrouped} } -// GroupByKind toggles whether fields at the same TOML level are reordered -// into the group-by-kind layout (scalars first, then tables, then arrays of -// tables). When set to false, the emitter preserves the source declaration -// order (struct field order, or sorted key order for maps). -func (e *Encoder) GroupByKind(v bool) *Encoder { - e.groupByKind = v +// Layout sets the layout the encoder writes a document's entries in: +// LayoutKindGrouped, the default, reorders them scalars first, then tables, +// then arrays of tables; LayoutKindDeclaration preserves declaration order. +func (e *Encoder) Layout(kind LayoutKind) *Encoder { + e.layout = kind return e } @@ -589,12 +601,12 @@ func (e *Encoder) OmitEmptyArrays() *Encoder { return e } -// UseLiteralMultiline sets the length threshold at which a multi-line string +// LiteralMultiline sets the length threshold at which a multi-line string // is emitted as a literal triple-quoted string instead of the escaped form. // Use 0 or any negative value to disable (always escaped). The literal form // is selected only when the value contains an internal newline; otherwise the // single-line basic form is used regardless of this setting. -func (e *Encoder) UseLiteralMultiline(threshold int) *Encoder { +func (e *Encoder) LiteralMultiline(threshold int) *Encoder { e.literalMultilineAt = threshold return e } @@ -609,7 +621,7 @@ func (e *Encoder) UseLiteralMultiline(threshold int) *Encoder { // value array. An inlined table that does not fit the line is written across // lines, which TOML 1.1 allows. // -// With GroupByKind(false) the layout is already for presentation only, and an +// With LayoutKindDeclaration the layout is already for presentation only, and an // inlined table follows the same rule as any other value line: it lands in the // section of the header that precedes it. func (e *Encoder) InlineTables(threshold int) *Encoder {