Transformation and Validation

Transformation

The Composite projection should aim to ease its use in the expanders. Ideally, the expanders themselves should never need to check if a particular reference exists, or try to parse values that are not expander specific.

This means that as much as possible should be done in the transformation itself.

But to perform the transformation correctly, the model must be either already valid, or a clear way to patch the model must be available.

Implementation

The validation and/or transformation can be done via:

Standardized Executable Model

The transformation and rules are defined in a formal model language (e.g. EVL/EOL) and executed through existing (generic) EVL/EOL tools.

  • NS model would need to be ECore-compliant
  • tied to single environment (Eclipse)

Expanded Skeleton and Custom Code

Only the metainformation is expanded, the actual validation code is always custom.

  • implementation for every technology (Java/Expander transformation, JavaScript/PR GUI validation, Smalltalk/modeler validation)
  • each implementation tied to specific model projections (Trees/Composites/Modeler/Info)
  • probably most flexible to deal with non-systematic issues
  • quickly out-of-sync between platforms
  • however not all platforms require the same validations
class DataOptionDisplayNameRule {
	public void check(DataElementTree dataElement, DataOptionTree dataOption, Critiques critiques) {
		// anchor:custom-validation:start
		// anchor:custom-validation:end
	}
}
DataOptionDisplayNameRule>>check: aDataOption forCritiquesDo: aCriticBlock
	"anchor:custom-validation:start"
	"anchor:custom-validation:end"

Expanded Specification

The rules are defined in a technology-neutral format (OCL/EOL/OGNL-like). Expressions are expanded/generated into technology-specific code. Each platform defines how to best transform the code based on the model/projection constraints.

e.g. simple expression

{EOL/OCL}
fieldOperatorPair.finder;
fieldOperatorPair.field;
{java/tree}
// because getFinder() returns FinderTree
fieldOperatorPairTree.getFinder();
// because getField() returns DataRef
(FieldTree)elementsRegistry.findTree(fieldOperatorPairTree.getField()).getValue();
{java/composite}
// both are Composites
fieldOperatorPairComposite.getFinder();
fieldOperatorPairComposite.getField();
{smalltalk}
"both are graph nodes"
fieldOperatorPair finder
fieldOperatorPair field

e.g. forAll loop & exists

{EOL/OCL}
dataOption.value.split('_').forAll(name |
	dataOption.dataElement.fields.exists(f | f.name = name))

a pattern specifies how the code of forAll / exists / etc. should look like

{java}
...
boolean forAll = true;
for (String name : StringUtil.split(dataOptionTree.getValue(), '_')) {
	List<FieldTree> _fields = dataOptionTree.getDataElement().getFields();
	boolean exists = false;
	for (FieldTree f : _fields) {
		exists = f.getName().equals(name);
		if (exists) {
			break;
		}
	}
}
...
{smalltalk}
(dataOption value splitOn: '_') allSatisfy: [ :name |
	dataOption dataElement fields anySatisfy: [ :f | f name = name ] ].

Seems like the cleanest approach to define rules without having to rely on (possibly) convoluted logic of Tree projections and Composite lookups. As the code is expanded, it is always obvious from the code what is being executed and how – any bug/issue with the patterns is immediately obvious. Because the code is expanded for each rule, still possiblity to override the behavior via custom anchors instead of the expanded implementation.

But: requires parsing the EVL/OCL/OGNL expressions into AST and then transforming them. Depending on the complexity of the syntax can be relatively easy (support only a strict subset of OCL that we can support in a normalized way) or very complex (full EVL/OCL/OGNL support).

Unclear how Stringly-typed interfaces should be approached (e.g. dataChild.field should be String or Field?).

Can be supported transitionally – start with custom implementation and slowly add support to the pattern transformer until custom code can disappear.

Example Case: DataOptionDisplayName rule

A displayName property should refer to (a combination of) fields of the data element it is defined in.

Examples

separate error messages

<DataOption Person::hasDisplayName>: <Field Person::firstName> is missing
<DataOption Person::hasDisplayName>: <Field Person::secondName> is missing

vs combined into one

<DataOption Person::hasDisplayName> has value "firstName_secondName", but the following fields are missing: firstName, secondName
"smalltalk"
DataOptionDisplayNameRule>>check: aDataOption forCritiquesDo: aCriticBlock
	| dataElement displayName fieldNames |
	"inplace guard"
	aDataOption dataOptionType name = 'hasDisplayName' ifFalse: [ ^ self ].
	fieldNames := aDataOption value splitOn: $_.
	fieldNames do: [ :fieldName |
		self requireField: fieldName in: dataElement do: aCriticBlock.
	].

BaseRule>>requireField: aFieldName in: aDataElement do: aCriticBlock
	aDataElement fields
		detect: [ :each | each name = aFieldName ]
		ifNone: [ aCriticBlock cull: '<Field {dataElement.name}::{fieldName}> is missing.' ]
