Code Generation

This section covers generating Rust and Python type bindings from definition files. The generated types deserialize .cfg.json5 files at runtime and re-check the definition’s value bounds (min, max, a string’s length and pattern), so the consuming software works with a typed, validated model instead of raw JSON. Neither the generated types nor the JSON Schema enforce the constraints of a definition.

Invocation

cfu def generate <workspace-path> -i <Definition> \
    --schema-output <dir> \
    --rust-code-output <dir> \
    --python-code-output <dir> [--python-output-style chiisai|forge]
  • -i, --definition selects what to generate: a definition name or the path of a .def file or folder. May be repeated. Required.

  • At least one output option must be given. --schema-output writes JSON Schemas (a .def.schema.json per definition and merged schemas), the code outputs write one source file per definition. For a definition in a mergeable file mode, the strict <Name>.merged.schema.json validates the fully merged configuration, and an additional <Name>.schema.json treats every field as optional so that individual partial files validate against it. Every generated schema is self-validated, and a result that is not a valid JSON Schema is reported as an error.

  • -t, --continuous regenerates whenever an input changes, which pairs well with a running build.

Run the generation after every definition change so the artifacts stay in sync.

The Example

The chapters below use the TurnIndicator getting-started definition:

indicator: {
    title: "Turn indicator function",
    description: "Settings of the direction indicator flasher.",
    multiplicity: "1..1",
    entity: {
        flashFrequency: {
            title: "Flash frequency",
            description: "How often the indicator lamps flash per second.",
            multiplicity: "1..1",
            u8: {
                min: 1,
                max: 3,
                unit: "Hz"
            }
        },
        dimmingLevel: {
            description: "PWM duty cycle for the indicator lamps in percent.",
            u8: {
                min: 0,
                max: 100,
                default: 100,
                unit: "%"
            }
        },
        comfortMode: {
            description: "Behavior when the driver tips the indicator stalk.",
            enum: {
                literals: [
                    "OFF",
                    "THREE_FLASHES",
                    "FIVE_FLASHES"
                ],
                default: "THREE_FLASHES"
            }
        }
    }
}

The generated files shown are real artifacts produced from this definition, so they always match the current generator.

Rust Bindings

--rust-code-output writes one file per definition, named after the definition in snake_case (turn_indicator.rs). A path ending in .rs is the target file itself, which keeps a file name other than the definition name (only one definition may be selected then). The file depends on serde and the small neoncfg runtime crate (parameter wrapper types and check helpers, without any tree-sitter or LSP dependencies).

How definition constructs map:

Definition Rust

Definition, entity, struct

struct with #[derive(Debug, Deserialize)]

Entity identity

public name: Option<String>, id: Option<String> fields

enum

Rust enum with the literals as variants

bool

BoolParameter, unpacked to bool

Integers

IntParameter<u8..i64>, hex strings parse transparently

f64

FloatParameter<f64>

string

StringParameter, unpacked to &str

linkerSymbol

plain String

filePath

StringParameter, unpacked to &str

ref

RefParameter, unpacked to a RefValue

union

a serde enum, internally tagged on $type or #[serde(untagged)]

0..1

accessor returns Result<Option<T>>

1..1

accessor returns Result<T>

lists

accessor returns Result<&[T]> or Result<Vec<T>>

Property names become snake_case fields with a #[serde(rename = "…​")] back to the configuration spelling; names that collide with Rust keywords are raw identifiers (r#ref). Parameter fields are private; access goes through generated methods that unpack the wrapper, apply the definition’s min, max, and default, and return Result:

pub fn flash_frequency(&self) -> Result<u8> {
    IntParameter::unpack_parameter_req(self.flash_frequency.as_ref(), 1, 3, None)
}

pub fn dimming_level(&self) -> Result<Option<u8>> {
    IntParameter::unpack_parameter_opt(self.dimming_level.as_ref(), 0, 100, Some(100))
}

