Skip to content

About

No description, website, or topics provided.

Resources

Stars

1 star

Watchers

1 watching

Forks

Repository files navigation

ps.editor.actions - Generate Product Specifications

This repository provides a reusable GitHub Action that fetches metadata from Geonorge and feature catalogue information from an OGC API - Features endpoint to assemble a complete product specification package. The action downloads psdata JSON, builds feature catalogue caches (JSON, Markdown, PlantUML) and renders a Markdown specification using the bundled Handlebars-style template or a custom template you supply.

Usage

jobs:
  produktspesifikasjon:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - id: prepare
        uses: arkitektum/ps.editor.actions@main
        with:
          metadata-id: 12345678-abcd-1234-abcd-1234567890ab
          ogc-feature-api: https://example.com/collections
          output-directory: produktspesifikasjon
          product-slug: mitt-produkt
          updated: 2025-01-01

      - name: Render UML to PNG
        if: steps.prepare.outputs.feature-catalogue-uml != ''
        run: |
          sudo apt-get update
          sudo apt-get install -y plantuml graphviz
          set -euo pipefail
          while IFS= read -r file; do
            output_dir="$(dirname "$file")"
            plantuml -tpng -output "$PWD/$output_dir" "$file"
          done < <(git ls-files '*.puml')

      - name: Derive PNG path
        if: steps.prepare.outputs.feature-catalogue-uml != ''
        id: feature-png
        run: |
          png="${{ steps.prepare.outputs.feature-catalogue-uml }}"
          png="${png%.puml}.png"
          echo "path=$png" >> "$GITHUB_OUTPUT"

      - id: assemble
        uses: arkitektum/ps.editor.actions/assemble@main
        with:
          psdata-path: ${{ steps.prepare.outputs.psdata-path }}
          output-path: ${{ steps.prepare.outputs.spec-markdown }}
          feature-catalogue-markdown: ${{ steps.prepare.outputs.feature-catalogue-markdown }}
          feature-catalogue-uml: ${{ steps.prepare.outputs.feature-catalogue-uml }}
          feature-catalogue-png: ${{ steps.feature-png.outputs.path }}
          updated: 2025-01-01

The first action fetches and prepares every artefact. The optional PlantUML step converts the diagram to PNG before the second action stitches everything into the final Markdown document.

Actions

Prepare artefacts (arkitektum/ps.editor.actions@main)

Inputs:

  • metadata-id (required): Geonorge metadata UUID used to fetch psdata content.
  • ogc-feature-api: Fully qualified URL to an OGC API - Features /collections endpoint. Optional; omit when you only want psdata or when using xmi-model instead.
  • feature-type-filter: Optional list of feature type names to include (case-insensitive exact match). Provide multiple values to keep only selected feature types (applies to both OGC API and XMI).
  • scopes: Optional YAML/JSON list (or file path) describing multiple scopes. When supplied, each scope generates its own objektkatalog.md under a scope-named folder, and the main index.md links to each scope catalogue while keeping the main data model diagram.
  • output-directory (default produktspesifikasjon): Directory that will contain the generated artefacts.
  • product-slug: Overrides the auto-generated folder name (derived from the psdata title).
  • template-path: Path to a Handlebars-style template if you want to replace data/template/ps.md.hbs.
  • updated: Explicit value for the updated field in the rendered Markdown front matter (propagated to the assemble step).
  • xmi-model: Optional path or URL to a SOSI UML XMI feature catalogue. When supplied the OGC API input is ignored.
  • xmi-username / xmi-password (default sosi/sosi): Credentials used to download the XMI catalogue.
  • postgis-output (default false): Also write a PostGIS DDL script of the data model — see PostGIS schema.
  • postgis-schema: Database schema the script creates the tables in. Omit to use the search path (normally public).
  • pygeoapi-postgis-output (default false): Also write a pygeoapi configuration that serves the PostGIS tables with the PostgreSQL provider — see pygeoapi configuration for PostGIS.
  • pygeoapi-gpkg-output (default false): Also write a pygeoapi configuration that serves the GeoPackage tables with the SQLiteGPKG provider — see pygeoapi configuration for GeoPackage.

Outputs:

  • spec-directory: Absolute path to the directory containing all generated files.
  • psdata-path: Path to the psdata JSON file.
  • feature-catalogue-json: Path to the collected feature catalogue JSON cache.
  • feature-catalogue-markdown: Path to the feature catalogue Markdown table (blank if no entries were found).
  • feature-catalogue-uml: Path to the feature catalogue PlantUML diagram (blank if no entries were found).
  • xmi-feature-catalogue-json: Path to the XMI feature catalogue JSON cache when generated.
  • xmi-feature-catalogue-markdown: Path to the XMI feature catalogue Markdown table when generated.
  • xmi-feature-catalogue-uml: Path to the XMI feature catalogue PlantUML diagram when generated.
  • feature-catalogue-postgis: Path to the PostGIS DDL script (<slug>.postgis.sql) when postgis-output is set.
  • feature-catalogue-pygeoapi-postgis: Path to the PostGIS pygeoapi configuration (<slug>.pygeoapi-postgis.yml) when pygeoapi-postgis-output is set.
  • feature-catalogue-pygeoapi-gpkg: Path to the GeoPackage pygeoapi configuration (<slug>.pygeoapi-gpkg.yml) when pygeoapi-gpkg-output is set.
  • spec-markdown: Reserved path for the final product specification Markdown (always <spec-directory>/index.md). The file is created by the assemble action.

