Address reviewer findings: reference hint, consumer-terms type errors, misuse exit code, immutability and parity docs
Tests / vet + fmt + tests (pull_request) Successful in 57s
Tests / vet + fmt + tests (pull_request) Successful in 57s
This commit is contained in:
@@ -44,8 +44,8 @@ fejkdata -n 3 --separator ', ' sv_SE.word # nät, barn, sol
|
|||||||
fejkdata --list # every path the data offers
|
fejkdata --list # every path the data offers
|
||||||
fejkdata --data-path ./mydata sv_SE.word # layer a directory over the shipped data
|
fejkdata --data-path ./mydata sv_SE.word # layer a directory over the shipped data
|
||||||
fejkdata --no-shipped-data -d ./mydata --list # only your data
|
fejkdata --no-shipped-data -d ./mydata --list # only your data
|
||||||
fejkdata 'name: {/sv_SE.person.last}' # name: Eriksson (an inline template)
|
fejkdata 'name: {/sv_SE.person.last}' # name: <a surname> — an inline template
|
||||||
fejkdata '{"format":"name: {x}","x":["bosse","lina"]}' # name: bosse
|
fejkdata '{"format":"name: {x}","x":["bosse","lina"]}' # name: bosse or name: lina
|
||||||
```
|
```
|
||||||
|
|
||||||
A path names a category, or a field inside one: each dot segment descends one
|
A path names a category, or a field inside one: each dot segment descends one
|
||||||
|
|||||||
+20
-5
@@ -25,12 +25,13 @@ const usage = `Usage: fejkdata [flags] <path|template>
|
|||||||
|
|
||||||
<path> a category, or a dotted path into one (person, person.last)
|
<path> a category, or a dotted path into one (person, person.last)
|
||||||
<template> a format string or JSON value to render inline, e.g.
|
<template> a format string or JSON value to render inline, e.g.
|
||||||
'name: {/sv_SE.person.last}' or '{"format":"{x}","x":[1,2]}'
|
'name: {/sv_SE.person.last}' or '{"format":"{x}","x":["bosse","lina"]}'
|
||||||
|
|
||||||
An argument containing a { token, or a JSON object or array, is a template; any
|
An argument containing a { token, or a JSON object or array, is a template; any
|
||||||
other argument is a path (a path never contains a brace). Templates reach the data
|
other argument is a path (a path never contains a brace). Templates reach the data
|
||||||
with a reference: {/sv_SE.person.first} from the root, whether the data is shipped
|
with a reference: {/sv_SE.person.first} from the root, whether the data is shipped
|
||||||
or layered with --data-path.
|
or layered with --data-path. A [ that is not valid JSON names no template and no
|
||||||
|
path.
|
||||||
|
|
||||||
-d, --data-path D a data directory to layer over the shipped data (repeatable; last wins on a clash)
|
-d, --data-path D a data directory to layer over the shipped data (repeatable; last wins on a clash)
|
||||||
-h, --help print this help, then exit
|
-h, --help print this help, then exit
|
||||||
@@ -229,13 +230,16 @@ func (in invocation) options() []fejkdata.Option {
|
|||||||
func (in invocation) write(f *fejkdata.Generator, w io.Writer) error {
|
func (in invocation) write(f *fejkdata.Generator, w io.Writer) error {
|
||||||
arg := in.paths[0]
|
arg := in.paths[0]
|
||||||
var draw func() (string, error)
|
var draw func() (string, error)
|
||||||
if isTemplate(arg) {
|
switch {
|
||||||
|
case isTemplate(arg):
|
||||||
t, err := f.NewTemplate(arg)
|
t, err := f.NewTemplate(arg)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return templateError{err}
|
||||||
}
|
}
|
||||||
draw = func() (string, error) { return t.Fake(), nil }
|
draw = func() (string, error) { return t.Fake(), nil }
|
||||||
} else {
|
case strings.HasPrefix(arg, "["):
|
||||||
|
return templateError{fmt.Errorf("%q starts with [ but is not valid JSON, so it names no template and no path", arg)}
|
||||||
|
default:
|
||||||
draw = func() (string, error) { return f.Fake(arg) }
|
draw = func() (string, error) { return f.Fake(arg) }
|
||||||
}
|
}
|
||||||
out := bufio.NewWriter(w)
|
out := bufio.NewWriter(w)
|
||||||
@@ -253,6 +257,13 @@ func (in invocation) write(f *fejkdata.Generator, w io.Writer) error {
|
|||||||
return out.Flush()
|
return out.Flush()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// templateError marks a render failure that is the argument's own fault — an
|
||||||
|
// inline template that does not compile. run reports it as misuse (exit 2, with a
|
||||||
|
// pointer to --help), unlike an unknown path, which is a runtime error (exit 1).
|
||||||
|
type templateError struct{ error }
|
||||||
|
|
||||||
|
func (e templateError) Unwrap() error { return e.error }
|
||||||
|
|
||||||
// isTemplate reports whether an argument is an inline template rather than a
|
// isTemplate reports whether an argument is an inline template rather than a
|
||||||
// path: a format string carrying a { token, or a JSON object or array. A name may
|
// path: a format string carrying a { token, or a JSON object or array. A name may
|
||||||
// not contain a brace or bracket, so both spellings collide with no path; the [
|
// not contain a brace or bracket, so both spellings collide with no path; the [
|
||||||
@@ -299,6 +310,10 @@ func run(args []string, stdout, stderr io.Writer) int {
|
|||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
if err := in.write(f, stdout); err != nil {
|
if err := in.write(f, stdout); err != nil {
|
||||||
|
var te templateError
|
||||||
|
if errors.As(err, &te) {
|
||||||
|
return misuse(stderr, te.error)
|
||||||
|
}
|
||||||
fmt.Fprintln(stderr, err)
|
fmt.Fprintln(stderr, err)
|
||||||
return 1
|
return 1
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -62,6 +62,9 @@ func loadData(sources []dataSource) (map[string]node, error) {
|
|||||||
if len(root) == 0 {
|
if len(root) == 0 {
|
||||||
return nil, fmt.Errorf("no .json data found")
|
return nil, fmt.Errorf("no .json data found")
|
||||||
}
|
}
|
||||||
|
// The fences here and the scoped ones [Generator.NewTemplate] runs are one
|
||||||
|
// sequence split across two entry points; a new fence lands a twin below and
|
||||||
|
// in NewTemplate (see its comment for the one omission, checkNoCycles).
|
||||||
if err := linkRefs(root); err != nil {
|
if err := linkRefs(root); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|||||||
+2
-1
@@ -39,7 +39,8 @@ var ErrNoData = errors.New("no data: WithoutShippedData needs at least one WithD
|
|||||||
|
|
||||||
// Generator generates fake data from a loaded namespace tree. Create one with [New].
|
// Generator generates fake data from a loaded namespace tree. Create one with [New].
|
||||||
// It is safe for concurrent use; a seeded sequence is reproducible only when drawn
|
// It is safe for concurrent use; a seeded sequence is reproducible only when drawn
|
||||||
// from one goroutine.
|
// from one goroutine. The compiled tree is immutable after [New], and Fake,
|
||||||
|
// NewTemplate and List read it concurrently without a lock.
|
||||||
type Generator struct {
|
type Generator struct {
|
||||||
mu sync.Mutex
|
mu sync.Mutex
|
||||||
rand *session
|
rand *session
|
||||||
|
|||||||
@@ -87,10 +87,24 @@ func compileItem(v any) (node, error) {
|
|||||||
case map[string]any:
|
case map[string]any:
|
||||||
return compileTemplate(v)
|
return compileTemplate(v)
|
||||||
default:
|
default:
|
||||||
return nil, fmt.Errorf("unsupported node type %T", v)
|
return nil, fmt.Errorf("a template value must be a string, a list or an object, not %s", jsonKind(v))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// jsonKind names a JSON value a template cannot hold, in the data format's own
|
||||||
|
// terms rather than the decoding library's.
|
||||||
|
func jsonKind(v any) string {
|
||||||
|
switch v.(type) {
|
||||||
|
case float64:
|
||||||
|
return "a number"
|
||||||
|
case bool:
|
||||||
|
return "a boolean"
|
||||||
|
case nil:
|
||||||
|
return "null"
|
||||||
|
}
|
||||||
|
return fmt.Sprintf("%T", v)
|
||||||
|
}
|
||||||
|
|
||||||
func compileString(s string) (node, error) {
|
func compileString(s string) (node, error) {
|
||||||
if err := checkTokens(s, nil); err != nil {
|
if err := checkTokens(s, nil); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
|||||||
@@ -32,6 +32,9 @@ func (f *Generator) Fake(path string) (string, error) {
|
|||||||
|
|
||||||
// Template is an inline template compiled, referenced and validated against a
|
// Template is an inline template compiled, referenced and validated against a
|
||||||
// generator's loaded data once, ready to render many times with [Template.Fake].
|
// generator's loaded data once, ready to render many times with [Template.Fake].
|
||||||
|
// It is safe for concurrent use: Fake serializes on its generator's lock, so a
|
||||||
|
// seeded sequence is reproducible only when a generator — and its templates — are
|
||||||
|
// drawn from one goroutine.
|
||||||
type Template struct {
|
type Template struct {
|
||||||
g *Generator
|
g *Generator
|
||||||
n node
|
n node
|
||||||
@@ -48,6 +51,10 @@ func (t *Template) Fake() string {
|
|||||||
// binds its references against the loaded tree, so repeated renders pay the
|
// binds its references against the loaded tree, so repeated renders pay the
|
||||||
// compile and validation once. It shares [New]'s guarantees: a bad template errors
|
// compile and validation once. It shares [New]'s guarantees: a bad template errors
|
||||||
// here, and rendering cannot fail.
|
// here, and rendering cannot fail.
|
||||||
|
// NewTemplate runs the same fences loadData does for a category, scoped to one
|
||||||
|
// inline node with everything but checkNoCycles: a reference binds only into the
|
||||||
|
// loaded tree, which has no path into this node, so rendering it cannot reach
|
||||||
|
// itself. A new fence belongs in both places (loadData and here).
|
||||||
func (f *Generator) NewTemplate(input string) (*Template, error) {
|
func (f *Generator) NewTemplate(input string) (*Template, error) {
|
||||||
n, err := compileInput(input)
|
n, err := compileInput(input)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -56,8 +63,6 @@ func (f *Generator) NewTemplate(input string) (*Template, error) {
|
|||||||
if err := linkNodeRefs(n, f.categories); err != nil {
|
if err := linkNodeRefs(n, f.categories); err != nil {
|
||||||
return nil, fmt.Errorf("fejkdata: %w", err)
|
return nil, fmt.Errorf("fejkdata: %w", err)
|
||||||
}
|
}
|
||||||
// No cycle is possible: a reference binds only into the loaded tree, which has
|
|
||||||
// no path into this node, so rendering it cannot reach itself.
|
|
||||||
if err := checkNodeRepeatReach(n); err != nil {
|
if err := checkNodeRepeatReach(n); err != nil {
|
||||||
return nil, fmt.Errorf("fejkdata: %w", err)
|
return nil, fmt.Errorf("fejkdata: %w", err)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -164,6 +164,9 @@ func checkArm(name string, fields map[string]node) error {
|
|||||||
if isOption(a.key) {
|
if isOption(a.key) {
|
||||||
return fmt.Errorf("%q is an option and can never be a field", a.key)
|
return fmt.Errorf("%q is an option and can never be a field", a.key)
|
||||||
}
|
}
|
||||||
|
if len(fields) == 0 {
|
||||||
|
return fmt.Errorf("no field %q; a token names a sibling field, and a bare string has none — write {/%s} to reference the data", a.key, name)
|
||||||
|
}
|
||||||
return fmt.Errorf("no field %q", a.key)
|
return fmt.Errorf("no field %q", a.key)
|
||||||
}
|
}
|
||||||
if err := checkPath(head, a.tail, a.key); err != nil {
|
if err := checkPath(head, a.tail, a.key); err != nil {
|
||||||
|
|||||||
Reference in New Issue
Block a user