Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Templates: Ontology-Driven Code Generation

Templates are the bridge between your RDF ontology and generated code. They use SPARQL queries to extract semantic knowledge and Tera templates to render it.

The Template Workflow

RDF Ontology (domain.ttl)
         ↓
SPARQL Queries (extract classes, properties)
         ↓
Template Variables (structured data)
         ↓
Tera Rendering (generate code)
         ↓
Output Files (models.rs, api.ts, etc.)

Key insight: Templates don't just substitute variables—they query your knowledge graph.

Template Anatomy

A ggen template has two parts:

  1. Frontmatter (YAML): Configuration, RDF/SPARQL, output path
  2. Body (Tera template): Code to render

Example: Rust Struct Generator

File: templates/rust/model.tmpl

---
# Output path (Tera variables supported)
to: src/models/{{ class_name | snake_case }}.rs

# Default variables
vars:
  namespace: "http://example.org/"
  generate_serde: true

# RDF ontology sources
rdf:
  - "domain.ttl"                    # Local file
  - "http://schema.org/Person"      # Remote ontology
  inline: |                          # Inline RDF
    @prefix ex: <http://example.org/> .
    ex:TestClass a rdfs:Class .

# SHACL validation (optional)
shape:
  - "shapes/model-constraints.shacl.ttl"

# SPARQL: Extract single values (scalar variables)
sparql:
  vars:
    - name: class_name
      query: |
        PREFIX rdfs: <http://www.w3.org/2000/01/rdf-schema#>
        SELECT ?class_name WHERE {
          ?class a rdfs:Class ;
                 rdfs:label ?class_name .
        } LIMIT 1

    - name: class_comment
      query: |
        PREFIX rdfs: <http://www.w3.org/2000/01/rdf-schema#>
        SELECT ?class_comment WHERE {
          ?class a rdfs:Class ;
                 rdfs:comment ?class_comment .
        } LIMIT 1

# SPARQL: Extract row sets (matrix variables for fan-out)
sparql:
  matrix:
    - name: properties
      query: |
        PREFIX rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#>
        PREFIX rdfs: <http://www.w3.org/2000/01/rdf-schema#>
        SELECT ?prop_name ?prop_type ?is_required WHERE {
          ?prop rdfs:domain ?class ;
                rdfs:label ?prop_name ;
                rdfs:range ?range .
          BIND(STRAFTER(STR(?range), "#") AS ?prop_type)
          OPTIONAL { ?prop ex:required ?is_required }
        }
        ORDER BY ?prop_name

# Deterministic output
determinism:
  seed: "{{ class_name }}-v1"
  sort: "prop_name"
---
use serde::{Deserialize, Serialize};
use uuid::Uuid;

{% if class_comment %}
/// {{ class_comment }}
{% endif %}
#[derive(Debug, Clone{% if generate_serde %}, Serialize, Deserialize{% endif %})]
pub struct {{ class_name | pascal_case }} {
    pub id: Uuid,
{% for prop in properties %}
    {% if prop.is_required == "true" %}
    pub {{ prop.prop_name | snake_case }}: {{ prop.prop_type | rust_type }},
    {% else %}
    pub {{ prop.prop_name | snake_case }}: Option<{{ prop.prop_type | rust_type }}>,
    {% endif %}
{% endfor %}
}

impl {{ class_name | pascal_case }} {
    pub fn new({% for prop in properties %}{{ prop.prop_name | snake_case }}: {{ prop.prop_type | rust_type }}{% if not loop.last %}, {% endif %}{% endfor %}) -> Self {
        Self {
            id: Uuid::new_v4(),
{% for prop in properties %}
            {{ prop.prop_name | snake_case }},
{% endfor %}
        }
    }
}

How It Works

1. Load RDF ontology:

@prefix ex: <http://example.org/> .
@prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#> .

ex:User a rdfs:Class ;
    rdfs:label "User" ;
    rdfs:comment "Application user" .

ex:userName a rdf:Property ;
    rdfs:domain ex:User ;
    rdfs:range xsd:string ;
    ex:required "true" .

2. SPARQL extracts data:

  • class_name = "User"
  • class_comment = "Application user"
  • properties = [{ prop_name: "name", prop_type: "string", is_required: "true" }]

3. Tera renders template:

#![allow(unused)]
fn main() {
/// Application user
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct User {
    pub id: Uuid,
    pub name: String,
}
}

Result: Ontology changes automatically flow to code!

Frontmatter Reference

to: Output Path

# Static path
to: src/main.rs

# Dynamic path (uses template variables)
to: src/models/{{ class_name | snake_case }}.rs

# Multiple files (SPARQL matrix fan-out)
to: src/{{ endpoint_name }}.rs  # One file per row in matrix

vars: Default Variables

vars:
  namespace: "http://example.org/"
  author: "Generated by ggen"
  version: "1.0.0"

  # Can be overridden via CLI
  # ggen gen rust model --vars namespace="http://custom.org/"

rdf: Ontology Sources