Assemble specification (arkitektum/ps.editor.actions/assemble@main)

Inputs:

  • psdata-path (required): Path to the psdata JSON file from the prepare step.
  • output-path (required): Target path for the rendered Markdown specification.
  • template-path: Optional override for the Handlebars-style template.
  • feature-catalogue-markdown: Optional path to the feature catalogue Markdown table.
  • feature-catalogue-uml: Optional path to the feature catalogue PlantUML source (embedded when no PNG is provided).
  • feature-catalogue-png: Optional path to the rendered PlantUML PNG diagram. When missing or unavailable, the PlantUML source is embedded instead.
  • xmi-feature-catalogue-markdown: Optional path to the XMI feature catalogue Markdown table.
  • xmi-feature-catalogue-uml: Optional path to the XMI feature catalogue PlantUML source.
  • xmi-feature-catalogue-png: Optional path to the rendered XMI PlantUML PNG diagram.
  • updated: Optional override for the updated metadata field.

Empty headings are removed automatically when their sections end up blank after templating. If you want to keep the headings in place when running the script directly, pass --keep-empty-headings.

Outputs:

  • spec-markdown: Path to the assembled product specification Markdown document (always <spec-directory>/index.md).

Build Pages artefact (arkitektum/ps.editor.actions/publish@main)

This optional action wraps the static-site build and artefact upload steps before actions/deploy-pages. Example:

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - uses: arkitektum/ps.editor.actions/publish@main
        with:
          checkout: false                 # already checked out
          upload-path: site

  deploy:
    runs-on: ubuntu-latest
    needs: build
    environment:
      name: github-pages
      url: ${{ steps.deployment.outputs.page_url }}
    permissions:
      pages: write
      id-token: write
    steps:
      - name: Deploy to GitHub Pages
        id: deployment
        uses: actions/deploy-pages@v4

Inputs:

  • checkout (default false): Set to true if the action should fetch the repository.
  • python-version (default 3.11): Passed to actions/setup-python.
  • requirements: Additional requirements file to install; leave blank (default) to skip. The action installs markdown and PyYAML automatically.
  • extra-packages: Additional pip packages to install.
  • upload-path (default site): Directory uploaded via actions/upload-pages-artifact. The action invokes build_github_pages.py from this repository with <source> --output <upload-path>.
  • working-directory (default .): Directory for installation and build commands.
  • pythonpath: Exported as PYTHONPATH while running the build command.
  • source (default produktspesifikasjon): Root directory that contains the generated Markdown specifications.
  • artifact-name (default github-pages): Name of the uploaded artefact.

Generate schemas (arkitektum/ps.editor.actions/shapechange@main)

Turns a feature catalogue into a GML application schema (XSD) and a JSON Schema by way of ShapeChange. The action runs the two lightweight Python halves; ShapeChange itself is a 112 MB Java distribution and is executed by the calling workflow, exactly the way PlantUML is. See Running ShapeChange for the complete snippet.