Every title, description, range, unit, and multiplicity from the definition is emitted as a doc comment, so the definition’s documentation reaches the consuming developer in their IDE.

The complete generated file:

Details
#![allow(dead_code)]
#![allow(unused_imports)]
#![allow(deprecated)]

use anyhow::Result;
use neoncfg::{check_list, check_list_opt, check_req};
use neoncfg::params::*;
use serde::Deserialize;

/// **TurnIndicator - Turn indicator**
///
/// Turn indicator function configuration of a body ECU.
///
/// ---
///  * Configuration Type: `TurnIndicator`
///  * Kind: `Model`
#[derive(Debug, Deserialize)]
pub struct TurnIndicator {
    /// The name of this entity, used for referencing and error reporting.
    pub name: Option<String>,

    /// The global unique ID of this entity, used for global referencing. It will also set the name of the entity, if not set.
    pub id: Option<String>,

    /// **Indicator - Turn indicator function**
    ///
    /// Settings of the direction indicator flasher.
    ///
    /// ---
    ///  * Configuration Type: `TurnIndicator::Indicator`
    ///  * Kind: `Entity`
    ///  * Multiplicity: `1..1`
    pub indicator: Option<Indicator>,
}

impl TurnIndicator {
    pub const TYPE_NAME: &str = "TurnIndicator";
}

/// **Indicator - Turn indicator function**
///
/// Settings of the direction indicator flasher.
///
/// ---
///  * Configuration Type: `TurnIndicator::Indicator`
///  * Kind: `Entity`
#[derive(Debug, Deserialize)]
pub struct Indicator {
    /// The name of this entity, used for referencing and error reporting.
    pub name: Option<String>,

    /// The global unique ID of this entity, used for global referencing. It will also set the name of the entity, if not set.
    pub id: Option<String>,

    /// **FlashFrequency - Flash frequency**
    ///
    /// How often the indicator lamps flash per second.
    ///
    /// ---
    ///  * Minimum value: `1 Hz`
    ///  * Maximum value: `3 Hz`
    ///  * Display format: `Decimal`
    ///  * Unit: `Hz`
    ///  * Configuration Type: `TurnIndicator::Indicator::FlashFrequency`
    ///  * Datatype: `u8`
    ///  * Kind: `Integer Parameter`
    ///  * Multiplicity: `1..1`
    #[serde(rename = "flashFrequency")]
    flash_frequency: Option<IntParameter<u8>>,

    /// **DimmingLevel**
    ///
    /// PWM duty cycle for the indicator lamps in percent.
    ///
    /// ---
    ///  * Minimum value: `0 %`
    ///  * Maximum value: `100 %`
    ///  * Default value: `100 %`
    ///  * Display format: `Decimal`
    ///  * Unit: `%`
    ///  * Configuration Type: `TurnIndicator::Indicator::DimmingLevel`
    ///  * Datatype: `u8`
    ///  * Kind: `Integer Parameter`
    ///  * Multiplicity: `0..1`
    #[serde(rename = "dimmingLevel")]
    dimming_level: Option<IntParameter<u8>>,

    /// **ComfortMode**
    ///
    /// Behavior when the driver tips the indicator stalk.
    ///
    /// ---
    ///  * Default value: `THREE_FLASHES`
    ///  * Possible literals:
    ///     * `OFF`
    ///     * `THREE_FLASHES`
    ///     * `FIVE_FLASHES`
    ///  * Configuration Type: `TurnIndicator::Indicator::ComfortMode`
    ///  * Datatype: `enum`
    ///  * Kind: `Enumeration Parameter`
    ///  * Multiplicity: `0..1`
    #[serde(rename = "comfortMode")]
    comfort_mode: Option<EnumParameter<ComfortMode>>,
}

impl Indicator {
    pub const TYPE_NAME: &str = "Indicator";