rdf:
  # Local files (relative to template or project root)
  - "domain.ttl"
  - "graphs/ontology.ttl"

  # Remote ontologies (HTTP/HTTPS)
  - "http://schema.org/Person"
  - "https://www.w3.org/ns/org#"

  # Inline RDF (for testing or small snippets)
  inline: |
    @prefix ex: <http://example.org/> .
    ex:User a rdfs:Class .

Load order: All sources merged into single RDF graph before SPARQL queries execute.

shape: SHACL Validation

shape:
  - "shapes/user-constraints.shacl.ttl"
  - "shapes/api-spec.shacl.ttl"

Purpose: Validate ontology before generation (catches missing required properties, invalid types, etc.).

Example SHACL shape:

@prefix sh: <http://www.w3.org/ns/shacl#> .
@prefix ex: <http://example.org/> .

ex:UserShape a sh:NodeShape ;
    sh:targetClass ex:User ;
    sh:property [
        sh:path ex:userName ;
        sh:minCount 1 ;        # Required property
        sh:datatype xsd:string ;
    ] .

sparql.vars: Scalar Variables

Extract single values from the ontology:

sparql:
  vars:
    - name: class_name
      query: |
        SELECT ?class_name WHERE {
          ?class a rdfs:Class ;
                 rdfs:label ?class_name .
        } LIMIT 1

    - name: total_properties
      query: |
        SELECT (COUNT(?prop) AS ?total_properties) WHERE {
          ?prop rdfs:domain ?class .
        }

Access in template:

pub struct {{ class_name }} {
    // {{ total_properties }} properties total
}

sparql.matrix: Row Sets (Fan-Out)

Extract multiple rows to generate repeated structures:

sparql:
  matrix:
    - name: properties
      query: |
        SELECT ?name ?type ?required WHERE {
          ?prop rdfs:domain ?class ;
                rdfs:label ?name ;
                rdfs:range ?type .
          OPTIONAL { ?prop ex:required ?required }
        }

Access in template:

{% for prop in properties %}
pub {{ prop.name }}: {{ prop.type }},
{% endfor %}

Fan-out behavior:

  • If to: src/{{ class_name }}.rs, generates one file per row
  • If to: src/models.rs, all rows available in one template

determinism: Reproducible Output

determinism:
  seed: "user-model-v1"      # Seed for random operations
  sort: "property_name"      # Sort matrix rows before rendering

Why? Ensures identical output for identical input (critical for version control).

Built-in Filters

Tera filters transform variables during rendering:

String Filters

{{ class_name | snake_case }}       # User → user
{{ class_name | pascal_case }}      # user → User
{{ class_name | camel_case }}       # user_name → userName
{{ class_name | kebab_case }}       # UserName → user-name
{{ class_name | upper }}            # user → USER
{{ class_name | lower }}            # USER → user
{{ class_name | title }}            # user name → User Name

Type Mapping Filters

{{ xsd_type | rust_type }}          # xsd:string → String
{{ xsd_type | typescript_type }}    # xsd:integer → number
{{ xsd_type | python_type }}        # xsd:decimal → float
{{ xsd_type | graphql_type }}       # xsd:string → String!

Custom filters: Define in ~/.ggen/filters.toml or project .ggen/filters.toml.

Template Discovery

ggen searches for templates in this order:

  1. Marketplace packages: .ggen/packages/<package-id>/templates/
  2. Project templates: templates/<scope>/<action>/
  3. Global templates: ~/.ggen/templates/

Marketplace Templates

# Search marketplace
ggen marketplace search "rust models"

# Install package
ggen marketplace install io.ggen.templates.rust-models

# Use template
ggen template generate-rdf \
  --ontology domain.ttl \
  --template io.ggen.templates.rust-models:model.tmpl

Local Templates

# Create local template
mkdir -p templates/rust/model/
cat > templates/rust/model/struct.tmpl << 'EOF'
---
to: src/{{ class_name }}.rs
rdf: ["domain.ttl"]
sparql:
  vars:
    - name: class_name
      query: "SELECT ?class_name WHERE { ?c rdfs:label ?class_name } LIMIT 1"
---
pub struct {{ class_name }} {}
EOF

# Use local template
ggen gen rust model --ontology domain.ttl

Common Template Patterns

Pattern 1: One Model Per Class

Generate separate files for each class in ontology:

---
to: src/models/{{ class_name | snake_case }}.rs
sparql:
  matrix:
    - name: classes
      query: |
        SELECT ?class_name WHERE {
          ?class a rdfs:Class ;
                 rdfs:label ?class_name .
        }
---
# Template renders once per class_name

Pattern 2: All Models in One File

Generate single file with all classes:

---
to: src/models.rs
sparql:
  matrix:
    - name: classes
      query: |
        SELECT ?class_name ?properties WHERE {
          ?class a rdfs:Class ;
                 rdfs:label ?class_name .
          {
            SELECT ?class (GROUP_CONCAT(?prop; separator=",") AS ?properties) WHERE {
              ?prop rdfs:domain ?class .
            }
            GROUP BY ?class
          }
        }
---
{% for class in classes %}
pub struct {{ class.class_name }} { /* ... */ }
{% endfor %}

Pattern 3: API Endpoint from Ontology

