UP | HOME

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
[2023-12-20 Wed] 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

004-state-machine-example.svg

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.

Created: 2024-01-18 Thu 08:47