    /// The name of this entity, used for referencing and error reporting.
    pub fn name(&self) -> &str {
        if let Some(name) = &self.name {
            name.as_str()
        } else if let Some(id) = &self.id {
            id.as_str()
        } else {
            Self::TYPE_NAME
        }
    }

    /// How often the indicator lamps flash per second.
    ///
    /// ---
    ///  * Minimum value: `1 Hz`
    ///  * Maximum value: `3 Hz`
    ///  * Display format: `Decimal`
    ///  * Unit: `Hz`
    ///  * Configuration Type: `TurnIndicator::Indicator::FlashFrequency`
    ///  * Datatype: `u8`
    ///  * Kind: `Integer Parameter`
    ///  * Multiplicity: `1..1`
    pub fn flash_frequency(&self) -> Result<u8> {
        IntParameter::unpack_parameter_req(self.flash_frequency.as_ref(), 1, 3, None)
    }

    /// PWM duty cycle for the indicator lamps in percent.
    ///
    /// ---
    ///  * Minimum value: `0 %`
    ///  * Maximum value: `100 %`
    ///  * Default value: `100 %`
    ///  * Display format: `Decimal`
    ///  * Unit: `%`
    ///  * Configuration Type: `TurnIndicator::Indicator::DimmingLevel`
    ///  * Datatype: `u8`
    ///  * Kind: `Integer Parameter`
    ///  * Multiplicity: `0..1`
    pub fn dimming_level(&self) -> Result<Option<u8>> {
        IntParameter::unpack_parameter_opt(self.dimming_level.as_ref(), 0, 100, Some(100))
    }

    /// Behavior when the driver tips the indicator stalk.
    ///
    /// ---
    ///  * Default value: `THREE_FLASHES`
    ///  * Possible literals:
    ///     * `OFF`
    ///     * `THREE_FLASHES`
    ///     * `FIVE_FLASHES`
    ///  * Configuration Type: `TurnIndicator::Indicator::ComfortMode`
    ///  * Datatype: `enum`
    ///  * Kind: `Enumeration Parameter`
    ///  * Multiplicity: `0..1`
    pub fn comfort_mode(&self) -> Result<Option<ComfortMode>> {
        EnumParameter::unpack_parameter_opt(self.comfort_mode.as_ref(), Some(ComfortMode::ThreeFlashes))
    }
}

/// **ComfortMode**
///
/// Behavior when the driver tips the indicator stalk.
///
/// ---
///  * Default value: `THREE_FLASHES`
///  * Possible literals:
///     * `OFF`
///     * `THREE_FLASHES`
///     * `FIVE_FLASHES`
///  * Configuration Type: `TurnIndicator::Indicator::ComfortMode`
///  * Datatype: `enum`
///  * Kind: `Enumeration Parameter`
#[derive(Debug, Deserialize, Clone, Eq, PartialEq)]
pub enum ComfortMode {
    #[serde(rename = "OFF")]
    Off,
    #[serde(rename = "THREE_FLASHES")]
    ThreeFlashes,
    #[serde(rename = "FIVE_FLASHES")]
    FiveFlashes,
}

Consuming the Bindings

The generated types deserialize a configuration directly:

let cfg = json5::from_str::<TurnIndicator>(&std::fs::read_to_string(path)?)?;
let indicator = cfg.indicator.expect("validated configuration");
let hz = indicator.flash_frequency()?; // range-checked, 1..=3
let level = indicator.dimming_level()?.unwrap_or_default(); // default 100 applied

Deserialization accepts any structurally compatible JSON5; the constraint checks live in the accessors. Run cfu cfg validate in the build to catch errors early and treat accessor errors as defense in depth.

Python Bindings

--python-code-output writes one module per definition (turn_indicator.py). A path ending in .py is the target file itself, so an existing module (for example model.py) is regenerated in place and keeps its computed blocks (only one definition may be selected then). Two output styles exist:

  • chiisai (default): classes extend IBaseModel/IStructModel from the chiisai_bm package, parameters are wrapped in IParameterObject[T], and lists are ModelSequence[T].

  • forge: plain Pydantic v2 BaseModel classes (frozen), parameters are the raw Python types, lists are list[T].

