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
12 changed files with 73 additions and 118 deletions
Showing only changes of commit 6b26b18104 - Show all commits
+1 -1
View File
@@ -31,7 +31,7 @@ replacement, and each removed path, column or flag.
fejkdata`, and adds nothing under `--format text`: `echo`'s newline ends the render, and `--separator` fejkdata`, and adds nothing under `--format text`: `echo`'s newline ends the render, and `--separator`
defaults to empty. Whitespace around a JSON template is dropped. A lone reference, defaults to empty. Whitespace around a JSON template is dropped. A lone reference,
`{/users}`, is the record `users` under `--format`. An argument other than a flag, an `{/users}`, is the record `users` under `--format`. An argument other than a flag, an
empty or blank stdin, or nothing piped in, exits 2. empty or blank stdin, one holding only `null`, or nothing piped in, exits 2.
- Table categories: a category JSON naming a `rows` TSV beside it, with the options - Table categories: a category JSON naming a `rows` TSV beside it, with the options
`key`, `name`, `weight` and `parent`; a path selects a row by key or name, `key`, `name`, `weight` and `parent`; a path selects a row by key or name,
`misc.territory[SE]`, and descends to a linked table by name, one path drawing each `misc.territory[SE]`, and descends to a linked table by name, one path drawing each
+13 -13
View File
@@ -41,8 +41,8 @@ these paths; a template reads one as `{/path}`.
Under `--format text`, the default, what you send is what renders, and fejkdata adds nothing. A format string keeps every Under `--format text`, the default, what you send is what renders, and fejkdata adds nothing. A format string keeps every
byte, so `echo`, a file or a heredoc ends each render with its newline, and `printf '%s'` byte, so `echo`, a file or a heredoc ends each render with its newline, and `printf '%s'`
sends none; `-n` joins renders with `--separator`, empty by default. A JSON template is sends none; `-n` joins renders with `--separator`, empty by default. A JSON template is
an object, array or string, and the whitespace around it is dropped; `null` alone an object, array or string, and the whitespace around it is dropped; `null`, `42` or
renders nothing, and `42` or `true` alone is refused. A JSON template that should end `true` alone is refused. A JSON template that should end
each render in a newline writes `\n` in its format, from a quoted heredoc or a file. A each render in a newline writes `\n` in its format, from a quoted heredoc or a file. A
lone reference, one `{/…}` and nothing else, is the record it names under `--format`, one lone reference, one `{/…}` and nothing else, is the record it names under `--format`, one
newline after it allowed. Some shells' `echo` reads a backslash as an escape, so pipe a newline after it allowed. Some shells' `echo` reads a backslash as an escape, so pipe a
@@ -65,7 +65,7 @@ template holding one from a quoted heredoc or a file. The rest of stdin's rules
follows (`-n3`, `-n 3`) and short flags bundle (`-hn 3`) — see follows (`-n3`, `-n 3`) and short flags bundle (`-hn 3`) — see
[Decisions](docs/decisions.md#flags-follow-getopt_long); flags go anywhere, `--` ends them. Exit codes: `0` success, `1` runtime error (missing [Decisions](docs/decisions.md#flags-follow-getopt_long); flags go anywhere, `--` ends them. Exit codes: `0` success, `1` runtime error (missing
dir, a lone reference to nothing under `--format`), `2` misuse — a bad flag, an argument dir, a lone reference to nothing under `--format`), `2` misuse — a bad flag, an argument
other than a flag, an empty, blank or unreadable stdin, nothing piped in, or a other than a flag, an empty, blank, `null` or unreadable stdin, nothing piped in, or a
template that does not compile. From a checkout: template that does not compile. From a checkout:
`go run ./cmd/fejkdata …`. `go run ./cmd/fejkdata …`.
@@ -336,7 +336,7 @@ generator:
with the same error every call. with the same error every call.
- `Template.Fake` cannot fail. - `Template.Fake` cannot fail.
A path, wherever a call takes one, may start with `/`, as a reference does. A path, wherever a call takes one, may start with one `/`, as a reference does.
A `*Record` carries its columns via `Columns()` — each a `Column` of `Name`, A `*Record` carries its columns via `Columns()` — each a `Column` of `Name`,
`DataType`, rendered `Value` and `Null` — and serializes them with `JSON()` `DataType`, rendered `Value` and `Null` — and serializes them with `JSON()`
@@ -409,8 +409,8 @@ are two draws each. Only a [name](#names) keeps one pick for every read of it.
Renders e.g. `100 Main St, Apt 4B — tel 555-0199`. Random characters come from Renders e.g. `100 Main St, Apt 4B — tel 555-0199`. Random characters come from
the sample functions, so a phone pattern is `070-{digits(3)} {digits(2)} {digits(2)}`. the sample functions, so a phone pattern is `070-{digits(3)} {digits(2)} {digits(2)}`.
A lone `}` is a load error naming `}}`; the arms of `{a|b}` must differ, a A lone `}` is a load error naming `}}`. A repeated arm counts as written, so
repeated arm being a second spelling of [weight](#weight). `{a|a|b}` draws `a` two times in three.
### Weight ### Weight
@@ -425,10 +425,10 @@ An item of a choice may carry a `weight` (default `1`) to skew its odds:
``` ```
Renders `070-412 38 91` ten times as often as `08-…`. A string item is weighted by Renders `070-412 38 91` ten times as often as `08-…`. A string item is weighted by
writing it as `{ "format": "AB", "weight": 3 }`. Rejected at load: a weight that writing it as `{ "format": "AB", "weight": 3 }`. A repeated item counts as written,
is negative, non-numeric, `0` (never drawn — remove the item) or outside a choice, so `["a", "a", "b"]` draws `a` two times in three. Rejected at load: a weight that is
and a repeated item — `["a", "a", "b"]` is `[{ "format": "a", "weight": 2 }, "b"]`, negative, non-numeric or `0` (never drawn — remove the item), and one where it does
and the error says so. nothing: outside a choice, or on the item of a one-item choice.
### Repeat ### Repeat
@@ -472,9 +472,9 @@ order.id: datatype integer: "1{digits(2)}" is not one value; write one literal o
A column whose format is one read of a record's column and nothing else — A column whose format is one read of a record's column and nothing else —
`"score": "{/src.score}"`, `"score": "{s.score}"` with `s` bound to `/src`, or `"a": "{p}"` `"score": "{/src.score}"`, `"score": "{s.score}"` with `s` bound to `/src`, or `"a": "{p}"`
beside `{place as p}` in its own record — is that column: it takes the column's datatype and beside `{place as p}` in its own record — is that column: it takes the column's datatype and
is null where the column is, and a struct field tagged `src.score` is nil there. A `datatype` of its own types the is null where the column is, and a struct field tagged `src.score` is nil there. A
column's values where they prove it, and one restating the datatype it takes is refused. `datatype` of its own types the column's values where they prove it, and `"string"`
Any other read renders the column's text, a null as `""`. reads them as text. Any other read renders the column's text, a null as `""`.
A typed column's `{calc()}` must be proven to print a number: each operand a number A typed column's `{calc()}` must be proven to print a number: each operand a number
literal, an `{int()}`, `{float()}`, `{seq()}` or `{digits()}` call, a calc, or a read of literal, an `{int()}`, `{float()}`, `{seq()}` or `{digits()}` call, a calc, or a read of
+16 -12
View File
@@ -45,8 +45,8 @@ path into one ({/sv_SE.person.last}), a table's row by key or name
Under --format text, the default, what you send is what renders, and fejkdata adds Under --format text, the default, what you send is what renders, and fejkdata 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
printf '%s' sends none. -n joins renders with --separator, empty by default. A JSON printf '%s' sends none. -n joins renders with --separator, empty by default. A JSON
template is an object, array or string, and the whitespace around it is dropped; null template is an object, array or string, and the whitespace around it is dropped; null,
alone renders nothing, and 42 or true alone is refused. A quoted heredoc, <<'EOF', 42 or true alone is refused. A quoted heredoc, <<'EOF',
passes $, a backslash or a quote as written. passes $, 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
@@ -291,13 +291,13 @@ func operandError(arg string) error {
if fi, err := os.Stat(arg); err == nil && fi.Mode().IsRegular() { if fi, err := os.Stat(arg); err == nil && fi.Mode().IsRegular() {
return fmt.Errorf("%s; read a file from stdin: fejkdata < %s", unexpected, shellQuoted(arg)) return fmt.Errorf("%s; read a file from stdin: fejkdata < %s", unexpected, shellQuoted(arg))
} }
example := pipeHint example, ref := pipeHint, "{/"+strings.TrimPrefix(arg, "/")+"}"
if _, lone := loneReference(arg); lone { if _, lone := loneReference(arg); lone {
example = piped(arg) example = piped(arg)
} else if inline, err := fejkdata.IsTemplate(arg); inline { } else if inline, err := fejkdata.IsTemplate(arg); inline {
example = piped(arg) example = piped(arg)
} else if _, lone := loneReference("{/" + arg + "}"); lone && err == nil { } else if _, lone := loneReference(ref); lone && err == nil {
example = piped("{/" + arg + "}") example = piped(ref)
} }
return fmt.Errorf("%s; the template is read from stdin: %s", unexpected, example) return fmt.Errorf("%s; the template is read from stdin: %s", unexpected, example)
} }
@@ -448,24 +448,27 @@ type input struct {
record string record string
} }
// parseInput reads stdin. A format string is every byte of stdin; a JSON template drops // parseInput reads stdin as the template, and as a record where it is a lone reference: JSON
// the whitespace around it; a lone reference names a record, with one newline ending it // with the whitespace around it dropped, else a format string with one newline ending it
// allowed. // allowed.
func parseInput(raw string) (input, error) { func parseInput(raw string) (input, error) {
trimmed := strings.Trim(raw, jsonSpace) trimmed := strings.Trim(raw, jsonSpace)
if trimmed == "" { switch trimmed {
case "":
return input{}, fmt.Errorf("stdin holds no template: %s", pipeHint) return input{}, fmt.Errorf("stdin holds no template: %s", pipeHint)
case "null":
return input{}, fmt.Errorf("stdin holds only null, as jq prints for a missing key: %s", pipeHint)
} }
template := raw text := raw
if json.Valid([]byte(trimmed)) { if json.Valid([]byte(trimmed)) {
template = trimmed text = trimmed
} }
text, found := strings.CutSuffix(template, "\n") text, found := strings.CutSuffix(text, "\n")
if found { if found {
text = strings.TrimSuffix(text, "\r") text = strings.TrimSuffix(text, "\r")
} }
record, _ := loneReference(text) record, _ := loneReference(text)
return input{template: template, record: record}, nil return input{template: raw, record: record}, nil
} }
// loneReference returns the path that text reads when it is one reference from the root // loneReference returns the path that text reads when it is one reference from the root
@@ -485,6 +488,7 @@ func loneReference(text string) (string, bool) {
// isPath reports whether a reference's body after its / is a path alone: no second /, no // isPath reports whether a reference's body after its / is a path alone: no second /, no
// folder sigil, and none of the |, ( or " as " a token reads outside a selector. // folder sigil, and none of the |, ( or " as " a token reads outside a selector.
// It copies the grammar, since the CLI reads only the library's public API.
func isPath(body string) bool { func isPath(body string) bool {
if body == "" || strings.ContainsAny(body[:1], "/.") { if body == "" || strings.ContainsAny(body[:1], "/.") {
return false return false
+4 -13
View File
@@ -100,7 +100,7 @@ func columnDatatype(n node) DataType {
// itemDatatype is the datatype a column item declares, else that of the column it reads. // itemDatatype is the datatype a column item declares, else that of the column it reads.
func itemDatatype(t *template) DataType { func itemDatatype(t *template) DataType {
if t.datatype == DataTypeString && t.readsColumn != nil { if !t.declared && t.readsColumn != nil {
return columnDatatype(t.readsColumn.column) return columnDatatype(t.readsColumn.column)
} }
return t.datatype return t.datatype
@@ -139,7 +139,7 @@ const (
func kindOf(t *template) itemKind { func kindOf(t *template) itemKind {
switch { switch {
case t.datatype != DataTypeString: case t.declared:
return kindDeclares return kindDeclares
case itemDatatype(t) != DataTypeString: case itemDatatype(t) != DataTypeString:
return kindReads return kindReads
@@ -187,12 +187,12 @@ func (c clash) retypeRead() error {
if c.fits() { if c.fits() {
return fmt.Errorf("%s, so %s", c.held, typedAs(c.fix, c.want)) return fmt.Errorf("%s, so %s", c.held, typedAs(c.fix, c.want))
} }
return fmt.Errorf("%s, so to read %q as text, %s", c.held, c.fix.format, asText(c.fix)) return fmt.Errorf("%s, so to read %q as text, %s", c.held, c.fix.format, typedAs(c.fix, DataTypeString))
} }
func (c clash) textBesideRead() error { func (c clash) textBesideRead() error {
if !c.fits() { if !c.fits() {
return fmt.Errorf(`item %q is not %s, the datatype item %q takes from the column it reads; to read that column as text, %s`, c.fix.format, datatype.Noun(c.want), c.other.format, asText(c.other)) return fmt.Errorf(`item %q is not %s, the datatype item %q takes from the column it reads; to read that column as text, %s`, c.fix.format, datatype.Noun(c.want), c.other.format, typedAs(c.other, DataTypeString))
} }
return c.declareText() return c.declareText()
} }
@@ -212,12 +212,3 @@ func typedAs(t *template, d DataType) string {
} }
return fmt.Sprintf(`give %q "datatype": %q`, t.format, d) return fmt.Sprintf(`give %q "datatype": %q`, t.format, d)
} }
// asText names the spelling that reads the column a column-read item reads as text, keeping the
// other keys an object item carries.
func asText(t *template) string {
if t.fromString {
return fmt.Sprintf(`write {"format":"{text}","text":%q}`, t.format)
}
return fmt.Sprintf(`set its "format" to "{text}" and add "text": %q`, t.format)
}
+1 -24
View File
@@ -61,13 +61,7 @@ func checkToken(t grammar.Token, fields map[string]node) ([]unboundRead, error)
case grammar.NameBind: case grammar.NameBind:
return nil, checkBind(t, fields) return nil, checkBind(t, fields)
case grammar.PathRead: case grammar.PathRead:
unbound, err := checkReads(t, fields, false) return checkReads(t, fields, false)
if err != nil {
return nil, err
}
// Last, so an arm broken on its own terms is reported as that: a repeat is
// the consequence of such a mistake, not the mistake itself.
return unbound, checkNoRepeatedArm(t.Body, t.Arms)
} }
return nil, nil return nil, nil
} }
@@ -178,23 +172,6 @@ func tokenReads(t grammar.Token) []string {
return t.Arms return t.Arms
} }
// checkNoRepeatedArm rejects {a|a|b}: an alternation draws its arms evenly, so a
// repeated one is a second spelling of weight. The error names the spelling that
// does skew a draw.
func checkNoRepeatedArm(body string, names []string) error {
if len(names) < 2 {
return nil
}
seen := make(map[string]bool, len(names))
for _, name := range names {
if seen[name] {
return fmt.Errorf("token {%s}: arm %q is repeated; an alternation draws its arms evenly, so skew the odds with a choice's weights instead", body, name)
}
seen[name] = true
}
return nil
}
// formatOps is a compiled format: its ops, and the size of its literal text. // formatOps is a compiled format: its ops, and the size of its literal text.
type formatOps struct { type formatOps struct {
ops []op ops []op
+14 -3
View File
@@ -16,12 +16,23 @@ func IsTemplate(arg string) (bool, error) {
if strings.HasPrefix(strings.TrimSpace(arg), "[") { if strings.HasPrefix(strings.TrimSpace(arg), "[") {
return false, fmt.Errorf(`%q starts with "[" and is not valid JSON, so it names no template; a path starts with a name`, arg) return false, fmt.Errorf(`%q starts with "[" and is not valid JSON, so it names no template; a path starts with a name`, arg)
} }
_, err := SplitPath(CallerPath(arg)) path, err := CallerPath(arg)
if err != nil {
return false, err
}
_, err = SplitPath(path)
return false, err return false, err
} }
// CallerPath is a path as a caller passed it, without the slashes it may start with. // CallerPath is a path as a caller passed it, without the one / it may start with, as a
func CallerPath(path string) string { return strings.TrimLeft(path, "/") } // reference does.
func CallerPath(path string) (string, error) {
rest := strings.TrimPrefix(path, "/")
if strings.HasPrefix(rest, "/") {
return "", fmt.Errorf("path %s starts with more than one /; write /%s", path, strings.TrimLeft(rest, "/"))
}
return rest, nil
}
func isJSONStart(arg string) bool { func isJSONStart(arg string) bool {
return strings.HasPrefix(arg, "[") || strings.HasPrefix(arg, `"`) return strings.HasPrefix(arg, "[") || strings.HasPrefix(arg, `"`)
+8 -40
View File
@@ -1,7 +1,6 @@
package fejkdata package fejkdata
import ( import (
"encoding/json"
"fmt" "fmt"
"math" "math"
@@ -53,6 +52,7 @@ type template struct {
repeat int repeat int
separator string separator string
datatype DataType datatype DataType
declared bool // carries a "datatype", which wins over the datatype of a column it reads
fromString bool // written as a JSON string rather than an object fromString bool // written as a JSON string rather than an object
isRecord bool // compiled at the top without a repeat, so its fields are record columns isRecord bool // compiled at the top without a repeat, so its fields are record columns
unbound []unboundRead // the heads its tokens read that no field holds unbound []unboundRead // the heads its tokens read that no field holds
@@ -186,17 +186,11 @@ func compileChoice(items []any, pos position) (node, error) {
if pos == inColumn { if pos == inColumn {
itemPos = inColumn itemPos = inColumn
} }
if len(items) == 0 { switch len(items) {
case 0:
return nil, fmt.Errorf("empty choice") return nil, fmt.Errorf("empty choice")
} case 1:
if len(items) == 1 { return compileAt(items[0], pos)
if _, err := weightOf(items[0]); err != nil {
return nil, err
}
return compileItem(items[0], pos)
}
if err := checkNoRepeatedItem(items); err != nil {
return nil, err
} }
c := &choice{items: make([]node, len(items))} c := &choice{items: make([]node, len(items))}
cum := make([]float64, len(items)) cum := make([]float64, len(items))
@@ -228,34 +222,6 @@ func compileChoice(items []any, pos position) (node, error) {
return c, nil return c, nil
} }
// checkNoRepeatedItem rejects a choice that lists one item twice: a draw is even
// over the items, so a repeat is a second spelling of weight. The error names the
// spelling that does skew a draw.
func checkNoRepeatedItem(items []any) error {
seen := make(map[string]int, len(items))
for i, raw := range items {
key, isString := raw.(string)
if !isString {
b, err := json.Marshal(raw)
if err != nil {
return err
}
key = "\x00" + string(b)
}
if j, dup := seen[key]; dup {
if s, isString := raw.(string); isString {
return fmt.Errorf("choice item %q is repeated; skew the odds with a weight instead: { \"format\": %q, \"weight\": 2 }", s, s)
}
if raw == nil {
return fmt.Errorf("choice item %d repeats null; a null takes no weight, so weight the other items instead", i)
}
return fmt.Errorf("choice item %d repeats item %d; skew the odds with a weight on one of them instead", i, j)
}
seen[key] = i
}
return nil
}
func compileTemplate(m map[string]any, pos position) (node, error) { func compileTemplate(m map[string]any, pos position) (node, error) {
if _, isTable := m["rows"]; isTable { if _, isTable := m["rows"]; isTable {
return nil, fmt.Errorf("rows names a TSV beside a category's file, so only a category is a table; an inline template has no file beside it") return nil, fmt.Errorf("rows names a TSV beside a category's file, so only a category is a table; an inline template has no file beside it")
@@ -276,12 +242,13 @@ func compileTemplate(m map[string]any, pos position) (node, error) {
if err != nil { if err != nil {
return nil, err return nil, err
} }
return &template{format: o.format, tokens: toks, fields: fields, repeat: o.repeat, separator: o.separator, datatype: o.datatype, isRecord: fieldPos == inColumn, unbound: unbound}, nil return &template{format: o.format, tokens: toks, fields: fields, repeat: o.repeat, separator: o.separator, datatype: o.datatype, declared: o.declared, isRecord: fieldPos == inColumn, unbound: unbound}, nil
} }
// templateOptions is what a template object's option keys say. // templateOptions is what a template object's option keys say.
type templateOptions struct { type templateOptions struct {
datatype DataType datatype DataType
declared bool
format string format string
repeat int repeat int
separator string separator string
@@ -299,6 +266,7 @@ func readOptions(m map[string]any, pos position) (templateOptions, error) {
return o, err return o, err
} }
o.repeat = repeat o.repeat = repeat
_, o.declared = m["datatype"]
if o.datatype, err = datatypeOf(m, pos); err != nil { if o.datatype, err = datatypeOf(m, pos); err != nil {
return o, err return o, err
} }
+4 -1
View File
@@ -125,7 +125,10 @@ func literal(c Column, quote func(string) string, nullText string) string {
func (f *Generator) FakeRecord(path string) (*Record, error) { func (f *Generator) FakeRecord(path string) (*Record, error) {
f.mu.Lock() f.mu.Lock()
defer f.mu.Unlock() defer f.mu.Unlock()
path = grammar.CallerPath(path) path, err := grammar.CallerPath(path)
if err != nil {
return nil, fmt.Errorf("fejkdata: %w", err)
}
segments, err := grammar.SplitPath(path) segments, err := grammar.SplitPath(path)
if err != nil { if err != nil {
return nil, fmt.Errorf("fejkdata: %w", err) return nil, fmt.Errorf("fejkdata: %w", err)
+4 -1
View File
@@ -16,7 +16,10 @@ import (
func (f *Generator) Fake(path string) (string, error) { func (f *Generator) Fake(path string) (string, error) {
f.mu.Lock() f.mu.Lock()
defer f.mu.Unlock() defer f.mu.Unlock()
path = grammar.CallerPath(path) path, err := grammar.CallerPath(path)
if err != nil {
return "", fmt.Errorf("fejkdata: %w", err)
}
segments, err := grammar.SplitPath(path) segments, err := grammar.SplitPath(path)
if err != nil { if err != nil {
return "", fmt.Errorf("fejkdata: %w", err) return "", fmt.Errorf("fejkdata: %w", err)
+2 -2
View File
@@ -101,10 +101,10 @@ type columnRead struct {
} }
// columnReadOf is the record's column t's format reads, where the format only reads one reference // columnReadOf is the record's column t's format reads, where the format only reads one reference
// or name, and nil where it does not. // or name, and nil where it does not or t declares a string, which reads the column as text.
func columnReadOf(t *template, targets map[*nameBinding]nameTarget) *columnRead { func columnReadOf(t *template, targets map[*nameBinding]nameTarget) *columnRead {
ops := t.compiled.ops ops := t.compiled.ops
if t.repeat != 1 || len(ops) != 1 || ops[0].Kind != grammar.PathRead || len(ops[0].arms) != 1 { if t.declared && t.datatype == DataTypeString || t.repeat != 1 || len(ops) != 1 || ops[0].Kind != grammar.PathRead || len(ops[0].arms) != 1 {
return nil return nil
} }
a := ops[0].arms[0] a := ops[0].arms[0]
+4 -1
View File
@@ -228,7 +228,10 @@ func tagValue(sf reflect.StructField, tag string) (any, error) {
case inline: case inline:
return inputValue(tag), nil return inputValue(tag), nil
} }
path := grammar.CallerPath(tag) path, err := grammar.CallerPath(tag)
if err != nil {
return nil, err
}
if err := grammar.CheckPathIdentifiers(path); err != nil { if err := grammar.CheckPathIdentifiers(path); err != nil {
return nil, err return nil, err
} }
+2 -7
View File
@@ -16,17 +16,12 @@ type valueProof struct {
columnMemo map[node]proven.Facts columnMemo map[node]proven.Facts
} }
// checkDatatype rejects a typed column item some render of which is not text of its datatype, // checkDatatype rejects a typed column item some render of which is not text of its datatype.
// and one restating the datatype of the column it is.
// docs/decisions.md#a-column-that-only-reads-one-reference-or-name-is-the-column-it-reads
func (p *valueProof) checkDatatype(label string, n node) error { func (p *valueProof) checkDatatype(label string, n node) error {
t, ok := n.(*template) t, ok := n.(*template)
if !ok || t.datatype == DataTypeString { if !ok || t.datatype == DataTypeString {
return nil return nil
} }
if r := t.readsColumn; r != nil && columnDatatype(r.column) == t.datatype {
return fmt.Errorf(`%s: %s takes datatype %s from the column it reads; drop "datatype"`, label, t.format, t.datatype)
}
if reason := p.proveColumnItem(t).Not[t.datatype]; reason != "" { if reason := p.proveColumnItem(t).Not[t.datatype]; reason != "" {
return fmt.Errorf("%s: datatype %s: %s", label, t.datatype, reason) return fmt.Errorf("%s: datatype %s: %s", label, t.datatype, reason)
} }
@@ -38,7 +33,7 @@ func (p *valueProof) checkDatatype(label string, n node) error {
func (p *valueProof) checkField(label string, ft reflect.Type, column node) error { func (p *valueProof) checkField(label string, ft reflect.Type, column node) error {
items, _ := columnItems(column) items, _ := columnItems(column)
for _, it := range items { for _, it := range items {
if it.datatype != DataTypeString { if it.declared {
return fmt.Errorf("%s: its Go type %s sets the datatype; drop \"datatype\"", label, ft) return fmt.Errorf("%s: its Go type %s sets the datatype; drop \"datatype\"", label, ft)
} }
} }