application

model grammar version 105

  1. The minimal model
  2. Data model structure
    1. The root rule
      1. Application users
      2. Interfaces
      3. The root node type
      4. Numerical types
    2. Node types
      1. Property attributes
      2. Reference set attributes
      3. Command attributes
      4. Action attributes
  3. Cardinality constraint
  4. References
    1. Reference behaviour
    2. Upstream & downstream
    3. Bidirectional references
    4. Sibling references
    5. Graph constraints
  5. Derived values
    1. Recursion and cyclic dependencies
    2. Expressions
      1. Derived texts
      2. Derived numbers
      3. Derived files
      4. Derived states
      5. Derived references
      6. Derived collections
  6. Permissions and Todos
    1. Permissions
      1. Read permissions
      2. Update permissions
      3. Create and delete permissions
      4. Command execution permissions
    2. Todo items
    3. User requirements
  7. Commands and Timers
  8. Navigation
    1. Node navigation
    2. Variable assignment and navigation
    3. Navigation for references & derivations
  9. User interface annotations
    1. Overview
      1. Defaults
      2. Descriptions
      3. Visibility
      4. Identifying properties
      5. Icons
    2. Actions
    3. Reference sets
    4. Commands
    5. Groups
    6. Collections
    7. Numbers
    8. Files
    9. Texts
    10. State groups
    11. States
    12. Todo items
    13. Numerical types

The minimal model


Every valid Alan model instantiates the root rule. From that rule we can extract a minimal model. In the application language, the minimal model is

users
	anonymous
interfaces
root { }
numerical-types

Thus, every application model defines users for access control, interfaces specifying operations and data from external systems, a root type, and numerical-types. The next section explains the minimal model in more detail.

Data model structure


The root rule

Application users

The language supports two classes of users: anonymous users and dynamic users. Anonymous users do not require login; dynamic users do.

The keyword anonymous enables anonymous user access to your application. If you do not plan to support anonymous users, you just leave it out: the 'no' state for 'allow anonymous user' is the default, requiring no code at all.

If your application supports a dynamic collection of users, you can enable user sign-up and authentication. Enabling user sign-up means that your application will have a sign-up page for new users.

User authentication can either be application-specific, or it can be done via a single application for single sign-on. For an application that handles user authentication, you need to specify:

The following model expresses an application that supports all aforementioned features:

users
	dynamic: .'Users'
		user-initializer: (
			'Type' = create 'Unknown' ( )
		)

		passwords: .'Passwords'
			password-value: .'Data'.'Password'
			password-status: .'Data'.'Active' (
				| active => 'Yes' ( )
				| reset => 'No' ( )
			)
			password-initializer: (
				'Data' = ( )
			)

		authorities: .'Authorities'
			identities: .'Identities'>'User'
			identity-initializer: ( )

root {
	can-update: user .'Type'?'Admin'

	'Users': collection ['Name'] {
		'Name': text
		'Type': stategroup (
			'Admin' { }
			'Unknown' { }
		)
	}
	'Passwords': collection ['User'] {
		'User': text -> ^ .'Users'[]
		'Data': group {
			can-update: user is ( ^ >'User' )

			'Password': text
			'Active': stategroup (
				'No' { }
				'Yes' { }
			)
		}
	}
	'Authorities': collection ['Authority'] {
		'Authority': text
		'Identities': collection ['Identity'] {
			'Identity': text
			'User': text -> ^ ^ .'Users'[]
		}
	}
}

The initializers are for setting initial values for a user, password or identity node. For example, the user-initializer sets the Type of users that sign up to Unknown. Thus, users cannot sign up as an Admin; Admin permissions are required to make a new user Admin.

'allow anonymous user': [ users ] stategroup (
	'no' { }
	'yes' { [ anonymous ] }
)
'has dynamic users': stategroup (
	'no' { }
	'yes' {
		'users collection path': [ dynamic: ] component 'descendant base property path'
		'supports user sign-up': stategroup (
			'yes' {
				'user initializer': [ user-initializer: ] component 'user initializer'
			}
			'no' { }
		)
		'has password authentication': stategroup (
			'yes' {
				'passwords collection path': [ passwords: ] component 'descendant base property path'
				'password text path': [ password-value: ] component 'descendant base property path'
				'password status': [ password-status: ] group {
					'state group path': component 'descendant base property path'
					'cases': [ (, ) ] dictionary (
						static 'active' [ active ]
						static 'reset' [ reset ]
					) { [ | ]
						'state': [ => ] reference
						'initializer': component 'command object expression'
					}
				}
				'password initializer': [ password-initializer: ] component 'password initializer'
			}
			'no' { }
		)
		'has external authentication': stategroup (
			'yes' {
				'authorities path': [ authorities: ] component 'descendant base property path'
				'identities path': [ identities: ] component 'descendant base property path'
				'user reference': [ > ] reference
				'identity initializer': [ identity-initializer: ] component 'identity initializer'
			}
			'no' { }
		)
	}
)
Interfaces

If your application consumes data from external sources, you will have defined an Alan interface. For consuming an interface, you mention it in the interfaces section of your application model. This way, you can reference the interface when configuring permissions. The path is for specifying on which nodes the interface conformant data will be imported at runtime.

The nodes themselves may only be modified via the interface: their value source has to be interface. You express that at the node type with can-update: interface '<imported interfaces id>'.

interfaces
	'Supplier' ( ) = .'Supplier Data' // one supplier
		connection-status: .'Status' (
			| connected => 'Connected' ( )
			| connecting => 'Connecting' ( )
			| disconnected => 'Disconnected' ( )
		)
	'Catalogue' ( ) = .'Catalogues'[] // multiple catalogues
		initializer: ( /* initial data when item is added */ )
		connection-status: .'Status' (
			| connected => 'Connected' ( )
			| connecting => 'Connecting' ( )
			| disconnected => 'Disconnected' ( )
		)

root {
	'Supplier Data': group {
		can-update: interface 'Supplier'

		'Delivery Time': number 'days'
		'Status': stategroup (
			'Connected' { }
			'Connecting' { }
			'Disconnected' { }
		)
	}
	'Catalogues': collection ['Catalogue'] {
		can-update: interface 'Catalogue'

		'Catalogue': text
		'Status': stategroup (
			'Connected' { }
			'Connecting' { }
			'Disconnected' { }
		)
	}
}
'imported interfaces': [ interfaces ] dictionary {
	'parameters': [ (, ) ] dictionary {
		'value': [ = . ] reference
	}
	'path': [ = ] component 'object path tail'
	'instances': stategroup (
		'many' { [ [] ]
			'initializer': [ initializer: ] component 'interface instance initializer'
		}
		'one' { }
	)
	'connection status': stategroup (
		'no' { }
		'yes' { [ connection-status: ]
			'state group path': component 'descendant base property path'
			'cases': [ (, ) ] dictionary (
				static 'connected' [ connected ]
				static 'connecting' [ connecting ]
				static 'disconnected' [ disconnected ]
			) { [ | ]
				'state': [ => ] reference
				'dataset status': stategroup (
					'no' { }
					'yes' { [ dataset-status: ]
						'state group path': component 'descendant base property path'
						'cases': [ (, ) ] dictionary (
							static 'available' [ available ]
							static 'unavailable' [ unavailable ]
						) { [ | ]
							'state': [ => ] reference
							'initializer': component 'command object expression'
						}
					}
				)
				'initializer': component 'command object expression'
			}
		}
	)
}
The root node type

Alan models are hierarchical models specifying hierarchical data. An Alan model is a hierarchy of nested types with a single root type:

root { }

The root type (short for node type) is a complex type that nests other (complex) types. Types are surrounded by curly braces, and their identification is a path which starts from the root type. We refer to an instance of a type as a node. The rule for defining a type carries the same name: node; it should be read as the node type rule, as we define node types in a model (for legacy reasons it is called the 'node' rule).

'root': [ root ] component 'node'
Numerical types

Numbers in an application model require a numerical type. Also, computations with numbers of different numerical types require conversion rules. Divisions require a division conversion rule, products require a product conversion rule, and so on. Some examples of numerical types, with conversion rules are

'date' in 'days' @date
'date and time' in 'seconds' @date-time
'days'
'milliseconds'
	= 'seconds' * 1 * 10 ^ 3 // rule for converting 'seconds' to milliseconds
	@numerical-type: (
		label: "sec"
		decimals: 3
	)
'minutes' @duration: minutes
'seconds'
	= 'seconds' * 'factor' // 'seconds' times a 'factor' produces 'seconds'
	= 'seconds' / 'factor'
	= 'milliseconds' * 1 * 10 ^ -3
	= 'minutes' * 60 * 10 ^ 0
	@duration: seconds
'factor'
	= 'factor' * 'factor'
	= 'seconds' / 'seconds'

Annotations like @date map numerical types to formats for easy modification in the GUI. With @factor you can present 1000 milliseconds as 1 second to the application user.
If you do so, be sure to set @label: to "sec" as well!

'numerical types': [ numerical-types ] dictionary {
	'type': stategroup (
		'scale' {
			'timer resolution': stategroup (
				'none' { }
				'seconds' { [ : time-in-seconds ] }
			)
			'range type': [ in ] reference
		}
		'quantity' {
			'product conversions': dictionary { [ = ]
				'right': [ * ] reference
			}
			'division conversions': dictionary { [ = ]
				'denominator': [ / ] reference
			}
		}
	)
	'singular conversions': dictionary { [ = ]
		'type': stategroup (
			'factor' {
				'invert': stategroup (
					'no' { [ * ] }
					'yes' { [ / ] }
				)
				'factor': integer
				'base': [ * ] integer
				'exponent': [ ^ ] integer
			}
			'base' {
				'offset': [ + ] integer
				'base': [ * ] integer
				'exponent': [ ^ ] integer
				'unit conversion': stategroup (
					'no' { }
					'yes' {
						'conversion rule': [ in ] reference
					}
				)
			}
		)
	}
	'ui': component 'ui numerical type'
}

Node types