A public entity or struct can carry a _codeGen annotation naming its Python module, for example _codeGen: { python: { import: "confugami_common.interfaces" } }. Generation then emits a guarded import wherever another definition uses that type, whether embedded (typeRef, inherits, a struct or union member) or as a ref target, instead of leaving it for the Global-Header computed block:

try:
    from confugami_common.interfaces import ForeignType  # type: ignore # pyright: ignore
except ImportError:
    from fsp_config_foundation import Entity as ForeignType

The except branch falls back to the generic base class under the same name, so the module still imports when the annotated package is not installed: Entity/Struct from fsp_config_foundation for forge style, IBaseModel/IStructModel from chiisai_bm for chiisai. An externally defined type without the annotation still needs a hand written Global-Header import.

Common to both styles:

Definition Python

Entity, struct

class with type_fqn_ discriminator (alias $type)

enum

StrEnum, literals as SCREAMING_SNAKE_CASE constants

Integers, f64

int / float with Annotated[…​, Ge(min), Le(max)]

string

str with MinLen/MaxLen/pattern annotations

ref

Ref["TargetType"] forward reference

union

discriminated A | B union via type_fqn_

0..1

T | None = None, or the default value

lists

sequence types with an empty default when optional

Field names become snake_case with an alias back to the configuration spelling. A name that collides with a Python keyword or a built-in type name gets a trailing underscore (str becomes str_), since a field binds its name in the class body and would otherwise shadow the built-in for the annotations that follow. Defaults from the definition become field defaults, and the generated docstrings carry the same documentation as the Rust output.

The complete generated file (chiisai style):

Details
from annotated_types import Ge, Le, Lt, MinLen, MaxLen # noqa: F401
from chiisai_bm import computed, IBaseModel, IField, IModelObject, IParameterObject, IStructModel, ModelSequence, publish, Ref, subscribe # noqa: F401
from enum import StrEnum # noqa: F401
from typing import Annotated, Literal # noqa: F401



# <COMPUTED-BLOCK Global-Header>
# </COMPUTED-BLOCK>

# ------------------ Begin Generated Classes ------------------

class ComfortModeEnum(StrEnum):
    """
    **ComfortMode**
    
    Behavior when the driver tips the indicator stalk.
    
    ---
     * Default value: `THREE_FLASHES`
     * Possible literals:
        * `OFF`
        * `THREE_FLASHES`
        * `FIVE_FLASHES`
     * Configuration Type: `TurnIndicator::Indicator::ComfortMode`
     * Datatype: `enum`
     * Kind: `Enumeration Parameter`
    """
    OFF = "OFF"
    THREE_FLASHES = "THREE_FLASHES"
    FIVE_FLASHES = "FIVE_FLASHES"
    # <COMPUTED-BLOCK TurnIndicator::Indicator::ComfortMode>
    # </COMPUTED-BLOCK>


