Reject options that cannot take effect, and skip entries that are not data

This commit is contained in:
M
2026-08-28 00:17:39 +02:00
committed by lilleman-tw
parent 8d0393713c
commit b14caae3a8
4 changed files with 63 additions and 6 deletions
+39 -2
View File
@@ -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
}