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,303 @@
1
+ ---
2
+ title: Document Validation Framework
3
+ nav_order: 12
4
+ parent: Guides
5
+ ---
6
+
7
+ # Document Validation Framework
8
+ :toc:
9
+ :toclevels: 3
10
+
11
+ == Overview
12
+
13
+ Lutaml::Model provides a document-level validation framework
14
+ (`Lutaml::Model::Validation`) for validating structural integrity,
15
+ cross-references, and conformance against domain-specific rules.
16
+
17
+ This framework is **orthogonal** to the existing attribute-level validation
18
+ (`validate`/`validate!` on model instances). Attribute validation checks type
19
+ constraints, enumerations, and collection sizes. The document validation
20
+ framework checks document-level concerns like structural integrity,
21
+ cross-references, and conformance rules.
22
+
23
+ The framework is designed for reuse across the lutaml ecosystem
24
+ (uniword, svg_conform, lutaml-model) and follows open/closed principles --
25
+ you extend it by subclassing, not by modifying framework code.
26
+
27
+ == Core components
28
+
29
+ === Issue
30
+
31
+ `Lutaml::Model::Validation::Issue` is a `Lutaml::Model::Serializable` subclass
32
+ representing a validation finding.
33
+
34
+ [source,ruby]
35
+ ----
36
+ issue = Lutaml::Model::Validation::Issue.new(
37
+ severity: "error", # "error", "warning", "info", or "notice"
38
+ code: "DOC-020", # Rule-specific code
39
+ message: "Missing author",# Human-readable description
40
+ location: "word/document.xml", # Optional: file path or XPath
41
+ line: 42, # Optional: line number
42
+ suggestion: "Add an author element" # Optional: fix hint
43
+ )
44
+
45
+ issue.error? # => true
46
+ issue.warning? # => false
47
+
48
+ # Serializable to JSON/YAML
49
+ issue.to_json
50
+ ----
51
+
52
+ Severity values are validated against `Issue::SEVERITIES` (`%w[error warning info notice]`).
53
+ Invalid severities raise `ArgumentError`.
54
+
55
+ === Rule
56
+
57
+ `Lutaml::Model::Validation::Rule` is an abstract base class. Subclass it and
58
+ override methods to implement domain-specific validation logic.
59
+
60
+ [source,ruby]
61
+ ----
62
+ class FontConsistencyRule < Lutaml::Model::Validation::Rule
63
+ def code = "DOC-010"
64
+ def category = :fonts
65
+ def severity = "warning"
66
+
67
+ def applicable?(context)
68
+ context.key?(:fonts)
69
+ end
70
+
71
+ def check(context)
72
+ fonts = context[:fonts]
73
+ return [] if fonts.length <= 1
74
+
75
+ [issue("Document uses #{fonts.length} different fonts")]
76
+ end
77
+ end
78
+ ----
79
+
80
+ Key methods to override:
81
+
82
+ |===
83
+ | Method | Default | Description
84
+
85
+ | `code` | `nil` | Unique rule identifier (e.g., "DOC-010")
86
+ | `category` | `:general` | Rule category for filtering
87
+ | `severity` | `"error"` | Default severity for issues
88
+ | `applicable?(context)` | `true` | Whether to run this rule for the given context
89
+ | `check(context)` | `[]` | Returns an `Array<Issue>` of findings
90
+ | `needs_deferred?` | `false` | For streaming validation
91
+ | `complete(context)` | `[]` | Final issues after deferred collection
92
+ |===
93
+
94
+ Use the private `issue(message, **overrides)` helper inside `check` to create
95
+ issues that inherit the rule's severity and code by default.
96
+
97
+ === Registry
98
+
99
+ `Lutaml::Model::Validation::Registry` is an instance-based rule store with
100
+ cached instantiation.
101
+
102
+ [source,ruby]
103
+ ----
104
+ registry = Lutaml::Model::Validation::Registry.new
105
+
106
+ # Register rule classes (instances are cached after first materialization)
107
+ registry.register(FontConsistencyRule)
108
+ registry.register(RequiredFieldsRule)
109
+
110
+ # Query — instances are cached, subsequent calls return the same objects
111
+ registry.all # => [#<FontConsistencyRule>, #<RequiredFieldsRule>]
112
+ registry.find("DOC-010") # => #<FontConsistencyRule>
113
+ registry.for_category(:fonts) # => [#<FontConsistencyRule>]
114
+ registry.size # => 2
115
+
116
+ # Duplicate registration is prevented
117
+ registry.register(FontConsistencyRule) # no-op
118
+ registry.size # => 2
119
+
120
+ # Reset clears both classes and cached instances
121
+ registry.reset!
122
+ ----
123
+
124
+ NOTE: Rule instances are cached after the first call to `all`, `find`, or
125
+ `for_category`. Registering a new rule or calling `reset!` invalidates the
126
+ cache automatically.
127
+
128
+ The `auto_discover(dir, pattern:)` method scans a directory for rule files:
129
+
130
+ [source,ruby]
131
+ ----
132
+ registry.auto_discover("lib/rules/", pattern: "**/*_rule.rb")
133
+ ----
134
+
135
+ === Profile
136
+
137
+ `Lutaml::Model::Validation::Profile` selects which rules run during validation.
138
+ Profiles are loaded from YAML and support import-based composition.
139
+
140
+ `Profile.load(path)` validates the YAML structure — it raises `ArgumentError`
141
+ if the file does not contain a `name` string key.
142
+
143
+ [source,yaml]
144
+ ----
145
+ # profiles/basic.yml
146
+ name: basic
147
+ description: Basic validation
148
+ rules:
149
+ - RequiredFieldsRule
150
+ - StyleReferencesRule
151
+ ----
152
+
153
+ [source,yaml]
154
+ ----
155
+ # profiles/strict.yml
156
+ name: strict
157
+ import:
158
+ - basic
159
+ rules:
160
+ - BookmarksRule
161
+ - ImagesRule
162
+ ----
163
+
164
+ [source,ruby]
165
+ ----
166
+ registry = Lutaml::Model::Validation.new_registry
167
+ registry.register(RequiredFieldsRule)
168
+ registry.register(StyleReferencesRule)
169
+ registry.register(BookmarksRule)
170
+ registry.register(ImagesRule)
171
+
172
+ basic = Lutaml::Model::Validation::Profile.load("profiles/basic.yml")
173
+ strict = Lutaml::Model::Validation::Profile.load("profiles/strict.yml")
174
+
175
+ # Resolve a profile to get rule instances
176
+ profiles = { "basic" => basic, "strict" => strict }
177
+ rules = strict.resolve(registry, profiles)
178
+ # => [RequiredFieldsRule, StyleReferencesRule, BookmarksRule, ImagesRule]
179
+
180
+ # Circular imports are detected and raise ArgumentError
181
+ # profile_a imports profile_b, profile_b imports profile_a => ArgumentError
182
+ ----
183
+
184
+ === Context
185
+
186
+ `Lutaml::Model::Validation::Context` provides mutable error accumulation and
187
+ per-rule state, useful for streaming or multi-pass validation.
188
+
189
+ [source,ruby]
190
+ ----
191
+ context = Lutaml::Model::Validation::Context.new
192
+ context.add_error(issue)
193
+ context.add_errors([issue1, issue2])
194
+ context.errors # => [issue, issue1, issue2]
195
+
196
+ # Per-rule state for accumulation during streaming
197
+ state = context.rule_state("DOC-010")
198
+ state[:count] = (state[:count] || 0) + 1
199
+
200
+ context.reset! # Clear all errors and state
201
+ ----
202
+
203
+ === Report and LayerResult
204
+
205
+ `Lutaml::Model::Validation::Report` and `LayerResult` are serializable report
206
+ models for structured validation output.
207
+
208
+ [source,ruby]
209
+ ----
210
+ issue = Lutaml::Model::Validation::Issue.new(
211
+ severity: "error", code: "DOC-001", message: "Missing title"
212
+ )
213
+ layer = Lutaml::Model::Validation::LayerResult.new(
214
+ name: "Structure", status: "fail", duration_ms: 15, issues: [issue]
215
+ )
216
+ report = Lutaml::Model::Validation::Report.new(
217
+ source: "document.docx", valid: false, duration_ms: 150, layers: [layer]
218
+ )
219
+
220
+ report.issues # => [issue]
221
+ report.errors # => [issue]
222
+ report.warnings # => []
223
+ report.to_json # Full JSON serialization
224
+ ----
225
+
226
+ === Remediation
227
+
228
+ `Lutaml::Model::Validation::Remediation` is an abstract base class for
229
+ auto-fix logic. Override `id`, `targets`, `applicable?`, `fix`, and `preview`.
230
+
231
+ NOTE: The base `fix` method raises `NotImplementedError`. You must override
232
+ it in your subclass.
233
+
234
+ [source,ruby]
235
+ ----
236
+ class FixBrokenReferences < Lutaml::Model::Validation::Remediation
237
+ def id = "REM-001"
238
+ def targets = ["DOC-020"]
239
+
240
+ def applicable?(_context, report)
241
+ report.any? { |i| i.code == "DOC-020" }
242
+ end
243
+
244
+ def fix(context, report)
245
+ # Apply fixes
246
+ Lutaml::Model::Validation::RemediationResult.new(
247
+ success: true,
248
+ message: "Fixed 3 broken references",
249
+ fixed_codes: ["DOC-020"]
250
+ )
251
+ end
252
+
253
+ def preview(context, report)
254
+ # Dry-run (optional)
255
+ "Will fix 3 broken references"
256
+ end
257
+ end
258
+ ----
259
+
260
+ == Running validation
261
+
262
+ === Return issues array
263
+
264
+ [source,ruby]
265
+ ----
266
+ issues = Lutaml::Model::Validation.validate(context_hash, registry)
267
+ issues.each do |issue|
268
+ puts "[#{issue.severity}] #{issue.code}: #{issue.message}"
269
+ end
270
+ ----
271
+
272
+ === Raise on errors
273
+
274
+ [source,ruby]
275
+ ----
276
+ begin
277
+ Lutaml::Model::Validation.validate!(context_hash, registry)
278
+ rescue Lutaml::Model::Validation::ValidationError => e
279
+ puts e.message # => "[DOC-001] Missing title\n[DOC-020] Broken reference"
280
+ e.issues # => [#<Issue code="DOC-001">, #<Issue code="DOC-020">]
281
+ end
282
+ ----
283
+
284
+ `ValidationError` exposes an `issues` accessor containing the error-severity
285
+ issues that triggered the exception.
286
+
287
+ `validate!` raises only for `error` severity issues. Warnings, info, and notice
288
+ issues are returned silently.
289
+
290
+ === With profiles
291
+
292
+ [source,ruby]
293
+ ----
294
+ issues = Lutaml::Model::Validation.validate(context, registry, profile: profile)
295
+ ----
296
+
297
+ == Design principles
298
+
299
+ * **Open/Closed**: Extend by subclassing Rule and Remediation, not by modifying framework code.
300
+ * **Instance-based Registry**: Multiple registries can coexist for different validation contexts.
301
+ * **Serializable**: Issue, Report, and RemediationResult serialize to JSON via lutaml-model.
302
+ * **Composable Profiles**: YAML profiles with import resolution for rule reuse.
303
+ * **Orthogonal**: Works alongside existing attribute validation, not replacing it.
@@ -22,6 +22,17 @@ Task-oriented guides for accomplishing specific goals with Lutaml::Model.
22
22
  * link:../keyvalue-serialization[Key-Value Serialization] - JSON/YAML/TOML/Hash
