Struct-filling #14
@@ -30,9 +30,10 @@ a JSON object, array or string, or that carries a `{` token, is instead an
|
|||||||
spot. Its tokens reach the data by reference from the root —
|
spot. Its tokens reach the data by reference from the root —
|
||||||
`{/sv_SE.person.last}`, so shipped and `--data-path` categories are alike
|
`{/sv_SE.person.last}`, so shipped and `--data-path` categories are alike
|
||||||
available. An inline template sits in no folder, so the folder-relative `{.name}`
|
available. An inline template sits in no folder, so the folder-relative `{.name}`
|
||||||
and `{..name}` are rejected naming the root spelling. A path never contains a
|
and `{..name}` are rejected naming the root spelling, and one reference alone —
|
||||||
brace, a bracket or a quote, so the two cannot collide (see
|
`{/sv_SE.person}` — is the path written as a template, rejected naming the path, as is
|
||||||
[Decisions](#decisions)).
|
a path written `/sv_SE.person`. A path never contains a brace, a bracket or a quote,
|
||||||
|
so the two cannot collide (see [Decisions](#decisions)).
|
||||||
|
|
||||||
| Flag | |
|
| Flag | |
|
||||||
|------|--|
|
|------|--|
|
||||||
@@ -155,6 +156,8 @@ v = t.Fake() // render many times,
|
|||||||
r, err := f.FakeRecord("users") // one record: each field a column
|
r, err := f.FakeRecord("users") // one record: each field a column
|
||||||
s := r.JSON() // {"first":"Ada","last":"Lovelace"}
|
s := r.JSON() // {"first":"Ada","last":"Lovelace"}
|
||||||
r, err = f.FakeRecordTemplate(`{"format":"{x}","x":["a","b"]}`) // compile + render inline
|
r, err = f.FakeRecordTemplate(`{"format":"{x}","x":["a","b"]}`) // compile + render inline
|
||||||
|
err = f.FakeStruct(&user) // fill a struct's fake:"…" tagged fields
|
||||||
|
ok, err := fejkdata.IsTemplate(arg) // an inline template by its shape, else a path
|
||||||
```
|
```
|
||||||
|
|
||||||
| Option | |
|
| Option | |
|
||||||
@@ -170,6 +173,31 @@ A `*Record` carries its columns via `Columns()` — each a `Column` of `Name`,
|
|||||||
CLI's `--format` writes. `FakeRecord` and `FakeRecordTemplate` take a record; a
|
CLI's `--format` writes. `FakeRecord` and `FakeRecordTemplate` take a record; a
|
||||||
path or template that is not one — a bare string, a choice, or a folder — errors.
|
path or template that is not one — a bare string, a choice, or a folder — errors.
|
||||||
|
|
||||||
|
```go
|
||||||
|
type User struct {
|
||||||
|
ID int64 `fake:"{seq()}"`
|
||||||
|
Last string `fake:"sv_SE.person.last"`
|
||||||
|
Age uint8 `fake:"{int(18,99)}"`
|
||||||
|
Nick *string `fake:"[null, \"{/sv_SE.username}\"]"`
|
||||||
|
Home Address // filled from Address's own tags
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
`FakeStruct` fills a struct through a pointer: each exported field tagged `fake:"…"` is
|
||||||
|
a column of one record, its tag a path or an inline template — told apart by
|
||||||
|
`IsTemplate`, as the CLI tells an argument — and its Go type the column's
|
||||||
|
[datatype](#datatype): a string, bool, integer or float kind, or a pointer to one,
|
||||||
|
which a [`null`](#null) item leaves nil. An integer stays within int64 whatever its
|
||||||
|
kind, and a value the kind cannot hold, such as `{int(0,300)}` in a `uint8`, is refused
|
||||||
|
naming a kind that holds it. The fields an embedded struct promotes are columns of the
|
||||||
|
same record; a named struct field, or a pointer to one, fills from its own tags as a
|
||||||
|
record of its own, so its references draw apart from its parent's. `fake:"-"` leaves a
|
||||||
|
struct field, embedded or named, or a pointer to one, unfilled. Untagged fields keep
|
||||||
|
their values, and so does a pointer back to a struct already being filled; a type
|
||||||
|
whose fields reach more than 1024 structs is refused, naming `fake:"-"` to cut it. The
|
||||||
|
first call for a type compiles its tags and reports what they get wrong, with the same
|
||||||
|
error on every later call; a `datatype` in a tag names the Go type that already sets it.
|
||||||
|
|
||||||
A `*Generator` is safe for concurrent use; a seeded sequence is reproducible only
|
A `*Generator` is safe for concurrent use; a seeded sequence is reproducible only
|
||||||
when drawn from one goroutine. Changing how a value is composed shifts the seeded
|
when drawn from one goroutine. Changing how a value is composed shifts the seeded
|
||||||
stream for that value and everything drawn after it.
|
stream for that value and everything drawn after it.
|
||||||
@@ -182,8 +210,8 @@ work with no data on disk. A directory is a namespace: each JSON file is a
|
|||||||
category named after the file, each subdirectory a dot-path segment, so
|
category named after the file, each subdirectory a dot-path segment, so
|
||||||
`mydata/sv_SE/person.json` is `sv_SE.person` and replaces the shipped one.
|
`mydata/sv_SE/person.json` is `sv_SE.person` and replaces the shipped one.
|
||||||
Sources merge in order; matching folders combine, any other clash is won by the
|
Sources merge in order; matching folders combine, any other clash is won by the
|
||||||
last loaded. Names may not use `.`, `|`, `(`, `{`, `}`, `[`, `]`, `"` or `/`;
|
last loaded. Names may not use `.`, `|`, `(`, `{`, `}`, `[`, `]`, `"` or `/`, nor be
|
||||||
dot-prefixed entries are skipped, so a data directory can also be a checkout.
|
`-`, which a struct tag reserves; dot-prefixed entries are skipped, so a data directory can also be a checkout.
|
||||||
|
|
||||||
Each locale carries `address`, `color`, `company`, `date`, `email`, `ip`,
|
Each locale carries `address`, `color`, `company`, `date`, `email`, `ip`,
|
||||||
`person`, `phone`, `price`, `sentence`, `ssn`, `time`, `url`, `username`,
|
`person`, `phone`, `price`, `sentence`, `ssn`, `time`, `url`, `username`,
|
||||||
@@ -473,7 +501,8 @@ tokens add cost in proportion to the output.
|
|||||||
one is a load error naming the right one.
|
one is a load error naming the right one.
|
||||||
3. **Every mistake is a load error** — `New` rejects the data and `NewTemplate`
|
3. **Every mistake is a load error** — `New` rejects the data and `NewTemplate`
|
||||||
the inline template; on a loaded generator `Fake` fails only for an unknown
|
the inline template; on a loaded generator `Fake` fails only for an unknown
|
||||||
path, and `Template.Fake` cannot fail at all.
|
path, `FakeStruct` only for a non-struct argument or a type its tags do not
|
||||||
|
describe, with the same error every call, and `Template.Fake` cannot fail at all.
|
||||||
4. **Zero to a value in one command** — `go install`, then `fejkdata sv_SE.person`:
|
4. **Zero to a value in one command** — `go install`, then `fejkdata sv_SE.person`:
|
||||||
no checkout, no flag. Flags are GNU-form (`--seed 42`, `-n 3`) in any position;
|
no checkout, no flag. Flags are GNU-form (`--seed 42`, `-n 3`) in any position;
|
||||||
the first custom template needs no escape and no option.
|
the first custom template needs no escape and no option.
|
||||||
@@ -510,7 +539,10 @@ tokens add cost in proportion to the output.
|
|||||||
whole — though only a leading one could collide — keeps one simple name rule
|
whole — though only a leading one could collide — keeps one simple name rule
|
||||||
instead of a leading-position special case. The JSON string is what makes the
|
instead of a leading-position special case. The JSON string is what makes the
|
||||||
library's own advice reachable: the error for an object holding only a format
|
library's own advice reachable: the error for an object holding only a format
|
||||||
names `"…"`, and that spelling has to work where it is printed.
|
names `"…"`, and that spelling has to work where it is printed. An argument or struct
|
||||||
|
tag of one reference alone, `{/users}`, is refused naming the path `users`: both
|
||||||
|
render the same text, and only the path names a record. `IsTemplate` exports the
|
||||||
|
rule, so the CLI, struct tags and any other caller read one.
|
||||||
- **An inline template skips the cycle fence, and only that one.** `New` proves the
|
- **An inline template skips the cycle fence, and only that one.** `New` proves the
|
||||||
loaded tree acyclic, an inline node is a finite tree of its own, and nothing in
|
loaded tree acyclic, an inline node is a finite tree of its own, and nothing in
|
||||||
the tree can reference it, so no render of it reaches itself. Every other fence
|
the tree can reference it, so no render of it reaches itself. Every other fence
|
||||||
@@ -579,13 +611,27 @@ tokens add cost in proportion to the output.
|
|||||||
- **Samples say what they emit, transforms what they do.** `{upper(2)}` is two
|
- **Samples say what they emit, transforms what they do.** `{upper(2)}` is two
|
||||||
letters, `{uppercase(x)}` is `x` upper-cased; one name for both would turn on
|
letters, `{uppercase(x)}` is `x` upper-cased; one name for both would turn on
|
||||||
whether the argument looks like a number.
|
whether the argument looks like a number.
|
||||||
- **A record is a template seen as columns, not a second schema format.** A
|
- **A record is a template seen as columns; a Go struct is the one second schema.** A
|
||||||
template's `format` composes its fields into one string; `FakeRecord` and
|
template's `format` composes its fields into one string; `FakeRecord` and
|
||||||
`--format` project the same fields as columns. Two views of one dataset, so a
|
`--format` project the same fields as columns. Two views of one dataset, so a
|
||||||
record author writes the same JSON they already know, and a column is the same
|
record author writes the same JSON they already know, and a column is the same
|
||||||
field `Fake` renders by dotted path. The `format` is inert to a record — a
|
field `Fake` renders by dotted path. The `format` is inert to a record — a
|
||||||
record-only template writes `"format": ""` — but it is compiled and fenced, so
|
record-only template writes `"format": ""` — but it is compiled and fenced, so
|
||||||
a template that loads renders as whichever shape is asked for.
|
a template that loads renders as whichever shape is asked for. `FakeStruct` takes
|
||||||
|
its columns from a struct instead, because a Go caller has already written that
|
||||||
|
schema: the fields name the columns and their types are the datatypes, so a tag
|
||||||
|
says only what to draw, and a `datatype` in it would be a second spelling of the
|
||||||
|
type.
|
||||||
|
- **A struct's records follow Go's field access, and compile on first use.** The
|
||||||
|
fields an embedded struct promotes are the struct's own — `e.First`, as
|
||||||
|
`encoding/json` and SQL mappers read them — so they are columns of its record and
|
||||||
|
share its draws; a tagged field that another field hides is refused, not dropped. A
|
||||||
|
named struct field is another entity and a record of its own. `fake:"-"` leaves a
|
||||||
|
struct field, embedded or named, unfilled, so no name may be `-`; a pointer back to
|
||||||
|
a struct already being filled is left alone, since filling it would never end. `New` cannot
|
||||||
|
see a caller's types, so the first `FakeStruct` for a type compiles its tags and the
|
||||||
|
answer, error included, is kept per type: a test's first call is its load, and no
|
||||||
|
`NewStruct` handle is needed, as the cache already compiles once.
|
||||||
- **A record shares one reference draw per category.** Two columns that reference
|
- **A record shares one reference draw per category.** Two columns that reference
|
||||||
one category — `{/currency.code}` beside `{/currency.symbol}` — read one draw of
|
one category — `{/currency.code}` beside `{/currency.symbol}` — read one draw of
|
||||||
it, so a record's facts agree the way a template's [correlated
|
it, so a record's facts agree the way a template's [correlated
|
||||||
@@ -674,7 +720,8 @@ node.go the node model and JSON -> node compilation
|
|||||||
path.go the dotted-path walk, and proving a path resolves
|
path.go the dotted-path walk, and proving a path resolves
|
||||||
render.go Fake and the recursive renderer (choices, format strings, expansions)
|
render.go Fake and the recursive renderer (choices, format strings, expansions)
|
||||||
record.go records: Record, the JSON/CSV/SQL serializers, and their entry points
|
record.go records: Record, the JSON/CSV/SQL serializers, and their entry points
|
||||||
inline.go inline templates: Template, NewTemplate, FakeTemplate, and their compile and link
|
struct.go structs: FakeStruct, fake tags, and a field's Go type as its column's datatype
|
||||||
|
inline.go inline templates: Template, NewTemplate, FakeTemplate, IsTemplate, and their compile and link
|
||||||
template.go the {token} grammar: scanning, tokens, operands, validation, compiling a format
|
template.go the {token} grammar: scanning, tokens, operands, validation, compiling a format
|
||||||
hold.go the hold: one draw per expansion for paths and operands, and its fences
|
hold.go the hold: one draw per expansion for paths and operands, and its fences
|
||||||
reference.go reference sigils, and binding references across the tree
|
reference.go reference sigils, and binding references across the tree
|
||||||
|
|||||||
+4
-12
@@ -9,7 +9,6 @@ package main
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"bufio"
|
"bufio"
|
||||||
"encoding/json"
|
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
@@ -415,20 +414,13 @@ const (
|
|||||||
argTemplate
|
argTemplate
|
||||||
)
|
)
|
||||||
|
|
||||||
// classify reads what a positional argument names by its shape: a { token, or a
|
// classify reads what a positional argument names by its shape (see fejkdata.IsTemplate).
|
||||||
// JSON object, array or string, is an inline template; anything else is a path.
|
|
||||||
func classify(arg string) (argKind, error) {
|
func classify(arg string) (argKind, error) {
|
||||||
if strings.ContainsRune(arg, '{') || (isJSONStart(strings.TrimSpace(arg)) && json.Valid([]byte(arg))) {
|
inline, err := fejkdata.IsTemplate(arg)
|
||||||
|
if inline {
|
||||||
return argTemplate, nil
|
return argTemplate, nil
|
||||||
}
|
}
|
||||||
if i := strings.IndexAny(arg, `[]}"`); i >= 0 {
|
return argPath, err
|
||||||
return argPath, fmt.Errorf("%q holds a %q, which no path may, and it is not valid JSON, so it names no template either", arg, arg[i:i+1])
|
|
||||||
}
|
|
||||||
return argPath, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func isJSONStart(arg string) bool {
|
|
||||||
return strings.HasPrefix(arg, "[") || strings.HasPrefix(arg, `"`)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func main() { os.Exit(run(os.Args[1:], os.Stdout, os.Stderr)) }
|
func main() { os.Exit(run(os.Args[1:], os.Stdout, os.Stderr)) }
|
||||||
|
|||||||
+14
-33
@@ -305,42 +305,22 @@ func TestRunShippedDataByDefault(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestClassify(t *testing.T) {
|
func TestClassify(t *testing.T) {
|
||||||
for arg, want := range map[string]argKind{
|
for arg, want := range map[string]argKind{"sv_SE.person": argPath, "name: {x}": argTemplate} {
|
||||||
"sv_SE.person": argPath,
|
if got, err := classify(arg); err != nil || got != want {
|
||||||
"person.last": argPath,
|
|
||||||
"name: {x}": argTemplate, // a { token: a path can never carry a brace
|
|
||||||
`{"format":"x"}`: argTemplate,
|
|
||||||
`["a","b"]`: argTemplate, // a JSON array carries no brace
|
|
||||||
`[1, 2]`: argTemplate,
|
|
||||||
` ["a","b"]`: argTemplate, // padding is the template's own error, not a shape verdict
|
|
||||||
`"hello"`: argTemplate, // a JSON string, the spelling a format-only object names
|
|
||||||
} {
|
|
||||||
got, err := classify(arg)
|
|
||||||
if err != nil || got != want {
|
|
||||||
t.Errorf("classify(%q) = %v, %v; want %v", arg, got, err, want)
|
t.Errorf("classify(%q) = %v, %v; want %v", arg, got, err, want)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
for arg, want := range map[string]string{
|
if _, err := classify("[abc]"); err == nil || !strings.Contains(err.Error(), `holds a "["`) {
|
||||||
"[abc]": `holds a "["`,
|
t.Errorf("classify([abc]) = %v; want it rejected naming the bracket", err)
|
||||||
"[abc].field": `holds a "["`,
|
|
||||||
"x[1]": `holds a "["`,
|
|
||||||
"a]b": `holds a "]"`,
|
|
||||||
"a}b": `holds a "}"`,
|
|
||||||
`"abc`: `holds a "\""`,
|
|
||||||
`"a]b`: `holds a "\""`, // the opener the reader typed, not the bracket behind it
|
|
||||||
} {
|
|
||||||
_, err := classify(arg)
|
|
||||||
if err == nil || !strings.Contains(err.Error(), want) {
|
|
||||||
t.Errorf("classify(%q) = %v; want it rejected naming %s", arg, err, want)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestUsageReferencesResolve(t *testing.T) {
|
func TestUsageReferencesResolve(t *testing.T) {
|
||||||
for _, token := range regexp.MustCompile(`\{/[^}]+\}`).FindAllString(usage, -1) {
|
for _, token := range regexp.MustCompile(`\{/[^}]+\}`).FindAllString(usage, -1) {
|
||||||
code, out, errb := runOut("--seed", "1", token)
|
path := token[2 : len(token)-1]
|
||||||
|
code, out, errb := runOut("--seed", "1", path)
|
||||||
if code != 0 || strings.TrimSpace(out) == "" {
|
if code != 0 || strings.TrimSpace(out) == "" {
|
||||||
t.Errorf("usage advertises %s: run = %d, %q, stderr %q", token, code, out, errb)
|
t.Errorf("usage advertises %s: run %s = %d, %q, stderr %q", token, path, code, out, errb)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -373,12 +353,13 @@ func TestRunInlineTemplate(t *testing.T) {
|
|||||||
|
|
||||||
func TestRunTemplateMisuse(t *testing.T) {
|
func TestRunTemplateMisuse(t *testing.T) {
|
||||||
for arg, want := range map[string]string{
|
for arg, want := range map[string]string{
|
||||||
"{bad": "unterminated",
|
"{bad": "unterminated",
|
||||||
"[red,green]": "names no template either",
|
"[red,green]": "names no template either",
|
||||||
`{"format":"x"}`: "is a string",
|
`{"format":"x"}`: "is a string",
|
||||||
"{/no.such.path}": "no entry",
|
"name: {/no.such.path}": "no entry",
|
||||||
"x[1]": "names no template either",
|
"{/sv_SE.person}": "write sv_SE.person",
|
||||||
` ["a","b"] `: "may not be padded",
|
"x[1]": "names no template either",
|
||||||
|
` ["a","b"] `: "may not be padded",
|
||||||
} {
|
} {
|
||||||
code, out, errb := runOut("--seed", "1", arg)
|
code, out, errb := runOut("--seed", "1", arg)
|
||||||
if code != 2 || out != "" || !strings.Contains(errb, "try 'fejkdata --help'") || !strings.Contains(errb, want) {
|
if code != 2 || out != "" || !strings.Contains(errb, "try 'fejkdata --help'") || !strings.Contains(errb, want) {
|
||||||
|
|||||||
+20
-13
@@ -67,19 +67,7 @@ func datatypeOf(m map[string]any, pos position) (DataType, error) {
|
|||||||
// columnDatatype is the datatype a column's items declare. They must agree, since a
|
// columnDatatype is the datatype a column's items declare. They must agree, since a
|
||||||
// column holds one; a column only ever null is a string.
|
// column holds one; a column only ever null is a string.
|
||||||
func columnDatatype(n node) (DataType, error) {
|
func columnDatatype(n node) (DataType, error) {
|
||||||
var items []*template
|
items, _ := columnItems(n)
|
||||||
var collect func(node)
|
|
||||||
collect = func(n node) {
|
|
||||||
switch n := n.(type) {
|
|
||||||
case *choice:
|
|
||||||
for _, it := range n.items {
|
|
||||||
collect(it)
|
|
||||||
}
|
|
||||||
case *template:
|
|
||||||
items = append(items, n)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
collect(n)
|
|
||||||
if len(items) == 0 {
|
if len(items) == 0 {
|
||||||
return DataTypeString, nil
|
return DataTypeString, nil
|
||||||
}
|
}
|
||||||
@@ -91,6 +79,25 @@ func columnDatatype(n node) (DataType, error) {
|
|||||||
return items[0].datatype, nil
|
return items[0].datatype, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// columnItems is a column's template items, its choices unwrapped, and whether one is null.
|
||||||
|
func columnItems(n node) (items []*template, nullable bool) {
|
||||||
|
var collect func(node)
|
||||||
|
collect = func(n node) {
|
||||||
|
switch n := n.(type) {
|
||||||
|
case *choice:
|
||||||
|
for _, it := range n.items {
|
||||||
|
collect(it)
|
||||||
|
}
|
||||||
|
case *template:
|
||||||
|
items = append(items, n)
|
||||||
|
case *null:
|
||||||
|
nullable = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
collect(n)
|
||||||
|
return items, nullable
|
||||||
|
}
|
||||||
|
|
||||||
// disagreement names the fix for two items of one column declaring different datatypes.
|
// disagreement names the fix for two items of one column declaring different datatypes.
|
||||||
func disagreement(a, b *template) error {
|
func disagreement(a, b *template) error {
|
||||||
typed, bare := a, b
|
typed, bare := a, b
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ import (
|
|||||||
"io/fs"
|
"io/fs"
|
||||||
"math/rand/v2"
|
"math/rand/v2"
|
||||||
"os"
|
"os"
|
||||||
|
"reflect"
|
||||||
"sort"
|
"sort"
|
||||||
"sync"
|
"sync"
|
||||||
)
|
)
|
||||||
@@ -46,6 +47,7 @@ type Generator struct {
|
|||||||
rand *session
|
rand *session
|
||||||
categories map[string]node
|
categories map[string]node
|
||||||
records map[node]recordShape
|
records map[node]recordShape
|
||||||
|
structs map[reflect.Type]structResult
|
||||||
}
|
}
|
||||||
|
|
||||||
// session is one generator's mutable render state: the seeded rng plus the {seq()}
|
// session is one generator's mutable render state: the seeded rng plus the {seq()}
|
||||||
|
|||||||
@@ -173,8 +173,8 @@ func treeScope(root map[string]node) nodeScope {
|
|||||||
return func(fn func(path string, n node) error) error { return walkNodes(root, fn) }
|
return func(fn func(path string, n node) error) error { return walkNodes(root, fn) }
|
||||||
}
|
}
|
||||||
|
|
||||||
func inlineScope(n node) nodeScope {
|
func inlineScope(n node, label string) nodeScope {
|
||||||
return func(fn func(path string, m node) error) error { return eachNode(n, "template", fn) }
|
return func(fn func(path string, m node) error) error { return eachNode(n, label, fn) }
|
||||||
}
|
}
|
||||||
|
|
||||||
// checkScope runs the per-node fences over a scope, each over the whole scope
|
// checkScope runs the per-node fences over a scope, each over the whole scope
|
||||||
|
|||||||
@@ -32,11 +32,7 @@ func (f *Generator) NewTemplate(input string) (*Template, error) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("fejkdata: %w", err)
|
return nil, fmt.Errorf("fejkdata: %w", err)
|
||||||
}
|
}
|
||||||
scope := inlineScope(n)
|
if err := bindInline(n, "template", f.categories); err != nil {
|
||||||
if err := linkNodeRefs(scope, f.categories); err != nil {
|
|
||||||
return nil, fmt.Errorf("fejkdata: %w", err)
|
|
||||||
}
|
|
||||||
if err := checkScope(scope); err != nil {
|
|
||||||
return nil, fmt.Errorf("fejkdata: %w", err)
|
return nil, fmt.Errorf("fejkdata: %w", err)
|
||||||
}
|
}
|
||||||
return &Template{g: f, n: n}, nil
|
return &Template{g: f, n: n}, nil
|
||||||
@@ -53,17 +49,102 @@ func (f *Generator) FakeTemplate(input string) (string, error) {
|
|||||||
return t.Fake(), nil
|
return t.Fake(), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// compileInput compiles an inline template: a JSON value, or a bare format string
|
// IsTemplate reports whether arg is an inline template rather than a path, by its shape: a {
|
||||||
// when the input is not JSON.
|
// token, or a JSON object, array or string, is a template, and anything else is a path. A
|
||||||
|
// name never holds a bracket, a brace or a quote, so an arg holding one that is not valid
|
||||||
|
// JSON names neither, and errors; so do a template of one reference alone, which is a path
|
||||||
|
// written as a template, and a path written with a leading /.
|
||||||
|
func IsTemplate(arg string) (bool, error) {
|
||||||
|
inline, err := isTemplate(arg)
|
||||||
|
if err != nil {
|
||||||
|
return false, fmt.Errorf("fejkdata: %w", err)
|
||||||
|
}
|
||||||
|
return inline, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func isTemplate(arg string) (bool, error) {
|
||||||
|
if strings.ContainsRune(arg, '{') || (isJSONStart(strings.TrimSpace(arg)) && json.Valid([]byte(arg))) {
|
||||||
|
if path, lone := loneReference(arg); lone {
|
||||||
|
return false, pathAdvice(path, fmt.Sprintf("%s is the path %s written as a template", arg, path))
|
||||||
|
}
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
|
if i := strings.IndexAny(arg, `[]}"`); i >= 0 {
|
||||||
|
return false, fmt.Errorf("%q holds a %q, which no path may, and it is not valid JSON, so it names no template either", arg, arg[i:i+1])
|
||||||
|
}
|
||||||
|
if path := strings.TrimLeft(arg, "/"); path != arg && path != "" {
|
||||||
|
return false, pathAdvice(path, fmt.Sprintf("path %s starts with /, and every path starts at the root already", arg))
|
||||||
|
}
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func isJSONStart(arg string) bool {
|
||||||
|
return strings.HasPrefix(arg, "[") || strings.HasPrefix(arg, `"`)
|
||||||
|
}
|
||||||
|
|
||||||
|
// pathAdvice refuses a spelling of path, naming path to write, or why no name can spell it.
|
||||||
|
func pathAdvice(path, refusal string) error {
|
||||||
|
if err := checkPathNames(path); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return fmt.Errorf("%s; write %s", refusal, path)
|
||||||
|
}
|
||||||
|
|
||||||
|
// loneReference is the path a template spells when the value it holds — a format string, a
|
||||||
|
// JSON string, or an object holding only a format — is one reference token and nothing else.
|
||||||
|
func loneReference(arg string) (string, bool) {
|
||||||
|
var raw any
|
||||||
|
if json.Unmarshal([]byte(arg), &raw) != nil {
|
||||||
|
raw = arg
|
||||||
|
}
|
||||||
|
if m, isObject := raw.(map[string]any); isObject && len(m) == 1 {
|
||||||
|
raw = m["format"]
|
||||||
|
}
|
||||||
|
format, isString := raw.(string)
|
||||||
|
var units []ftoken
|
||||||
|
if !isString || eachToken(format, func(t ftoken) error { units = append(units, t); return nil }) != nil || len(units) != 1 {
|
||||||
|
return "", false
|
||||||
|
}
|
||||||
|
body := units[0].body
|
||||||
|
if units[0].kind != 'b' || !isRef(body) || strings.ContainsAny(body, "|(") {
|
||||||
|
return "", false
|
||||||
|
}
|
||||||
|
if strings.HasPrefix(body, "/") {
|
||||||
|
body = "/" + strings.TrimLeft(body, "/")
|
||||||
|
}
|
||||||
|
_, path, err := refShape(body)
|
||||||
|
return path, err == nil
|
||||||
|
}
|
||||||
|
|
||||||
func compileInput(input string) (node, error) {
|
func compileInput(input string) (node, error) {
|
||||||
|
v, err := inputValue(input)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return compile(v)
|
||||||
|
}
|
||||||
|
|
||||||
|
// inputValue reads an inline template as the value compile takes: the JSON value it holds, or
|
||||||
|
// the input itself as a format string when it is not JSON.
|
||||||
|
func inputValue(input string) (any, error) {
|
||||||
var raw any
|
var raw any
|
||||||
if err := json.Unmarshal([]byte(input), &raw); err != nil {
|
if err := json.Unmarshal([]byte(input), &raw); err != nil {
|
||||||
return compile(input)
|
return input, nil
|
||||||
}
|
}
|
||||||
if trimmed := strings.TrimSpace(input); trimmed != input {
|
if trimmed := strings.TrimSpace(input); trimmed != input {
|
||||||
return nil, fmt.Errorf("a JSON template may not be padded with spaces, which a format string would render; write %s", trimmed)
|
return nil, fmt.Errorf("a JSON template may not be padded with spaces, which a format string would render; write %s", trimmed)
|
||||||
}
|
}
|
||||||
return compile(raw)
|
return raw, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// bindInline links an inline node's references against root and runs the fences over it,
|
||||||
|
// naming its nodes from label.
|
||||||
|
func bindInline(n node, label string, root map[string]node) error {
|
||||||
|
scope := inlineScope(n, label)
|
||||||
|
if err := linkNodeRefs(scope, root); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return checkScope(scope)
|
||||||
}
|
}
|
||||||
|
|
||||||
// linkNodeRefs binds the references in an inline node's templates against the
|
// linkNodeRefs binds the references in an inline node's templates against the
|
||||||
|
|||||||
@@ -150,6 +150,51 @@ func TestNewTemplateReusable(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestIsTemplate(t *testing.T) {
|
||||||
|
for arg, want := range map[string]bool{
|
||||||
|
"sv_SE.person": false,
|
||||||
|
"person.last": false,
|
||||||
|
"name: {x}": true,
|
||||||
|
`{"format":"x"}`: true,
|
||||||
|
`["a","b"]`: true,
|
||||||
|
`[1, 2]`: true,
|
||||||
|
` ["a","b"]`: true, // padding is the template's own error, not a shape verdict
|
||||||
|
`"hello"`: true,
|
||||||
|
"{/a}{/b}": true,
|
||||||
|
"{/a|/b}": true,
|
||||||
|
"{uppercase(/a)}": true,
|
||||||
|
"{{/a}}": true,
|
||||||
|
`"{/a} x"`: true,
|
||||||
|
} {
|
||||||
|
if got, err := IsTemplate(arg); err != nil || got != want {
|
||||||
|
t.Errorf("IsTemplate(%q) = %v, %v; want %v", arg, got, err, want)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for arg, want := range map[string]string{
|
||||||
|
"[abc]": `holds a "["`,
|
||||||
|
"[abc].field": `holds a "["`,
|
||||||
|
"x[1]": `holds a "["`,
|
||||||
|
"a]b": `holds a "]"`,
|
||||||
|
"a}b": `holds a "}"`,
|
||||||
|
`"abc`: `holds a "\""`,
|
||||||
|
`"a]b`: `holds a "\""`, // the opener the reader typed, not the bracket behind it
|
||||||
|
"{/sv_SE.person.last}": "{/sv_SE.person.last} is the path sv_SE.person.last written as a template; write sv_SE.person.last",
|
||||||
|
`"{/sv_SE.person}"`: "write sv_SE.person",
|
||||||
|
"{.person.last}": "write person.last",
|
||||||
|
"/sv_SE.person": "write sv_SE.person",
|
||||||
|
` "{/sv_SE.person}"`: "write sv_SE.person",
|
||||||
|
`{"format":"{/sv_SE.person}"}`: "write sv_SE.person",
|
||||||
|
"//sv_SE.person": "write sv_SE.person",
|
||||||
|
"{//sv_SE.person}": "{//sv_SE.person} is the path sv_SE.person written as a template; write sv_SE.person",
|
||||||
|
"/sv_SE/person": `path "sv_SE/person" contains "/"`,
|
||||||
|
"{/-}": `path "-" is reserved`,
|
||||||
|
} {
|
||||||
|
if _, err := IsTemplate(arg); err == nil || !strings.Contains(err.Error(), want) {
|
||||||
|
t.Errorf("IsTemplate(%q) = %v; want it rejected naming %s", arg, err, want)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestFakeTemplateRepeatBound(t *testing.T) {
|
func TestFakeTemplateRepeatBound(t *testing.T) {
|
||||||
f := shipped(t)
|
f := shipped(t)
|
||||||
_, err := f.FakeTemplate(`{"format":"{x}","repeat":200,"x":{"format":"{y}","repeat":200,"y":{"format":"z","repeat":200}}}`)
|
_, err := f.FakeTemplate(`{"format":"{x}","repeat":200,"x":{"format":"{y}","repeat":200,"y":{"format":"z","repeat":200}}}`)
|
||||||
|
|||||||
@@ -298,6 +298,10 @@ func TestNewErrors(t *testing.T) {
|
|||||||
map[string]string{"a(b/cat": `"1"`},
|
map[string]string{"a(b/cat": `"1"`},
|
||||||
`folder "a(b" contains "("`,
|
`folder "a(b" contains "("`,
|
||||||
},
|
},
|
||||||
|
"field name a struct tag reserves": {
|
||||||
|
map[string]string{"a": `{"format":"{x}","x":"1","-":"2"}`},
|
||||||
|
`field "-" is reserved`,
|
||||||
|
},
|
||||||
// A repeated arm skews an alternation, which weight is the spelling for.
|
// A repeated arm skews an alternation, which weight is the spelling for.
|
||||||
"repeated alternation arm": {
|
"repeated alternation arm": {
|
||||||
map[string]string{"a": `{"format":"{x|x}","x":"1"}`},
|
map[string]string{"a": `{"format":"{x|x}","x":"1"}`},
|
||||||
|
|||||||
@@ -378,13 +378,17 @@ const reservedInName = ".|({}/[]\""
|
|||||||
// reservedList spells reservedInName for an error message, so the two cannot drift.
|
// reservedList spells reservedInName for an error message, so the two cannot drift.
|
||||||
var reservedList = strings.Join(strings.Split(reservedInName, ""), " ")
|
var reservedList = strings.Join(strings.Split(reservedInName, ""), " ")
|
||||||
|
|
||||||
// checkName rejects a name the dot path, {token} and JSON grammars cannot spell.
|
// checkName rejects a name the dot path, {token} and JSON grammars cannot spell, or a struct
|
||||||
|
// tag cannot read.
|
||||||
// Both a category or folder and a field go through it, so there is one answer to
|
// Both a category or folder and a field go through it, so there is one answer to
|
||||||
// what a name may contain.
|
// what a name may contain.
|
||||||
func checkName(name string) error {
|
func checkName(name string) error {
|
||||||
if name == "" {
|
if name == "" {
|
||||||
return fmt.Errorf("%q is empty, which is not a path segment, so List never offers it", name)
|
return fmt.Errorf("%q is empty, which is not a path segment, so List never offers it", name)
|
||||||
}
|
}
|
||||||
|
if name == "-" {
|
||||||
|
return fmt.Errorf(`%q is reserved: the struct tag fake:"-" leaves a field unfilled, so no tag could read it; rename it`, name)
|
||||||
|
}
|
||||||
if i := strings.IndexAny(name, reservedInName); i >= 0 {
|
if i := strings.IndexAny(name, reservedInName); i >= 0 {
|
||||||
return fmt.Errorf("%q contains %q; a name may not use %s, which the dot path, {token} and JSON grammars reserve",
|
return fmt.Errorf("%q contains %q; a name may not use %s, which the dot path, {token} and JSON grammars reserve",
|
||||||
name, name[i:i+1], reservedList)
|
name, name[i:i+1], reservedList)
|
||||||
@@ -392,6 +396,16 @@ func checkName(name string) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// checkPathNames rejects a dotted path with a segment no name may be.
|
||||||
|
func checkPathNames(path string) error {
|
||||||
|
for _, seg := range strings.Split(path, ".") {
|
||||||
|
if err := checkName(seg); err != nil {
|
||||||
|
return fmt.Errorf("path %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
// isOption reports whether a template key configures the node instead of naming a
|
// isOption reports whether a template key configures the node instead of naming a
|
||||||
// field. These names can never be fields.
|
// field. These names can never be fields.
|
||||||
func isOption(name string) bool {
|
func isOption(name string) bool {
|
||||||
|
|||||||
@@ -75,6 +75,25 @@ func TestNoRecordAllocRegression(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestNoStructAllocRegression(t *testing.T) {
|
||||||
|
f, err := New(WithoutShippedData(), WithDataFS(fstest.MapFS{"x.json": {Data: []byte(`{"format":"","a":"x","b":"y","c":"z"}`)}}))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
var v struct {
|
||||||
|
A string `fake:"x.a"`
|
||||||
|
B string `fake:"x.b"`
|
||||||
|
C string `fake:"x.c"`
|
||||||
|
}
|
||||||
|
if err := f.FakeStruct(&v); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
const base = 5.0
|
||||||
|
if allocs := testing.AllocsPerRun(10000, func() { f.FakeStruct(&v) }); allocs > base*1.10 {
|
||||||
|
t.Errorf("FakeStruct: %.1f allocs/op regressed past %.1f (baseline %.1f + 10%%); compiling the type per call is the usual cause", allocs, base*1.10, base)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func BenchmarkNestedDepth25(b *testing.B) { benchPath(b, tmpData(b, "deep", nestedJSON(25)), "deep") }
|
func BenchmarkNestedDepth25(b *testing.B) { benchPath(b, tmpData(b, "deep", nestedJSON(25)), "deep") }
|
||||||
func BenchmarkNestedDepth100(b *testing.B) { benchPath(b, tmpData(b, "deep", nestedJSON(100)), "deep") }
|
func BenchmarkNestedDepth100(b *testing.B) { benchPath(b, tmpData(b, "deep", nestedJSON(100)), "deep") }
|
||||||
func BenchmarkWideTokens100(b *testing.B) {
|
func BenchmarkWideTokens100(b *testing.B) {
|
||||||
|
|||||||
@@ -118,6 +118,13 @@ func TestFakeIsSafeForConcurrentUse(t *testing.T) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
tmpl.Fake()
|
tmpl.Fake()
|
||||||
|
var u struct {
|
||||||
|
Last string `fake:"sv_SE.person.last"`
|
||||||
|
}
|
||||||
|
if err := f.FakeStruct(&u); err != nil {
|
||||||
|
t.Error(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,422 @@
|
|||||||
|
package fejkdata
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"math"
|
||||||
|
"reflect"
|
||||||
|
"slices"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
// FakeStruct fills the struct v points to. Each exported field tagged `fake:"…"` is a
|
||||||
|
// column of one record: its tag a path or an inline template, told apart as [IsTemplate]
|
||||||
|
// tells them, and its Go type the column's datatype. The fields an embedded struct promotes
|
||||||
|
// are columns of that record too, while a named struct field, or a pointer to one, fills
|
||||||
|
// from its own tags as a record of its own. The first call for a type compiles its tags,
|
||||||
|
// so a later call for that type fails only as the first did.
|
||||||
|
func (f *Generator) FakeStruct(v any) error {
|
||||||
|
p := reflect.ValueOf(v)
|
||||||
|
if p.Kind() != reflect.Pointer || p.IsNil() || p.Elem().Kind() != reflect.Struct {
|
||||||
|
return fmt.Errorf("fejkdata: FakeStruct fills a struct through a non-nil pointer, got %T", v)
|
||||||
|
}
|
||||||
|
f.mu.Lock()
|
||||||
|
defer f.mu.Unlock()
|
||||||
|
shape, err := f.structShapeOf(p.Elem().Type())
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("fejkdata: %w", err)
|
||||||
|
}
|
||||||
|
shape.fill(f.rand, p.Elem())
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type structResult struct {
|
||||||
|
shape *structShape
|
||||||
|
err error
|
||||||
|
}
|
||||||
|
|
||||||
|
// maxStructs caps the structs compiling one type walks through its fields.
|
||||||
|
const maxStructs = 1 << 10
|
||||||
|
|
||||||
|
// structShapeOf compiles a struct type once and remembers the answer. Callers hold the
|
||||||
|
// generator's lock.
|
||||||
|
func (f *Generator) structShapeOf(t reflect.Type) (*structShape, error) {
|
||||||
|
if r, done := f.structs[t]; done {
|
||||||
|
return r.shape, r.err
|
||||||
|
}
|
||||||
|
label := t.Name()
|
||||||
|
if label == "" {
|
||||||
|
label = "struct"
|
||||||
|
}
|
||||||
|
sc := &structCompile{root: f.categories, visiting: map[reflect.Type]bool{}, structs: maxStructs}
|
||||||
|
shape, err := sc.record(t, label)
|
||||||
|
if err == nil && shape.empty() {
|
||||||
|
err = fmt.Errorf("%s has no fake tags, so nothing to fill", t)
|
||||||
|
}
|
||||||
|
if f.structs == nil {
|
||||||
|
f.structs = map[reflect.Type]structResult{}
|
||||||
|
}
|
||||||
|
f.structs[t] = structResult{shape, err}
|
||||||
|
return shape, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// structShape is a struct type compiled to fill: its tagged fields as one record, the field
|
||||||
|
// index path each column fills, and the struct fields carrying tags of their own.
|
||||||
|
type structShape struct {
|
||||||
|
record *template
|
||||||
|
columns []Column
|
||||||
|
fields [][]int
|
||||||
|
nested []nestedStruct
|
||||||
|
}
|
||||||
|
|
||||||
|
// nestedStruct is a named struct field, or a pointer to one, filled as a record of its own.
|
||||||
|
type nestedStruct struct {
|
||||||
|
index []int
|
||||||
|
shape *structShape
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *structShape) empty() bool { return s.record == nil && len(s.nested) == 0 }
|
||||||
|
|
||||||
|
// structCompile is what compiling one struct type shares across the structs it reaches: the
|
||||||
|
// loaded tree, the types compiling or embedded above, so a pointer back to one is left alone
|
||||||
|
// rather than filled without end, and how many more structs it may walk.
|
||||||
|
type structCompile struct {
|
||||||
|
root map[string]node
|
||||||
|
visiting map[reflect.Type]bool
|
||||||
|
structs int
|
||||||
|
}
|
||||||
|
|
||||||
|
// structFields gathers what one struct type fills: its tagged fields, those its embedded
|
||||||
|
// structs promote included, as the tags of one record, and its named struct fields as nested
|
||||||
|
// records.
|
||||||
|
type structFields struct {
|
||||||
|
*structCompile
|
||||||
|
t reflect.Type
|
||||||
|
label string
|
||||||
|
tags map[string]any
|
||||||
|
shape *structShape
|
||||||
|
}
|
||||||
|
|
||||||
|
func (sc *structCompile) record(t reflect.Type, label string) (*structShape, error) {
|
||||||
|
if err := sc.spend(label); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
sc.visiting[t] = true
|
||||||
|
defer delete(sc.visiting, t)
|
||||||
|
c := &structFields{structCompile: sc, t: t, label: label, tags: map[string]any{}, shape: &structShape{}}
|
||||||
|
if err := c.walk(t, nil); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if len(c.tags) > 0 {
|
||||||
|
if err := c.shape.compileRecord(sc.root, t, label, c.tags); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return c.shape, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (sc *structCompile) spend(label string) error {
|
||||||
|
if sc.structs--; sc.structs >= 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return fmt.Errorf(`%s: the struct fields reach more than %d structs; leave a struct field unfilled with fake:"-"`, label, maxStructs)
|
||||||
|
}
|
||||||
|
|
||||||
|
// walk gathers the fields of struct type t, which sits at index within c.t.
|
||||||
|
func (c *structFields) walk(t reflect.Type, index []int) error {
|
||||||
|
for i := 0; i < t.NumField(); i++ {
|
||||||
|
sf := t.Field(i)
|
||||||
|
sf.Index = append(index[:len(index):len(index)], i)
|
||||||
|
if err := c.field(sf); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *structFields) field(sf reflect.StructField) error {
|
||||||
|
tag, tagged := sf.Tag.Lookup("fake")
|
||||||
|
elem := structOf(sf.Type)
|
||||||
|
switch {
|
||||||
|
case tagged && tag == "-":
|
||||||
|
if elem == nil {
|
||||||
|
return fmt.Errorf(`%s.%s: fake:"-" leaves a struct field unfilled, and any other untagged field keeps its value already; drop the tag`, c.label, sf.Name)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
case tagged:
|
||||||
|
return c.column(sf, tag)
|
||||||
|
case elem == nil || c.visiting[elem]:
|
||||||
|
return nil
|
||||||
|
case sf.Anonymous:
|
||||||
|
return c.embed(sf, elem)
|
||||||
|
case sf.IsExported():
|
||||||
|
return c.nest(sf, elem)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// structOf is the struct type a field holds, by value or through a pointer; nil when none.
|
||||||
|
func structOf(t reflect.Type) reflect.Type {
|
||||||
|
if t.Kind() == reflect.Pointer {
|
||||||
|
t = t.Elem()
|
||||||
|
}
|
||||||
|
if t.Kind() != reflect.Struct {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return t
|
||||||
|
}
|
||||||
|
|
||||||
|
// column adds a tagged field's tag to the record, refusing one another field hides.
|
||||||
|
func (c *structFields) column(sf reflect.StructField, tag string) error {
|
||||||
|
if visible, ok := c.t.FieldByName(sf.Name); !ok || !slices.Equal(visible.Index, sf.Index) {
|
||||||
|
return fmt.Errorf("%s.%s: hidden by another field named %s, so its fake tag cannot fill it; rename one", c.label, fieldPath(c.t, sf.Index), sf.Name)
|
||||||
|
}
|
||||||
|
v, err := tagValue(sf, tag)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("%s.%s: %w", c.label, sf.Name, err)
|
||||||
|
}
|
||||||
|
c.tags[sf.Name] = v
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// fieldPath names the field at index within t through each struct it is embedded in.
|
||||||
|
func fieldPath(t reflect.Type, index []int) string {
|
||||||
|
names := make([]string, len(index))
|
||||||
|
for i := range index {
|
||||||
|
names[i] = t.FieldByIndex(index[:i+1]).Name
|
||||||
|
}
|
||||||
|
return strings.Join(names, ".")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *structFields) embed(sf reflect.StructField, elem reflect.Type) error {
|
||||||
|
if err := c.spend(c.label + "." + fieldPath(c.t, sf.Index)); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
c.visiting[elem] = true
|
||||||
|
defer delete(c.visiting, elem)
|
||||||
|
tags, nested := len(c.tags), len(c.shape.nested)
|
||||||
|
if err := c.walk(elem, sf.Index); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if sf.Type.Kind() == reflect.Pointer && !sf.IsExported() && (len(c.tags) > tags || len(c.shape.nested) > nested) {
|
||||||
|
return fmt.Errorf("%s.%s: an unexported embedded pointer field cannot be set, so the tags beneath it cannot fill; embed %s by value", c.label, fieldPath(c.t, sf.Index), elem)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *structFields) nest(sf reflect.StructField, elem reflect.Type) error {
|
||||||
|
nested, err := c.record(elem, c.label+"."+sf.Name)
|
||||||
|
if err != nil || nested.empty() {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
c.shape.nested = append(c.shape.nested, nestedStruct{sf.Index, nested})
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// tagValue reads a field's fake tag as the value its column compiles from: an inline template
|
||||||
|
// as written, or a path as the reference {/path}.
|
||||||
|
func tagValue(sf reflect.StructField, tag string) (any, error) {
|
||||||
|
if err := checkTaggedType(sf); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
inline, err := isTemplate(tag)
|
||||||
|
switch {
|
||||||
|
case err != nil:
|
||||||
|
return nil, err
|
||||||
|
case inline:
|
||||||
|
return inputValue(tag)
|
||||||
|
}
|
||||||
|
if err := checkPathNames(tag); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return "{/" + tag + "}", nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// checkTaggedType rejects a tagged field no column can fill.
|
||||||
|
func checkTaggedType(sf reflect.StructField) error {
|
||||||
|
_, holds := columnKinds[sf.Type.Kind()]
|
||||||
|
if sf.Type.Kind() == reflect.Pointer {
|
||||||
|
_, holds = columnKinds[sf.Type.Elem().Kind()]
|
||||||
|
}
|
||||||
|
switch {
|
||||||
|
case !sf.IsExported():
|
||||||
|
return errors.New("unexported, so its fake tag cannot fill it")
|
||||||
|
case holds:
|
||||||
|
return nil
|
||||||
|
case structOf(sf.Type) != nil:
|
||||||
|
return errors.New(`a struct field fills from the tags on its own fields; drop this one, or write fake:"-" to leave it unfilled`)
|
||||||
|
}
|
||||||
|
return fmt.Errorf("a fake tag fills a string, bool, integer or float field, or a pointer to one, not %s", sf.Type)
|
||||||
|
}
|
||||||
|
|
||||||
|
// compileRecord compiles the tagged fields of t as one record, and proves each column holds
|
||||||
|
// only what its field's Go type can.
|
||||||
|
func (s *structShape) compileRecord(root map[string]node, t reflect.Type, label string, tags map[string]any) error {
|
||||||
|
tags["format"] = ""
|
||||||
|
n, err := compile(tags)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("%s: %w", label, err)
|
||||||
|
}
|
||||||
|
if err := bindInline(n, label, root); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
record, columns, err := recordOf(n)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("%s: %w", label, err)
|
||||||
|
}
|
||||||
|
proof := &valueProof{}
|
||||||
|
s.fields = make([][]int, len(columns))
|
||||||
|
for i, c := range columns {
|
||||||
|
sf, _ := t.FieldByName(c.Name)
|
||||||
|
if c.DataType != DataTypeString {
|
||||||
|
return fmt.Errorf("%s.%s: its Go type %s sets the datatype; drop \"datatype\"", label, c.Name, sf.Type)
|
||||||
|
}
|
||||||
|
if err := proof.checkField(label+"."+c.Name, sf.Type, record.fields[c.Name]); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
s.fields[i] = sf.Index
|
||||||
|
}
|
||||||
|
s.record, s.columns = record, columns
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// columnKind is what a field of one Go kind holds: the datatype its text proves as, the range a
|
||||||
|
// number of it stays in, and the kind to name when a value is not proven within that range.
|
||||||
|
type columnKind struct {
|
||||||
|
datatype DataType
|
||||||
|
lo, hi float64
|
||||||
|
wider reflect.Kind
|
||||||
|
}
|
||||||
|
|
||||||
|
var columnKinds = map[reflect.Kind]columnKind{
|
||||||
|
reflect.Bool: {datatype: DataTypeBoolean},
|
||||||
|
reflect.Float32: {DataTypeNumber, -math.MaxFloat32, math.MaxFloat32, reflect.Float64},
|
||||||
|
reflect.Float64: {DataTypeNumber, -math.MaxFloat64, math.MaxFloat64, reflect.Float64},
|
||||||
|
reflect.Int: {DataTypeInteger, math.MinInt, math.MaxInt, reflect.Int64},
|
||||||
|
reflect.Int16: {DataTypeInteger, math.MinInt16, math.MaxInt16, reflect.Int64},
|
||||||
|
reflect.Int32: {DataTypeInteger, math.MinInt32, math.MaxInt32, reflect.Int64},
|
||||||
|
reflect.Int64: {DataTypeInteger, math.MinInt64, math.MaxInt64, reflect.Int64},
|
||||||
|
reflect.Int8: {DataTypeInteger, math.MinInt8, math.MaxInt8, reflect.Int64},
|
||||||
|
reflect.String: {datatype: DataTypeString},
|
||||||
|
reflect.Uint: {DataTypeInteger, 0, math.MaxUint, reflect.Int64},
|
||||||
|
reflect.Uint16: {DataTypeInteger, 0, math.MaxUint16, reflect.Int64},
|
||||||
|
reflect.Uint32: {DataTypeInteger, 0, math.MaxUint32, reflect.Int64},
|
||||||
|
reflect.Uint64: {DataTypeInteger, 0, math.MaxUint64, reflect.Int64},
|
||||||
|
reflect.Uint8: {DataTypeInteger, 0, math.MaxUint8, reflect.Int64},
|
||||||
|
}
|
||||||
|
|
||||||
|
// holds reports whether a field of this kind holds every value v proves. An integer prints
|
||||||
|
// whole, so its bounds round inward first.
|
||||||
|
func (k columnKind) holds(v proven) bool {
|
||||||
|
switch k.datatype {
|
||||||
|
case DataTypeString, DataTypeBoolean:
|
||||||
|
return true
|
||||||
|
case DataTypeInteger:
|
||||||
|
return math.Ceil(v.lo) >= k.lo && math.Floor(v.hi) <= k.hi
|
||||||
|
}
|
||||||
|
return v.lo >= k.lo && v.hi <= k.hi
|
||||||
|
}
|
||||||
|
|
||||||
|
// checkField rejects a column some render of which a field of Go type ft cannot hold: a null
|
||||||
|
// outside a pointer, or a value its kind's datatype or range refuses.
|
||||||
|
func (p *valueProof) checkField(label string, ft reflect.Type, column node) error {
|
||||||
|
items, nullable := columnItems(column)
|
||||||
|
elem := ft
|
||||||
|
if ft.Kind() == reflect.Pointer {
|
||||||
|
elem = ft.Elem()
|
||||||
|
} else if nullable {
|
||||||
|
return fmt.Errorf("%s: its tag can draw null, which %s cannot hold; make it *%s", label, ft, ft)
|
||||||
|
}
|
||||||
|
kind := columnKinds[elem.Kind()]
|
||||||
|
if kind.datatype == DataTypeString {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
for _, it := range items {
|
||||||
|
v := p.of(it)
|
||||||
|
if reason := v.not[kind.datatype]; reason != "" {
|
||||||
|
return fmt.Errorf("%s (%s): %s", label, ft, reason)
|
||||||
|
}
|
||||||
|
if !kind.holds(v) {
|
||||||
|
return fmt.Errorf("%s (%s): %q is not proven within %s; make it %s", label, ft, it.format, elem.Kind(), kind.wider)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// fill draws the record into v's tagged fields, then each nested struct as a record of its own.
|
||||||
|
func (s *structShape) fill(sess *session, v reflect.Value) {
|
||||||
|
if s.record != nil {
|
||||||
|
for i, c := range renderRecord(sess, s.record, s.columns).columns {
|
||||||
|
setColumn(fieldAt(v, s.fields[i]), c)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for _, n := range s.nested {
|
||||||
|
field := fieldAt(v, n.index)
|
||||||
|
if field.Kind() == reflect.Pointer {
|
||||||
|
if field.IsNil() {
|
||||||
|
field.Set(reflect.New(field.Type().Elem()))
|
||||||
|
}
|
||||||
|
field = field.Elem()
|
||||||
|
}
|
||||||
|
n.shape.fill(sess, field)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// fieldAt is v's field at index, allocating each nil embedded pointer on the way.
|
||||||
|
func fieldAt(v reflect.Value, index []int) reflect.Value {
|
||||||
|
for _, i := range index {
|
||||||
|
if v.Kind() == reflect.Pointer {
|
||||||
|
if v.IsNil() {
|
||||||
|
v.Set(reflect.New(v.Type().Elem()))
|
||||||
|
}
|
||||||
|
v = v.Elem()
|
||||||
|
}
|
||||||
|
v = v.Field(i)
|
||||||
|
}
|
||||||
|
return v
|
||||||
|
}
|
||||||
|
|
||||||
|
// setColumn writes a drawn column into its field: a null as a nil pointer, a value through a
|
||||||
|
// fresh pointer or straight into the field.
|
||||||
|
func setColumn(field reflect.Value, c Column) {
|
||||||
|
if field.Kind() != reflect.Pointer {
|
||||||
|
setText(field, c.Value)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if c.Null {
|
||||||
|
field.SetZero()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
value := reflect.New(field.Type().Elem())
|
||||||
|
setText(value.Elem(), c.Value)
|
||||||
|
field.Set(value)
|
||||||
|
}
|
||||||
|
|
||||||
|
// setText parses text into a field of one of columnKinds, which checkField proved it parses as.
|
||||||
|
func setText(field reflect.Value, text string) {
|
||||||
|
var err error
|
||||||
|
switch kind := columnKinds[field.Kind()]; {
|
||||||
|
case kind.datatype == DataTypeString:
|
||||||
|
field.SetString(text)
|
||||||
|
case kind.datatype == DataTypeBoolean:
|
||||||
|
var b bool
|
||||||
|
b, err = strconv.ParseBool(text)
|
||||||
|
field.SetBool(b)
|
||||||
|
case kind.datatype == DataTypeNumber:
|
||||||
|
var x float64
|
||||||
|
x, err = strconv.ParseFloat(text, field.Type().Bits())
|
||||||
|
field.SetFloat(x)
|
||||||
|
case field.CanInt():
|
||||||
|
var n int64
|
||||||
|
n, err = strconv.ParseInt(text, 10, field.Type().Bits())
|
||||||
|
field.SetInt(n)
|
||||||
|
default:
|
||||||
|
var n uint64
|
||||||
|
n, err = strconv.ParseUint(text, 10, field.Type().Bits())
|
||||||
|
field.SetUint(n)
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
panic(fmt.Sprintf("fejkdata: %q reached a %s field unproven: %v", text, field.Type(), err))
|
||||||
|
}
|
||||||
|
}
|
||||||
+262
@@ -0,0 +1,262 @@
|
|||||||
|
package fejkdata
|
||||||
|
|
||||||
|
import (
|
||||||
|
"reflect"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
type structPlace struct {
|
||||||
|
City string `fake:"place.city"`
|
||||||
|
Zip string `fake:"place.zip"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type structUser struct {
|
||||||
|
Active bool `fake:"[\"true\",\"false\"]"`
|
||||||
|
Age uint8 `fake:"{int(18,99)}"`
|
||||||
|
Email string `fake:"{lowercase(/person.first)}@example.com"`
|
||||||
|
First string `fake:"person.first"`
|
||||||
|
Home structPlace
|
||||||
|
ID int64 `fake:"{seq()}"`
|
||||||
|
Last string `fake:"person.last"`
|
||||||
|
Level uint8 `fake:"{float(0,255,0)}"`
|
||||||
|
Nick *string `fake:"[null,\"bo\"]"`
|
||||||
|
Note string
|
||||||
|
Rank *int `fake:"{\"format\":\"{r}\",\"r\":[\"1\",\"2\"]}"`
|
||||||
|
Score float32 `fake:"{float(0,1,2)}"`
|
||||||
|
Skip *structPlace `fake:"-"`
|
||||||
|
Work *structPlace
|
||||||
|
hidden structPlace
|
||||||
|
}
|
||||||
|
|
||||||
|
type structGiven struct {
|
||||||
|
First string `fake:"person.first"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type StructFamily struct {
|
||||||
|
Last string `fake:"person.last"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type structEmployee struct {
|
||||||
|
structGiven
|
||||||
|
*StructFamily
|
||||||
|
Email string `fake:"{lowercase(/person.first)}@example.com"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type structLink struct {
|
||||||
|
Name string `fake:"person.first"`
|
||||||
|
Next *structLink
|
||||||
|
}
|
||||||
|
|
||||||
|
func structData(t *testing.T) *Generator {
|
||||||
|
t.Helper()
|
||||||
|
return newGenerator(t, writeData(t, map[string]string{
|
||||||
|
"person": `[{"format":"{first} {last}","first":"Ada","last":"Lovelace"},{"format":"{first} {last}","first":"Bo","last":"Ek"}]`,
|
||||||
|
"place": `[{"format":"{city}","city":"Stockholm","zip":"111 22"},{"format":"{city}","city":"Tranås","zip":"573 31"}]`,
|
||||||
|
"trip": `{"format":"","leg":[{"format":"{to}","to":"Oslo"},{"format":"{to}","to":"Rome"}]}`,
|
||||||
|
}), WithSeed(1))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestFakeStructFillsTaggedFields(t *testing.T) {
|
||||||
|
a, b := structData(t), structData(t)
|
||||||
|
people := map[string]string{"Ada": "Lovelace", "Bo": "Ek"}
|
||||||
|
zips := map[string]string{"Stockholm": "111 22", "Tranås": "573 31"}
|
||||||
|
actives, nils := 0, 0
|
||||||
|
for i := 0; i < 100; i++ {
|
||||||
|
oldNick, oldTwinNick := "old", "old"
|
||||||
|
u, twin := structUser{Nick: &oldNick, Note: "keep"}, structUser{Nick: &oldTwinNick, Note: "keep"}
|
||||||
|
if err := a.FakeStruct(&u); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if err := b.FakeStruct(&twin); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
switch {
|
||||||
|
case !reflect.DeepEqual(u, twin):
|
||||||
|
t.Fatalf("same seed diverged: %+v != %+v", u, twin)
|
||||||
|
case people[u.First] != u.Last || u.Email != strings.ToLower(u.First)+"@example.com":
|
||||||
|
t.Fatalf("person fields %q %q %q, want one person drawn across the struct", u.First, u.Last, u.Email)
|
||||||
|
case zips[u.Home.City] != u.Home.Zip || u.Work == nil || zips[u.Work.City] != u.Work.Zip:
|
||||||
|
t.Fatalf("places %+v, %+v, want each nested struct one place, the pointer allocated", u.Home, u.Work)
|
||||||
|
case u.ID != int64(i+1) || u.Age < 18 || u.Age > 99 || u.Score < 0 || u.Score > 1 || u.Rank == nil || (*u.Rank != 1 && *u.Rank != 2):
|
||||||
|
t.Fatalf("typed fields %+v, want each the value its tag draws", u)
|
||||||
|
case u.Nick != nil && *u.Nick != "bo", u.Note != "keep", u.hidden != (structPlace{}), u.Skip != nil, oldNick != "old":
|
||||||
|
t.Fatalf("%+v: want Nick nil or bo, and the untagged fields left as they were", u)
|
||||||
|
}
|
||||||
|
if u.Active {
|
||||||
|
actives++
|
||||||
|
}
|
||||||
|
if u.Nick == nil {
|
||||||
|
nils++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if actives == 0 || actives == 100 || nils == 0 || nils == 100 {
|
||||||
|
t.Errorf("100 draws gave %d active and %d nil nicks, want both outcomes of each", actives, nils)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestFakeStructFillsEmbeddedFieldsIntoItsRecord(t *testing.T) {
|
||||||
|
f := structData(t)
|
||||||
|
people := map[string]string{"Ada": "Lovelace", "Bo": "Ek"}
|
||||||
|
for i := 0; i < 100; i++ {
|
||||||
|
var e structEmployee
|
||||||
|
if err := f.FakeStruct(&e); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if e.StructFamily == nil || people[e.First] != e.Last || e.Email != strings.ToLower(e.First)+"@example.com" {
|
||||||
|
t.Fatalf("%+v, %+v: want the promoted fields one person with the struct's own, the embedded pointer allocated", e, e.StructFamily)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
var skipped struct {
|
||||||
|
structGiven `fake:"-"`
|
||||||
|
Email string `fake:"{lowercase(/person.first)}@example.com"`
|
||||||
|
}
|
||||||
|
if err := f.FakeStruct(&skipped); err != nil || skipped.First != "" || skipped.Email == "" {
|
||||||
|
t.Errorf("FakeStruct = %v, %+v; want the embedded struct under fake:\"-\" left unfilled beside the filled field", err, skipped)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestFakeStructDrawsANestedStructApart(t *testing.T) {
|
||||||
|
f := structData(t)
|
||||||
|
for i := 0; i < 100; i++ {
|
||||||
|
var trip struct{ From, To structPlace }
|
||||||
|
if err := f.FakeStruct(&trip); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if trip.From.City != trip.To.City {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
t.Error("From and To drew one place in 100 trips; a nested struct is a record of its own, so each draws apart")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestFakeStructLeavesAPointerBackAlone(t *testing.T) {
|
||||||
|
var l structLink
|
||||||
|
if err := structData(t).FakeStruct(&l); err != nil || l.Name == "" || l.Next != nil {
|
||||||
|
t.Errorf("FakeStruct = %v, %+v; want Name filled and Next, a pointer back to the struct being filled, left nil", err, l)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestFakeStructErrors(t *testing.T) {
|
||||||
|
f := structData(t)
|
||||||
|
for _, c := range []struct {
|
||||||
|
v any
|
||||||
|
want string
|
||||||
|
}{
|
||||||
|
{structUser{}, "fills a struct through a non-nil pointer, got fejkdata.structUser"},
|
||||||
|
{(*structUser)(nil), "through a non-nil pointer"},
|
||||||
|
{new(int), "through a non-nil pointer"},
|
||||||
|
{nil, "through a non-nil pointer"},
|
||||||
|
{&struct{ A string }{}, "has no fake tags"},
|
||||||
|
{&struct {
|
||||||
|
a string `fake:"person.first"`
|
||||||
|
}{}, ".a: unexported"},
|
||||||
|
{&struct {
|
||||||
|
A []string `fake:"person.first"`
|
||||||
|
}{}, ".A: a fake tag fills a string, bool, integer or float field, or a pointer to one, not []string"},
|
||||||
|
{&struct {
|
||||||
|
A **int `fake:"{int(1,9)}"`
|
||||||
|
}{}, "not **int"},
|
||||||
|
{&struct {
|
||||||
|
A structPlace `fake:"place"`
|
||||||
|
}{}, ".A: a struct field fills from the tags on its own fields"},
|
||||||
|
{&struct {
|
||||||
|
A int `fake:"{\"format\":\"{int(1,9)}\",\"datatype\":\"integer\"}"`
|
||||||
|
}{}, `its Go type int sets the datatype; drop "datatype"`},
|
||||||
|
{&struct {
|
||||||
|
A int `fake:"[null,\"{int(1,9)}\"]"`
|
||||||
|
}{}, "can draw null, which int cannot hold; make it *int"},
|
||||||
|
{&struct {
|
||||||
|
A int `fake:"{digits(3)}"`
|
||||||
|
}{}, ".A (int): {digits(3)} prints text, not an integer"},
|
||||||
|
{&struct {
|
||||||
|
A int `fake:"person.first"`
|
||||||
|
}{}, `"Ada" is not an integer`},
|
||||||
|
{&struct {
|
||||||
|
A int8 `fake:"{int(0,300)}"`
|
||||||
|
}{}, `"{int(0,300)}" is not proven within int8; make it int64`},
|
||||||
|
{&struct {
|
||||||
|
A uint `fake:"{int(-1,5)}"`
|
||||||
|
}{}, `"{int(-1,5)}" is not proven within uint; make it int64`},
|
||||||
|
{&struct {
|
||||||
|
A float32 `fake:"[\"1\",\"1e39\"]"`
|
||||||
|
}{}, `"1e39" is not proven within float32; make it float64`},
|
||||||
|
{&struct {
|
||||||
|
A int32 `fake:"{seq()}"`
|
||||||
|
}{}, `"{seq()}" is not proven within int32; make it int64`},
|
||||||
|
{&struct {
|
||||||
|
A bool `fake:"{int(0,1)}"`
|
||||||
|
}{}, "prints an integer, not a boolean"},
|
||||||
|
{&struct {
|
||||||
|
A string `fake:"nope.x"`
|
||||||
|
}{}, `no entry "nope"`},
|
||||||
|
{&struct {
|
||||||
|
A string `fake:"{/person.first}"`
|
||||||
|
}{}, "is the path person.first written as a template; write person.first"},
|
||||||
|
{&struct {
|
||||||
|
A string `fake:"\"{/person.first}\""`
|
||||||
|
}{}, "is the path person.first written as a template; write person.first"},
|
||||||
|
{&struct {
|
||||||
|
A string `fake:"a|b"`
|
||||||
|
}{}, `contains "|"`},
|
||||||
|
{&struct {
|
||||||
|
A string `fake:""`
|
||||||
|
}{}, "is empty"},
|
||||||
|
{&struct {
|
||||||
|
A string `fake:"[abc]"`
|
||||||
|
}{}, `holds a "["`},
|
||||||
|
{&struct {
|
||||||
|
A string `fake:"{.person.first} x"`
|
||||||
|
}{}, "write {/person.first}"},
|
||||||
|
{&struct {
|
||||||
|
A string `fake:"/person.first"`
|
||||||
|
}{}, "write person.first"},
|
||||||
|
{&struct {
|
||||||
|
A string `fake:"-"`
|
||||||
|
}{}, `fake:"-" leaves a struct field unfilled`},
|
||||||
|
{&struct{ *structGiven }{}, "an unexported embedded pointer field cannot be set"},
|
||||||
|
{&struct {
|
||||||
|
structGiven
|
||||||
|
First string `fake:"person.last"`
|
||||||
|
}{}, "struct.structGiven.First: hidden by another field named First"},
|
||||||
|
{&struct {
|
||||||
|
A string `fake:"{x}"`
|
||||||
|
}{}, `no field "x"`},
|
||||||
|
{&struct {
|
||||||
|
A string `fake:"trip.leg"`
|
||||||
|
B string `fake:"trip.leg.to"`
|
||||||
|
}{}, "reads a path into"},
|
||||||
|
{&struct {
|
||||||
|
Trip struct {
|
||||||
|
A int `fake:"{digits(3)}"`
|
||||||
|
}
|
||||||
|
}{}, ": struct.Trip.A (int): {digits(3)} prints text"},
|
||||||
|
} {
|
||||||
|
err := f.FakeStruct(c.v)
|
||||||
|
if err == nil || !strings.Contains(err.Error(), c.want) {
|
||||||
|
t.Errorf("FakeStruct(%T) = %v, want an error containing %q", c.v, err, c.want)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if again := f.FakeStruct(c.v); again == nil || again.Error() != err.Error() {
|
||||||
|
t.Errorf("FakeStruct(%T) again = %v, want the first call's error, %v", c.v, again, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestFakeStructBoundsTheStructsATypeReaches(t *testing.T) {
|
||||||
|
f := structData(t)
|
||||||
|
place := reflect.TypeOf(structPlace{})
|
||||||
|
tree := place
|
||||||
|
for depth := 1; depth <= 8; depth++ {
|
||||||
|
tree = reflect.StructOf([]reflect.StructField{{Name: "L", Type: reflect.PointerTo(tree)}, {Name: "R", Type: reflect.PointerTo(tree)}})
|
||||||
|
}
|
||||||
|
fields := []reflect.StructField{{Name: "L", Type: reflect.PointerTo(tree)}, {Name: "R", Type: reflect.PointerTo(tree)}, {Name: "P", Type: place}}
|
||||||
|
if err := f.FakeStruct(reflect.New(reflect.StructOf(fields)).Interface()); err != nil {
|
||||||
|
t.Fatalf("a type reaching 1024 structs: FakeStruct = %v, want it filled", err)
|
||||||
|
}
|
||||||
|
embedded := reflect.StructField{Name: "StructFamily", Type: reflect.TypeOf(StructFamily{}), Anonymous: true}
|
||||||
|
err := f.FakeStruct(reflect.New(reflect.StructOf(append(fields, embedded))).Interface())
|
||||||
|
if err == nil || !strings.Contains(err.Error(), "more than 1024 structs") || !strings.Contains(err.Error(), `leave a struct field unfilled with fake:"-"`) {
|
||||||
|
t.Errorf("a type reaching 1025 structs, the last embedded: FakeStruct = %v, want it refused naming the cap and fake:\"-\"", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -6,11 +6,6 @@ The record API lands first, so the data update can use it.
|
|||||||
|
|
||||||
### Record API
|
### Record API
|
||||||
|
|
||||||
- Struct-filling — fill a Go struct from `fake:"…"` tags holding a path or an
|
|
||||||
inline template, for parity with gofakeit and go-faker. The field's Go type is
|
|
||||||
the column type, through the same conversion and load checks as typed columns,
|
|
||||||
and a nested struct is its own draw group. Revise the Decision "A record is a
|
|
||||||
template seen as columns, not a second schema format" with that reason.
|
|
||||||
- Draw groups — references into one category share one draw per render (one
|
- Draw groups — references into one category share one draw per render (one
|
||||||
record, or one `Fake`) in both views; each `repeat` iteration draws anew, and a
|
record, or one `Fake`) in both views; each `repeat` iteration draws anew, and a
|
||||||
bare reference draws each time. An option naming a draw group splits a render
|
bare reference draws each time. An option naming a draw group splits a render
|
||||||
@@ -24,8 +19,9 @@ The record API lands first, so the data update can use it.
|
|||||||
- two bare `{/sv_SE.word}` → two words
|
- two bare `{/sv_SE.word}` → two words
|
||||||
- Reference inheritance — settle whether a column that is exactly one reference to
|
- Reference inheritance — settle whether a column that is exactly one reference to
|
||||||
another record's column, like `{/src.score}`, takes that column's datatype and
|
another record's column, like `{/src.score}`, takes that column's datatype and
|
||||||
null. Today a null there writes `""`, and a typed column reading it is refused.
|
null. Today a null there writes `""`, a `*T` struct field reading it gets `""`
|
||||||
Settle before draw groups and the data update.
|
rather than nil, and a typed column reading it is refused. Settle before draw
|
||||||
|
groups and the data update.
|
||||||
|
|
||||||
### Data
|
### Data
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user