Encode OGC ModSpec using `yaml2text` templates
Purpose
OGC standards use the ModSpec model to encode requirements, and sometimes there are a lot of them. ISO/TC 211 has also begun to encode requirements in OGC ModSpec fashion.
yaml2text
is a Metanorma plugin that allows you to encode large amounts of
data that share the same structure in a reduced number of lines, via pre-defined
template. The data for the plugin is arranged in YAML format,
and the template is written in Liquid.
The main goal of this article is to introduce you to the application of
yaml2text
to encode ModSpec requirement instances.
To read this article you need to be familiar with the encoding basics of
yaml2text
and ModSpec instances in Metanorma. To that end, it is
recommended to read these first before continuing:
Encoding requirements with yaml2text
In order to ensure that you use yaml2text
efficiently,
and to avoid code repetition, follow these steps:
-
Place and arrange all the requirements data into a YAML file.
-
Write the template in Liquid and save it in a separate
.liquid
file. -
Create a
yaml2text
block in the Metanorma document specifying the corresponding YAML file, and including the Liquid template using theinclude::
directive. -
Compile the document to test the correct rendering of the requirements; debug if necessary.
Now, let’s look at two examples: a simple one and a larger one.
Encoding a simple requirement
OGC ModSpec instances are typically encoded as a definition list.
Note
|
There are two methods to encode requirements: as a definition list or as attributes. We adopt the recommended practice of the definition list here. |
In this example, we want to encode the following Requirement using yaml2text
.
Requirement 1 | |
---|---|
Identifier |
|
Statement |
For each UML class defined or referenced in the Relief Package: |
A |
The Implementation Specification SHALL contain an element which represents the same concept as that defined for the UML class. |
B |
The Implementation Specification SHALL represent associations with the same source, target, direction, roles, and multiplicities as those of the UML class. |
First, we define the data file.
The data file represents the requirement using a fixed structure in YAML.
Let’s call it data.yaml
.
data.yaml
representing the Sample requirement to be encoded---
identifier: /req/relief/classes
statement: "For each UML class defined or referenced in the Relief Package:"
parts:
- The Implementation Specification SHALL contain an element which represents the
same concept as that defined for the UML class.
- The Implementation Specification SHALL represent associations with the same
source, target, direction, roles, and multiplicities as those of the UML class.
In YAML, data is represented using key-value pairs. Also note that we used
array representation for the parts
field. This is how it is done when we have
several elements mapped to a single field.
Once we have our data properly structured in YAML, we proceed to write the template in Liquid.
We could write our Liquid template directly in the yaml2text
block,
but it is good practice to do so in a separate file, the template file.
Let’s call this file template.liquid
.
yaml2text
requires naming a context
variable that will represent the
totality of the data saved in the YAML file. Let’s call this variable context
.
Having all set, the template is defined as follows:
template.liquid
for rendering the Sample requirement to be encoded[requirement]
====
[%metadata]
identifier:: {{ context.identifier }}
statement:: {{ context.statement }}
{% for part in context.parts %}
part:: {{ part }}
{% endfor %}
====
Note
|
In Liquid, arrays are typically handled with for loops:
|
With the data file and the template file, we proceed to create the
yaml2text
block in our Metanorma document:
yaml2text
block encoding the Sample requirement to be encoded[yaml2text,data.yaml,context] (1)
--
include::template.liquid[] (2)
--
-
The data file
data.yaml
is passed into the block. -
The template file
template.liquid
receives thecontext
variable from the block.
Here, we have assumed that data.yaml
and template.liquid
are in the same
location as the Metanorma document. Remember that the path to these files is
calculated based on relative location.
At this point, we can compile the document to check if the requirement
renders correctly. Note that for such a small template, we could place the code right
inside of the yaml2text
block without the need for the include
directive.
But we do this mainly to avoid code repetition in subsequent blocks.
Once Metanorma processes the Liquid template, the yaml2text
block
will result in this content:
yaml2text
processed block[requirement]
====
[%metadata]
identifier:: /req/relief/classes
statement:: For each UML class defined or referenced in the Relief Package:
part:: The Implementation Specification SHALL contain an element which represents the
same concept as that defined for the UML class.
part:: The Implementation Specification SHALL represent associations with the same
source, target, direction, roles, and multiplicities as those of the UML class.
====
That’s it! The process to encode a requirement using yaml2text
is that simple.
Now, let’s investigate a more complex example.
Encoding a Conformance class with embedded Conformance tests
In ModSpec, Conformance classes contains Conformance tests.
The challenge in managing them is that while the Conformance class links to individual Conformance tests, the individual Conformance tests also have to link back to the Conformance class. Hence we opt to encode all of them in a single YAML file.
Let’s encode a Conformance class that is already defined by this YAML markup.
Note
|
This is a real example from the source files of the published ISO 19115-3:2023. |
data.yaml
of a Conformance class instance arranged in YAML format---
conformance_classes:
- name: Validation of XML instance for metadata basic information
identifier: https://standards.isotc211.org/19115/-1/1/conf/metadata-xml/basic
target: https://standards.isotc211.org/19115/-1/1/req/metadata-xml/basic
dependencies:
- https://standards.isotc211.org/19115/-1/1/conf/metadata-minimal-xml
- https://standards.isotc211.org/19115/-1/1/conf/metadata-xml/common
- https://standards.isotc211.org/19115/-1/1/conf/metadata-xml/multilingual
tests:
- name: Validate with XSD
identifier: https://standards.isotc211.org/19115/-1/1/conf/metadata-xml/basic/schema-valid
targets:
- https://standards.isotc211.org/19115/-1/1/req/metadata-xml/basic/valid
method: Validate with metadataBase.xsd
- name: Verify presence of identification information
identifier: https://standards.isotc211.org/19115/-1/1/conf/metadata-xml/basic/identification
targets:
- https://standards.isotc211.org/19115/-1/1/req/metadata-xml/basic/identification
method: |
Inspection to determine that the element populating the "identification"
property is defined in the substitution group for
Abstract_ResourceDescription.
In this arrangement, the conformance_classes
field is meant to bundle several
Conformance classes. Here only one Conformance class is shown.
Each Conformance class has the following components:
-
name
-
identifier
-
target
-
several
dependencies
(array) -
several
tests
(array)
Under tests
, each Conformance test is composed of:
-
name
-
identifier
-
target
(array) -
method
Once the structure of the data is well-understood, we can proceed to write the Liquid template.
As above, we define context
as the context variable.
template.liquid
that renders the Conformance class and Conformance tests{% for scope in context.conformance_classes %}
.{{scope.name}}
[conformance_class]
====
[%metadata]
identifier:: {{scope.identifier}}
target:: {{scope.target}}
{% for depend in {{scope.dependencies}} %}
inherit:: {{depend}}
{% endfor %}
{% for test in {{scope.tests}} %}
conformance-test:: {{test.identifier}}
{% endfor %}
====
{% for test in {{scope.tests}} %}
{% if {{test.name}} %}
.{{test.name}}
{% endif %}
[conformance_test]
====
[%metadata]
identifier:: {{test.identifier}}
{% for target in {{test.targets}} %}
target:: {{target}}
{% endfor %}
{% for depend in {{test.dependencies}} %}
inherit:: {{depend}}
{% endfor %}
{% if {{test.method}} %}
test-method::
+
--
{{test.method}}
--
{% endif %}
====
{% endfor %}
{% endfor %}
Multiple if statements are used to verify the presence of data in fields. This is necessary when dealing with multiple requirement instances.
This template, assumed to be saved as the file template.liquid
at the same
location as the Metanorma file, is to be included in a yaml2text
block inside
the Metanorma document.
yaml2text
block that encodes Conformance classes and Conformance tests[yaml2text,data.yaml,context]
--
include::template.liquid[]
--
From here, we can compile the document to verify its correct rendering, and debug if necessary.
This process is equally applicable to any other ModSpec instances, including Recommendations and Permissions.
External resources
Thanks to OGC, the OGC GeoPose document (GitHub) is an open-source, fully fledged example of this approach in encoding Requirements and Conformance classes.
Since it is a real-life example, the templates provided there are more generic and comprehensive (i.e. longer) than what we have explained here. The fundamentals, however, are the same as what is explained in this post.
Feel free to use them directly, or as a guide to design your own templates according to your needs!