Node types contain a permissions definition for controlling read and update permissions, a todo definition to mark a node as a todo-item, and attributes. An attribute is of type property, reference-set, command, or action.

Property attributes

A property specifies a part of the data structure. Alan supports six different property types: text, file, number, collection, stategroup, and group.

Text, file, and number are primitive property types. Text properties hold an unbounded string value. File properties hold two unbounded string values: a file token and a file extension. Number properties hold an integer value, with an optional positive annotation to ensure values greater zero. For ensuring that number values have a predefined accuracy, Alan does not support number values with a fractional component. For expressing the accuracy of a number, number properties reference a numerical type.

'Name'        : text
'Price'       : number 'euro'
'Release Date': number positive 'date and time'

A collection property holds a map of key-value pairs. Keys are text values that have to be unique such that we can reference them unambiguously. A key field has to be specified explicitly, after the keyword collection; values are nodes of an inline defined type that specifies the key field:

Customers : collection ['Customer ID'] {
	'Customer ID': text
	... /* other attributes of this type */ ...
}

A state group property holds a value indicating a state. States are the alternatives to an aspect that a state group property indicates. For example, red, orange, or green for a color property of a traffic light. The type of the property value corresponds to one out of multiple predefined state types, such as simple or assembled:

'Product Type': stategroup (
	'Simple' { ... /* attributes of this type */ ... }
	'Assembled' { ... /* attributes of this type */ ... }
)

A group property groups related property values, which is useful for presentational purposes:

'Address': group {
	'City': text
	'State': text
	'Zip Code': text
}

Derived values. Properties hold either base values or derived values (derivations). Derived values are computed from base values and other derived values. Application users cannot modify derived values. Properties holding derived values require an expression for computing their values at runtime:

'City': text					// property holding a base value
'copy of City': text = .'City'  // property with an expression for deriving its value

The derivations section provides the grammar for derivation expressions, detailing the different types of computations that the language supports.

Presentation options. The ui component properties reference GUI annotation rules for tweaking and tuning the behaviour and presentation of properties in the generated GUI. Some examples of GUI annotations are

Reference set attributes

A reference-set is for creating bidirectional references. A reference set attribute holds the inverse of a reference which is by default unidirectional. References determine the values for reference sets; they create inverse references when your model specifies it. Derivation expressions refer to them for computations (e.g. derived numbers).

Command attributes

A command is a complex parametrized atomic operation on the dataset. The application engine executes a command in a single transaction. A command attribute specification consists of a definition of parameters for the command and an implementation.

Action attributes

An action is a sequence of operations that a webclient (GUI) executes. That is, unlike a command, an action can perform multiple different operations, interactively. Actions provide users with wizards to guide them through their processes. For an action, you can specify parameters that users have to provide before starting the execution of the action. After the parameters, you have specify which operations should be performed.

'node' { [ {, } ]
	'permissions definition': component 'node permissions definition'
	'todo definition': component 'todo definition'
	'has attributes': stategroup = node-switch .'attributes' (
		| nodes = 'yes' {
			'first' = first
			'last' = last
		}
		| none  = 'no'
	)
	'attributes': dictionary {
		'has predecessor': stategroup = node-switch predecessor (
			| node = 'yes' { 'attribute' = predecessor }
			| none = 'no'
		)
		'has successor': stategroup = node-switch successor (
			| node = 'yes' { 'attribute' = successor }
			| none = 'no'
		)
		'type': stategroup (
			'reference set' { [ : reference-set -> ]
				'direction': component 'direction annotation'
				'recursion': component 'recursion annotation'
				'referenced type path': group {
					'head': component 'object path tail'
					'root': stategroup (
						'context' { }
						'sibling' {
							'definition': component 'sibling reference definition'
						}
					)
					'path': component 'plural object path tail'
				}
				'value source': [ = ] stategroup (
					'reference' {
						'reference': [ inverse ] component 'dependency step'
					}
					'union branch' {
						'branch': [ branch ] reference
					}
				)
				'ui': component 'ui reference set attribute'
			}
			'command' { [ : command ]
				'permission definition': component 'command permission definition'
				'type': stategroup (
					'global' {
						'ui': component 'ui command attribute'
					}
					'component' { [ component ] }
				)
				'parameters': component 'node'
				'implementation': stategroup (
					'external' { [ external ] }
					'internal' {
						'variable assignment': component 'optional variable assignment'
						'implementation': component 'command'
					}
				)
			}
			'action' { [ : action ]
				'ui': component 'ui action attribute'
				'parameters': component 'node'
				'variable assignment': component 'optional variable assignment'
				'action': component 'ui action'
			}
			'property' { [ : ]
				'type': stategroup (
					'group' { [ group ]
						'type': stategroup (
							'base' { }
							'derived' { [ = ] }
						)
						'ui': component 'ui group property'
						'node': component 'node'
					}
					'collection' { [ collection ]
						'key property': [ [, ] ] reference
						'cardinality constraint': component 'lower bound cardinality constraint'
						'graph constraints': component 'graph constraints definition'
						'type': stategroup (
							'base' { }
							'derived' { [ = ]
								'recursion': component 'recursion annotation'
								'key constraint': stategroup (
									'no' {
										'branches': [ flatten (, ) ] dictionary {
											'expression': [ = ] component 'flatten expression'
											'variable assignment': component 'optional named object assignment'
											'node initializer': component 'node initializer'
										}
										'separator': [ join ] stategroup (
											'dot' { [ . ] }
											'dash' { [ - ] }
											'colon' { [ : ] }
											'greater than' { [ > ] }
											'space' { [ space ] }
										)
									}
									'yes' {
										'expression': component 'plural reference expression'
									}
								)
							}
						)
						'permissions': component 'item permissions definition'
						'ui': component 'ui collection property'
						'node': component 'node'
					}
					'number' { [ number ]
						'set type': stategroup (
							'integer' { }
							'positive' { [ positive ] }
						)
						'numerical type': reference
						'type': stategroup (
							'base' {
								'type': stategroup (
									'causal' {
										'type': stategroup (
											'mutation' { [ = mutation-time ]
												'watched property': [ . ] reference
											}
											'creation' { [ = creation-time ] }
											'destruction' {
												'destruction operation': stategroup (
													'set to lifetime' { [ = life-time ] }
													'add lifetime' { [ add life-time ] }
													'subtract lifetime' { [ subtract life-time ] }
												)
												'watched stategroup': [ . ] reference
												'watched state': [ ? ] reference
											}
										)
									}
									'simple' { }
								)
							}
							'derived' { [ = ]
								'value source': stategroup (
									'parameter' { [ parameter ] }
									'expression' {
										'recursion': component 'recursion annotation'
										'expression': component 'derivation expression'
									}
								)
							}
						)
						'behaviour': stategroup (
							'none' { }
							'timer' { [ timer ]
								'implementation': [ ontimeout ] component 'command'
							}
						)
						'ui': component 'ui number property'
					}
					'file' { [ file ]
						'type': stategroup (
							'base' { }
							'derived' { [ = ]
								'value source': stategroup (
									'parameter' { [ parameter ] }
									'expression' {
										'recursion': component 'recursion annotation'
										'expression': component 'derivation expression'
									}
								)
							}
						)
						'ui': component 'ui file property'
					}
					'text' { [ text ]
						'has reference': stategroup (
							'no' { }
							'yes' {
								'behaviour': component 'reference behaviour'
								'behaviour specialization': stategroup (
									'none' { }
									'propagate key to value' { [ <- ] }
								)
								'direction': component 'direction annotation'
								'recursion': component 'recursion annotation'
								'expression': component 'entry reference definition'
								'assignment': component 'optional named object assignment'
								'ui': component 'ui reference rule'
								'rules': component 'where rules definition'
							}
						)
						'type': stategroup (
							'base' { }
							'derived' { [ = ]
								'value source': stategroup (
									'parameter' { [ parameter ] }
									'derived key' { [ key ] }
									'expression' {
										'recursion': component 'recursion annotation'
										'expression': component 'derivation expression'
									}
								)
							}
						)
						'ui': component 'ui text property'
					}
					'state group' { [ stategroup ]
						'type': stategroup (
							'base' { }
							'derived' { [ = ]
								'value source': stategroup (
									'parameter' { [ parameter ] }
									'expression' {
										'recursion': component 'recursion annotation'
										'expression': component 'derivation expression'
									}
								)
							}
						)
						'ui': component 'ui state group property'
						'first state': reference = first
						'states': [ (, ) ] dictionary {
							'has successor': stategroup = node-switch successor (
								| node = 'yes' { 'successor' = successor }
								| none = 'no'
							)
							'record lifetime': stategroup (
								'yes' { [ life-time ]
									'meta property': [ = . ] reference
									'creation timestamp': [ from ] reference
								}
								'no' { }
							)
							'rules': component 'where rules definition'
							'permissions': component 'item permissions definition'
							'ui': component 'ui state'
							'node': component 'node'
						}
					}
				)
			}
		)
	}
}

Cardinality constraint


Collections support a lower bound cardinality constraint, ensuring that a collection is not empty. This is useful for ensuring that you can always derive a reference to the source/sink of an ordered graph. It also ensures that min, max, and similar computations on sets of number values do not require a fallback value.

'lower bound cardinality constraint' {
	'constraint': stategroup (
		'non empty' { [ non-empty ] }
		'none' { }
	)
}

References


References indicate relations between different parts of your application data. For example, suppose that you have Orders and Products, where Orders reference a Product. You can express that reference in your model, such that your users can see it, and such that you can use it in computations:

'Products': collection ['Name'] {
	'Name': text
	'In Stock': stategroup (
		'No' { }
		'Yes' { }
	)
}
'Orders': collection ['ID'] {
	'ID': text
	'Product': text -> ^ .'Products'[] as $'p'     // reference rule for text
		where 'In Stock' -> $'p'.'In Stock'?'Yes' // additional reference rule for the text
	'Ready': stategroup (
		'No' { }
		'Yes' { }
	)
	'Sent for Delivery': stategroup (
		'No' { }
		'Yes' where 'Order Ready' -> .'Ready'?'Yes' { } // reference rule for state
	)
}

