lutaml-model 0.8.4 → 0.8.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (94) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/dependent-tests.yml +5 -0
  3. data/.rubocop.yml +18 -0
  4. data/.rubocop_todo.yml +91 -22
  5. data/Gemfile +2 -0
  6. data/README.adoc +114 -2
  7. data/docs/_guides/index.adoc +18 -0
  8. data/docs/_guides/jsonld-serialization.adoc +217 -0
  9. data/docs/_guides/rdf-serialization.adoc +344 -0
  10. data/docs/_guides/turtle-serialization.adoc +224 -0
  11. data/docs/_migrations/0-8-0-namespace-restructuring.adoc +90 -0
  12. data/docs/_pages/serialization_adapters.adoc +31 -0
  13. data/docs/_references/index.adoc +1 -0
  14. data/docs/_references/rdf-namespaces.adoc +243 -0
  15. data/docs/index.adoc +3 -2
  16. data/lib/lutaml/jsonld/adapter.rb +23 -0
  17. data/lib/lutaml/jsonld/context.rb +69 -0
  18. data/lib/lutaml/jsonld/term_definition.rb +39 -0
  19. data/lib/lutaml/jsonld/transform.rb +174 -0
  20. data/lib/lutaml/jsonld.rb +23 -0
  21. data/lib/lutaml/model/format_registry.rb +10 -1
  22. data/lib/lutaml/model/serialize/format_conversion.rb +17 -1
  23. data/lib/lutaml/model/version.rb +1 -1
  24. data/lib/lutaml/model.rb +6 -0
  25. data/lib/lutaml/rdf/error.rb +7 -0
  26. data/lib/lutaml/rdf/iri.rb +44 -0
  27. data/lib/lutaml/rdf/language_tagged.rb +11 -0
  28. data/lib/lutaml/rdf/literal.rb +62 -0
  29. data/lib/lutaml/rdf/mapping.rb +71 -0
  30. data/lib/lutaml/rdf/mapping_rule.rb +35 -0
  31. data/lib/lutaml/rdf/member_rule.rb +13 -0
  32. data/lib/lutaml/rdf/namespace.rb +58 -0
  33. data/lib/lutaml/rdf/namespace_set.rb +69 -0
  34. data/lib/lutaml/rdf/namespaces/dcterms_namespace.rb +12 -0
  35. data/lib/lutaml/rdf/namespaces/owl_namespace.rb +12 -0
  36. data/lib/lutaml/rdf/namespaces/rdf_namespace.rb +14 -0
  37. data/lib/lutaml/rdf/namespaces/rdfs_namespace.rb +12 -0
  38. data/lib/lutaml/rdf/namespaces/skos_namespace.rb +12 -0
  39. data/lib/lutaml/rdf/namespaces/xsd_namespace.rb +12 -0
  40. data/lib/lutaml/rdf/namespaces.rb +14 -0
  41. data/lib/lutaml/rdf/transform.rb +36 -0
  42. data/lib/lutaml/rdf.rb +19 -0
  43. data/lib/lutaml/turtle/adapter.rb +35 -0
  44. data/lib/lutaml/turtle/mapping.rb +7 -0
  45. data/lib/lutaml/turtle/transform.rb +158 -0
  46. data/lib/lutaml/turtle.rb +22 -0
  47. data/lib/lutaml/xml/adapter/adapter_helpers.rb +1 -42
  48. data/lib/lutaml/xml/adapter/base_adapter.rb +48 -458
  49. data/lib/lutaml/xml/adapter/namespace_data.rb +0 -17
  50. data/lib/lutaml/xml/adapter/namespace_uri_collector.rb +71 -0
  51. data/lib/lutaml/xml/adapter/nokogiri_adapter.rb +5 -1110
  52. data/lib/lutaml/xml/adapter/oga_adapter.rb +6 -846
  53. data/lib/lutaml/xml/adapter/ox_adapter.rb +7 -884
  54. data/lib/lutaml/xml/adapter/plan_based_builder.rb +929 -0
  55. data/lib/lutaml/xml/adapter/rexml_adapter.rb +10 -864
  56. data/lib/lutaml/xml/adapter/xml_parser.rb +86 -0
  57. data/lib/lutaml/xml/adapter/xml_serializer.rb +291 -0
  58. data/lib/lutaml/xml/adapter.rb +0 -1
  59. data/lib/lutaml/xml/adapter_element.rb +7 -1
  60. data/lib/lutaml/xml/builder/base.rb +0 -1
  61. data/lib/lutaml/xml/data_model.rb +9 -1
  62. data/lib/lutaml/xml/document.rb +3 -1
  63. data/lib/lutaml/xml/element.rb +13 -10
  64. data/lib/lutaml/xml/serialization/format_conversion.rb +19 -42
  65. data/lib/lutaml/xml/serialization/instance_methods.rb +26 -35
  66. data/lib/lutaml/xml/transformation/custom_method_wrapper.rb +34 -55
  67. data/lib/lutaml/xml/transformation/rule_applier.rb +1 -1
  68. data/lib/lutaml/xml/xml_element.rb +24 -20
  69. data/spec/lutaml/integration/edge_cases_spec.rb +109 -0
  70. data/spec/lutaml/integration/multi_format_spec.rb +106 -0
  71. data/spec/lutaml/integration/round_trip_spec.rb +170 -0
  72. data/spec/lutaml/jsonld/adapter_spec.rb +46 -0
  73. data/spec/lutaml/jsonld/context_spec.rb +114 -0
  74. data/spec/lutaml/jsonld/term_definition_spec.rb +55 -0
  75. data/spec/lutaml/jsonld/transform_spec.rb +211 -0
  76. data/spec/lutaml/rdf/graph_serialization_spec.rb +137 -0
  77. data/spec/lutaml/rdf/iri_spec.rb +73 -0
  78. data/spec/lutaml/rdf/literal_spec.rb +98 -0
  79. data/spec/lutaml/rdf/mapping_spec.rb +164 -0
  80. data/spec/lutaml/rdf/member_rule_spec.rb +17 -0
  81. data/spec/lutaml/rdf/namespace_set_spec.rb +115 -0
  82. data/spec/lutaml/rdf/namespace_spec.rb +241 -0
  83. data/spec/lutaml/rdf/rdf_transform_spec.rb +82 -0
  84. data/spec/lutaml/turtle/adapter_spec.rb +47 -0
  85. data/spec/lutaml/turtle/mapping_spec.rb +123 -0
  86. data/spec/lutaml/turtle/transform_spec.rb +273 -0
  87. data/spec/lutaml/xml/adapter/base_adapter_regression_spec.rb +151 -0
  88. data/spec/lutaml/xml/adapter/order_spec.rb +150 -0
  89. data/spec/lutaml/xml/clear_parse_state_spec.rb +139 -0
  90. data/spec/lutaml/xml/doubly_defined_namespace_spec.rb +0 -2
  91. data/spec/lutaml/xml/schema/compiler_spec.rb +75 -69
  92. data/spec/lutaml/xml/transformation/custom_method_wrapper_spec.rb +213 -14
  93. metadata +58 -3
  94. data/lib/lutaml/xml/adapter/xml_serialization.rb +0 -145
