Make great documentation goal 1.2, and plan the README's reorganisation and the nested-template gaps #33
+10
-8
@@ -27,8 +27,9 @@ const usage = `Usage: <template> | COMMAND [flags]
|
|||||||
|
|
||||||
COMMAND reads one template from stdin and renders it:
|
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
|
echo '{"format":"{x}","x":["bosse","lina"]}' | COMMAND
|
||||||
|
COMMAND --list # the paths a {/path} reference reads
|
||||||
COMMAND < template.txt
|
COMMAND < template.txt
|
||||||
|
|
||||||
COMMAND <<'EOF'
|
COMMAND <<'EOF'
|
||||||
@@ -36,11 +37,10 @@ born {date(1990-01-01,2010-12-31,'2006-01-02')}
|
|||||||
EOF
|
EOF
|
||||||
|
|
||||||
A template is a format string, or a JSON object, array, string or null. Its {…} tokens
|
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
|
reach the data by reference from the root, {/path}, for any path --list prints, whether
|
||||||
is shipped or layered with --data-path. A reference names a category or a dotted
|
the data is shipped or layered with --data-path. A reference names a category or a
|
||||||
path into one ({/sv_SE.person.last}), a table's row by key or name
|
dotted path into one, a table's row by key or name ({/table[KEY]}), and .. steps up
|
||||||
({/misc.territory[SE]}), and .. steps up from a row to the row it links to
|
from a row to the row it links to.
|
||||||
({/geo.SE.locality..municipality.name}).
|
|
||||||
|
|
||||||
Under --format text, the default, what you send is what renders, and COMMAND adds
|
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
|
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.
|
a backslash or a quote as written.
|
||||||
|
|
||||||
With --format json, ndjson, csv or sql the template must be a record — a JSON
|
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
|
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
|
written as one JSON array, one JSON object per line, one CSV row (after a header), or
|
||||||
one INSERT.
|
one INSERT.
|
||||||
@@ -73,6 +73,8 @@ one INSERT.
|
|||||||
|
|
||||||
A short flag's value attaches or follows (-n3, -n 3); short flags bundle (-hn 3);
|
A short flag's value attaches or follows (-n3, -n 3); short flags bundle (-hn 3);
|
||||||
-- ends the flags.
|
-- ends the flags.
|
||||||
|
|
||||||
|
Exit codes: 0 success, 1 runtime error, 2 misuse.
|
||||||
`
|
`
|
||||||
|
|
||||||
type invocation struct {
|
type invocation struct {
|
||||||
@@ -279,7 +281,7 @@ func (in invocation) checkFlags() error {
|
|||||||
return nil
|
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
|
// 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
|
// it most likely means: dropping it for -, a file for a file's name, else a template
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
package datafiles
|
package datafiles
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io/fs"
|
"io/fs"
|
||||||
"os"
|
"os"
|
||||||
@@ -91,15 +92,25 @@ func (s Source) check() error {
|
|||||||
return fmt.Errorf("a data path is empty")
|
return fmt.Errorf("a data path is empty")
|
||||||
}
|
}
|
||||||
info, err := os.Stat(s.label)
|
info, err := os.Stat(s.label)
|
||||||
|
if errors.Is(err, fs.ErrNotExist) {
|
||||||
|
return noDirectory(s.label)
|
||||||
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("data path %s: %w", s.label, err)
|
return fmt.Errorf("data path %s: %w", s.label, err)
|
||||||
}
|
}
|
||||||
if !info.IsDir() {
|
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
|
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.
|
// 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) {
|
func (s Source) walkDir(dir []string, compile func(Category) error) (bool, error) {
|
||||||
full := s.dirPath(dir)
|
full := s.dirPath(dir)
|
||||||
|
|||||||
@@ -4,7 +4,9 @@ package jsonvalue
|
|||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"unicode/utf8"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Value is a JSON value that only Decode, String and Object make, so a template never compiles
|
// 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) {
|
func Decode(b []byte) (Value, error) {
|
||||||
var v any
|
var v any
|
||||||
if !json.Valid(b) {
|
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 := json.NewDecoder(bytes.NewReader(b))
|
||||||
dec.UseNumber()
|
dec.UseNumber()
|
||||||
@@ -23,6 +25,18 @@ func Decode(b []byte) (Value, error) {
|
|||||||
return Value{v}, err
|
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.
|
// String is the JSON string s.
|
||||||
func String(s string) Value { return Value{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])
|
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)
|
record, err := recordOf(n)
|
||||||
if errors.Is(err, ErrNoColumns) {
|
if errors.Is(err, ErrNoColumns) || errors.Is(err, errChoiceRecord) {
|
||||||
ns := grammar.IdentifierSegments(segments)
|
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)
|
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
|
return t.Fake(), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// ErrNoColumns is the one record fence a path can answer, so its entry point names
|
// ErrNoColumns and errChoiceRecord are the record fences a path can answer, so its entry
|
||||||
// the record to write instead.
|
// point names the record to write instead.
|
||||||
var ErrNoColumns = errors.New("has no fields, so no columns")
|
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.
|
// recordOf is the record template n is; settleRecords fixed its columns at load.
|
||||||
func recordOf(n node) (*template, error) {
|
func recordOf(n node) (*template, error) {
|
||||||
if tb, isTable := n.(*table); isTable {
|
if tb, isTable := n.(*table); isTable {
|
||||||
@@ -220,7 +222,7 @@ func recordOf(n node) (*template, error) {
|
|||||||
}
|
}
|
||||||
t, ok := n.(*template)
|
t, ok := n.(*template)
|
||||||
if !ok {
|
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 {
|
if len(t.fields) == 0 {
|
||||||
return nil, ErrNoColumns
|
return nil, ErrNoColumns
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ func (f *Generator) FakeStruct(v any) error {
|
|||||||
defer f.mu.Unlock()
|
defer f.mu.Unlock()
|
||||||
shape, err := f.structShapeOf(p.Elem().Type())
|
shape, err := f.structShapeOf(p.Elem().Type())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("fejkdata: %w", err)
|
return f.noDataNote(fmt.Errorf("fejkdata: %w", err))
|
||||||
}
|
}
|
||||||
shape.fill(f.drawState, p.Elem())
|
shape.fill(f.drawState, p.Elem())
|
||||||
return nil
|
return nil
|
||||||
|
|||||||
Reference in New Issue
Block a user