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 (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 nests arrays or inline tables deeper used to run the stack out and is now
rejected with a `SyntaxError` naming the limit. 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 - `Decoder.UseNumber()` decodes the integers and floats of the document into
`Number`, which carries the literal the document wrote, so `0x1f`, `1_000`, `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 `+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`. 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 ## Documents
`Parse` returns a `Document`: the value tree together with what a map cannot `Parse` returns a `Document`: the value tree together with what a map cannot
+26
View File
@@ -24,6 +24,7 @@ import (
"context" "context"
"errors" "errors"
"fmt" "fmt"
"os"
"slices" "slices"
) )
@@ -127,6 +128,31 @@ func ParseMapContext(ctx context.Context, data []byte) (map[string]any, error) {
return tree, err 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 // parseOptions bound the work one parse may do and the shape it produces. A
// zero field takes the default. // zero field takes the default.
type parseOptions struct { type parseOptions struct {
+47
View File
@@ -6,6 +6,8 @@ package interpres
import ( import (
"errors" "errors"
"math" "math"
"os"
"path/filepath"
"strings" "strings"
"testing" "testing"
"time" "time"
@@ -726,3 +728,48 @@ func TestParseNestingLimit(t *testing.T) {
t.Errorf("Msg = %q, want it to name the nesting limit", se.Msg) 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)
}
}