Inputs:

  • mode (default generate): generate writes the SCXML model and the ShapeChange configuration; check reads the log ShapeChange produced, fails the job on errors and uploads the artefact.
  • output-directory (default shapechange): Directory holding the model, configuration, log and generated schemas.
  • feature-catalogue-json: Path to a feature catalogue JSON file, e.g. the prepare action's feature-catalogue-json output. Works for every source the prepare action supports (OGC API, GeoPackage, XMI).
  • xmi-model: Path or URL to a SOSI UML XMI feature catalogue, read directly instead of a JSON file.
  • xmi-username / xmi-password (default sosi/sosi): Credentials used to download the XMI catalogue.
  • target-namespace (required in generate mode): Target namespace of the application schema. None of the feature catalogue sources carry one, and ShapeChange selects application schemas by exactly this value.
  • xmlns-prefix (default app): Namespace prefix of the application schema.
  • schema-version (default 1.0): Version of the application schema.
  • schema-name: Name of the application schema package. Defaults to the input file stem.
  • xsd-document: File name of the generated XML Schema document. Defaults to <SchemaName>.xsd.
  • targets (default xsd,json): Which ShapeChange targets to enable.
  • xsd-encoding-rule (default sosi): ShapeChange XML Schema encoding rule. sosi is Kartverket's SOSI profile, written into the generated configuration — see The SOSI profile. Built-in alternatives are iso19136_2007, gml33, iso19139_2007 and ogcSweCommon2.
  • json-schema-version (default 2019-09): One of 2020-12, 2019-09, draft-07, OpenApi30.
  • json-base-uri: Base URI used when constructing $id values in the JSON Schema output.
  • json-encoding-rule (default sosiJson): sosiJson is written into the generated configuration; the built-in alternatives are defaultGeoJson and defaultPlainJson.
  • referenced-schemas: YAML/JSON list (or a file path) of application schemas this model references but does not define, so ShapeChange imports the borrowed types instead of copying them — see Schemas the model borrows from.
  • represent-tagged-values (default SOSI_navn,SOSI_verdi,NVDB_ID): Tagged values emitted as sc:taggedValue appinfo in the XSD. Needs an encoding rule with rule-xsd-all-tagged-values, which sosi has.
  • codelist-as-dictionary (default model): model, true or false. Decides how code lists are encoded — see Code lists.
  • entity-type-name (default @type): Name of the entity type member in the JSON Schema output.
  • xml-schema-target-class / json-schema-target-class: Override the ShapeChange target classes. The Java package names changed in ShapeChange 4.0.0; the defaults target 4.x.
  • bundled-includes (default false): Reference the standard rules and map entries bundled with the ShapeChange distribution instead of the copies on shapechange.net. Requires running the jar from the distribution root.
  • upload-artifact (default true) / artifact-name (default shapechange-schemas): Artefact upload in check mode.
  • python-version (default 3.11): Passed to actions/setup-python.

Outputs:

  • output-directory: Directory holding the model, configuration, log and schemas.
  • scxml-model: Path to the generated ShapeChange SCXML model.
  • shapechange-config: Path to the generated ShapeChange configuration document — pass this to java -jar ShapeChange.jar -c.
  • shapechange-log: Path to the XML log ShapeChange writes.
  • xsd-directory / json-schema-directory: Directories that receive the generated schemas.

Exporting PlantUML to PNG

PNG diagrams are recommended for readability, but the assemble action now falls back to embedding the PlantUML source when no PNG is supplied. Use the workflow snippet above—or any other conversion job—to generate PNGs when you want rendered graphics.

Running ShapeChange

ShapeChange converts the data model into a GML application schema (XSD) and a JSON Schema. Like PlantUML, it is a heavyweight external tool, so the calling workflow runs it:

      - id: schema-model
        uses: arkitektum/ps.editor.actions/shapechange@main
        with:
          feature-catalogue-json: ${{ steps.prepare.outputs.feature-catalogue-json }}
          target-namespace: https://skjema.geonorge.no/SOSI/produktspesifikasjon/DyrkbarJord/1.0
          xmlns-prefix: dyrkbarjord
          schema-name: DyrkbarJord
          json-base-uri: https://skjema.geonorge.no/SOSI/produktspesifikasjon/DyrkbarJord

      - uses: actions/setup-java@v4
        with:
          distribution: temurin
          java-version: "21"

      - name: Cache ShapeChange
        id: shapechange-cache
        uses: actions/cache@v4
        with:
          path: ~/shapechange
          key: shapechange-4.0.0

      - name: Download ShapeChange
        if: steps.shapechange-cache.outputs.cache-hit != 'true'
        run: |
          set -euo pipefail
          mkdir -p ~/shapechange
          curl -sSL -o /tmp/shapechange.zip \
            https://github.com/ShapeChange/ShapeChange/releases/download/4.0.0/ShapeChange-4.0.0.zip
          unzip -q /tmp/shapechange.zip -d ~/shapechange

      - name: Run ShapeChange
        working-directory: ~/shapechange
        run: |
          set -euo pipefail
          java -Dfile.encoding=UTF-8 -jar ShapeChange-4.0.0.jar \
            -c "${{ steps.schema-model.outputs.shapechange-config }}"

      - id: schemas
        uses: arkitektum/ps.editor.actions/shapechange@main
        with:
          mode: check
          output-directory: ${{ steps.schema-model.outputs.output-directory }}

Things worth knowing about this pipeline:

  • ShapeChange is only distributed as a ZIP. There is no fat jar and no Maven Central artefact, and ShapeChange-4.0.0.jar relies on a sibling lib/ directory, so the whole archive has to be unpacked. It requires Java 21.
  • Always pass -c. Without a configuration file ShapeChange opens a Swing dialog, which hangs a CI job.
  • working-directory is the distribution root because ShapeChange resolves relative paths against the working directory. Everything this action writes into the configuration is an absolute path, so only the bundled xi:include references depend on it.
  • The check step is not optional. ShapeChange returns a non-zero exit code only for a fatal abort; ordinary model and target errors are written to the log while the process still exits 0. mode: check parses that log, turns messages into GitHub annotations and fails the job when errors were reported.

