RFC004: Move to Type-State Entities
Status: Draft
Table of Contents
1. Introduction
In our experience with SDML we have seen multiple cases of entities (and structures, but we will use entities as our example here) where the presence or absence of fields is dependent on the state of the entity. To model this we have seen users create an enumeration of the states of the entity and then constraints on fields to determine their application. However, to accomplish this it requires any field that may not be present in all states to be optional, that is to have a cardinality with a lower bound of 0 for those cases where it is not required. This leads to complex models of the following form.
module shipping is enum ShipmentState of Created Planned Loaded InTransit Delivered end entity Shipment is identity id -> gs1:ShipmentIdentifier consignment -> Consignment ;; parent source -> {0..1} gs1:GlobalLocationIdentifier destination -> {0..1} gs1:GlobalLocationIdentifier state -> ShipmentState units -> {0..} gs1:LogisticsUnitIdentifier is assert state_planned = "units must be present if state /= Created" end assets -> {0..} gs1:LogisticsAssetIdentifier is assert state_loaded = "assets must be present if state not in Created, Planned" end embarkation_date -> {0..1} xsd:dateTime is assert state_created = "embarkation_date must be present if state not in Created, Planned, Loaded" end delivery_date -> {0..1} xsd:dateTime is assert state_created = "delivery_date must be present if state = Delivered" end end end
This RFC proposes a change to the grammar and semantics to model specific states within an entity and how these states affect the presence of particular fields.
Document Status History
Date | Status | Comment | Grammar Version |
---|---|---|---|
Draft | 0.2.7 |
2. Motivation
One common use for constraints in our domain model is to define the cardinality of fields under certain conditions; especially to make them optional or required depending on the state of the entity. In programming languages there is an approach, little supported in mainstream languages, called Type-State programming where any non-trivial type has a set of states and its definition is dependent on this state.
For example, a common programming error is to try to read or write to a file handle that has not yet been open, or was
open but is now closed. We need to model new, open, and closed as distinct states during which only a subset of
operations are allowed. In the following invented language we follow the type name Self
with a state in brackets. Note
that the state Closed
is a terminal state as it has no operations and so nothing can be legally done with it.
type FileHandle { state { fn open(path): Self[Open]; } state Open { fn read(): Self[Open]; fn write(): Self[Open]; fn close(): Self[Closed]; } state Closed {} }
Typically, as in this example type-state languages focus on sequencing of operations rather than in data; although that would be possible, as in:
type FileHandle { state { } state Open { path: Path } state Closed {} }
3. Alternatives Considered
3.1. States with fields
entity Shipment is identity id -> gs1:ShipmentIdentifier consignment -> Consignment ;; parent source -> {0..1} gs1:GlobalLocationIdentifier destination -> {0..1} gs1:GlobalLocationIdentifier initial state Planned is units -> {1..} gs1:LogisticsUnitIdentifier on LoadingComplete => Loaded end state Loaded is assets -> {1..} gs1:LogisticsAssetIdentifier on Embarked => InTransit end state InTransit is embarkation_date -> xsd:dateTime on ev -> DeliveryEvent if ev.signed => Delivered end state Delivered is delivery_date -> xsd:dateTime end end
- New reserved words:
initial
,state
,on
,if
. - Reserved states:
created
,updated
,deleted
– maybe. - New operator:
=>
– or something. - New constraint functions:
state()
3.2. Simple field constraints
union ShipmentStates of Created Planned Loaded InTransit Delivered end entity Shipment is identity id -> gs1:ShipmentIdentifier state -> ShipmentState = Created consignment -> Consignment ;; parent source -> {0..1} gs1:GlobalLocationIdentifier destination -> {0..1} gs1:GlobalLocationIdentifier units -> {1..} gs1:LogisticsUnitIdentifier not in [Created] assets -> {1..} gs1:LogisticsAssetIdentifier not in [created Planned] embarkation_date -> xsd:dateTime not in [created Planned Loaded] delivery_date -> xsd:dateTime not in [created Planned Loaded InTransit] end
units -> {1..} gs1:LogisticsUnitIdentifier is assert state_cardinality is self.container.state = Created implies self.min_occurs >= 1
3.3. States separate from fields
entity Shipment is identity id -> gs1:ShipmentIdentifier consignment -> Consignment ;; parent source -> {0..1} gs1:GlobalLocationIdentifier destination -> {0..1} gs1:GlobalLocationIdentifier units -> {1..} gs1:LogisticsUnitIdentifier not in [Created] assets -> {1..} gs1:LogisticsAssetIdentifier not in [created Planned] embarkation_date -> xsd:dateTime not in [created Planned Loaded] delivery_date -> xsd:dateTime not in [created Planned Loaded InTransit] statecase state of created to Planned Planned on LoadingComplete to Loaded Loaded on Embarked to InTransit InTransit on ev -> DeliveryEvent if ev.signed => Delivered Delivered end end
Figure 1: Example State Machine
3.4. Potential de-sugaring
structure ShipmentPlanned is units -> {1..} gs1:LogisticsUnitIdentifier end structure ShipmentLoaded is units -> {1..} gs1:LogisticsUnitIdentifier assets -> {1..} gs1:LogisticsAssetIdentifier end structure ShipmentInTransit is units -> {1..} gs1:LogisticsUnitIdentifier assets -> {1..} gs1:LogisticsAssetIdentifier embarkation_date -> xsd:dateTime end structure ShipmentDelivered is units -> {1..} gs1:LogisticsUnitIdentifier assets -> {1..} gs1:LogisticsAssetIdentifier embarkation_date -> xsd:dateTime delivery_date -> xsd:dateTime end union ShipmentStates of ShipmentPlanned ShipmentLoaded ShipmentInTransit ShipmentDelivered end entity Shipment is identity id -> gs1:ShipmentIdentifier consignment -> Consignment ;; parent source -> {0..1} gs1:GlobalLocationIdentifier destination -> {0..1} gs1:GlobalLocationIdentifier current_state -> ShipmentStates end
4. Proposed Change
The following changes are required:
What are you proposing to change?
4.1. Test cases
What test cases do you intend to include.
4.2. Impact
What is the scope of the impact, this should relate to any tags you added to the section in the index file.