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.
- checksums.yaml +4 -4
- data/.github/workflows/dependent-tests.yml +3 -1
- data/.rubocop.yml +18 -0
- data/.rubocop_todo.yml +16 -22
- data/Gemfile +2 -0
- data/README.adoc +327 -3
- data/docs/_guides/document-validation.adoc +303 -0
- data/docs/_guides/index.adoc +19 -0
- data/docs/_guides/jsonld-serialization.adoc +217 -0
- data/docs/_guides/rdf-serialization.adoc +344 -0
- data/docs/_guides/turtle-serialization.adoc +224 -0
- data/docs/_guides/xml-mapping.adoc +9 -1
- data/docs/_guides/xml_mappings/07_best_practices.adoc +36 -0
- data/docs/_guides/xml_mappings/08_troubleshooting.adoc +89 -0
- data/docs/_pages/serialization_adapters.adoc +31 -0
- data/docs/_references/index.adoc +1 -0
- data/docs/_references/rdf-namespaces.adoc +243 -0
- data/docs/_tutorials/lutaml-xml-architecture.adoc +6 -1
- data/docs/index.adoc +3 -2
- data/lib/lutaml/jsonld/adapter.rb +23 -0
- data/lib/lutaml/jsonld/context.rb +69 -0
- data/lib/lutaml/jsonld/term_definition.rb +39 -0
- data/lib/lutaml/jsonld/transform.rb +174 -0
- data/lib/lutaml/jsonld.rb +23 -0
- data/lib/lutaml/model/attribute.rb +19 -1
- data/lib/lutaml/model/error/liquid_drop_already_registered_error.rb +11 -0
- data/lib/lutaml/model/error/ordered_content_mapping_error.rb +17 -0
- data/lib/lutaml/model/format_registry.rb +10 -1
- data/lib/lutaml/model/global_context.rb +1 -0
- data/lib/lutaml/model/liquefiable.rb +12 -15
- data/lib/lutaml/model/mapping/mapping_rule.rb +10 -2
- data/lib/lutaml/model/mapping_hash.rb +1 -1
- data/lib/lutaml/model/serialize/format_conversion.rb +17 -1
- data/lib/lutaml/model/services/transformer.rb +67 -32
- data/lib/lutaml/model/transform.rb +41 -4
- data/lib/lutaml/model/uninitialized_class.rb +11 -5
- data/lib/lutaml/model/validation/concerns/has_issues.rb +27 -0
- data/lib/lutaml/model/validation/context.rb +36 -0
- data/lib/lutaml/model/validation/issue.rb +62 -0
- data/lib/lutaml/model/validation/layer_result.rb +34 -0
- data/lib/lutaml/model/validation/profile.rb +66 -0
- data/lib/lutaml/model/validation/registry.rb +60 -0
- data/lib/lutaml/model/validation/remediation.rb +33 -0
- data/lib/lutaml/model/validation/remediation_result.rb +20 -0
- data/lib/lutaml/model/validation/report.rb +39 -0
- data/lib/lutaml/model/validation/rule.rb +59 -0
- data/lib/lutaml/model/validation.rb +2 -1
- data/lib/lutaml/model/validation_framework.rb +77 -0
- data/lib/lutaml/model/version.rb +1 -1
- data/lib/lutaml/model.rb +10 -0
- data/lib/lutaml/rdf/error.rb +7 -0
- data/lib/lutaml/rdf/iri.rb +44 -0
- data/lib/lutaml/rdf/language_tagged.rb +11 -0
- data/lib/lutaml/rdf/literal.rb +62 -0
- data/lib/lutaml/rdf/mapping.rb +71 -0
- data/lib/lutaml/rdf/mapping_rule.rb +35 -0
- data/lib/lutaml/rdf/member_rule.rb +13 -0
- data/lib/lutaml/rdf/namespace.rb +58 -0
- data/lib/lutaml/rdf/namespace_set.rb +69 -0
- data/lib/lutaml/rdf/namespaces/dcterms_namespace.rb +12 -0
- data/lib/lutaml/rdf/namespaces/owl_namespace.rb +12 -0
- data/lib/lutaml/rdf/namespaces/rdf_namespace.rb +14 -0
- data/lib/lutaml/rdf/namespaces/rdfs_namespace.rb +12 -0
- data/lib/lutaml/rdf/namespaces/skos_namespace.rb +12 -0
- data/lib/lutaml/rdf/namespaces/xsd_namespace.rb +12 -0
- data/lib/lutaml/rdf/namespaces.rb +14 -0
- data/lib/lutaml/rdf/transform.rb +36 -0
- data/lib/lutaml/rdf.rb +19 -0
- data/lib/lutaml/turtle/adapter.rb +35 -0
- data/lib/lutaml/turtle/mapping.rb +7 -0
- data/lib/lutaml/turtle/transform.rb +158 -0
- data/lib/lutaml/turtle.rb +22 -0
- data/lib/lutaml/xml/adapter/nokogiri_adapter.rb +9 -2
- data/lib/lutaml/xml/adapter/oga_adapter.rb +11 -3
- data/lib/lutaml/xml/adapter/ox_adapter.rb +5 -2
- data/lib/lutaml/xml/adapter/rexml_adapter.rb +10 -3
- data/lib/lutaml/xml/adapter_element.rb +26 -2
- data/lib/lutaml/xml/data_model.rb +14 -0
- data/lib/lutaml/xml/document.rb +3 -0
- data/lib/lutaml/xml/element.rb +8 -2
- data/lib/lutaml/xml/mapping.rb +9 -0
- data/lib/lutaml/xml/model_transform.rb +42 -0
- data/lib/lutaml/xml/schema/xsd/base.rb +4 -1
- data/lib/lutaml/xml/serialization/instance_methods.rb +3 -1
- data/lib/lutaml/xml/transformation/ordered_applier.rb +46 -2
- data/lib/lutaml/xml/transformation.rb +40 -1
- data/lib/lutaml/xml/xml_element.rb +8 -7
- data/lutaml-model.gemspec +1 -1
- data/spec/lutaml/integration/edge_cases_spec.rb +109 -0
- data/spec/lutaml/integration/multi_format_spec.rb +106 -0
- data/spec/lutaml/integration/round_trip_spec.rb +170 -0
- data/spec/lutaml/jsonld/adapter_spec.rb +46 -0
- data/spec/lutaml/jsonld/context_spec.rb +114 -0
- data/spec/lutaml/jsonld/term_definition_spec.rb +55 -0
- data/spec/lutaml/jsonld/transform_spec.rb +211 -0
- data/spec/lutaml/model/attribute_default_cache_spec.rb +58 -0
- data/spec/lutaml/model/liquefiable_spec.rb +22 -6
- data/spec/lutaml/model/liquid_compatibility_spec.rb +442 -0
- data/spec/lutaml/model/ordered_content_spec.rb +5 -5
- data/spec/lutaml/model/services/transformer_spec.rb +43 -0
- data/spec/lutaml/model/transform_cache_spec.rb +62 -0
- data/spec/lutaml/model/transform_dynamic_attributes_spec.rb +41 -0
- data/spec/lutaml/model/uninitialized_class_deep_dup_spec.rb +39 -0
- data/spec/lutaml/model/uninitialized_class_spec.rb +14 -2
- data/spec/lutaml/model/validation/concerns/has_issues_spec.rb +76 -0
- data/spec/lutaml/model/validation/context_spec.rb +60 -0
- data/spec/lutaml/model/validation/issue_spec.rb +77 -0
- data/spec/lutaml/model/validation/layer_result_spec.rb +66 -0
- data/spec/lutaml/model/validation/profile_spec.rb +134 -0
- data/spec/lutaml/model/validation/registry_spec.rb +94 -0
- data/spec/lutaml/model/validation/remediation_result_spec.rb +23 -0
- data/spec/lutaml/model/validation/remediation_spec.rb +72 -0
- data/spec/lutaml/model/validation/report_spec.rb +58 -0
- data/spec/lutaml/model/validation/rule_spec.rb +134 -0
- data/spec/lutaml/model/validation/uninitialized_class_validate_spec.rb +29 -0
- data/spec/lutaml/model/validation/validation_error_spec.rb +29 -0
- data/spec/lutaml/model/validation/validation_framework_spec.rb +110 -0
- data/spec/lutaml/rdf/graph_serialization_spec.rb +137 -0
- data/spec/lutaml/rdf/iri_spec.rb +73 -0
- data/spec/lutaml/rdf/literal_spec.rb +98 -0
- data/spec/lutaml/rdf/mapping_spec.rb +164 -0
- data/spec/lutaml/rdf/member_rule_spec.rb +17 -0
- data/spec/lutaml/rdf/namespace_set_spec.rb +115 -0
- data/spec/lutaml/rdf/namespace_spec.rb +241 -0
- data/spec/lutaml/rdf/rdf_transform_spec.rb +82 -0
- data/spec/lutaml/turtle/adapter_spec.rb +47 -0
- data/spec/lutaml/turtle/mapping_spec.rb +123 -0
- data/spec/lutaml/turtle/transform_spec.rb +273 -0
- data/spec/lutaml/xml/content_model_validation_spec.rb +157 -0
- data/spec/lutaml/xml/mapping_spec.rb +12 -7
- metadata +95 -7
- 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.
|
data/docs/_guides/index.adoc
CHANGED
|
@@ -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
|