lutaml-model 0.8.3 → 0.8.5

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 (132) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/dependent-tests.yml +3 -1
  3. data/.rubocop.yml +18 -0
  4. data/.rubocop_todo.yml +16 -22
  5. data/Gemfile +2 -0
  6. data/README.adoc +327 -3
  7. data/docs/_guides/document-validation.adoc +303 -0
  8. data/docs/_guides/index.adoc +19 -0
  9. data/docs/_guides/jsonld-serialization.adoc +217 -0
  10. data/docs/_guides/rdf-serialization.adoc +344 -0
  11. data/docs/_guides/turtle-serialization.adoc +224 -0
  12. data/docs/_guides/xml-mapping.adoc +9 -1
  13. data/docs/_guides/xml_mappings/07_best_practices.adoc +36 -0
  14. data/docs/_guides/xml_mappings/08_troubleshooting.adoc +89 -0
  15. data/docs/_pages/serialization_adapters.adoc +31 -0
  16. data/docs/_references/index.adoc +1 -0
  17. data/docs/_references/rdf-namespaces.adoc +243 -0
  18. data/docs/_tutorials/lutaml-xml-architecture.adoc +6 -1
  19. data/docs/index.adoc +3 -2
  20. data/lib/lutaml/jsonld/adapter.rb +23 -0
  21. data/lib/lutaml/jsonld/context.rb +69 -0
  22. data/lib/lutaml/jsonld/term_definition.rb +39 -0
  23. data/lib/lutaml/jsonld/transform.rb +174 -0
  24. data/lib/lutaml/jsonld.rb +23 -0
  25. data/lib/lutaml/model/attribute.rb +19 -1
  26. data/lib/lutaml/model/error/liquid_drop_already_registered_error.rb +11 -0
  27. data/lib/lutaml/model/error/ordered_content_mapping_error.rb +17 -0
  28. data/lib/lutaml/model/format_registry.rb +10 -1
  29. data/lib/lutaml/model/global_context.rb +1 -0
  30. data/lib/lutaml/model/liquefiable.rb +12 -15
  31. data/lib/lutaml/model/mapping/mapping_rule.rb +10 -2
  32. data/lib/lutaml/model/mapping_hash.rb +1 -1
  33. data/lib/lutaml/model/serialize/format_conversion.rb +17 -1
  34. data/lib/lutaml/model/services/transformer.rb +67 -32
  35. data/lib/lutaml/model/transform.rb +41 -4
  36. data/lib/lutaml/model/uninitialized_class.rb +11 -5
  37. data/lib/lutaml/model/validation/concerns/has_issues.rb +27 -0
  38. data/lib/lutaml/model/validation/context.rb +36 -0
  39. data/lib/lutaml/model/validation/issue.rb +62 -0
  40. data/lib/lutaml/model/validation/layer_result.rb +34 -0
  41. data/lib/lutaml/model/validation/profile.rb +66 -0
  42. data/lib/lutaml/model/validation/registry.rb +60 -0
  43. data/lib/lutaml/model/validation/remediation.rb +33 -0
  44. data/lib/lutaml/model/validation/remediation_result.rb +20 -0
  45. data/lib/lutaml/model/validation/report.rb +39 -0
  46. data/lib/lutaml/model/validation/rule.rb +59 -0
  47. data/lib/lutaml/model/validation.rb +2 -1
  48. data/lib/lutaml/model/validation_framework.rb +77 -0
  49. data/lib/lutaml/model/version.rb +1 -1
  50. data/lib/lutaml/model.rb +10 -0
  51. data/lib/lutaml/rdf/error.rb +7 -0
  52. data/lib/lutaml/rdf/iri.rb +44 -0
  53. data/lib/lutaml/rdf/language_tagged.rb +11 -0
  54. data/lib/lutaml/rdf/literal.rb +62 -0
  55. data/lib/lutaml/rdf/mapping.rb +71 -0
  56. data/lib/lutaml/rdf/mapping_rule.rb +35 -0
  57. data/lib/lutaml/rdf/member_rule.rb +13 -0
  58. data/lib/lutaml/rdf/namespace.rb +58 -0
  59. data/lib/lutaml/rdf/namespace_set.rb +69 -0
  60. data/lib/lutaml/rdf/namespaces/dcterms_namespace.rb +12 -0
  61. data/lib/lutaml/rdf/namespaces/owl_namespace.rb +12 -0
  62. data/lib/lutaml/rdf/namespaces/rdf_namespace.rb +14 -0
  63. data/lib/lutaml/rdf/namespaces/rdfs_namespace.rb +12 -0
  64. data/lib/lutaml/rdf/namespaces/skos_namespace.rb +12 -0
  65. data/lib/lutaml/rdf/namespaces/xsd_namespace.rb +12 -0
  66. data/lib/lutaml/rdf/namespaces.rb +14 -0
  67. data/lib/lutaml/rdf/transform.rb +36 -0
  68. data/lib/lutaml/rdf.rb +19 -0
  69. data/lib/lutaml/turtle/adapter.rb +35 -0
  70. data/lib/lutaml/turtle/mapping.rb +7 -0
  71. data/lib/lutaml/turtle/transform.rb +158 -0
  72. data/lib/lutaml/turtle.rb +22 -0
  73. data/lib/lutaml/xml/adapter/nokogiri_adapter.rb +9 -2
  74. data/lib/lutaml/xml/adapter/oga_adapter.rb +11 -3
  75. data/lib/lutaml/xml/adapter/ox_adapter.rb +5 -2
  76. data/lib/lutaml/xml/adapter/rexml_adapter.rb +10 -3
  77. data/lib/lutaml/xml/adapter_element.rb +26 -2
  78. data/lib/lutaml/xml/data_model.rb +14 -0
  79. data/lib/lutaml/xml/document.rb +3 -0
  80. data/lib/lutaml/xml/element.rb +8 -2
  81. data/lib/lutaml/xml/mapping.rb +9 -0
  82. data/lib/lutaml/xml/model_transform.rb +42 -0
  83. data/lib/lutaml/xml/schema/xsd/base.rb +4 -1
  84. data/lib/lutaml/xml/serialization/instance_methods.rb +3 -1
  85. data/lib/lutaml/xml/transformation/ordered_applier.rb +46 -2
  86. data/lib/lutaml/xml/transformation.rb +40 -1
  87. data/lib/lutaml/xml/xml_element.rb +8 -7
  88. data/lutaml-model.gemspec +1 -1
  89. data/spec/lutaml/integration/edge_cases_spec.rb +109 -0
  90. data/spec/lutaml/integration/multi_format_spec.rb +106 -0
  91. data/spec/lutaml/integration/round_trip_spec.rb +170 -0
  92. data/spec/lutaml/jsonld/adapter_spec.rb +46 -0
  93. data/spec/lutaml/jsonld/context_spec.rb +114 -0
  94. data/spec/lutaml/jsonld/term_definition_spec.rb +55 -0
  95. data/spec/lutaml/jsonld/transform_spec.rb +211 -0
  96. data/spec/lutaml/model/attribute_default_cache_spec.rb +58 -0
  97. data/spec/lutaml/model/liquefiable_spec.rb +22 -6
  98. data/spec/lutaml/model/liquid_compatibility_spec.rb +442 -0
  99. data/spec/lutaml/model/ordered_content_spec.rb +5 -5
  100. data/spec/lutaml/model/services/transformer_spec.rb +43 -0
  101. data/spec/lutaml/model/transform_cache_spec.rb +62 -0
  102. data/spec/lutaml/model/transform_dynamic_attributes_spec.rb +41 -0
  103. data/spec/lutaml/model/uninitialized_class_deep_dup_spec.rb +39 -0
  104. data/spec/lutaml/model/uninitialized_class_spec.rb +14 -2
  105. data/spec/lutaml/model/validation/concerns/has_issues_spec.rb +76 -0
  106. data/spec/lutaml/model/validation/context_spec.rb +60 -0
  107. data/spec/lutaml/model/validation/issue_spec.rb +77 -0
  108. data/spec/lutaml/model/validation/layer_result_spec.rb +66 -0
  109. data/spec/lutaml/model/validation/profile_spec.rb +134 -0
  110. data/spec/lutaml/model/validation/registry_spec.rb +94 -0
  111. data/spec/lutaml/model/validation/remediation_result_spec.rb +23 -0
  112. data/spec/lutaml/model/validation/remediation_spec.rb +72 -0
  113. data/spec/lutaml/model/validation/report_spec.rb +58 -0
  114. data/spec/lutaml/model/validation/rule_spec.rb +134 -0
  115. data/spec/lutaml/model/validation/uninitialized_class_validate_spec.rb +29 -0
  116. data/spec/lutaml/model/validation/validation_error_spec.rb +29 -0
  117. data/spec/lutaml/model/validation/validation_framework_spec.rb +110 -0
  118. data/spec/lutaml/rdf/graph_serialization_spec.rb +137 -0
  119. data/spec/lutaml/rdf/iri_spec.rb +73 -0
  120. data/spec/lutaml/rdf/literal_spec.rb +98 -0
  121. data/spec/lutaml/rdf/mapping_spec.rb +164 -0
  122. data/spec/lutaml/rdf/member_rule_spec.rb +17 -0
  123. data/spec/lutaml/rdf/namespace_set_spec.rb +115 -0
  124. data/spec/lutaml/rdf/namespace_spec.rb +241 -0
  125. data/spec/lutaml/rdf/rdf_transform_spec.rb +82 -0
  126. data/spec/lutaml/turtle/adapter_spec.rb +47 -0
  127. data/spec/lutaml/turtle/mapping_spec.rb +123 -0
  128. data/spec/lutaml/turtle/transform_spec.rb +273 -0
  129. data/spec/lutaml/xml/content_model_validation_spec.rb +157 -0
  130. data/spec/lutaml/xml/mapping_spec.rb +12 -7
  131. metadata +95 -7
  132. data/spec/fixtures/liquid_templates/_ceramics.liquid +0 -3
