Table of Contents
- Semantic Projections
Semantic Projections
The Core Concept
Semantic projections are the mechanism by which ggen transforms a single, language-agnostic RDF ontology into multiple language-specific code representations.
RDF Ontology (Semantic Model)
↓
┌───────────────────┼───────────────────┐
↓ ↓ ↓
Rust Structs TypeScript Types Python Classes
The key insight: The domain model (ontology) is separate from its representation (projection).
This separation enables:
- Cross-language consistency: Same business logic across codebases
- Automatic synchronization: Change ontology → regenerate all projections
- Single source of truth: One model, many representations
- Evolution without drift: Update once, deploy everywhere
One Ontology, Many Languages
Traditional approach:
Product.java → Manually kept in sync with
Product.ts → Each requires separate updates
Product.py → Easy to drift out of sync
product.sql → Different conventions, same entity
ggen approach:
# product_catalog.ttl (ONE source of truth)
pc:Product a rdfs:Class ;
rdfs:label "Product" .
pc:name rdfs:domain pc:Product ; rdfs:range xsd:string .
pc:price rdfs:domain pc:Product ; rdfs:range xsd:decimal .
# Generate all projections from one ontology
ggen gen rust/models.tmpl --graph product_catalog.ttl
ggen gen typescript/types.tmpl --graph product_catalog.ttl
ggen gen python/models.tmpl --graph product_catalog.ttl
ggen gen sql/schema.tmpl --graph product_catalog.ttl
Result: Four language-specific implementations, guaranteed to be in sync.
Type Mapping: Semantic to Language-Specific
ggen maps RDF datatypes (XSD schema types) to appropriate language-specific types.
XSD Datatypes to Language Types
| XSD Type | Rust | TypeScript | Python | SQL |
|---|---|---|---|---|
xsd:string | String | string | str | VARCHAR |
xsd:integer | i64 | number | int | INTEGER |
xsd:decimal | f64 | number | float | DECIMAL |
xsd:boolean | bool | boolean | bool | BOOLEAN |
xsd:date | NaiveDate | Date | date | DATE |
xsd:dateTime | DateTime | Date | datetime | TIMESTAMP |
These mappings are configurable via Handlebars helpers in templates.
Example: Product Price Across Languages
Ontology:
pc:price a rdf:Property ;
rdfs:domain pc:Product ;
rdfs:range xsd:decimal ;
rdfs:label "price" .
Projected to:
| Language | Field Declaration |
|---|---|
| Rust | pub price: f64 |
| TypeScript | price: number |
| Python | price: float |
| SQL | price DECIMAL(10, 2) |
| GraphQL | price: Float! |
The ontology never changes. Only the projection templates differ.
Relationship Mapping: Predicates to Methods
RDF relationships (object properties) project to different code patterns depending on the language.
RDF Relationships
# Product belongs to a Category
pc:category a rdf:Property ;
rdfs:domain pc:Product ;
rdfs:range pc:Category ;
rdfs:label "category" .
# Product has a Supplier
pc:supplier a rdf:Property ;
rdfs:domain pc:Product ;
rdfs:range pc:Supplier ;
rdfs:label "supplier" .
Projected to Code
Rust:
#![allow(unused)] fn main() { pub struct Product { pub name: String, pub price: f64, pub category: Category, // Foreign key relationship pub supplier: Supplier, } impl Product { /// Get the category for this product pub fn get_category(&self) -> &Category { &self.category } /// Get the supplier for this product pub fn get_supplier(&self) -> &Supplier { &self.supplier } } }
TypeScript:
interface Product {
name: string;
price: number;
category: Category;
supplier: Supplier;
}
class ProductService {
async getCategory(product: Product): Promise<Category> {
return product.category;
}
async getSupplier(product: Product): Promise<Supplier> {
return product.supplier;
}
}
SQL:
CREATE TABLE products (
id SERIAL PRIMARY KEY,
name VARCHAR(255) NOT NULL,
price DECIMAL(10, 2) NOT NULL,
category_id INTEGER REFERENCES categories(id),
supplier_id INTEGER REFERENCES suppliers(id)
);
Same relationship, different representations. The ontology defines the semantics, templates define the syntax.
Complete Example: Product Catalog Projections
Let's see a full example of one ontology generating code in five languages.
The Ontology (Language-Agnostic)
@prefix pc: <http://example.org/product_catalog#> .
@prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#> .
@prefix xsd: <http://www.w3.org/2001/XMLSchema#> .
# Classes
pc:Product a rdfs:Class ;
rdfs:label "Product" ;
rdfs:comment "A product in the e-commerce catalog" .
pc:Category a rdfs:Class ;
rdfs:label "Category" ;
rdfs:comment "A product category" .
# Data properties (primitives)
pc:name rdfs:domain pc:Product ; rdfs:range xsd:string .
pc:price rdfs:domain pc:Product ; rdfs:range xsd:decimal .
pc:sku rdfs:domain pc:Product ; rdfs:range xsd:string .
# Object properties (relationships)
pc:category rdfs:domain pc:Product ; rdfs:range pc:Category .
Projection 1: Rust Struct
Template: rust/models.tmpl
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Product {
{{#each properties}}
pub {{ name }}: {{ rust_type datatype }},
{{/each}}
}
Generated: src/models/product.rs
#![allow(unused)] fn main() { #[derive(Debug, Clone, Serialize, Deserialize)] pub struct Product { pub name: String, pub price: f64, pub sku: String, pub category: Category, } }
Projection 2: TypeScript Interface
Template: typescript/types.tmpl
export interface Product {
{{#each properties}}
{{ name }}: {{ ts_type datatype }};
{{/each}}
}
Generated: src/types/Product.ts
export interface Product {
name: string;
price: number;
sku: string;
category: Category;
}
Projection 3: Python Dataclass
Template: python/models.tmpl
@dataclass
class Product:
{{#each properties}}
{{ name }}: {{ python_type datatype }}
{{/each}}
Generated: models/product.py
from dataclasses import dataclass
@dataclass
class Product:
name: str
price: float
sku: str
category: Category
Projection 4: SQL Table Schema
Template: sql/schema.tmpl
CREATE TABLE products (
id SERIAL PRIMARY KEY,
{{#each properties}}
{{ name }} {{ sql_type datatype }}{{#if required}} NOT NULL{{/if}},
{{/each}}
);
Generated: migrations/001_create_products.sql
CREATE TABLE products (
id SERIAL PRIMARY KEY,
name VARCHAR(255) NOT NULL,
price DECIMAL(10, 2) NOT NULL,
sku VARCHAR(50) NOT NULL,
category_id INTEGER REFERENCES categories(id)
);
Projection 5: GraphQL Type
Template: graphql/schema.tmpl
type Product {
{{#each properties}}
{{ name }}: {{ graphql_type datatype }}!
{{/each}}
}
Generated: schema/product.graphql
type Product {
name: String!
price: Float!
sku: String!
category: Category!
}
Five languages, one source of truth, complete consistency.
Evolution: Update Once, Regenerate Everywhere
The real power of semantic projections emerges when evolving your domain model.
Step 1: Modify the Ontology
Add a rating field to Product:
# Add to product_catalog.ttl
pc:rating a rdf:Property ;
rdfs:domain pc:Product ;
rdfs:range xsd:decimal ;
rdfs:label "rating" ;
rdfs:comment "Product rating from 0.0 to 5.0" .
Step 2: Regenerate All Projections
# One command per projection
ggen gen rust/models.tmpl --graph product_catalog.ttl
ggen gen typescript/types.tmpl --graph product_catalog.ttl
ggen gen python/models.tmpl --graph product_catalog.ttl
ggen gen sql/schema.tmpl --graph product_catalog.ttl
ggen gen graphql/schema.tmpl --graph product_catalog.ttl
Or batch with a script:
# regenerate-all.sh
for template in templates/*.tmpl; do
ggen gen "$template" --graph product_catalog.ttl
done
The Result
All five languages now have the rating field:
#![allow(unused)] fn main() { // Rust pub struct Product { pub name: String, pub price: f64, pub sku: String, pub rating: f64, // ← NEW pub category: Category, } }
// TypeScript
export interface Product {
name: string;
price: number;
sku: string;
rating: number; // ← NEW
category: Category;
}
# Python
@dataclass
class Product:
name: str
price: float
sku: str
rating: float # ← NEW
category: Category
-- SQL
CREATE TABLE products (
id SERIAL PRIMARY KEY,
name VARCHAR(255) NOT NULL,
price DECIMAL(10, 2) NOT NULL,
sku VARCHAR(50) NOT NULL,
rating DECIMAL(2, 1), -- NEW
category_id INTEGER REFERENCES categories(id)
);
# GraphQL
type Product {
name: String!
price: Float!
sku: String!
rating: Float! # ← NEW
category: Category!
}
No manual editing. No copy-paste. No drift. Guaranteed synchronization.
How Projections Work Internally
Under the hood, ggen performs these steps for each projection:
- Load ontology into Oxigraph RDF store
- Execute SPARQL query defined in template frontmatter
- Extract variables from query results
- Map types using Handlebars helpers (e.g.,
{{ rust_type }}) - Render template with mapped variables
- Write output to specified file path
Example template with SPARQL query:
---
to: src/models/{{ class_name }}.rs
vars:
class_name: Product
sparql: |
PREFIX pc: <http://example.org/product_catalog#>
PREFIX rdfs: <http://www.w3.org/2000/01/rdf-schema#>
SELECT ?property ?datatype ?label WHERE {
?property rdfs:domain pc:Product .
?property rdfs:range ?datatype .
?property rdfs:label ?label .
}
ORDER BY ?label
---
pub struct {{ class_name }} {
{{#each sparql_results}}
pub {{ ?label }}: {{ rust_type ?datatype }},
{{/each}}
}
The SPARQL query extracts data from the ontology. The template renders it as Rust code.
Projection Patterns and Best Practices
Standard Pattern
- Define ontology in language-agnostic RDF
- Create templates for each target language
- Use SPARQL to extract exactly what each template needs
- Map types with Handlebars helpers
- Regenerate whenever ontology changes
Marketplace Pattern
For reusable projections, ggen supports the marketplace gpack pattern:
# Install projection templates from marketplace
ggen add io.ggen.rust.models
ggen add io.ggen.typescript.types
ggen add io.ggen.sql.schema
# Generate from marketplace templates
ggen gen io.ggen.rust.models:models.tmpl --graph product_catalog.ttl
ggen gen io.ggen.typescript.types:types.tmpl --graph product_catalog.ttl
Benefits:
- Pre-built, tested templates
- Consistent code style across projects
- Community-maintained type mappings
- Version-locked for determinism
Best Practices
1. Use semantic types in ontology:
# Good: Semantic precision
pc:createdAt rdfs:range xsd:dateTime .
pc:isActive rdfs:range xsd:boolean .
# Avoid: Generic types lose information
pc:createdAt rdfs:range xsd:string . # ❌ Lost temporal semantics
2. Leverage SPARQL for complex queries:
# Extract only required properties (not optional)
SELECT ?property ?datatype WHERE {
?property rdfs:domain ?class .
?property rdfs:range ?datatype .
FILTER EXISTS { ?shape sh:property [ sh:path ?property ; sh:minCount 1 ] }
}
3. Create custom type mappings:
{{! Custom helper for domain-specific types }}
{{ custom_type datatype }}
{{! Where custom_type might map: }}
{{! xsd:string + pc:UUID → Uuid (Rust) or uuid.UUID (Python) }}
4. Document projection conventions:
# Type Mapping Conventions
| Ontology Type | Rust Type | Notes |
|--------------|----------|-------|
| xsd:decimal + pc:Price | Decimal | Use rust_decimal crate for precision |
| xsd:string + pc:Email | String | Add validation in constructor |
5. Automate regeneration in CI:
# .github/workflows/codegen.yml
- name: Regenerate projections
run: |
./scripts/regenerate-all.sh
git diff --exit-code || echo "::error::Projections out of sync"
This ensures ontology changes are caught before merging.
Semantic projections are the bridge between abstract domain models and concrete implementations. By separating semantics from syntax, ggen enables true cross-language consistency and effortless evolution.