harness.toml loading via a stdlib-only TOML subset parser (tables, bare
keys, strings/ints/bools, multi-line arrays; anything richer fails loudly).
Secrets are refs only (env:/file:/literal:, bw: reserved) and are redacted
from every error path. Model routing v0: static class -> tier alias ->
concrete model map per the DESIGN model-selection layer; requests carry the
resolved concrete model and unknown classes fail hard so routing stays
auditable. Table-driven tests cover the parser, validation, key refs, and
routing decisions.
💘 Generated with Crush
Assisted-by: Crush:glm-5.2
426 lines
9.5 KiB
Go
426 lines
9.5 KiB
Go
// Package config implements the harness.toml surface for the MOPAC harness.
|
|
//
|
|
// The TOML parser in toml.go is a deliberate stdlib-only subset: tables,
|
|
// bare keys, basic strings, integers, booleans, and (single- or multi-line)
|
|
// string/int/bool arrays. Anything richer is a parse error so misconfigured
|
|
// files fail loudly instead of silently.
|
|
package config
|
|
|
|
import (
|
|
"fmt"
|
|
"os"
|
|
"sort"
|
|
"strings"
|
|
)
|
|
|
|
// TOMLDoc is the parsed representation of harness.toml: nested tables are
|
|
// TOMLDoc values; leaves are string, int64, bool, or []any of those.
|
|
type TOMLDoc map[string]any
|
|
|
|
// Table returns the nested table at the given key path, or an empty doc.
|
|
func (d TOMLDoc) Table(keys ...string) TOMLDoc {
|
|
cur := any(d)
|
|
for _, k := range keys {
|
|
m, ok := cur.(TOMLDoc)
|
|
if !ok {
|
|
return TOMLDoc{}
|
|
}
|
|
cur = m[k]
|
|
}
|
|
if m, ok := cur.(TOMLDoc); ok {
|
|
return m
|
|
}
|
|
return TOMLDoc{}
|
|
}
|
|
|
|
func (d TOMLDoc) String(key string) (string, bool) {
|
|
v, ok := d[key].(string)
|
|
return v, ok
|
|
}
|
|
|
|
func (d TOMLDoc) Int(key string) (int64, bool) {
|
|
v, ok := d[key].(int64)
|
|
return v, ok
|
|
}
|
|
|
|
func (d TOMLDoc) Bool(key string) (bool, bool) {
|
|
v, ok := d[key].(bool)
|
|
return v, ok
|
|
}
|
|
|
|
func (d TOMLDoc) StringList(key string) ([]string, bool) {
|
|
raw, ok := d[key].([]any)
|
|
if !ok {
|
|
return nil, false
|
|
}
|
|
out := make([]string, 0, len(raw))
|
|
for _, e := range raw {
|
|
s, ok := e.(string)
|
|
if !ok {
|
|
return nil, false
|
|
}
|
|
out = append(out, s)
|
|
}
|
|
return out, true
|
|
}
|
|
|
|
// Keys returns the table's key names in sorted order.
|
|
func (d TOMLDoc) Keys() []string {
|
|
out := make([]string, 0, len(d))
|
|
for k := range d {
|
|
out = append(out, k)
|
|
}
|
|
sort.Strings(out)
|
|
return out
|
|
}
|
|
|
|
// ParseTOML parses the harness.toml subset.
|
|
func ParseTOML(src string) (TOMLDoc, error) {
|
|
lines, err := logicalLines(src)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
root := TOMLDoc{}
|
|
cur := root
|
|
for _, ln := range lines {
|
|
if strings.HasPrefix(ln.text, "[") {
|
|
path, err := parseTableName(ln.text, ln.no)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
cur, err = descend(root, path, ln.no)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
continue
|
|
}
|
|
if err := parseKeyValue(cur, ln.text, ln.no); err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
return root, nil
|
|
}
|
|
|
|
type logicalLine struct {
|
|
no int
|
|
text string
|
|
}
|
|
|
|
// logicalLines joins physical lines into logical ones: comments are stripped
|
|
// (quote-aware), blank lines dropped, and unbalanced array brackets pull the
|
|
// next line in so multi-line arrays work. Strings may not span lines.
|
|
func logicalLines(src string) ([]logicalLine, error) {
|
|
var out []logicalLine
|
|
var buf strings.Builder
|
|
curNo := 0
|
|
depth := 0
|
|
for i, raw := range strings.Split(src, "\n") {
|
|
no := i + 1
|
|
line := stripComment(strings.TrimSuffix(raw, "\r"))
|
|
if buf.Len() == 0 {
|
|
trimmed := strings.TrimSpace(line)
|
|
if trimmed == "" {
|
|
continue
|
|
}
|
|
curNo = no
|
|
buf.WriteString(trimmed)
|
|
} else {
|
|
buf.WriteString(" ")
|
|
buf.WriteString(strings.TrimSpace(line))
|
|
}
|
|
inStr, esc := false, false
|
|
d := 0
|
|
for _, r := range line {
|
|
switch {
|
|
case esc:
|
|
esc = false
|
|
case inStr && r == '\\':
|
|
esc = true
|
|
case r == '"':
|
|
inStr = !inStr
|
|
case !inStr && r == '[':
|
|
d++
|
|
case !inStr && r == ']':
|
|
d--
|
|
}
|
|
}
|
|
if inStr {
|
|
return nil, fmt.Errorf("harness.toml:%d: unterminated string", no)
|
|
}
|
|
if depth+d < 0 {
|
|
return nil, fmt.Errorf("harness.toml:%d: unbalanced ']'", no)
|
|
}
|
|
depth += d
|
|
if depth == 0 {
|
|
out = append(out, logicalLine{no: curNo, text: buf.String()})
|
|
buf.Reset()
|
|
}
|
|
}
|
|
if depth != 0 {
|
|
return nil, fmt.Errorf("harness.toml:%d: unterminated array (missing ']')", curNo)
|
|
}
|
|
return out, nil
|
|
}
|
|
|
|
// stripComment removes a trailing # comment, ignoring # inside strings.
|
|
func stripComment(line string) string {
|
|
inStr, esc := false, false
|
|
for i, r := range line {
|
|
switch {
|
|
case esc:
|
|
esc = false
|
|
case inStr && r == '\\':
|
|
esc = true
|
|
case r == '"':
|
|
inStr = !inStr
|
|
case r == '#' && !inStr:
|
|
return line[:i]
|
|
}
|
|
}
|
|
return line
|
|
}
|
|
|
|
func parseTableName(text string, no int) ([]string, error) {
|
|
if !strings.HasSuffix(text, "]") {
|
|
return nil, fmt.Errorf("harness.toml:%d: malformed table header %q", no, text)
|
|
}
|
|
inner := strings.TrimSuffix(strings.TrimPrefix(text, "["), "]")
|
|
if inner == "" {
|
|
return nil, fmt.Errorf("harness.toml:%d: empty table name", no)
|
|
}
|
|
parts := strings.Split(inner, ".")
|
|
for _, p := range parts {
|
|
if !isBareKey(p) {
|
|
return nil, fmt.Errorf("harness.toml:%d: invalid table name segment %q", no, p)
|
|
}
|
|
}
|
|
return parts, nil
|
|
}
|
|
|
|
func descend(root TOMLDoc, path []string, no int) (TOMLDoc, error) {
|
|
cur := root
|
|
for _, k := range path {
|
|
if existing, ok := cur[k]; ok {
|
|
sub, ok := existing.(TOMLDoc)
|
|
if !ok {
|
|
return nil, fmt.Errorf("harness.toml:%d: key %q already has a value, cannot become table", no, k)
|
|
}
|
|
cur = sub
|
|
continue
|
|
}
|
|
sub := TOMLDoc{}
|
|
cur[k] = sub
|
|
cur = sub
|
|
}
|
|
return cur, nil
|
|
}
|
|
|
|
func parseKeyValue(m TOMLDoc, text string, no int) error {
|
|
eq := strings.IndexByte(text, '=')
|
|
if eq < 0 {
|
|
return fmt.Errorf("harness.toml:%d: expected key = value, got %q", no, text)
|
|
}
|
|
key := strings.TrimSpace(text[:eq])
|
|
if !isBareKey(key) {
|
|
return fmt.Errorf("harness.toml:%d: invalid key %q", no, key)
|
|
}
|
|
valStr := strings.TrimSpace(text[eq+1:])
|
|
if valStr == "" {
|
|
return fmt.Errorf("harness.toml:%d: missing value for key %q", no, key)
|
|
}
|
|
val, err := parseValue(valStr, no)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if _, exists := m[key]; exists {
|
|
return fmt.Errorf("harness.toml:%d: duplicate key %q", no, key)
|
|
}
|
|
m[key] = val
|
|
return nil
|
|
}
|
|
|
|
func isBareKey(s string) bool {
|
|
if s == "" {
|
|
return false
|
|
}
|
|
for _, r := range s {
|
|
switch {
|
|
case r >= 'a' && r <= 'z', r >= 'A' && r <= 'Z', r >= '0' && r <= '9', r == '_', r == '-':
|
|
default:
|
|
return false
|
|
}
|
|
}
|
|
return true
|
|
}
|
|
|
|
func parseValue(v string, no int) (any, error) {
|
|
switch {
|
|
case strings.HasPrefix(v, `"`):
|
|
return parseStringValue(v, no)
|
|
case strings.HasPrefix(v, "["):
|
|
return parseArrayValue(v, no)
|
|
default:
|
|
return parseScalar(v, no)
|
|
}
|
|
}
|
|
|
|
func parseScalar(v string, no int) (any, error) {
|
|
switch v {
|
|
case "true":
|
|
return true, nil
|
|
case "false":
|
|
return false, nil
|
|
}
|
|
n, err := parseTOMLInt(v)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("harness.toml:%d: unsupported value %q (want string, int, bool, or array)", no, v)
|
|
}
|
|
return n, nil
|
|
}
|
|
|
|
func parseTOMLInt(v string) (int64, error) {
|
|
s := strings.ReplaceAll(v, "_", "")
|
|
return parseSignedInt(s)
|
|
}
|
|
|
|
func parseSignedInt(s string) (int64, error) {
|
|
var n int64
|
|
i := 0
|
|
neg := false
|
|
if i < len(s) && (s[i] == '+' || s[i] == '-') {
|
|
neg = s[i] == '-'
|
|
i++
|
|
}
|
|
if i >= len(s) {
|
|
return 0, fmt.Errorf("not an integer: %q", s)
|
|
}
|
|
for ; i < len(s); i++ {
|
|
if s[i] < '0' || s[i] > '9' {
|
|
return 0, fmt.Errorf("not an integer: %q", s)
|
|
}
|
|
n = n*10 + int64(s[i]-'0')
|
|
if n < 0 {
|
|
return 0, fmt.Errorf("integer overflow: %q", s)
|
|
}
|
|
}
|
|
if neg {
|
|
n = -n
|
|
}
|
|
return n, nil
|
|
}
|
|
|
|
func parseStringValue(v string, no int) (string, error) {
|
|
if len(v) < 2 {
|
|
return "", fmt.Errorf("harness.toml:%d: unterminated string %q", no, v)
|
|
}
|
|
var b strings.Builder
|
|
i := 1
|
|
for i < len(v) {
|
|
c := v[i]
|
|
if c == '"' {
|
|
if rest := strings.TrimSpace(v[i+1:]); rest != "" {
|
|
return "", fmt.Errorf("harness.toml:%d: unexpected trailing content after string: %q", no, rest)
|
|
}
|
|
return b.String(), nil
|
|
}
|
|
if c != '\\' {
|
|
b.WriteByte(c)
|
|
i++
|
|
continue
|
|
}
|
|
if i+1 >= len(v) {
|
|
return "", fmt.Errorf("harness.toml:%d: dangling escape in %q", no, v)
|
|
}
|
|
switch v[i+1] {
|
|
case '"':
|
|
b.WriteByte('"')
|
|
case '\\':
|
|
b.WriteByte('\\')
|
|
case 'n':
|
|
b.WriteByte('\n')
|
|
case 't':
|
|
b.WriteByte('\t')
|
|
case 'r':
|
|
b.WriteByte('\r')
|
|
default:
|
|
return "", fmt.Errorf("harness.toml:%d: unsupported escape \\%c", no, v[i+1])
|
|
}
|
|
i += 2
|
|
}
|
|
return "", fmt.Errorf("harness.toml:%d: unterminated string", no)
|
|
}
|
|
|
|
func parseArrayValue(v string, no int) ([]any, error) {
|
|
if !strings.HasSuffix(v, "]") {
|
|
return nil, fmt.Errorf("harness.toml:%d: unterminated array %q", no, v)
|
|
}
|
|
inner := strings.TrimSpace(v[1 : len(v)-1])
|
|
if inner == "" {
|
|
return []any{}, nil
|
|
}
|
|
var out []any
|
|
elems := splitTopLevel(inner, ',')
|
|
if len(elems) > 0 && strings.TrimSpace(elems[len(elems)-1]) == "" {
|
|
elems = elems[:len(elems)-1] // tolerate a trailing comma
|
|
}
|
|
for _, elem := range elems {
|
|
elem = strings.TrimSpace(elem)
|
|
if elem == "" {
|
|
return nil, fmt.Errorf("harness.toml:%d: empty array element in %q", no, v)
|
|
}
|
|
val, err := parseValue(elem, no)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
out = append(out, val)
|
|
}
|
|
return out, nil
|
|
}
|
|
|
|
// splitTopLevel splits on sep outside of double-quoted strings.
|
|
func splitTopLevel(s string, sep rune) []string {
|
|
var out []string
|
|
var cur strings.Builder
|
|
inStr, esc := false, false
|
|
for _, r := range s {
|
|
switch {
|
|
case esc:
|
|
esc = false
|
|
cur.WriteRune(r)
|
|
case inStr && r == '\\':
|
|
esc = true
|
|
cur.WriteRune(r)
|
|
case r == '"':
|
|
inStr = !inStr
|
|
cur.WriteRune(r)
|
|
case r == sep && !inStr:
|
|
out = append(out, cur.String())
|
|
cur.Reset()
|
|
default:
|
|
cur.WriteRune(r)
|
|
}
|
|
}
|
|
out = append(out, cur.String())
|
|
return out
|
|
}
|
|
|
|
// Load reads, parses, and validates harness.toml at path.
|
|
func Load(path string) (*Config, error) {
|
|
data, err := os.ReadFile(path)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("config: read %s: %w", path, err)
|
|
}
|
|
doc, err := ParseTOML(string(data))
|
|
if err != nil {
|
|
return nil, fmt.Errorf("config: %w", err)
|
|
}
|
|
cfg := Default()
|
|
if err := cfg.apply(doc); err != nil {
|
|
return nil, fmt.Errorf("config: %w", err)
|
|
}
|
|
if err := cfg.Validate(); err != nil {
|
|
return nil, fmt.Errorf("config: %w", err)
|
|
}
|
|
return cfg, nil
|
|
}
|