feat: add ParseFile and Valid
Test / test (push) Successful in 2m33s

Assisted-by: GLM 5.3 Flash
This commit is contained in:
2026-09-21 23:51:58 +02:00
parent ce0c1ebd9d
commit 3cc168f39a
4 changed files with 101 additions and 0 deletions
+5
View File
@@ -46,6 +46,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
(10000 levels, which no hand-written document approaches): a document that
nests arrays or inline tables deeper used to run the stack out and is now
rejected with a `SyntaxError` naming the limit.
- `ParseFile(path)` reads the file and parses it into a `Document`, with the
file name at the front of every error it returns, read failure and parse
failure alike. `Valid(data)` reports whether a document parses, nil on
success and the parse error on failure, the library call the `-validate`
mode of interpres-decode is built on.
- `Decoder.UseNumber()` decodes the integers and floats of the document into
`Number`, which carries the literal the document wrote, so `0x1f`, `1_000`,
`+1.0` and `inf` survive a round trip with their spelling instead of the
+23
View File
@@ -50,6 +50,29 @@ tree, err := interpres.ParseMap([]byte("title = \"x\"\nport = 8080\n"))
The cancellable variant of `ParseMap`.
### `func ParseFile(path string) (*Document, error)`
Reads the file at `path` and parses it into a [Document](#documents), the shape
`Parse` gives. Both a read failure and a parse failure come back with the file
name as their first words, wrapped so `errors.AsType` still reaches the
`SyntaxError` inside a parse failure.
```go
doc, err := interpres.ParseFile("config.toml")
```
### `func Valid(data []byte) error`
Reports whether `data` is a valid TOML document: `nil` when the parser accepts
it, the parse error when it does not. It is the library call the `-validate`
mode of interpres-decode is built on.
```go
if err := interpres.Valid(data); err != nil {
fmt.Println("invalid:", err)
}
```
## Documents
`Parse` returns a `Document`: the value tree together with what a map cannot
+26
View File
@@ -24,6 +24,7 @@ import (
"context"
"errors"
"fmt"
"os"
"slices"
)
@@ -127,6 +128,31 @@ func ParseMapContext(ctx context.Context, data []byte) (map[string]any, error) {
return tree, err
}
// ParseFile reads the TOML document at path and parses it into a Document,
// the shape Parse gives. Every error names the file it came from: a read
// failure and a parse failure alike carry the path as their first words,
// wrapped so errors.AsType still reaches the SyntaxError inside.
func ParseFile(path string) (*Document, error) {
data, err := os.ReadFile(path)
if err != nil {
return nil, fmt.Errorf("%s: %w", path, err)
}
doc, err := Parse(data)
if err != nil {
return nil, fmt.Errorf("%s: %w", path, err)
}
return doc, nil
}
// 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
// the -validate mode of interpres-decode is built on, and it reads nothing
// but the bytes it is given.
func Valid(data []byte) error {
_, err := ParseMapContext(context.Background(), data)
return err
}
// parseOptions bound the work one parse may do and the shape it produces. A
// zero field takes the default.
type parseOptions struct {
+47
View File
@@ -6,6 +6,8 @@ package interpres
import (
"errors"
"math"
"os"
"path/filepath"
"strings"
"testing"
"time"
@@ -726,3 +728,48 @@ func TestParseNestingLimit(t *testing.T) {
t.Errorf("Msg = %q, want it to name the nesting limit", se.Msg)
}
}
func TestParseFile(t *testing.T) {
path := filepath.Join(t.TempDir(), "config.toml")
if err := os.WriteFile(path, []byte("port = 8080\n"), 0o644); err != nil {
t.Fatal(err)
}
doc, err := ParseFile(path)
if err != nil {
t.Fatal(err)
}
if got := doc.Map()["port"]; got != int64(8080) {
t.Errorf("port = %v, want 8080", got)
}
_, err = ParseFile(filepath.Join(t.TempDir(), "missing.toml"))
if err == nil || !strings.Contains(err.Error(), "missing.toml") {
t.Errorf("read error = %v, want it to name the file", err)
}
bad := filepath.Join(t.TempDir(), "broken.toml")
if err := os.WriteFile(bad, []byte("port =\n"), 0o644); err != nil {
t.Fatal(err)
}
_, err = ParseFile(bad)
if err == nil || !strings.Contains(err.Error(), "broken.toml") {
t.Errorf("parse error = %v, want it to name the file", err)
}
s, ok := errors.AsType[*SyntaxError](err)
if !ok || s.Line != 1 {
t.Errorf("parse error = %v, want a SyntaxError with line 1 inside", err)
}
}
func TestValid(t *testing.T) {
if err := Valid([]byte("a = 1\n[t]\nb = 2\n")); err != nil {
t.Errorf("Valid(valid) = %v, want nil", err)
}
err := Valid([]byte("a = \n"))
if err == nil {
t.Fatal("Valid(invalid) = nil, want an error")
}
if _, ok := errors.AsType[*SyntaxError](err); !ok {
t.Errorf("Valid(invalid) = %v, want a SyntaxError", err)
}
}