Compare commits
3
Commits
bccaf087c8
...
b695b69768
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b695b69768 | ||
|
|
959eaba4b0 | ||
|
|
8f85bb68fa |
@@ -24,9 +24,22 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||||||
toml-test tagged JSON from stdin and writes the TOML document it describes.
|
toml-test tagged JSON from stdin and writes the TOML document it describes.
|
||||||
The compliance suite now runs the encoder as well as the decoder, 214
|
The compliance suite now runs the encoder as well as the decoder, 214
|
||||||
encoder cases against the tagged JSON of the valid corpus.
|
encoder cases against the tagged JSON of the valid corpus.
|
||||||
|
- `Encoder.InlineTables(threshold)`: a sub-table whose single-line rendering is
|
||||||
|
at most `threshold` bytes is written as an inline table instead of a header
|
||||||
|
section, which shortens a document of small tables. An array of tables keeps
|
||||||
|
its header form, because its inline form would re-parse as a value array.
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
|
|
||||||
|
- 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
|
||||||
|
same value, and a document written without seconds now comes back without
|
||||||
|
them. `LocalDateTime.String()`, `LocalTime.String()` and the offset date-time
|
||||||
|
rendering follow the same rule.
|
||||||
|
- An inline table that would pass the hundredth column is written across lines
|
||||||
|
with a trailing comma and one tab of indentation per nesting level, the shape
|
||||||
|
TOML 1.1 allows an inline table to take.
|
||||||
- TOML 1.1 is the acceptance contract, and TOML 1.0 is not. The compliance
|
- TOML 1.1 is the acceptance contract, and TOML 1.0 is not. The compliance
|
||||||
suite runs the 1.1 corpus alone, and the promise that every 1.0 document
|
suite runs the 1.1 corpus alone, and the promise that every 1.0 document
|
||||||
parses exactly as before is withdrawn. Nothing that parses today stops
|
parses exactly as before is withdrawn. Nothing that parses today stops
|
||||||
|
|||||||
@@ -24,7 +24,8 @@ the entire official [toml-test](https://github.com/toml-lang/toml-test) suite:
|
|||||||
- **Cancellation**: every entry point has a `*Context` sibling that honours a
|
- **Cancellation**: every entry point has a `*Context` sibling that honours a
|
||||||
`context.Context`.
|
`context.Context`.
|
||||||
- **Configurable emission**: `Encoder` options for declaration-order output,
|
- **Configurable emission**: `Encoder` options for declaration-order output,
|
||||||
omitting empty arrays, and literal multiline strings.
|
omitting empty arrays, literal multiline strings, and inlining small
|
||||||
|
sub-tables.
|
||||||
|
|
||||||
## Install
|
## Install
|
||||||
|
|
||||||
|
|||||||
@@ -307,13 +307,13 @@ func TestRunEncoderScalars(t *testing.T) {
|
|||||||
t.Fatalf("Run returned %d, stderr = %q", code, stderr.String())
|
t.Fatalf("Run returned %d, stderr = %q", code, stderr.String())
|
||||||
}
|
}
|
||||||
want := "b = false\n" +
|
want := "b = false\n" +
|
||||||
"dt = 1979-05-27T07:32:00-07:00\n" +
|
"dt = 1979-05-27T07:32-07:00\n" +
|
||||||
"f = inf\n" +
|
"f = inf\n" +
|
||||||
"g = 1.5\n" +
|
"g = 1.5\n" +
|
||||||
"i = -9223372036854775808\n" +
|
"i = -9223372036854775808\n" +
|
||||||
"ld = 1979-05-27\n" +
|
"ld = 1979-05-27\n" +
|
||||||
"ldt = 1979-05-27T07:32:00\n" +
|
"ldt = 1979-05-27T07:32\n" +
|
||||||
"lt = 07:32:00.999000000\n" +
|
"lt = 07:32:00.999\n" +
|
||||||
"nl = \"line1\\nline2\"\n" +
|
"nl = \"line1\\nline2\"\n" +
|
||||||
"s = \"quote \\\" and backslash \\\\\"\n"
|
"s = \"quote \\\" and backslash \\\\\"\n"
|
||||||
if stdout.String() != want {
|
if stdout.String() != want {
|
||||||
|
|||||||
+28
-15
@@ -28,29 +28,42 @@ type LocalDate struct{ time.Time }
|
|||||||
type LocalTime struct{ time.Time }
|
type LocalTime struct{ time.Time }
|
||||||
|
|
||||||
// String returns the TOML-canonical rendering of the local date-time, e.g.
|
// String returns the TOML-canonical rendering of the local date-time, e.g.
|
||||||
// "1979-05-27T07:32:00" or "...:00.000000123" when the time has a fractional
|
// "1979-05-27T07:32" or "1979-05-27T07:32:00.5" when the time carries a
|
||||||
// second. The fractional component is zero-padded to nanosecond precision.
|
// fractional second. TOML 1.1 makes the seconds optional, so they appear only
|
||||||
|
// when they are non-zero, and a fraction drops its trailing zeros.
|
||||||
func (ldt LocalDateTime) String() string {
|
func (ldt LocalDateTime) String() string {
|
||||||
base := ldt.Format("2006-01-02T15:04:05")
|
return ldt.Format("2006-01-02T") + clockString(ldt.Time)
|
||||||
if ns := ldt.Nanosecond(); ns > 0 {
|
|
||||||
return base + "." + fmt.Sprintf("%09d", ns)
|
|
||||||
}
|
|
||||||
return base
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// String returns the TOML-canonical rendering of the local date, e.g.
|
// String returns the TOML-canonical rendering of the local date, e.g.
|
||||||
// "1979-05-27".
|
// "1979-05-27".
|
||||||
func (ld LocalDate) String() string { return ld.Format("2006-01-02") }
|
func (ld LocalDate) String() string { return ld.Format("2006-01-02") }
|
||||||
|
|
||||||
// String returns the TOML-canonical rendering of the local time, e.g.
|
// String returns the TOML-canonical rendering of the local time, e.g. "07:32"
|
||||||
// "07:32:00" or "...:00.000000123" when the time has a fractional second.
|
// or "07:32:00.5" when the time carries a fractional second.
|
||||||
// The fractional component is zero-padded to nanosecond precision.
|
func (lt LocalTime) String() string { return clockString(lt.Time) }
|
||||||
func (lt LocalTime) String() string {
|
|
||||||
base := lt.Format("15:04:05")
|
// clockString renders a time of day the way TOML writes it: the seconds appear
|
||||||
if ns := lt.Nanosecond(); ns > 0 {
|
// only when the value carries them, and a fractional second drops its trailing
|
||||||
return base + "." + fmt.Sprintf("%09d", ns)
|
// zeros, so half a second is "00.5" and not "00.500000000". Both are the same
|
||||||
|
// value either way; the shorter form is the one TOML 1.1 allows.
|
||||||
|
func clockString(t time.Time) string {
|
||||||
|
out := t.Format("15:04")
|
||||||
|
ns := t.Nanosecond()
|
||||||
|
if t.Second() != 0 || ns != 0 {
|
||||||
|
out += t.Format(":05")
|
||||||
}
|
}
|
||||||
return base
|
if ns > 0 {
|
||||||
|
out += "." + strings.TrimRight(fmt.Sprintf("%09d", ns), "0")
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
// offsetString renders an offset date-time, the fourth TOML kind, in the same
|
||||||
|
// shape: no zero seconds, no trailing zeros in the fraction, and the offset
|
||||||
|
// written as "Z" when it is zero.
|
||||||
|
func offsetString(t time.Time) string {
|
||||||
|
return t.Format("2006-01-02T") + clockString(t) + t.Format("Z07:00")
|
||||||
}
|
}
|
||||||
|
|
||||||
var (
|
var (
|
||||||
|
|||||||
+57
-8
@@ -145,11 +145,12 @@ variants decode into `LocalDateTime`, `LocalDate` and `LocalTime`, whose
|
|||||||
embedded `time.Time` is normalised to UTC (midnight UTC for a local date, the
|
embedded `time.Time` is normalised to UTC (midnight UTC for a local date, the
|
||||||
zero date for a local time). Every kind may omit the seconds as of TOML 1.1
|
zero date for a local time). Every kind may omit the seconds as of TOML 1.1
|
||||||
(`07:32`, `1979-05-27T07:32`); such a value carries a zero second, and the
|
(`07:32`, `1979-05-27T07:32`); such a value carries a zero second, and the
|
||||||
canonical rendering writes full seconds. There is no implicit conversion
|
encoder writes the seconds only when the value carries them, so a document
|
||||||
between the offset and local kinds; assigning one to the other is an error.
|
written without seconds comes back without them. There is no implicit
|
||||||
The four types take a bare timestamp and never a quoted string, so a document
|
conversion between the offset and local kinds; assigning one to the other is an
|
||||||
that writes a date-time with quotes does not decode into them, and neither
|
error. The four types take a bare timestamp and never a quoted string, so a
|
||||||
`encoding.TextUnmarshaler` nor the embedded `time.Time` changes that.
|
document that writes a date-time with quotes does not decode into them, and
|
||||||
|
neither `encoding.TextUnmarshaler` nor the embedded `time.Time` changes that.
|
||||||
|
|
||||||
### Arrays of tables
|
### Arrays of tables
|
||||||
|
|
||||||
@@ -426,6 +427,48 @@ carry verbatim (an embedded run of three single quotes, a control character
|
|||||||
other than tab or newline, or a carriage return outside a CRLF pair) also keeps
|
other than tab or newline, or a carriage return outside a CRLF pair) also keeps
|
||||||
the basic form, so the output always re-parses to the same value.
|
the basic form, so the output always re-parses to the same value.
|
||||||
|
|
||||||
|
### Inline tables
|
||||||
|
|
||||||
|
A table element of a value array, and a sub-table inlined by
|
||||||
|
[`InlineTables`](#compact-documents), is written as one `{a = 1, b = 2}` line
|
||||||
|
while it fits. An inline table that would pass the hundredth column carries
|
||||||
|
newlines and a trailing comma instead, which TOML 1.1 allows:
|
||||||
|
|
||||||
|
```toml
|
||||||
|
arr = [1, {
|
||||||
|
n = 1,
|
||||||
|
name = "a value long enough to push this line well past the one hundred column limit",
|
||||||
|
}]
|
||||||
|
```
|
||||||
|
|
||||||
|
The closing brace and the entries are indented one tab per nesting level, a
|
||||||
|
nested table is measured on its own line, and the output re-parses to the same
|
||||||
|
value either way.
|
||||||
|
|
||||||
|
### Compact documents
|
||||||
|
|
||||||
|
`InlineTables(threshold)` writes a sub-table as an inline table when its
|
||||||
|
single-line rendering is at most `threshold` bytes, and as a table header
|
||||||
|
section when it is longer. A document of small tables therefore grows shorter:
|
||||||
|
|
||||||
|
```go
|
||||||
|
out, err := interpres.NewEncoder().InlineTables(60).Marshal(cfg)
|
||||||
|
```
|
||||||
|
|
||||||
|
With `60` and a table of three short entries, the same value is written
|
||||||
|
|
||||||
|
```toml
|
||||||
|
server = {host = "127.0.0.1", port = 9090, tls = {on = false}}
|
||||||
|
```
|
||||||
|
|
||||||
|
instead of three lines under a `[server]` header and a `[server.tls]` section.
|
||||||
|
A nested sub-table takes part in the same way, and the whole option is off at
|
||||||
|
`0` or less. Two limits are deliberate. An array of tables keeps the `[[a]]`
|
||||||
|
header form, because its inline form re-parses as a value array and would change
|
||||||
|
the value's Go type. And because an inlined table is a value line, every one of
|
||||||
|
them precedes the first header of its document, so a table inlined next to a
|
||||||
|
header is not read back as part of that header's section.
|
||||||
|
|
||||||
### Cancellation
|
### Cancellation
|
||||||
|
|
||||||
`MarshalContext` and `(*Encoder).MarshalContext` accept a `context.Context`. The
|
`MarshalContext` and `(*Encoder).MarshalContext` accept a `context.Context`. The
|
||||||
@@ -440,6 +483,8 @@ The output is not byte-identical to any document that produced the value:
|
|||||||
- map keys are emitted in sorted order
|
- map keys are emitted in sorted order
|
||||||
- the choice between `[table]` headers and inline tables is not preserved
|
- the choice between `[table]` headers and inline tables is not preserved
|
||||||
- strings use the basic quoted form unless the literal option above applies
|
- strings use the basic quoted form unless the literal option above applies
|
||||||
|
- a date-time drops its zero seconds and the trailing zeros of its fraction, so
|
||||||
|
`07:32:00` is written `07:32`; both are the same value
|
||||||
- floats always carry a `.` or an exponent, so a float `1` is emitted as `1.0`
|
- floats always carry a `.` or an exponent, so a float `1` is emitted as `1.0`
|
||||||
and stays distinguishable from the integer `1` across a round-trip; negative
|
and stays distinguishable from the integer `1` across a round-trip; negative
|
||||||
zero is normalised to `0.0`
|
zero is normalised to `0.0`
|
||||||
@@ -516,12 +561,14 @@ encoder:
|
|||||||
| `GroupByKind(v bool)` | `true` | group entries as scalars, then sub-tables, then arrays of tables; `false` preserves declaration order |
|
| `GroupByKind(v bool)` | `true` | group entries as scalars, then sub-tables, then arrays of tables; `false` 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 `'''...'''` |
|
| `UseLiteralMultiline(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 |
|
||||||
|
|
||||||
```go
|
```go
|
||||||
out, err := interpres.NewEncoder().
|
out, err := interpres.NewEncoder().
|
||||||
GroupByKind(false).
|
GroupByKind(false).
|
||||||
OmitEmptyArrays().
|
OmitEmptyArrays().
|
||||||
UseLiteralMultiline(80).
|
UseLiteralMultiline(80).
|
||||||
|
InlineTables(60).
|
||||||
MarshalContext(ctx, cfg)
|
MarshalContext(ctx, cfg)
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -545,9 +592,11 @@ type LocalDate struct{ time.Time } // 1979-05-27
|
|||||||
type LocalTime struct{ time.Time } // 07:32:00.999999
|
type LocalTime struct{ time.Time } // 07:32:00.999999
|
||||||
```
|
```
|
||||||
|
|
||||||
Each carries a `String()` method returning the TOML-canonical rendering, with
|
Each carries a `String()` method returning the TOML-canonical rendering: the
|
||||||
the fractional second zero-padded to nanosecond precision when present. The
|
seconds appear only when the value carries them, and a fractional second drops
|
||||||
types are produced by `Parse` and accepted by `Marshal`.
|
its trailing zeros, so `07:32:00` renders as `07:32` and a half second as
|
||||||
|
`00.5`. The types are produced by `Parse` and accepted by `Marshal`, which
|
||||||
|
writes them through `String()`.
|
||||||
|
|
||||||
## Errors
|
## Errors
|
||||||
|
|
||||||
|
|||||||
@@ -28,18 +28,50 @@ var (
|
|||||||
textMarshalerType = reflect.TypeFor[encoding.TextMarshaler]()
|
textMarshalerType = reflect.TypeFor[encoding.TextMarshaler]()
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// inlineLimit is the column past which an inline table is written across
|
||||||
|
// lines. TOML 1.1 lets an inline table carry newlines and a trailing comma, so
|
||||||
|
// a long one stays readable instead of running off the line.
|
||||||
|
const inlineLimit = 100
|
||||||
|
|
||||||
|
// noInlineBreak is the limit a measuring encoder carries, high enough that the
|
||||||
|
// form it renders is always the single-line one.
|
||||||
|
const noInlineBreak = 1 << 30
|
||||||
|
|
||||||
// encoder produces a TOML document from a Go value via a small intermediate
|
// encoder produces a TOML document from a Go value via a small intermediate
|
||||||
// representation that preserves the order in which fields were declared.
|
// representation that preserves the order in which fields were declared.
|
||||||
type encoder struct {
|
type encoder struct {
|
||||||
buf bytes.Buffer
|
buf bytes.Buffer
|
||||||
ctx context.Context
|
ctx context.Context
|
||||||
opts Encoder
|
opts Encoder
|
||||||
|
|
||||||
|
// inlineDepth is the nesting level inside inline tables, which decides
|
||||||
|
// their indentation.
|
||||||
|
inlineDepth int
|
||||||
|
|
||||||
|
// limit is the column at which an inline table is broken; only a
|
||||||
|
// measuring encoder raises it.
|
||||||
|
limit int
|
||||||
}
|
}
|
||||||
|
|
||||||
func newEncoder() *encoder { return &encoder{} }
|
func newEncoder() *encoder { return &encoder{limit: inlineLimit} }
|
||||||
|
|
||||||
|
// flat returns an encoder that measures a value by rendering it on one line,
|
||||||
|
// so a caller can decide which form to write before writing it.
|
||||||
|
func (e *encoder) flat() *encoder {
|
||||||
|
return &encoder{ctx: e.ctx, opts: e.opts, limit: noInlineBreak}
|
||||||
|
}
|
||||||
|
|
||||||
func (e *encoder) bytes() []byte { return e.buf.Bytes() }
|
func (e *encoder) bytes() []byte { return e.buf.Bytes() }
|
||||||
|
|
||||||
|
// column reports how many bytes the current line already holds, so a form can
|
||||||
|
// be measured against the limit before it is written.
|
||||||
|
func (e *encoder) column() int {
|
||||||
|
if i := bytes.LastIndexByte(e.buf.Bytes(), '\n'); i >= 0 {
|
||||||
|
return e.buf.Len() - i - 1
|
||||||
|
}
|
||||||
|
return e.buf.Len()
|
||||||
|
}
|
||||||
|
|
||||||
func (e *encoder) checkCtx() error {
|
func (e *encoder) checkCtx() error {
|
||||||
if e.ctx == nil {
|
if e.ctx == nil {
|
||||||
return nil
|
return nil
|
||||||
@@ -709,7 +741,20 @@ func (e *encoder) emitDoc(doc *tomlDoc, prefix []string) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// An inlined sub-table is a value line, so it has to precede every
|
||||||
|
// header of this document: a line written after a [header] would be
|
||||||
|
// read back as part of that table.
|
||||||
|
headers := make([]entry, 0, len(tables))
|
||||||
for _, t := range tables {
|
for _, t := range tables {
|
||||||
|
inlined, err := e.writeInlineSubTableIfSmall(t.key, t.doc)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if !inlined {
|
||||||
|
headers = append(headers, t)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for _, t := range headers {
|
||||||
path := append(append([]string{}, prefix...), t.key)
|
path := append(append([]string{}, prefix...), t.key)
|
||||||
e.writeBlankLine()
|
e.writeBlankLine()
|
||||||
e.buf.WriteByte('[')
|
e.buf.WriteByte('[')
|
||||||
@@ -750,6 +795,13 @@ func (e *encoder) emitDoc(doc *tomlDoc, prefix []string) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
case entryTable:
|
case entryTable:
|
||||||
|
inlined, err := e.writeInlineSubTableIfSmall(ent.key, ent.doc)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if inlined {
|
||||||
|
continue
|
||||||
|
}
|
||||||
path := append(append([]string{}, prefix...), ent.key)
|
path := append(append([]string{}, prefix...), ent.key)
|
||||||
e.writeBlankLine()
|
e.writeBlankLine()
|
||||||
e.buf.WriteByte('[')
|
e.buf.WriteByte('[')
|
||||||
@@ -891,7 +943,7 @@ func (e *encoder) writeValue(val any) error {
|
|||||||
case float64:
|
case float64:
|
||||||
return e.writeFloat(v)
|
return e.writeFloat(v)
|
||||||
case time.Time:
|
case time.Time:
|
||||||
e.buf.WriteString(v.Format(time.RFC3339Nano))
|
e.buf.WriteString(offsetString(v))
|
||||||
return nil
|
return nil
|
||||||
case LocalDateTime:
|
case LocalDateTime:
|
||||||
e.buf.WriteString(v.String())
|
e.buf.WriteString(v.String())
|
||||||
@@ -915,7 +967,7 @@ func (e *encoder) writeValue(val any) error {
|
|||||||
e.buf.WriteByte(']')
|
e.buf.WriteByte(']')
|
||||||
return nil
|
return nil
|
||||||
case map[string]any:
|
case map[string]any:
|
||||||
return e.writeInlineTable(v)
|
return e.writeInlineMap(v)
|
||||||
case nil:
|
case nil:
|
||||||
return fmt.Errorf("interpres: cannot encode nil value")
|
return fmt.Errorf("interpres: cannot encode nil value")
|
||||||
default:
|
default:
|
||||||
@@ -923,10 +975,24 @@ func (e *encoder) writeValue(val any) error {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// writeInlineTable renders m as a TOML inline table with sorted keys, the
|
// writeInlineMap renders m as a TOML inline table, on one line when it fits
|
||||||
// order buildMapDoc uses for header tables. It backs the table elements of a
|
// there and across lines when it does not.
|
||||||
// value array, where the [[header]] form is not available.
|
func (e *encoder) writeInlineMap(m map[string]any) error {
|
||||||
func (e *encoder) writeInlineTable(m map[string]any) error {
|
flat := e.flat()
|
||||||
|
if err := flat.writeInlineMapFlat(m); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if e.column()+flat.buf.Len() <= e.limit {
|
||||||
|
e.buf.Write(flat.buf.Bytes())
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return e.writeInlineMapMultiline(m)
|
||||||
|
}
|
||||||
|
|
||||||
|
// writeInlineMapFlat renders m as a single-line inline table with sorted keys,
|
||||||
|
// the order buildMapDoc uses for header tables. It backs the table elements of
|
||||||
|
// a value array, where the [[header]] form is not available.
|
||||||
|
func (e *encoder) writeInlineMapFlat(m map[string]any) error {
|
||||||
keys := slices.Sorted(maps.Keys(m))
|
keys := slices.Sorted(maps.Keys(m))
|
||||||
e.buf.WriteByte('{')
|
e.buf.WriteByte('{')
|
||||||
for i, k := range keys {
|
for i, k := range keys {
|
||||||
@@ -945,6 +1011,137 @@ func (e *encoder) writeInlineTable(m map[string]any) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// writeInlineMapMultiline renders m with one entry per line and a trailing
|
||||||
|
// comma, the form TOML 1.1 allows for an inline table too long for one line.
|
||||||
|
func (e *encoder) writeInlineMapMultiline(m map[string]any) error {
|
||||||
|
keys := slices.Sorted(maps.Keys(m))
|
||||||
|
e.buf.WriteString("{\n")
|
||||||
|
e.inlineDepth++
|
||||||
|
for _, k := range keys {
|
||||||
|
e.writeInlineIndent()
|
||||||
|
if err := e.writeKey(k); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
e.buf.WriteString(" = ")
|
||||||
|
if err := e.writeValue(m[k]); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
e.buf.WriteString(",\n")
|
||||||
|
}
|
||||||
|
e.inlineDepth--
|
||||||
|
e.writeInlineIndent()
|
||||||
|
e.buf.WriteByte('}')
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// writeInlineIndent writes one tab per inline-table nesting level.
|
||||||
|
func (e *encoder) writeInlineIndent() {
|
||||||
|
for range e.inlineDepth {
|
||||||
|
e.buf.WriteByte('\t')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// errInlineArrayOfTables reports an attempt to render an array of tables
|
||||||
|
// inline, which has no form that keeps the value's type.
|
||||||
|
var errInlineArrayOfTables = errors.New("interpres: an array of tables has no inline form")
|
||||||
|
|
||||||
|
// inlinableDoc reports whether doc can be written as an inline table without
|
||||||
|
// changing the type of any value: scalars, value arrays and further sub-tables
|
||||||
|
// are fine, while an array of tables is not, because its inline form would
|
||||||
|
// re-parse as a value array.
|
||||||
|
func inlinableDoc(doc *tomlDoc) bool {
|
||||||
|
for _, ent := range doc.entries {
|
||||||
|
switch ent.kind {
|
||||||
|
case entryArray:
|
||||||
|
return false
|
||||||
|
case entryTable:
|
||||||
|
if !inlinableDoc(ent.doc) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// writeInlineDocEntry writes one "key = value" binding of an inline table,
|
||||||
|
// without the separator that follows it.
|
||||||
|
func (e *encoder) writeInlineDocEntry(ent entry) error {
|
||||||
|
if err := e.writeKey(ent.key); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
e.buf.WriteString(" = ")
|
||||||
|
switch ent.kind {
|
||||||
|
case entryTable:
|
||||||
|
return e.writeInlineDoc(ent.doc)
|
||||||
|
case entryArray:
|
||||||
|
return errInlineArrayOfTables
|
||||||
|
default:
|
||||||
|
return e.writeValue(ent.val)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// writeInlineDoc renders doc as a single-line inline table in entry order, the
|
||||||
|
// order the fields were declared in.
|
||||||
|
func (e *encoder) writeInlineDoc(doc *tomlDoc) error {
|
||||||
|
e.buf.WriteByte('{')
|
||||||
|
for i, ent := range doc.entries {
|
||||||
|
if i > 0 {
|
||||||
|
e.buf.WriteString(", ")
|
||||||
|
}
|
||||||
|
if err := e.writeInlineDocEntry(ent); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
e.buf.WriteByte('}')
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// writeInlineDocMultiline renders doc with one entry per line and a trailing
|
||||||
|
// comma, the form TOML 1.1 allows for an inline table too long for one line.
|
||||||
|
func (e *encoder) writeInlineDocMultiline(doc *tomlDoc) error {
|
||||||
|
e.buf.WriteString("{\n")
|
||||||
|
e.inlineDepth++
|
||||||
|
for _, ent := range doc.entries {
|
||||||
|
e.writeInlineIndent()
|
||||||
|
if err := e.writeInlineDocEntry(ent); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
e.buf.WriteString(",\n")
|
||||||
|
}
|
||||||
|
e.inlineDepth--
|
||||||
|
e.writeInlineIndent()
|
||||||
|
e.buf.WriteByte('}')
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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) {
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
flat := e.flat()
|
||||||
|
if err := flat.writeInlineDoc(doc); err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
if flat.buf.Len() > e.opts.inlineTablesAt {
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
if err := e.writeKey(name); err != nil {
|
||||||
|
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 {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
e.buf.WriteByte('\n')
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
|
|
||||||
func (e *encoder) writeStringVal(s string) error {
|
func (e *encoder) writeStringVal(s string) error {
|
||||||
if e.opts.literalMultilineAt > 0 && strings.ContainsRune(s, '\n') &&
|
if e.opts.literalMultilineAt > 0 && strings.ContainsRune(s, '\n') &&
|
||||||
len(s) >= e.opts.literalMultilineAt && canBeLiteralMultiline(s) {
|
len(s) >= e.opts.literalMultilineAt && canBeLiteralMultiline(s) {
|
||||||
|
|||||||
+240
-12
@@ -326,7 +326,7 @@ func TestMarshalerReturningTime(t *testing.T) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("marshal: %v", err)
|
t.Fatalf("marshal: %v", err)
|
||||||
}
|
}
|
||||||
want := "m = 2026-06-26T10:00:00Z\n"
|
want := "m = 2026-06-26T10:00Z\n"
|
||||||
if string(out) != want {
|
if string(out) != want {
|
||||||
t.Errorf("output mismatch:\ngot: %q\nwant: %q", out, want)
|
t.Errorf("output mismatch:\ngot: %q\nwant: %q", out, want)
|
||||||
}
|
}
|
||||||
@@ -466,7 +466,7 @@ func TestMarshalEmbeddedScalarStruct(t *testing.T) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("marshal: %v", err)
|
t.Fatalf("marshal: %v", err)
|
||||||
}
|
}
|
||||||
want := "name = \"x\"\ns = 2026-06-26T00:00:00\n"
|
want := "name = \"x\"\ns = 2026-06-26T00:00\n"
|
||||||
if string(out) != want {
|
if string(out) != want {
|
||||||
t.Errorf("output mismatch:\ngot: %q\nwant: %q", out, want)
|
t.Errorf("output mismatch:\ngot: %q\nwant: %q", out, want)
|
||||||
}
|
}
|
||||||
@@ -541,7 +541,7 @@ func TestMarshalDateTime(t *testing.T) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("marshal: %v", err)
|
t.Fatalf("marshal: %v", err)
|
||||||
}
|
}
|
||||||
want := "offset = 2026-06-26T10:00:00Z\nlocal = 2026-06-26T07:32:00\nday = 2026-06-26\nclock = 07:32:00\n"
|
want := "offset = 2026-06-26T10:00Z\nlocal = 2026-06-26T07:32\nday = 2026-06-26\nclock = 07:32\n"
|
||||||
if string(out) != want {
|
if string(out) != want {
|
||||||
t.Errorf("output mismatch:\ngot: %q\nwant: %q", out, want)
|
t.Errorf("output mismatch:\ngot: %q\nwant: %q", out, want)
|
||||||
}
|
}
|
||||||
@@ -688,7 +688,7 @@ func TestMarshalInlineTableWithDatetime(t *testing.T) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("marshal: %v", err)
|
t.Fatalf("marshal: %v", err)
|
||||||
}
|
}
|
||||||
want := "mix = [1979-05-27T07:32:00Z, {t = 1979-05-27T07:32:00}]\n"
|
want := "mix = [1979-05-27T07:32Z, {t = 1979-05-27T07:32}]\n"
|
||||||
if string(out) != want {
|
if string(out) != want {
|
||||||
t.Fatalf("output mismatch:\ngot: %q\nwant: %q", out, want)
|
t.Fatalf("output mismatch:\ngot: %q\nwant: %q", out, want)
|
||||||
}
|
}
|
||||||
@@ -907,7 +907,7 @@ func TestMarshalTagOptionOmitZero(t *testing.T) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("marshal: %v", err)
|
t.Fatalf("marshal: %v", err)
|
||||||
}
|
}
|
||||||
want = "name = \"x\"\ncount = 1\nratio = 0.5\nwhen = 2026-09-17T12:00:00Z\nalways = \"kept\"\n\n[server]\nhost = \"h\"\n"
|
want = "name = \"x\"\ncount = 1\nratio = 0.5\nwhen = 2026-09-17T12:00Z\nalways = \"kept\"\n\n[server]\nhost = \"h\"\n"
|
||||||
if string(out) != want {
|
if string(out) != want {
|
||||||
t.Fatalf("output mismatch:\ngot: %q\nwant: %q", out, want)
|
t.Fatalf("output mismatch:\ngot: %q\nwant: %q", out, want)
|
||||||
}
|
}
|
||||||
@@ -1196,6 +1196,8 @@ qty = 2
|
|||||||
|
|
||||||
[meta]
|
[meta]
|
||||||
created = 2026-06-26T10:00:00Z
|
created = 2026-06-26T10:00:00Z
|
||||||
|
|
||||||
|
mixed = [1, {n = 1, name = "a value long enough to push this line well past the one hundred column limit"}]
|
||||||
`)
|
`)
|
||||||
tree1, err := Parse(src)
|
tree1, err := Parse(src)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -1258,24 +1260,35 @@ func TestLocalDateString(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestLocalDateTimeString(t *testing.T) {
|
func TestLocalDateTimeString(t *testing.T) {
|
||||||
|
// The rendering drops zero seconds and the trailing zeros of a fraction,
|
||||||
|
// which TOML 1.1 allows and which keeps a value written without seconds
|
||||||
|
// written without them.
|
||||||
ldt := LocalDateTime{Time: time.Date(1979, 5, 27, 7, 32, 0, 0, time.UTC)}
|
ldt := LocalDateTime{Time: time.Date(1979, 5, 27, 7, 32, 0, 0, time.UTC)}
|
||||||
if got := ldt.String(); got != "1979-05-27T07:32:00" {
|
if got := ldt.String(); got != "1979-05-27T07:32" {
|
||||||
t.Errorf("LocalDateTime.String() = %q, want 1979-05-27T07:32:00", got)
|
t.Errorf("LocalDateTime.String() = %q, want 1979-05-27T07:32", got)
|
||||||
}
|
}
|
||||||
ldt2 := LocalDateTime{Time: time.Date(1979, 5, 27, 7, 32, 0, 5, time.UTC)}
|
ldt2 := LocalDateTime{Time: time.Date(1979, 5, 27, 7, 32, 0, 5, time.UTC)}
|
||||||
if got := ldt2.String(); got != "1979-05-27T07:32:00.000000005" {
|
if got := ldt2.String(); got != "1979-05-27T07:32:00.000000005" {
|
||||||
t.Errorf("LocalDateTime.String() = %q, want 1979-05-27T07:32:00.000000005", got)
|
t.Errorf("LocalDateTime.String() = %q, want 1979-05-27T07:32:00.000000005", got)
|
||||||
}
|
}
|
||||||
ldt3 := LocalDateTime{Time: time.Date(1979, 5, 27, 7, 32, 0, 500, time.UTC)}
|
ldt3 := LocalDateTime{Time: time.Date(1979, 5, 27, 7, 32, 0, 500, time.UTC)}
|
||||||
if got := ldt3.String(); got != "1979-05-27T07:32:00.000000500" {
|
if got := ldt3.String(); got != "1979-05-27T07:32:00.0000005" {
|
||||||
t.Errorf("LocalDateTime.String() = %q, want 1979-05-27T07:32:00.000000500", got)
|
t.Errorf("LocalDateTime.String() = %q, want 1979-05-27T07:32:00.0000005", got)
|
||||||
|
}
|
||||||
|
ldt4 := LocalDateTime{Time: time.Date(1979, 5, 27, 7, 32, 30, 500000000, time.UTC)}
|
||||||
|
if got := ldt4.String(); got != "1979-05-27T07:32:30.5" {
|
||||||
|
t.Errorf("LocalDateTime.String() = %q, want 1979-05-27T07:32:30.5", got)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestLocalTimeString(t *testing.T) {
|
func TestLocalTimeString(t *testing.T) {
|
||||||
lt := LocalTime{Time: time.Date(0, 1, 1, 7, 32, 0, 0, time.UTC)}
|
lt := LocalTime{Time: time.Date(0, 1, 1, 7, 32, 0, 0, time.UTC)}
|
||||||
if got := lt.String(); got != "07:32:00" {
|
if got := lt.String(); got != "07:32" {
|
||||||
t.Errorf("LocalTime.String() = %q, want 07:32:00", got)
|
t.Errorf("LocalTime.String() = %q, want 07:32", got)
|
||||||
|
}
|
||||||
|
lt2 := LocalTime{Time: time.Date(0, 1, 1, 7, 32, 15, 250000000, time.UTC)}
|
||||||
|
if got := lt2.String(); got != "07:32:15.25" {
|
||||||
|
t.Errorf("LocalTime.String() = %q, want 07:32:15.25", got)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1490,7 +1503,7 @@ func TestMarshalTextLeavesDateTimesAlone(t *testing.T) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("marshal: %v", err)
|
t.Fatalf("marshal: %v", err)
|
||||||
}
|
}
|
||||||
want := "stamp = 2026-06-26T10:00:00Z\nptr = 2026-06-26T10:00:00Z\nday = 1979-05-27\nat = 1979-05-27T07:32:00\nclock = 07:32:00\n"
|
want := "stamp = 2026-06-26T10:00Z\nptr = 2026-06-26T10:00Z\nday = 1979-05-27\nat = 1979-05-27T07:32\nclock = 07:32\n"
|
||||||
if string(out) != want {
|
if string(out) != want {
|
||||||
t.Errorf("output mismatch:\ngot: %q\nwant: %q", out, want)
|
t.Errorf("output mismatch:\ngot: %q\nwant: %q", out, want)
|
||||||
}
|
}
|
||||||
@@ -1572,3 +1585,218 @@ func TestMarshalTextValuesRoundTrip(t *testing.T) {
|
|||||||
t.Errorf("Tag = %q, want %q", back.Tag, in.Tag)
|
t.Errorf("Tag = %q, want %q", back.Tag, in.Tag)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// --- TOML 1.1 output forms -------------------------------------------------
|
||||||
|
|
||||||
|
func TestMarshalDateTimeRendering(t *testing.T) {
|
||||||
|
// The seconds are written only when the value carries them, and a fraction
|
||||||
|
// drops its trailing zeros. Both are the same value either way; the shorter
|
||||||
|
// form is the one TOML 1.1 allows.
|
||||||
|
base := time.Date(2026, 6, 26, 10, 0, 0, 0, time.UTC)
|
||||||
|
cases := []struct {
|
||||||
|
name string
|
||||||
|
val any
|
||||||
|
want string
|
||||||
|
}{
|
||||||
|
{"offset-zero-seconds", base, "v = 2026-06-26T10:00Z\n"},
|
||||||
|
{"offset-seconds", base.Add(30 * time.Second), "v = 2026-06-26T10:00:30Z\n"},
|
||||||
|
{"offset-fraction", base.Add(500 * time.Millisecond), "v = 2026-06-26T10:00:00.5Z\n"},
|
||||||
|
{"offset-zone", time.Date(2026, 6, 26, 10, 0, 0, 0, time.FixedZone("", -7*3600)), "v = 2026-06-26T10:00-07:00\n"},
|
||||||
|
{"local-zero-seconds", LocalDateTime{Time: base}, "v = 2026-06-26T10:00\n"},
|
||||||
|
{"local-fraction", LocalDateTime{Time: base.Add(2500 * time.Millisecond)}, "v = 2026-06-26T10:00:02.5\n"},
|
||||||
|
{"date", LocalDate{Time: base}, "v = 2026-06-26\n"},
|
||||||
|
{"time-zero-seconds", LocalTime{Time: base}, "v = 10:00\n"},
|
||||||
|
{"time-seconds", LocalTime{Time: base.Add(15 * time.Second)}, "v = 10:00:15\n"},
|
||||||
|
{"time-nanoseconds", LocalTime{Time: base.Add(123456789 * time.Nanosecond)}, "v = 10:00:00.123456789\n"},
|
||||||
|
}
|
||||||
|
for _, c := range cases {
|
||||||
|
out, err := Marshal(map[string]any{"v": c.val})
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("%s: marshal: %v", c.name, err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if string(out) != c.want {
|
||||||
|
t.Errorf("%s: output mismatch:\ngot: %q\nwant: %q", c.name, out, c.want)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMarshalInlineTableBreaksWhenLong(t *testing.T) {
|
||||||
|
// A table element of a value array is written inline; a long one carries
|
||||||
|
// newlines and a trailing comma instead of running past the line limit,
|
||||||
|
// which TOML 1.1 allows an inline table to do.
|
||||||
|
const long = "a-very-long-value-that-pushes-the-line-well-past-the-one-hundred-column-limit"
|
||||||
|
type Cfg struct {
|
||||||
|
Arr []any `toml:"arr"`
|
||||||
|
}
|
||||||
|
out, err := Marshal(Cfg{Arr: []any{int64(1), map[string]any{"n": int64(1), "name": long}}})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("marshal: %v", err)
|
||||||
|
}
|
||||||
|
want := "arr = [1, {\n\tn = 1,\n\tname = \"" + long + "\",\n}]\n"
|
||||||
|
if string(out) != want {
|
||||||
|
t.Errorf("output mismatch:\ngot: %q\nwant: %q", out, want)
|
||||||
|
}
|
||||||
|
|
||||||
|
// The same values without the long string stay on one line.
|
||||||
|
out, err = Marshal(Cfg{Arr: []any{int64(1), map[string]any{"n": int64(1), "name": "short"}}})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("marshal: %v", err)
|
||||||
|
}
|
||||||
|
if want := "arr = [1, {n = 1, name = \"short\"}]\n"; string(out) != want {
|
||||||
|
t.Errorf("output mismatch:\ngot: %q\nwant: %q", out, want)
|
||||||
|
}
|
||||||
|
|
||||||
|
// The broken form parses back to the same tree.
|
||||||
|
tree, err := Parse(out)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("parse of the encoder output: %v", err)
|
||||||
|
}
|
||||||
|
if got := len(tree["arr"].([]any)); got != 2 {
|
||||||
|
t.Fatalf("arr has %d elements, want 2", got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMarshalNestedInlineTableBreaksIndependently(t *testing.T) {
|
||||||
|
// A nested table breaks on its own measure, so a table whose entries stay
|
||||||
|
// short keeps the one-line form inside a parent that broke.
|
||||||
|
const long = "a-very-long-value-that-pushes-the-line-well-past-the-one-hundred-column-limit"
|
||||||
|
type Cfg struct {
|
||||||
|
Arr []any `toml:"arr"`
|
||||||
|
}
|
||||||
|
out, err := Marshal(Cfg{Arr: []any{int64(1), map[string]any{"n": int64(1), "sub": map[string]any{"name": long}}}})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("marshal: %v", err)
|
||||||
|
}
|
||||||
|
want := "arr = [1, {\n\tn = 1,\n\tsub = {name = \"" + long + "\"},\n}]\n"
|
||||||
|
if string(out) != want {
|
||||||
|
t.Errorf("output mismatch:\ngot: %q\nwant: %q", out, want)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type inlineTLS struct {
|
||||||
|
On bool `toml:"on"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type inlineServer struct {
|
||||||
|
Host string `toml:"host"`
|
||||||
|
Port int `toml:"port"`
|
||||||
|
TLS inlineTLS `toml:"tls"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type inlineBig struct {
|
||||||
|
A int `toml:"a"`
|
||||||
|
B int `toml:"b"`
|
||||||
|
C int `toml:"c"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestEncoderInlineTables(t *testing.T) {
|
||||||
|
type Cfg struct {
|
||||||
|
Server inlineServer `toml:"server"`
|
||||||
|
Big inlineBig `toml:"big"`
|
||||||
|
}
|
||||||
|
cfg := Cfg{Server: inlineServer{Host: "127.0.0.1", Port: 9090}, Big: inlineBig{A: 1, B: 2, C: 3}}
|
||||||
|
|
||||||
|
// The default keeps every sub-table a header section.
|
||||||
|
headerForm, err := Marshal(cfg)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("marshal: %v", err)
|
||||||
|
}
|
||||||
|
want := "[server]\nhost = \"127.0.0.1\"\nport = 9090\n\n[server.tls]\non = false\n\n[big]\na = 1\nb = 2\nc = 3\n"
|
||||||
|
if string(headerForm) != want {
|
||||||
|
t.Errorf("default output mismatch:\ngot: %q\nwant: %q", headerForm, want)
|
||||||
|
}
|
||||||
|
|
||||||
|
// With the option both fit the threshold and become inline tables, nested
|
||||||
|
// ones included.
|
||||||
|
out, err := NewEncoder().InlineTables(60).Marshal(cfg)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("marshal: %v", err)
|
||||||
|
}
|
||||||
|
want = "server = {host = \"127.0.0.1\", port = 9090, tls = {on = false}}\nbig = {a = 1, b = 2, c = 3}\n"
|
||||||
|
if string(out) != want {
|
||||||
|
t.Errorf("compact output mismatch:\ngot: %q\nwant: %q", out, want)
|
||||||
|
}
|
||||||
|
|
||||||
|
// A threshold below the rendering keeps the header form.
|
||||||
|
out, err = NewEncoder().InlineTables(10).Marshal(cfg)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("marshal: %v", err)
|
||||||
|
}
|
||||||
|
if string(out) != string(headerForm) {
|
||||||
|
t.Errorf("small threshold output mismatch:\ngot: %q\nwant: %q", out, headerForm)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestEncoderInlineTablesOrderAndRoundTrip(t *testing.T) {
|
||||||
|
// An inlined sub-table is a value line, so it precedes every header of the
|
||||||
|
// document; written after a header it would be read back as part of that
|
||||||
|
// table. The compact form and the header form parse to the same tree.
|
||||||
|
type Four struct {
|
||||||
|
A int `toml:"a"`
|
||||||
|
B int `toml:"b"`
|
||||||
|
C int `toml:"c"`
|
||||||
|
D int `toml:"d"`
|
||||||
|
}
|
||||||
|
type Cfg struct {
|
||||||
|
Small inlineTLS `toml:"small"`
|
||||||
|
Big Four `toml:"big"`
|
||||||
|
}
|
||||||
|
cfg := Cfg{Small: inlineTLS{On: true}, Big: Four{A: 1, B: 2, C: 3, D: 4}}
|
||||||
|
|
||||||
|
headerForm, err := Marshal(cfg)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("marshal: %v", err)
|
||||||
|
}
|
||||||
|
compact, err := NewEncoder().InlineTables(20).Marshal(cfg)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("marshal: %v", err)
|
||||||
|
}
|
||||||
|
want := "small = {on = true}\n\n[big]\na = 1\nb = 2\nc = 3\nd = 4\n"
|
||||||
|
if string(compact) != want {
|
||||||
|
t.Errorf("output mismatch:\ngot: %q\nwant: %q", compact, want)
|
||||||
|
}
|
||||||
|
|
||||||
|
got, err := Parse(compact)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("parse of the compact output: %v", err)
|
||||||
|
}
|
||||||
|
ref, err := Parse(headerForm)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("parse of the header output: %v", err)
|
||||||
|
}
|
||||||
|
if !reflect.DeepEqual(got, ref) {
|
||||||
|
t.Errorf("the compact form changed the tree:\ncompact: %#v\nheaders: %#v", got, ref)
|
||||||
|
}
|
||||||
|
if _, ok := got["big"].(map[string]any); !ok {
|
||||||
|
t.Errorf("big = %#v, want a table", got["big"])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestEncoderInlineTablesKeepsArraysOfTables(t *testing.T) {
|
||||||
|
// An array of tables has no inline form that keeps the value's type, so the
|
||||||
|
// option leaves it alone and the tree keeps its []map[string]any shape.
|
||||||
|
type Item struct {
|
||||||
|
N int `toml:"n"`
|
||||||
|
}
|
||||||
|
type Cfg struct {
|
||||||
|
Items []Item `toml:"items"`
|
||||||
|
Small inlineTLS `toml:"small"`
|
||||||
|
}
|
||||||
|
cfg := Cfg{Items: []Item{{N: 1}}, Small: inlineTLS{On: true}}
|
||||||
|
out, err := NewEncoder().InlineTables(60).Marshal(cfg)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("marshal: %v", err)
|
||||||
|
}
|
||||||
|
want := "small = {on = true}\n\n[[items]]\nn = 1\n"
|
||||||
|
if string(out) != want {
|
||||||
|
t.Errorf("output mismatch:\ngot: %q\nwant: %q", out, want)
|
||||||
|
}
|
||||||
|
tree, err := Parse(out)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("parse: %v", err)
|
||||||
|
}
|
||||||
|
if _, ok := tree["items"].([]map[string]any); !ok {
|
||||||
|
t.Errorf("items = %#v, want []map[string]any", tree["items"])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
+28
-3
@@ -232,7 +232,11 @@ type Unmarshaler interface {
|
|||||||
// inline table.
|
// inline table.
|
||||||
// - Scalars encode as TOML scalars: bool, int64, float64, string, time.Time
|
// - Scalars encode as TOML scalars: bool, int64, float64, string, time.Time
|
||||||
// (offset date-time), and LocalDateTime/LocalDate/LocalTime (local
|
// (offset date-time), and LocalDateTime/LocalDate/LocalTime (local
|
||||||
// variants).
|
// variants). A date-time writes its seconds only when the value carries
|
||||||
|
// them, and drops the trailing zeros of a fractional second.
|
||||||
|
// - A table element of a value array, and a sub-table inlined by
|
||||||
|
// Encoder.InlineTables, is written as an inline table, across lines when it
|
||||||
|
// does not fit one.
|
||||||
// - Values implementing Marshaler are encoded by calling MarshalTOML and
|
// - Values implementing Marshaler are encoded by calling MarshalTOML and
|
||||||
// using its result.
|
// using its result.
|
||||||
// - Values implementing encoding.TextMarshaler, and not one of the
|
// - Values implementing encoding.TextMarshaler, and not one of the
|
||||||
@@ -261,14 +265,16 @@ func MarshalContext(ctx context.Context, v any) ([]byte, error) {
|
|||||||
|
|
||||||
// An Encoder encodes Go values into TOML.
|
// An Encoder encodes Go values into TOML.
|
||||||
//
|
//
|
||||||
// All options default to the behaviour earlier releases used, and the defaults
|
// All options default to the behaviour that passes the toml-test compliance
|
||||||
// pass the toml-test compliance suite in both directions:
|
// suite in both directions:
|
||||||
//
|
//
|
||||||
// GroupByKind: true (scalars first, then tables, then arrays of tables)
|
// GroupByKind: true (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
|
||||||
// literal one)
|
// literal one)
|
||||||
|
// InlineTablesAt: 0 (always emit a table header, never an inline
|
||||||
|
// table)
|
||||||
//
|
//
|
||||||
// Use the chainable option methods to opt out. The option state is private;
|
// Use the chainable option methods to opt out. The option state is private;
|
||||||
// callers that need the underlying knobs reach for the methods rather than
|
// callers that need the underlying knobs reach for the methods rather than
|
||||||
@@ -277,6 +283,7 @@ type Encoder struct {
|
|||||||
groupByKind bool // default true; set via (*Encoder).GroupByKind
|
groupByKind bool // default true; set via (*Encoder).GroupByKind
|
||||||
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).UseLiteralMultiline
|
||||||
|
inlineTablesAt int // default 0; set via (*Encoder).InlineTables
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewEncoder returns an Encoder with default options.
|
// NewEncoder returns an Encoder with default options.
|
||||||
@@ -309,6 +316,24 @@ func (e *Encoder) UseLiteralMultiline(threshold int) *Encoder {
|
|||||||
return e
|
return e
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// InlineTables sets the size limit, in bytes of the single-line rendering, at
|
||||||
|
// which a sub-table is written as an inline table instead of a table header,
|
||||||
|
// which makes a document of small tables shorter. Use 0 or any negative value
|
||||||
|
// to disable (always emit a header).
|
||||||
|
//
|
||||||
|
// A sub-table is inlined only when doing so keeps every value's type: an array
|
||||||
|
// of tables keeps its header form, because its inline form would re-parse as a
|
||||||
|
// 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
|
||||||
|
// 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 {
|
||||||
|
e.inlineTablesAt = threshold
|
||||||
|
return e
|
||||||
|
}
|
||||||
|
|
||||||
// Marshal encodes v to TOML bytes. It is equivalent to calling Marshal with v.
|
// Marshal encodes v to TOML bytes. It is equivalent to calling Marshal with v.
|
||||||
//
|
//
|
||||||
// Marshal is equivalent to MarshalContext with context.Background.
|
// Marshal is equivalent to MarshalContext with context.Background.
|
||||||
|
|||||||
+6
-4
@@ -611,11 +611,13 @@ odt2 = 1979-05-27 07:32-07:00
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("parse: %v", err)
|
t.Fatalf("parse: %v", err)
|
||||||
}
|
}
|
||||||
if got := tree["t"].(LocalTime).String(); got != "13:37:00" {
|
// A value written without seconds comes back without them: the seconds are
|
||||||
t.Errorf("t = %q, want %q", got, "13:37:00")
|
// only written when the value carries them.
|
||||||
|
if got := tree["t"].(LocalTime).String(); got != "13:37" {
|
||||||
|
t.Errorf("t = %q, want %q", got, "13:37")
|
||||||
}
|
}
|
||||||
if got := tree["dt"].(LocalDateTime).String(); got != "1979-05-27T07:32:00" {
|
if got := tree["dt"].(LocalDateTime).String(); got != "1979-05-27T07:32" {
|
||||||
t.Errorf("dt = %q, want %q", got, "1979-05-27T07:32:00")
|
t.Errorf("dt = %q, want %q", got, "1979-05-27T07:32")
|
||||||
}
|
}
|
||||||
if got := tree["odt1"].(time.Time).Format(time.RFC3339Nano); got != "1979-05-27T07:32:00Z" {
|
if got := tree["odt1"].(time.Time).Format(time.RFC3339Nano); got != "1979-05-27T07:32:00Z" {
|
||||||
t.Errorf("odt1 = %q", got)
|
t.Errorf("odt1 = %q", got)
|
||||||
|
|||||||
Reference in New Issue
Block a user