The example indicates that you can specify multiple references at a text property. You can also specify additional rules for the ‘main’ reference with keyword $, which refers to the referenced node.

In order for the text value to reference an item in a collection, its value has to equal the key value of an item in the specified collection. At runtime, reference expressions always produce a single specific node (or none at all, if the reference is broken). The language ensures that. In the GUI, references translate to a select list from which the application user has to choose an item.

Alternatively, sometimes a specific state for a state group is related to another node. The where-rule Order Ready expresses that: an order can be Sent for Delivery if and only if it is Ready.

Reference behaviour

The reference behaviour that you specify, determines how the runtime treats a reference. Mandatory references (specified with keyword ->) are constraints that are enforced by the runtime: they have to resolve to a node. Optional references (specified with keyword ~>) do not have to resolve to a node.

'reference behaviour' {
	'type': stategroup (
		'mandatory' { [ -> ] }
		'optional' { [ ~> ] }
	)
}

The language ensures that references either point to a single specific node, or none at all. Thus, mandatory references unambiguously point to a single specific node. Because of that, derived value computations and command invocations can safely use them.

Optional references are especially useful when you want user data to reference imported data (can-update: interface '...'). Mandatory references are not allowed among data from different sources, such that data import is always successfull.

Upstream & downstream

Reference constraint expressions exist in two different flavours: (1) upstream reference expressions that use earlier defined attributes and (2) downstream reference expressions that use later defined attributes. Downstream references require an explicit annotation: downstream after the arrow (->/~>) specifying the reference behaviour.

'direction annotation' {
	'direction': stategroup (
		'upstream' { }
		'downstream' { [ downstream ] }
	)
}
'graph traversal definition' {
	'traversal type': stategroup (
		'base order' { }
		'inverse order' { [ inverse ] }
	)
	'graph': reference
}
'graph traversal selector' {
	'ancestor path': component 'ancestor node path'
	'graph traversal': component 'graph traversal definition'
	'has tail': stategroup (
		'no' { }
		'yes' { [ ^ ]
			'tail': component 'graph traversal selector'
		}
	)
}
'recursion annotation' {
	'recursive graph traversal': stategroup (
		'no' { }
		'yes' { [ ( recurse, ) ]
			'selector': component 'graph traversal selector'
		}
	)
}

Below we explain the annotation for sibling navigation.

Bidirectional references

Text references are either unidirectional or bidirectional. For bidirectional references, you specify a reference-set. If a reference is downstream reference, then the reference-set holds upstream references. Conversely, if a reference is upstream, then the reference-set holds downstream references:

'Products': collection ['Name'] {
	'Name': text
	// reference-set holding downstream references to Orders added by Product references,
	'Orders': reference-set -> downstream ^ .'Orders'* = inverse >'Product'
}
'Orders': collection ['ID'] {
	'ID': text
	// a (bidirectional) upstream reference:
	'Product': text -> ^ .'Products'[]
}

The expression ^ .'Products' is a navigation expression that produces exactly one collection at runtime. The Alan runtime interpretes such navigation expressions as follows: starting from the Orders node, go to the parent node (as expressed by the navigation step ^); then select the Products collection found on that node. Thus, at runtime, navigation expressions are executed relative to the context node for which the expression should be evaluated.

Sibling references

If you want references from Products to other Products, you will find that this line gives an error:

`Other Product`: text -> ^ .'Products'[]

The compiler requires that you reference an earlier defined property, and not the Products property that defines the expression. For references from Products to other other Products, you need special sibling references.

'entry reference definition' {
	'collection path': component 'variablized object path'
	'type': stategroup (
		'simple' { [ [] ] }
		'sibling' {
			'definition': component 'sibling reference definition'
		}
	)
	'tail': component 'descendant object path'
}
'sibling reference definition' {
	'graph participation': [ sibling ] stategroup (
		'no' {
			'support self reference': stategroup (
				'no' { }
				'yes' { [ || self ] }
			)
		}
		'yes' { [ in (, ) ]
			'graph traversal': component 'graph traversal definition'
		}
	)
}
'where rule object path' {
	'context': stategroup (
		'this' {
			'head': component 'context object step'
		}
		'sibling rule' {
			'rule': reference
		}
	)
	'path': component 'object path tail'
}
'where rules definition' {
	'has rule': stategroup = node-switch .'rules' (
		| nodes = 'yes' { 'first' = first }
		| none  = 'no'
	)
	'rules': dictionary { [ where ]
		'has predecessor': stategroup = node-switch predecessor (
			| node = 'yes' { 'rule' = predecessor }
			| none = 'no'
		)
		'behaviour': component 'reference behaviour'
		'direction': component 'direction annotation'
		'recursion': component 'recursion annotation'
		'type': stategroup (
			'entry reference' {
				'expression': component 'entry reference definition'
			}
			'node reference' {
				'object path': component 'where rule object path'
				'type': stategroup (
					'existence' { }
					'equality' { [ is (, ) ]
						'node path': component 'where rule object path'
					}
					'containment' { [ [, ] ]
						'key node path': component 'where rule object path'
					}
				)
			}
		)
		'ui': component 'ui reference rule'
	}
}

The following model expresses sibling references from Products to other Products which are corresponding (Parts), and also from Orders to a Previous Order:

'Products': collection ['Name']
	'assembly': acyclic-graph
{
	'Name': text
	'Parts': collection ['Product'] {
		// an 'upstream' sibling Product reference that partakes in the 'assembly' graph:
		'Product': text -> ^ sibling in ('assembly')
		'Part Price': number 'euro'
			= ( recurse ^ 'assembly' ) >'Product' /*[1]*/ .'Product Price' /*[2a]*/
	}
	'Parts Cost': number 'euro' = sum .'Parts'* .'Part Price'
	'Assembly Cost': number 'euro'
	'Product Price': number 'euro'
		= ( recurse 'assembly' ) sum ( .'Parts Cost', .'Assembly Cost' ) /*[2b]*/
}
'Orders': collection ['Year']
	'timeline': ordered-graph .'Is First Order' ( ?'Yes'|| ?'No'>'Previous Order' )
{
	'Year': text
	'Price': number 'euro'
	'Is First Order': stategroup (
		'Yes' { }
		'No' {
			'Previous Order': text -> ^ sibling in ('timeline')
		}
	)
	'Total Sales Value': number 'euro' = ( recurse 'timeline' ) switch .'Is First Order' (
		|'Yes' => .'Price'
		|'No' as $'no' => sum (
			.'Price',
			$'no'>'Previous Order'.'Total Sales Value' // recursion!
		)
	)
}
Graph constraints

The model above also expresses graph constraints. Graph constraints ensure termination for recursive computations traversing a graph. Graph constraints ensure that a set of references (edges) together form a graph satisfying a specific property. For example, an acyclic-graph constraint ensures that references that partake in the graph, form a directed acyclic graph. For a detailed explanation, see AlanLight.

'graph constraints definition' {
	'graphs': dictionary {
		'type': [ : ] stategroup (
			'acyclic' { [ acyclic-graph ] }
			'ordered' { [ ordered-graph ]
				'ordering property path': group {
					'ordering state group path': component 'descendant base property path'
					'ordering states': [ (, ) ] group {
						'sink state': [ ? ] reference
						'edge state': [ || ? ] reference
						'edge reference': [ > ] reference
					}
				}
			}
		)
		'value type': stategroup (
			'base' { }
			// 'derived' { }
		)
	}
}

The example model expresses that Products form an acyclic assembly graph via their Parts: Product references of Parts partake in the acyclic assembly graph on Products. The constraint ensures that recursive Part Price, Parts Cost and Product Price computations terminate.

For Orders, the model expresses an ordered timeline graph, which means that a strict total ordering exists for all Orders. That is: only one Orders item does not have preceding Order. All other Orders do. For ordered graphs, the application language supports deriving the source and the sink of the graph: the first Order and the most recent Order.

Note that the expression for computing a Part Price starts with an annotation: ( recurse ^ 'assembly' ). That is because the Part Price is computed recursively: for all recursive computations, the annotation is required. The (hierarchy of) graph(s) that you have to name as part of the annotation, imply an order in which recursive computations can be evaluated. If the annotation is missing, the compiler produces an error message indicating that a ‘cyclic dependency’ exists. The existence of an order for evaluating recursion ensures that the recursive computations are finite.

A recursion annotation should be placed at a property or reference definition

  1. if the recursive expression for computing the value contains a sibling navigation step, like >'Product' [1] in the example.
  2. if the property or reference ('Product Price' at [2b]) is referenced by another recursive expression after a sibling navigation step (>'Product' at [2a]).

Note that when providing a recursion annotation, only sibling references that partake in the indicated graphs may be used. That is, when a computation depends on multiple sibling references partaking in different graphs, you need to split it up into separate computations (derivations). For computing the value that you need, you can combine the results from the separate computations.

Sometimes, computations traverse the inverse of a graph (by using a reference-set attribute). For such computations, you need to add the annotation inverse: ( recurse inverse 'assembly' ).

Derived values


Derived values (derivations) are computed from base values and other derived values. The application language supports derived texts, numbers, files, states, references, and collections. Derived texts, numbers, files, states, and references share common grammar rules: the derivation expression rule and corresponding parts presented below. The language has special rules for deriving collections.

Recursion and cyclic dependencies

The application language guarantees termination for derived value computations. The solution for achieving this guarantee, rejects recursive computations by default (you will get a cyclic dependency error).

But, the application does support recursion, including mutual recursion. For recursive computations, you need to specify which (acyclic/ordered) graph the computation traverses, such that termination is guaranteed, as shown in the sibling references example. For that purpose, you need an annotation after the = sign, and before the derivation expression itself: = ( recurse /* ^ path to graph ^*/ '<graph>' ) ...expression.... If you add the annotation to your expression, the expression can only refer to sibling references that partake in the specified graph '<graph>'.

Note that the annotation enables you to specify a graph per level in the data hierarchy. That is, a single computation can traverse both a child and parent graph (and ancestor graphs higher up in the hierarchy). To ensure termination, you cannot traverse multiple graphs from the same collection, as multiple acyclic graphs may together form cycles.