Feeding ShapeChange a model

ShapeChange cannot read the SOSI XMI files directly: its XMI10 reader requires XMI 1.0 with a DOCTYPE, while the exports on sosi.geonorge.no are XMI 1.1. The action therefore reads every source through this repository's own loaders and writes a ShapeChange SCXML model, which is the recommended interchange format. The same path serves OGC API, GeoPackage and XMI sources, so feature-catalogue-json is the input to reach for; xmi-model is a shortcut when there is no prepare step.

Because the JSON structure is flat, the model ShapeChange sees is reconstructed:

  • top-level entries become <<featureType>> classes, nested attribute groups become <<dataType>> classes;
  • value domains become <<codeList>> or <<enumeration>> according to the source model's stereotype, falling back on the presence of an external codeList URI for sources that carry no stereotypes;
  • geometry is normalised to the ISO 19107 GM_* types, and other types to ISO 19103 names such as CharacterString and Integer;
  • SOSI_navn, SOSI_verdi and NVDB_ID tagged values are carried through from XMI sources;
  • targetNamespace, xmlns, version, xsdDocument and jsonDocument are synthesised from the action inputs, since no source carries them.

Constraints (OCL) and classes that are not feature types do not survive the trip through the JSON structure. GeoPackage sources are the thinnest input of all — no relationships, no packages, no code lists — so their schema is a flat rendering of the tables.

Schemas the model borrows from

A SOSI model is rarely self-contained. Reguleringsplan-2024.xml defines 35 feature types but references 55 more classes that live elsewhere — Arealplan and Identifikasjon in Planregister, Posisjonskvalitet and Målemetode in the general SOSI packages. Enterprise Architect records these as <EAStub name="Arealplan" UMLType="Class"/>, carrying a name and nothing else: no namespace, no file path.

Left alone, the borrowed types are copied into your schema. The XSD looks fine and ShapeChange reports no warnings, but Identifikasjon ends up as app:IdentifikasjonPropertyType in your namespace instead of being imported from the schema that owns it. For the Reguleringsplan model that affects 43 types.

referenced-schemas tells ShapeChange who owns what:

- name: Planregister
  xmi-model: https://sosi.geonorge.no/svn/SOSI/.../Arealplan%205.0/Planregister.xml
  target-namespace: https://skjema.geonorge.no/SOSI/produktspesifikasjon/Planregister/20260101
  xmlns-prefix: pr
  version: "20260101"
  location: https://skjema.geonorge.no/SOSI/produktspesifikasjon/Planregister/20260101/Planregister.xsd

The referenced model is read only to learn which type names it defines. Those types are then placed in that schema's package, and the result is a proper import:

<import namespace="https://skjema.geonorge.no/SOSI/produktspesifikasjon/Planregister/20260101"
        schemaLocation="https://.../Planregister.xsd"/>
...
<element name="identifikasjon" type="pr:IdentifikasjonPropertyType"/>

Only your own schema is generated — the referenced ones are imported, not rebuilt, because appSchemaNameRegex still pins the run to the main schema.

Things worth knowing:

  • location is what supplies schemaLocation. ShapeChange derives the import itself from the other package's target namespace, but it has no idea where that schema is published. Omit location and you get an import without a location. One entry per schema is enough — not one per type.
  • The declaration cannot be derived from the model. An EA stub has only a name, so the namespace, prefix and published location have to come from you. The file name is not a reliable guide either: Reguleringsplan-2024.xml contains a package called Reguleringsplan-2026-Utkast.
  • A schema whose types are never used is skipped, so an over-broad list costs nothing but a download.
  • Types you do not declare stay local. Declaring Planregister but not the general SOSI packages leaves Posisjonskvalitet in your namespace. Add another entry to move it too.

The SOSI profile

Stock ShapeChange produces a generic ISO 19136 schema. The application schemas published on skjema.geonorge.no look different, and kartverket/ShapeChange-Add-In shows why: Kartverket runs ShapeChange with a SOSI-specific encoding rule and an extra map-entry file. That add-in is a Windows-only Enterprise Architect wrapper, so none of it runs in CI — but the two configuration fragments are plain ShapeChange and are reused here.

xsd-encoding-rule: sosi (the default) writes Kartverket's rule into the generated configuration:

<EncodingRule name="sosi" extends="iso19136_2007">
  <rule name="rule-xsd-prop-length-size-pattern"/>
  <rule name="rule-xsd-all-tagged-values"/>
  <rule name="rule-xsd-all-notEncoded"/>
  <rule name="rule-xsd-pkg-schematron"/>
  <rule name="rule-xsd-prop-nillable"/>
  <rule name="rule-xsd-prop-targetCodeListURI"/>
