lutaml-model 0.8.19 → 0.8.23
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 +1 -1
- data/.github/workflows/rake.yml +1 -0
- data/.rubocop.yml +1 -1
- data/.rubocop_todo.yml +133 -72
- data/bench/bench_common.rb +8 -5
- data/bench/bench_niso.rb +1 -2
- data/bench/gate_config.rb +4 -1
- data/benchmark/serialization_benchmark.rb +4 -4
- data/docs/_guides/value-transformations.adoc +27 -0
- data/docs/_guides/xml-mapping.adoc +31 -0
- data/lib/compat/opal/generate_boot.rb +3 -1
- data/lib/compat/opal/lutaml_model_boot.rb +10 -7
- data/lib/compat/opal/moxml_boot.rb +6 -1
- data/lib/lutaml/hash_format/format.rb +29 -0
- data/lib/lutaml/hash_format.rb +5 -27
- data/lib/lutaml/json/adapter/oj_adapter.rb +7 -0
- data/lib/lutaml/json/adapter/standard_adapter.rb +17 -5
- data/lib/lutaml/json/format.rb +61 -0
- data/lib/lutaml/json/generator_options.rb +62 -0
- data/lib/lutaml/json.rb +4 -59
- data/lib/lutaml/jsonl/adapter/standard_adapter.rb +1 -1
- data/lib/lutaml/jsonl/format.rb +22 -0
- data/lib/lutaml/jsonl.rb +5 -19
- data/lib/lutaml/jsonld/adapter.rb +12 -4
- 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/jsonl/standard_adapter.rb +1 -1
- data/lib/lutaml/key_value/format.rb +26 -0
- data/lib/lutaml/key_value/transform.rb +4 -1
- data/lib/lutaml/key_value/transformation/value_serializer.rb +0 -2
- data/lib/lutaml/key_value.rb +4 -21
- data/lib/lutaml/model/attribute.rb +38 -4
- data/lib/lutaml/model/collection.rb +7 -2
- data/lib/lutaml/model/compiled_rule.rb +1 -1
- data/lib/lutaml/model/config.rb +2 -2
- data/lib/lutaml/model/consolidation_map.rb +2 -2
- data/lib/lutaml/model/consolidation_rule/dispatch_block.rb +2 -2
- data/lib/lutaml/model/errors.rb +2 -2
- data/lib/lutaml/model/global_context.rb +4 -2
- data/lib/lutaml/model/mapping/mapping_rule.rb +4 -1
- data/lib/lutaml/model/mapping/model_mapping.rb +2 -2
- data/lib/lutaml/model/runtime_compatibility.rb +2 -2
- data/lib/lutaml/model/schema/class_loader.rb +2 -1
- data/lib/lutaml/model/schema/decorators/definition_collection.rb +4 -4
- data/lib/lutaml/model/schema/definitions/namespace.rb +2 -1
- data/lib/lutaml/model/schema/file_writer.rb +5 -2
- data/lib/lutaml/model/schema/renderers/base.rb +4 -3
- data/lib/lutaml/model/schema/renderers/mappings.rb +9 -3
- data/lib/lutaml/model/schema/renderers/member_decls.rb +9 -3
- data/lib/lutaml/model/schema/renderers/model.rb +18 -6
- data/lib/lutaml/model/schema/renderers/required_files_calculator.rb +2 -1
- data/lib/lutaml/model/schema/renderers.rb +2 -1
- data/lib/lutaml/model/schema/rnc_compiler/source_resolver.rb +6 -2
- data/lib/lutaml/model/schema/rnc_compiler.rb +3 -1
- data/lib/lutaml/model/schema/rng_compiler/element_visitor.rb +35 -15
- data/lib/lutaml/model/schema/rng_compiler/value_type_resolver.rb +4 -2
- data/lib/lutaml/model/schema/rng_compiler.rb +10 -5
- data/lib/lutaml/model/schema/xml_compiler/element_order.rb +2 -1
- data/lib/lutaml/model/schema/xml_compiler/spec_builder/complex_types.rb +4 -2
- data/lib/lutaml/model/schema/xml_compiler/spec_builder/members.rb +39 -13
- data/lib/lutaml/model/schema/xml_compiler/spec_builder.rb +29 -10
- data/lib/lutaml/model/schema/xml_compiler/supported_data_types.rb +2 -2
- data/lib/lutaml/model/schema/xml_compiler.rb +10 -5
- data/lib/lutaml/model/sequence.rb +4 -4
- data/lib/lutaml/model/serialization_adapter.rb +1 -1
- data/lib/lutaml/model/serialize/attribute_definition.rb +1 -0
- data/lib/lutaml/model/serialize/format_conversion.rb +40 -9
- data/lib/lutaml/model/serialize/initialization.rb +9 -1
- data/lib/lutaml/model/serialize/model_import.rb +1 -0
- data/lib/lutaml/model/serialize.rb +22 -3
- data/lib/lutaml/model/transformation.rb +5 -0
- data/lib/lutaml/model/type/uninitialized_class_guard.rb +1 -1
- data/lib/lutaml/model/type.rb +9 -0
- data/lib/lutaml/model/type_registry.rb +2 -2
- data/lib/lutaml/model/uninitialized_class.rb +1 -1
- data/lib/lutaml/model/union.rb +5 -2
- data/lib/lutaml/model/utils.rb +2 -2
- data/lib/lutaml/model/value_policy.rb +27 -0
- data/lib/lutaml/model/version.rb +1 -1
- data/lib/lutaml/model.rb +17 -16
- data/lib/lutaml/rdf/mapping.rb +2 -2
- data/lib/lutaml/rdf/namespace_set.rb +2 -2
- data/lib/lutaml/rdf/transform.rb +2 -2
- data/lib/lutaml/toml/format.rb +48 -0
- data/lib/lutaml/toml.rb +5 -46
- data/lib/lutaml/xml/adapter/base_adapter.rb +2 -2
- data/lib/lutaml/xml/builder/base.rb +2 -2
- data/lib/lutaml/xml/format.rb +331 -0
- data/lib/lutaml/xml/mapping.rb +26 -2
- data/lib/lutaml/xml/mapping_rule.rb +62 -3
- data/lib/lutaml/xml/model_transform.rb +183 -55
- data/lib/lutaml/xml/namespace_collector.rb +3 -3
- data/lib/lutaml/xml/parse_session.rb +59 -0
- data/lib/lutaml/xml/parsed_namespace_set.rb +2 -2
- data/lib/lutaml/xml/schema/builder.rb +5 -5
- data/lib/lutaml/xml/schema/xsd/schema_location_mapping.rb +2 -2
- data/lib/lutaml/xml/schema/xsd.rb +0 -2
- data/lib/lutaml/xml/schema/xsd_schema.rb +2 -1
- data/lib/lutaml/xml/serialization/format_conversion.rb +3 -3
- data/lib/lutaml/xml/transformation/element_builder.rb +2 -1
- data/lib/lutaml/xml/transformation/ordered_applier.rb +2 -1
- data/lib/lutaml/xml/transformation.rb +29 -0
- data/lib/lutaml/xml/xml_element.rb +2 -0
- data/lib/lutaml/xml.rb +4 -328
- data/lib/lutaml/yaml/format.rb +30 -0
- data/lib/lutaml/yaml.rb +5 -28
- data/lib/lutaml/yamls/adapter/mapping.rb +2 -2
- data/lib/lutaml/yamls/format.rb +22 -0
- data/lib/lutaml/yamls.rb +5 -19
- data/lib/tasks/benchmark_runner.rb +2 -2
- data/lutaml-model.gemspec +1 -1
- data/spec/lutaml/json/adapter/standard_adapter_spec.rb +159 -0
- data/spec/lutaml/jsonld/adapter_spec.rb +19 -0
- data/spec/lutaml/model/attribute_apply_value_map_spec.rb +26 -13
- data/spec/lutaml/model/attribute_spec.rb +8 -3
- data/spec/lutaml/model/collection_spec.rb +1 -1
- data/spec/lutaml/model/collection_state_nesting_spec.rb +44 -0
- data/spec/lutaml/model/custom_bibtex_adapter_spec.rb +2 -2
- data/spec/lutaml/model/custom_from_empty_values_spec.rb +74 -0
- data/spec/lutaml/model/custom_vobject_adapter_spec.rb +8 -8
- data/spec/lutaml/model/defaults_spec.rb +45 -0
- data/spec/lutaml/model/generator_defaults_spec.rb +101 -0
- data/spec/lutaml/model/json_spec.rb +1 -2
- data/spec/lutaml/model/json_state_nesting_spec.rb +82 -0
- data/spec/lutaml/model/serialize/builder_spec.rb +6 -3
- data/spec/lutaml/model/store_spec.rb +3 -1
- data/spec/lutaml/model/transform_apply_value_map_spec.rb +6 -3
- data/spec/lutaml/model/union_attribute_spec.rb +4 -2
- data/spec/lutaml/model/value_policy_spec.rb +82 -0
- data/spec/lutaml/require_load_order_spec.rb +25 -0
- data/spec/lutaml/xml/attribute_element_namespace_disjoint_spec.rb +88 -0
- data/spec/lutaml/xml/attribute_namespace_disjointness_spec.rb +125 -0
- data/spec/lutaml/xml/out_of_namespace_parsing_spec.rb +123 -0
- data/spec/lutaml/xml/schema/rnc_compiler_spec.rb +17 -6
- data/spec/lutaml/xml/schema/rng_compiler_spec.rb +15 -6
- data/spec/lutaml/xml/shared_element_rules_spec.rb +64 -0
- data/spec/lutaml/xml/w3c_types_spec.rb +3 -6
- metadata +26 -4
|
@@ -183,6 +183,34 @@ module Lutaml
|
|
|
183
183
|
result
|
|
184
184
|
end
|
|
185
185
|
|
|
186
|
+
# Match names an attribute rule consumes during parsing, mirroring how
|
|
187
|
+
# serialization qualifies the same attribute (resolve_namespace):
|
|
188
|
+
# a rule serialized with a prefix matches only attributes in its
|
|
189
|
+
# namespace; a rule serialized unprefixed matches only namespace-less
|
|
190
|
+
# attributes.
|
|
191
|
+
#
|
|
192
|
+
# Performance: memoized on the rule with zero-allocation cache hits.
|
|
193
|
+
# The inputs are constant for a given rule, register and parent model,
|
|
194
|
+
# so every element of a collection reuses the first resolution.
|
|
195
|
+
def attribute_match_names(attr, register, parent_ns_class, form_default,
|
|
196
|
+
default_namespace)
|
|
197
|
+
if defined?(@_amn_names) && @_amn_register == register &&
|
|
198
|
+
@_amn_parent_ns == parent_ns_class &&
|
|
199
|
+
@_amn_form_default == form_default &&
|
|
200
|
+
@_amn_default_ns == default_namespace
|
|
201
|
+
return @_amn_names
|
|
202
|
+
end
|
|
203
|
+
|
|
204
|
+
result = compute_attribute_match_names(attr, register, parent_ns_class,
|
|
205
|
+
form_default, default_namespace)
|
|
206
|
+
@_amn_register = register
|
|
207
|
+
@_amn_parent_ns = parent_ns_class
|
|
208
|
+
@_amn_form_default = form_default
|
|
209
|
+
@_amn_default_ns = default_namespace
|
|
210
|
+
@_amn_names = result
|
|
211
|
+
result
|
|
212
|
+
end
|
|
213
|
+
|
|
186
214
|
def namespaced_name(parent_namespace = nil, name = self.name)
|
|
187
215
|
if name.to_s == "lang"
|
|
188
216
|
::Lutaml::Model::Utils.blank?(prefix) ? name.to_s : "#{prefix}:#{name}"
|
|
@@ -274,6 +302,33 @@ module Lutaml
|
|
|
274
302
|
|
|
275
303
|
private
|
|
276
304
|
|
|
305
|
+
# Extracted body of attribute_match_names for caching
|
|
306
|
+
def compute_attribute_match_names(attr, register, parent_ns_class,
|
|
307
|
+
form_default, default_namespace)
|
|
308
|
+
ns_info = resolve_namespace(
|
|
309
|
+
attr: attr,
|
|
310
|
+
register: register,
|
|
311
|
+
parent_ns_class: parent_ns_class,
|
|
312
|
+
form_default: form_default,
|
|
313
|
+
)
|
|
314
|
+
uri = ns_info[:uri]
|
|
315
|
+
# Per W3C, an unprefixed attribute is in NO namespace regardless of
|
|
316
|
+
# the element's namespace. The qualified alias is only legitimate
|
|
317
|
+
# under schema-level attribute_form_default :qualified — the
|
|
318
|
+
# via_schema_default leniency (preserves ogc-gml's srsName handling
|
|
319
|
+
# under :qualified schema defaults). Without this gate, a plain
|
|
320
|
+
# attribute rule on an element-namespaced class inherits the
|
|
321
|
+
# element's namespace as default_namespace and steals a
|
|
322
|
+
# namespaced-sibling attribute whose local name matches (lutaml-model#758).
|
|
323
|
+
strict_match = uri && ns_info[:prefix] &&
|
|
324
|
+
!ns_info[:unqualified_same_ns] && !ns_info[:via_schema_default]
|
|
325
|
+
if strict_match
|
|
326
|
+
["#{uri}:#{name}"].freeze
|
|
327
|
+
else
|
|
328
|
+
namespaced_names(default_namespace)
|
|
329
|
+
end
|
|
330
|
+
end
|
|
331
|
+
|
|
277
332
|
# Extracted body of namespaced_names for caching
|
|
278
333
|
def compute_namespaced_names(parent_namespace)
|
|
279
334
|
if multiple_mappings?
|
|
@@ -413,7 +468,8 @@ form_default = :unqualified)
|
|
|
413
468
|
end
|
|
414
469
|
|
|
415
470
|
# 2. Type-level namespace
|
|
416
|
-
if attr && (type_ns_class = attr.type_namespace_class(register))
|
|
471
|
+
if attr && (type_ns_class = attr.type_namespace_class(register)) &&
|
|
472
|
+
type_ns_class != :blank
|
|
417
473
|
result = build_namespace_result_from_class(type_ns_class)
|
|
418
474
|
# CRITICAL W3C FLAG: Mark when attribute is in same namespace as parent with :unqualified
|
|
419
475
|
# Serialization code will check this to omit prefix per attributeFormDefault
|
|
@@ -427,7 +483,9 @@ form_default = :unqualified)
|
|
|
427
483
|
# When schema specifies attributeFormDefault="qualified", attributes
|
|
428
484
|
# must be qualified (prefixed) with the parent element's namespace
|
|
429
485
|
if form_default == :qualified && parent_ns_class
|
|
430
|
-
|
|
486
|
+
result = build_namespace_result_from_class(parent_ns_class)
|
|
487
|
+
result[:via_schema_default] = true
|
|
488
|
+
return result
|
|
431
489
|
end
|
|
432
490
|
|
|
433
491
|
# 4. No namespace (W3C default for unprefixed attributes)
|
|
@@ -490,7 +548,8 @@ form_default = :unqualified)
|
|
|
490
548
|
end
|
|
491
549
|
|
|
492
550
|
# 4. Type-level namespace
|
|
493
|
-
if attr && (type_ns_class = attr.type_namespace_class(register))
|
|
551
|
+
if attr && (type_ns_class = attr.type_namespace_class(register)) &&
|
|
552
|
+
type_ns_class != :blank
|
|
494
553
|
# Check if Type namespace matches parent default namespace
|
|
495
554
|
# If so, use parent's namespace with parent's actual prefix
|
|
496
555
|
if type_ns_class.uri == parent_ns_uri && parent_ns_class
|
|
@@ -86,8 +86,8 @@ module Lutaml
|
|
|
86
86
|
end
|
|
87
87
|
end
|
|
88
88
|
root_and_parent_assignment(instance, options)
|
|
89
|
-
apply_xml_mapping(data, instance, options,
|
|
90
|
-
|
|
89
|
+
apply_xml_mapping(ParseSession.new(data, instance, options,
|
|
90
|
+
child_register))
|
|
91
91
|
end
|
|
92
92
|
|
|
93
93
|
# Build a DeclarationPlan from the parsed element tree's namespace declarations.
|
|
@@ -206,17 +206,12 @@ module Lutaml
|
|
|
206
206
|
|
|
207
207
|
private
|
|
208
208
|
|
|
209
|
-
def apply_xml_mapping(
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
effective_register ||= if instance_is_serialize && instance.lutaml_register
|
|
216
|
-
instance.lutaml_register
|
|
217
|
-
else
|
|
218
|
-
lutaml_register
|
|
219
|
-
end
|
|
209
|
+
def apply_xml_mapping(session)
|
|
210
|
+
doc = session.doc
|
|
211
|
+
instance = session.instance
|
|
212
|
+
options = session.options
|
|
213
|
+
effective_register = session.register
|
|
214
|
+
instance_is_serialize = session.instance_is_serialize
|
|
220
215
|
|
|
221
216
|
# Performance: Resolve xml_mapping once — used for both rules and metadata
|
|
222
217
|
xml_mapping = mappings_for(:xml, effective_register)
|
|
@@ -263,6 +258,18 @@ effective_register = nil, instance_is_serialize = nil)
|
|
|
263
258
|
end
|
|
264
259
|
end
|
|
265
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
|
+
|
|
266
273
|
mappings.each do |rule|
|
|
267
274
|
# Performance: Cache rule properties accessed multiple times
|
|
268
275
|
rule.name
|
|
@@ -273,6 +280,11 @@ effective_register = nil, instance_is_serialize = nil)
|
|
|
273
280
|
attr = attribute_for_rule(rule)
|
|
274
281
|
next if attr&.derived?
|
|
275
282
|
|
|
283
|
+
if (group = grouped_plain_rules[rule_to]) &&
|
|
284
|
+
group.size > 1 && attr&.collection? && !group.first.equal?(rule)
|
|
285
|
+
next
|
|
286
|
+
end
|
|
287
|
+
|
|
276
288
|
raise "Attribute '#{rule_to}' not found in #{context}" unless valid_rule?(
|
|
277
289
|
rule, attr
|
|
278
290
|
)
|
|
@@ -290,6 +302,26 @@ effective_register = nil, instance_is_serialize = nil)
|
|
|
290
302
|
doc.root.inner_xml
|
|
291
303
|
elsif rule.content_mapping?
|
|
292
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
|
|
293
325
|
elsif child_names_set && !rule.attribute? &&
|
|
294
326
|
!child_matches_rule?(rule, child_names_set,
|
|
295
327
|
default_namespace)
|
|
@@ -303,8 +335,7 @@ effective_register = nil, instance_is_serialize = nil)
|
|
|
303
335
|
end
|
|
304
336
|
else
|
|
305
337
|
# Performance: Pass cached attr to avoid recomputing attribute_for_rule
|
|
306
|
-
val = value_for_rule(
|
|
307
|
-
effective_register, instance_is_serialize)
|
|
338
|
+
val = value_for_rule(session, rule, new_opts, attr)
|
|
308
339
|
|
|
309
340
|
if (val.nil? || ::Lutaml::Model::Utils.uninitialized?(val)) &&
|
|
310
341
|
(instance.using_default?(rule_to) || rule.render_default)
|
|
@@ -437,13 +468,20 @@ _effective_register)
|
|
|
437
468
|
# - URI format: "http://.../namespace:attr" or "urn:...:attr"
|
|
438
469
|
# - Simple prefix format: "my-ns:attr" (namespace URI is a simple string, not a URI)
|
|
439
470
|
# - Unprefixed: "attr" (no namespace)
|
|
440
|
-
# Performance:
|
|
441
|
-
|
|
471
|
+
# Performance: most rules carry a single name — skip the map and
|
|
472
|
+
# its result array entirely on that path.
|
|
473
|
+
if rule_names.size == 1
|
|
474
|
+
rn = rule_names[0]
|
|
442
475
|
converted = convert_rule_name_to_attribute_name(doc, rn)
|
|
443
|
-
converted || rn
|
|
444
|
-
|
|
476
|
+
value = doc.root.find_attribute_value(converted || rn)
|
|
477
|
+
else
|
|
478
|
+
attribute_names = rule_names.map do |rn|
|
|
479
|
+
converted = convert_rule_name_to_attribute_name(doc, rn)
|
|
480
|
+
converted || rn
|
|
481
|
+
end
|
|
445
482
|
|
|
446
|
-
|
|
483
|
+
value = doc.root.find_attribute_value(attribute_names)
|
|
484
|
+
end
|
|
447
485
|
|
|
448
486
|
# Fallback: if value is nil, try to match by local name only.
|
|
449
487
|
# This handles the case where the namespace prefix is not declared in the document
|
|
@@ -468,25 +506,36 @@ _effective_register)
|
|
|
468
506
|
def convert_rule_name_to_attribute_name(doc, rule_name)
|
|
469
507
|
return nil unless rule_name.include?(":")
|
|
470
508
|
|
|
471
|
-
#
|
|
472
|
-
#
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
# Determine if namespace_part is a URI format or a simple prefix
|
|
478
|
-
# URI formats: contains "://" (http/https) or starts with "urn:"
|
|
479
|
-
is_uri_format = namespace_part.include?("://") || namespace_part.start_with?("urn:")
|
|
509
|
+
# URI-vs-prefix detection works on the whole rule name: the
|
|
510
|
+
# namespace part is a prefix of rule_name, so "://" anywhere in
|
|
511
|
+
# the name means a URI-form namespace, and a "urn:" namespace
|
|
512
|
+
# makes the whole name start with "urn:". Slicing the parts out
|
|
513
|
+
# first would allocate two strings per call on this hot path.
|
|
514
|
+
is_uri_format = rule_name.include?("://") || rule_name.start_with?("urn:")
|
|
480
515
|
|
|
516
|
+
last_colon_index = rule_name.rindex(":")
|
|
481
517
|
if is_uri_format
|
|
482
|
-
# URI format:
|
|
483
|
-
|
|
484
|
-
|
|
518
|
+
# URI format: find the attribute whose resolved namespace and
|
|
519
|
+
# local name spell the rule name. Comparison is by namespace
|
|
520
|
+
# prefix + local suffix so prefixed attributes (whose
|
|
521
|
+
# namespaced_name is "prefix:local", never "uri:local") still
|
|
522
|
+
# match, and unprefixed_name is not called per attribute
|
|
523
|
+
# (it splits prefixed names and allocates).
|
|
524
|
+
local_name = rule_name[(last_colon_index + 1)..]
|
|
525
|
+
doc.root.attributes.each_value.find do |attr|
|
|
526
|
+
ns = attr.namespace
|
|
527
|
+
next false if ns.nil? || ns.length > last_colon_index
|
|
528
|
+
next false unless rule_name.start_with?(ns) && rule_name[ns.length] == ":"
|
|
529
|
+
|
|
530
|
+
attr.unprefixed_name == local_name
|
|
485
531
|
end&.name
|
|
486
532
|
else
|
|
487
533
|
# Simple prefix format: look up the actual prefix from document's namespace declarations
|
|
488
534
|
# The namespace_part is the namespace URI declared in the document (e.g., "my-ns").
|
|
489
535
|
# Find the corresponding prefix (e.g., "my") and build "my:val".
|
|
536
|
+
namespace_part = rule_name[0...last_colon_index]
|
|
537
|
+
local_name = rule_name[(last_colon_index + 1)..]
|
|
538
|
+
|
|
490
539
|
ns_data = doc.root.namespaces.values.find do |nd|
|
|
491
540
|
nd.uri == namespace_part
|
|
492
541
|
end
|
|
@@ -507,31 +556,44 @@ _effective_register)
|
|
|
507
556
|
|
|
508
557
|
last_colon_index = rn.rindex(":")
|
|
509
558
|
local_name = rn[(last_colon_index + 1)..]
|
|
559
|
+
rule_uri = rn[0...last_colon_index]
|
|
560
|
+
|
|
561
|
+
matched_attr = doc.root.attributes.each_value.find do |attr|
|
|
562
|
+
# Local-name fallback serves two lenient cases only:
|
|
563
|
+
# - an attribute with an undeclared prefix (raw name keeps the
|
|
564
|
+
# colon, namespace unresolvable), the historical purpose;
|
|
565
|
+
# - an attribute whose resolved namespace IS one of the rule's
|
|
566
|
+
# namespace URIs (prefix rebinding, lutaml-model#744).
|
|
567
|
+
# Everything else — including a definitively namespace-less
|
|
568
|
+
# attribute and an attribute from a foreign namespace — must
|
|
569
|
+
# not satisfy the fallback, or a plain rule steals a
|
|
570
|
+
# namespaced sibling attribute (lutaml-model#758). An
|
|
571
|
+
# XmlAttribute with a resolved namespace has namespaced_name
|
|
572
|
+
# "URI:local", so this comparison is allocation-free.
|
|
573
|
+
if attr.namespace.nil?
|
|
574
|
+
next false if attr.namespace_prefix.nil? &&
|
|
575
|
+
!attr.name.include?(":")
|
|
510
576
|
|
|
511
|
-
matched_attr = doc.root.attributes.values.find do |attr|
|
|
512
|
-
# Match by the attribute's namespaced_name, unprefixed_name, or by extracting
|
|
513
|
-
# local name from the attribute's key (handles unresolved prefixes).
|
|
514
|
-
attr.namespaced_name == local_name ||
|
|
515
577
|
attr.unprefixed_name == local_name ||
|
|
516
|
-
|
|
578
|
+
((colon = attr.name.rindex(":")) ? attr.name[(colon + 1)..] : attr.name) == local_name
|
|
579
|
+
else
|
|
580
|
+
# Prefixed attributes carry namespaced_name "prefix:local",
|
|
581
|
+
# which a URI-form rule name can never equal — match by the
|
|
582
|
+
# attribute's resolved namespace instead.
|
|
583
|
+
attr.namespace == rule_uri && attr.unprefixed_name == local_name
|
|
584
|
+
end
|
|
517
585
|
end
|
|
518
586
|
return matched_attr&.value if matched_attr
|
|
519
587
|
end
|
|
520
588
|
nil
|
|
521
589
|
end
|
|
522
590
|
|
|
523
|
-
def value_for_rule(
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
instance_is_serialize =
|
|
528
|
-
|
|
529
|
-
# Use threaded register from apply_xml_mapping if provided, otherwise derive
|
|
530
|
-
effective_register ||= if instance_is_serialize && instance.lutaml_register
|
|
531
|
-
instance.lutaml_register
|
|
532
|
-
else
|
|
533
|
-
lutaml_register
|
|
534
|
-
end
|
|
591
|
+
def value_for_rule(session, rule, options, cached_attr = nil,
|
|
592
|
+
extra_rule_names = nil)
|
|
593
|
+
doc = session.doc
|
|
594
|
+
instance = session.instance
|
|
595
|
+
instance_is_serialize = session.instance_is_serialize
|
|
596
|
+
effective_register = session.register
|
|
535
597
|
|
|
536
598
|
# Performance: Use cached attr from caller if available
|
|
537
599
|
attr = cached_attr || attribute_for_rule(rule)
|
|
@@ -544,9 +606,20 @@ _effective_register)
|
|
|
544
606
|
rule_namespace = rule_namespace_set ? rule.namespace : nil
|
|
545
607
|
options[:default_namespace]
|
|
546
608
|
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
609
|
+
rule_names = if rule.attribute?
|
|
610
|
+
resolve_attribute_rule_names(rule, attr, options,
|
|
611
|
+
effective_register,
|
|
612
|
+
instance, instance_is_serialize)
|
|
613
|
+
else
|
|
614
|
+
resolve_rule_names_with_type(rule, attr, options,
|
|
615
|
+
effective_register, attr_type)
|
|
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
|
|
550
623
|
|
|
551
624
|
return value_for_xml_attribute(doc, rule, rule_names) if rule.attribute?
|
|
552
625
|
|
|
@@ -565,15 +638,25 @@ _effective_register)
|
|
|
565
638
|
end
|
|
566
639
|
type_ns_prefix_str = type_ns_class&.prefix_default&.to_s
|
|
567
640
|
type_ns_all_uris = type_ns_class&.all_uris
|
|
641
|
+
# Adopt the parsed document's namespace (memoized on the session):
|
|
642
|
+
# when the whole document is out-of-namespace, treat it as an
|
|
643
|
+
# implicit alias so lenient parsing is lossless (#754).
|
|
644
|
+
adopted_uri = session.adopted_namespace_uri
|
|
645
|
+
if adopted_uri && !type_ns_all_uris&.include?(adopted_uri)
|
|
646
|
+
type_ns_all_uris = (type_ns_all_uris || []) + [adopted_uri]
|
|
647
|
+
end
|
|
568
648
|
end
|
|
569
649
|
|
|
570
650
|
# Pre-compute model's namespace class for simple-type alias matching
|
|
571
|
-
# (Third branch in select loop —
|
|
651
|
+
# (Third branch in select loop — memoized on the session)
|
|
572
652
|
model_ns_class = if instance_is_serialize &&
|
|
573
653
|
!rule_namespace_set && !attr_type_is_serializable
|
|
574
|
-
|
|
654
|
+
session.xml_mapping&.namespace_class
|
|
575
655
|
end
|
|
576
|
-
|
|
656
|
+
# Includes the adopted document namespace (#754): a prefixed child
|
|
657
|
+
# in the document's (foreign) namespace must still bind by local
|
|
658
|
+
# name, or its content is silently dropped.
|
|
659
|
+
model_ns_all_uris = model_ns_class ? session.model_namespace_uris : nil
|
|
577
660
|
|
|
578
661
|
# Performance: Use cached element children from XmlElement
|
|
579
662
|
# This avoids O(children * rules) scans by pre-filtering once per element
|
|
@@ -604,6 +687,13 @@ _effective_register)
|
|
|
604
687
|
children = nil # fall through to select for alias matching
|
|
605
688
|
else
|
|
606
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
|
|
607
697
|
end
|
|
608
698
|
else
|
|
609
699
|
children = nil
|
|
@@ -879,6 +969,44 @@ effective_register = lutaml_register)
|
|
|
879
969
|
end
|
|
880
970
|
end
|
|
881
971
|
|
|
972
|
+
# Match names for an attribute rule, mirroring how serialization
|
|
973
|
+
# qualifies the same attribute (MappingRule#attribute_match_names):
|
|
974
|
+
# a rule serialized with a prefix matches only attributes in its
|
|
975
|
+
# namespace; a rule serialized unprefixed matches only namespace-less
|
|
976
|
+
# attributes.
|
|
977
|
+
def resolve_attribute_rule_names(rule, attr, options, effective_register,
|
|
978
|
+
instance, instance_is_serialize)
|
|
979
|
+
return rule.namespaced_names(options[:default_namespace]) if attr.nil? ||
|
|
980
|
+
rule.multiple_mappings?
|
|
981
|
+
|
|
982
|
+
if instance_is_serialize
|
|
983
|
+
parent_ns_class, form_default = parent_attribute_context(instance)
|
|
984
|
+
else
|
|
985
|
+
parent_ns_class = nil
|
|
986
|
+
form_default = :unqualified
|
|
987
|
+
end
|
|
988
|
+
rule.attribute_match_names(attr, effective_register, parent_ns_class,
|
|
989
|
+
form_default,
|
|
990
|
+
options[:default_namespace])
|
|
991
|
+
end
|
|
992
|
+
|
|
993
|
+
# [namespace_class, attribute_form_default] for the instance's model,
|
|
994
|
+
# keyed on the model class. Attribute rules of one element share the
|
|
995
|
+
# model, so a 1-entry cache avoids a mappings_for lookup per rule.
|
|
996
|
+
def parent_attribute_context(instance)
|
|
997
|
+
klass = instance.class
|
|
998
|
+
if @parent_ns_klass == klass && @parent_ns_value
|
|
999
|
+
return @parent_ns_value
|
|
1000
|
+
end
|
|
1001
|
+
|
|
1002
|
+
ns_class = klass.mappings_for(:xml)&.namespace_class
|
|
1003
|
+
value = [ns_class,
|
|
1004
|
+
ns_class&.attribute_form_default || :unqualified]
|
|
1005
|
+
@parent_ns_klass = klass
|
|
1006
|
+
@parent_ns_value = value
|
|
1007
|
+
value
|
|
1008
|
+
end
|
|
1009
|
+
|
|
882
1010
|
# Resolve rule names with type namespace support
|
|
883
1011
|
#
|
|
884
1012
|
# @param rule [MappingRule] the mapping rule
|
|
@@ -37,18 +37,18 @@ module Lutaml
|
|
|
37
37
|
# @param mapping [Xml::Mapping] the XML mapping for this element
|
|
38
38
|
# @param options [Hash] additional options including mapper_class for recursive calls
|
|
39
39
|
# @return [NamespaceNeeds] namespace needs structure
|
|
40
|
-
def collect(element, mapping, visited: nil, **)
|
|
40
|
+
def collect(element, mapping, visited: nil, **options)
|
|
41
41
|
# If this is a top-level call (no visited yet), ensure cleanup
|
|
42
42
|
if visited.nil?
|
|
43
43
|
Thread.current[:namespace_collector_call_stack] = []
|
|
44
44
|
begin
|
|
45
|
-
collect_internal(element, mapping, visited: nil, **)
|
|
45
|
+
collect_internal(element, mapping, visited: nil, **options)
|
|
46
46
|
ensure
|
|
47
47
|
Thread.current[:namespace_collector_call_stack] = nil
|
|
48
48
|
end
|
|
49
49
|
else
|
|
50
50
|
# Nested call - don't clean up yet
|
|
51
|
-
collect_internal(element, mapping, visited: visited, **)
|
|
51
|
+
collect_internal(element, mapping, visited: visited, **options)
|
|
52
52
|
end
|
|
53
53
|
end
|
|
54
54
|
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Lutaml
|
|
4
|
+
module Xml
|
|
5
|
+
# Per-parse context threaded through ModelTransform's mapping passes.
|
|
6
|
+
#
|
|
7
|
+
# Bundles everything one data_to_model call needs — the parsed doc, the
|
|
8
|
+
# instance under construction, the effective register, and the flags
|
|
9
|
+
# derived from them — so apply_xml_mapping and value_for_rule take one
|
|
10
|
+
# object instead of a growing positional parameter list. Memoizes the
|
|
11
|
+
# per-parse lookups (mapping, namespace class, adopted document
|
|
12
|
+
# namespace) that every rule would otherwise recompute.
|
|
13
|
+
class ParseSession
|
|
14
|
+
attr_reader :doc, :instance, :options, :register
|
|
15
|
+
|
|
16
|
+
def initialize(doc, instance, options, register)
|
|
17
|
+
@doc = doc
|
|
18
|
+
@instance = instance
|
|
19
|
+
@options = options
|
|
20
|
+
@register = register
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def instance_is_serialize
|
|
24
|
+
@instance_is_serialize ||= instance.is_a?(::Lutaml::Model::Serialize)
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def model_class
|
|
28
|
+
@model_class ||= instance.class
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def xml_mapping
|
|
32
|
+
@xml_mapping ||= model_class.mappings_for(:xml, register)
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# The document's own namespace when it differs from the model's
|
|
36
|
+
# bound namespace (lenient, out-of-namespace documents). Adopted as
|
|
37
|
+
# an implicit alias for element matching (lutaml-model#754).
|
|
38
|
+
def adopted_namespace_uri
|
|
39
|
+
@adopted_namespace_uri ||=
|
|
40
|
+
instance_is_serialize ? instance.original_namespace_uri : nil
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# Namespace URIs accepted for children of this model: the model's
|
|
44
|
+
# namespace URIs plus the adopted document namespace when present.
|
|
45
|
+
def model_namespace_uris
|
|
46
|
+
@model_namespace_uris ||=
|
|
47
|
+
begin
|
|
48
|
+
ns_class = instance_is_serialize ? xml_mapping&.namespace_class : nil
|
|
49
|
+
uris = ns_class&.all_uris
|
|
50
|
+
adopted = adopted_namespace_uri
|
|
51
|
+
if ns_class && adopted && !uris&.include?(adopted)
|
|
52
|
+
uris = (uris || []) + [adopted]
|
|
53
|
+
end
|
|
54
|
+
uris
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|
|
@@ -20,7 +20,7 @@ module Lutaml
|
|
|
20
20
|
# Supported schema builders (separate from XML parsing adapters)
|
|
21
21
|
SUPPORTED_BUILDERS = %i[nokogiri oga].freeze
|
|
22
22
|
|
|
23
|
-
def initialize(adapter_type: nil, options: {}, &)
|
|
23
|
+
def initialize(adapter_type: nil, options: {}, &block)
|
|
24
24
|
# Use specified adapter, or configured XML adapter, or default to Nokogiri
|
|
25
25
|
requested_adapter = adapter_type || Lutaml::Model::Config.xml_adapter_type || :nokogiri
|
|
26
26
|
|
|
@@ -32,7 +32,7 @@ module Lutaml
|
|
|
32
32
|
:nokogiri
|
|
33
33
|
end
|
|
34
34
|
@options = options
|
|
35
|
-
@builder = create_builder(&)
|
|
35
|
+
@builder = create_builder(&block)
|
|
36
36
|
end
|
|
37
37
|
|
|
38
38
|
# Generate the XSD schema XML string
|
|
@@ -45,12 +45,12 @@ module Lutaml
|
|
|
45
45
|
|
|
46
46
|
private
|
|
47
47
|
|
|
48
|
-
def create_builder(&)
|
|
48
|
+
def create_builder(&block)
|
|
49
49
|
case @adapter_type
|
|
50
50
|
when :nokogiri
|
|
51
|
-
Nokogiri.new(@options, &)
|
|
51
|
+
Nokogiri.new(@options, &block)
|
|
52
52
|
when :oga
|
|
53
|
-
Oga.new(@options, &)
|
|
53
|
+
Oga.new(@options, &block)
|
|
54
54
|
end
|
|
55
55
|
end
|
|
56
56
|
end
|
|
@@ -28,7 +28,7 @@ module Lutaml
|
|
|
28
28
|
# @param to [String] The target path
|
|
29
29
|
# @param pattern [Boolean, nil] Explicit pattern flag (optional)
|
|
30
30
|
def initialize(attributes = nil, from: nil, to: nil, pattern: nil,
|
|
31
|
-
**)
|
|
31
|
+
**options)
|
|
32
32
|
# Handle hash argument from Lutaml::Model deserialization
|
|
33
33
|
if attributes.is_a?(Hash)
|
|
34
34
|
from = attributes[:from] || attributes["from"]
|
|
@@ -45,7 +45,7 @@ module Lutaml
|
|
|
45
45
|
# Use explicit pattern flag if provided, otherwise use detected value
|
|
46
46
|
final_pattern = pattern.nil? ? detected_pattern : pattern
|
|
47
47
|
|
|
48
|
-
super(from: from_str, to: to, pattern: final_pattern, **)
|
|
48
|
+
super(from: from_str, to: to, pattern: final_pattern, **options)
|
|
49
49
|
end
|
|
50
50
|
|
|
51
51
|
# Convert hash-based mapping to SchemaLocationMapping instance
|
|
@@ -141,9 +141,7 @@ module Lutaml
|
|
|
141
141
|
# Trigger autoloads for all XSD model classes to ensure they are
|
|
142
142
|
# registered before parsing begins. This is necessary because
|
|
143
143
|
# type resolution during parsing looks up classes in the register.
|
|
144
|
-
# rubocop:disable Style/RedundantFetchOverride
|
|
145
144
|
XSD_AUTOLOAD_CLASSES.each { |c| const_get(c) unless c == :VERSION }
|
|
146
|
-
# rubocop:enable Style/RedundantFetchOverride
|
|
147
145
|
|
|
148
146
|
# Validate XSD schema structure before parsing (unless disabled)
|
|
149
147
|
if validate_schema && !nested_schema
|
|
@@ -410,7 +410,8 @@ attr_name)
|
|
|
410
410
|
def self.get_attribute_xsd_type(attr, attr_type, register,
|
|
411
411
|
_mapping_rule = nil)
|
|
412
412
|
if attr.union?
|
|
413
|
-
raise Lutaml::Model::UnionSchemaUnsupportedError.new(attr.name,
|
|
413
|
+
raise Lutaml::Model::UnionSchemaUnsupportedError.new(attr.name,
|
|
414
|
+
"XSD")
|
|
414
415
|
end
|
|
415
416
|
|
|
416
417
|
# 1. Check for deprecated attribute-level xsd_type override
|
|
@@ -89,7 +89,7 @@ module Lutaml
|
|
|
89
89
|
end
|
|
90
90
|
|
|
91
91
|
# Override choice to set format: :xml so Choice knows which format it belongs to.
|
|
92
|
-
def choice(min: 1, max: 1, format: :xml, &)
|
|
92
|
+
def choice(min: 1, max: 1, format: :xml, &block)
|
|
93
93
|
super
|
|
94
94
|
end
|
|
95
95
|
|
|
@@ -101,9 +101,9 @@ module Lutaml
|
|
|
101
101
|
# @param format [Symbol] The format
|
|
102
102
|
# @param args [Array] Additional arguments (mapping class for XML)
|
|
103
103
|
# @param block [Proc] The DSL block
|
|
104
|
-
def process_mapping(format, *args, &)
|
|
104
|
+
def process_mapping(format, *args, &block)
|
|
105
105
|
if format == :xml && args.any? && args.first.is_a?(Class) && args.first < Lutaml::Xml::Mapping
|
|
106
|
-
process_xml_mapping_class_inheritance(args.first, &)
|
|
106
|
+
process_xml_mapping_class_inheritance(args.first, &block)
|
|
107
107
|
else
|
|
108
108
|
super
|
|
109
109
|
end
|
|
@@ -143,7 +143,8 @@ parent_element_form_default)
|
|
|
143
143
|
# Get transformation for the actual class. Unions resolve the member
|
|
144
144
|
# from the value's own class, like polymorphism.
|
|
145
145
|
child_transformation = if union
|
|
146
|
-
value.class.transformation_for(:xml,
|
|
146
|
+
value.class.transformation_for(:xml,
|
|
147
|
+
register)
|
|
147
148
|
elsif is_polymorphic || is_polymorphic_subtype
|
|
148
149
|
actual_class.transformation_for(:xml,
|
|
149
150
|
register)
|
|
@@ -326,7 +326,8 @@ compiled_rules, mapping, processed_text_nodes)
|
|
|
326
326
|
compiled_rules
|
|
327
327
|
end
|
|
328
328
|
|
|
329
|
-
emitted_counts = element_order_coverage(model_instance,
|
|
329
|
+
emitted_counts = element_order_coverage(model_instance,
|
|
330
|
+
compiled_rules)
|
|
330
331
|
|
|
331
332
|
rules_to_apply.each do |rule|
|
|
332
333
|
mapping_type = rule.option(:mapping_type)
|