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
- `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
+2 -2
View File
@@ -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)
```
+8 -8
View File
@@ -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)
```
+1 -1
View File
@@ -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
+2 -2
View File
@@ -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
+17 -17
View File
@@ -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)
+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)
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.
+1 -1
View File
@@ -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]]",
+25 -13
View File
@@ -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
layout LayoutKind // default LayoutKindGrouped; set via (*Encoder).Layout
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
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 {