lutaml-model 0.8.4 → 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 +12 -18
- 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/_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/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
- metadata +50 -1
|
@@ -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
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Lutaml
|
|
4
|
+
module JsonLd
|
|
5
|
+
class Context
|
|
6
|
+
attr_reader :prefixes, :terms
|
|
7
|
+
|
|
8
|
+
def initialize
|
|
9
|
+
@prefixes = {}
|
|
10
|
+
@terms = {}
|
|
11
|
+
@vocab = nil
|
|
12
|
+
@language = nil
|
|
13
|
+
@base = nil
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def prefix(namespace_class)
|
|
17
|
+
@prefixes[namespace_class.prefix] = namespace_class.uri
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def vocab(uri = nil)
|
|
21
|
+
@vocab = uri if uri
|
|
22
|
+
@vocab
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def language(lang = nil)
|
|
26
|
+
@language = lang if lang
|
|
27
|
+
@language
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def base(uri = nil)
|
|
31
|
+
@base = uri if uri
|
|
32
|
+
@base
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def term(name, id: nil, type: nil, container: nil, language: nil,
|
|
36
|
+
reverse: false)
|
|
37
|
+
@terms[name] = TermDefinition.new(
|
|
38
|
+
name: name,
|
|
39
|
+
id: id,
|
|
40
|
+
type: type,
|
|
41
|
+
container: container,
|
|
42
|
+
language: language,
|
|
43
|
+
reverse: reverse,
|
|
44
|
+
)
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def to_hash
|
|
48
|
+
ctx = {}
|
|
49
|
+
ctx["@vocab"] = @vocab if @vocab
|
|
50
|
+
ctx["@language"] = @language if @language
|
|
51
|
+
ctx["@base"] = @base if @base
|
|
52
|
+
@prefixes.each { |pfx, uri| ctx[pfx] = uri }
|
|
53
|
+
@terms.each_value { |td| ctx.merge!(td.to_context_hash) }
|
|
54
|
+
ctx
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def resolve(term_name)
|
|
58
|
+
if term_name.include?(":")
|
|
59
|
+
pfx, local = term_name.split(":", 2)
|
|
60
|
+
"#{@prefixes[pfx]}#{local}" if @prefixes.key?(pfx)
|
|
61
|
+
elsif @terms.key?(term_name)
|
|
62
|
+
@terms[term_name].id
|
|
63
|
+
elsif @vocab
|
|
64
|
+
"#{@vocab}#{term_name}"
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
end
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Lutaml
|
|
4
|
+
module JsonLd
|
|
5
|
+
class TermDefinition
|
|
6
|
+
attr_reader :name, :id, :type, :container, :language, :reverse
|
|
7
|
+
|
|
8
|
+
def initialize(name:, id: nil, type: nil, container: nil, language: nil,
|
|
9
|
+
reverse: false)
|
|
10
|
+
@name = name
|
|
11
|
+
@id = id
|
|
12
|
+
@type = type
|
|
13
|
+
@container = container
|
|
14
|
+
@language = language
|
|
15
|
+
@reverse = reverse
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def to_context_hash
|
|
19
|
+
if simple_mapping?
|
|
20
|
+
{ @name => @id }
|
|
21
|
+
else
|
|
22
|
+
defn = {}
|
|
23
|
+
defn["@id"] = @id if @id
|
|
24
|
+
defn["@type"] = @type if @type
|
|
25
|
+
defn["@container"] = "@#{@container}" if @container
|
|
26
|
+
defn["@language"] = @language if @language
|
|
27
|
+
defn["@reverse"] = @reverse if @reverse
|
|
28
|
+
{ @name => defn }
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
private
|
|
33
|
+
|
|
34
|
+
def simple_mapping?
|
|
35
|
+
@id && @type.nil? && @container.nil? && @language.nil? && !@reverse
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
|
|
5
|
+
module Lutaml
|
|
6
|
+
module JsonLd
|
|
7
|
+
class Transform < Lutaml::Rdf::Transform
|
|
8
|
+
def model_to_data(instance, _format, options = {})
|
|
9
|
+
mapping = extract_mapping(options)
|
|
10
|
+
return {} unless mapping
|
|
11
|
+
|
|
12
|
+
if mapping.rdf_members.any?
|
|
13
|
+
build_graph_document(mapping, instance)
|
|
14
|
+
else
|
|
15
|
+
build_resource_object(mapping, instance)
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def data_to_model(data, _format, options = {})
|
|
20
|
+
mapping = extract_mapping(options)
|
|
21
|
+
return model_class.new unless mapping
|
|
22
|
+
|
|
23
|
+
hash = data.is_a?(String) ? JSON.parse(data) : data
|
|
24
|
+
|
|
25
|
+
if hash.key?("@graph") && hash["@graph"].is_a?(Array) && !hash["@graph"].empty?
|
|
26
|
+
graph_data = hash["@graph"]
|
|
27
|
+
first = graph_data.first
|
|
28
|
+
hash = first.is_a?(Hash) ? first : {}
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
hash = strip_jsonld_keywords(hash)
|
|
32
|
+
|
|
33
|
+
attrs = {}
|
|
34
|
+
mapping.rdf_predicates.each do |rule|
|
|
35
|
+
value = hash[rule.predicate_name]
|
|
36
|
+
next if value.nil?
|
|
37
|
+
|
|
38
|
+
attrs[rule.to] = if rule.lang_tagged && value.is_a?(Hash)
|
|
39
|
+
flatten_language_map(value)
|
|
40
|
+
else
|
|
41
|
+
value
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
build_instance(attrs, options)
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
private
|
|
49
|
+
|
|
50
|
+
def extract_mapping(options)
|
|
51
|
+
options[:mappings] || mappings_for(:jsonld, lutaml_register)
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def build_graph_document(mapping, instance)
|
|
55
|
+
context = build_merged_context(mapping, instance)
|
|
56
|
+
graph = []
|
|
57
|
+
|
|
58
|
+
if mapping.rdf_subject
|
|
59
|
+
resource = build_resource_data(mapping, instance)
|
|
60
|
+
graph << resource unless resource.empty?
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
mapping.rdf_members.each do |member_rule|
|
|
64
|
+
collection = Array(instance.public_send(member_rule.attr_name))
|
|
65
|
+
collection.each do |member|
|
|
66
|
+
member_mapping = member.class.mappings[:jsonld]
|
|
67
|
+
next unless member_mapping
|
|
68
|
+
|
|
69
|
+
resource = build_resource_data(member_mapping, member)
|
|
70
|
+
graph << resource unless resource.empty?
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
{ "@context" => context, "@graph" => graph }
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def build_merged_context(mapping, instance)
|
|
78
|
+
context_hash = build_context_from_mapping(mapping).to_hash
|
|
79
|
+
|
|
80
|
+
mapping.rdf_members.each do |member_rule|
|
|
81
|
+
collection = Array(instance.public_send(member_rule.attr_name))
|
|
82
|
+
next if collection.empty?
|
|
83
|
+
|
|
84
|
+
member_mapping = collection.first.class.mappings[:jsonld]
|
|
85
|
+
next unless member_mapping
|
|
86
|
+
|
|
87
|
+
context_hash.merge!(build_context_from_mapping(member_mapping).to_hash)
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
context_hash
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def build_context_from_mapping(mapping)
|
|
94
|
+
context = Context.new
|
|
95
|
+
mapping.namespace_set.each { |ns| context.prefix(ns) }
|
|
96
|
+
mapping.rdf_predicates.each do |pred|
|
|
97
|
+
if pred.lang_tagged
|
|
98
|
+
context.term(pred.predicate_name,
|
|
99
|
+
id: pred.uri,
|
|
100
|
+
container: :language)
|
|
101
|
+
else
|
|
102
|
+
context.term(pred.predicate_name, id: pred.uri)
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
context
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
def build_resource_object(mapping, instance)
|
|
109
|
+
context = build_context_from_mapping(mapping).to_hash
|
|
110
|
+
data = build_resource_data(mapping, instance)
|
|
111
|
+
{ "@context" => context }.merge(data)
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
def build_resource_data(mapping, instance)
|
|
115
|
+
result = {}
|
|
116
|
+
|
|
117
|
+
if mapping.rdf_type
|
|
118
|
+
result["@type"] = resolve_type_compact(mapping)
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
if mapping.rdf_subject
|
|
122
|
+
result["@id"] = resolve_subject_uri(mapping, instance)
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
mapping.rdf_predicates.each do |rule|
|
|
126
|
+
value = instance.public_send(rule.to)
|
|
127
|
+
next if value.nil?
|
|
128
|
+
next if value.is_a?(String) && value.empty?
|
|
129
|
+
|
|
130
|
+
result[rule.predicate_name] = if rule.lang_tagged
|
|
131
|
+
build_language_map(value)
|
|
132
|
+
else
|
|
133
|
+
serialize_rdf_value(value)
|
|
134
|
+
end
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
result
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
def build_language_map(values)
|
|
141
|
+
case values
|
|
142
|
+
when Array
|
|
143
|
+
map = {}
|
|
144
|
+
values.each do |v|
|
|
145
|
+
lang = extract_language(v)
|
|
146
|
+
map[lang] = v.to_s if lang
|
|
147
|
+
end
|
|
148
|
+
map.empty? ? nil : map
|
|
149
|
+
else
|
|
150
|
+
lang = extract_language(values)
|
|
151
|
+
lang ? { lang => values.to_s } : values.to_s
|
|
152
|
+
end
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
def flatten_language_map(lang_map)
|
|
156
|
+
lang_map.values
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
def serialize_rdf_value(value)
|
|
160
|
+
case value
|
|
161
|
+
when Array then value.map { |v| serialize_rdf_value(v) }
|
|
162
|
+
when Integer, Float, TrueClass, FalseClass then value
|
|
163
|
+
else value.to_s
|
|
164
|
+
end
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
def strip_jsonld_keywords(data)
|
|
168
|
+
return data unless data.is_a?(Hash)
|
|
169
|
+
|
|
170
|
+
data.reject { |key, _| key.start_with?("@") }
|
|
171
|
+
end
|
|
172
|
+
end
|
|
173
|
+
end
|
|
174
|
+
end
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "model"
|
|
4
|
+
require_relative "rdf"
|
|
5
|
+
|
|
6
|
+
module Lutaml
|
|
7
|
+
module JsonLd
|
|
8
|
+
autoload :Context, "#{__dir__}/jsonld/context"
|
|
9
|
+
autoload :TermDefinition, "#{__dir__}/jsonld/term_definition"
|
|
10
|
+
autoload :Transform, "#{__dir__}/jsonld/transform"
|
|
11
|
+
autoload :Adapter, "#{__dir__}/jsonld/adapter"
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
Lutaml::Model::FormatRegistry.register(
|
|
16
|
+
:jsonld,
|
|
17
|
+
mapping_class: Lutaml::Rdf::Mapping,
|
|
18
|
+
adapter_class: Lutaml::JsonLd::Adapter,
|
|
19
|
+
transformer: Lutaml::JsonLd::Transform,
|
|
20
|
+
key_value: false,
|
|
21
|
+
rdf: true,
|
|
22
|
+
error_types: ["JSON::ParserError"],
|
|
23
|
+
)
|
|
@@ -23,7 +23,7 @@ module Lutaml
|
|
|
23
23
|
# @param adapter_options [Hash, nil] { available: [...], default: :name }
|
|
24
24
|
def register(format, mapping_class:, adapter_class:, transformer:,
|
|
25
25
|
adapter_loader: nil, castable_type: nil, key_value: nil,
|
|
26
|
-
error_types: nil, adapter_options: nil)
|
|
26
|
+
rdf: nil, error_types: nil, adapter_options: nil)
|
|
27
27
|
validate_registration!(format, mapping_class, transformer)
|
|
28
28
|
|
|
29
29
|
registered_formats[format] = {
|
|
@@ -33,6 +33,7 @@ module Lutaml
|
|
|
33
33
|
adapter_loader: adapter_loader,
|
|
34
34
|
castable_type: castable_type,
|
|
35
35
|
key_value: key_value,
|
|
36
|
+
rdf: rdf,
|
|
36
37
|
error_types: error_types,
|
|
37
38
|
adapter_options: adapter_options,
|
|
38
39
|
registered_at: Time.now,
|
|
@@ -109,6 +110,14 @@ module Lutaml
|
|
|
109
110
|
registered_formats.dig(format, :key_value) == true
|
|
110
111
|
end
|
|
111
112
|
|
|
113
|
+
def rdf_formats
|
|
114
|
+
registered_formats.select { |_, info| info[:rdf] }.keys
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
def rdf?(format)
|
|
118
|
+
registered_formats.dig(format, :rdf) == true
|
|
119
|
+
end
|
|
120
|
+
|
|
112
121
|
def error_types_for(format)
|
|
113
122
|
registered_formats.dig(format, :error_types)
|
|
114
123
|
end
|
|
@@ -15,7 +15,12 @@ module Lutaml
|
|
|
15
15
|
# @param block [Proc] The DSL block to evaluate
|
|
16
16
|
def process_mapping(format, *_args, &)
|
|
17
17
|
klass = ::Lutaml::Model::Config.mappings_class_for(format)
|
|
18
|
-
mappings[format]
|
|
18
|
+
existing = mappings[format]
|
|
19
|
+
mappings[format] = if existing.nil? || !existing.is_a?(klass)
|
|
20
|
+
klass.new
|
|
21
|
+
else
|
|
22
|
+
existing
|
|
23
|
+
end
|
|
19
24
|
mappings[format].instance_eval(&)
|
|
20
25
|
|
|
21
26
|
if mappings[format].respond_to?(:finalize)
|
|
@@ -273,6 +278,17 @@ module Lutaml
|
|
|
273
278
|
end
|
|
274
279
|
end
|
|
275
280
|
|
|
281
|
+
# Define RDF mappings for multiple formats (Turtle, JSON-LD, etc.).
|
|
282
|
+
# Discovers RDF formats dynamically from FormatRegistry.
|
|
283
|
+
#
|
|
284
|
+
# @param block [Proc] The DSL block
|
|
285
|
+
def rdf(&block)
|
|
286
|
+
Lutaml::Model::FormatRegistry.rdf_formats.each do |format|
|
|
287
|
+
mappings[format] = Lutaml::Rdf::Mapping.new
|
|
288
|
+
mappings[format].instance_eval(&block)
|
|
289
|
+
end
|
|
290
|
+
end
|
|
291
|
+
|
|
276
292
|
# Get resolved mapping for a format
|
|
277
293
|
#
|
|
278
294
|
# Delegates to TransformationRegistry for centralized caching
|
data/lib/lutaml/model/version.rb
CHANGED
data/lib/lutaml/model.rb
CHANGED
|
@@ -10,6 +10,8 @@ module Lutaml
|
|
|
10
10
|
autoload :Jsonl, "#{__dir__}/jsonl"
|
|
11
11
|
autoload :Yamls, "#{__dir__}/yamls"
|
|
12
12
|
autoload :Xml, "#{__dir__}/xml"
|
|
13
|
+
autoload :JsonLd, "#{__dir__}/jsonld"
|
|
14
|
+
autoload :Turtle, "#{__dir__}/turtle"
|
|
13
15
|
|
|
14
16
|
module Model
|
|
15
17
|
# Autoloads for lazy loading - set up BEFORE any requires
|
|
@@ -241,6 +243,10 @@ require "#{__dir__}/jsonl"
|
|
|
241
243
|
require "#{__dir__}/yamls"
|
|
242
244
|
require "#{__dir__}/xml"
|
|
243
245
|
|
|
246
|
+
# Optional formats: require "lutaml/jsonld" or "lutaml/turtle" to enable.
|
|
247
|
+
# These are not eagerly loaded because they depend on optional gems
|
|
248
|
+
# (e.g., rdf-turtle) that most users don't need.
|
|
249
|
+
|
|
244
250
|
# Prepend builder interface into Serialize
|
|
245
251
|
# Builder must be prepended AFTER XML so its initialize runs first
|
|
246
252
|
# (Builder -> XML InstanceMethods -> Serialize)
|