lutaml-model 0.8.10 → 0.8.12

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 (57) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/dependent-repos.json +1 -0
  3. data/.github/workflows/opal.yml +31 -0
  4. data/.rspec-opal +5 -0
  5. data/.rubocop_todo.yml +68 -7
  6. data/README.adoc +53 -1
  7. data/docs/_guides/index.adoc +4 -0
  8. data/docs/_guides/jsonld-serialization.adoc +3 -1
  9. data/docs/_guides/opal.adoc +221 -0
  10. data/docs/_guides/rdf-serialization.adoc +94 -8
  11. data/docs/_guides/turtle-serialization.adoc +17 -4
  12. data/docs/_guides/xml_mappings/07_best_practices.adoc +2 -1
  13. data/docs/_pages/configuration.adoc +9 -4
  14. data/docs/_pages/index.adoc +1 -0
  15. data/docs/_pages/serialization_adapters.adoc +3 -2
  16. data/docs/index.adoc +1 -0
  17. data/lib/lutaml/hash_format/adapter/mapping.rb +2 -4
  18. data/lib/lutaml/json/adapter/mapping.rb +2 -4
  19. data/lib/lutaml/jsonl/adapter/mapping.rb +2 -4
  20. data/lib/lutaml/jsonld/transform.rb +70 -24
  21. data/lib/lutaml/key_value/adapter/hash/mapping.rb +2 -4
  22. data/lib/lutaml/key_value/adapter/json/mapping.rb +2 -4
  23. data/lib/lutaml/key_value/adapter/jsonl/mapping.rb +2 -4
  24. data/lib/lutaml/key_value/adapter/toml/mapping.rb +2 -4
  25. data/lib/lutaml/key_value/adapter/yaml/mapping.rb +2 -4
  26. data/lib/lutaml/key_value/adapter/yamls/mapping.rb +2 -4
  27. data/lib/lutaml/key_value/mapping.rb +4 -4
  28. data/lib/lutaml/model/adapter_resolver.rb +5 -8
  29. data/lib/lutaml/model/mapping/mapping.rb +12 -0
  30. data/lib/lutaml/model/store.rb +51 -4
  31. data/lib/lutaml/model/version.rb +1 -1
  32. data/lib/lutaml/rdf/mapping.rb +19 -13
  33. data/lib/lutaml/rdf/mapping_rule.rb +19 -2
  34. data/lib/lutaml/rdf/member_rule.rb +19 -2
  35. data/lib/lutaml/rdf/transform.rb +20 -11
  36. data/lib/lutaml/toml/adapter/mapping.rb +2 -4
  37. data/lib/lutaml/turtle/transform.rb +125 -53
  38. data/lib/lutaml/xml/schema/xsd.rb +5 -4
  39. data/lib/lutaml/xml/schema.rb +8 -5
  40. data/lib/lutaml/xml/xml_orderable.rb +17 -0
  41. data/lib/lutaml/xml.rb +8 -11
  42. data/lib/lutaml/yaml/adapter/mapping.rb +2 -4
  43. data/lib/lutaml/yamls/adapter/mapping.rb +7 -3
  44. data/lutaml-model.gemspec +1 -1
  45. data/spec/lutaml/jsonld/transform_spec.rb +239 -0
  46. data/spec/lutaml/model/opal_smoke_spec.rb +117 -0
  47. data/spec/lutaml/model/store_spec.rb +156 -2
  48. data/spec/lutaml/rdf/mapping_rule_spec.rb +97 -0
  49. data/spec/lutaml/rdf/mapping_spec.rb +74 -4
  50. data/spec/lutaml/rdf/member_rule_spec.rb +41 -0
  51. data/spec/lutaml/rdf/rdf_transform_spec.rb +95 -29
  52. data/spec/lutaml/turtle/mapping_spec.rb +2 -2
  53. data/spec/lutaml/turtle/transform_spec.rb +315 -0
  54. data/spec/lutaml/xml/opal_xml_spec.rb +145 -0
  55. data/spec/lutaml/xml/xml_spec.rb +64 -13
  56. data/spec/support/opal.rb +6 -0
  57. metadata +12 -4