</EncodingRule>

Every individual rule is stock ShapeChange — only the grouping is Kartverket's — so no patched ShapeChange is needed. Two of them do the visible work:

  • rule-xsd-all-tagged-values, together with represent-tagged-values, emits <sc:taggedValue tag="SOSI_navn">FLATE</sc:taggedValue> appinfo, exactly as in the published Geonorge schemas;
  • rule-xsd-prop-targetCodeListURI emits <sc:targetCodeListURI> carrying each code list's registry URI, so the link to register.geonorge.no survives whichever encoding the code list gets.

The profile is purely additive: running the same model through iso19136_2007 and sosi yields structurally identical schemas, and the sosi output only gains the sc: import and the appinfo blocks.

Alongside it, shapechange/data/StandardMapEntries_sosi.xml maps the Norwegian SOSI type names — Navn, Høyde, Dybde, Organisasjonsnummer, URI, and the geometry types Punkt/Kurve/Flate/Sverm. It is vendored rather than fetched from sosi.geonorge.no, because the upstream copy is served over plain HTTP from a version-pinned path. One deliberate fix: upstream maps Høyde twice, to double and to string; only the double mapping is kept.

Set xsd-encoding-rule: iso19136_2007 to opt out and get plain ShapeChange behaviour.

Code lists

ShapeChange encodes a <<codeList>> in one of two shapes, chosen by its asDictionary tagged value:

asDictionary XSD
true <element name="kommunenummer" type="gml:CodeType"/>
false <union memberTypes="app:KommunenummerEnumerationType app:KommunenummerOtherType"/> with pattern "other: \w{2,}"

The published Geonorge schemas use the union form throughout. codelist-as-dictionary decides which you get: model (default) keeps whatever the source model says, while true/false overrides it. The registry URI is preserved either way through rule-xsd-prop-targetCodeListURI, so the choice is about instance-document form, not about losing information.

An <<enumeration>> is different again — a closed restriction with no escape hatch. That is why the source stereotype is carried through rather than guessed.

JSON Schema and non-ASCII class names

Both built-in JSON encoding rules include rule-json-cls-name-as-anchor, which encodes every class name as a JSON Schema $anchor. JSON Schema requires anchors to match ^[A-Za-z][-A-Za-z0-9.:_]*$, so a Norwegian name such as Målemetode produces a document that fails strict metaschema validation.

ShapeChange encoding rules can only add rules, never remove one, so the fix is a rule that does not extend a built-in. That is what json-encoding-rule: sosiJson (the default) is: Kartverket's three-rule defaultJson plus what is needed for geometry, identity and documentation, and without the anchor rule. The result validates cleanly as Draft 2019-09 while keeping GeoJSON geometry ("geometry": {"$ref": "https://geojson.org/schema/MultiPolygon.json"}).

Set json-encoding-rule: defaultGeoJson for stock behaviour, at the cost of the $anchor problem returning.

Namespace and version conventions

Geonorge date-stamps its application schemas, using the same stamp as both the last path segment of the namespace and the schema version:

target-namespace: https://skjema.geonorge.no/SOSI/produktspesifikasjon/Dyrkbarjord/20250530
schema-version: "20250530"
xmlns-prefix: app

app is the prefix Geonorge uses, and is already the default.

PostGIS schema

With postgis-output: true (or --postgis-output locally) the prepare action also writes <slug>.postgis.sql: a script that creates an empty PostGIS schema for the data model. It is plain SQL — nothing connects to a database — and runs in a single transaction:

psql -v ON_ERROR_STOP=1 -d mydb -f produktspesifikasjon/<slug>/<slug>.postgis.sql

The layout follows the same conventions as the GeoPackage output, so one ldproxy provider structure fits both:

  • every table has an integer identity primary key objid, plus an objtype column holding the class name;
  • abstract classes get no table; their attributes are materialised in every concrete subtype;
  • single-valued data types are flattened into columns (identifikasjon_lokalid);
  • an attribute that may repeat gets a child table <table>_<attribute> whose <table>_fk column references the owner with ON DELETE CASCADE.

Where PostGIS can say more than a GeoPackage, it does:

  • Several geometries stay in one table. Bygning with a surface and a point keeps both as geometry columns; there is no split into Bygning_flate/Bygning_punkt. Each geometry column gets a GIST index.
  • Code lists become lookup tables (identifier, description) filled with the code values and referenced by a foreign key. External code lists are fetched from the Geonorge register; when that fails the column stays text and the register URL goes into the column comment.
  • Enumerations become CHECK constraints, since a closed <<enumeration>> has no register to grow from.
  • Associations follow their multiplicities. A single-valued end becomes a <role>_fk column, many-to-many becomes a join table with its own objid and a unique pair of foreign keys. When only one end's multiplicity is known — sources other than XMI record just the far end — a join table is used, since it fits every case. An association to an abstract class references each concrete subtype.
  • Descriptions become COMMENT ON TABLE/COMMENT ON COLUMN, and every foreign-key column is indexed.
  • External classes (external: true, defined in another model) get no table, and associations to them are left out.

