feat(ogar-adapter-ttl): scaffold Turtle (RDF/OWL) adapter — Phase 2a#37
Conversation
New crate `crates/ogar-adapter-ttl` — companion to ogar-adapter-
surrealql, completes the Morris-syntax axis (per ADR-023 and
docs/RDF-OWL-ALIGNMENT.md §2) for OGAR's two canonical wire formats:
| source dialect | adapter | direction |
|----------------------|------------------------|-----------------|
| SurrealQL DDL | ogar-adapter-surrealql | parse + emit |
| Turtle (RDF/OWL) | ogar-adapter-ttl (NEW) | parse + emit |
# Public API
emit_ttl(classes, prefix) -> String (always available)
parse_ttl(ttl) -> Result<Vec<Class>, TtlParseError>
(behind `ttl-parser`)
# Emit (feature-free)
Composes `ogar-emitter::TripleEmitter::emit_class` output and renders
the triples as a canonical Turtle document — `@prefix` block at the
head, subject-grouped triples body, full identity URIs (with RFC 3987-
unsafe characters percent-encoded for the angle-bracket form).
The percent-encoding step is necessary because OGAR's
`association_identity` uses `->` as the class-to-relation separator
(`ogit-erp/Invoice->customer`), and `>` is illegal inside Turtle
angle-bracket IRIs. Encoded as `%3E` on emit; oxttl recovers it
symmetrically on parse.
# Parse (behind `ttl-parser` feature)
Uses `oxttl` (oxigraph project's pure-Rust streaming Turtle parser,
v0.2.3 + oxrdf 0.3 for the typed Term values). The walker:
1. Drives `oxttl::TurtleParser::for_slice` to get the triple stream.
2. Groups by subject in a HashMap.
3. Finds every subject with `?s rdf:type ogar:Class` -> Candidate
Class. Strips the OGAR namespace from predicate URIs to match
against local names (`parentClass`, `hasField`, `hasAssociation`,
`description`, `tableName`, `recordOrder`).
4. For each `ogar:hasField`, recursively lifts the field subject
(must have `rdf:type ogar:Field`) -> Attribute, populating
`fieldName` / `fieldType` (or `rdfs:range` as the OWL-standard
variant).
5. For each `ogar:hasAssociation`, recursively lifts -> Association,
reading `kind`, `relationName`, `targetClass`.
6. Preserves definition order (sort by subject IRI).
# Supported lifts (v1)
- Class { name, parent, description, table_name, record_order }.
- Attribute (rdfs:range typed) via ogar:hasField -> ogar:fieldName /
ogar:fieldType / rdfs:range.
- Association (BelongsTo / HasOne / HasMany / HasAndBelongsToMany)
via ogar:hasAssociation -> ogar:kind / ogar:targetClass /
ogar:relationName.
# Not yet supported (next sprint — Phase 2b or follow-up)
- EnumDecl (owl:oneOf / owl:unionOf lift).
- ActionDef / KausalSpec (lifecycle — TTL doesn't carry state
machines natively; OGAR-extension predicates only).
- Alignment-axiom parsing (owl:equivalentClass /
owl:equivalentProperty) — Phase 4 ogar-pattern shape.
- Full vocab/ogar.ttl round-trip.
# Tests (9 — 5 emit always, 4 parse + round-trip behind feature)
emit_empty_classes_produces_just_prefix_block
emit_minimal_class_includes_rdf_type_ogar_class
emit_class_with_parent_emits_parent_class_predicate
emit_class_with_attribute_emits_field_triples
parse_minimal_class_lifts_one_class [feature-on]
parse_returns_unimplemented_or_error_on_invalid_ttl [feature-on]
round_trip_minimal_class_preserves_name [feature-on]
round_trip_class_with_parent_preserves_inheritance [feature-on]
round_trip_class_with_belongs_to_preserves_association
[feature-on]
The third round-trip is the load-bearing one: it exercises the
class -> field -> association path and the percent-encoding fix
for `->` in URIs.
# Workspace + CI
- `Cargo.toml`: added `crates/ogar-adapter-ttl` to workspace members.
- `.github/workflows/ci.yml`: added
`cargo test -p ogar-adapter-ttl --features ttl-parser` —
same crate-scoped pattern as #29's `surrealdb-parser` step and
#33's `surrealql-hint` step. Feature is opt-in; default workspace
build stays light.
# Verification
cargo test --workspace -> clean
cargo test -p ogar-adapter-ttl -> 5/5
cargo test -p ogar-adapter-ttl --features ttl-parser -> 9/9
cargo test -p ogar-adapter-surrealql --features surrealdb-parser
-> 33/33
cargo test -p ogar-knowable-from --features surrealql-hint -> 10/10
cargo check --workspace --all-targets -> clean
PII abort-guard (word-boundary): CLEAN on all touched files.
# Position in sequencing
Per `docs/RDF-OWL-ALIGNMENT.md §10`:
Phase 1 (#30): RDF-OWL-ALIGNMENT doc MERGED
Phase 2a (this): ogar-adapter-ttl scaffold OPENS
Phase 2b: ogar-from-ecto + ogar-adapter-clickhouse-ddl QUEUED
Phase 3: vart-backend QUEUED
Phase 4: ogar-pattern QUEUED
...
https://claude.ai/code/session_01PBTGaPCSnnt6u3pjXpbLwY
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 04493de47e
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| match ogar_local(p) { | ||
| Some("parentClass") => class.parent = Some(local_name(o)), | ||
| Some("description") => class.description = Some(o.clone()), | ||
| Some("tableName") => class.table_name = Some(o.clone()), | ||
| Some("recordOrder") => class.record_order = Some(o.clone()), |
There was a problem hiding this comment.
Preserve sourceLanguage when lifting classes
For any emitted non-default language such as Python or SurrealQl, TripleEmitter always writes ogar:sourceLanguage, but this match never handles it; Class::new leaves language at its default (Ruby), so parse_ttl(emit_ttl(...)) silently changes non-Ruby classes to Ruby despite the documented supported lift.
Useful? React with 👍 / 👎.
|
|
||
| // Preserve definition order by subject string (subject IRIs are | ||
| // emitted in definition order from `ogar-emitter`). | ||
| classes.sort_by(|a, b| a.0.cmp(&b.0)); |
There was a problem hiding this comment.
Preserve input class order instead of sorting subjects
When parsing TTL emitted for classes in a meaningful order such as [B, A], this sort alphabetizes by subject IRI after the HashMap has already discarded stream order, so parse_ttl returns [A, B]. The adapter promises to preserve definition order, and reordering top-level Vec<Class> entries can create unstable downstream projections and diffs for callers that keep source declaration order.
Useful? React with 👍 / 👎.
Summary
New crate
crates/ogar-adapter-ttl— companion toogar-adapter-surrealql. Completes the Morris-syntax axis (per ADR-023 +docs/RDF-OWL-ALIGNMENT.md §2) for OGAR's two canonical wire formats:ogar-adapter-surrealqlogar-adapter-ttl(this PR)This is Phase 2a of the brutal-upgrade sequencing in
RDF-OWL-ALIGNMENT.md §10.Public API
Emit (feature-free)
Composes
ogar-emitter::TripleEmitter::emit_classoutput → canonical Turtle:@prefixblock + subject-grouped triples + full identity URIs.Includes percent-encoding for the angle-bracket IRI form (RFC 3987) — necessary because OGAR's
association_identityuses->as the class-to-relation separator (ogit-erp/Invoice->customer), and>is illegal inside Turtle IRIs. Encoded as%3Eon emit;oxttlrecovers it symmetrically on parse.Parse (behind
ttl-parserfeature)Uses
oxttlv0.2.3 +oxrdfv0.3 (oxigraph project, pure Rust, RFC-compliant). The walker:oxttl::TurtleParser::for_slice→ triple stream?s rdf:type ogar:Class→ candidate ClassparentClass,hasField,hasAssociation, …)ogar:Field) →Attributeand associations (ogar:Association) →AssociationSupported lifts (v1)
Class { name, parent, description, table_name, record_order }Attribute(rdfs:range typed)ogar:hasField→ogar:fieldName/ogar:fieldType/rdfs:rangeAssociation(BelongsTo / HasOne / HasMany / HasAndBelongsToMany)ogar:hasAssociation→ogar:kind/ogar:targetClass/ogar:relationNameNot yet supported (next sprint)
EnumDecl(lift fromowl:oneOf/owl:unionOf)ActionDef/KausalSpec(lifecycle — TTL doesn't carry state machines natively; OGAR-extension predicates only)owl:equivalentClass/owl:equivalentProperty) — Phase 4ogar-patternshapevocab/ogar.ttlround-tripTests (9 — 5 emit always, 4 parse + round-trip behind feature)
emit_empty_classes_produces_just_prefix_blockemit_minimal_class_includes_rdf_type_ogar_classrdf:type ogar:Classemit_class_with_parent_emits_parent_class_predicateogar:parentClasspredicate firesemit_class_with_attribute_emits_field_triplesogar:hasField+ogar:fieldNamefireparse_minimal_class_lifts_one_classttl-parser?s rdf:type ogar:Classparse_returns_unimplemented_or_error_on_invalid_ttlttl-parserround_trip_minimal_class_preserves_namettl-parserround_trip_class_with_parent_preserves_inheritancettl-parserround_trip_class_with_belongs_to_preserves_associationttl-parser->in URIsWorkspace + CI
Cargo.toml: addedcrates/ogar-adapter-ttlto workspace members.github/workflows/ci.yml: addedcargo test -p ogar-adapter-ttl --features ttl-parser— same crate-scoped pattern as floor: fix non-exhaustive errors in surrealql adapter + add compile CI #29'ssurrealdb-parserstep and feat(knowable-from): wire schema_ddl_hint via surrealql-hint feature + ADR-023 #33'ssurrealql-hintstepVerification
PII abort-guard (word-boundary): CLEAN on all touched files.
Note on the queue
PR #34 (
SPLAT-NATIVE-CUSTOMER.md) + PR #35 (its Codex follow-up — IVD-MDR → MDR Annex VIII) are the other session's work — left to them per your previous instruction. This PR is OGAR session's own.https://claude.ai/code/session_01PBTGaPCSnnt6u3pjXpbLwY