Make great documentation goal 1.2, and plan the README's reorganisation and the nested-template gaps #33

Open
lilleman wants to merge 966 commits from worktree-todo-relative-dates-exact-counts into main
5 changed files with 44 additions and 15 deletions
Showing only changes of commit 9671600fff - Show all commits
+10 -8
View File
@@ -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
+12 -1
View File
@@ -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)
+15 -1
View File
@@ -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} }
+6 -4
View File
@@ -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
+1 -1
View File
@@ -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