Give the help examples that read no data and the exit codes, say a data path is no directory in one shape, suggest a record of one for a choice, name a JSON syntax error's line and column, and say no data is loaded from FakeStruct
This commit is contained in:
+10
-8
@@ -27,8 +27,9 @@ const usage = `Usage: <template> | COMMAND [flags]
|
||||
|
||||
COMMAND reads one template from stdin and renders it:
|
||||
|
||||
echo 'name: {/sv_SE.person.last}' | COMMAND
|
||||
echo 'id: {int(1,999)}' | COMMAND
|
||||
echo '{"format":"{x}","x":["bosse","lina"]}' | COMMAND
|
||||
COMMAND --list # the paths a {/path} reference reads
|
||||
COMMAND < template.txt
|
||||
|
||||
COMMAND <<'EOF'
|
||||
@@ -36,11 +37,10 @@ born {date(1990-01-01,2010-12-31,'2006-01-02')}
|
||||
EOF
|
||||
|
||||
A template is a format string, or a JSON object, array, string or null. Its {…} tokens
|
||||
reach the data by reference from the root, {/sv_SE.person.last}, whether the data
|
||||
is shipped or layered with --data-path. A reference names a category or a dotted
|
||||
path into one ({/sv_SE.person.last}), a table's row by key or name
|
||||
({/misc.territory[SE]}), and .. steps up from a row to the row it links to
|
||||
({/geo.SE.locality..municipality.name}).
|
||||
reach the data by reference from the root, {/path}, for any path --list prints, whether
|
||||
the data is shipped or layered with --data-path. A reference names a category or a
|
||||
dotted path into one, a table's row by key or name ({/table[KEY]}), and .. steps up
|
||||
from a row to the row it links to.
|
||||
|
||||
Under --format text, the default, what you send is what renders, and COMMAND adds
|
||||
nothing: a format string keeps every byte, so echo's newline ends each render, and
|
||||
@@ -51,7 +51,7 @@ format string like any other, and renders as sent. A quoted heredoc, <<'EOF', pa
|
||||
a backslash or a quote as written.
|
||||
|
||||
With --format json, ndjson, csv or sql the template must be a record — a JSON
|
||||
template whose fields are its columns, or a lone reference such as {/sv_SE.person}:
|
||||
template whose fields are its columns, or a lone reference such as {/path}:
|
||||
one reference and nothing else, with one newline after it allowed — and the rows are
|
||||
written as one JSON array, one JSON object per line, one CSV row (after a header), or
|
||||
one INSERT.
|
||||
@@ -73,6 +73,8 @@ one INSERT.
|
||||
|
||||
A short flag's value attaches or follows (-n3, -n 3); short flags bundle (-hn 3);
|
||||
-- ends the flags.
|
||||
|
||||
Exit codes: 0 success, 1 runtime error, 2 misuse.
|
||||
`
|
||||
|
||||
type invocation struct {
|
||||
@@ -279,7 +281,7 @@ func (in invocation) checkFlags() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func pipeHint(name string) string { return "echo '{/sv_SE.person}' | " + name }
|
||||
func pipeHint(name string) string { return "echo 'id: {int(1,999)}' | " + name }
|
||||
|
||||
// operandError refuses an argument that is no flag, naming the spelling that reads what
|
||||
// it most likely means: dropping it for -, a file for a file's name, else a template
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
package datafiles
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io/fs"
|
||||
"os"
|
||||
@@ -91,15 +92,25 @@ func (s Source) check() error {
|
||||
return fmt.Errorf("a data path is empty")
|
||||
}
|
||||
info, err := os.Stat(s.label)
|
||||
if errors.Is(err, fs.ErrNotExist) {
|
||||
return noDirectory(s.label)
|
||||
}
|
||||
if err != nil {
|
||||
return fmt.Errorf("data path %s: %w", s.label, err)
|
||||
}
|
||||
if !info.IsDir() {
|
||||
return fmt.Errorf("%s is not a directory", s.label)
|
||||
return fmt.Errorf("data path %s: not a directory", s.label)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// noDirectory is a data path naming nothing, which errors.Is still matches as fs.ErrNotExist.
|
||||
type noDirectory string
|
||||
|
||||
func (d noDirectory) Error() string { return "data path " + string(d) + ": no such directory" }
|
||||
|
||||
func (noDirectory) Unwrap() error { return fs.ErrNotExist }
|
||||
|
||||
// walkDir walks the folder that dir names, and reports whether it handed over any category.
|
||||
func (s Source) walkDir(dir []string, compile func(Category) error) (bool, error) {
|
||||
full := s.dirPath(dir)
|
||||
|
||||
@@ -4,7 +4,9 @@ package jsonvalue
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"unicode/utf8"
|
||||
)
|
||||
|
||||
// Value is a JSON value that only Decode, String and Object make, so a template never compiles
|
||||
@@ -15,7 +17,7 @@ type Value struct{ v any }
|
||||
func Decode(b []byte) (Value, error) {
|
||||
var v any
|
||||
if !json.Valid(b) {
|
||||
return Value{}, json.Unmarshal(b, &v)
|
||||
return Value{}, located(b, json.Unmarshal(b, &v))
|
||||
}
|
||||
dec := json.NewDecoder(bytes.NewReader(b))
|
||||
dec.UseNumber()
|
||||
@@ -23,6 +25,18 @@ func Decode(b []byte) (Value, error) {
|
||||
return Value{v}, err
|
||||
}
|
||||
|
||||
// located prefixes a syntax error with the line and column of the byte it stopped at.
|
||||
func located(b []byte, err error) error {
|
||||
var syntax *json.SyntaxError
|
||||
if !errors.As(err, &syntax) {
|
||||
return err
|
||||
}
|
||||
before := b[:max(syntax.Offset-1, 0)]
|
||||
line := bytes.Count(before, []byte("\n")) + 1
|
||||
column := utf8.RuneCount(before[bytes.LastIndexByte(before, '\n')+1:]) + 1
|
||||
return fmt.Errorf("line %d, column %d: %w", line, column, err)
|
||||
}
|
||||
|
||||
// String is the JSON string s.
|
||||
func String(s string) Value { return Value{s} }
|
||||
|
||||
|
||||
@@ -143,7 +143,7 @@ func (f *Generator) FakeRecord(path string) (*Record, error) {
|
||||
return nil, fmt.Errorf("fejkdata: %s descends into %q, a field; only a category-level template is a record", path, tail[0])
|
||||
}
|
||||
record, err := recordOf(n)
|
||||
if errors.Is(err, ErrNoColumns) {
|
||||
if errors.Is(err, ErrNoColumns) || errors.Is(err, errChoiceRecord) {
|
||||
ns := grammar.IdentifierSegments(segments)
|
||||
return nil, fmt.Errorf(`fejkdata: %s %w; render it as a column of one: {"format":"","%s":"{/%s}"}`, path, err, ns[len(ns)-1], path)
|
||||
}
|
||||
@@ -209,10 +209,12 @@ func (f *Generator) FakeRecordTemplate(input string) (*Record, error) {
|
||||
return t.Fake(), nil
|
||||
}
|
||||
|
||||
// ErrNoColumns is the one record fence a path can answer, so its entry point names
|
||||
// the record to write instead.
|
||||
// ErrNoColumns and errChoiceRecord are the record fences a path can answer, so its entry
|
||||
// point names the record to write instead.
|
||||
var ErrNoColumns = errors.New("has no fields, so no columns")
|
||||
|
||||
var errChoiceRecord = errors.New("names a choice, not a template; a record is a template whose fields are its columns")
|
||||
|
||||
// recordOf is the record template n is; settleRecords fixed its columns at load.
|
||||
func recordOf(n node) (*template, error) {
|
||||
if tb, isTable := n.(*table); isTable {
|
||||
@@ -220,7 +222,7 @@ func recordOf(n node) (*template, error) {
|
||||
}
|
||||
t, ok := n.(*template)
|
||||
if !ok {
|
||||
return nil, errors.New("names a choice, not a template; a record is a template whose fields are its columns")
|
||||
return nil, errChoiceRecord
|
||||
}
|
||||
if len(t.fields) == 0 {
|
||||
return nil, ErrNoColumns
|
||||
|
||||
@@ -31,7 +31,7 @@ func (f *Generator) FakeStruct(v any) error {
|
||||
defer f.mu.Unlock()
|
||||
shape, err := f.structShapeOf(p.Elem().Type())
|
||||
if err != nil {
|
||||
return fmt.Errorf("fejkdata: %w", err)
|
||||
return f.noDataNote(fmt.Errorf("fejkdata: %w", err))
|
||||
}
|
||||
shape.fill(f.drawState, p.Elem())
|
||||
return nil
|
||||
|
||||
Reference in New Issue
Block a user