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.
Files changed (64) 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 +12 -18
  5. data/Gemfile +2 -0
  6. data/README.adoc +114 -2
  7. data/docs/_guides/index.adoc +18 -0
  8. data/docs/_guides/jsonld-serialization.adoc +217 -0
  9. data/docs/_guides/rdf-serialization.adoc +344 -0
  10. data/docs/_guides/turtle-serialization.adoc +224 -0
  11. data/docs/_pages/serialization_adapters.adoc +31 -0
  12. data/docs/_references/index.adoc +1 -0
  13. data/docs/_references/rdf-namespaces.adoc +243 -0
  14. data/docs/index.adoc +3 -2
  15. data/lib/lutaml/jsonld/adapter.rb +23 -0
  16. data/lib/lutaml/jsonld/context.rb +69 -0
  17. data/lib/lutaml/jsonld/term_definition.rb +39 -0
  18. data/lib/lutaml/jsonld/transform.rb +174 -0
  19. data/lib/lutaml/jsonld.rb +23 -0
  20. data/lib/lutaml/model/format_registry.rb +10 -1
  21. data/lib/lutaml/model/serialize/format_conversion.rb +17 -1
  22. data/lib/lutaml/model/version.rb +1 -1
  23. data/lib/lutaml/model.rb +6 -0
  24. data/lib/lutaml/rdf/error.rb +7 -0
  25. data/lib/lutaml/rdf/iri.rb +44 -0
  26. data/lib/lutaml/rdf/language_tagged.rb +11 -0
  27. data/lib/lutaml/rdf/literal.rb +62 -0
  28. data/lib/lutaml/rdf/mapping.rb +71 -0
  29. data/lib/lutaml/rdf/mapping_rule.rb +35 -0
  30. data/lib/lutaml/rdf/member_rule.rb +13 -0
  31. data/lib/lutaml/rdf/namespace.rb +58 -0
  32. data/lib/lutaml/rdf/namespace_set.rb +69 -0
  33. data/lib/lutaml/rdf/namespaces/dcterms_namespace.rb +12 -0
  34. data/lib/lutaml/rdf/namespaces/owl_namespace.rb +12 -0
  35. data/lib/lutaml/rdf/namespaces/rdf_namespace.rb +14 -0
  36. data/lib/lutaml/rdf/namespaces/rdfs_namespace.rb +12 -0
  37. data/lib/lutaml/rdf/namespaces/skos_namespace.rb +12 -0
  38. data/lib/lutaml/rdf/namespaces/xsd_namespace.rb +12 -0
  39. data/lib/lutaml/rdf/namespaces.rb +14 -0
  40. data/lib/lutaml/rdf/transform.rb +36 -0
  41. data/lib/lutaml/rdf.rb +19 -0
  42. data/lib/lutaml/turtle/adapter.rb +35 -0
  43. data/lib/lutaml/turtle/mapping.rb +7 -0
  44. data/lib/lutaml/turtle/transform.rb +158 -0
  45. data/lib/lutaml/turtle.rb +22 -0
  46. data/spec/lutaml/integration/edge_cases_spec.rb +109 -0
  47. data/spec/lutaml/integration/multi_format_spec.rb +106 -0
  48. data/spec/lutaml/integration/round_trip_spec.rb +170 -0
  49. data/spec/lutaml/jsonld/adapter_spec.rb +46 -0
  50. data/spec/lutaml/jsonld/context_spec.rb +114 -0
  51. data/spec/lutaml/jsonld/term_definition_spec.rb +55 -0
  52. data/spec/lutaml/jsonld/transform_spec.rb +211 -0
  53. data/spec/lutaml/rdf/graph_serialization_spec.rb +137 -0
  54. data/spec/lutaml/rdf/iri_spec.rb +73 -0
  55. data/spec/lutaml/rdf/literal_spec.rb +98 -0
  56. data/spec/lutaml/rdf/mapping_spec.rb +164 -0
  57. data/spec/lutaml/rdf/member_rule_spec.rb +17 -0
  58. data/spec/lutaml/rdf/namespace_set_spec.rb +115 -0
  59. data/spec/lutaml/rdf/namespace_spec.rb +241 -0
  60. data/spec/lutaml/rdf/rdf_transform_spec.rb +82 -0
  61. data/spec/lutaml/turtle/adapter_spec.rb +47 -0
  62. data/spec/lutaml/turtle/mapping_spec.rb +123 -0
  63. data/spec/lutaml/turtle/transform_spec.rb +273 -0
  64. 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] ||= klass.new
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
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Lutaml
4
4
  module Model
5
- VERSION = "0.8.4"
5
+ VERSION = "0.8.5"
6
6
  end
7
7
  end
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)
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Lutaml
4
+ module Rdf
5
+ class Error < Lutaml::Model::Error; end
6
+ end
7
+ end