Reusing Content with Mixins

The same settings turn up on element after element: a sampling rate, a filter window, a trace flag. A mixin is that block of configuration written once and applied to the elements a selector picks out, instead of copied into each of them. This chapter shows how to declare one, how to choose which elements it applies to, and which value wins when a mixin and an element disagree.

Declaring a Mixin

Mixins live in a top-level $mixins list, in any configuration file of the definition. Each entry has two halves: the $ keys that select the elements it applies to, and the configuration content it contributes to each of them.

{
    $definition: "SensorLoop@1.0",
    $mixins: [
        {
            $mixin: "FastLoop",
            $type: "SensorLoop::Sensor",
            sampleRate: 1000,
            filter: {
                window: 4
            }
        },
        {
            $mixin: "Traced",
            $type: "SensorLoop::Sensor",
            trace: true
        }
    ]
}

$type is required and is the type path of the content: FastLoop holds the content of a SensorLoop::Sensor, so it may set sampleRate and filter and nothing else. $mixin names the mixin, and elements apply it by that name:

{
    $definition: "SensorLoop@1.0",
    sensors: [
        {
            name: "Wheel",
            pin: 3,
            $applyMixins: "FastLoop"
        },
        {
            name: "Brake",
            pin: 7,
            $applyMixins: [
                "FastLoop",
                "Traced"
            ],
            // Written on the element, so it wins over the mixin's value.
            sampleRate: 2000
        },
        {
            name: "Yaw",
            pin: 11,
            $applyMixins: "FastTraced"
        },
        {
            name: "Ambient",
            pin: 24
        }
    ]
}

$applyMixins takes a single name or a list and works on any entity or struct object. On a union value it applies to the member its $type selects, so the mixin’s $type names that member, never the union. The mixin and the elements applying it need not sit in the same file, only in the same definition.

Applying to Every Element of a Type

Leaving $mixin out is one filter fewer, not a different feature: the mixin then applies to every element of its $type.

{
    $definition: "SensorLoop@1.0",
    $mixins: [
        // No '$mixin', so no name filter: this one applies to every sensor.
        {
            $type: "SensorLoop::Sensor",
            trace: false
        }
    ]
}

That is how a class-wide default is written, and it is the shape a definition author ships in a .default.def.json5 or .preconfig.def.json5 file:

{
    $definition: "SensorLoop@1.0",
    $fileType: "DefaultValues",
    $mixins: [
        {
            $type: "SensorLoop::Sensor",
            sampleRate: 100
        }
    ]
}

Nothing but $fileType distinguishes those files here, so one mechanism serves the project author reusing a block and the definition author shipping defaults.

Forgetting $mixin is exactly how an unnamed mixin is written, so read the Mixin applies to count above the declaration, or hover its $type, to see how far it currently goes before relying on it.

$type may also name a type others inherit from, in which case the mixin applies to every type assignable to it.

Which Value Wins

Within one $fileType, the more specific source wins:

  1. Content written directly on the element.

  2. A named mixin the element applies.

  3. An unnamed mixin of the element’s type.

Brake above sets sampleRate itself, so it keeps 2000 although FastLoop says 1000, and it applies Traced, so trace is true although the unnamed mixin says false. $fileType still decides first: a mixin in a higher-priority file beats a value written directly in a lower-priority one. Two sources of the same rank disagreeing is a merge conflict, as between two ordinary files.

Everything else follows the rules of Working with Multiple Files, a mixin’s content taking the role of a contributing file. Two consequences:

  • A nested entity written inside a mixin body ends up below every element the mixin applies to. It merges into the element’s existing child of that name, and where there is none it is created as an ordinary entity, addressable by its own path below each of those elements.

  • A mixin never creates the element it applies to, one has to be declared somewhere for the selector to pick it out. Below that element it does create what it writes, nested entities included, except when its $type is a struct type: such a mixin only fills occurrences a file already writes, so a struct nobody writes stays absent.

Building a Mixin on Others

A mixin body’s root may itself carry $applyMixins, so one mixin can build on others:

{
    $definition: "SensorLoop@1.0",
    $mixins: [
        // Builds on 'FastLoop' and 'Traced', and would win over both if it set a value itself.
        {
            $mixin: "FastTraced",
            $type: "SensorLoop::Sensor",
            $applyMixins: [
                "FastLoop",
                "Traced"
            ]
        }
    ]
}

The rule is the one above, a level up: the applying mixin wins over what it pulls in, and the deeper a mixin sits in the chain the weaker it is. How the content arrived at the element still decides first, so a mixin pulled in by a named mixin ranks as named.

A mixin may only apply one whose $type applies to its own type, checked at every step of the chain.

Content that arrives through a mixin does not pull further mixins in beyond the chain the mixin itself declares. An unnamed mixin therefore never applies to content inside a mixin body, and $applyMixins below a body root is an error rather than a silent no-op.

Where Mixins Are Allowed

$mixins and $applyMixins need a definition whose configurationFileMode is MultipleFilesEntitiesMergeable or MultipleFilesEntitiesMergeableWithPriority, the same two modes that allow fragments. A single-file definition has no second file a mixin could come from, so both keys are rejected there.

A mixin name is unique per definition, across every file: a second declaration of the same name is an error on both. It is an identifier, same as an entity name, so $mixin: "fast loop" is rejected.

Identity is rejected throughout a body, because a mixin applies to many elements and cannot give them one: name and id on the entry itself, and id on anything nested inside it, which would end up on every element the mixin applies to. A nested name is fine, it identifies the child below each element the mixin applies to, and that is what the by-identity merge goes by.