// java
class DataOptionDisplayNameRule {

	public boolean guard(DataElementTree dataElement, DataOptionTree dataOption, Critiques critiques) {
		return dataOption.getDataOptionType().getName().equals('hasDisplayName');
	}

	public void check(DataElementTree dataElement, DataOptionTree dataOption, Critiques critiques) {
		// or inplace guard
		if (!dataOption.getDataOptionType().getName().equals('hasDisplayName')) {
			return;
		}
		List<String> fieldNames = StringUtil.splitToList(dataOption.getValue(), '_');
		for (String fieldName : fieldNames) {
			critiques.requireField(fieldName, dataElement);
		}
	}
}

class Critiques {
	public List<String> issues;

	public void requireField(String fieldName, DataElementTree dataElement) {
		for (FieldTree field : dataElement.getFields()) {
			if (field.getName().equals(fieldName)) {
				return;
			}
		}
		issues.add("<Field {dataElement.name}::{fieldName} is missing.");
	}
}
// EVL (+OCL)
context DataOption {
	critique DataOptionDisplayName {
		guard : self.dataOptionType.name = "hasDisplayName"

		check : self.value.split('_').forAll(name |
		  self.dataElement.fields.exists(f | f.name = name))

		// strict separation between the check and message may result in having to implement the check twice
		message : "<DataOption " + dataOption.dataElement.name + "::" + dataOption.dataOptionType.name + ">"
			+ "has value \"" + self.value + "\", but the following fields are missing: "
			+ (self.value.split('_').asSet() - self.dataElement.fields.collect(f | f.name).asSet()).join(', ')
	}
}
// XML (EVL simulated with OGNL, similar to the Expander mapper)
<context name="DataOption" as="dataOption"/>

<guard eval="dataOption.dataOptionType.name = 'hasDisplayName'"/>

<let name="fieldNames" eval="StringUtil.split(dataOption.value, '_')"/>
<!-- modify OGNL to delegate method calls on String to StringUtil? -->
<let name="fieldNamesCleaner?" eval="dataOption.value.splitOn('_')"/>

<check eval="fieldNames.do(:f | assert.requireField: f in: dataOption.dataElement)"/>
<message eval="..."/>

Tests

testPassing

"smalltalk"
testPassing
	| el dataOption |
	el := NSMDataElement new name: 'Person'.
	el fields add: (NSMField new name: 'firstName').
	el fields add: (NSMField new name: 'secondName').
	dataOption := NSMDataOption new.
	dataOption type: (NSMDataOptionType new name: 'hasDisplayName').
	dataOption value: 'firstName_secondName';
	el dataOptions add: dataOption.
	self denyRule: dataOption
// java
@Test
public void test_passing() {
	DataElementTree el = new DataElementTree("Person");
	el.getFields().add(new FieldTree("firstName"));
	el.getFields().add(new FieldTree("secondName"));
	DataOptionTree dataOption = new DataOptionTree();
	dataOption.setDataOptionType(DataRef.withName("hasDisplayName"));
	dataOption.setValue("firstName_secondName");
	el.getDataOptions().add(dataOption);
	// tree has no links to parent, both objects are needed
	denyRule(el, dataOption);
}

// EVL (EOL-ish)
var el : new DataElement;
el.name = "Person";
var f1 : new Field;
f1.name = "firstName";
var f2 : new Field;
f2.name = "secondName";
dataElement.fields = {f1, f2};
var dataOption : new DataOption;
var dataOptionType : new DataOptionType;
dataOptionType.name = "hasDisplayName";
dataOption.dataOptionType = dataOptionType;
dataOption.value = "firstName_secondName";
dataElement.dataOptions.add(dataOption);

validator.shouldPass(DataOptionDisplayName, dataOption);

testFailing

testFailing
	| el dataOption|
	el := NSMDataElement new name: 'Person'.
	dataOption := NSPDataOptionTypes hasDisplayName value: 'firstName_secondName'.
	el dataOptions add: dataOption.
	self assertRule: dataOption.
	self assertHints:
		{'<DataOption Person::hasDisplayName> has value "firstName_secondName", but <Field Person::firstName> is missing'.
		'<DataOption Person::hasDisplayName> has value "firstName_secondName", but <Field Person::secondName> is missing'}
@Test
public void test_failing() {
	DataElementTree el = new DataElementTree('Person');
	DataOptionTree dataOption = new DataOptionTree();
	dataOption.setDataOptionType(DataRef.withName('hasDisplayName'));
	dataOption.setValue('firstName_secondName');
	el.getDataOptions().add(dataOption);
	// tree has no links to parent, both objects are needed
	assertRule(el, dataOption,
		"<DataOption Person::hasDisplayName> has value \"firstName_secondName\", but <Field Person::firstName> is missing",
		"<DataOption Person::hasDisplayName> has value \"firstName_secondName\", but <Field Person::secondName> is missing");
}

References