@@ -0,0 +1,217 @@
1
+ ---
2
+ title: JSON-LD Serialization
3
+ nav_order: 15
4
+ ---
5
+
6
+ = JSON-LD Serialization
7
+
8
+ Lutaml::Model supports serialization to and from JSON-LD (W3C JSON-LD 1.1) using
9
+ SKOS, Dublin Core Terms, and other W3C vocabularies.
10
+
11
+ == Setup
12
+
13
+ JSON-LD is built on the key-value serialization pipeline and requires no
14
+ additional gems beyond what JSON already uses. The `json-ld` gem may be added
15
+ for advanced JSON-LD Processing (expansion, compaction, flattening) in the
16
+ future.
17
+
18
+ JSON-LD is a **key-value** format adapter that extends the built-in key-value
19
+ serialization with JSON-LD-specific constructs: `@context`, `@type`, and
20
+ `@id`.
21
+
22
+ == Mapping DSL
23
+
24
+ Use the `jsonld do` block in your model to define JSON-LD mappings:
25
+
26
+ [source,ruby]
27
+ ----
28
+ class Concept < Lutaml::Model::Serializable
29
+ attribute :name, :string
30
+ attribute :description, :string
31
+
32
+ jsonld do
33
+ context do
34
+ prefix Lutaml::Rdf::Namespaces::SkosNamespace
35
+ vocab "http://example.org/ns/"
36
+
37
+ term "name", id: "http://example.org/name"
38
+ term "description", id: "http://example.org/description"
39
+ end
40
+
41
+ type "skos:Concept"
42
+ id { |m| "http://example.org/concept/#{m.name}" }
43
+
44
+ map "name", to: :name
45
+ map "description", to: :description
46
+ end
47
+ end
48
+ ----
49
+
50
+ === Context
51
+
52
+ The `context` block builds the `@context` object in the JSON-LD output:
53
+
54
+ * `prefix(NamespaceClass)` — registers an RDF namespace as a prefix
55
+ * `vocab(uri)` — sets `@vocab` for unprefixed terms
56
+ * `language(code)` — sets default `@language`
57
+ * `base(uri)` — sets `@base` URI
58
+ * `term(name, ...)` — defines a term with optional `id:`, `type:`,
59
+ `container:`, `language:`, `reverse:`
60
+
61
+ Context resolution:
62
+
63
+ * Compact IRIs (e.g., `"skos:Concept"`) are expanded to full URIs via prefix
64
+ * Terms are resolved via the term definitions
65
+ * Unprefixed names are resolved via `@vocab`
66
+
67
+ === Type and ID
68
+
69
+ * `type "skos:Concept"` — sets `@type` in the output (resolved to full IRI via
70
+ context)
71
+ * `id { |model| ... }` — generates `@id` from the model instance
72
+
73
+ Both are optional. Omit `type` or `id` if your JSON-LD document does not
74
+ require them.
75
+
76
+ === map
77
+
78
+ `map` entries work identically to key-value serialization, defining the JSON
79
+ properties serialized from model attributes.
80
+
81
+ == Serialization
82
+
83
+ [source,ruby]
84
+ ----
85
+ concept = Concept.new(name: "test", description: "A test concept")
86
+ jsonld = concept.to_jsonld
87
+ ----
88
+
89
+ Produces:
90
+
91
+ [source,json]
92
+ ----
93
+ {
94
+ "@context": {
95
+ "skos": "http://www.w3.org/2004/02/skos/core#",
96
+ "@vocab": "http://example.org/ns/",
97
+ "name": "http://example.org/name",
98
+ "description": "http://example.org/description"
99
+ },
100
+ "@type": "http://www.w3.org/2004/02/skos/core#Concept",
101
+ "@id": "http://example.org/concept/test",
102
+ "name": "test",
103
+ "description": "A test concept"
104
+ }
105
+ ----
106
+
107
+ Nil attribute values are omitted from the output.
108
+
109
+ == Deserialization
110
+
111
+ [source,ruby]
112
+ ----
113
+ concept = Concept.from_jsonld(jsonld_string)
114
+ puts concept.name # => "test"
115
+ ----
116
+
117
+ The `from_jsonld` method:
118
+
119
+ . Parses the JSON-LD string into a hash via `JSON.parse`
120
+ . Strips all `@`-prefixed keywords (`@context`, `@type`, `@id`, `@graph`,
121
+ etc.) before attribute mapping
122
+ . Delegates attribute mapping to the key-value transform pipeline
123
+
124
+ This prevents JSON-LD keywords from colliding with model attributes named
125
+ `type`, `id`, `context`, etc.
126
+
127
+ == Round-trip
128
+
129
+ Model data round-trips through JSON-LD serialization:
130
+
131
+ [source,ruby]
132
+ ----
133
+ restored = Concept.from_jsonld(concept.to_jsonld)
134
+ restored.name == concept.name # => true
135
+ ----
136
+
137
+ The `@context` structure is preserved across round-trips because it is defined
138
+ in the mapping, not derived from the input.
139
+
140
+ == Unified `rdf` DSL
141
+
142
+ If you need both JSON-LD and Turtle output from the same model, use the
143
+ unified `rdf` DSL instead of separate `jsonld` and `turtle` blocks. This
144
+ defines the mapping once and auto-generates `@context` from predicates:
145
+
146
+ [source,ruby]
147
+ ----
148
+ class Concept < Lutaml::Model::Serializable
149
+ attribute :name, :string
150
+ attribute :code, :string
151
+
152
+ rdf do
153
+ namespace Lutaml::Rdf::Namespaces::SkosNamespace
154
+ subject { |m| "http://example.org/#{m.code}" }
155
+ type "skos:Concept"
156
+ predicate :prefLabel, namespace: SkosNamespace, to: :name
157
+ predicate :notation, namespace: SkosNamespace, to: :code
158
+ end
159
+ end
160
+ ----
161
+
162
+ See link:../_guides/rdf-serialization.adoc[Unified RDF Serialization] for the
163
+ complete guide including graph-level serialization with `members`.
164
+
165
+ == Multi-format models
166
+
167
+ A single model can define `json`, `jsonld`, `turtle`, and `rdf` mappings
168
+ simultaneously. Each format operates independently:
169
+
170
+ [source,ruby]
171
+ ----
172
+ class Concept < Lutaml::Model::Serializable
173
+ attribute :name, :string
174
+ attribute :code, :string
175
+
176
+ json do
177
+ map "name", to: :name
178
+ map "code", to: :code
179
+ end
180
+
181
+ rdf do
182
+ namespace Lutaml::Rdf::Namespaces::SkosNamespace
183
+ subject { |m| "http://example.org/#{m.code}" }
184
+ type "skos:Concept"
185
+ predicate :prefLabel, namespace: SkosNamespace, to: :name
186
+ predicate :notation, namespace: SkosNamespace, to: :code
187
+ end
188
+ end
189
+ ----
190
+
191
+ * `to_json` produces plain JSON without `@context`
192
+ * `to_jsonld` produces JSON-LD with auto-generated `@context`, `@type`, `@id`
193
+ * `to_turtle` produces Turtle with `@prefix` declarations
194
+
195
+ The unified `rdf` block creates mappings for both `:turtle` and `:jsonld`
196
+ formats. If separate `jsonld do` or `turtle do` blocks are also defined, they
197
+ take precedence over the `rdf` block for their respective format.
198
+
199
+ == Error Handling
200
+
201
+ * `JSON::ParserError` — raised for malformed JSON input
202
+ * All parsing errors are wrapped in `Lutaml::Model::InvalidFormatError` by the
203
+ format pipeline
204
+
205
+ == Architecture
206
+
207
+ The JSON-LD format is composed of:
208
+
209
+ * `Lutaml::JsonLd::Adapter` — extends `KeyValue::Document`; parses JSON-LD
210
+ strings to hashes and serializes hashes back to JSON
211
+ * `Lutaml::JsonLd::Context` — DSL for building `@context` with prefixes,
212
+ vocab, terms, language, and base
213
+ * `Lutaml::JsonLd::TermDefinition` — value object for term definitions with
214
+ `@id`, `@type`, `@container`, `@language`, `@reverse`
215
+ * `Lutaml::JsonLd::Transform` — inherits from `Rdf::Transform`; auto-generates
216
+ `@context` from predicates, injects `@type`/`@id` on export, strips
217
+ `@`-keywords on import
@@ -0,0 +1,344 @@
1
+ ---
2
+ title: Unified RDF Serialization
3
+ nav_order: 17
4
+ ---
5
+
6
+ = Unified RDF Serialization
7
+
8
+ Lutaml::Model provides a unified `rdf` DSL that defines RDF mappings once for
9
+ both JSON-LD and Turtle serialization. This follows the same principle as
10
+ `key_value do ... end` which serves JSON, YAML, and TOML from a single block.
11
+
12
+ Both JSON-LD and Turtle are RDF serialization formats representing the same
13
+ subject–predicate–object triples. The `rdf` DSL lets you define the mapping
14
+ once, and the format adapters handle syntax differences automatically.
15
+
16
+ == Setup
17
+
18
+ Add the `rdf-turtle` gem to your Gemfile (required for Turtle output):
19
+
20
+ [source,ruby]
21
+ ----
22
+ gem "rdf-turtle", "~> 3.3"
23
+ ----
24
+
25
+ No additional gem is needed for JSON-LD output.
26
+
27
+ == When to Use `rdf` vs Separate `jsonld`/`turtle` Blocks
28
+
29
+ Use `rdf do ... end` when:
30
+ * Your model maps to RDF resources using standard vocabularies (SKOS, DC, etc.)
31
+ * You need both JSON-LD and Turtle output from the same model
32
+ * You want predicate-based mapping with automatic `@context` generation
33
+
34
+ Use `jsonld do ... end` when:
35
+ * You need full control over the JSON-LD `@context` structure
36
+ * You are mapping to JSON properties that don't map to RDF predicates
37
+
38
+ Use `turtle do ... end` when:
39
+ * You only need Turtle output
40
+ * You need Turtle-specific features not available in the unified DSL
41
+
42
+ Both the unified `rdf` block and the format-specific blocks can coexist on the
43
+ same model. If both are defined, format-specific blocks take precedence.
44
+
45
+ == The `rdf` DSL
46
+
47
+ === Basic Model
48
+
49
+ [source,ruby]
50
+ ----
51
+ class Concept < Lutaml::Model::Serializable
52
+ attribute :code, :string
53
+ attribute :name, :string
54
+
55
+ rdf do
56
+ namespace SkosNamespace
57
+
58
+ subject { |m| "http://example.org/concept/#{m.code}" }
59
+ type "skos:Concept"
60
+
61
+ predicate :notation, namespace: SkosNamespace, to: :code
62
+ predicate :prefLabel, namespace: SkosNamespace, to: :name
63
+ end
64
+ end
65
+ ----
66
+
67
+ This single `rdf` block creates mappings for both `:turtle` and `:jsonld`
68
+ formats automatically.
69
+
70
+ === DSL Methods
71
+
72
+ ==== namespace
73
+
74
+ Declares the RDF namespaces used by predicates. Accepts one or more
75
+ `Lutaml::Rdf::Namespace` subclass references:
76
+
77
+ [source,ruby]
78
+ ----
79
+ namespace SkosNamespace, DctermsNamespace
80
+ ----
81
+
82
+ In Turtle output, each namespace becomes an `@prefix` declaration. In JSON-LD
83
+ output, each becomes a prefix entry in `@context`.
84
+
85
+ ==== subject
86
+
87
+ Required for top-level resources. A block that generates the subject URI from
88
+ the model instance:
89
+
90
+ [source,ruby]
91
+ ----
92
+ subject { |m| "http://example.org/concept/#{m.code}" }
93
+ ----
94
+
95
+ ==== type
96
+
97
+ Sets the RDF type (`rdf:type`). Compact IRIs are resolved via declared
98
+ namespaces:
99
+
100
+ [source,ruby]
101
+ ----
102
+ type "skos:Concept"
103
+ # resolves to <http://www.w3.org/2004/02/skos/core#Concept>
104
+ ----
105
+
106
+ ==== predicate
107
+
108
+ Each `predicate` creates a mapping between an RDF predicate and a model
109
+ attribute:
110
+
111
+ [source,ruby]
112
+ ----
113
+ predicate :prefLabel, namespace: SkosNamespace, to: :name, lang_tagged: true
114
+ ----
115
+
116
+ Parameters:
117
+
118
+ * `name` — the local name in the namespace (e.g., `:prefLabel`)
119
+ * `namespace:` — the `Lutaml::Rdf::Namespace` subclass (required)
120
+ * `to:` — the model attribute to read (required)
121
+ * `lang_tagged:` (default: `false`) — if true, values are serialized with
122
+ language tags (see <<language-tagged-values>>)
123
+
124
+ ==== members
125
+
126
+ Declares that a container model contains member resources that should be
127
+ serialized as separate subjects in the output graph (see <<graph-serialization>>).
128
+
129
+ == Serialization
130
+
131
+ === Turtle
132
+
133
+ [source,ruby]
134
+ ----
135
+ concept = Concept.new(code: "2119", name: "component")
136
+ turtle = concept.to_turtle
137
+ ----
138
+
139
+ Produces:
140
+
141
+ [source,turtle]
142
+ ----
143
+ @prefix skos: <http://www.w3.org/2004/02/skos/core#> .
144
+
145
+ <http://example.org/concept/2119> a skos:Concept;
146
+ skos:notation "2119";
147
+ skos:prefLabel "component" .
148
+ ----
149
+
150
+ === JSON-LD
151
+
152
+ [source,ruby]
153
+ ----
154
+ jsonld = concept.to_jsonld
155
+ ----
156
+
157
+ Produces:
158
+
159
+ [source,json]
160
+ ----
161
+ {
162
+ "@context": {
163
+ "skos": "http://www.w3.org/2004/02/skos/core#",
164
+ "notation": "skos:notation",
165
+ "prefLabel": "skos:prefLabel"
166
+ },
167
+ "@type": "skos:Concept",
168
+ "@id": "http://example.org/concept/2119",
169
+ "notation": "2119",
170
+ "prefLabel": "component"
171
+ }
172
+ ----
173
+
174
+ The `@context` is auto-generated from the declared namespaces and predicates.
175
+ No manual context definition is needed.
176
+
177
+ == [[language-tagged-values]]Language-Tagged Values
178
+
179
+ When a predicate is declared with `lang_tagged: true`, values are serialized
180
+ with language information:
181
+
182
+ **Turtle:**
183
+ ```turtle
184
+ skos:prefLabel "component"@eng ;
185
+ skos:prefLabel "composant"@fra ;
186
+ ```
187
+
188
+ **JSON-LD:**
189
+ ```json
190
+ "prefLabel": { "eng": "component", "fra": "composant" }
191
+ ```
192
+
193
+ In JSON-LD, the `@context` auto-generates a language container:
194
+ ```json
195
+ "prefLabel": { "@id": "skos:prefLabel", "@container": "@language" }
196
+ ```
197
+
198
+ For this to work, the attribute's values must include the
199
+ `Lutaml::Rdf::LanguageTagged` module (or be a `Lutaml::Rdf::Literal`). A simple
200
+ value object is sufficient:
201
+
202
+ [source,ruby]
203
+ ----
204
+ class LocalizedLiteral < Lutaml::Model::Serializable
205
+ attribute :value, :string
206
+ attribute :language_code, :string
207
+ end
208
+ ----
209
+
210
+ == [[graph-serialization]]Graph Serialization
211
+
212
+ When a container model holds a collection of member resources, use `members` to
213
+ serialize them as separate subjects in the same RDF graph.
214
+
215
+ === Container Model
216
+
217
+ [source,ruby]
218
+ ----
219
+ class Vocabulary < Lutaml::Model::Serializable
220
+ attribute :id, :string
221
+ attribute :concepts, Concept, collection: true
222
+
223
+ rdf do
224
+ namespace SkosNamespace
225
+
226
+ subject { |v| "http://example.org/vocab/#{v.id}" }
227
+ type "skos:ConceptScheme"
228
+ predicate :prefLabel, namespace: SkosNamespace, to: :id
229
+
230
+ members :concepts
231
+ end
232
+ end
233
+ ----
234
+
235
+ === Turtle Output with Members
236
+
237
+ [source,ruby]
238
+ ----
239
+ vocab = Vocabulary.new(id: "iso1087", concepts: [concept1, concept2])
240
+ puts vocab.to_turtle
241
+ ----
242
+
243
+ Produces a single Turtle document with the container and all member triples:
244
+
245
+ [source,turtle]
246
+ ----
247
+ @prefix skos: <http://www.w3.org/2004/02/skos/core#> .
248
+
249
+ <http://example.org/vocab/iso1087> a skos:ConceptScheme;
250
+ skos:prefLabel "iso1087" .
251
+
252
+ <http://example.org/concept/2119> a skos:Concept;
253
+ skos:notation "2119";
254
+ skos:prefLabel "component" .
255
+
256
+ <http://example.org/concept/2120> a skos:Concept;
257
+ skos:notation "2120";
258
+ skos:prefLabel "intension" .
259
+ ----
260
+
261
+ === JSON-LD Output with Members
262
+
263
+ [source,ruby]
264
+ ----
265
+ puts vocab.to_jsonld
266
+ ----
267
+
268
+ Produces a JSON-LD document with `@graph` containing all resources:
269
+
270
+ [source,json]
271
+ ----
272
+ {
273
+ "@context": {
274
+ "skos": "http://www.w3.org/2004/02/skos/core#",
275
+ "prefLabel": "skos:prefLabel",
276
+ "notation": "skos:notation"
277
+ },
278
+ "@graph": [
279
+ {
280
+ "@id": "http://example.org/vocab/iso1087",
281
+ "@type": "skos:ConceptScheme",
282
+ "prefLabel": "iso1087"
283
+ },
284
+ {
285
+ "@id": "http://example.org/concept/2119",
286
+ "@type": "skos:Concept",
287
+ "notation": "2119",
288
+ "prefLabel": "component"
289
+ },
290
+ {
291
+ "@id": "http://example.org/concept/2120",
292
+ "@type": "skos:Concept",
293
+ "notation": "2120",
294
+ "prefLabel": "intension"
295
+ }
296
+ ]
297
+ }
298
+ ----
299
+
300
+ === Member-Only Models
301
+
302
+ A container model without a `subject` block serializes only the member triples.
303
+ This is useful when you don't need a container resource:
304
+
305
+ [source,ruby]
306
+ ----
307
+ class MemberOnly < Lutaml::Model::Serializable
308
+ attribute :items, Concept, collection: true
309
+
310
+ rdf do
311
+ namespace SkosNamespace
312
+ members :items
313
+ end
314
+ end
315
+ ----
316
+
317
+ === Namespace Merging
318
+
319
+ When a container and its members declare different namespaces, all namespaces
320
+ are merged in the output. Prefix declarations in Turtle and `@context` entries
321
+ in JSON-LD include the union of all namespaces.
322
+
323
+ == Architecture
324
+
325
+ The unified RDF infrastructure consists of:
326
+
327
+ * `Lutaml::Rdf::Mapping` — Unified mapping base class with `namespace`,
328
+ `subject`, `type`, `predicate`, and `members` DSL methods
329
+ * `Lutaml::Rdf::MappingRule` — Value object for predicate-to-attribute mappings
330
+ * `Lutaml::Rdf::MemberRule` — Value object for `members` declarations
331
+ * `Lutaml::Rdf::Transform` — Base transform class with shared logic for subject
332
+ URI resolution, type resolution, and language extraction
333
+ * `Lutaml::Turtle::Transform` — Inherits from `Rdf::Transform`, produces Turtle
334
+ * `Lutaml::JsonLd::Transform` — Inherits from `Rdf::Transform`, produces JSON-LD
335
+
336
+ Both format-specific transforms detect `Rdf::Mapping` instances and dispatch to
337
+ unified serialization logic.
338
+
339
+ == Error Handling
340
+
341
+ * `Lutaml::Turtle::MissingSubjectError` — raised when a model with predicates
342
+ has no `subject` block and no `members` declaration
343
+ * `ArgumentError` — raised when a predicate's `namespace` is not a
344
+ `Rdf::Namespace` subclass