Table and column names are lowercased and transliterated to ASCII: æ→ae, ø→oe, å→aa, and anything else outside [a-z0-9_] becomes _, so Dyrkbar jord.Høyde becomes dyrkbar_jord.hoeyde. Names longer than PostgreSQL's 63-byte limit are shortened with a hash suffix rather than truncated, so they cannot collide. Code values and descriptions keep their original spelling.

From Python, postgis.build_postgis_ddl(feature_types, ...) returns the script and postgis.write_postgis_ddl(feature_types, path, ...) writes it. Both also take default_srid (default 25833, used when a feature type carries no storage CRS), allow_z, owner (the schema's AUTHORIZATION), read_role (granted SELECT on the schema's tables) and model_metadata (see below).

Reading a PostGIS schema back

postgis.load_feature_types_from_postgis(path_or_url, schema=None) reads a .sql script back into feature types, the same structure the XMI, OGC API and GeoPackage loaders return. The script is parsed, not executed, so no database or driver is needed. As a scope source:

scopes:
  - name: database
    generator: postgis
    schema: administrative_enheter   # optional: one schema out of several

Without a source, the script is read from the repository at inputs/<product-slug>/postgis.schema.sql, relative to the repository root (GITHUB_WORKSPACE in Actions, otherwise the working directory), where <product-slug> is the product specification's slug (product-slug/--slug, or derived from the title). The scope's source link then points at the file on GitHub. A missing file is an error. Give a source (a path or http(s) URL) to read a script from somewhere else.

Model metadata. Lowercased names and flattened columns lose information: lokalId becomes lokalid, identifikasjon.lokalId becomes the column identifikasjon_lokalid, and abstract supertypes have no table. So the writer also records the model as a last line in each table and column comment:

COMMENT ON COLUMN "kommune"."identifikasjon_lokalid" IS 'lokal identifikator av et objekt

@ps {"from":"SOSI_Fellesegenskaper","path":[{"name":"identifikasjon","type":"Identifikasjon",...},{"name":"lokalId",...}]}';

The line holds the attribute's own keys (name, type, multiplicity, tagged values, value domain), its path through flattened data types, the class that declared it, and on feature tables the feature type itself plus the types that have no table. A script written with it reads back into exactly the feature catalogue it came from. Comments are part of the schema, so the metadata survives pg_dump --schema-only. Code values that the lookup tables or CHECK constraints already hold are not repeated. Pass model_metadata=False to leave the line out.

Other scripts. Without the @ps lines — pg_dump --schema-only output, full dumps (their COPY data fills the code lists), scripts from the Gistools PostGIS generator — the reader applies the writer's conventions in reverse: identifier/description tables are code lists, a table with one required foreign key that cascades from an owner (or is named <owner>_…) holds a repeating attribute, two foreign keys unique together form a many-to-many association, any other foreign key to a feature table is an association, CHECK (col IN …) is an enumeration, and objtype's default gives the class name. Geometry comes from geometry(Type, srid), AddGeometryColumn, or enforce_srid checks. Names then stay as the database has them, and flattened columns stay flat.

The parser splits statements on ; outside strings, quoted names, comments and dollar-quoted function bodies, and skips statements it does not need (functions, indexes, grants, SET). A recognised statement that does not parse raises an error naming it.

pygeoapi configuration for PostGIS

With pygeoapi-postgis-output: true (or --pygeoapi-postgis-output locally) the prepare action also writes <slug>.pygeoapi-postgis.yml: a pygeoapi configuration that publishes the tables of the PostGIS schema as OGC API - Features collections. It is generated from the same table model as <slug>.postgis.sql, so it is usually written together with postgis-output, and it uses postgis-schema as its search_path (default public).

Every realisable feature type — every concrete class, i.e. every class with a feature table in the PostGIS script — becomes a resources entry with one PostgreSQL provider:

Field Value
resource key the class name, latinised, lowercase kebab-case: AdministrativEnhet → administrativ-enhet, Dyrkbar jord → dyrkbar-jord, ÅpenGrunn → aapen-grunn
title / description the class name (or title) and its description
keywords the class's keywords, else its title
extents.spatial.bbox the dataset's extent from psdata (identificationSection.extent.geographicElement), in CRS84
data.dbname the slug, sanitised like the table names
data.search_path postgis-schema, else public
table / id_field the feature table and its primary key objid
geom_field / storage_crs the class's geometry column and its SRID. With several geometries the one the class declares as its geometry is used — the PostgreSQL provider serves one. A class without geometry gets neither, nor a crs list.
crs CRS84 and the common Norwegian/European CRSs (EPSG 25832/25833/25835, 5972/5973/5975, 3857, 3575, 4326, 3035, 4258, 5942), plus the storage CRS if it is not among them

