Metanorma: Aequitate Verum

Encode OGC ModSpec using `yaml2text` templates

Author’s picture Manuel Fuenmayor on 04 Apr 2024

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:

  1. Place and arrange all the requirements data into a YAML file.

  2. Write the template in Liquid and save it in a separate .liquid file.

  3. Create a yaml2text block in the Metanorma document specifying the corresponding YAML file, and including the Liquid template using the include:: directive.

  4. 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.

Table 1. Sample requirement to be encoded
Requirement 1

Identifier

/req/relief/classes

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.

YAML file 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 file 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:

{% for element in elements %}
//... content ...
{% endfor %}

With the data file and the template file, we proceed to create the yaml2text block in our Metanorma document:

Definition of the yaml2text block encoding the Sample requirement to be encoded
[yaml2text,data.yaml,context] (1)
--
include::template.liquid[] (2)
--
  1. The data file data.yaml is passed into the block.

  2. The template file template.liquid receives the context 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:

Output of the 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 file 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 file 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!