Skip to main content

NativeTypes

NativeTypes represent how a ValueType is mapped to specific use-cases. They are the native representations of these Types in the different technologies.

E.g. this nativeType define the String java class:

<nativeType name="java:transport:java.lang.String">
<value>String</value>
<using>
<usage>
<value>java.lang.String</value>
</usage>
</using>
<technology>JDK</technology>
<concern>transport</concern>
</nativeType>

Each SimpleValueType is linked to a number of NativeTypes. This definition can be extended by redefining a SimpleValueType in a DataResource with the additional NativeTypes.

<simpleValueType name="String">
<nativeTypes>
<nativeType>java:transport:java.lang.String</nativeType>
<nativeType>java:creation:string</nativeType>
<nativeType>java:isUndefined:string:nullOrEmpty</nativeType>
</nativeTypes>
</simpleValueType>

Using NativeTypes in Expanders

Use nativeTypes.get() to get the type connected to a concern and technology. This method will return a NativeType.

<let name="jpa" eval="technologies.get('JPA')"/>
<let name="jpaType" eval="nativeTypes.get(field, 'entity', jpa)"/> <!-- or use 'JPA' directly -->
<uses eval="jpaType"/>
<value name="type" eval="jpaType.value"/>

Introducing NativeTypes for a new Expander

When writing a new Expander, you might need to define a new set of NativeTypes.

Pick a unique concern string and a relevant technology. If your Expander defines a technology other than COMMON, then this is often the best choice.

Next, create a nativeTypes dataResource and use the definition of the NativeTypes to implement the expander.

<dataResource type="elements::NativeType">
<nativeType name="angular:input:text">
<technology>ANGULAR</technology>
<concern>input</concern>
<value>my-text-input-component</value>
<using>
<uses>
<value>TextInputComponent from @my/library</value>
<type>component</type>
</uses>
</using>
</nativeType>
<nativeType name="angular:input:date">
<technology>ANGULAR</technology>
<concern>input</concern>
<value>my-date-input-component</value>
<using>
<uses>
<value>DateInputComponent from @my/library</value>
<type>component</type>
</uses>
</using>
</nativeType>
</dataResource>

The value attribute is the type representation in the expander template, while a list of usages can be provided for the required imports. If a type is provided on a usage it will override the type if provided on the <uses> mapping. If no type is defined it will default to the type on the <uses>.

NativeType propagation

As we've seen in the previous example, we can call nativeTypes.get() on any element, such as elements::Field. How does it know to resolve the NativeType within field.valueField.type? The logic behind this is facilitated by the TypeAware interface on the metamodel Composite.

Option
meta.nativeTypes.typeAware ElementClass

Adds the TypeAware interface to an ElementClass.

<options>
<meta.nativeTypes.typeAware/>
</options>

The interface declares a method NativeType nativeType(String concern, String technology). This method can return one of three results:

  • NativeType.empty() the nativeType could not be resolved by the current element.
  • NativeType.resolved(String value) the nativeType was resolved with a value.
  • NativeType.resolveFor(CompositeProjection other) the nativeType must be resolved on a different element.

The examples below indicate how this method can be implemented:

@Override
public NativeType nativeType(String concern, String technology) {
if (this.mValueField != null) {
return NativeType.resolveFor(this.mValueField);
}
if (this.mLinkField != null) {
return NativeType.resolveFor(this.mLinkField);
}
return NativeType.empty();
}

Derived NativeTypes

Not all types in your model need to have static definitions like String does. A great example are the EnumTypes. Their value depends on their properties. For example the java className is enumType.packageName + '.' + enumType.name.

To represent these kinds of dynamic types we have introduced Derived NativeTypes. Instead of providing static strings, their value will be derived based on a template. Take for example the following DerivedNativeType:

<derivedNativeType name="java:transport:EnumType">
<template>${enumType.name}</template>
<using>
<usage>
<template>${enumType.packageName}.${enumType.name}</template>
</usage>
</using>
<elementType>enumTypeElements::EnumType</elementType>
<technology>JDK</technology>
<concern>transport</concern>
<tags><!-- optionally add tags as a condition --></tags>
</derivedNativeType>

As you can see the derivedNativeType targets a specific elementType, which is also present as the context of the templates. The template expressions are evaluated the same way as the format() function in a mapping file. This means any valid OGNL expression can be placed within ${}.