Expressions

A derivation expression starts with optional switch statements followed by a subexpression that produces a value. The following code sample exemplifies the use of the different types of switch statements, where each case produces a text value:

'Orders': collection ['Order'] {
	'Order': text
	'Price': number 'euro'
}
'Store Status': stategroup (
	'Open' { }
	'Closed' { }
)

/* state switch */
'Store Status Label': text = switch .'Store Status' (
	|'Open'   => "Open"
	|'Closed' => "Closed"
)

/* node switch */
'Favorite Order': text ~> .'Orders'[]
'Favorite Order Exists?': text = switch >'Favorite Order' (
	| none => "No"
	| node => "Yes"
)

/* node set switch */
'How Many Orders?': text = switch .'Orders'* (
	| none  => "No Orders"
	| node  => "One Order"
	| nodes => "More Than One Order"
)

/* number switch */
'Order Count': number 'count' = count .'Orders'*
'More Than 10 Orders?': text = switch .'Order Count' compare ( 10 ) (
	| >  => "More than 10"
	| <  => "Less than 10"
	| == => "Exactly 10"
)
'derivation expression' {
	'type': stategroup (
		'produce value' {
			'expression': component 'derivation expression tail'
		}
		'state switch' { [ switch ]
			'state group path': component 'singular variablized object path'
			'states': [ (, ) ] dictionary { [ | ]
				'variable assignment': component 'optional named object assignment'
				'expression': [ => ] component 'derivation expression'
			}
		}
		'node switch' { [ switch ]
			'node path': component 'variablized object path'
			'operation': stategroup (
				'equality' {
					'other node path': [ is (, ) ] component 'singular variablized object path'
				}
				'existence' { }
			)
			'cases': [ (, ) ] group { dynamic-order
				'node': group { [ | node ]
					'variable assignment': component 'optional named object assignment'
					'expression': [ => ] component 'derivation expression'
				}
				'none': [ | none => ] component 'derivation expression'
			}
		}
		'set switch' { [ switch ]
			'nodes path': component 'object set path'
			'cases': [ (, ) ] group { dynamic-order
				'match none': stategroup (
					'no' { }
					'yes' { [ | none => ]
						'expression': component 'derivation expression'
					}
				)
				'match node': stategroup (
					'no' { }
					'yes' { [ | node ]
						'variable assignment': component 'optional named object assignment'
						'expression': [ => ] component 'derivation expression'
					}
				)
				'match nodes': stategroup (
					'no' { }
					'yes' { [ | nodes ]
						'variable assignment': component 'optional named object assignment'
						'expression': [ => ] component 'derivation expression'
					}
				)
			}
		}
		'number switch' { [ switch ]
			'number path': component 'singular variablized object path'
			'compare to': [ compare (, ) ] stategroup (
				'constant' {
					'value': component 'constant number value'
				}
				'path' {
					'right number path': component 'singular variablized object path'
				}
			)
			'last case': reference = last
			'cases': [ (, ) ] dictionary (
				static 'equals' [ == ]
				static 'less than or greater than' [ <> ]
				static 'greater than' [ > ]
				static 'greater than or equals' [ >= ]
				static 'less than' [ < ]
				static 'less than or equals' [ <= ]
			) { [ | ]
				'has predecessor': stategroup = node-switch predecessor (
					| node = 'yes' { 'case' = predecessor }
					| none = 'no'
				)
				'variable assignment': component 'optional named object assignment'
				'expression': [ => ] component 'derivation expression'
			}
		}
		'recurse' {
			'step': stategroup (
				'start' {
					'binding node path': [ on ] component 'singular variablized object path'
					'recursion start assignment': component 'named object assignment'
					'graph traversal': [ in ] component 'graph traversal definition'
					'expression': [ => ] component 'derivation expression'
				}
				'repeat' { [ recurse ]
					'recursion start': [ on ] component 'named object step'
					'sibling node path': [ = ] component 'singular variablized object path'
				}
			)
		}
	)
}
'derivation expression list' {
	'head': component 'derivation expression tail'
	'has tail': stategroup (
		'no' { }
		'yes' { [ , ]
			'tail': component 'derivation expression list'
		}
	)
}
'derivation expression tail' {
	'type': stategroup (
		'reference' {
			'type': stategroup (
				'branch' // branch of derived collection constructed using flatten expressions
				{
					'branch': [ branch ] reference
					'expression': [ [, ] ] component 'singular variablized object path'
				}
				'ordered graph node' {
					'type': stategroup (
						'source' { [ source-of ] }
						'sink' { [ sink-of ] }
					)
					'collection path': [, * ] component 'singular variablized object path'
					'ordered graph': [ in ] reference
				}
			)
		}
		'text' {
			'type': stategroup (
				'static' {
					'value': text
				}
				'concatenation' { [ concat (, ) ]
					'expression': component 'derivation expression list'
				}
			)
		}
		'number' {
			'expression': component 'number expression'
		}
		'state' {
			'initializer': component 'state initializer'
		}
		'dynamic' {
			'path': component 'singular variablized object path'
		}
	)
}

Derived texts

A derived text value can consist of static text values and text values from other properties:

'Address': group {
	'Street'       : text // e.g. "Huntington Rd"
	'Street number': text // e.g. "12B"
}
// label for an external billing system, like "Huntington Rd 12B":
'Address label': text = concat ( .'Address'.'Street', " ", .'Address'.'Street number' )

Derived numbers

Examples of derived number properties, including required conversion rules:

root {
	'Tax Percentage': number positive 'percent'
	'Products': collection ['Name'] {
		'Name': text
		'Price': number 'eurocent'
		'Price (euro)': number 'euro' = from 'eurocent' .'Price'
		'Order Unit Quantity': number positive 'items'
		'Orders': reference-set -> downstream ^ .'Orders'* = inverse >'Product'
		'Sales Value': number 'eurocent' = sum <'Orders'* .'Price'
		'Items Sold': number 'items' = count <'Orders'*
	}
	'Total Sales Value': number 'eurocent' = sum .'Products'* .'Sales Value'
	'Number of Products': number 'items' = count .'Products'*
	'Orders': collection ['ID'] {
		'ID': text
		'Product': text -> ^ .'Products'[]
		'Quantity': number positive 'items'
		'Loss': number 'items' = remainder (
			.'Quantity',
			>'Product' .'Order Unit Quantity'
		)
		'Price': number 'eurocent' = product (
			>'Product'.'Price' as 'eurocent',
			.'Quantity'
		)
		'Order Units': number positive 'units' = division ceil (
			.'Quantity' as 'items' ,
			>'Product'.'Order Unit Quantity'
		)
		'Tax': number 'eurocent'
		'Gross Price': number 'eurocent' = sum ( .'Price', .'Tax' )
		'Creation Time': number 'date and time'
		'Estimated Lead Time': number 'seconds'
		'Estimated Delivery Time': number 'date and time' = add (
			.'Creation Time',
			.'Estimated Lead Time' )
		'Delivered': stategroup (
			'No' { }
			'Yes' {
				'Delivery Time': number positive 'date and time'
				'Lead Time': number 'seconds' = diff 'date and time' (
					.'Delivery Time',
					^ .'Creation Time'
				)
			}
		)
	}
	'Gross Income': number 'eurocent' = sum .'Orders'* .'Gross Price'
}
numerical-types
	'percent'
	'euro'
		= 'eurocent' * 1 * 10 ^ -2
	'eurocent'
		= 'eurocent' * 'items'
	'units'
		= 'items' / 'items'
	'items'
	'date and time' in 'seconds'
	'seconds'
'number expression' {
	'type': stategroup (
		'constant' {
			'value': component 'constant number value'
		}
		'unary' {
			'type': stategroup (
				'absolute value' { [ abs ] }
				'numerical type conversion' {
					'conversion': [ from ] reference
				}
				'sign inversion' { [ - ] }
			)
			'expression': component 'derivation expression tail'
		}
		'aggregate' {
			'type': stategroup (
				'collection operation' {
					'type': stategroup (
						'property' {
							'operation': stategroup (
								'minimum' { [ min ] }
								'maximum' { [ max ] }
								'sum' { [ sum ] }
								'standard deviation' { [ std ] }
							)
							'numbers path': component 'object set path'
						}
						'count' { [ count ]
							'nodes path': component 'object set path'
						}
					)
				}
				'remainder' { [ remainder (, ) ]
					'numerator': [, , ] component 'unary number expression'
					'denominator': component 'unary number expression'
				}
				'division' { [ division ]
					'rounding': stategroup (
						'ordinary' { }
						'ceil' { [ ceil ] }
						'floor' { [ floor ] }
					)
					'expressions': [ (, ) ] group {
						'numerator': component 'unary number expression'
						'conversion rule': [ as, , ] reference
						'denominator': component 'unary number expression'
					}
				}
				'product' { [ product (, ) ]
					'left': component 'unary number expression'
					'conversion rule': [ as, , ] reference
					'right': component 'unary number expression'
				}
				'list operation' {
					'operation': stategroup (
						'sum' { [ sum ] }
						'maximum' { [ max ] }
						'minimum' { [ min ] }
					)
					'numbers': [ (, ) ] component 'derivation expression list'
				}
				'addition' { [ add (, ) ]
					'left': [, , ] component 'unary number expression'
					'right': component 'unary number expression'
				}
				'difference' { [ diff ]
					'scale type': reference
					'expressions': [ (, ) ] group {
						'left': [, , ] component 'unary number expression'
						'right': component 'unary number expression'
					}
				}
			)
		}
	)
}
'unary number expression' {
	'expression': component 'derivation expression tail'
}
'constant number value' {
	'value': integer
}

Derived files

Derived file values take their value (token + extension) from another file value. For example, you can derive a Contract which is a Default Contract in case of a Standard Agreement, and a Custom Contract in case of a Custom Agreement:

'Standard Contract': file
'Agreement': stategroup (
	'Standard' { }
	'Custom' {
		'Contract': file
	}
)
'Contract': file = switch .'Agreement' (
	|'Standard' => .'Standard Contract'
	|'Custom' as $'con'  => $'con'.'Contract'
)