@@ -3,14 +3,31 @@
3
3
  module Lutaml
4
4
  module Rdf
5
5
  class MappingRule
6
- attr_reader :predicate_name, :namespace, :to, :lang_tagged
6
+ attr_reader :predicate_name, :namespace, :to, :lang_tagged, :uri_reference
7
7
 
8
- def initialize(predicate_name, namespace:, to:, lang_tagged: false)
8
+ def initialize(predicate_name, namespace:, to:, lang_tagged: false,
9
+ uri_reference: false)
9
10
  validate!(predicate_name, namespace, to)
11
+ if lang_tagged && uri_reference
12
+ raise ArgumentError,
13
+ "lang_tagged and uri_reference are mutually exclusive"
14
+ end
15
+
10
16
  @predicate_name = predicate_name.to_s.freeze
11
17
  @namespace = namespace
12
18
  @to = to
13
19
  @lang_tagged = lang_tagged
20
+ @uri_reference = uri_reference
21
+ end
22
+
23
+ def kind
24
+ if uri_reference
25
+ :uri_reference
26
+ elsif lang_tagged
27
+ :lang_tagged
28
+ else
29
+ :plain
30
+ end
14
31
  end
15
32
 
16
33
  def uri
@@ -3,10 +3,27 @@
3
3
  module Lutaml
4
4
  module Rdf
5
5
  class MemberRule
6
- attr_reader :attr_name
6
+ attr_reader :attr_name, :predicate_name, :namespace
7
+
8
+ def initialize(attr_name, predicate_name: nil, namespace: nil)
9
+ if predicate_name && !namespace
10
+ raise ArgumentError,
11
+ "namespace is required when predicate_name is provided"
12
+ end
7
13
 
8
- def initialize(attr_name)
9
14
  @attr_name = attr_name.to_sym
15
+ @predicate_name = predicate_name
16
+ @namespace = namespace
17
+ end
18
+
19
+ def linked?
20
+ !!@predicate_name
21
+ end
22
+
23
+ def linked_predicate_uri
24
+ return nil unless linked?
25
+
26
+ @namespace[@predicate_name]
10
27
  end
11
28
  end
12
29
  end
@@ -3,22 +3,35 @@
3
3
  module Lutaml
4
4
  module Rdf
5
5
  class Transform < Lutaml::Model::Transform
6
- protected
7
-
8
6
  def resolve_subject_uri(mapping, instance)
9
7
  mapping.rdf_subject&.call(instance)
10
8
  end
11
9
 
12
- def resolve_type_uri(mapping)
13
- return unless mapping.rdf_type
10
+ def resolve_single_type_uri(mapping, type_value)
11
+ mapping.namespace_set.resolve_compact_iri(type_value)
12
+ end
13
+
14
+ def resolve_type_uris(mapping)
15
+ return [] unless mapping.rdf_type.any?
16
+
17
+ mapping.rdf_type.map { |t| resolve_single_type_uri(mapping, t) }
18
+ end
19
+
20
+ def each_member(instance, member_rule, &)
21
+ collection = Array(instance.public_send(member_rule.attr_name))
22
+ collection.each(&)
23
+ end
14
24
 
15
- mapping.namespace_set.resolve_compact_iri(mapping.rdf_type)
25
+ def member_mapping_for(member, format)
26
+ member.class.mappings[format]
16
27
  end
17
28
 
18
- def resolve_type_compact(mapping)
19
- mapping.rdf_type
29
+ def extract_language(value)
30
+ value.language_tag if value.is_a?(Lutaml::Rdf::LanguageTagged)
20
31
  end
21
32
 
33
+ protected
34
+
22
35
  def build_instance(attrs, options)
