fix(api): one statement per value array, parseas options and bounded reads
Assisted-by: GLM 5.3
This commit is contained in:
+89
@@ -0,0 +1,89 @@
|
|||||||
|
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
package interpres
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
// errReader fails every read with a fixed error.
|
||||||
|
type errReader struct{ err error }
|
||||||
|
|
||||||
|
func (r errReader) Read([]byte) (int, error) { return 0, r.err }
|
||||||
|
|
||||||
|
// TestUnmarshalRead covers the streaming entry: the happy path with options,
|
||||||
|
// a failing reader, and MaxInputSize bounding what a reader is drained into.
|
||||||
|
func TestUnmarshalRead(t *testing.T) {
|
||||||
|
var got struct {
|
||||||
|
Name string `toml:"name"`
|
||||||
|
N int `toml:"n"`
|
||||||
|
}
|
||||||
|
err := UnmarshalRead(strings.NewReader("name = \"x\"\n"), &got, RejectUnknownFields(true))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("UnmarshalRead: %v", err)
|
||||||
|
}
|
||||||
|
if got.Name != "x" {
|
||||||
|
t.Errorf("Name = %q", got.Name)
|
||||||
|
}
|
||||||
|
|
||||||
|
readErr := errors.New("boom")
|
||||||
|
if err := UnmarshalRead(errReader{readErr}, &got); !errors.Is(err, readErr) {
|
||||||
|
t.Errorf("err = %v, want the read error wrapped", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = UnmarshalRead(strings.NewReader("name = \"x\"\n"), &got, MaxInputSize(4))
|
||||||
|
if err == nil || !strings.Contains(err.Error(), "over the limit") {
|
||||||
|
t.Errorf("err = %v, want the size limit", err)
|
||||||
|
}
|
||||||
|
// The limit bounds the read itself: a reader that would supply far more
|
||||||
|
// than the limit is not drained into memory first.
|
||||||
|
big := strings.Repeat("x", 1<<20)
|
||||||
|
if err := UnmarshalRead(strings.NewReader(big), &got, MaxInputSize(16)); err == nil || !strings.Contains(err.Error(), "over the limit") {
|
||||||
|
t.Errorf("err = %v, want the size limit before the read completes", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestParseAsWithOptions covers the generic shorthand carrying options.
|
||||||
|
func TestParseAsWithOptions(t *testing.T) {
|
||||||
|
type cfg struct {
|
||||||
|
Name string `toml:"name"`
|
||||||
|
}
|
||||||
|
got, err := ParseAs[cfg]([]byte("name = \"x\"\nrogue = 1\n"), RejectUnknownFields(true))
|
||||||
|
if err == nil || !strings.Contains(err.Error(), "unknown field") {
|
||||||
|
t.Errorf("err = %v, want the strict failure", err)
|
||||||
|
}
|
||||||
|
// The statements before the failure stay written, the contract the
|
||||||
|
// targeted path documents and encoding/json follows.
|
||||||
|
if got.Name != "x" {
|
||||||
|
t.Errorf("Name = %q, want the statement before the failure kept", got.Name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestStatementsValueArrays pins that a value array is one statement, a
|
||||||
|
// scalar array and an array of inline tables alike; only an array of tables
|
||||||
|
// yields per element.
|
||||||
|
func TestStatementsValueArrays(t *testing.T) {
|
||||||
|
src := strings.NewReader("port = [8080, 9090]\nmix = [{y = 1, x = 2}]\n[[items]]\nn = 1\n")
|
||||||
|
var got []Statement
|
||||||
|
for stmt, err := range Statements(src) {
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
got = append(got, stmt)
|
||||||
|
}
|
||||||
|
if len(got) != 3 {
|
||||||
|
t.Fatalf("got %d statements, want 3", len(got))
|
||||||
|
}
|
||||||
|
if got[0].Index != -1 || got[0].Table != nil {
|
||||||
|
t.Errorf("port statement = %+v, want one plain key/value", got[0])
|
||||||
|
}
|
||||||
|
if got[1].Index != -1 || got[1].Table != nil {
|
||||||
|
t.Errorf("mix statement = %+v, want one plain key/value", got[1])
|
||||||
|
}
|
||||||
|
if got[2].Index != 0 || got[2].Table == nil {
|
||||||
|
t.Errorf("items statement = %+v, want the element with its node", got[2])
|
||||||
|
}
|
||||||
|
}
|
||||||
+72
-50
@@ -16,8 +16,10 @@
|
|||||||
// doc, err := interpres.Parse(data)
|
// doc, err := interpres.Parse(data)
|
||||||
// tree := doc.Map()
|
// tree := doc.Map()
|
||||||
//
|
//
|
||||||
// A Decoder allows strict decoding that rejects keys without a matching
|
// Strict decoding that rejects keys without a matching struct field is an
|
||||||
// struct field, mirroring the RejectUnknownFields option of encoding/json/v2.
|
// option, mirroring the RejectUnknownMembers option of encoding/json/v2:
|
||||||
|
//
|
||||||
|
// err := interpres.Unmarshal(data, &cfg, interpres.RejectUnknownFields(true))
|
||||||
package interpres
|
package interpres
|
||||||
|
|
||||||
import (
|
import (
|
||||||
@@ -212,7 +214,7 @@ func ParseFile(path string) (*Document, error) {
|
|||||||
|
|
||||||
// Valid reports whether data is a valid TOML document: nil when the parser
|
// Valid reports whether data is a valid TOML document: nil when the parser
|
||||||
// accepts it, and the parse error when it does not. It is the library call
|
// accepts it, and the parse error when it does not. It is the library call
|
||||||
// the -validate mode of interpres-decode is built on, and it reads nothing
|
// the --validate mode of interpres-decode is built on, and it reads nothing
|
||||||
// but the bytes it is given.
|
// but the bytes it is given.
|
||||||
func Valid(data []byte) error {
|
func Valid(data []byte) error {
|
||||||
_, err := ParseMapContext(context.Background(), data)
|
_, err := ParseMapContext(context.Background(), data)
|
||||||
@@ -256,9 +258,6 @@ func parseWithOptions(ctx context.Context, data []byte, opts parseOptions, wantD
|
|||||||
return tree, &Document{root: p.doc, footer: p.footer}, nil
|
return tree, &Document{root: p.doc, footer: p.footer}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Unmarshal parses a TOML document and stores the result in the value pointed
|
|
||||||
// to by v. v is typically a pointer to a struct or to a map[string]any.
|
|
||||||
//
|
|
||||||
// Unmarshal parses a TOML document and stores the result in the value pointed
|
// Unmarshal parses a TOML document and stores the result in the value pointed
|
||||||
// to by v. v is typically a pointer to a struct or to a map[string]any.
|
// to by v. v is typically a pointer to a struct or to a map[string]any.
|
||||||
// Options tune the call; with none, unknown keys are ignored, numbers are
|
// Options tune the call; with none, unknown keys are ignored, numbers are
|
||||||
@@ -266,7 +265,9 @@ func parseWithOptions(ctx context.Context, data []byte, opts parseOptions, wantD
|
|||||||
//
|
//
|
||||||
// Struct fields are matched to TOML keys by the `toml:"name"` tag, or by a
|
// Struct fields are matched to TOML keys by the `toml:"name"` tag, or by a
|
||||||
// case-insensitive match on the field name when no tag is present. A tag of
|
// case-insensitive match on the field name when no tag is present. A tag of
|
||||||
// "-" skips the field.
|
// "-" skips the field. Two document keys that differ only in case and both
|
||||||
|
// match one field resolve deterministically: the lexicographically greater
|
||||||
|
// one wins, the same key winning every run.
|
||||||
//
|
//
|
||||||
// A destination implementing Unmarshaler receives the parsed value as it is,
|
// A destination implementing Unmarshaler receives the parsed value as it is,
|
||||||
// a TOML string fills a destination implementing encoding.TextUnmarshaler, and
|
// a TOML string fills a destination implementing encoding.TextUnmarshaler, and
|
||||||
@@ -281,12 +282,12 @@ func Unmarshal(data []byte, v any, opts ...UnmarshalOption) error {
|
|||||||
// ParseAs decodes a TOML document into T in one call, the generic shorthand
|
// ParseAs decodes a TOML document into T in one call, the generic shorthand
|
||||||
// for Unmarshal with a destination variable:
|
// for Unmarshal with a destination variable:
|
||||||
//
|
//
|
||||||
// cfg, err := interpres.ParseAs[Config](data)
|
// cfg, err := interpres.ParseAs[Config](data, interpres.RejectUnknownFields(true))
|
||||||
//
|
//
|
||||||
// The zero T comes back with the error.
|
// The options are Unmarshal's. The zero T comes back with the error.
|
||||||
func ParseAs[T any](data []byte) (T, error) {
|
func ParseAs[T any](data []byte, opts ...UnmarshalOption) (T, error) {
|
||||||
var v T
|
var v T
|
||||||
err := Unmarshal(data, &v)
|
err := Unmarshal(data, &v, opts...)
|
||||||
return v, err
|
return v, err
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -315,10 +316,19 @@ func UnmarshalContext(ctx context.Context, data []byte, v any, opts ...Unmarshal
|
|||||||
|
|
||||||
// UnmarshalRead reads the document from r and decodes it into v, the
|
// UnmarshalRead reads the document from r and decodes it into v, the
|
||||||
// streaming-shaped entry the json/v2 vocabulary uses. The reader is
|
// streaming-shaped entry the json/v2 vocabulary uses. The reader is
|
||||||
// consumed in full, because the parser scans its source in place; the
|
// consumed in full, because the parser scans its source in place; with
|
||||||
// options and the behaviour are Unmarshal's.
|
// MaxInputSize set, reading stops one byte past the limit so the size the
|
||||||
|
// option bounds is the memory held, not what a reader is drained into first.
|
||||||
|
// The options and the behaviour are Unmarshal's.
|
||||||
func UnmarshalRead(r io.Reader, v any, opts ...UnmarshalOption) error {
|
func UnmarshalRead(r io.Reader, v any, opts ...UnmarshalOption) error {
|
||||||
data, err := io.ReadAll(r)
|
s := settingsFor(opts)
|
||||||
|
var data []byte
|
||||||
|
var err error
|
||||||
|
if s.maxInputSize > 0 {
|
||||||
|
data, err = io.ReadAll(io.LimitReader(r, int64(s.maxInputSize)+1))
|
||||||
|
} else {
|
||||||
|
data, err = io.ReadAll(r)
|
||||||
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("interpres: read: %w", err)
|
return fmt.Errorf("interpres: read: %w", err)
|
||||||
}
|
}
|
||||||
@@ -338,18 +348,19 @@ func UnmarshalRead(r io.Reader, v any, opts ...UnmarshalOption) error {
|
|||||||
// tree path, so every option means the same thing on every document.
|
// tree path, so every option means the same thing on every document.
|
||||||
type UnmarshalOption func(*decodeSettings)
|
type UnmarshalOption func(*decodeSettings)
|
||||||
|
|
||||||
// decodeSettings is the option carrier of one decode call.
|
// decodeSettings is the option carrier of one decode call. The context is
|
||||||
|
// not one: it arrives as its own argument, because every entry point names it
|
||||||
|
// explicitly.
|
||||||
type decodeSettings struct {
|
type decodeSettings struct {
|
||||||
disallowUnknown bool
|
disallowUnknown bool
|
||||||
useNumber bool
|
useNumber bool
|
||||||
maxDepth int
|
maxDepth int
|
||||||
maxInputSize int
|
maxInputSize int
|
||||||
localLoc *time.Location
|
localLoc *time.Location
|
||||||
ctx context.Context
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func settingsFor(opts []UnmarshalOption) *decodeSettings {
|
func settingsFor(opts []UnmarshalOption) *decodeSettings {
|
||||||
s := &decodeSettings{ctx: context.Background()}
|
s := &decodeSettings{}
|
||||||
for _, opt := range opts {
|
for _, opt := range opts {
|
||||||
opt(s)
|
opt(s)
|
||||||
}
|
}
|
||||||
@@ -452,8 +463,8 @@ type Marshaler interface {
|
|||||||
// argument is whatever the parser produced for that key: one of string,
|
// argument is whatever the parser produced for that key: one of string,
|
||||||
// bool, int64, float64, OffsetDateTime, LocalDateTime, LocalDate, LocalTime,
|
// bool, int64, float64, OffsetDateTime, LocalDateTime, LocalDate, LocalTime,
|
||||||
// []any, or map[string]any. A tree built by hand may carry a plain time.Time
|
// []any, or map[string]any. A tree built by hand may carry a plain time.Time
|
||||||
// where the parser would put an OffsetDateTime, and a Decoder configured with
|
// where the parser would put an OffsetDateTime, and NumbersAsLiterals a
|
||||||
// UseNumber a Number.
|
// Number.
|
||||||
//
|
//
|
||||||
// UnmarshalTOML may parse, inspect, or transform the value however it likes,
|
// UnmarshalTOML may parse, inspect, or transform the value however it likes,
|
||||||
// then store the result by mutating its receiver through the standard
|
// then store the result by mutating its receiver through the standard
|
||||||
@@ -461,7 +472,7 @@ type Marshaler interface {
|
|||||||
// reflect.Value.Set or by reassigning fields through a pointer the receiver
|
// reflect.Value.Set or by reassigning fields through a pointer the receiver
|
||||||
// holds).
|
// holds).
|
||||||
//
|
//
|
||||||
// UnmarshalTOML is invoked from (*Decoder).Decode / Unmarshal when the
|
// UnmarshalTOML is invoked from Unmarshal and its siblings when the
|
||||||
// destination type implements the interface. The decoder does not need to
|
// destination type implements the interface. The decoder does not need to
|
||||||
// consult the concrete return value; whatever the receiver stores is kept.
|
// consult the concrete return value; whatever the receiver stores is kept.
|
||||||
//
|
//
|
||||||
@@ -482,16 +493,21 @@ type UnmarshalerContext interface {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Marshal returns the TOML encoding of v. The output is valid TOML 1.1.
|
// Marshal returns the TOML encoding of v. The output is valid TOML 1.1.
|
||||||
|
// Options tune the emission; with none, the layout groups entries by kind,
|
||||||
|
// empty arrays emit and sub-tables take the header form.
|
||||||
//
|
//
|
||||||
// Marshal traverses v using reflection and applies the following rules:
|
// Marshal traverses v using reflection and applies the following rules:
|
||||||
//
|
//
|
||||||
// - The top-level value must be a struct or a map[string]V. Pointers are
|
// - The top-level value must be a struct, a map[string]V or an OrderedMap
|
||||||
// followed; a nil top-level pointer is an error.
|
// (or a non-nil pointer to one). A Document writes itself back, and a nil
|
||||||
|
// one is an error.
|
||||||
// - Struct fields are matched by `toml:"name"` tag (case-insensitive
|
// - Struct fields are matched by `toml:"name"` tag (case-insensitive
|
||||||
// fallback to field name; `-` skips). The tag options `omitzero` (skip
|
// fallback to field name; `-` skips). The tag option `omitzero` skips a
|
||||||
// the zero value of the field's type) and `omitempty` (skip an empty
|
// field holding the zero value of its type (a type with an IsZero method
|
||||||
// slice, array, or map) drop a field from the output on encode; the
|
// decides through it), and `omitempty` skips a value that is empty in
|
||||||
// decoder ignores them. Anonymous (embedded) fields without a tag are
|
// the encoding/json sense: an empty string, a zero number, false, a nil
|
||||||
|
// pointer or interface, and an empty slice, array or map. The decoder
|
||||||
|
// ignores both options. Anonymous (embedded) fields without a tag are
|
||||||
// inlined.
|
// inlined.
|
||||||
// - Maps use sorted keys for deterministic output.
|
// - Maps use sorted keys for deterministic output.
|
||||||
// - Slices and arrays of structs or maps become TOML arrays of tables; a
|
// - Slices and arrays of structs or maps become TOML arrays of tables; a
|
||||||
@@ -502,10 +518,12 @@ type UnmarshalerContext 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
|
||||||
// and OffsetDateTime (offset date-time), and LocalDateTime/LocalDate/
|
// and OffsetDateTime (offset date-time), and LocalDateTime/LocalDate/
|
||||||
// LocalTime (local variants). A date-time writes its seconds only when the value carries
|
// LocalTime (local variants). A date-time writes its seconds only when
|
||||||
// them, and drops the trailing zeros of a fractional second.
|
// the value carries them, and drops the trailing zeros of a fractional
|
||||||
// - A table element of a value array, and a sub-table inlined by
|
// second. A zone offset that is not a whole number of minutes is refused,
|
||||||
// Encoder.InlineTables, is written as an inline table, across lines when it
|
// because TOML has no form that carries its seconds.
|
||||||
|
// - A table element of a value array, and a sub-table the InlineTables
|
||||||
|
// option inlines, is written as an inline table, across lines when it
|
||||||
// does not fit one.
|
// 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.
|
||||||
@@ -516,14 +534,10 @@ type UnmarshalerContext interface {
|
|||||||
//
|
//
|
||||||
// Marshal rejects a value that nests deeper than 10000 levels with an error
|
// Marshal rejects a value that nests deeper than 10000 levels with an error
|
||||||
// naming the limit, so cyclic data is reported instead of running the stack
|
// naming the limit, so cyclic data is reported instead of running the stack
|
||||||
// out. The output is not guaranteed to be byte-identical to
|
// out. The output is not guaranteed to be byte-identical to the input that
|
||||||
// the input that produced v: comments, whitespace, key order (for maps),
|
// produced v: comments, whitespace, key order (for maps), string quoting
|
||||||
// string quoting style, and the choice between `[table]` headers and inline
|
// style, and the choice between `[table]` headers and inline tables are not
|
||||||
// tables are not preserved.
|
// preserved.
|
||||||
//
|
|
||||||
// Marshal returns the TOML encoding of v. Options tune the emission; with
|
|
||||||
// none, the layout groups entries by kind, empty arrays emit and sub-tables
|
|
||||||
// take the header form.
|
|
||||||
//
|
//
|
||||||
// Marshal is equivalent to MarshalContext with context.Background.
|
// Marshal is equivalent to MarshalContext with context.Background.
|
||||||
func Marshal(v any, opts ...MarshalOption) ([]byte, error) {
|
func Marshal(v any, opts ...MarshalOption) ([]byte, error) {
|
||||||
@@ -548,12 +562,13 @@ type Statement struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Statements reads a TOML document from r and returns an iterator over its
|
// Statements reads a TOML document from r and returns an iterator over its
|
||||||
// top-level statements in written order: key/value statements, a [table]
|
// top-level statements in written order: key/value statements, including a
|
||||||
// header as one statement carrying its Table node, and an [[array of
|
// value that is an array or an inline table, a [table] header as one
|
||||||
// tables]] as one statement per element, each with the element's node and
|
// statement carrying its Table node, and an [[array of tables]] as one
|
||||||
// its Index. Iteration stops at the first error, which arrives as the second
|
// statement per element, each with the element's node and its Index.
|
||||||
// value, and at a false yield: a caller that breaks after the statement it
|
// Iteration stops at the first error, which arrives as the second value, and
|
||||||
// wanted reads no further ones.
|
// at a false yield: a caller that breaks after the statement it wanted reads
|
||||||
|
// no further ones.
|
||||||
//
|
//
|
||||||
// The reader is consumed in full before the first statement is yielded,
|
// The reader is consumed in full before the first statement is yielded,
|
||||||
// because the parser scans the source in place; processing the yielded
|
// because the parser scans the source in place; processing the yielded
|
||||||
@@ -572,8 +587,12 @@ func Statements(r io.Reader) iter.Seq2[Statement, error] {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
for _, e := range doc.Root().Entries() {
|
for _, e := range doc.Root().Entries() {
|
||||||
if els := e.Elements(); len(els) > 0 {
|
// Only an array of tables yields per element, the branch the
|
||||||
for i, el := range els {
|
// write side takes too: a value array is one statement whatever
|
||||||
|
// its elements, and an emptied array of tables holds no element
|
||||||
|
// to yield.
|
||||||
|
if _, isTables := e.Value().([]map[string]any); isTables && len(e.Elements()) > 0 {
|
||||||
|
for i, el := range e.Elements() {
|
||||||
if !yield(Statement{Key: e.Key(), Value: e.Value(), Table: el, Index: i}, nil) {
|
if !yield(Statement{Key: e.Key(), Value: e.Value(), Table: el, Index: i}, nil) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -648,14 +667,14 @@ const (
|
|||||||
// interpres.InlineTables(60))
|
// interpres.InlineTables(60))
|
||||||
type MarshalOption func(*encodeSettings)
|
type MarshalOption func(*encodeSettings)
|
||||||
|
|
||||||
// encodeSettings is the option carrier of one encode call.
|
// encodeSettings is the option carrier of one encode call. As on the decode
|
||||||
|
// side, the context arrives as its own argument.
|
||||||
type encodeSettings struct {
|
type encodeSettings struct {
|
||||||
ctx context.Context
|
|
||||||
cfg encodeConfig
|
cfg encodeConfig
|
||||||
}
|
}
|
||||||
|
|
||||||
func settingsForEncode(opts []MarshalOption) *encodeSettings {
|
func settingsForEncode(opts []MarshalOption) *encodeSettings {
|
||||||
s := &encodeSettings{ctx: context.Background(), cfg: encodeConfig{layout: LayoutKindGrouped}}
|
s := &encodeSettings{cfg: encodeConfig{layout: LayoutKindGrouped}}
|
||||||
for _, opt := range opts {
|
for _, opt := range opts {
|
||||||
opt(s)
|
opt(s)
|
||||||
}
|
}
|
||||||
@@ -681,6 +700,7 @@ func (s *encodeSettings) marshal(ctx context.Context, v any) ([]byte, error) {
|
|||||||
// Layout sets the layout the encoder writes a document's entries in:
|
// Layout sets the layout the encoder writes a document's entries in:
|
||||||
// LayoutKindGrouped, the default, reorders them scalars first, then tables,
|
// LayoutKindGrouped, the default, reorders them scalars first, then tables,
|
||||||
// then arrays of tables; LayoutKindDeclaration preserves declaration order.
|
// then arrays of tables; LayoutKindDeclaration preserves declaration order.
|
||||||
|
// A value the two constants do not name behaves as LayoutKindGrouped.
|
||||||
func Layout(kind LayoutKind) MarshalOption {
|
func Layout(kind LayoutKind) MarshalOption {
|
||||||
return func(s *encodeSettings) { s.cfg.layout = kind }
|
return func(s *encodeSettings) { s.cfg.layout = kind }
|
||||||
}
|
}
|
||||||
@@ -727,7 +747,9 @@ func InlineTables(threshold int) MarshalOption {
|
|||||||
// Go doc comments are not visible to reflection, so the tag is the channel
|
// Go doc comments are not visible to reflection, so the tag is the channel
|
||||||
// that carries the text. Off by default, and a field without a `comment=`
|
// that carries the text. Off by default, and a field without a `comment=`
|
||||||
// option prints none. Multi-line comments carry newlines in the tag, each
|
// option prints none. Multi-line comments carry newlines in the tag, each
|
||||||
// line printed with its own "# " marker.
|
// line printed with its own "# " marker. The tag's options separate with
|
||||||
|
// commas, so the comment text itself cannot carry one; the first comma ends
|
||||||
|
// it.
|
||||||
func EmitFieldComments(v bool) MarshalOption {
|
func EmitFieldComments(v bool) MarshalOption {
|
||||||
return func(s *encodeSettings) { s.cfg.emitFieldComments = v }
|
return func(s *encodeSettings) { s.cfg.emitFieldComments = v }
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user