Derived states

Derived states are computed using the abovementioned switch expressions, where different cases lead to different states. For example, we can derive if a Product exists in a Catalog of Products from a Catalog Provider. For this purpose, we check if the optional Product reference on an Order produces a node or nothing:

'Catalog': group { can-update: interface 'Catalog Provider'
	'Products': collection ['Name'] {
		'Name': text
		'Price': number 'eurocent'
		'Description': text
	}
}
'Orders': collection ['ID'] {
	'ID': text
	'Product': text ~> ^ .'Catalog'.'Products'[]
	'Product found': stategroup = switch >'Product' (
		| node as $ => 'Yes' where 'Found Product' = $ ( 'Price' = $ .'Price' )
		| none => 'No' ( )
	) (
		'Yes' where 'Found Product' -> >'Product' {
			'Description': text = .&'Found Product'.'Description'
			'Price': number 'eurocent' = parameter
		}
		'No' { }
	)
}

The expression for deriving a state, has to satisfy all where rules for the state. In the example, the where rule expresses that the optional Product reference is no longer optional. Instead, the reference is mandatory in context of the Yes state. The expression ‘proves’ that. You can use the Found Product for deriving values such as Description on the state node.

An expression for deriving a state can express how to initialize property values of the state as well, such as Price in the example. To make this work, you have to specify = parameter for properties that you want to set with the state initializer. This language construct is especially useful when a reference to a node like 'Found Product' is not available. Furthermore, it useful for special operations such as source-of and sink-of, which we discuss below.

'state initializer' {
	'state': reference
	'rule arguments': dictionary { [ where ]
		'expression': [ = ] component 'derivation expression'
	}
	'node initializer': component 'node initializer'
}
'node initializer' {
	'arguments': [ (, ) ] dictionary {
		'expression': [ = ] component 'derivation expression'
	}
}

Derived references

Sometimes it is useful to derive a reference to a collection entry for use by the application user and other computations. Similar to base references, derived references require you to express a text property follow by a reference definition. For deriving a reference, you have to provide an expression that produces a node that matches the reference definition. The definition of 'Product copy' depicts that:

'Products': collection ['Name'] {
	'Name': text
}
'Orders': collection ['Year']
	'timeline': ordered-graph .'Is First Order' ( ?'Yes'|| ?'No'>'Previous Order' )
{
	'Year': text
	'Product': text -> ^ .'Products'[]
	'Product copy': text -> ^ .'Products'[] = >'Product' /* derived reference */
	'Is First Order': stategroup (
		'Yes' { }
		'No' {
			'Previous Order': text -> ^ sibling in ('timeline')
		}
	)
}

/* source-of/sink-of of graph */
'Has Orders': stategroup = switch .'Orders'* (
	| nodes as $ => 'Yes' (
		'Oldest Order' = sink-of $ * in 'timeline'
		'Most Recent Order' = source-of $ * in 'timeline'
	)
	| none => 'No' ( )
) (
	'Yes' {
		'Oldest Order'     : text -> ^ .'Orders'[] = parameter /* derived reference */
		'Most Recent Order': text -> ^ .'Orders'[] = parameter /* derived reference */
	}
	'No' { }
)

With the special source-of and sink-of operation, you can derive a reference to the respective nodes in an ordered graph. These operations can only be applied to a collection for which we can guarantee non-emptiness, as otherwise the source/sink do not exist. Therefore, we first switch on the content of the 'Orders' collection. If it holds nodes (meaning that it is not empty), then we can apply the source-of and sink-of operations.

Derived collections

'flatten expression' {
	'head': component 'group ancestor node path'
	'path': component 'plural descendant node path'
}
'reference set subset step' {
	'subset': stategroup (
		'no' { [ * ] }
		'yes' { [ [, ] ]
			'head': component 'variablized object path'
			'type': [, * ] stategroup (
				'simple' { }
				'sibling' { [ sibling ] }
			)
		}
	)
}
'plural reference expression' {
	'type': stategroup (
		'augment' {
			'collection path': component 'variablized object path'
			'filter path': [ * ] component 'descendant object path'
		}
		'union' { [ union ]
			'branches': [ (, ) ] dictionary {
				'has predecessor': stategroup = node-switch predecessor (
					| node = 'yes' { }
					| none = 'no'
				)
				'has successor': stategroup = node-switch successor (
					| node = 'yes' { }
					| none = 'no'
				)
				'source': [ = ] stategroup (
					'dependency' {
						'path': component 'plural object path'
						'dependency': component 'dependency step'
					}
					'dependency inversion' {
						'reference set path': component 'variablized object path'
						'subset path': component 'reference set subset step'
					}
				)
			}
			/* The 'first branch' is computed by the compiler.
			** For recursive union computations, the first branch may not require recursion,
			** and should ensure the required cardinality corresponding to the cardinality constraint (one for non-empty). */
			'first branch': reference = first
		}
	)
}
'plural descendant node path' {
	'has steps': stategroup (
		'no' { }
		'yes' {
			'property': [ . ] component 'property step'
			'value type': stategroup (
				'choice' {
					'state': [ ? ] reference
				}
				'node' { }
				'collection' { [ * ] }
			)
			'tail': component 'plural descendant node path'
		}
	)
}

Permissions and Todos


The application language supports expression permission and todo requirements down to the level of a single specific user or interface. At the node type level you can specify permissions for nodes: read and update permissions. In addition, you can express that a node creates a todo-item in a todo-list of your application. At collection properties and states, you can specify item permissions: create and delete permissions for collection entries and states. At command attributes you can express permission requirements for executing a command.

'node permissions definition' { dynamic-order
	'read permission': stategroup (
		'inherited' { }
		'explicit' { [ can-read: ]
			'permission': component 'permission'
		}
	)
	'update permission': stategroup (
		'inherited' { }
		'explicit' { [ can-update: ]
			'permission': component 'permission'
		}
	)
}
'item permissions definition' { dynamic-order
	'create permission': stategroup (
		'inherited' { }
		'explicit' { [ can-create: ]
			'permission': component 'permission'
		}
	)
	'delete permission': stategroup (
		'inherited' { }
		'explicit' { [ can-delete: ]
			'permission': component 'permission'
		}
	)
}
'permission' {
	'source': stategroup (
		'user' {
			'requirement': component 'user requirement'
		}
		'imported interface' {
			'interface': [ interface ] reference
		}
	)
}

Permissions

When read permissions and update permissions are not specified at the root node type, all application users can read and update all application data. Thus, if your applications supports anonymous users, anyone with your application’s url can access all application data.

To restrict access to your application data, you can start by specifying some permissions at the root node type. The line can-read: user at the root expresses that only users with a user account (in the Users collection), have access to application data:

root {
	can-read: user
	can-update: user .'Type'?'Admin'

	'Users': collection ['ID']
		can-create: user .'Type'?'Admin'
		can-delete: user .'Type'?'Admin'
	{
		can-read: user is ( /*this*/ ) || user .'Type'?'Admin'
		can-update: user .'Type'?'Admin' // NOTE: unneeded

		'ID': text
		'Address': group { can-update: ^ is ( user )
			'Street': text
			'City': text
		}
		'Type': stategroup (
			'Admin' { }
			'Employee' { }
			'Unknown' { has-todo: user .'Type'?'Admin' }
		)
	}
	// only team members can read team information:
	'Teams': collection ['Name'] { can-read: .'Members' [ user .'ID' ]
		'Name': text
		'Members': collection ['Member'] {
			'Member': text ~> ^ ^ .'Users'[]
		}
		'Description': text
	}
}

Node types inherit read and update permission requirements from their ancestor node types. That is, if you specify required permissions at the root node type, you do not have to repeat it a child node type. The child node type takes the permission requirements from the root node type.

Read permissions

Read permission requirements are cumulative. That is, in order to read a node, a user requires read permission for all ancestors nodes of that node as well. For example, in order to read a Members node specified above, a user needs permissions for the ancestor Teams-node, as well as the root node.

Update permissions

Update permission requirements are not cumulative. Instead, update permissions for child nodes override permission requirements for parent nodes. For example, for updates to the root node, we can require an admin: user .'Type'?'Admin'. If we were to omit other permission requirements, only admins would be able to update application data because of inheritance. But, because of can-update: ^ is ( user ) at Address in the model, users can (only!) update their own Address information.

NOTE: for updating a node, users have to be able to read ancestor nodes!

Create and delete permissions

Create and delete permission requirements for collection entries and states (items) inherit update permission requirements from the parent node type. The permissions are one-off overrides that are not carried down in the node type hierarchy. That is, they only apply to the state type or collection attribute where they are specified.

Command execution permissions

Command execution permissions are independent of aforementioned permissions. Execution permission requirements only apply to the command for which they are specified. If a command A calls another command B, then required permissions for B are not checked. That is, the application only verifies that the user is permitted to execute command A.

NOTE: for executing a command, the command has to be reachable, meaning that users need read access for the node on which they want to execute the command!

'command permission definition' {
	'execute permission': stategroup (
		'reachable' { }
		'explicit' { [ can-execute: ]
			'requirement': component 'user requirement'
		}
	)
}

Todo items

Todo items are shown in a special section of your generated application. The list of todo items is constructed from nodes that are marked as todos. You express that in your model with has-todo: followed by an expression that specifies to which users the todo item applies.

'todo definition' {
	'todo': stategroup (
		'no' { }
		'yes' { [ has-todo: ]
			'requirement': component 'user requirement'
			'ui': component 'ui todo'
		}
	)
}

User requirements

For user requirements you can depend on your application data via complex expressions that traverse your model. With the || keyword, you specify alternatives. With a where clause, you specify requirements on top of requirements (‘and’).

'user requirement' {
	'expression': component 'node expression'
	'has filter': stategroup (
		'no' { }
		'yes' {
			'assignment': component 'optional variable assignment'
			'path': [ where (, ) ] component 'user requirement'
		}
	)
	'has alternative': stategroup (
		'no' { }
		'yes' { [ || ]
			'alternative': component 'user requirement'
		}
	)
}

