Reusable Types and Inheritance

Larger definitions repeat structure: the same filter block on several parameters, common properties shared by all sensor kinds. This chapter covers the three reuse mechanisms: named types with typeRef, entity inheritance with inherits, and visibility control with scope.

Named Types: The types Array

Types that are used in more than one place are declared once in the top level types array. Each entry is a regular element, but it has no property name, so it must carry an explicit typeName:

types: [
    {
        typeName: "SensorBase",
        title: "Common sensor properties",
        entity: {
            sampleDivider: {
                description: "Sample every n-th acquisition cycle.",
                u8: {
                    min: 1,
                    max: 64,
                    default: 1
                }
            }
        }
    },
    {
        typeName: "Filter",
        title: "First order low pass filter",
        struct: {
            cutoff: {
                multiplicity: "1..1",
                f64: {
                    min: 0.1,
                    max: 1000.0,
                    unit: "Hz"
                }
            },
            order: {
                u8: {
                    min: 1,
                    max: 4,
                    default: 1
                }
            }
        }
    },
    {
        typeName: "AnalogInput",
        struct: {
            channel: {
                description: "ADC channel the sensor is wired to.",
                multiplicity: "1..1",
                u8: {
                    min: 0,
                    max: 15
                }
            },
            voltageRange: {
                enum: {
                    literals: [
                        "RANGE_5V",
                        "RANGE_10V"
                    ],
                    default: "RANGE_5V"
                }
            }
        }
    },
    {
        typeName: "DigitalInput",
        struct: {
            pin: {
                description: "Digital input pin the sensor is wired to.",
                multiplicity: "1..1",
                u8: {
                    min: 0,
                    max: 31
                }
            },
            inverted: {
                bool: {
                    default: false
                }
            }
        }
    },
]

A type is addressed by its type path: the definition name plus the type name, for example SensorEcu::Filter. Elements nested inside the definition tree can also be reused if you give them a typeName; their path is then the full ancestor chain.

Using a Type: typeRef

typeRef replaces the type keyword of an element with a reference to a named type:

inputFilter: {
    description: "Filter applied before scaling.",
    typeRef: "SensorEcu::Filter"
},
outputFilter: {
    description: "Filter applied after scaling.",
    typeRef: "SensorEcu::Filter"
},

Both filters have the exact shape of SensorEcu::Filter, and a change to the type propagates to every use. The referenced type may be an entity, struct, or parameter type.

Recursive Types

A typeRef may build recursive types of any depth. The sensor groups of the example are such a tree.

{
    typeName: "SensorGroup",
    title: "Group of sensors",
    description: "A group of sensors that may hold further groups, so a diagnostic tree of any depth can be modelled.",
    entity: {
        memberRefs: {
            description: "The sensors belonging to this group.",
            typeName: "MemberRef",
            multiplicity: "0..*",
            ref: {
                refTargetTypes: [
                    "SensorEcu::Ecu::Sensor"
                ]
            }
        },
        subGroups: {
            description: "Nested groups. The list multiplicity is what makes the cycle legal.",
            multiplicity: "0..*",
            typeRef: "SensorEcu::SensorGroup"
        }
    }
}

The type is used like any other named type:

sensorGroups: {
    description: "Top level sensor groups. Each group nests further groups of the same type.",
    multiplicity: "0..*",
    typeRef: "SensorEcu::SensorGroup"
}

A configuration then nests as deeply as it needs to:

sensorGroups: [
    {
        name: "Powertrain",
        memberRefs: ["/SensorEcu/FrontBodyEcu/OilTemp"],
        subGroups: [
            {
                name: "Ignition",
                memberRefs: ["id:ignition"]
            }
        ]
    }
]

Two rules keep such a cycle usable, and breaking either is an error:

  • One property on the cycle has a list multiplicity, subGroups here.

  • One property on the cycle is optional. If all were required, every group would have to contain a further group and no configuration could ever be complete.

A cycle may run through any number of types, across definition documents, and an inherited property counts as a hop. A union may sit on a cycle but must not name itself in its own typeRefs.

Entity Inheritance: inherits

An entity element can inherit another entity type. The children of the parent are resolved into the child, so the configuration author sees the union of both:

sensors: {
    title: "Sensors",
    description: "The sensors acquired by this ECU.",
    typeName: "Sensor",
    multiplicity: "1..*",
    inherits: "SensorEcu::SensorBase",
    entity: {
    }
},

The element’s own children are elided here; the type reference shows the element in full.

In the running example every sensor gets the inherited sampleDivider parameter next to its own children:

{
    id: "ignition",
    sampleDivider: 4,
    input: {
        $type: "SensorEcu::DigitalInput",
        pin: 12,
        inverted: true
    }
}

Rules:

  • Only entities can inherit, and only from entities.

  • Inheritance cycles are definition errors.

  • Duplicate property names across the inheritance chain are definition errors.

  • The parent must be visible: public, or declared in the same definition.

Structs cannot inherit; if two structs share fields, extract the shared part into a named struct type and embed it via typeRef.

Visibility: scope

Every element has a scope:

  • definition (the default): visible only within the defining definition.

  • public: usable from other definitions in the workspace, as an inherits parent, typeRef target, union member, or refTargetTypes entry. A constraint of another definition reads only its public properties.

Keep types private unless they are meant as an extension point. A public type is API: other definitions may depend on it, and renaming or removing it breaks them.

Unions of Named Types

Unions combine naturally with named types: declare the variants in types, then list them as typeRefs:

input: {
    description: "The electrical input of the sensor.",
    multiplicity: "1..1",
    union: {
        typeRefs: [
            "SensorEcu::AnalogInput",
            "SensorEcu::DigitalInput"
        ]
    }
},

Since the variants are named types, they can be reused by other unions or elements, and the $type discriminator values the configuration author writes (SensorEcu::AnalogInput) stay stable.