diff --git a/CHANGELOG.md b/CHANGELOG.md index 68f409f..3cd9903 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/docs/API.md b/docs/API.md index 60fcf3a..1d0bb70 100644 --- a/docs/API.md +++ b/docs/API.md @@ -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 diff --git a/interpres.go b/interpres.go index e04dd3d..2ca543b 100644 --- a/interpres.go +++ b/interpres.go @@ -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 { diff --git a/interpres_test.go b/interpres_test.go index c1d0093..3aed974 100644 --- a/interpres_test.go +++ b/interpres_test.go @@ -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) + } +}