lutaml-model 0.8.22 → 0.8.24
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/performance.yml +1 -1
- data/Gemfile +6 -0
- data/README.adoc +16 -7
- data/docs/_guides/index.adoc +4 -0
- data/docs/_guides/native-engines.adoc +61 -0
- data/docs/_guides/xml-mapping.adoc +31 -0
- data/docs/_pages/configuration.adoc +11 -7
- data/docs/_pages/serialization_adapters.adoc +14 -5
- data/lib/compat/opal/lutaml_model_boot.rb +2 -0
- data/lib/lutaml/json/adapter/multi_json_adapter.rb +12 -1
- data/lib/lutaml/json/adapter/oj_adapter.rb +7 -0
- data/lib/lutaml/json/adapter/standard_adapter.rb +17 -5
- data/lib/lutaml/json/adapter/yeptris_adapter.rb +21 -0
- data/lib/lutaml/json/adapter.rb +1 -0
- data/lib/lutaml/json/format.rb +1 -1
- data/lib/lutaml/json/generator_options.rb +62 -0
- data/lib/lutaml/jsonl/adapter/standard_adapter.rb +1 -1
- data/lib/lutaml/jsonld/adapter.rb +12 -4
- data/lib/lutaml/key_value/adapter/json/multi_json_adapter.rb +16 -1
- data/lib/lutaml/key_value/adapter/json/oj_adapter.rb +7 -0
- data/lib/lutaml/key_value/adapter/json/standard_adapter.rb +17 -5
- data/lib/lutaml/key_value/adapter/json/yeptris_adapter.rb +15 -0
- data/lib/lutaml/key_value/adapter/jsonl/standard_adapter.rb +1 -1
- data/lib/lutaml/key_value/adapter/toml/teptris_adapter.rb +42 -0
- data/lib/lutaml/key_value/adapter/toml.rb +1 -0
- data/lib/lutaml/key_value/adapter/yaml/yeptris_adapter.rb +15 -0
- data/lib/lutaml/key_value/transform.rb +82 -0
- data/lib/lutaml/key_value/transformation/value_serializer.rb +25 -0
- data/lib/lutaml/key_value/transformation.rb +25 -1
- data/lib/lutaml/model/adapter_resolver.rb +23 -6
- data/lib/lutaml/model/attribute.rb +31 -6
- data/lib/lutaml/model/collection.rb +5 -0
- data/lib/lutaml/model/configuration.rb +5 -0
- data/lib/lutaml/model/error/unknown_adapter_type_error.rb +4 -4
- data/lib/lutaml/model/serialization_adapter.rb +1 -1
- data/lib/lutaml/model/serialize/format_conversion.rb +75 -7
- data/lib/lutaml/model/serialize.rb +18 -0
- data/lib/lutaml/model/toml.rb +10 -6
- data/lib/lutaml/model/transformation.rb +5 -0
- data/lib/lutaml/model/type/date_time.rb +2 -2
- data/lib/lutaml/model/type/value.rb +24 -2
- data/lib/lutaml/model/utils.rb +6 -5
- data/lib/lutaml/model/version.rb +1 -1
- data/lib/lutaml/toml/adapter/teptris_adapter.rb +14 -0
- data/lib/lutaml/toml/adapter.rb +1 -0
- data/lib/lutaml/toml/format.rb +1 -1
- data/lib/lutaml/xml/adapter/leptris_adapter.rb +15 -0
- data/lib/lutaml/xml/adapter_element.rb +111 -3
- data/lib/lutaml/xml/builder/base.rb +3 -1
- data/lib/lutaml/xml/builder/leptris.rb +13 -0
- data/lib/lutaml/xml/builder/nokogiri.rb +3 -0
- data/lib/lutaml/xml/builder.rb +1 -0
- data/lib/lutaml/xml/format.rb +4 -1
- data/lib/lutaml/xml/leptris/element.rb +34 -0
- data/lib/lutaml/xml/mapping.rb +24 -0
- data/lib/lutaml/xml/model_transform.rb +52 -1
- data/lib/lutaml/xml/plan_compiler.rb +139 -0
- data/lib/lutaml/xml/plan_hydrator.rb +80 -0
- data/lib/lutaml/xml/transformation/custom_method_wrapper.rb +8 -0
- data/lib/lutaml/xml/transformation.rb +29 -0
- data/lib/lutaml/xml/xml_element.rb +6 -2
- data/lib/lutaml/yaml/adapter/yeptris_adapter.rb +40 -0
- data/lib/lutaml/yaml/adapter.rb +5 -0
- data/lib/lutaml/yaml/format.rb +1 -1
- data/lib/tasks/performance.rake +5 -0
- data/lib/tasks/performance_comparator.rb +19 -3
- data/lib/tasks/performance_probe.rake +33 -0
- data/lib/tasks/performance_probe.rb +106 -0
- data/lib/tasks/performance_probe_xml.rb +54 -0
- data/lutaml-model.gemspec +1 -1
- data/spec/lutaml/json/adapter/standard_adapter_spec.rb +159 -0
- data/spec/lutaml/json/yeptris_adapter_spec.rb +63 -0
- data/spec/lutaml/jsonld/adapter_spec.rb +19 -0
- data/spec/lutaml/key_value/adapter/toml/teptris_adapter_spec.rb +71 -0
- data/spec/lutaml/model/collection_spec.rb +1 -1
- data/spec/lutaml/model/collection_state_nesting_spec.rb +44 -0
- data/spec/lutaml/model/generator_defaults_spec.rb +101 -0
- data/spec/lutaml/model/json_adapter_spec.rb +6 -0
- data/spec/lutaml/model/json_state_nesting_spec.rb +82 -0
- data/spec/lutaml/model/toml_adapter_spec.rb +8 -0
- data/spec/lutaml/model/toml_spec.rb +36 -1
- data/spec/lutaml/xml/plan_fast_path_spec.rb +96 -0
- data/spec/lutaml/xml/shared_element_rules_spec.rb +64 -0
- data/spec/lutaml/xml/xml_spec.rb +13 -0
- data/spec/lutaml/yaml/yeptris_adapter_spec.rb +63 -0
- data/spec/spec_helper.rb +7 -1
- metadata +29 -4
data/lib/lutaml/model/toml.rb
CHANGED
|
@@ -7,6 +7,7 @@ module Lutaml
|
|
|
7
7
|
module Toml
|
|
8
8
|
Lutaml::Model::RuntimeCompatibility.define_native_aliases(
|
|
9
9
|
self,
|
|
10
|
+
TeptrisAdapter: "::Lutaml::Toml::Adapter::TeptrisAdapter",
|
|
10
11
|
TomlibAdapter: "::Lutaml::Toml::Adapter::TomlibAdapter",
|
|
11
12
|
TomlRbAdapter: "::Lutaml::Toml::Adapter::TomlRbAdapter",
|
|
12
13
|
)
|
|
@@ -18,15 +19,18 @@ module Lutaml
|
|
|
18
19
|
def self.detect_toml_adapter
|
|
19
20
|
return nil if Lutaml::Model.opal?
|
|
20
21
|
|
|
21
|
-
#
|
|
22
|
-
|
|
23
|
-
|
|
22
|
+
# teptris 0.2.12+ parses ~3x and dumps ~8x faster than tomlib
|
|
23
|
+
# and ships prebuilts for every platform (mingw-ucrt restored
|
|
24
|
+
# in 0.2.4+) — native TOML on Windows retires the tomlib
|
|
25
|
+
# segfault workaround (previously pure-Ruby toml-rb only).
|
|
26
|
+
return :teptris if Lutaml::Model::Utils.safe_load("teptris", :Teptris)
|
|
24
27
|
|
|
25
|
-
|
|
28
|
+
# tomlib segfaults on Windows when parsing invalid TOML
|
|
29
|
+
if !Lutaml::Model::RuntimeCompatibility.windows? &&
|
|
30
|
+
Lutaml::Model::Utils.safe_load("tomlib", :Tomlib)
|
|
31
|
+
return :tomlib
|
|
26
32
|
end
|
|
27
33
|
|
|
28
|
-
# On non-Windows, prefer tomlib
|
|
29
|
-
return :tomlib if Lutaml::Model::Utils.safe_load("tomlib", :Tomlib)
|
|
30
34
|
return :toml_rb if Lutaml::Model::Utils.safe_load("toml-rb", :TomlRb)
|
|
31
35
|
|
|
32
36
|
nil
|
|
@@ -37,9 +37,14 @@ module Lutaml
|
|
|
37
37
|
@format = format
|
|
38
38
|
@register = register
|
|
39
39
|
@compiled_rules = compile_rules(mapping_dsl)
|
|
40
|
+
after_compile
|
|
40
41
|
freeze
|
|
41
42
|
end
|
|
42
43
|
|
|
44
|
+
# Hook for format subclasses to derive rule-set-level data after
|
|
45
|
+
# compilation, before the transformation is frozen and shared.
|
|
46
|
+
def after_compile; end
|
|
47
|
+
|
|
43
48
|
# Transform a model instance into format-specific representation
|
|
44
49
|
#
|
|
45
50
|
# @abstract Subclasses must implement this method
|
|
@@ -2,6 +2,16 @@
|
|
|
2
2
|
|
|
3
3
|
module Lutaml
|
|
4
4
|
module Model
|
|
5
|
+
# Isolated holder for format_type_serializer_for's memo: Type::Value
|
|
6
|
+
# can be frozen by suites, which would freeze an ivar Hash with it.
|
|
7
|
+
SERIALIZER_LOOKUP_CACHE = ::Class.new do
|
|
8
|
+
class << self
|
|
9
|
+
def cache
|
|
10
|
+
@cache ||= {}
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
end.cache
|
|
14
|
+
|
|
5
15
|
module Type
|
|
6
16
|
# Base class for all value types
|
|
7
17
|
class Value
|
|
@@ -35,14 +45,26 @@ from: nil)
|
|
|
35
45
|
# @param type_class [Class] the type class to look up
|
|
36
46
|
# @return [Hash, nil] { to: Proc, from: Proc } or nil
|
|
37
47
|
def format_type_serializer_for(format, type_class)
|
|
48
|
+
# Hot path: memoize per [format, resolved class] — the hierarchy
|
|
49
|
+
# walk below only runs on a miss. Format serializers register at
|
|
50
|
+
# load time, so a hit is stable.
|
|
51
|
+
cache = SERIALIZER_LOOKUP_CACHE
|
|
52
|
+
key = [format, type_class]
|
|
53
|
+
return cache[key] if cache.key?(key)
|
|
54
|
+
|
|
38
55
|
klass = type_class
|
|
56
|
+
found = nil
|
|
39
57
|
while klass && klass <= Value
|
|
40
58
|
s = @format_type_serializers[[format, klass]]
|
|
41
|
-
|
|
59
|
+
if s
|
|
60
|
+
found = s
|
|
61
|
+
break
|
|
62
|
+
end
|
|
42
63
|
|
|
43
64
|
klass = klass.superclass
|
|
44
65
|
end
|
|
45
|
-
|
|
66
|
+
cache[key] = found
|
|
67
|
+
found
|
|
46
68
|
end
|
|
47
69
|
end
|
|
48
70
|
|
data/lib/lutaml/model/utils.rb
CHANGED
|
@@ -121,11 +121,12 @@ module Lutaml
|
|
|
121
121
|
end
|
|
122
122
|
|
|
123
123
|
def blank?(value)
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
124
|
+
# Hot path: the nil test is a pointer compare, and nil dominates
|
|
125
|
+
# in optional-field-heavy documents.
|
|
126
|
+
return true if value.nil?
|
|
127
|
+
return false unless value.is_a?(::String) || value.is_a?(::Array) || value.is_a?(::Hash)
|
|
128
|
+
|
|
129
|
+
value.empty?
|
|
129
130
|
end
|
|
130
131
|
|
|
131
132
|
def empty_collection?(collection)
|
data/lib/lutaml/model/version.rb
CHANGED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../../key_value/adapter/toml/teptris_adapter"
|
|
4
|
+
|
|
5
|
+
module Lutaml
|
|
6
|
+
module Toml
|
|
7
|
+
module Adapter
|
|
8
|
+
# The implementation lives in the KeyValue namespace (the live
|
|
9
|
+
# surface for adapter resolution); this alias keeps the legacy
|
|
10
|
+
# Lutaml::Toml::Adapter surface complete.
|
|
11
|
+
TeptrisAdapter = Lutaml::KeyValue::Adapter::Toml::TeptrisAdapter
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
end
|
data/lib/lutaml/toml/adapter.rb
CHANGED
|
@@ -11,6 +11,7 @@ module Lutaml
|
|
|
11
11
|
autoload :Transform, "#{__dir__}/adapter/transform"
|
|
12
12
|
Lutaml::Model::RuntimeCompatibility.autoload_native(
|
|
13
13
|
self,
|
|
14
|
+
TeptrisAdapter: "#{__dir__}/adapter/teptris_adapter",
|
|
14
15
|
TomlRbAdapter: "#{__dir__}/adapter/toml_rb_adapter",
|
|
15
16
|
TomlibAdapter: "#{__dir__}/adapter/tomlib_adapter",
|
|
16
17
|
)
|
data/lib/lutaml/toml/format.rb
CHANGED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "moxml/adapter/leptris"
|
|
4
|
+
|
|
5
|
+
module Lutaml
|
|
6
|
+
module Xml
|
|
7
|
+
module Adapter
|
|
8
|
+
class LeptrisAdapter < BaseAdapter
|
|
9
|
+
MOXML_ADAPTER = Moxml::Adapter::Leptris
|
|
10
|
+
BUILDER_CLASS = Builder::Leptris
|
|
11
|
+
PARSED_ELEMENT_CLASS = Lutaml::Xml::LeptrisElement
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
end
|
|
@@ -22,7 +22,11 @@ module Lutaml
|
|
|
22
22
|
text = case node
|
|
23
23
|
when Moxml::Element
|
|
24
24
|
namespace_name = node.namespace&.prefix
|
|
25
|
-
|
|
25
|
+
# moxml >= 0.5.33 contract: #namespaces is the full
|
|
26
|
+
# in-scope map (parity with Nokogiri, #198/#201); the
|
|
27
|
+
# own-declarations shape this parse path needs is
|
|
28
|
+
# #namespace_definitions.
|
|
29
|
+
ns_defs = node.namespace_definitions
|
|
26
30
|
|
|
27
31
|
has_empty_xmlns = ns_defs.any? do |ns|
|
|
28
32
|
ns.prefix.nil? && ns.uri == ""
|
|
@@ -172,14 +176,118 @@ module Lutaml
|
|
|
172
176
|
def parse_children(node, default_namespace: nil)
|
|
173
177
|
return [] unless node.children
|
|
174
178
|
|
|
179
|
+
# Non-element children (text/cdata/comment/PI) stay RAW moxml
|
|
180
|
+
# nodes in the list — the hot scalar parse path reads text off
|
|
181
|
+
# the element (inner_text) and never touches their wrappers, so
|
|
182
|
+
# wrapping every text node eagerly paid half the parse tree for
|
|
183
|
+
# nothing. #children wraps them on first access; document order
|
|
184
|
+
# is preserved because they sit at their original positions.
|
|
175
185
|
node.children.filter_map do |child|
|
|
176
186
|
next if (child.is_a?(Moxml::Text) || child.is_a?(Moxml::Cdata)) && child.content.empty?
|
|
177
187
|
|
|
178
|
-
|
|
179
|
-
|
|
188
|
+
if child.is_a?(Moxml::Element)
|
|
189
|
+
self.class.new(child, parent: self,
|
|
190
|
+
default_namespace: default_namespace)
|
|
191
|
+
else
|
|
192
|
+
child
|
|
193
|
+
end
|
|
180
194
|
end
|
|
181
195
|
end
|
|
182
196
|
|
|
197
|
+
public
|
|
198
|
+
|
|
199
|
+
def children
|
|
200
|
+
unless @non_element_children_wrapped
|
|
201
|
+
@children.map! do |child|
|
|
202
|
+
if child.is_a?(Moxml::Node) && !child.is_a?(Moxml::Element)
|
|
203
|
+
self.class.new(child, parent: self)
|
|
204
|
+
else
|
|
205
|
+
child
|
|
206
|
+
end
|
|
207
|
+
end
|
|
208
|
+
@non_element_children_wrapped = true
|
|
209
|
+
end
|
|
210
|
+
@children
|
|
211
|
+
end
|
|
212
|
+
|
|
213
|
+
def children=(new_children)
|
|
214
|
+
@non_element_children_wrapped = true
|
|
215
|
+
super
|
|
216
|
+
end
|
|
217
|
+
|
|
218
|
+
def element_children
|
|
219
|
+
return @element_children if defined?(@element_children)
|
|
220
|
+
|
|
221
|
+
@element_children = @children.reject do |child|
|
|
222
|
+
raw_non_element_child?(child) ||
|
|
223
|
+
(child.is_a?(XmlElement) &&
|
|
224
|
+
(child.text? || child.processing_instruction?))
|
|
225
|
+
end
|
|
226
|
+
end
|
|
227
|
+
|
|
228
|
+
def raw_non_element_child?(child)
|
|
229
|
+
child.is_a?(Moxml::Node) && !child.is_a?(Moxml::Element)
|
|
230
|
+
end
|
|
231
|
+
|
|
232
|
+
# Order consumes raw non-element children inline: wrapping them
|
|
233
|
+
# just to read .text/.content would materialize the whole lazy
|
|
234
|
+
# layer on every parse.
|
|
235
|
+
def order
|
|
236
|
+
return @order_cache if @order_cache
|
|
237
|
+
|
|
238
|
+
@order_cache = @children.filter_map do |child|
|
|
239
|
+
case child
|
|
240
|
+
when Moxml::Cdata
|
|
241
|
+
Lutaml::Xml::Element.new("Text", "#cdata-section",
|
|
242
|
+
text_content: child.content,
|
|
243
|
+
node_type: :cdata)
|
|
244
|
+
when Moxml::Text
|
|
245
|
+
next if child.content.nil?
|
|
246
|
+
|
|
247
|
+
Lutaml::Xml::Element.new("Text", "text",
|
|
248
|
+
text_content: child.content,
|
|
249
|
+
node_type: :text)
|
|
250
|
+
when Moxml::Comment
|
|
251
|
+
Lutaml::Xml::Element.new("Comment", "comment",
|
|
252
|
+
text_content: child.content,
|
|
253
|
+
node_type: :comment)
|
|
254
|
+
when Moxml::ProcessingInstruction
|
|
255
|
+
Lutaml::Xml::Element.new("ProcessingInstruction",
|
|
256
|
+
child.target,
|
|
257
|
+
text_content: child.content.to_s.sub(/\A\s+/, ""),
|
|
258
|
+
node_type: :processing_instruction)
|
|
259
|
+
else
|
|
260
|
+
next if child.is_a?(Moxml::Node)
|
|
261
|
+
|
|
262
|
+
if child.cdata?
|
|
263
|
+
Lutaml::Xml::Element.new("Text", "#cdata-section",
|
|
264
|
+
text_content: child.text,
|
|
265
|
+
node_type: :cdata)
|
|
266
|
+
elsif child.text?
|
|
267
|
+
next if child.text.nil?
|
|
268
|
+
|
|
269
|
+
Lutaml::Xml::Element.new("Text", "text",
|
|
270
|
+
text_content: child.text,
|
|
271
|
+
node_type: :text)
|
|
272
|
+
elsif child.comment?
|
|
273
|
+
Lutaml::Xml::Element.new("Comment", "comment",
|
|
274
|
+
text_content: child.text,
|
|
275
|
+
node_type: :comment)
|
|
276
|
+
elsif child.processing_instruction?
|
|
277
|
+
Lutaml::Xml::Element.new("ProcessingInstruction",
|
|
278
|
+
child.unprefixed_name,
|
|
279
|
+
text_content: child.text,
|
|
280
|
+
node_type: :processing_instruction)
|
|
281
|
+
else
|
|
282
|
+
Lutaml::Xml::Element.new("Element", child.unprefixed_name,
|
|
283
|
+
node_type: :element,
|
|
284
|
+
namespace_uri: child.namespace_uri,
|
|
285
|
+
namespace_prefix: child.namespace_prefix)
|
|
286
|
+
end
|
|
287
|
+
end
|
|
288
|
+
end.each(&:freeze).freeze
|
|
289
|
+
end
|
|
290
|
+
|
|
183
291
|
def add_namespaces_from_defs(ns_defs, is_root: false)
|
|
184
292
|
has_default_xmlns = is_root || ns_defs.any? { |ns| ns.prefix.nil? }
|
|
185
293
|
|
|
@@ -223,7 +223,9 @@ module Lutaml
|
|
|
223
223
|
ns = new_el.in_scope_namespaces.find { |n| n.prefix == prefix }
|
|
224
224
|
new_el.namespace = ns if ns
|
|
225
225
|
elsif !prefix_unset && prefix.nil?
|
|
226
|
-
|
|
226
|
+
# Skip the no-op clear: namespace-less elements pay the
|
|
227
|
+
# undeclare path on some engines (leptris) for nothing.
|
|
228
|
+
new_el.namespace = nil unless new_el.namespace.nil?
|
|
227
229
|
end
|
|
228
230
|
end
|
|
229
231
|
|
data/lib/lutaml/xml/builder.rb
CHANGED
data/lib/lutaml/xml/format.rb
CHANGED
|
@@ -21,6 +21,8 @@ end
|
|
|
21
21
|
|
|
22
22
|
module Lutaml
|
|
23
23
|
module Xml
|
|
24
|
+
autoload :PlanCompiler, "#{__dir__}/plan_compiler"
|
|
25
|
+
autoload :PlanHydrator, "#{__dir__}/plan_hydrator"
|
|
24
26
|
# Error module for XML-specific errors
|
|
25
27
|
module Error
|
|
26
28
|
autoload :XmlError, "#{__dir__}/error/xml_error"
|
|
@@ -161,6 +163,7 @@ module Lutaml
|
|
|
161
163
|
NokogiriElement: "#{__dir__}/nokogiri/element",
|
|
162
164
|
Nokogiri: "#{__dir__}/nokogiri",
|
|
163
165
|
Ox: "#{__dir__}/ox",
|
|
166
|
+
LeptrisElement: "#{__dir__}/leptris/element",
|
|
164
167
|
)
|
|
165
168
|
end
|
|
166
169
|
end
|
|
@@ -192,7 +195,7 @@ Lutaml::Model::FormatRegistry.register(
|
|
|
192
195
|
}
|
|
193
196
|
else
|
|
194
197
|
{
|
|
195
|
-
available: %i[nokogiri ox oga rexml],
|
|
198
|
+
available: %i[nokogiri ox oga rexml leptris],
|
|
196
199
|
default: :nokogiri,
|
|
197
200
|
}
|
|
198
201
|
end,
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Lutaml
|
|
4
|
+
module Xml
|
|
5
|
+
class LeptrisElement < AdapterElement
|
|
6
|
+
def initialize(node, parent: nil, default_namespace: nil)
|
|
7
|
+
@raw_text = node.raw_content if node.is_a?(Moxml::Text)
|
|
8
|
+
super
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
private
|
|
12
|
+
|
|
13
|
+
def adapter_class
|
|
14
|
+
Lutaml::Xml::Adapter::LeptrisAdapter
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def build_text_for_xml
|
|
18
|
+
@raw_text || @text
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def build_element_xml(builder)
|
|
22
|
+
builder.create_and_add_element(name,
|
|
23
|
+
prefix: namespace_prefix,
|
|
24
|
+
attributes: build_attributes(self)) do |xml|
|
|
25
|
+
children.each { |child| child.build_xml(xml) }
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def attribute_value_for_build(attr)
|
|
30
|
+
attr.raw_value
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
data/lib/lutaml/xml/mapping.rb
CHANGED
|
@@ -1071,6 +1071,30 @@ module Lutaml
|
|
|
1071
1071
|
result
|
|
1072
1072
|
end
|
|
1073
1073
|
|
|
1074
|
+
# lutaml-model#765: plain element rules grouped by target attribute
|
|
1075
|
+
# name, for spelling-tolerance mapping (several element names feeding
|
|
1076
|
+
# one attribute). Cached per register so the parse hot path only pays
|
|
1077
|
+
# a hash lookup. Grouping uses rule-level properties only — the
|
|
1078
|
+
# transform applies the collection-attribute filter.
|
|
1079
|
+
#
|
|
1080
|
+
# @return [Hash{Symbol, String => Array<MappingRule>}]
|
|
1081
|
+
def plain_element_rules_by_attr(register_id = nil)
|
|
1082
|
+
reg_key = register_id || :default
|
|
1083
|
+
@plain_element_rules_by_attr ||= {}
|
|
1084
|
+
@plain_element_rules_by_attr[reg_key] ||= begin
|
|
1085
|
+
grouped = {}
|
|
1086
|
+
mappings(reg_key).each do |r|
|
|
1087
|
+
next if r.attribute? || r.raw_mapping? || r.content_mapping? ||
|
|
1088
|
+
r.cdata || r.has_custom_method_for_deserialization? ||
|
|
1089
|
+
(r.transform.is_a?(Hash) && !r.transform.empty?) ||
|
|
1090
|
+
r.transform.is_a?(Class)
|
|
1091
|
+
|
|
1092
|
+
(grouped[r.to] ||= []) << r
|
|
1093
|
+
end
|
|
1094
|
+
grouped
|
|
1095
|
+
end
|
|
1096
|
+
end
|
|
1097
|
+
|
|
1074
1098
|
def importable_mappings
|
|
1075
1099
|
@importable_mappings ||= []
|
|
1076
1100
|
end
|
|
@@ -258,6 +258,18 @@ module Lutaml
|
|
|
258
258
|
end
|
|
259
259
|
end
|
|
260
260
|
|
|
261
|
+
# lutaml-model#765: several plain element rules may target one
|
|
262
|
+
# collection attribute (spelling tolerance, e.g. editorial-group vs
|
|
263
|
+
# editorialgroup). The first rule of such a group performs matching
|
|
264
|
+
# widened to every spelling so content merges in document order;
|
|
265
|
+
# the remaining rules are no-ops so their assignment cannot
|
|
266
|
+
# silently overwrite the merged value.
|
|
267
|
+
# Grouping is cached on the mapping DSL (per register); only the
|
|
268
|
+
# collection filter runs here.
|
|
269
|
+
grouped_plain_rules = xml_mapping.plain_element_rules_by_attr(
|
|
270
|
+
effective_register,
|
|
271
|
+
)
|
|
272
|
+
|
|
261
273
|
mappings.each do |rule|
|
|
262
274
|
# Performance: Cache rule properties accessed multiple times
|
|
263
275
|
rule.name
|
|
@@ -268,6 +280,11 @@ module Lutaml
|
|
|
268
280
|
attr = attribute_for_rule(rule)
|
|
269
281
|
next if attr&.derived?
|
|
270
282
|
|
|
283
|
+
if (group = grouped_plain_rules[rule_to]) &&
|
|
284
|
+
group.size > 1 && attr&.collection? && !group.first.equal?(rule)
|
|
285
|
+
next
|
|
286
|
+
end
|
|
287
|
+
|
|
271
288
|
raise "Attribute '#{rule_to}' not found in #{context}" unless valid_rule?(
|
|
272
289
|
rule, attr
|
|
273
290
|
)
|
|
@@ -285,6 +302,26 @@ module Lutaml
|
|
|
285
302
|
doc.root.inner_xml
|
|
286
303
|
elsif rule.content_mapping?
|
|
287
304
|
rule.cdata ? doc.cdata : doc.text
|
|
305
|
+
elsif (group = grouped_plain_rules[rule_to]) &&
|
|
306
|
+
group.size > 1 && attr&.collection?
|
|
307
|
+
# First rule of a #765 group: match every spelling of
|
|
308
|
+
# the group so the children merge in document order.
|
|
309
|
+
if child_names_set &&
|
|
310
|
+
group.drop(1).none? do |s|
|
|
311
|
+
child_matches_rule?(s, child_names_set,
|
|
312
|
+
default_namespace)
|
|
313
|
+
end && !child_matches_rule?(rule, child_names_set,
|
|
314
|
+
default_namespace)
|
|
315
|
+
::Lutaml::Model::UninitializedClass.instance
|
|
316
|
+
else
|
|
317
|
+
extra_names = group.drop(1).flat_map do |s|
|
|
318
|
+
resolve_rule_names_with_type(s, attr, new_opts,
|
|
319
|
+
effective_register,
|
|
320
|
+
attr&.type(effective_register))
|
|
321
|
+
end
|
|
322
|
+
value_for_rule(session, rule, new_opts, attr,
|
|
323
|
+
extra_names)
|
|
324
|
+
end
|
|
288
325
|
elsif child_names_set && !rule.attribute? &&
|
|
289
326
|
!child_matches_rule?(rule, child_names_set,
|
|
290
327
|
default_namespace)
|
|
@@ -551,7 +588,8 @@ _effective_register)
|
|
|
551
588
|
nil
|
|
552
589
|
end
|
|
553
590
|
|
|
554
|
-
def value_for_rule(session, rule, options, cached_attr = nil
|
|
591
|
+
def value_for_rule(session, rule, options, cached_attr = nil,
|
|
592
|
+
extra_rule_names = nil)
|
|
555
593
|
doc = session.doc
|
|
556
594
|
instance = session.instance
|
|
557
595
|
instance_is_serialize = session.instance_is_serialize
|
|
@@ -576,6 +614,12 @@ _effective_register)
|
|
|
576
614
|
resolve_rule_names_with_type(rule, attr, options,
|
|
577
615
|
effective_register, attr_type)
|
|
578
616
|
end
|
|
617
|
+
# lutaml-model#765: widened matching for the first rule of a
|
|
618
|
+
# shared-attribute group — children of sibling spellings must be
|
|
619
|
+
# selected too, in document order. Deduped: sibling resolution can
|
|
620
|
+
# yield the same spelling twice, and the child index would then
|
|
621
|
+
# return the same children again.
|
|
622
|
+
rule_names = rule_names.concat(extra_rule_names).uniq if extra_rule_names
|
|
579
623
|
|
|
580
624
|
return value_for_xml_attribute(doc, rule, rule_names) if rule.attribute?
|
|
581
625
|
|
|
@@ -643,6 +687,13 @@ _effective_register)
|
|
|
643
687
|
children = nil # fall through to select for alias matching
|
|
644
688
|
else
|
|
645
689
|
children = indexed.flatten(1)
|
|
690
|
+
# #765 widened matching: rule_names spans several spellings, so
|
|
691
|
+
# the flatten above yields rule-name order; restore document
|
|
692
|
+
# order for the merged group.
|
|
693
|
+
if extra_rule_names
|
|
694
|
+
position = element_children.each_with_index.to_h
|
|
695
|
+
children = children.sort_by { |c| position[c] }
|
|
696
|
+
end
|
|
646
697
|
end
|
|
647
698
|
else
|
|
648
699
|
children = nil
|