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.
- checksums.yaml +4 -4
- data/.github/workflows/dependent-tests.yml +5 -0
- data/.rubocop.yml +18 -0
- data/.rubocop_todo.yml +91 -22
- data/Gemfile +2 -0
- data/README.adoc +114 -2
- data/docs/_guides/index.adoc +18 -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/_migrations/0-8-0-namespace-restructuring.adoc +90 -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/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/format_registry.rb +10 -1
- data/lib/lutaml/model/serialize/format_conversion.rb +17 -1
- data/lib/lutaml/model/version.rb +1 -1
- data/lib/lutaml/model.rb +6 -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/adapter_helpers.rb +1 -42
- data/lib/lutaml/xml/adapter/base_adapter.rb +48 -458
- data/lib/lutaml/xml/adapter/namespace_data.rb +0 -17
- data/lib/lutaml/xml/adapter/namespace_uri_collector.rb +71 -0
- data/lib/lutaml/xml/adapter/nokogiri_adapter.rb +5 -1110
- data/lib/lutaml/xml/adapter/oga_adapter.rb +6 -846
- data/lib/lutaml/xml/adapter/ox_adapter.rb +7 -884
- data/lib/lutaml/xml/adapter/plan_based_builder.rb +929 -0
- data/lib/lutaml/xml/adapter/rexml_adapter.rb +10 -864
- data/lib/lutaml/xml/adapter/xml_parser.rb +86 -0
- data/lib/lutaml/xml/adapter/xml_serializer.rb +291 -0
- data/lib/lutaml/xml/adapter.rb +0 -1
- data/lib/lutaml/xml/adapter_element.rb +7 -1
- data/lib/lutaml/xml/builder/base.rb +0 -1
- data/lib/lutaml/xml/data_model.rb +9 -1
- data/lib/lutaml/xml/document.rb +3 -1
- data/lib/lutaml/xml/element.rb +13 -10
- data/lib/lutaml/xml/serialization/format_conversion.rb +19 -42
- data/lib/lutaml/xml/serialization/instance_methods.rb +26 -35
- data/lib/lutaml/xml/transformation/custom_method_wrapper.rb +34 -55
- data/lib/lutaml/xml/transformation/rule_applier.rb +1 -1
- data/lib/lutaml/xml/xml_element.rb +24 -20
- 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/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/adapter/base_adapter_regression_spec.rb +151 -0
- data/spec/lutaml/xml/adapter/order_spec.rb +150 -0
- data/spec/lutaml/xml/clear_parse_state_spec.rb +139 -0
- data/spec/lutaml/xml/doubly_defined_namespace_spec.rb +0 -2
- data/spec/lutaml/xml/schema/compiler_spec.rb +75 -69
- data/spec/lutaml/xml/transformation/custom_method_wrapper_spec.rb +213 -14
- metadata +58 -3
- data/lib/lutaml/xml/adapter/xml_serialization.rb +0 -145
|
@@ -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.
|
|
@@ -873,6 +873,96 @@ xml do
|
|
|
873
873
|
end
|
|
874
874
|
----
|
|
875
875
|
|
|
876
|
+
== Custom XML Serialization Methods
|
|
877
|
+
|
|
878
|
+
Custom XML methods declared via `with: { from: :foo_from_xml, to: :foo_to_xml }` continue to work in v0.8.0, but the runtime objects passed to them have changed. This section covers the API differences and the migration steps needed.
|
|
879
|
+
|
|
880
|
+
=== `doc` is now a `CustomMethodWrapper`
|
|
881
|
+
|
|
882
|
+
In v0.7.x the third argument of `def foo_to_xml(model, parent, doc)` was the underlying adapter document (Nokogiri-, Ox-, Oga-, REXML-backed). In v0.8.0 it is a `Lutaml::Xml::CustomMethodWrapper`, and `parent` is a `Lutaml::Xml::DataModel::XmlElement` rather than an adapter element. The wrapper exposes the same `create_element` / `add_text` / `add_attribute` / `add_element` surface, so straight-line code that uses only those methods keeps working.
|
|
883
|
+
|
|
884
|
+
=== `add_element(parent, x)` raises `TypeError` for foreign element types
|
|
885
|
+
|
|
886
|
+
`doc.add_element(parent, x)` accepts two shapes for `x`:
|
|
887
|
+
|
|
888
|
+
* A `String`, parsed as an XML fragment via Moxml and grafted in as `DataModel::XmlElement` children. This is the path most existing custom methods rely on (e.g. passing the result of `instance.to_xml`).
|
|
889
|
+
* A `Lutaml::Xml::DataModel::XmlElement`, added directly as a child.
|
|
890
|
+
|
|
891
|
+
Anything else — most commonly a `Nokogiri::XML::Element` left over from a v0.7-style custom method that built fragments with `Nokogiri::XML::Builder` — now raises a `TypeError` with a message like:
|
|
892
|
+
|
|
893
|
+
add_element expects a String or XmlElement, got Nokogiri::XML::Element.
|
|
894
|
+
Call .to_xml on the element first.
|
|
895
|
+
|
|
896
|
+
==== Fix
|
|
897
|
+
|
|
898
|
+
Serialize the foreign element to a string before handing it to the wrapper:
|
|
899
|
+
|
|
900
|
+
[source,ruby]
|
|
901
|
+
----
|
|
902
|
+
# v0.7-era pattern (works against Nokogiri-backed adapter doc, raises in 0.8)
|
|
903
|
+
b.parent.elements.first.elements.each { |x| doc.add_element(parent, x) }
|
|
904
|
+
|
|
905
|
+
# v0.8 fix — pass the string fragment, takes the Moxml path
|
|
906
|
+
b.parent.elements.first.elements.each { |x| doc.add_element(parent, x.to_xml) }
|
|
907
|
+
----
|
|
908
|
+
|
|
909
|
+
The same applies to single-element handoffs:
|
|
910
|
+
|
|
911
|
+
[source,ruby]
|
|
912
|
+
----
|
|
913
|
+
# v0.7-era
|
|
914
|
+
elem = b.parent.elements.first
|
|
915
|
+
doc.add_element(parent, elem)
|
|
916
|
+
|
|
917
|
+
# v0.8 fix
|
|
918
|
+
elem = b.parent.elements.first.to_xml
|
|
919
|
+
doc.add_element(parent, elem)
|
|
920
|
+
----
|
|
921
|
+
|
|
922
|
+
=== Sibling walks: `parent.elements` → `current_context.children`
|
|
923
|
+
|
|
924
|
+
Custom methods that introspect the document under construction — for example, an idempotency guard that checks whether a sibling element has already been emitted — need to be translated. The v0.7 idiom
|
|
925
|
+
|
|
926
|
+
[source,ruby]
|
|
927
|
+
----
|
|
928
|
+
def already_emitted?(doc)
|
|
929
|
+
doc.parent.elements.detect { |x| x.name == "foo" }
|
|
930
|
+
end
|
|
931
|
+
----
|
|
932
|
+
|
|
933
|
+
becomes
|
|
934
|
+
|
|
935
|
+
[source,ruby]
|
|
936
|
+
----
|
|
937
|
+
def already_emitted?(doc)
|
|
938
|
+
doc.current_context.children.detect { |x| x.name == "foo" }
|
|
939
|
+
end
|
|
940
|
+
----
|
|
941
|
+
|
|
942
|
+
If you need to support both v0.7 and v0.8 from the same codebase, use a version constant rather than `respond_to?` checks:
|
|
943
|
+
|
|
944
|
+
[source,ruby]
|
|
945
|
+
----
|
|
946
|
+
def already_emitted?(doc)
|
|
947
|
+
siblings = if defined?(Lutaml::VERSION) && Gem::Version.new(Lutaml::VERSION) >= Gem::Version.new("0.8")
|
|
948
|
+
doc.current_context.children
|
|
949
|
+
else
|
|
950
|
+
doc.parent.elements
|
|
951
|
+
end
|
|
952
|
+
siblings.detect { |x| x.name == "foo" }
|
|
953
|
+
end
|
|
954
|
+
----
|
|
955
|
+
|
|
956
|
+
=== Migration checklist for custom methods
|
|
957
|
+
|
|
958
|
+
Audit each `with: { to: :*_to_xml }` method for the following patterns:
|
|
959
|
+
|
|
960
|
+
. Does it call `Nokogiri::XML::Builder.new`, `Ox.dump`, or any other adapter-specific builder, then pass the resulting elements to `doc.add_element`? → Convert each element to a string via `.to_xml` (or equivalent) before passing.
|
|
961
|
+
. Does it walk `doc.parent.elements` / `doc.parent.children`? → Translate to `doc.current_context.children`.
|
|
962
|
+
. Does it cast `doc` to a specific adapter type, or call adapter-specific methods on it? → Replace with the wrapper API (`create_element`, `add_text`, `add_attribute`, `add_element`).
|
|
963
|
+
|
|
964
|
+
Test by round-tripping a representative fixture through `Model.from_xml(file).to_xml` and asserting equivalence. Foreign types will now raise `TypeError` rather than being silently dropped.
|
|
965
|
+
|
|
876
966
|
== Testing Your Migration
|
|
877
967
|
|
|
878
968
|
After updating, run your test suite:
|
|
@@ -25,6 +25,8 @@ LutaML, out of the box, supports the following serialization formats:
|
|
|
25
25
|
* YAML (https://yaml.org/[YAML version 1.2])
|
|
26
26
|
* JSON (https://www.ecma-international.org/publications-and-standards/standards/ecma-404/[ECMA-404 The JSON Data Interchange Standard], unofficial link: https://www.json.org[JSON])
|
|
27
27
|
* TOML (https://toml.io/en[TOML version 1.0])
|
|
28
|
+
* JSON-LD (https://www.w3.org/TR/json-ld11/[W3C JSON-LD 1.1])
|
|
29
|
+
* Turtle (https://www.w3.org/TR/turtle/[W3C RDF 1.1 Turtle])
|
|
28
30
|
|
|
29
31
|
The adapter interface is also used to support certain transformation of models
|
|
30
32
|
into an "end format", which is not a serialization format. For example, the
|
|
@@ -309,6 +311,35 @@ end
|
|
|
309
311
|
----
|
|
310
312
|
|
|
311
313
|
|
|
314
|
+
=== JSON-LD
|
|
315
|
+
|
|
316
|
+
Lutaml::Model supports serialization to and from JSON-LD (W3C JSON-LD 1.1).
|
|
317
|
+
JSON-LD is a key-value format that extends the built-in JSON serialization with
|
|
318
|
+
JSON-LD-specific constructs: `@context`, `@type`, and `@id`.
|
|
319
|
+
|
|
320
|
+
The adapter uses the standard Ruby `JSON` library internally — no additional gem
|
|
321
|
+
is required beyond `lutaml-model`.
|
|
322
|
+
|
|
323
|
+
See the link:../guides/jsonld-serialization[JSON-LD Serialization guide] for
|
|
324
|
+
mapping DSL details and usage examples.
|
|
325
|
+
|
|
326
|
+
|
|
327
|
+
=== Turtle
|
|
328
|
+
|
|
329
|
+
Lutaml::Model supports serialization to and from W3C RDF Turtle format.
|
|
330
|
+
Turtle is a non-key-value format based on RDF triples with its own mapping DSL.
|
|
331
|
+
|
|
332
|
+
Requires the `rdf-turtle` gem:
|
|
333
|
+
|
|
334
|
+
[source,ruby]
|
|
335
|
+
----
|
|
336
|
+
gem "rdf-turtle", "~> 3.3"
|
|
337
|
+
----
|
|
338
|
+
|
|
339
|
+
See the link:../guides/turtle-serialization[Turtle Serialization guide] for
|
|
340
|
+
mapping DSL details and usage examples.
|
|
341
|
+
|
|
342
|
+
|
|
312
343
|
=== Error handling
|
|
313
344
|
|
|
314
345
|
When parsing invalid serialization format data, an `InvalidFormatError` is
|
data/docs/_references/index.adoc
CHANGED
|
@@ -39,6 +39,7 @@ Deep dives into specific features:
|
|
|
39
39
|
|
|
40
40
|
* link:../format-independent-features[Format-Independent Features] - Cross-format mechanisms
|
|
41
41
|
* link:../parent-root-context[Parent and Root Context] - Model hierarchy navigation
|
|
42
|
+
* link:../rdf-namespaces[RDF Namespaces] - Namespace, NamespaceSet, Iri, Literal
|
|
42
43
|
|
|
43
44
|
== See also
|
|
44
45
|
|
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: RDF Namespaces
|
|
3
|
+
nav_order: 12
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
= RDF Namespaces
|
|
7
|
+
|
|
8
|
+
Lutaml::Model provides a comprehensive RDF infrastructure with `Namespace`,
|
|
9
|
+
`NamespaceSet`, `Iri`, and `Literal` classes. These are shared by the JSON-LD
|
|
10
|
+
and Turtle format adapters.
|
|
11
|
+
|
|
12
|
+
== Namespace
|
|
13
|
+
|
|
14
|
+
`Lutaml::Rdf::Namespace` is the abstract base class for RDF namespace
|
|
15
|
+
vocabularies. Subclasses define `uri` and `prefix` at the class level:
|
|
16
|
+
|
|
17
|
+
[source,ruby]
|
|
18
|
+
----
|
|
19
|
+
ns = Class.new(Lutaml::Rdf::Namespace)
|
|
20
|
+
ns.uri "http://example.org/ns/"
|
|
21
|
+
ns.prefix "ex"
|
|
22
|
+
|
|
23
|
+
ns["someName"] # => "http://example.org/ns/someName"
|
|
24
|
+
ns.prefixed("Name") # => "ex:Name"
|
|
25
|
+
----
|
|
26
|
+
|
|
27
|
+
=== Immutability
|
|
28
|
+
|
|
29
|
+
Once set, `uri` and `prefix` are frozen and cannot be changed:
|
|
30
|
+
|
|
31
|
+
[source,ruby]
|
|
32
|
+
----
|
|
33
|
+
ns = Class.new(Lutaml::Rdf::Namespace)
|
|
34
|
+
ns.uri "http://example.org/ns/"
|
|
35
|
+
ns.uri "http://other.org/" # => raises FrozenError
|
|
36
|
+
----
|
|
37
|
+
|
|
38
|
+
Each subclass has independent state — setting `uri` on one does not affect
|
|
39
|
+
another.
|
|
40
|
+
|
|
41
|
+
=== Equality
|
|
42
|
+
|
|
43
|
+
Namespace classes implement equality based on `uri` and `prefix`:
|
|
44
|
+
|
|
45
|
+
[source,ruby]
|
|
46
|
+
----
|
|
47
|
+
ns1 = Class.new(Lutaml::Rdf::Namespace)
|
|
48
|
+
ns1.uri "http://example.org/"
|
|
49
|
+
ns1.prefix "ex"
|
|
50
|
+
|
|
51
|
+
ns2 = Class.new(Lutaml::Rdf::Namespace)
|
|
52
|
+
ns2.uri "http://example.org/"
|
|
53
|
+
ns2.prefix "ex"
|
|
54
|
+
|
|
55
|
+
ns1 == ns2 # => true
|
|
56
|
+
----
|
|
57
|
+
|
|
58
|
+
=== Compact IRI resolution
|
|
59
|
+
|
|
60
|
+
[source,ruby]
|
|
61
|
+
----
|
|
62
|
+
Lutaml::Rdf::Namespace.resolve_compact_iri("skos:Concept", [SkosNamespace])
|
|
63
|
+
# => "http://www.w3.org/2004/02/skos/core#Concept"
|
|
64
|
+
|
|
65
|
+
Lutaml::Rdf::Namespace.resolve_compact_iri("plain_name", [SkosNamespace])
|
|
66
|
+
# => "plain_name" (returned as-is when no colon present)
|
|
67
|
+
----
|
|
68
|
+
|
|
69
|
+
== NamespaceSet
|
|
70
|
+
|
|
71
|
+
`Lutaml::Rdf::NamespaceSet` is an ordered, enumerable collection of namespace
|
|
72
|
+
classes with O(1) prefix lookup:
|
|
73
|
+
|
|
74
|
+
[source,ruby]
|
|
75
|
+
----
|
|
76
|
+
set = Lutaml::Rdf::NamespaceSet.new(
|
|
77
|
+
Lutaml::Rdf::Namespaces::SkosNamespace,
|
|
78
|
+
Lutaml::Rdf::Namespaces::DctermsNamespace
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
set["skos"] # => SkosNamespace (O(1) lookup)
|
|
82
|
+
set.size # => 2
|
|
83
|
+
set.resolve_compact_iri("skos:Concept")
|
|
84
|
+
# => "http://www.w3.org/2004/02/skos/core#Concept"
|
|
85
|
+
set.compact("http://www.w3.org/2004/02/skos/core#Concept")
|
|
86
|
+
# => "skos:Concept"
|
|
87
|
+
set.to_hash
|
|
88
|
+
# => { "skos" => "http://www.w3.org/2004/02/skos/core#",
|
|
89
|
+
# "dcterms" => "http://purl.org/dc/terms/" }
|
|
90
|
+
----
|
|
91
|
+
|
|
92
|
+
=== Collision detection
|
|
93
|
+
|
|
94
|
+
Adding two different namespace classes with the same prefix raises an error:
|
|
95
|
+
|
|
96
|
+
[source,ruby]
|
|
97
|
+
----
|
|
98
|
+
set = Lutaml::Rdf::NamespaceSet.new(SkosNamespace)
|
|
99
|
+
ns = Class.new(Lutaml::Rdf::Namespace)
|
|
100
|
+
ns.uri "http://other.org/"
|
|
101
|
+
ns.prefix "skos"
|
|
102
|
+
set.add(ns) # => raises ArgumentError: Prefix 'skos' conflicts
|
|
103
|
+
----
|
|
104
|
+
|
|
105
|
+
Adding the same class twice is allowed (idempotent).
|
|
106
|
+
|
|
107
|
+
== Iri
|
|
108
|
+
|
|
109
|
+
`Lutaml::Rdf::Iri` is an immutable value object for IRIs:
|
|
110
|
+
|
|
111
|
+
[source,ruby]
|
|
112
|
+
----
|
|
113
|
+
iri = Lutaml::Rdf::Iri.new("http://example.org/concept/1")
|
|
114
|
+
iri.to_s # => "http://example.org/concept/1"
|
|
115
|
+
iri.value # => "http://example.org/concept/1" (frozen)
|
|
116
|
+
|
|
117
|
+
# Expand compact → full
|
|
118
|
+
set = Lutaml::Rdf::NamespaceSet.new(SkosNamespace)
|
|
119
|
+
compact_iri = Lutaml::Rdf::Iri.new("skos:Concept")
|
|
120
|
+
compact_iri.expand(set) # => Iri("http://www.w3.org/2004/02/skos/core#Concept")
|
|
121
|
+
|
|
122
|
+
# Compact full → compact
|
|
123
|
+
full_iri = Lutaml::Rdf::Iri.new("http://www.w3.org/2004/02/skos/core#Concept")
|
|
124
|
+
full_iri.compact(set) # => "skos:Concept"
|
|
125
|
+
----
|
|
126
|
+
|
|
127
|
+
Implements `Comparable` for sorting.
|
|
128
|
+
|
|
129
|
+
== Literal
|
|
130
|
+
|
|
131
|
+
`Lutaml::Rdf::Literal` is a value object for RDF literals with optional
|
|
132
|
+
datatype and language tag:
|
|
133
|
+
|
|
134
|
+
[source,ruby]
|
|
135
|
+
----
|
|
136
|
+
# Plain literal
|
|
137
|
+
lit = Lutaml::Rdf::Literal.new("hello")
|
|
138
|
+
lit.to_turtle # => '"hello"'
|
|
139
|
+
lit.to_jsonld_term # => "hello"
|
|
140
|
+
|
|
141
|
+
# Language-tagged literal
|
|
142
|
+
lit = Lutaml::Rdf::Literal.new("hello", language: "en")
|
|
143
|
+
lit.to_turtle # => '"hello"@en'
|
|
144
|
+
lit.to_jsonld_term # => { "@value" => "hello", "@language" => "en" }
|
|
145
|
+
|
|
146
|
+
# Typed literal
|
|
147
|
+
lit = Lutaml::Rdf::Literal.new("42", datatype: "http://www.w3.org/2001/XMLSchema#integer")
|
|
148
|
+
lit.to_turtle # => '"42"^^<http://www.w3.org/2001/XMLSchema#integer>'
|
|
149
|
+
lit.to_jsonld_term # => { "@value" => "42", "@type" => "xsd:integer" }
|
|
150
|
+
----
|
|
151
|
+
|
|
152
|
+
=== Special character escaping
|
|
153
|
+
|
|
154
|
+
Quotes, newlines, tabs, and backslashes are automatically escaped in Turtle
|
|
155
|
+
and JSON-LD output.
|
|
156
|
+
|
|
157
|
+
== Standard W3C Namespaces
|
|
158
|
+
|
|
159
|
+
Pre-defined namespace classes are in `Lutaml::Rdf::Namespaces`:
|
|
160
|
+
|
|
161
|
+
[cols="1,1,1"]
|
|
162
|
+
|===
|
|
163
|
+
| Class | Prefix | URI
|
|
164
|
+
|
|
165
|
+
| `SkosNamespace` | `skos` | `http://www.w3.org/2004/02/skos/core#`
|
|
166
|
+
| `DctermsNamespace` | `dcterms` | `http://purl.org/dc/terms/`
|
|
167
|
+
| `RdfSyntaxNamespace` | `rdf` | `http://www.w3.org/1999/02/22-rdf-syntax-ns#`
|
|
168
|
+
| `RdfsNamespace` | `rdfs` | `http://www.w3.org/2000/01/rdf-schema#`
|
|
169
|
+
| `OwlNamespace` | `owl` | `http://www.w3.org/2002/07/owl#`
|
|
170
|
+
| `XsdNamespace` | `xsd` | `http://www.w3.org/2001/XMLSchema#`
|
|
171
|
+
|===
|
|
172
|
+
|
|
173
|
+
NOTE: `RdfNamespace` is a backward-compatible alias for `RdfSyntaxNamespace`.
|
|
174
|
+
|
|
175
|
+
== Custom Namespaces
|
|
176
|
+
|
|
177
|
+
Define your own by subclassing:
|
|
178
|
+
|
|
179
|
+
[source,ruby]
|
|
180
|
+
----
|
|
181
|
+
class MyNamespace < Lutaml::Rdf::Namespace
|
|
182
|
+
uri "http://example.org/vocab/"
|
|
183
|
+
prefix "my"
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
MyNamespace["term"] # => "http://example.org/vocab/term"
|
|
187
|
+
----
|
|
188
|
+
|
|
189
|
+
== Error hierarchy
|
|
190
|
+
|
|
191
|
+
[cols="1,1"]
|
|
192
|
+
|===
|
|
193
|
+
| Class | Description
|
|
194
|
+
|
|
195
|
+
| `Lutaml::Rdf::Error` | Base error for all RDF errors
|
|
196
|
+
| `Lutaml::Turtle::MissingSubjectError` | Raised when Turtle mapping has no `subject` block
|
|
197
|
+
|===
|
|
198
|
+
|
|
199
|
+
== Unified RDF Mapping
|
|
200
|
+
|
|
201
|
+
`Lutaml::Rdf::Mapping` is the unified mapping base class shared by both Turtle
|
|
202
|
+
and JSON-LD format adapters. It provides the `rdf` DSL for defining
|
|
203
|
+
predicate-based mappings once and using them for both output formats.
|
|
204
|
+
|
|
205
|
+
See link:../_guides/rdf-serialization.adoc[Unified RDF Serialization] for the
|
|
206
|
+
complete guide.
|
|
207
|
+
|
|
208
|
+
=== MappingRule
|
|
209
|
+
|
|
210
|
+
`Lutaml::Rdf::MappingRule` is a value object for predicate-to-attribute
|
|
211
|
+
mappings:
|
|
212
|
+
|
|
213
|
+
[source,ruby]
|
|
214
|
+
----
|
|
215
|
+
# Created by the `predicate` DSL method
|
|
216
|
+
predicate :prefLabel, namespace: SkosNamespace, to: :name, lang_tagged: true
|
|
217
|
+
----
|
|
218
|
+
|
|
219
|
+
Each rule stores: `predicate_name`, `namespace`, `to` (attribute symbol), and
|
|
220
|
+
`lang_tagged` flag.
|
|
221
|
+
|
|
222
|
+
=== MemberRule
|
|
223
|
+
|
|
224
|
+
`Lutaml::Rdf::MemberRule` is a value object for the `members` declaration in
|
|
225
|
+
graph-level RDF mapping:
|
|
226
|
+
|
|
227
|
+
[source,ruby]
|
|
228
|
+
----
|
|
229
|
+
# In a container model's rdf block
|
|
230
|
+
members :concepts
|
|
231
|
+
----
|
|
232
|
+
|
|
233
|
+
Stores the attribute name referencing the collection of member models.
|
|
234
|
+
|
|
235
|
+
=== Transform
|
|
236
|
+
|
|
237
|
+
`Lutaml::Rdf::Transform` is the base transform class for RDF serialization,
|
|
238
|
+
shared by both `Turtle::Transform` and `JsonLd::Transform`. It provides:
|
|
239
|
+
|
|
240
|
+
* `resolve_subject_uri(mapping, instance)` — evaluates the subject block
|
|
241
|
+
* `resolve_type_uri(mapping)` — resolves compact IRI to full URI
|
|
242
|
+
* `resolve_type_compact(mapping)` — returns the compact IRI form
|
|
243
|
+
* `extract_language(value)` — extracts language code from `LanguageTagged` values
|
data/docs/index.adoc
CHANGED
|
@@ -11,13 +11,14 @@ image:https://img.shields.io/github/license/lutaml/lutaml-model.svg[License]
|
|
|
11
11
|
image:https://github.com/lutaml/lutaml-model/actions/workflows/rake.yml/badge.svg["Build", link="https://github.com/lutaml/lutaml-model/actions/workflows/rake.yml"]
|
|
12
12
|
image:https://github.com/lutaml/lutaml-model/actions/workflows/dependent-tests.yml/badge.svg["Dependent tests", link="https://github.com/lutaml/lutaml-model/actions/workflows/dependent-tests.yml"]
|
|
13
13
|
|
|
14
|
-
Lutaml::Model is a Ruby library for creating information models with attributes and types, providing flexible and comprehensive mechanisms for serializing to and from multiple formats including XML, JSON, YAML, TOML, and Hash.
|
|
14
|
+
Lutaml::Model is a Ruby library for creating information models with attributes and types, providing flexible and comprehensive mechanisms for serializing to and from multiple formats including XML, JSON, YAML, TOML, JSON-LD, Turtle, and Hash.
|
|
15
15
|
|
|
16
16
|
== Key features
|
|
17
17
|
|
|
18
18
|
* **Model-driven design** - Define models with attributes and types
|
|
19
|
-
* **Multi-format serialization** - XML, JSON, YAML, TOML, Hash, YAML Stream
|
|
19
|
+
* **Multi-format serialization** - XML, JSON, YAML, TOML, JSON-LD, Turtle, Hash, YAML Stream
|
|
20
20
|
* **XML namespace support** - Full W3C namespace implementation
|
|
21
|
+
* **Linked Data support** - RDF namespaces, JSON-LD, and Turtle serialization
|
|
21
22
|
* **Validation** - Built-in validation with custom rules
|
|
22
23
|
* **Schema generation** - Generate XSD, JSON Schema, YAML Schema
|
|
23
24
|
* **Polymorphism** - Handle multiple types elegantly
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
|
|
5
|
+
module Lutaml
|
|
6
|
+
module JsonLd
|
|
7
|
+
class Adapter < Lutaml::KeyValue::Document
|
|
8
|
+
def self.parse(jsonld_string, _options = {})
|
|
9
|
+
JSON.parse(jsonld_string, create_additions: false)
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def to_jsonld(*args)
|
|
13
|
+
options = args.first || {}
|
|
14
|
+
data = @attributes
|
|
15
|
+
if options[:pretty]
|
|
16
|
+
JSON.pretty_generate(data, *args)
|
|
17
|
+
else
|
|
18
|
+
JSON.generate(data, *args)
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|