refactor: rename the encoder layout options
Test / test (push) Successful in 1m35s

Assisted-by: GLM 5.3 Flash
This commit is contained in:
2026-09-22 01:09:00 +02:00
parent bef1d3fbd9
commit d92bb56853
9 changed files with 66 additions and 49 deletions
+5
View File
@@ -125,6 +125,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Changed ### 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 - `DecodeError` and `EncodeError` carry one `Path` type, a list of segments
(`"items"`, `"[0]"`, `"weight"`) with a `String()` rendering the TOML (`"items"`, `"[0]"`, `"weight"`) with a `String()` rendering the TOML
notation, `items[0].weight`. The decode error used to hold a bare notation, `items[0].weight`. The decode error used to hold a bare
+2 -2
View File
@@ -131,9 +131,9 @@ The value `MarshalTOML` returns is encoded in place of the receiver;
```go ```go
out, err := interpres.NewEncoder(). out, err := interpres.NewEncoder().
GroupByKind(false). // preserve declaration order Layout(interpres.LayoutKindDeclaration), // preserve declaration order
OmitEmptyArrays(). // skip empty scalar arrays 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) Marshal(cfg)
``` ```
+8 -8
View File
@@ -555,11 +555,11 @@ parsed as keys of the sub-table.
### Preserving declaration order ### 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: instead, emitting each header immediately before its content:
```go ```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 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 ### Long strings
By default every string is emitted as a basic `"..."` string with the escapes 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 switches strings that contain a newline and are at least `threshold` bytes long
to the literal `'''...'''` form, which carries the newlines verbatim: to the literal `'''...'''` form, which carries the newlines verbatim:
```go ```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 Single-line strings keep the basic form regardless of the threshold, and a
@@ -826,17 +826,17 @@ encoder:
| Method | Default | Effect | | 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 | | `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 | | `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 | | `EmitFieldComments()` | off | print the `comment=` tag option of a field above its line or header |
```go ```go
out, err := interpres.NewEncoder(). out, err := interpres.NewEncoder().
GroupByKind(false). Layout(interpres.LayoutKindDeclaration).
OmitEmptyArrays(). OmitEmptyArrays().
UseLiteralMultiline(80). LiteralMultiline(80).
InlineTables(60). InlineTables(60).
MarshalContext(ctx, cfg) MarshalContext(ctx, cfg)
``` ```
+1 -1
View File
@@ -77,7 +77,7 @@ sequenceDiagram
``` ```
Encoding walks the other way. `encode.go` first builds a `tomlDoc` from the 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. without a second reflection pass, and why cancellation is checked during both.
```mermaid ```mermaid
+2 -2
View File
@@ -295,7 +295,7 @@ const (
// entry is one binding in a tomlDoc. entries live in a single slice in the // 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 // 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). // slice (the default).
type entry struct { type entry struct {
kind entryKind kind entryKind
@@ -1157,7 +1157,7 @@ func (e *encoder) writeBlankLine() {
} }
func (e *encoder) emitDoc(doc *tomlDoc, prefix []string) error { 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 // Scalars first, then inline sub-tables as value lines, then the
// remaining tables as headers, then arrays of tables. Each pass walks // remaining tables as headers, then arrays of tables. Each pass walks
// the entries in place; grouping copies of them cost the encoder a // the entries in place; grouping copies of them cost the encoder a
+17 -17
View File
@@ -96,8 +96,8 @@ func TestMarshalContextHonoursCancellation(t *testing.T) {
} }
} }
func TestEncoderGroupByKindDefault(t *testing.T) { func TestEncoderLayoutGroupedDefault(t *testing.T) {
// NewEncoder must default to GroupByKind=true so legacy callers keep the // NewEncoder must default to LayoutKindGrouped so legacy callers keep the
// scalars-first ordering. // scalars-first ordering.
type Cfg struct { type Cfg struct {
Name string `toml:"name"` 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 { type Inner struct {
Host string `toml:"host"` Host string `toml:"host"`
} }
@@ -131,11 +131,11 @@ func TestEncoderGroupByKindFalsePreservesOrder(t *testing.T) {
Server: Inner{Host: "h"}, Server: Inner{Host: "h"},
Debug: true, Debug: true,
} }
out, err := NewEncoder().GroupByKind(false).Marshal(in) out, err := NewEncoder().Layout(LayoutKindDeclaration).Marshal(in)
if err != nil { if err != nil {
t.Fatalf("marshal: %v", err) 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 // 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 // parsed as a sub-table key. That is the user's trade-off; see
// docs/API.md. // docs/API.md.
@@ -145,8 +145,8 @@ func TestEncoderGroupByKindFalsePreservesOrder(t *testing.T) {
} }
} }
func TestEncoderGroupByKindTrueDefaultOrder(t *testing.T) { func TestEncoderLayoutGroupedDefaultOrder(t *testing.T) {
// The default (GroupByKind=true) must lift the trailing scalar ahead of // The default (LayoutKindGrouped) must lift the trailing scalar ahead of
// the [server] block so the document round-trips losslessly. // the [server] block so the document round-trips losslessly.
type Inner struct { type Inner struct {
Host string `toml:"host"` 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 { type Cfg struct {
Long string `toml:"long"` Long string `toml:"long"`
} }
long := strings.Repeat("a", 50) + "\nline two\nline three" 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 { if err != nil {
t.Fatalf("marshal: %v", err) 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. // A multi-line value shorter than the threshold must remain escaped.
type Cfg struct { type Cfg struct {
Short string `toml:"short"` 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 { if err != nil {
t.Fatalf("marshal: %v", err) t.Fatalf("marshal: %v", err)
} }
@@ -254,12 +254,12 @@ func TestEncoderUseLiteralMultilineBelowThreshold(t *testing.T) {
} }
} }
func TestEncoderUseLiteralMultilineThresholdZero(t *testing.T) { func TestEncoderLiteralMultilineThresholdZero(t *testing.T) {
// UseLiteralMultiline(0) disables the literal form entirely. // LiteralMultiline(0) disables the literal form entirely.
type Cfg struct { type Cfg struct {
S string `toml:"s"` 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 { if err != nil {
t.Fatalf("marshal: %v", err) t.Fatalf("marshal: %v", err)
} }
@@ -282,7 +282,7 @@ func TestEncoderLiteralMultilineFallsBackWhenUnsafe(t *testing.T) {
{"lone carriage return", "first\rsecond\nthird"}, {"lone carriage return", "first\rsecond\nthird"},
} }
for _, c := range cases { 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 { if err != nil {
t.Fatalf("%s: marshal: %v", c.name, err) t.Fatalf("%s: marshal: %v", c.name, err)
} }
@@ -483,9 +483,9 @@ func TestEncoderChainedOptions(t *testing.T) {
} }
long := strings.Repeat("x", 200) long := strings.Repeat("x", 200)
out, err := NewEncoder(). out, err := NewEncoder().
GroupByKind(false). Layout(LayoutKindDeclaration).
OmitEmptyArrays(). OmitEmptyArrays().
UseLiteralMultiline(50). LiteralMultiline(50).
Marshal(Cfg{S: "short", I: Inner{V: long}}) Marshal(Cfg{S: "short", I: Inner{V: long}})
if err != nil { if err != nil {
t.Fatalf("marshal: %v", err) t.Fatalf("marshal: %v", err)
+2 -2
View File
@@ -130,12 +130,12 @@ func Run(stdout, stderr io.Writer) int {
} }
fmt.Fprintf(stdout, "\n--- marshal (group by kind, default) ---\n%s", out) 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 { if err != nil {
fmt.Fprintln(stderr, "marshal:", err) fmt.Fprintln(stderr, "marshal:", err)
return 1 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 // Demonstrate Unmarshaler-style mutation: re-decode the second output to
// prove it round-trips back into the same Go value. // prove it round-trips back into the same Go value.
+1 -1
View File
@@ -25,7 +25,7 @@ func TestRunPrintsConfigAndMarshal(t *testing.T) {
"admin=false", "admin=false",
"--- marshal (group by kind, default) ---", "--- marshal (group by kind, default) ---",
`title = "interpres demo"`, `title = "interpres demo"`,
"--- marshal (GroupByKind=false) ---", "--- marshal (LayoutKindDeclaration) ---",
"[server]", "[server]",
"port = 9090", "port = 9090",
"[[users]]", "[[users]]",
+25 -13
View File
@@ -545,12 +545,25 @@ func MarshalContext(ctx context.Context, v any) ([]byte, error) {
return NewEncoder().MarshalContext(ctx, v) 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. // An Encoder encodes Go values into TOML.
// //
// All options default to the behaviour that passes the toml-test compliance // All options default to the behaviour that passes the toml-test compliance
// suite in both directions: // 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; // OmitEmptyArrays: false (a nil/empty []string slice emits [] as a value;
// a nil/empty []Item struct slice is still skipped) // a nil/empty []Item struct slice is still skipped)
// LiteralMultilineAt: 0 (always emit the escaped basic form, never a // 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 // callers that need the underlying knobs reach for the methods rather than
// reading or mutating fields. // reading or mutating fields.
type Encoder struct { type Encoder struct {
groupByKind bool // default true; set via (*Encoder).GroupByKind layout LayoutKind // default LayoutKindGrouped; set via (*Encoder).Layout
omitEmptyArrays bool // default false; set via (*Encoder).OmitEmptyArrays omitEmptyArrays bool // default false; set via (*Encoder).OmitEmptyArrays
literalMultilineAt int // default 0; set via (*Encoder).UseLiteralMultiline literalMultilineAt int // default 0; set via (*Encoder).LiteralMultiline
inlineTablesAt int // default 0; set via (*Encoder).InlineTables inlineTablesAt int // default 0; set via (*Encoder).InlineTables
emitFieldComments bool // default false; set via (*Encoder).EmitFieldComments emitFieldComments bool // default false; set via (*Encoder).EmitFieldComments
} }
// NewEncoder returns an Encoder with default options. // 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 // Layout sets the layout the encoder writes a document's entries in:
// into the group-by-kind layout (scalars first, then tables, then arrays of // LayoutKindGrouped, the default, reorders them scalars first, then tables,
// tables). When set to false, the emitter preserves the source declaration // then arrays of tables; LayoutKindDeclaration preserves declaration order.
// order (struct field order, or sorted key order for maps). func (e *Encoder) Layout(kind LayoutKind) *Encoder {
func (e *Encoder) GroupByKind(v bool) *Encoder { e.layout = kind
e.groupByKind = v
return e return e
} }
@@ -589,12 +601,12 @@ func (e *Encoder) OmitEmptyArrays() *Encoder {
return e 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. // 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 // 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 // is selected only when the value contains an internal newline; otherwise the
// single-line basic form is used regardless of this setting. // 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 e.literalMultilineAt = threshold
return e 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 // value array. An inlined table that does not fit the line is written across
// lines, which TOML 1.1 allows. // 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 // inlined table follows the same rule as any other value line: it lands in the
// section of the header that precedes it. // section of the header that precedes it.
func (e *Encoder) InlineTables(threshold int) *Encoder { func (e *Encoder) InlineTables(threshold int) *Encoder {