23
36
  child_register = Lutaml::Model::Register.resolve_for_child(
24
37
  model_class, lutaml_register
@@ -27,10 +40,6 @@ module Lutaml
27
40
  root_and_parent_assignment(instance, options)
28
41
  instance
29
42
  end
30
-
31
- def extract_language(value)
32
- value.language_tag if value.is_a?(Lutaml::Rdf::LanguageTagged)
33
- end
34
43
  end
35
44
  end
36
45
  end
@@ -8,10 +8,8 @@ module Lutaml
8
8
  super(:toml)
9
9
  end
10
10
 
11
- def deep_dup
12
- self.class.new.tap do |new_mapping|
13
- new_mapping.mappings = duplicate_mappings
14
- end
11
+ def dup_instance
12
+ self.class.new
15
13
  end
16
14
 
17
15
  def validate!(key, to, with, render_nil, render_empty)
@@ -46,56 +46,101 @@ module Lutaml
46
46
  def build_graph(mapping, instance)
47
47
  graph = RDF::Graph.new
48
48
 
49
- has_predicates_or_type = mapping.rdf_type || mapping.rdf_predicates.any?
50
-
51
- if has_predicates_or_type
52
- subject_uri = if mapping.rdf_subject
53
- RDF::URI(resolve_subject_uri(mapping, instance))
54
- else
55
- RDF::Node.new
56
- end
57
-
58
- if mapping.rdf_type
59
- type_uri = RDF::URI(resolve_type_uri(mapping))
60
- graph << RDF::Statement.new(subject_uri, RDF.type, type_uri)
49
+ has_resource_data =
50
+ mapping.rdf_type.any? ||
51
+ mapping.rdf_predicates.any? ||
52
+ mapping.rdf_members.any?(&:linked?)
53
+
54
+ if has_resource_data
55
+ subject_uri = resolve_subject(mapping, instance)
56
+ build_resource_triples(graph, mapping, instance, subject_uri)
57
+ end
58
+
59
+ build_member_subgraphs(graph, mapping, instance)
60
+
61
+ graph
62
+ end
63
+
64
+ def resolve_subject(mapping, instance)
65
+ if mapping.rdf_subject
66
+ RDF::URI(resolve_subject_uri(mapping, instance))
67
+ else
68
+ RDF::Node.new
69
+ end
70
+ end
71
+
72
+ def build_resource_triples(graph, mapping, instance, subject_uri)
73
+ mapping.rdf_type.each do |type_value|
74
+ type_uri = RDF::URI(resolve_single_type_uri(mapping, type_value))
75
+ graph << RDF::Statement.new(subject_uri, RDF.type, type_uri)
76
+ end
77
+
78
+ mapping.rdf_predicates.each do |rule|
79
+ value = instance.public_send(rule.to)
80
+ next if value.nil?
81
+
82
+ Array(value).each do |v|
83
+ object = build_rdf_object(v, rule, mapping.namespace_set)
84
+ graph << RDF::Statement.new(subject_uri, RDF::URI(rule.uri), object)
61
85
  end
86
+ end
87
+
88
+ mapping.rdf_members.each do |member_rule|
89
+ next unless member_rule.linked?
62
90
 
63
- mapping.rdf_predicates.each do |rule|
64
- value = instance.public_send(rule.to)
65
- next if value.nil?
91
+ each_member(instance, member_rule) do |member|
92
+ member_mapping = member_mapping_for(member, :turtle)
93
+ next unless member_mapping
66
94
 
67
- Array(value).each do |v|
68
- object = build_rdf_object(v, rule)
69
- graph << RDF::Statement.new(subject_uri, RDF::URI(rule.uri),
70
- object)
71
- end
95
+ member_subject = RDF::URI(resolve_subject_uri(member_mapping,
96
+ member))
97
+ graph << RDF::Statement.new(
98
+ subject_uri,
99
+ RDF::URI(member_rule.linked_predicate_uri),
100
+ member_subject,
101
+ )
72
102
  end
73
103
  end
104
+ end
74
105
 
106
+ def build_member_subgraphs(graph, mapping, instance)
75
107
  mapping.rdf_members.each do |member_rule|
76
- collection = Array(instance.public_send(member_rule.attr_name))
77
- collection.each do |member|
78
- member_mapping = member.class.mappings[:turtle]
108
+ each_member(instance, member_rule) do |member|
109
+ member_mapping = member_mapping_for(member, :turtle)
79
110
  next unless member_mapping
80
111
 
81
112
  graph << build_graph(member_mapping, member)
82
113
  end
83
114
  end
84
-
85
- graph
86
115
  end
87
116
 
88
- def build_rdf_object(value, rule)
89
- if rule.lang_tagged
117
+ def build_rdf_object(value, rule, namespace_set)
118
+ case rule.kind
119
+ when :uri_reference
120
+ build_uri_reference_object(value, namespace_set)
121
+ when :lang_tagged
90
122
  lang = extract_language(value)
91
123
  RDF::Literal.new(value.to_s, language: lang)
92
124
  else
93
- case value
94
- when Integer then RDF::Literal.new(value, datatype: RDF::XSD.integer)
95
- when Float then RDF::Literal.new(value, datatype: RDF::XSD.double)
96
- when TrueClass, FalseClass then RDF::Literal.new(value, datatype: RDF::XSD.boolean)
97
- else RDF::Literal.new(value.to_s)
98
- end
125
+ build_plain_literal(value)
126
+ end
127
+ end
128
+
129
+ def build_uri_reference_object(value, namespace_set)
130
+ resolved = if value.to_s.include?(":")
131
+ namespace_set.resolve_compact_iri(value.to_s)
132
+ else
133
+ value.to_s
134
+ end
135
+ RDF::URI.new(resolved)
136
+ end
137
+
138
+ def build_plain_literal(value)
139
+ case value
140
+ when Integer then RDF::Literal.new(value, datatype: RDF::XSD.integer)
141
+ when Float then RDF::Literal.new(value, datatype: RDF::XSD.double)
142
+ when TrueClass, FalseClass then RDF::Literal.new(value, datatype: RDF::XSD.boolean)
143
+ else RDF::Literal.new(value.to_s)
99
144
  end
100
145
  end
101
146
 
@@ -103,13 +148,12 @@ module Lutaml
103
148
  ns_set = mapping.namespace_set
104
149
 
105
150
  mapping.rdf_members.each do |member_rule|
106
- collection = Array(instance.public_send(member_rule.attr_name))
107
- next if collection.empty?
108
-
109
- member_mapping = collection.first.class.mappings[:turtle]
110
- next unless member_mapping
151
+ each_member(instance, member_rule) do |member|
152
+ member_mapping = member_mapping_for(member, :turtle)
153
+ next unless member_mapping
111
154
 
112
- ns_set = ns_set.merge(member_mapping.namespace_set)
155
+ ns_set = ns_set.merge(member_mapping.namespace_set)
156
+ end
113
157
  end
114
158
 
115
159
  ns_set.each.with_object({}) do |ns, h|
@@ -119,38 +163,66 @@ module Lutaml
119
163
 
120
164
  def extract_attributes(graph, mapping)
121
165
  attrs = {}
122
- type_uri = resolve_type_uri(mapping)
166
+ type_uris = resolve_type_uris(mapping)
123
167
 
124
- matching_subjects = find_subjects_by_type(graph, type_uri)
168
+ matching_subjects = find_subjects_by_types(graph, type_uris)
125
169
 
126
170
  matching_subjects.each do |subject|
127
- mapping.rdf_predicates.each do |rule|
128
- stmts = graph.query([subject, RDF::URI(rule.uri), nil])
129
- next if stmts.empty?
130
-
131
- values = stmts.map { |s| literal_to_ruby(s.object) }
132
- attrs[rule.to] = values.length == 1 ? values.first : values
133
- end
171
+ attrs["id"] = subject.to_s unless subject.node?
172
+ extract_predicate_attributes(graph, subject, mapping, attrs)
134
173
  end
135
174
 
136
175
  attrs
137
176
  end
138
177
 
139
- def find_subjects_by_type(graph, type_uri)
140
- graph.query([nil, RDF.type, RDF::URI(type_uri)]).map(&:subject).uniq
178
+ def extract_predicate_attributes(graph, subject, mapping, attrs)
179
+ mapping.rdf_predicates.each do |rule|
180
+ stmts = graph.query([subject, RDF::URI(rule.uri), nil])
181
+ next if stmts.empty?
182
+
183
+ values = stmts.map do |s|
184
+ literal_to_ruby(s.object, rule, mapping.namespace_set)
185
+ end
186
+ attrs[rule.to] = values.length == 1 ? values.first : values
187
+ end
188
+ end
189
+
190
+ def find_subjects_by_types(graph, type_uris)
191
+ type_uris.flat_map do |type_uri|
192
+ graph.query([nil, RDF.type, RDF::URI(type_uri)]).map(&:subject).uniq
193
+ end.uniq
141
194
  end
142
195
 
143
- def literal_to_ruby(rdf_object)
196
+ def literal_to_ruby(rdf_object, rule, namespace_set)
144
197
  case rdf_object
198
+ when RDF::URI
199
+ uri_to_ruby(rdf_object, rule, namespace_set)
145
200
  when RDF::Literal
201
+ literal_value_to_ruby(rdf_object, rule)
202
+ else
203
+ rdf_object.to_s
204
+ end
205
+ end
206
+
207
+ def uri_to_ruby(rdf_object, rule, namespace_set)
208
+ uri_str = rdf_object.to_s
209
+ if rule.kind == :uri_reference
210
+ namespace_set.compact(uri_str) || uri_str
211
+ else
212
+ uri_str
213
+ end
214
+ end
215
+
216
+ def literal_value_to_ruby(rdf_object, rule)
217
+ if rule.kind == :lang_tagged && rdf_object.language
218
+ rdf_object.value
219
+ else
146
220
  case rdf_object.datatype
147
221
  when RDF::XSD.integer then rdf_object.value.to_i
148
222
  when RDF::XSD.double, RDF::XSD.decimal, RDF::XSD.float then rdf_object.value.to_f
149
223
  when RDF::XSD.boolean then rdf_object.value == "true"
150
224
  else rdf_object.value
151
225
  end
152
- else
153
- rdf_object.to_s
154
226
  end
155
227
  end
156
228
  end
@@ -2,11 +2,12 @@
2
2
 
3
3
  # NOTE: Do NOT require lutaml/model here. This file is autoloaded via
4
4
  # Lutaml::Xml::Schema::Xsd, and requiring lutaml/model creates a circular
5
- # dependency since lutaml/xml.rb requires lutaml/model. The adapter type
6
- # is already set by lutaml/xml.rb at the end.
5
+ # dependency since lutaml/xml.rb requires lutaml/model. This file only sets
6
+ # a fallback XML adapter when no adapter has been selected yet.
7
7
 
8
- adapter = RUBY_ENGINE == "opal" ? :oga : :nokogiri
9
- Lutaml::Model::Config.xml_adapter_type = adapter unless defined?(Lutaml::Model::Config.xml_adapter_type)
8
+ unless defined?(Lutaml::Model::Config.xml_adapter_type)
9
+ Lutaml::Model::Config.xml_adapter_type = Lutaml::Model::AdapterResolver.detect_xml_adapter
10
+ end
10
11
 
11
12
  # Require the XsdNamespace class for XSD schema support
12
13
  require_relative "xsd_namespace"
@@ -3,11 +3,14 @@
3
3
  module Lutaml
4
4
  module Xml
5
5
  module Schema
6
- autoload :Xsd, "#{__dir__}/schema/xsd"
7
- autoload :XsdSchema, "#{__dir__}/schema/xsd_schema"
8
- autoload :RelaxngSchema, "#{__dir__}/schema/relaxng_schema"
9
- autoload :Builder, "#{__dir__}/schema/builder"
10
- autoload :BuiltinTypes, "#{__dir__}/schema/builtin_types"
6
+ Lutaml::Model::RuntimeCompatibility.autoload_native(
7
+ self,
8
+ Xsd: "#{__dir__}/schema/xsd",
9
+ XsdSchema: "#{__dir__}/schema/xsd_schema",
10
+ RelaxngSchema: "#{__dir__}/schema/relaxng_schema",
11
+ Builder: "#{__dir__}/schema/builder",
12
+ BuiltinTypes: "#{__dir__}/schema/builtin_types",
13
+ )
11
14
  end
12
15
  end
13
16
  end
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Lutaml
4
+ module Xml
5
+ # Mixin for XML element/attribute ordering state.
6
+ #
7
+ # Included by:
8
+ # - Lutaml::Xml::Serialization::InstanceMethods (Serialize instances)
9
+ # - Plain model classes via add_format_specific_model_methods
10
+ #
11
+ # Enables Lutaml::Model to check ordering capability with is_a? instead
12
+ # of respond_to?, following proper OOP type-checking.
13
+ module XmlOrderable
14
+ attr_accessor :element_order, :attribute_order, :ordered, :mixed
15
+ end
16
+ end
17
+ end
data/lib/lutaml/xml.rb CHANGED
@@ -42,16 +42,13 @@ module Lutaml
42
42
  # XML Schema modules
43
43
  autoload :Schema, "#{__dir__}/xml/schema"
44
44
 
45
- # Detect available XML adapter
46
- # @return [Symbol, nil] :nokogiri, :ox, :oga, :rexml, or nil
45
+ # Detect available XML adapter.
46
+ # Delegates to moxml, which is the authority on XML adapter
47
+ # availability and platform constraints.
48
+ #
49
+ # @return [Symbol] adapter type name
47
50
  def self.detect_xml_adapter
48
- return :oga if Lutaml::Model::RuntimeCompatibility.opal?
49
- return :nokogiri if Lutaml::Model::Utils.safe_load("nokogiri", :Nokogiri)
50
- return :ox if Lutaml::Model::Utils.safe_load("ox", :Ox)
51
- return :oga if Lutaml::Model::Utils.safe_load("oga", :Oga)
52
- return :rexml if Lutaml::Model::Utils.safe_load("rexml", :REXML)
53
-
54
- nil
51
+ Moxml::Config.runtime_default_adapter
55
52
  end
56
53
 
57
54
  # Get the current XML adapter
@@ -174,8 +171,8 @@ Lutaml::Model::FormatRegistry.register(
174
171
  ],
175
172
  adapter_options: if Lutaml::Model::RuntimeCompatibility.opal?
176
173
  {
177
- available: %i[oga],
178
- default: :oga,
174
+ available: %i[rexml],
175
+ default: :rexml,
179
176
  }
180
177
  else
181
178
  {
@@ -8,10 +8,8 @@ module Lutaml
8
8
  super(:yaml)
9
9
  end
10
10
 
11
- def deep_dup
12
- self.class.new.tap do |new_mapping|
13
- new_mapping.mappings = duplicate_mappings
14
- end
11
+ def dup_instance
12
+ self.class.new
15
13
  end
16
14
  end
17
15
  end
@@ -4,7 +4,7 @@ module Lutaml
4
4
  module Yamls
5
5
  module Adapter
6
6
  class Mapping < Lutaml::KeyValue::Mapping
7
- attr_reader :yamls_sequence
7
+ attr_accessor :yamls_sequence
8
8
 
9
9
  def initialize
10
10
  super(:yaml)
@@ -15,9 +15,13 @@ module Lutaml
15
15
  @yamls_sequence.instance_eval(&)
16
16
  end
17
17
 
18
+ def dup_instance
19
+ self.class.new
20
+ end
21
+
18
22
  def deep_dup
19
- self.class.new.tap do |new_mapping|
20
- new_mapping.mappings = duplicate_mappings
23
+ super.tap do |new_mapping|
24
+ new_mapping.yamls_sequence = @yamls_sequence&.dup
21
25
  end
22
26
  end
23
27
  end
data/lutaml-model.gemspec CHANGED
@@ -36,7 +36,7 @@ Gem::Specification.new do |spec|
36
36
  spec.add_dependency "canon"
37
37
  spec.add_dependency "concurrent-ruby"
38
38
  spec.add_dependency "liquid", ">= 4.0", "< 6.0"
39
- spec.add_dependency "moxml", ">= 0.1.20"
39
+ spec.add_dependency "moxml", ">= 0.1.22"
40
40
  spec.add_dependency "ostruct"
41
41
  spec.add_dependency "rubyzip", "~> 2.3"
42
42
  spec.add_dependency "thor"