@@ -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
@@ -0,0 +1,224 @@
1
+ ---
2
+ title: Turtle Serialization
3
+ nav_order: 16
4
+ ---
5
+
6
+ = Turtle Serialization
7
+
8
+ Lutaml::Model supports serialization to and from W3C RDF Turtle format using
9
+ SKOS, Dublin Core Terms, and other W3C vocabularies.
10
+
11
+ == Setup
12
+
13
+ Add the `rdf-turtle` gem to your Gemfile:
14
+
15
+ [source,ruby]
16
+ ----
17
+ gem "rdf-turtle", "~> 3.3"
18
+ ----
19
+
20
+ Turtle is a **non-key-value** format adapter with its own mapping DSL based on
21
+ RDF triples (subject–predicate–object). It is registered as a first-class
22
+ format in the `FormatRegistry` with dedicated `Mapping`, `Transform`, and
23
+ `Adapter` classes.
24
+
25
+ == Mapping DSL
26
+
27
+ Use the `turtle do` block in your model to define Turtle mappings:
28
+
29
+ [source,ruby]
30
+ ----
31
+ class Concept < Lutaml::Model::Serializable
32
+ attribute :name, :string
33
+ attribute :description, :string
34
+ attribute :code, :string
35
+
36
+ turtle do
37
+ namespace Lutaml::Rdf::Namespaces::SkosNamespace,
38
+ Lutaml::Rdf::Namespaces::DctermsNamespace
39
+
40
+ subject { |m| "http://example.org/concept/#{m.code}" }
41
+ type "skos:Concept"
42
+
43
+ predicate :prefLabel,
44
+ namespace: Lutaml::Rdf::Namespaces::SkosNamespace,
45
+ to: :name,
46
+ lang_tagged: true
47
+
48
+ predicate :definition,
49
+ namespace: Lutaml::Rdf::Namespaces::SkosNamespace,
50
+ to: :description
51
+
52
+ predicate :notation,
53
+ namespace: Lutaml::Rdf::Namespaces::SkosNamespace,
54
+ to: :code
55
+ end
56
+ end
57
+ ----
58
+
59
+ === namespace
60
+
61
+ Declares the `@prefix` lines in the output. Accepts one or more
62
+ `Lutaml::Rdf::Namespace` subclass references. Internally builds a
63
+ `NamespaceSet` for O(1) prefix lookup and IRI resolution.
64
+
65
+ === subject
66
+
67
+ Required for top-level models with predicates and no `members`. A block that
68
+ generates the subject URI from the model instance. Raises
69
+ `Lutaml::Turtle::MissingSubjectError` if not defined when needed.
70
+
71
+ Member models (serialized via a container's `members` declaration) can omit the
72
+ `subject` block — blank nodes are used automatically.
73
+
74
+ === type
75
+
76
+ Sets the RDF type (`rdf:type`) triple. Compact IRIs (e.g., `"skos:Concept"`)
77
+ are resolved to full URIs via the declared namespaces. Full URIs (e.g.,
78
+ `"http://example.org/MyType"`) are used as-is.
79
+
80
+ === predicate
81
+
82
+ Each `predicate` creates a `Lutaml::Rdf::MappingRule` value object:
83
+
84
+ * `name` — the local name in the namespace (e.g., `:prefLabel`)
85
+ * `namespace:` — the `Lutaml::Rdf::Namespace` subclass (required, validated)
86
+ * `to:` — the model attribute to read/write (required)
87
+ * `lang_tagged:` (default: `false`) — if true, appends `@lang` suffix from the
88
+ value's `language_code` or `language` method
89
+
90
+ Validation errors:
91
+
92
+ * `namespace` must be a `Rdf::Namespace` subclass — raises `ArgumentError`
93
+ * `to:` is required — raises `ArgumentError`
94
+
95
+ == Serialization
96
+
97
+ [source,ruby]
98
+ ----
99
+ concept = Concept.new(name: "test", description: "A description", code: "2119")
100
+ turtle = concept.to_turtle
101
+ ----
102
+
103
+ Produces:
104
+
105
+ [source,turtle]
106
+ ----
107
+ @prefix skos: <http://www.w3.org/2004/02/skos/core#> .
108
+
109
+ <http://example.org/concept/2119> a skos:Concept;
110
+ skos:definition "A description";
111
+ skos:notation "2119";
112
+ skos:prefLabel "test" .
113
+ ----
114
+
115
+ The output uses the `RDF::Turtle::Writer` from the `rdf-turtle` gem, which
116
+ automatically compacts URIs using declared prefixes and uses native Turtle
117
+ syntax for typed literals.
118
+
119
+ === Typed values
120
+
121
+ Integer and boolean attributes are serialized using native Turtle literal
122
+ syntax:
123
+
124
+ * Integers → `42` (not `"42"^^xsd:integer`)
125
+ * Booleans → `true` / `false` (not `"true"^^xsd:boolean`)
126
+
127
+ === Collection attributes
128
+
129
+ When an attribute is defined with `collection: true`, each value produces a
130
+ separate triple with the same predicate. The writer may use comma-separated
131
+ object syntax:
132
+
133
+ [source,turtle]
134
+ ----
135
+ <http://example.org/1> skos:prefLabel "en", "fr" .
136
+ ----
137
+
138
+ == Special Characters
139
+
140
+ String values containing quotes, newlines, tabs, or backslashes are
141
+ automatically escaped. The `RDF::Turtle::Writer` uses triple-quoted strings
142
+ (`"""..."""`) for multi-line literals.
143
+
144
+ == Nil Values
145
+
146
+ Predicates for attributes with `nil` values are omitted from the output. If
147
+ all predicates produce no data, the result is an empty string.
148
+
149
+ == Deserialization
150
+
151
+ [source,ruby]
152
+ ----
153
+ concept = Concept.from_turtle(turtle_string)
154
+ puts concept.code # => "2119"
155
+ puts concept.description # => "A description"
156
+ ----
157
+
158
+ The `from_turtle` method:
159
+
160
+ . Parses the Turtle string into an `RDF::Graph` via `RDF::Turtle::Reader`
161
+ . Finds subjects by `rdf:type` matching the declared type
162
+ . Maps predicates back to model attributes using the declared predicate rules
163
+ . Converts RDF typed literals back to Ruby types (integers, booleans, etc.)
164
+
165
+ Language-tagged literals are extracted without the language tag.
166
+
167
+ == Round-trip
168
+
169
+ Model data round-trips through Turtle serialization:
170
+
171
+ [source,ruby]
172
+ ----
173
+ restored = Concept.from_turtle(concept.to_turtle)
174
+ restored.code == concept.code # => true
175
+ restored.description == concept.description # => true
176
+ ----
177
+
178
+ == Error Handling
179
+
180
+ * `Lutaml::Turtle::MissingSubjectError` — raised when serializing a model
181
+ without a `subject` block defined
182
+ * `RDF::ReaderError` — raised by the RDF parser for malformed Turtle input
183
+ * Both are wrapped in `Lutaml::Model::InvalidFormatError` by the format
184
+ pipeline
185
+
186
+ == Architecture
187
+
188
+ The Turtle format is composed of:
189
+
190
+ * `Lutaml::Turtle::Adapter` — parses Turtle strings to `RDF::Graph` and
191
+ serializes graphs back to Turtle
192
+ * `Lutaml::Turtle::Mapping` — empty subclass of `Rdf::Mapping`; inherits
193
+ `namespace`, `subject`, `type`, `predicate`, `members` DSL methods
194
+ * `Lutaml::Turtle::Transform` — inherits from `Rdf::Transform`; bidirectional
195
+ transform between model instances and RDF graphs via `model_to_data` /
196
+ `data_to_model`
197
+
198
+ == Unified `rdf` DSL
199
+
200
+ If you need both JSON-LD and Turtle output from the same model, use the
201
+ unified `rdf` DSL instead of separate `jsonld` and `turtle` blocks:
202
+
203
+ [source,ruby]
204
+ ----
205
+ class Concept < Lutaml::Model::Serializable
206
+ attribute :name, :string
207
+ attribute :code, :string
208
+
209
+ rdf do
210
+ namespace Lutaml::Rdf::Namespaces::SkosNamespace
211
+ subject { |m| "http://example.org/#{m.code}" }
212
+ type "skos:Concept"
213
+ predicate :prefLabel, namespace: SkosNamespace, to: :name
214
+ predicate :notation, namespace: SkosNamespace, to: :code
215
+ end
216
+ end
217
+ ----
218
+
219
+ The `rdf` block also supports `members` for graph-level serialization, where a
220
+ container model emits all member resources as separate subjects in the same
221
+ Turtle document.
222
+
223
+ See link:../_guides/rdf-serialization.adoc[Unified RDF Serialization] for the
224
+ complete guide.
@@ -307,7 +307,8 @@ end
307
307
  ====
308
308
  When a model has `mixed_content`, use `map_content` with `collection: true`
309
309
  to capture the multiple text segments between child elements.
310
- Without `collection: true`, only the first (or last) text segment is captured.
310
+ Without `collection: true`, a `MixedContentCollectionError` is raised at
311
+ finalization time.
311
312
  ====
312
313
 
313
314
  .Complete round-trip with `mixed_content` and `map_content collection: true`
@@ -496,6 +497,13 @@ Use `ordered` when:
496
497
 
497
498
  Use `sequence` when you need strict order validation (see <<Sequence patterns>>).
498
499
 
500
+ [IMPORTANT]
501
+ ====
502
+ `ordered` models element-only content -- `map_content` is not allowed.
503
+ If you need to capture text content between elements, use `mixed_content`
504
+ instead (see <<mixed-content>>).
505
+ ====
506
+
499
507
  Syntax:
500
508
 
501
509
  [source,ruby]
@@ -324,6 +324,42 @@ end
324
324
 
325
325
  == Mixed content practices
326
326
 
327
+ === Do not use `map_content` with `ordered` (element-only content)
328
+
329
+ The `ordered` method means element-only content -- text nodes are not modeled.
330
+ Using `map_content` with `ordered` raises `OrderedContentMappingError`.
331
+ If you need text between elements, use `mixed_content` instead.
332
+
333
+ **Don't:**
334
+
335
+ [source,ruby]
336
+ ----
337
+ class Paragraph < Lutaml::Model::Serializable
338
+ attribute :text, :string
339
+
340
+ xml do
341
+ element 'p'
342
+ ordered # Element-only -- no text content allowed
343
+ map_content to: :text # ERROR: OrderedContentMappingError
344
+ end
345
+ end
346
+ ----
347
+
348
+ **Do:**
349
+
350
+ [source,ruby]
351
+ ----
352
+ class Paragraph < Lutaml::Model::Serializable
353
+ attribute :text, :string, collection: true
354
+
355
+ xml do
356
+ element 'p'
357
+ mixed_content # Allows text + elements
358
+ map_content to: :text
359
+ end
360
+ end
361
+ ----
362
+
327
363
  === Always declare `mixed_content` for elements with interleaved text and children
328
364
 
329
365
  When an XML element contains both text nodes and child elements in arbitrary