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,
subGroupshere. -
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 aninheritsparent,typeReftarget, union member, orrefTargetTypesentry. A constraint of another definition reads only itspublicproperties.
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.