Copperlace Configuration

Copperlace reads configuration into a RuleSet. The current parser accepts HOCON syntax, but the renderer behavior described here is independent of that implementation detail.

Root Shape

The config root must be an object. Any non-object root fails compilation with InvalidConfigRoot.

origin = "{% hero:name %}{story}"
name = ["Mia"]
story = "{hero} walked home."

Top-Level Keys

Top-level keys become named rules unless the key is context and its value is an object. Path keys and nested objects can define dotted rule names.

origin = "{story}"
story = "A {mood} path"
mood = [bright, quiet]
name.first = ["Mia"]

Each top-level rule can be rendered directly by passing its key to RuleSet::render_rule. Dotted leaves can also be rendered directly, such as RuleSet::render_rule("name.first").

The selected top-level rule’s value shape also determines whether it is a text or structured render target:

  • string-valued top-level rules render as text;

  • list-valued top-level rules render as random text choices;

  • object-valued top-level rules can render structurally.

See Structured output for examples and CLI mode inference.

Special context Object

When top-level context is an object, its entries become lazy default values used by template references. They are not inserted into the normal rule table, but they can still be rendered by name through render_rule.

context = {
  hero = "{name}"
}

If top-level context is not an object, it is treated as a normal rule named context.

Callers may also pass initial render context values to a render call through Rust APIs, wrapper render overloads, JS/WASM context methods, or CLI --set key=value. Initial context values are strings, resolve before config-defined context defaults and named rules, and do not persist after the render finishes.

String Values

Strings are parsed as templates. Each {…​} section becomes a render-time expression that appends text to the output. Each {% …​ %} section becomes a render-time statement that may update render state but appends no text. All other text remains literal.

origin = "Hello {name}"

Literal expression braces can be escaped in template text with \{ and \}. Literal statement delimiters can be escaped with \{% and %\}. In normal quoted strings, escape the backslash itself as \\{, \\}, \\{%, or %\\}. In triple-quoted strings, write \{, \}, \{%, or %\} directly.

inline = "\\{name\\}"
json = """\{
  "name": {name | quote}
\}"""
statement = """\{% hero:name %\}"""

Supported expressions:

  • {rule} resolves a bound value, context default, or named rule.

  • {rule | processor} renders rule and passes the result through one or more processors from left to right.

Supported statements:

  • {% alias:rule %} binds the rendered rule value to alias if alias is not already bound for this render.

  • {% alias:=rule %} always renders rule, stores the result under alias, and replaces any existing value.

Binding statements can also use processors. {% alias:rule | uppercase %} stores the processed result under alias; {% alias:=rule | uppercase %} overwrites alias with the processed result.

Overwrite bindings are parsed before bind-if-missing bindings, so {% alias:=rule %} is distinct from {% alias:rule %}.

Supported builtin processors are:

  • uppercase

  • lowercase

  • trim

  • capitalize

  • titlecase

  • article

  • past_tense

  • pluralize

  • singularize

  • possessive

  • present_participle

  • ordinal

  • sentence

  • quote

  • slug

The article processor prefixes the rendered value with a or an using English heuristics. It preserves the rendered value as-is, so use trim first when surrounding whitespace should be ignored.

item = ["apple", "user", "hour"]
origin = "You found {item | article}."

The past_tense processor converts one verb token to past tense using regular spelling rules and common irregular verbs. It preserves surrounding whitespace but returns an error for blank values or multi-word phrases.

action = ["walk", "run", "try"]
origin = "Mia {action | past_tense}."

The pluralize processor converts one noun token to plural form using regular spelling rules and common irregular nouns. It preserves surrounding whitespace but returns an error for blank values or multi-word phrases.

creature = ["cat", "person", "city"]
origin = "The town has many {creature | pluralize}."

The singularize processor converts one plural noun token to singular form using common reverse spelling rules and irregular nouns. It preserves surrounding whitespace but returns an error for blank values or multi-word phrases.