The service metadata comes from psdata: identification from the title, abstract, keywords, access constraints and metadata link; license from the licence and its URL; provider from the contact with role owner; contact from the contact with role pointOfContact.

Values only whoever runs the service knows — database host, port, user and password, and the provider's and contact's url — are written as "<REPLACE_ME>". So is any psdata value that is missing, rather than a guess. Fill them in before starting pygeoapi; pygeoapi also expands ${ENV_VAR} references, so the password can stay out of the file:

      data:
        host: ${POSTGRES_HOST}
        port: 5432
        dbname: dyrkbar_jord
        user: ${POSTGRES_USER}
        password: ${POSTGRES_PASSWORD}

From Python, pygeoapi_config.build_pygeoapi_postgis_config(feature_types, psdata=..., dbname=..., schema=...) returns the configuration as a dict and pygeoapi_config.write_pygeoapi_postgis_config(feature_types, path, ...) writes it. Pass the same default_srid/allow_z as to the PostGIS writer if you changed them there. The package is called pygeoapi_config so it does not shadow the pygeoapi package itself.

pygeoapi configuration for GeoPackage

With pygeoapi-gpkg-output: true (or --pygeoapi-gpkg-output locally) the prepare action also writes <slug>.pygeoapi-gpkg.yml: the same configuration — server, service metadata, collection titles, descriptions, keywords, extents and CRSs — as for PostGIS, but every collection is served by pygeoapi's SQLiteGPKG provider from the GeoPackage instead of from PostgreSQL:

    providers:
    - type: feature
      name: SQLiteGPKG
      data: ./<slug>.gpkg
      id_field: objid
      table: Bygning
      crs: [...]
      storage_crs: http://www.opengis.net/def/crs/EPSG/0/25833

It describes the GeoPackage that geopackage-output writes, so the two are usually turned on together, and the collections follow that GeoPackage's tables rather than the PostGIS ones:

  • data is the GeoPackage's file name, ./<slug>.gpkg. pygeoapi resolves a relative path from its working directory, not from the configuration file, so adjust it to where the file (with data in it) is deployed.
  • table is the feature type's name as it is — GeoPackage tables are not lowercased or transliterated — and id_field is the objid primary key.
  • A class with several geometries is split into one table per geometry (Bygning_flate, Bygning_punkt), as in the GeoPackage, and each becomes its own collection (bygning-flate, bygning-punkt).
  • There is no geom_field: SQLiteGPKG finds the geometry column in gpkg_geometry_columns itself. It also refuses a table without one, so a feature type without geometry (an aspatial attributes table in the GeoPackage) gets no collection.
  • storage_crs is the table's srs_id.

SQLiteGPKG needs the SpatiaLite extension (mod_spatialite) on the pygeoapi host. It builds its SQL with the table name unquoted, so a table whose name contains a space (Dyrkbar jord) cannot be served — rename the table in the GeoPackage and in the configuration. Norwegian letters are fine.

From Python, pygeoapi_config.build_pygeoapi_gpkg_config(feature_types, psdata=..., data=...) returns the configuration and pygeoapi_config.write_pygeoapi_gpkg_config(feature_types, path, ...) writes it; pass the same default_crs/split_multi_geometry as to write_geopackage if you changed them there. geopackage.writer.realised_feature_tables(feature_types, ...) lists the feature tables write_geopackage creates, without writing anything.

Template

The default template lives at data/template/ps.md.hbs. It expects the following placeholders to be populated by the generator:

  • incl_featuretypes_table: Markdown table generated from the OGC API feature catalogue metadata.
  • incl_featuretypes_uml: Feature catalogue diagram rendered as a PNG when available, otherwise the raw PlantUML source.
  • incl_featuretypes_xmi_table: Markdown table generated from the XMI feature catalogue when provided.
  • incl_featuretypes_xmi_uml: XMI feature catalogue diagram rendered as a PNG when available, otherwise the raw PlantUML source.

During the assemble step, every additional *.md file placed alongside the generated artefacts (the same directory that holds psdata_<slug>.json) is automatically injected into the template. A file named innledning.md, for example, becomes available through the placeholder {{incl_innledning}}. Files that already have a dedicated input—such as index.md or <slug>_feature_catalogue.md—are ignored to avoid conflicts.

You can provide a customised template via the template-path input to tailor the resulting Markdown documentation.

Local development

Prepare artefacts locally:

python scripts/generate_product_spec.py <metadata-id> [<ogc-feature-api>] --output-dir produktspesifikasjon/test --skip-spec-markdown

If you want to filter the feature catalogue, pass --feature-type-filter multiple times or as a comma-separated list (applies to both OGC API and XMI sources):