Commands and Timers


Commands enable external systems to perform operations via an Alan interface on application data. Furthermore, commands can perform operations on other systems that other systems provide via an Alan interface. For this to work, interfaces have to be listed in the interfaces section of an application model. Also, the external command needs to be consumed by the application model, like the Place Order command in the example:

'Products': collection ['Name'] {
	'Name': text
}
'Place Order': command { 'Product': text -> .'Products'[] } external
'Orders': collection ['ID'] {
	'ID': text
	'Product': text -> ^ .'Products'[]
	'Creation Time': number 'date and time' = creation-time
	'Status': stategroup (
		'New' {
			'Order from Manufacturer': command { }
				=> execute .'Place Order' ( 'Product' = ^ .'Product' )
				=> update ^ (
					'Status' = create 'Waiting for Manufacturer' ( )
				)
		}
		'Delivered' { }
		'Delayed' { }
		'Waiting for Manufacturer' {
			'Agreed Upon Delivery Time': number 'date and time'
				timer ontimeout => update ^ (
					'Status' = ensure 'Delayed' ( )
				)
		}
	)
}
// for external system 'Delivery Service':
'Register Delivery': command { 'Order': text -> .'Orders'[] }
	update @ >'Order' (
		'Status' = create 'Delivered' ( )
	)
'command object expression' {
	'properties': [ (, ) ] dictionary {
		'expression': [ = ] component 'command expression'
	}
}
'command object initialization behaviour' {
	'behaviour': stategroup (
		'ensure existence' { [ ensure ] }
		'fail when exists' { [ create ] }
	)
}
'command expression' {
	'operation': stategroup (
		'state switch' { [ switch ]
			'path': group {
				'path': component 'singular node path'
				'stategroup': [ . ] reference
			}
			'states': [ (, ) ] dictionary { [ | ]
				'variable assignment': component 'optional variable assignment'
				'expression': [ => ] component 'command expression'
			}
		}
		'node switch' { [ switch ]
			'expression': component 'node expression'
			'cases': [ (, ) ] group { dynamic-order
				'node case': [ | node ] group {
					'variable assignment': component 'optional variable assignment'
					'expression': [ => ] component 'command expression'
				}
				'none case': [ | none ] group {
					'expression': [ => ] component 'command expression'
				}
			}
		}
		'walk' { [ walk ]
			'path': component 'singular node path'
			'type': stategroup (
				'collection' {
					'collection': [ . ] reference
				}
				'reference set' {
					'reference set': [ < ] reference
				}
			)
			'tail': [ * ] component 'node path tail'
			'variable assignment': component 'optional variable assignment'
			'expression': [ (, ) ] component 'command expression'
		}
		'ignore' { [ ignore ] }
		'update properties' { [ update ]
			'path': component 'singular node path'
			'target': stategroup (
				'node' {
					'expression': component 'command object expression'
				}
				'property' {
					'property': [ . ] reference
					'expression': [ = ] component 'command expression'
				}
			)
		}
		'execute operation' { [ execute ]
			'path': component 'singular node path'
			'command': [ . ] reference
			'expression': component 'command object expression'
		}
		'produce value' {
			'value': stategroup (
				'object' {
					'expression': component 'command object expression'
				}
				'state' {
					'behaviour': component 'command object initialization behaviour'
					'state': reference
					'expression': component 'command object expression'
				}
				'scalar' {
					'expression': component 'scalar expression'
				}
				'empty collection' { [ empty ] }
				'current value' { [ current || ]
					'expression': component 'command expression'
				}
			)
		}
		'entry' {
			'type': stategroup (
				'create' {
					'behaviour': component 'command object initialization behaviour'
					'expression': component 'command object expression'
				}
				'delete' { [ delete ]
					'path': component 'singular node path'
				}
			)
		}
	)
}
'command' {
	'expression': [ => ] component 'command expression'
	'has next command': stategroup (
		'yes' {
			'command': component 'command'
		}
		'no' { }
	)
}
'user initializer' {
	'initializer': component 'command object expression'
}
'password initializer' {
	'initializer': component 'command object expression'
}
'identity initializer' {
	'initializer': component 'command object expression'
}
'interface instance initializer' {
	'initializer': component 'command object expression'
}
'scalar expression list' {
	'head': component 'scalar expression'
	'has tail': stategroup (
		'no' { }
		'yes' { [ , ]
			'tail': component 'scalar expression list'
		}
	)
}
'scalar expression' {
	'type': stategroup (
		'number' {
			'type': stategroup (
				'constant' {
					'value': component 'constant number value'
				}
				'now in seconds' { [ now ] }
				'unary expression' {
					'type': stategroup (
						'absolute value' { [ abs ] }
						'numerical type conversion' {
							'conversion': [ from ] reference
						}
						'sign inversion' { [ - ] }
					)
					'expression': component 'scalar expression'
				}
				'list expression' {
					'operation': stategroup (
						'sum' { [ sum ] }
						'minimum' { [ min ] }
						'maximum' { [ max ] }
						'product' { [ product ] }
					)
					'expression': [ (, ) ] component 'scalar expression list'
				}
				'binary expression' {
					'operation': stategroup (
						'division' { [ division ]
							'rounding': stategroup (
								'ordinary' { }
								'ceil' { [ ceil ] }
								'floor' { [ floor ] }
							)
						}
						'remainder' { [ remainder ] }
					)
					'expressions': [ (, ) ] group {
						'left': [, , ] component 'scalar expression'
						'right': component 'scalar expression'
					}
				}
			)
		}
		'text' {
			'type': stategroup (
				'static' {
					'value': text
				}
			)
		}
		'dynamic' {
			'path': component 'singular node path'
			'property': [ . ] reference
		}
	)
}

Navigation steps:

.'My Property'          // get property value
?'My State'             // require state
>'My Text'              // get referenced node (for text properties with a reference)
.'My Text'&'Where'      // get 'where'-rule value from property
.&'My State Where'      // get 'where'-rule result from context state
.'My Collection'*       // iterate collection
--
^              // go to parent node
$^             // go to parent context with $ object
$              // select nearest $ object
@              // select nearest parameter node

Node navigation

'ancestor node path' {
	'has steps': stategroup (
		'no' { }
		'yes' { [ ^ ]
			'tail': component 'ancestor node path'
		}
	)
}
'context node path' {
	'context': stategroup (
		'dynamic user' { [ user ] }
		'this' { }
		'variable' {
			'name': stategroup (
				'implicit' { [ $ ] }
				'explicit' {
					'head': component 'ancestor variable path'
					'variable': [ $ ] reference
				}
			)
		}
	)
}
'node step' {
	'type': stategroup (
		'parent' { [ ^ ] }
		'property rule' {
			'text': [ . ] reference
			'rule': [ & ] reference
		}
		'group' { [ . ]
			'group': reference
		}
		'state context rule' {
			'rule': [ .& ] reference
		}
		'state' {
			'state group': [ . ] reference
			'state': [ ? ] reference
		}
		'reference' { [ > ]
			'text': reference
		}
	)
}
'node path tail' {
	'has steps': stategroup (
		'no' { }
		'yes' {
			'step': component 'node step'
			'tail': component 'node path tail'
		}
	)
}
'node path' {
	'head': component 'context node path'
	'tail': component 'node path tail'
}
'singular node path' {
	'path': component 'node path'
}
'conditional node path' {
	'path': component 'node path'
}
'descendant base property path' {
	'head': component 'object path tail'
	'property': [ . ] reference
}
'node expression' {
	'node path': component 'conditional node path'
	'type': stategroup (
		'existence' { }
		'equality' {
			'expected node path': [ is (, ) ] component 'conditional node path'
		}
		'containment' {
			'collection': [ . ] reference
			'key path': [ [, ] ] group {
				'path': component 'conditional node path'
				'text': [ . ] reference
			}
		}
	)
}

Variable assignment and navigation

'optional variable assignment' {
	'has assignment': stategroup (
		'no' { }
		'yes' {
			'assignment': component 'variable assignment'
		}
	)
}
'variable assignment' {
	'name': [ as ] stategroup (
		'implicit' { [ $ ] }
		'explicit' {
			'name': reference = first
			'named objects': dictionary { [ $ ]
				'has successor': stategroup = node-switch successor (
					| node = 'yes' { }
					| none = 'no'
				)
			}
		}
	)
}
'named object assignment' {
	'name': [ as ] stategroup (
		'implicit' { [ $ ] }
		'explicit' {
			'name': reference = first
			'named objects': dictionary { [ $ ]
				'has successor': stategroup = node-switch successor (
					| node = 'yes' { }
					| none = 'no'
				)
			}
		}
	)
}
'optional named object assignment' {
	'has assignment': stategroup (
		'no' { }
		'yes' {
			'assignment': component 'named object assignment'
		}
	)
}
'ancestor named object path' {
	'has steps': stategroup (
		'no' { }
		'yes' { [ $^ ]
			'tail': component 'ancestor named object path'
		}
	)
}
'ancestor variable path' {
	'has steps': stategroup (
		'no' { }
		'yes' { [ $^ ]
			'tail': component 'ancestor variable path'
		}
	)
}
'named object step' {
	'name': stategroup (
		'implicit' { [ $ ] }
		'explicit' {
			'head': component 'ancestor named object path'
			'named object': [ $ ] reference
		}
	)
}
'context object step' {
	'context': stategroup (
		'this' { }
		'variable' {
			'step': component 'named object step'
		}
		'dynamic user' { [ user ] }
	)
}
'group ancestor node path' {
	'has steps': stategroup (
		'no' { }
		'yes' { [ ^ ]
			'tail': component 'group ancestor node path'
		}
	)
}
'property step' {
	'property': reference
}
'where rule step' {
	'rule': reference
}
'dependency step' {
	'type': stategroup (
		'reference' {
			'property': [ > ] component 'property step'
		}
		'reference rule' {
			'property': [ . ] component 'property step'
			'rule': [ & ] component 'where rule step'
		}
		'state context rule' {
			'rule': [ .& ] component 'where rule step'
		}
	)
}
'reference set step' {
	'reference set': [ < ] reference
}
'object path tail' {
	'has steps': stategroup (
		'no' { }
		'yes' {
			'type': stategroup (
				'parent' { [ ^ ] }
				'property value' {
					'property': [ . ] component 'property step'
				}
				'dependency' {
					'dependency': component 'dependency step'
				}
				'reference set' {
					'step': component 'reference set step'
				}
				'state' {
					'state': [ ? ] reference
				}
			)
			'tail': component 'object path tail'
		}
	)
}
'plural object path tail' {
	'has steps': stategroup (
		'no' { }
		'yes' { [ * ]
			'path': component 'object path tail'
			'tail': component 'plural object path tail'
		}
	)
}
'plural object path' {
	'head': component 'variablized object path'
	'path': component 'plural object path tail'
}
'descendant object path' {
	'path': component 'object path tail'
}
'variablized object path' {
	'head': component 'context object step'
	'path': component 'object path tail'
}
'singular variablized object path' {
	'path': component 'variablized object path'
}
'object set path' {
	'collection path': component 'singular variablized object path'
	'value path': [ * ] component 'object path tail'
}
'entry reference selector' {
	'definer': stategroup (
		'property' {
			'property': [ > ] reference
		}
		'rule' {
			'property': [ . ] reference
			'rule': [ & ] reference
		}
	)
}

