Reject options that cannot take effect, and skip entries that are not data
This commit is contained in:
@@ -152,8 +152,9 @@ A `*Fakes` is **not** safe for concurrent use — create one per goroutine.
|
||||
|
||||
The library ships a ready-to-use set under [`data/`](data): one folder per locale
|
||||
(`en_US`, `sv_SE`) plus a locale-neutral `misc` folder. Point either tool at the
|
||||
whole tree, a single folder, a copy, or your own directory — anywhere on disk; no
|
||||
naming rules.
|
||||
whole tree, a single folder, a copy, or your own directory — anywhere on disk. A
|
||||
category or folder name must not contain a dot, and dot-prefixed entries are
|
||||
skipped, so a data directory can also be a checkout.
|
||||
|
||||
A directory is just a namespace. Each JSON file is a category named after the
|
||||
file; each subdirectory is a dot-path segment — folders nest exactly like JSON
|
||||
@@ -229,6 +230,12 @@ fast instead of silently skewing output.
|
||||
This yields e.g. `bar foo baz`. `repeat` must be a positive integer and
|
||||
`separator` a string, both checked at `New`.
|
||||
|
||||
`format`, `weight`, `repeat` and `separator` are the only options; **any other key
|
||||
is a field**. So write `seperator` and you get a field by that name while the
|
||||
option stays unset. `New` rejects an option that cannot take effect — a
|
||||
`separator` without a `repeat` above 1, a `weight` outside a choice — and a
|
||||
category or folder name containing a dot, which no dot path could reach.
|
||||
|
||||
**Functions.** A `{name()}` token calls a built-in function instead of rendering
|
||||
a field. `{luhn()}` appends a Luhn check digit over the digits emitted **so far**
|
||||
in the current format (non-digits skipped but kept); unknown functions or wrong
|
||||
|
||||
@@ -53,6 +53,9 @@ func loadDir(dir string) (*group, error) {
|
||||
}
|
||||
g := &group{children: map[string]node{}}
|
||||
for _, e := range entries {
|
||||
if strings.HasPrefix(e.Name(), ".") { // hidden: a checkout or an editor's file, never data
|
||||
continue
|
||||
}
|
||||
full := filepath.Join(dir, e.Name())
|
||||
if e.IsDir() {
|
||||
if isRef(e.Name()) {
|
||||
@@ -62,9 +65,13 @@ func loadDir(dir string) (*group, error) {
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(child.children) > 0 {
|
||||
g.children[e.Name()] = child
|
||||
if len(child.children) == 0 {
|
||||
continue
|
||||
}
|
||||
if err := checkName(e.Name()); err != nil {
|
||||
return nil, fmt.Errorf("%s: folder %w", full, err)
|
||||
}
|
||||
g.children[e.Name()] = child
|
||||
continue
|
||||
}
|
||||
if !strings.HasSuffix(e.Name(), ".json") {
|
||||
@@ -74,6 +81,9 @@ func loadDir(dir string) (*group, error) {
|
||||
if isRef(name) {
|
||||
return nil, fmt.Errorf("%s: category %q starts with %q, which is reserved for {..path} bindings", full, name, refPrefix)
|
||||
}
|
||||
if err := checkName(name); err != nil {
|
||||
return nil, fmt.Errorf("%s: category %w", full, err)
|
||||
}
|
||||
b, err := os.ReadFile(full)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"fmt"
|
||||
"math"
|
||||
"sort"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// node is a compiled template element: literal, choice, or template. Compiling
|
||||
@@ -50,7 +51,20 @@ type template struct {
|
||||
func (*template) isNode() {}
|
||||
|
||||
// compile converts parsed JSON into a node tree, validating structure up front.
|
||||
// Only a choice's items carry a weight, so a numeric one here would be inert.
|
||||
func compile(v any) (node, error) {
|
||||
if m, ok := v.(map[string]any); ok {
|
||||
if w, weighted := m["weight"]; weighted {
|
||||
if _, isNumber := w.(float64); isNumber {
|
||||
return nil, fmt.Errorf("weight only skews a choice's items, so it has no effect here")
|
||||
}
|
||||
}
|
||||
}
|
||||
return compileItem(v)
|
||||
}
|
||||
|
||||
// compileItem compiles one node, allowing the weight a choice item may carry.
|
||||
func compileItem(v any) (node, error) {
|
||||
switch v := v.(type) {
|
||||
case string:
|
||||
return literal(v), nil
|
||||
@@ -81,7 +95,7 @@ func compileChoice(items []any) (node, error) {
|
||||
}
|
||||
total += w
|
||||
cum[i] = total
|
||||
n, err := compile(raw)
|
||||
n, err := compileItem(raw)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -115,6 +129,9 @@ func compileTemplate(m map[string]any) (node, error) {
|
||||
if sep, ok = sv.(string); !ok {
|
||||
return nil, fmt.Errorf("separator must be a string, got %T", sv)
|
||||
}
|
||||
if repeat == 1 {
|
||||
return nil, fmt.Errorf("separator joins repeated renders, so it has no effect without a repeat above 1")
|
||||
}
|
||||
}
|
||||
t := &template{format: format, fields: make(map[string]node, len(m)), repeat: repeat, separator: sep}
|
||||
keys := make([]string, 0, len(m))
|
||||
@@ -123,7 +140,7 @@ func compileTemplate(m map[string]any) (node, error) {
|
||||
}
|
||||
sort.Strings(keys) // so which of several bad fields is reported does not vary
|
||||
for _, k := range keys {
|
||||
if k == "format" || k == "weight" || k == "repeat" || k == "separator" {
|
||||
if isOption(k) {
|
||||
continue
|
||||
}
|
||||
if isRef(k) {
|
||||
@@ -182,3 +199,23 @@ func weightOf(raw any) (float64, error) {
|
||||
}
|
||||
return w, nil
|
||||
}
|
||||
|
||||
// checkName rejects a category or folder name no dot path can reach. A dot separates
|
||||
// path segments, so such a name is unaddressable by every route — unlike a field,
|
||||
// which its parent's format still reaches by token.
|
||||
func checkName(name string) error {
|
||||
if strings.Contains(name, ".") {
|
||||
return fmt.Errorf("%q contains a dot, which a dot path cannot reach", name)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// isOption reports whether a template key configures the node instead of naming a
|
||||
// field. These four names can never be fields.
|
||||
func isOption(name string) bool {
|
||||
switch name {
|
||||
case "format", "repeat", "separator", "weight":
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -143,6 +143,9 @@ func checkTokens(format string, fields map[string]node) error {
|
||||
continue // a root reference; its target is checked at New (see linkRefs)
|
||||
}
|
||||
if _, ok := fields[name]; !ok {
|
||||
if isOption(name) {
|
||||
return fmt.Errorf("token {%s}: %q is an option, never a field", t.body, name)
|
||||
}
|
||||
return fmt.Errorf("token {%s}: no field %q", t.body, name)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user