---
to: src/api/{{ endpoint_name | snake_case }}.rs
rdf: ["api-spec.ttl"]
sparql:
  vars:
    - name: endpoint_name
      query: |
        PREFIX hydra: <http://www.w3.org/ns/hydra/core#>
        SELECT ?endpoint_name WHERE {
          ?endpoint a hydra:Operation ;
                    rdfs:label ?endpoint_name .
        } LIMIT 1

  matrix:
    - name: operations
      query: |
        PREFIX hydra: <http://www.w3.org/ns/hydra/core#>
        SELECT ?method ?path WHERE {
          ?op a hydra:Operation ;
              hydra:method ?method ;
              hydra:template ?path .
        }
---
use axum::{Router, routing::{{ operations | map(attribute="method") | lower | join(", ") }}};

pub fn router() -> Router {
    Router::new()
{% for op in operations %}
        .route("{{ op.path }}", {{ op.method | lower }}(handle_{{ op.method | lower }}))
{% endfor %}
}

Pattern 4: GraphQL Schema from Ontology

---
to: schema.graphql
rdf: ["domain.ttl"]
sparql:
  matrix:
    - name: types
      query: |
        SELECT ?type_name ?description WHERE {
          ?type a rdfs:Class ;
                rdfs:label ?type_name ;
                rdfs:comment ?description .
        }

    - name: fields
      query: |
        SELECT ?type_name ?field_name ?field_type WHERE {
          ?field rdfs:domain ?type ;
                 rdfs:label ?field_name ;
                 rdfs:range ?range .
          ?type rdfs:label ?type_name .
          BIND(STRAFTER(STR(?range), "#") AS ?field_type)
        }
---
{% for type in types %}
"""
{{ type.description }}
"""
type {{ type.type_name }} {
{% for field in fields | filter(attribute="type_name", value=type.type_name) %}
  {{ field.field_name }}: {{ field.field_type | graphql_type }}
{% endfor %}
}
{% endfor %}

Testing Templates

Dry Run

Preview output without writing files:

ggen gen rust model --ontology domain.ttl --dry-run

Debug SPARQL

Inspect SPARQL query results:

ggen graph query domain.ttl --sparql "
  SELECT ?class ?prop WHERE {
    ?class a rdfs:Class .
    ?prop rdfs:domain ?class .
  }
"

Validate Template Syntax

ggen template validate templates/rust/model/struct.tmpl

Advanced: Custom SPARQL Functions

Define reusable SPARQL functions in .ggen/sparql-functions.rq:

PREFIX ex: <http://example.org/>
PREFIX fn: <http://ggen.io/functions/>

# Custom function: Get all ancestors of a class
SELECT ?ancestor WHERE {
  ?class rdfs:subClassOf+ ?ancestor .
}

Use in templates:

sparql:
  vars:
    - name: ancestors
      query: |
        PREFIX fn: <http://ggen.io/functions/>
        SELECT ?ancestor WHERE {
          ?class fn:ancestors ?ancestor .
        }

Marketplace Template Development

Create Template Package

# Initialize package
ggen marketplace init my-rust-templates

# Package structure
my-rust-templates/
├── ggen.toml              # Package manifest
├── templates/
│   ├── model.tmpl
│   ├── api.tmpl
│   └── graphql.tmpl
├── examples/
│   └── domain.ttl
└── README.md

Package Manifest (ggen.toml)

[package]
id = "io.example.rust-templates"
name = "Rust Code Templates"
version = "1.0.0"
description = "Generate Rust models, APIs, and GraphQL from RDF"
author = "Your Name <you@example.com>"
license = "MIT"
keywords = ["rust", "rdf", "code-generation"]

[templates]
model = "templates/model.tmpl"
api = "templates/api.tmpl"
graphql = "templates/graphql.tmpl"

[dependencies]
# Other packages this depends on
"io.ggen.filters.rust" = "^1.0"

Publish to Marketplace

# Validate package
ggen marketplace validate

# Test templates
ggen marketplace test

# Publish
ggen marketplace publish

Troubleshooting

"SPARQL query returned no results"

Cause: Query doesn't match ontology structure.

Debug:

# Inspect ontology
ggen graph export domain.ttl --format turtle | less

# Test query manually
ggen graph query domain.ttl --sparql "SELECT ?s ?p ?o WHERE { ?s ?p ?o } LIMIT 10"

"Template variable not found"

Cause: SPARQL vars query returned no binding.

Fix: Add OPTIONAL or provide default:

sparql:
  vars:
    - name: class_comment
      query: |
        SELECT ?class_comment WHERE {
          OPTIONAL { ?class rdfs:comment ?class_comment }
        }
      default: "No description"

"Invalid RDF syntax"

Validate ontology:

ggen graph validate domain.ttl --verbose

Common errors:

  • Missing prefix declaration: @prefix ex: <http://example.org/> .
  • Unclosed strings: rdfs:label "User" (missing closing quote)
  • Invalid URIs: Use angle brackets <http://...>

Next: Explore Marketplace for pre-built templates, or dive into SPARQL Guide for advanced queries.