creatures = ["cats", "people", "cities"]
origin = "One {creatures | singularize} waits."

The possessive processor adds an English possessive suffix to one token. It preserves surrounding whitespace but returns an error for blank values or multi-word phrases.

name = ["Mia", "James"]
origin = "{name | possessive} lantern glows."

The present_participle processor converts one verb token to its -ing form. It preserves surrounding whitespace but returns an error for blank values or multi-word phrases.

action = ["walk", "run", "lie"]
origin = "Mia is {action | present_participle}."

The ordinal processor adds an English ordinal suffix to one integer token. It preserves surrounding whitespace but returns an error for blank, multi-word, or non-integer values.

rank = [1, 2, 3, 11]
origin = "Mia finished {rank | ordinal}."

The sentence processor capitalizes the first alphabetic character in the rendered string and leaves the rest unchanged.

line = ["hello MIA"]
origin = "{line | sentence}"

The quote processor wraps the rendered string in ASCII double quotes and escapes internal double quotes and backslashes.

line = ["Mia said \"hi\""]
origin = "{line | quote}"

The slug processor lowercases rendered text, trims it, converts runs of non-alphanumeric characters to hyphens, removes apostrophes, and strips leading or trailing hyphens.

title = ["Mia's Story"]
origin = "{title | slug}"

Array Values

Arrays are random choices. The selected element is rendered using the same value rules as a top-level config value.

mood = [vexed, wistful, astute]

Arrays may contain weighted entries. A weighted entry is an object with only value and weight fields. The value field is rendered using the same rules as any other array element, and weight must be a finite non-negative number. If any array entry is weighted, plain entries in the same array receive weight 1.0.

mood = [
  { value = vexed, weight = 6 },
  { value = wistful, weight = 2.5 },
  astute
]

Individual zero weights are accepted, but a weighted choice must have at least one entry with a positive weight. Invalid weighted choices fail during RuleSet construction.

An empty array is accepted during RuleSet construction, but rendering it fails with EmptyChoice.

Inside an object-valued structured rule, arrays preserve their shape instead of acting as random choices. Top-level list structured rendering is not supported in v1; top-level list rules remain text choices.

Arrays inside object-valued structured rules are preserved for structured rendering. When a nested array looks like a malformed weighted text choice, it is still accepted as structured data; rendering that array as a dotted text rule fails with UnsupportedValue("array").

Scalar Values

Scalar values that are not strings, arrays, or objects are converted to strings using the parsed value’s to_string behavior.

count = 3
origin = "Count: {count}"

Object Values

Objects can organize rules into dotted names. Path keys and nested object syntax are equivalent, so name.first = ["Mia"] and name { first = ["Mia"] } both define a rule named name.first.

name {
  first = ["Mia"]
  family = ["Darcy"]
}

origin = "{name.first} {name.family}"

Object parents are not directly renderable. Rendering name in the example above fails with UnsupportedValue("object").

Object-valued top-level rules are structured render targets. Structured rendering preserves nested objects and arrays, while string leaves still use normal templates, rule calls, processors, bindings, context defaults, and initial context values.

Objects are also supported as the special top-level context value and as weighted choice entries inside arrays. Nested context values use the same dotted name behavior.

origin = { value = "nested" }

Example Configuration

name = ["Arjun", "Yuuma", "Darcy", "Mia", "Chiaki", "Izzi", "Azra", "Lina"]

animal = [unicorn, raven, sparrow, scorpion, coyote, eagle, owl,
    lizard, zebra, duck, kitten]

mood = [vexed, indignant, impassioned, wistful, astute, courteous]

story = [
  "{hero} traveled with her pet {heroPet}. {hero} was never {mood}, for the {heroPet} was always too {mood}."
]

origin = "{% hero:name %}{% heroPet:animal %}{story}"

context = {
  hero = "{name}"
  heroPet = "{animal}"
}