Assisted-by: GLM 5.3 Flash
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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
@@ -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)
|
||||
```
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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)
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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]]",
|
||||
|
||||
+28
-16
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user