23
23
  * link:../collection-serialization[Collection Serialization] - JSONL and YAML Stream
24
24
 
25
+ == Linked Data serialization
26
+
27
+ * link:../rdf-serialization[Unified RDF Serialization] - One `rdf` block for both JSON-LD and Turtle
28
+ * link:../jsonld-serialization[JSON-LD Serialization] - W3C JSON-LD 1.1 with `@context`
29
+ * link:../turtle-serialization[Turtle Serialization] - W3C RDF Turtle format
30
+
31
+ == RDF infrastructure
32
+
33
+ See link:../references/rdf-namespaces[RDF Namespaces] for namespace classes,
34
+ `NamespaceSet`, `Iri`, and `Literal` value objects.
35
+
25
36
  == Advanced features
26
37
 
27
38
  * link:../value-transformations[Value Transformations] - Transform values during serialization
@@ -33,6 +44,7 @@ Task-oriented guides for accomplishing specific goals with Lutaml::Model.
33
44
  * link:../liquid-templates[Liquid Templates] - Template integration
34
45
  * link:../ooxml-examples[OOXML Examples] - Real-world Office Open XML
35
46
  * link:../consolidation-mapping[Consolidation Mapping] - Group sibling elements into structured models
47
+ * link:../document-validation[Document Validation] - Document-level validation with rules, profiles, and remediation
36
48
 
37
49
  == By task
38
50
 
@@ -48,6 +60,13 @@ Task-oriented guides for accomplishing specific goals with Lutaml::Model.
48
60
  . link:../value-transformations[Transform values if needed]
49
61
  . link:../missing-values-handling[Handle nil/empty values]
50
62
 
63
+ === I want to serialize to JSON-LD or Turtle (Linked Data)
64
+
65
+ . link:../rdf-serialization[Unified RDF Serialization] (recommended — one block for both formats)
66
+ . link:../jsonld-serialization[JSON-LD Serialization guide] (legacy `jsonld do` block)
67
+ . link:../turtle-serialization[Turtle Serialization guide] (legacy `turtle do` block)
68
+ . link:../references/rdf-namespaces[RDF Namespaces reference]
69
+
51
70
  === I want to work with collections
52
71
 
53
72
  . link:../collection-serialization[Collection Serialization]
@@ -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