diff --git a/README.md b/README.md index 4f36582..e611941 100644 --- a/README.md +++ b/README.md @@ -31,9 +31,9 @@ spot. Its tokens reach the data by reference from the root — `{/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}` and `{..name}` are rejected naming the root spelling, and one reference alone — -`{/sv_SE.person}` — is the path written as a template, rejected naming the path. A -path never contains a brace, a bracket or a quote, so the two cannot collide (see -[Decisions](#decisions)). +`{/sv_SE.person}` — is the path written as a template, rejected naming the path, as is +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 | | |------|--| @@ -191,9 +191,9 @@ which a [`null`](#null) item leaves nil. An integer stays within int64 whatever 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, and `fake:"-"` -leaves it unfilled. Untagged fields keep their values, and so does a pointer back to a -struct already being filled. The first call for a type compiles its tags and reports +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. 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. @@ -209,8 +209,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 `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 -last loaded. Names may not use `.`, `|`, `(`, `{`, `}`, `[`, `]`, `"` or `/`; -dot-prefixed entries are skipped, so a data directory can also be a checkout. +last loaded. Names may not use `.`, `|`, `(`, `{`, `}`, `[`, `]`, `"` or `/`, nor be +`-`, 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`, `person`, `phone`, `price`, `sentence`, `ssn`, `time`, `url`, `username`, @@ -625,9 +625,9 @@ tokens add cost in proportion to the output. 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 it - unfilled, whatever a category named `-` holds, and a pointer back to a struct - already being filled is left alone, since filling it would never end. `New` cannot + 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. diff --git a/inline.go b/inline.go index 7dac749..1e29542 100644 --- a/inline.go +++ b/inline.go @@ -52,8 +52,8 @@ func (f *Generator) FakeTemplate(input string) (string, error) { // IsTemplate reports whether arg is an inline template rather than a path, by its shape: a { // 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 does a template of one reference alone, which is a path -// written as a template. +// 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 { @@ -72,6 +72,9 @@ func isTemplate(arg string) (bool, error) { 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 len(arg) > 1 && strings.HasPrefix(arg, "/") { + return false, fmt.Errorf("path %s starts with /, and every path starts at the root already; write %s", arg, arg[1:]) + } return false, nil } diff --git a/node.go b/node.go index cef1bc7..2a44c01 100644 --- a/node.go +++ b/node.go @@ -378,13 +378,17 @@ const reservedInName = ".|({}/[]\"" // reservedList spells reservedInName for an error message, so the two cannot drift. 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 // what a name may contain. func checkName(name string) error { if 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 { 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) diff --git a/struct.go b/struct.go index 92c8885..380223b 100644 --- a/struct.go +++ b/struct.go @@ -206,8 +206,6 @@ func tagValue(sf reflect.StructField, tag string) (any, error) { return nil, err case inline: return inputValue(tag) - case strings.HasPrefix(tag, "/"): - return nil, fmt.Errorf("path %q starts with /, and every path starts at the root already; write %s", tag, tag[1:]) } for _, seg := range strings.Split(tag, ".") { if err := checkName(seg); err != nil { @@ -290,13 +288,23 @@ var columnKinds = map[reflect.Kind]columnKind{ reflect.Uint8: {DataTypeInteger, 0, math.MaxUint8, reflect.Int64}, } -// reach is the least and greatest value v proves a field of this kind receives. An integer -// prints whole, so its bounds round inward. -func (k columnKind) reach(v proven) (lo, hi float64) { - if k.datatype == DataTypeInteger { - return math.Ceil(v.lo), math.Floor(v.hi) +// past is the bound of v a field of this kind cannot hold, if either is. An integer prints +// whole, so its bounds round inward first. +func (k columnKind) past(v proven) (float64, bool) { + lo, hi := v.lo, v.hi + switch k.datatype { + case DataTypeString, DataTypeBoolean: + return 0, false + case DataTypeInteger: + lo, hi = math.Ceil(lo), math.Floor(hi) } - return v.lo, v.hi + switch { + case lo < k.lo: + return lo, true + case hi > k.hi: + return hi, true + } + return 0, false } // checkField rejects a column some render of which a field of Go type ft cannot hold: a null @@ -318,15 +326,8 @@ func (p *valueProof) checkField(label string, ft reflect.Type, column node) erro if reason := v.not[kind.datatype]; reason != "" { return fmt.Errorf("%s (%s): %s", label, ft, reason) } - if kind.datatype == DataTypeBoolean { - continue - } - if lo, hi := kind.reach(v); lo < kind.lo || hi > kind.hi { - past := hi - if lo < kind.lo { - past = lo - } - return fmt.Errorf("%s (%s): %q can reach %s, past %s; make it %s", label, ft, it.format, strconv.FormatFloat(past, 'g', -1, 64), elem.Kind(), kind.wider) + if bound, over := kind.past(v); over { + return fmt.Errorf("%s (%s): %q can reach %s, past %s; make it %s", label, ft, it.format, strconv.FormatFloat(bound, 'g', -1, 64), elem.Kind(), kind.wider) } } return nil