User interface annotations


User interface annotations, or annotations for short, are recognizable by the @ character before the keyword. E.g. @default:. Most annotations affect generated user interfaces (a system of type auto-webclient); they typically do not affect custom user interfaces (system type webclient).

It is possible to use multiple annotations on a single property. Be aware that they should be added in a specific order. Consult this grammar for the order of annotations.

Overview

Different attribute types support the same types of annotations. This overview presents the commonly used annotations that different attribute types share.

Defaults

Most property types support the @default: annotation. Defaults apply default values to properties of new nodes only and fail silently. That is, if the path contains a state navigation step and the stategroup is not in that state, the default won’t be applied. In the following example the Score property will not be set when the state of Default Score is anything other than Known.

'Score': number positive 'score' @default: .'Default Score'?'Known'.'Value'

A default value is not necessarily a valid value. If validation rules or constraints apply, a default value can be invalid. When a derived value is used, make sure it is not a part of the new node. Derived values on new nodes are computed after the node is saved, and will therefore not be included in default values. For example, this initializes the Description with Deliver pieces:

'Description': text @default: concat ( "Deliver ", to-text .'To deliver', " pieces." )
'To deliver' : number 'pieces' = ^ >'Order'.'Amount'

Descriptions

All attribute types support the @description: annotation. This annotation adds additional information about an attribute: what it is meant for. Alan GUI’s typically present it as alt text, or a compact description at an input field.

Visibility

Commands and attributes that hold derived values, support the @hidden annotation. The annotation hides an attribute from the UI.

Identifying properties

Properties that are important for the identification of a collection entry can be marked with the @identifying annotation. Identifying properties are shown together with the unique identifier of a collection entry, whenever the entry is referenced. For example, when an employee has a uniquely identifying personnel number in a collection of employees, a reference to employee will show the name of the employee when the ‘name’ property is marked as identifying.

'ui identifying property selection' {
	'has properties': stategroup = node-switch .'properties' (
		| nodes = 'yes' { 'first' = first }
		| none  = 'no'
	)
	'properties': [ (, ) ] dictionary {
		'has successor': stategroup = node-switch successor (
			| node = 'yes' { 'successor' = successor }
			| none = 'no'
		)
		'value type': stategroup (
			'scalar' { }
			'node' {
				'selection': component 'ui identifying property selection'
			}
			'choice' {
				'state key is identifying': stategroup (
					'no' { [ @hidden ] }
					'yes' { }
				)
				'first state': reference = first
				'states': [ (, ) ] dictionary { [ | ]
					'has successor': stategroup = node-switch successor (
						| node = 'yes' { 'successor' = successor }
						| none = 'no'
					)
					'selection': [ => ] component 'ui identifying property selection'
				}
			}
		)
	}
}

Icons

For better visual identification, attributes support the @icon: annotation attribute. As an argument it takes an icon name. A complete list of the supported icons can be found at: fonts.google.com/icons.

Actions

'ui action attribute' {
	'has description': stategroup (
		'no' { }
		'yes' { [ @description: ]
			'description': text
		}
	)
}

An action attribute describes a sequence of actions (operations), as explained in the section on action attributes.

'ui action' {
	'expression': [ => ] component 'ui expression'
	'has next action': stategroup (
		'yes' {
			'action': component 'ui action'
		}
		'no' { }
	)
}

The keyword interactive instructs your application to display a screen to the user for applying/executing an operation.

'ui action interaction' {
	'interactive': stategroup (
		'yes' { [ interactive ] }
		'no' { }
	)
}

The keyword show presents the result of an operation after performing it.

'ui action visualization' {
	'show target': stategroup (
		'yes' { [ show ] }
		'no' { }
	)
}
'ui object expression' {
	'properties': [ (, ) ] dictionary {
		'default': [ = ] component 'ui expression'
	}
}
'ui entry expression list' {
	'entry expression': component 'ui expression'
	'more entries': stategroup (
		'no' { }
		'yes' { [ , ]
			'tail': component 'ui entry expression list'
		}
	)
}

Create a collection entry with the keyword create.

Note: user interaction for create within an update <path to node> ( ... ) statement yields different user interaction than update <path to collection> = create .... With the first statement, the GUI shows the entry that contains the node that the path after update yields ($):

... => update $ (
	'collection' = create interactive ( ... )
)

To show only the created entry, you can use this:

... => update $ .'collection' = create interactive ( ... )
'ui expression' {
	'operation': stategroup (
		'state switch' { [ switch ]
			'path': group {
				'path': component 'ui parametrized node path'
				'stategroup': [ . ] reference
			}
			'states': [ (, ) ] dictionary { [ | ]
				'variable assignment': component 'optional variable assignment'
				'expression': [ => ] component 'ui expression'
			}
		}
		'node switch' { [ switch ]
			'expression': component 'node expression'
			'cases': [ (, ) ] group { dynamic-order
				'node case': [ | node ] group {
					'variable assignment': component 'optional variable assignment'
					'expression': [ => ] component 'ui expression'
				}
				'none case': [ | none ] group {
					'expression': [ => ] component 'ui expression'
				}
			}
		}
		'walk' { [ walk ]
			'path': component 'singular node path'
			'collection': [ ., * ] reference
			'tail': component 'node path tail'
			'variable assignment': component 'optional variable assignment'
			'expression': [ (, ) ] component 'ui expression'
		}
		'ignore' { [ ignore ] }
		'update properties' { [ update ]
			'interaction': component 'ui action interaction'
			'visualization': component 'ui action visualization'
			'path': component 'singular node path'
			'target': stategroup (
				'node' {
					'expression': component 'ui object expression'
				}
				'property' {
					'property': [ . ] reference
					'expression': [ = ] component 'ui expression'
				}
			)
		}
		'execute operation' { [ execute ]
			'interaction': component 'ui action interaction'
			'path': component 'singular node path'
			'operation': [ . ] reference
			'expression': component 'ui object expression'
		}
		'produce value' {
			'value': stategroup (
				'object' {
					'expression': component 'ui object expression'
				}
				'state' {
					'state': [ create ] reference
					'expression': component 'ui object expression'
				}
				'scalar' {
					'expression': component 'ui scalar value expression'
				}
			)
		}
		'entry list' { [ (, ) ]
			'entries': component 'ui entry expression list'
		}
		'entry' {
			'type': stategroup (
				'create' { [ create ]
					'interaction': component 'ui action interaction'
					'visualization': component 'ui action visualization'
					'expression': component 'ui object expression'
				}
				'delete' { [ delete ]
					'interaction': component 'ui action interaction'
					'path': component 'singular node path'
				}
			)
		}
	)
}
'ui scalar value expression list' {
	'value': component 'ui scalar value expression'
	'has tail': stategroup (
		'no' { }
		'yes' { [ , ]
			'tail': component 'ui scalar value expression list'
		}
	)
}
'ui scalar value expression' {
	'type': stategroup (
		'text' {
			'type': stategroup (
				'auto increment' { [ auto-increment ]
					'scope': component 'ancestor node path'
				}
				'static' {
					'value': text
				}
				'guid' { [ guid ] }
				'number to text' { [ to-text ]
					'numerical type': reference
					'pad': stategroup (
						'yes' { [ pad ]
							'size': integer
							'character': [ with ] text
						}
						'no' { }
					)
					'expression': component 'ui scalar value expression'
				}
				'concatenation' { [ concat (, ) ]
					'list': component 'ui scalar value expression list'
				}
			)
		}
		'number' {
			'type': stategroup (
				'constant' {
					'value': component 'constant number value'
				}
				'now in seconds' { [ now ] }
				'unary expression' {
					'type': stategroup (
						'absolute value' { [ abs ] }
						'numerical type conversion' { [ from ]
							'representation based': stategroup (
								'ui date and time' { [ @date-time ] }
								'no' { }
							)
							'conversion': reference
						}
						'sign inversion' { [ - ] }
					)
					'expression': component 'ui scalar value expression'
				}
				'list expression' {
					'operation': stategroup (
						'sum' { [ sum ] }
						'minimum' { [ min ] }
						'maximum' { [ max ] }
						'product' { [ product ] }
					)
					'value': [ (, ) ] component 'ui scalar value expression list'
				}
				'binary expression' {
					'operation': stategroup (
						'division' { [ division ]
							'rounding': stategroup (
								'ordinary' { }
								'ceil' { [ ceil ] }
								'floor' { [ floor ] }
							)
						}
						'remainder' { [ remainder ] }
					)
					'expressions': [ (, ) ] group {
						'left': [, , ] component 'ui scalar value expression'
						'right': component 'ui scalar value expression'
					}
				}
			)
		}
		'style' {
			'type': stategroup (
				'property' { [ to-color ]
					'expression': component 'ui scalar value expression'
				}
				'fixed' {
					'style': component 'ui style'
				}
			)
		}
		'state' {
			'state': reference
		}
		'dynamic' {
			'path': component 'ui parametrized node path'
			'property': [ . ] reference
		}
		'sticky' { [ sticky ] }
	)
}
'ui scalar default' {
	'expression': component 'ui expression'
	'has fallback': stategroup (
		'yes' { [ || ]
			'fallback': component 'ui scalar default'
		}
		'no' { }
	)
}
'ui style' {
	'style': stategroup (
		'foreground' { [ foreground ] }
		'background' { [ background ] }
		'brand' { [ brand ] }
		'link' { [ link ] }
		'accent' { [ accent ] }
		'success' { [ success ] }
		'warning' { [ warning ] }
		'error' { [ error ] }
	)
}
'ui text validation' {
	'regular expression': text
}
'ui parametrized node path' {
	'path': component 'conditional node path'
}
'ui node path' {
	'path': component 'node path'
}
'node type id' {
	'steps': component 'node type id path'
}
'node type id path' {
	'has steps': stategroup (
		'no' { }
		'yes' {
			'property': [ . ] reference
			'value type': stategroup (
				'choice' {
					'state': [ ? ] reference
				}
				'node' { }
				'collection' { [ * ] }
			)
			'tail': component 'node type id path'
		}
	)
}
'ui property classification' {
	'classification': stategroup (
		'identifying' { [ @identifying ] }
		'outlining' { [ @outlining ] }
		'standard' { }
		'hidden' { [ @hidden ] }
	)
}