Do note that derived NativeTypes will only be evaluated at runtime when calling nativeTypes.get(). These are lazily evaluated, so only when resolved when required by the expander mapping.

NativeType Definition (legacy)

You may encounter NativeTypes with a definition field or expanders using a ValueType.getNativeType() method. This was the api to use before expanders-core 8.3.0. The definition covered both the templating and import concern. The expander mapping was responsible to collect the correct imports and format the value for the template.

Luckily the change is introduced in a backwards compatible manner so it doesn't affect expanders made before the new api. To fully support NativeTypes with a using list, the expander mapping will need to be migrated.

The new api is designed to create less friction in the mapping. Migrating shouldn't be difficult in most cases. Below some examples for how to migrate:

Before
<list name="fields">
<foreach name="field" in="dataElement.fields"/>
<filter eval="field.valueField neq null"/>
<let name="type" eval="field.valueField.type"/>
<let name="transport" eval="type.getNativeType('transport', 'JDK').get()"/>
<uses eval="transport.javaType"/>
<value name="transport" eval="transport.javaType.simpleName"/>
</list>
After
<list name="fields">
<foreach name="field" in="dataElement.fields"/>
<let name="transport" eval="nativeTypes.get(field, 'transport', 'JDK')"/>
<filter eval="transport.present"/>
<uses eval="transport"/>
<value name="transport" eval="transport.value"/>
</list>

This example shows two important improvements. First, we no longer depend on technology specific apis such as .javaType. Second, we no longer need to unwrap the result from getNativeType. You may have noticed the .get() call in the before part is unsafe.

Before
<let name="nativeType" eval="field.valueField.type.getNativeType('transport', 'JDK')"/>
<let name="type" eval="classBuilder.from(nativeType.map(:[ definition ]).orElse('java.lang.String'))"/>
<let name="initialValue" eval="field.valueField.type.getNativeType('defaultValue', 'JDK').map(:[ definition ])"/>
<uses eval="type"/>
<value name="typeName" eval="type.className"/>
<value name="initialValue" eval="initialValue.orElse('null')"/>
After
<let name="type" eval="nativeTypes.get(field, 'transport', 'JDK')"/>
<let name="initialValue" eval="nativeTypes.get(field, 'defaultValue', 'JDK')"/>
<uses eval="type"/>
<uses eval="type.empty ? 'java.lang.String' : null"/>
<value name="typeName" eval="type.getValueOrElse('String')"/>
<value name="initialValue" eval="initialValue.getValueOrElse('null')"/>

While this example does correctly handle missing values, it again shows less ceremony retrieving the type.

Before
<list name="imports">
<foreach name="field" in="dataElement.fields"/>
<filter eval="field.valueField neq null"/>
<let name="enumImports" eval="field.valueField.type.getNativeType('enum.imports', 'JPA')"/>
<filter eval="enumImports.present"/>
<foreach name="importedClass" in="enumImports.get().definition.split(';')"/>
<uses eval="importedClass"/>
</list>
After
<list name="imports">
<foreach name="field" in="dataElement.fields"/>
<uses eval="nativeTypes.get(field, 'enum.imports', 'JPA')"/>
</list>

Because nativeTypes can now simply define multiple imports, there is no logic needed to parse the import definition. We also notice less checking because the api can handle missing nativeTypes just fine.

Before
<!-- EditFormTsExpanderMapping.xml -->
<let name="formImportType" eval="field.valueField.type.getNativeType('form-input-import', angular)"/>
<uses type="component" eval="formImportType.map(:[ definition ])"/>
<!-- EditFormHtmlExpanderMapping.xml -->
<let name="formType" eval="field.valueField.type.getNativeType('form-input', angular)"/>
<filter eval="formType.present"/>
<value name="template" eval="formType.get().definition"/>
After
<!-- EditFormTsExpanderMapping.xml -->
<uses eval="nativeTypes.get(field, 'form-input', 'ANGULAR')"/>
<!-- EditFormHtmlExpanderMapping.xml -->
<let name="template" eval="nativeTypes.get(field, 'form-input', 'ANGULAR')"/>
<filter eval="template.present"/>
<value name="template" eval="template.value"/>

In this example we manage to remove the need for a separate 'imports' nativeType since these can now be defined directly on the form-input NativeType.