python scripts/generate_product_spec.py <metadata-id> https://dirmin.no/kart/server/wfs3/collections \
  --feature-type-filter Uttak --feature-type-filter Konsesjon

To split the specification into multiple scopes, pass a YAML/JSON payload (or a path to a YAML/JSON file) via scopes:

scopes:
  - name: datafangst
    source: https://sosi.geonorge.no/svn/SOSI/SOSI Del 3/Kommunal- og moderniseringsdepartementet/Arealplan/Arealplan 5.0/PlanleggingIgangsatt.xml
    generator: xmi
    description: Datamodellen brukes for å legge ved gml filer for planområdet som brukes i tjenesten for varsel om planoppstart.
  - name: innsynstjeneste
    source: https://plandata.ft-test.dibk.no/services/planleggingigangsatt/collections
    generator: ogc_feature_api
    description: Tjeneste for innsyn i planområder som er varslet for planlegging igangsatt.

source is a path (relative to the working directory) or an http(s) URL to the data model; url is still accepted as an older name for it. generator is one of xmi, ogc_feature_api, geopackage and postgis (a .sql script — see Reading a PostGIS schema back).

Each scope writes its catalogue to <spec-directory>/<scope-name>/objektkatalog.md, and the main specification links to them.

If you have a SOSI UML XMI export instead of an OGC API, omit the second positional argument and pass --xmi-model <path-or-url> (optionally override the default sosi/sosi credentials with --xmi-username and --xmi-password). You can also omit the OGC API argument entirely to only fetch psdata (feature catalogue artefacts will still be created but remain empty). The generated files will use the _xmi_feature_catalogue.* suffix to keep them separate from OGC-based artefacts.

Once you have enriched the artefacts (e.g. rendered UML to PNG), assemble the final Markdown:

python scripts/assemble_product_spec.py produktspesifikasjon/test/<slug>/psdata_<slug>.json --output produktspesifikasjon/test/<slug>/index.md --feature-catalogue-markdown produktspesifikasjon/test/<slug>/<slug>_feature_catalogue.md --feature-catalogue-uml produktspesifikasjon/test/<slug>/<slug>_feature_catalogue.puml --feature-catalogue-png produktspesifikasjon/test/<slug>/<slug>_feature_catalogue.puml.png

Adjust paths to match your slug and any generated PNG files. The commands mirror the behaviour in GitHub Actions.

When you also generate an XMI-based catalogue, add the corresponding arguments:

--xmi-feature-catalogue-markdown produktspesifikasjon/test/<slug>/<slug>_xmi_feature_catalogue.md \
--xmi-feature-catalogue-uml produktspesifikasjon/test/<slug>/<slug>_xmi_feature_catalogue.puml

and optionally --xmi-feature-catalogue-png <path> if you render the XMI diagram to PNG.

To produce schemas locally, generate the ShapeChange input, run the jar from an unpacked ShapeChange distribution, then evaluate the log:

python scripts/run_shapechange.py --mode generate \
  --feature-catalogue produktspesifikasjon/test/<slug>/<slug>_feature_catalogue.json \
  --target-namespace https://example.no/minmodell/1.0 \
  --xmlns-prefix mm --schema-name MinModell \
  --output-dir build/shapechange

cd /path/to/ShapeChange-4.0.0
java -Dfile.encoding=UTF-8 -jar ShapeChange-4.0.0.jar -c /abs/path/build/shapechange/shapechange-config.xml
cd -

python scripts/run_shapechange.py --mode check --output-dir build/shapechange

The generated XSD lands in build/shapechange/xsd/ and the JSON Schema in build/shapechange/jsonschema/.

SOSI XMI feature catalogues

Support specification projects from Enterprise Architect XMI exports. You can convert those catalogues to the JSON structure expected by the rest of this repository via the xmi.feature_catalog module (or by passing --xmi-model to scripts/generate_product_spec.py):

import json
from pathlib import Path
from xmi.feature_catalog import load_feature_types_from_xmi

feature_types = load_feature_types_from_xmi(
    "https://sosi.geonorge.no/svn/SOSI/SOSI%20Del%203/Statens%20kartverk/AdministrativeEnheter_FylkerOgKommuner-20240101.xml"
)

Path("feature_catalogue.json").write_text(json.dumps(feature_types, indent=2, ensure_ascii=False))

The helper understands both local files and remote URLs. When downloading from sosi.geonorge.no the default sosi/sosi credentials are supplied automatically, but you can override them via the username and password arguments if needed. When the XMI path is used, generated artefacts follow the <slug>_xmi_feature_catalogue.* naming scheme to make side-by-side comparisons with OGC sources easier.

About

No description, website, or topics provided.

Resources

Stars

1 star

Watchers

1 watching

Forks

Releases

Packages

Contributors

Languages