class Indicator(IBaseModel):
    """
    **Indicator - Turn indicator function**
    
    Settings of the direction indicator flasher.
    
    ---
     * Configuration Type: `TurnIndicator::Indicator`
     * Kind: `Entity`
    """
    type_fqn_: Annotated[Literal['TurnIndicator::Indicator'], IField(alias='$type')] = 'TurnIndicator::Indicator'
    """
    type_fqn_ is used for discriminated unions, it contains the fully qualified name of the type.
    Note: The suffix _ is on purpose to avoid name clashes with fields.
    """

    flash_frequency: Annotated[IParameterObject[int], Ge(1), Le(3), IField(alias="flashFrequency")]
    """
    How often the indicator lamps flash per second.
    
    ---
     * Minimum value: `1 Hz`
     * Maximum value: `3 Hz`
     * Display format: `Decimal`
     * Unit: `Hz`
     * Configuration Type: `TurnIndicator::Indicator::FlashFrequency`
     * Datatype: `u8`
     * Kind: `Integer Parameter`
     * Multiplicity: `1..1`
    """

    dimming_level: Annotated[IParameterObject[int], Ge(0), Le(100), IField(alias="dimmingLevel")] = IParameterObject(value=100)
    """
    PWM duty cycle for the indicator lamps in percent.
    
    ---
     * Minimum value: `0 %`
     * Maximum value: `100 %`
     * Default value: `100 %`
     * Display format: `Decimal`
     * Unit: `%`
     * Configuration Type: `TurnIndicator::Indicator::DimmingLevel`
     * Datatype: `u8`
     * Kind: `Integer Parameter`
     * Multiplicity: `0..1`
    """

    comfort_mode: Annotated[IParameterObject[ComfortModeEnum], IField(alias="comfortMode")] = IParameterObject(value=ComfortModeEnum.THREE_FLASHES)
    """
    Behavior when the driver tips the indicator stalk.
    
    ---
     * Default value: `THREE_FLASHES`
     * Possible literals:
        * `OFF`
        * `THREE_FLASHES`
        * `FIVE_FLASHES`
     * Configuration Type: `TurnIndicator::Indicator::ComfortMode`
     * Datatype: `enum`
     * Kind: `Enumeration Parameter`
     * Multiplicity: `0..1`
    """

    # <COMPUTED-BLOCK TurnIndicator::Indicator>
    # </COMPUTED-BLOCK>


class TurnIndicator(IBaseModel):
    """
    **TurnIndicator - Turn indicator**
    
    Turn indicator function configuration of a body ECU.
    
    ---
     * Configuration Type: `TurnIndicator`
     * Kind: `Model`
    """
    _original_definition_version: str = "1.0"
    "The definitionVersion from the definition file. Used for version checks."

    type_fqn_: Annotated[Literal['TurnIndicator'], IField(alias='$definition')] = 'TurnIndicator'
    """
    type_fqn_ is used for discriminated unions, it contains the fully qualified name of the type.
    Note: The suffix _ is on purpose to avoid name clashes with fields.
    """

    indicator: Indicator
    """
    Settings of the direction indicator flasher.
    
    ---
     * Configuration Type: `TurnIndicator::Indicator`
     * Kind: `Entity`
     * Multiplicity: `1..1`
    """

    # <COMPUTED-BLOCK TurnIndicator>
    # </COMPUTED-BLOCK>


TurnIndicator.model_rebuild() # noqa: F821

Regeneration preserves hand written code placed between the # <COMPUTED-BLOCK …​> markers, so computed properties and helpers survive a definition change.

Limitations and Non-effects

  • An allowTypeAmbiguity: true union becomes #[serde(untagged)], so the first member whose shape fits wins. Serde cannot fall through to the next member on a value range or pattern violation the way the validator does, so a scalar union of overlapping shapes may pick a different member than cfu reports.

  • linkerSymbol is a plain String in Rust without accessor validation.

  • Two types with the same simple name (for example Indicator and Indicator::Indicator) abort Rust generation with a clash error; rename one via typeName. See the pitfalls chapter.

  • An entity declaring nameSource or idSource (see Deriving the Identity from a Parameter) gets the third step in its generated name(): the source parameter, mangled the way the parser mangles it.

  • The generated JSON Schema carries the source as a custom nameSource / idSource keyword on the entity type, in both the normal and merged schema.

  • It names the stored unit of an int or float parameter in a custom baseUnit keyword, the canonical spelling of the base unit (B for unit: "Bytes").

  • unit, display, and embeddedTypeHint are documentation. They appear in doc comments but have no effect on generated types or checks.

  • A _deprecated element is marked with the language’s own mechanism, #[deprecated(note = "…​")] in Rust and @deprecated / Field(deprecated=…​) in Python, so consumers get the warning from their own compiler or type checker. See Deprecating Elements.