feat: parse into a Document that keeps order and comments
Test / test (push) Successful in 1m34s

Assisted-by: DeepSeek V4.1 Flash
This commit is contained in:
2026-09-20 10:40:57 +02:00
parent a6e3e3fe31
commit 72f8b21ac4
16 changed files with 939 additions and 119 deletions
+49 -20
View File
@@ -11,9 +11,10 @@
//
// out, err := interpres.Marshal(cfg)
//
// or, for an untyped tree:
// or, for the document with its key order and comments:
//
// tree, err := interpres.Parse(data)
// doc, err := interpres.Parse(data)
// tree := doc.Map()
//
// A Decoder allows strict decoding that rejects keys without a matching
// struct field, mirroring (*json.Decoder).DisallowUnknownFields.
@@ -89,22 +90,41 @@ func (e *EncodeError) Error() string { return "interpres: " + e.Path + ": " + e.
// Unwrap returns the failure the path points at.
func (e *EncodeError) Unwrap() error { return e.Err }
// Parse decodes a TOML document into a nested map[string]any.
// Parse decodes a TOML document into a Document: the values, the order the
// keys were written in, whether a table was written inline, and the comments.
// ParseMap gives the plain value tree instead.
//
// Values are mapped to Go types as follows: strings to string, integers to
// int64, floats to float64, booleans to bool, date-times to time.Time, arrays
// to []any, and tables (including inline tables) to map[string]any.
// int64, floats to float64, booleans to bool, offset date-times to
// OffsetDateTime, the local date-time kinds to their wrappers, arrays to
// []any, and tables (including inline tables) to map[string]any.
//
// Parse is equivalent to ParseContext with context.Background.
func Parse(data []byte) (map[string]any, error) {
func Parse(data []byte) (*Document, error) {
return ParseContext(context.Background(), data)
}
// ParseContext decodes a TOML document into a nested map[string]any, obeying
// ctx. The context is checked between top-level statements so cancellation is
// honoured before the parser has done substantial work.
func ParseContext(ctx context.Context, data []byte) (map[string]any, error) {
return parseWithOptions(ctx, data, parseOptions{})
// ParseContext decodes a TOML document into a Document, obeying ctx. The
// context is checked between top-level statements so cancellation is honoured
// before the parser has done substantial work.
func ParseContext(ctx context.Context, data []byte) (*Document, error) {
_, doc, err := parseWithOptions(ctx, data, parseOptions{}, true)
return doc, err
}
// ParseMap decodes a TOML document into a nested map[string]any, the value
// tree without the order and the comments a Document carries. It is the shape
// this package parsed into before [Document] existed.
//
// ParseMap is equivalent to ParseMapContext with context.Background.
func ParseMap(data []byte) (map[string]any, error) {
return ParseMapContext(context.Background(), data)
}
// ParseMapContext is the cancellable variant of ParseMap.
func ParseMapContext(ctx context.Context, data []byte) (map[string]any, error) {
tree, _, err := parseWithOptions(ctx, data, parseOptions{}, false)
return tree, err
}
// parseOptions bound the work one parse may do. A zero field takes the
@@ -114,15 +134,17 @@ type parseOptions struct {
maxInputSize int
}
func parseWithOptions(ctx context.Context, data []byte, opts parseOptions) (map[string]any, error) {
// parseWithOptions parses data, building the node tree of a Document when
// wantDoc asks for it, and returns both the value tree and that document.
func parseWithOptions(ctx context.Context, data []byte, opts parseOptions, wantDoc bool) (map[string]any, *Document, error) {
if err := ctx.Err(); err != nil {
return nil, err
return nil, nil, err
}
if opts.maxInputSize > 0 && len(data) > opts.maxInputSize {
return nil, fmt.Errorf("interpres: input is %d bytes, over the limit of %d", len(data), opts.maxInputSize)
return nil, nil, fmt.Errorf("interpres: input is %d bytes, over the limit of %d", len(data), opts.maxInputSize)
}
if !utf8.Valid(data) {
return nil, &SyntaxError{Line: 1, Msg: "input is not valid UTF-8"}
return nil, nil, &SyntaxError{Line: 1, Msg: "input is not valid UTF-8"}
}
maxDepth := opts.maxDepth
if maxDepth <= 0 {
@@ -130,8 +152,15 @@ func parseWithOptions(ctx context.Context, data []byte, opts parseOptions) (map[
}
// The parser scans data in place; it only reads the buffer, and every
// string it stores in the tree is copied out of it.
p := &parser{src: data, line: 1, ctx: ctx, maxDepth: maxDepth}
return p.parse()
p := &parser{src: data, line: 1, ctx: ctx, maxDepth: maxDepth, wantDoc: wantDoc}
tree, err := p.parse()
if err != nil {
return nil, nil, err
}
if !wantDoc {
return tree, nil, nil
}
return tree, &Document{root: p.doc, footer: p.footer}, nil
}
// Unmarshal parses a TOML document and stores the result in the value pointed
@@ -153,7 +182,7 @@ func Unmarshal(data []byte, v any) error {
// UnmarshalContext is the cancellable variant of Unmarshal.
func UnmarshalContext(ctx context.Context, data []byte, v any) error {
tree, err := ParseContext(ctx, data)
tree, err := ParseMapContext(ctx, data)
if err != nil {
return err
}
@@ -210,10 +239,10 @@ func (d *Decoder) Decode(data []byte, v any) error {
// DecodeContext is the cancellable variant of Decode.
func (d *Decoder) DecodeContext(ctx context.Context, data []byte, v any) error {
tree, err := parseWithOptions(ctx, data, parseOptions{
tree, _, err := parseWithOptions(ctx, data, parseOptions{
maxDepth: d.maxDepth,
maxInputSize: d.maxInputSize,
})
}, false)
if err != nil {
return err
}