Reference sets

'ui reference set attribute' { dynamic-order
	'visible': stategroup (
		'true' { }
		'false' { [ @hidden ] }
	)
	'break out': stategroup (
		'no' { }
		'yes' { [ @breakout ] }
	)
	'has description': stategroup (
		'no' { }
		'yes' { [ @description: ]
			'description': text
		}
	)
}

Commands

'ui command attribute' { dynamic-order
	'visible': stategroup (
		'true' { }
		'false' { [ @hidden ] }
	)
	'has description': stategroup (
		'no' { }
		'yes' { [ @description: ]
			'description': text
		}
	)
}

Groups

The @breakout annotation indicates that a group property should not be displayed together with the other properties. At the time of this writing, the group is put in a separate tab in the details view of an entry. Note that the tab is always added to the top level tabs of a details view. That is, if a group is inside a state or another group, it is not added to the tabs of the state.

'ui group property' { dynamic-order
	'classification': component 'ui property classification'
	'break out': stategroup (
		'no' { }
		'yes' { [ @breakout ] }
	)
	'has description': stategroup (
		'no' { }
		'yes' { [ @description: ]
			'description': text
		}
	)
	'icon': stategroup (
		'no' { }
		'yes' { [ @icon: ]
			'name': text
		}
	)
}

Collections

The @default: annotation copies entries of a source collection into the annotated collection. A subset of the entries from the source collection can be copied by specifying a state filter:

'Labels': collection ['Name'] {
	'Name': text
	'Default': stategroup (
		'Yes' { }
		'No' { }
	)
}
'Issues': collection ['Name']  {
	'Name': text
	'Labels': collection ['Name'] @default: >'Name'.'Default'?'Yes' {
		'Name': text -> ^ ^ .'Labels'[]
	}
}

This only applies to new nodes holding collections. This annotation does nothing for collections on the root node:

root {
	'People': collection ['Name'] {
		'Name': text
	}
	'Employees': collection ['Name'] @default: >'Name' { // This has no effect.
		'Name': text
	}
}

Collections can only be initialized from the collection that it references, using the key property, or using a derived link that derives from the key. In the latter case, other collections can be used to initialize an entry as long as the keys are the same.

'Imported Categories': collection ['Id'] {
	'Id': text
}
'Gegevens': collection ['Id'] @default: >'Category' {
	'Id': text /* -> some constraint or not */
	'Category': text ~> ^ .'Imported Categories'[] = .'Id'
}
'ui collection property' { dynamic-order
	'sort': stategroup (
		'no' { }
		'yes' {
			'direction': stategroup (
				'ascending' { [ @ascending: ] }
				'descending' { [ @descending: ] }
			)
			'path': component 'ui node path'
			'property': [ . ] reference
		}
	)
	'classification': component 'ui property classification'
	'break out': stategroup (
		'no' { }
		'yes' { [ @breakout ] }
	)
	'size': stategroup (
		'small' { [ @small ] }
		'large' { }
	)
	'can be dormant': stategroup (
		'no' { }
		'yes' { [ @dormant: ]
			'expression': component 'node path tail'
		}
	)
	'has description': stategroup (
		'no' { }
		'yes' { [ @description: ]
			'description': text
		}
	)
	'default': stategroup (
		'no' { }
		'yes' { [ @default: ]
			'key reference': component 'entry reference selector'
			'entry filter': component 'node path tail'
		}
	)
	'icon': stategroup (
		'no' { }
		'yes' { [ @icon: ]
			'name': text
		}
	)
	'has style': stategroup (
		'no' { }
		'yes' { [ @style: ]
			'style expression': component 'ui expression'
		}
	)
}

Numbers

'ui number property' { dynamic-order
	'classification': component 'ui property classification'
	'emphasis': stategroup (
		'no' { }
		'yes' { [ @emphasis ] }
	)
	'default': stategroup (
		'no' { }
		'yes' { [ @default: ]
			'default': component 'ui scalar default'
		}
	)
	'dynamic numerical type': stategroup (
		'no' { }
		'yes' { [ @numerical-type: ]
			'binding path': component 'node path'
		}
	)
	'metadata': stategroup (
		'no' { }
		'yes' { [ @metadata ] }
	)
	'validation': group {
		'has minimum': stategroup (
			'no' { }
			'yes' { [ @min: ]
				'minimum': component 'ui expression'
			}
		)
		'has maximum': stategroup (
			'no' { }
			'yes' { [ @max: ]
				'maximum': component 'ui expression'
			}
		)
	}
	'has description': stategroup (
		'no' { }
		'yes' { [ @description: ]
			'description': text
		}
	)
	'icon': stategroup (
		'no' { }
		'yes' { [ @icon: ]
			'name': text
		}
	)
}

Files

'ui file property' { dynamic-order
	'file name expression': stategroup (
		'no' { }
		'yes' { [ @name: ]
			'file name expression': component 'ui scalar default'
		}
	)
	'classification': component 'ui property classification'
	'has description': stategroup (
		'no' { }
		'yes' { [ @description: ]
			'description': text
		}
	)
	'icon': stategroup (
		'no' { }
		'yes' { [ @icon: ]
			'name': text
		}
	)
}

Texts

'ui text property' { dynamic-order
	'classification': component 'ui property classification'
	'emphasis': stategroup (
		'no' { }
		'yes' { [ @emphasis ] }
	)
	'type': stategroup (
		'default' { }
		'multi-line' { [ @multi-line ] }
		'color' { [ @color ] }
		'url' { [ @url ] }
		'markdown' { [ @markdown ] }
		'html' { [ @html ] }
	)
	'default value': stategroup (
		'no' { }
		'yes' { [ @default: ]
			'default': component 'ui scalar default'
		}
	)
	'has validation': stategroup (
		'no' { }
		'yes' { [ @validate: ]
			'rules': component 'ui text validation'
		}
	)
	'has description': stategroup (
		'no' { }
		'yes' { [ @description: ]
			'description': text
		}
	)
	'icon': stategroup (
		'no' { }
		'yes' { [ @icon: ]
			'name': text
		}
	)
	'has custom identifying properties': stategroup (
		'no' { }
		'yes' { [ @show: ]
			'selection': component 'ui identifying property selection'
		}
	)
	'is label': stategroup (
		'no' { }
		'yes' { [ @label ] }
	)
}
'ui reference rule' { //dynamic-order
	'has validation': stategroup (
		'no' { }
		'yes' { [ @validate: resolvable ] }
	)
}

State groups

'ui state group property' { dynamic-order
	'classification': component 'ui property classification'
	'emphasis': stategroup (
		'no' { }
		'yes' { [ @emphasis ] }
	)
	'default state': stategroup (
		'no' { }
		'yes' { [ @default: ]
			'default': component 'ui scalar default'
		}
	)
	'has description': stategroup (
		'no' { }
		'yes' { [ @description: ]
			'description': text
		}
	)
	'icon': stategroup (
		'no' { }
		'yes' { [ @icon: ]
			'name': text
		}
	)
}

States

'ui state' { dynamic-order
	'desired state': stategroup (
		'no' { }
		'yes' { [ @desired ] }
	)
	'verified state': stategroup (
		'no' { }
		'yes' { [ @verified ] }
	)
	'icon': stategroup (
		'no' { }
		'yes' { [ @icon: ]
			'name': text
		}
	)
	'has style': stategroup (
		'no' { }
		'yes' { [ @style: ]
			'style expression': component 'ui expression'
		}
	)
	'transitions': dictionary { [ @transition: ]
		'action': [ => execute ] reference
	}
}

Todo items

'ui todo' {
	'has description': stategroup (
		'no' { }
		'yes' { [ @description: ]
			'description': text
		}
	)
}

Numerical types

'ui numerical type' {
	'represent as': stategroup (
		'model' { }
		'date' { [ @date ] }
		'date and time' { [ @date-time ] }
		'duration' { [ @duration: ]
			'unit': stategroup (
				'seconds' { [ seconds ] }
				'minutes' { [ minutes ] }
				'hours' { [ hours ] }
			)
		}
		'custom type' { [ @numerical-type: ]
			'binding': stategroup (
				'root' { }
				'dynamic' { [ bind ]
					'path': component 'node type id'
					'assignment': component 'variable assignment'
				}
			)
			'properties': [ (, ) ] group {
				'label': [ label: ] component 'ui scalar value expression'
				'conversion': stategroup (
					'none' { }
					'point translation' {
						'decimals': [ decimals: ] component 'ui scalar value expression'
					}
				)
			}
		}
	)
}