Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

ADR-0009: Version 2 configuration only, held as a JSON value and validated against an embedded schema

Status: accepted · Date: 2026-10-13 · Decision makers: James Hebden

Context and Problem Statement

The Go tool moved from a v1 to a v2 configuration format (repositories, deployers, reloaders) and kept migration code. The Rust port had to decide which versions to load, how to represent a config that is merged from several sources (a base file, a local overlay, extends: fragments, and project files from cloned repositories), and where validation happens relative to variable expansion.

Decision Drivers

  • Deep-merging YAML from several sources, with some keys appending rather than replacing
  • Rejecting bad config with errors that point at the field
  • Schema as the single source of truth (it also drives the generated reference docs)
  • Don’t carry a v1 loader nobody needs

Considered Options

  1. Parse into serde_json::Value, merge with RFC 7386-style rules plus append-list keys, expand variables, validate against an embedded draft-07 JSON Schema, then deserialise into typed structs; v2 only
  2. Deserialise straight into typed structs and merge the structs
  3. Keep v1 loading and run the migration at load time

Decision Outcome

Chosen option: “JSON value, embedded schema, v2 only”, because merging untyped values is simpler than merging typed structs with optional fields, and the same schema file that the Go tool ships (schema/config.v2.schema.json, schema/caproni-project.schema.json) can be embedded with include_str!. config migrate exists but can only report “already latest”, because only v2 exists.

Variable expansion is hand-written rather than a shell. In configuration values $VAR, ${VAR}, length, indirection, slices, replacement, and prefix and suffix removal are supported, and the :-, :?, :+, and := forms are rejected as unsupported. Inside a variables: definition ${VAR:-default}, ${VAR:?message}, and ${VAR:+alternate} are also supported. $(...), backticks, and arithmetic are preserved verbatim and never evaluated. Remote extends: fragments must be https and named xianyun*.yaml or caproni*.yaml, capped at 10 MiB.

Consequences

Good:

  • One schema, shared with the Go tool and the docs
  • No command substitution in config values

Bad:

  • Expansion falls back to any process environment variable, and config print shows the result (measured 2026-10-13) (TM-CFG-003)
  • Remote fragments have no pinning or signature, and relative references inside them may accept http:// (TM-CFG-001, TM-CFG-002)
  • A project file from a cloned edit-mode repository can add hooks (TM-CFG-004)
  • Schema validation runs after expansion, and the schema is recompiled on every load
  • serde_yaml is deprecated, and alias expansion is unbounded (TM-SUP-002, TM-CFG-006)
  • The schema text still says the default static name is caproni

Neutral:

  • The persistent file cache for remote fragments is ported but not wired in, so --no-cache is currently a no-op

Implementation Location

  • crates/xianyun-core/src/config/caproni.rs
  • crates/xianyun-core/src/config/merge.rs
  • crates/xianyun-core/src/variables/expand.rs
  • crates/xianyun-core/src/variables/resolver.rs
  • schema/config.v2.schema.json

More Information