lutaml-model 0.8.85 → 0.8.86

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 7fcfb57e6eb2cff5a7cd47dbe44e7e2baceb10ea5e8f8c22c13fa712d5c8a1fb
4
- data.tar.gz: 35c30ed77dfee8d8a06a7a9f8f32f94178c3f092f54d949f831f6a29842d6b27
3
+ metadata.gz: 91e44ff1cc09c4f38dfe2a18d6d7bad7b70a7828ed6c590c5d808848c0c520eb
4
+ data.tar.gz: 679150748631c4dbb8063ca6d832028f41c47d9df33561f7f2833535acddb4b1
5
5
  SHA512:
6
- metadata.gz: 45bf3b55c3d356307035278d46709818fdd8ba4a1520741a58117f7419263b3c438f57097a8e736f1a87dac765bc6523e12479dd54b408aee44a23577f9d3726
7
- data.tar.gz: 0c6a92ec24a172e016ee0484a2579efed9c3f0c5c653e85ad2509ae441bb71fcffd6c68c4f72d17a250f4587e21af6072116783cf8229cb997692c48cdcc3d1d
6
+ metadata.gz: b5c248fcd779fdfa8652d69e8036c38c83b92068f764c56a505154efddee3270bee5cf3dc3cc9ca8f4ef3d056d37bba76cdb45f2e90b698e78478d3368a4d35b
7
+ data.tar.gz: d696297bd441cb8a1bc9b7b7adf18fa258e8708034e293c181178d91f58b19ad3ccab5e61689af91913b9318ab2120ded1973616e28d79fa741702e709ba2e07
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Lutaml
4
4
  module Model
5
- VERSION = "0.8.85"
5
+ VERSION = "0.8.86"
6
6
  end
7
7
  end
@@ -59,6 +59,7 @@ module Lutaml
59
59
 
60
60
  @elements = {}
61
61
  @attributes = {}
62
+ @any_element_rule = nil
62
63
  @element_sequence = []
63
64
  @content_mapping = nil
64
65
  @raw_mapping = nil
@@ -504,6 +505,26 @@ module Lutaml
504
505
  # @return [String, nil] the type name
505
506
  alias xsd_type type_name
506
507
 
508
+ # Catch-all element mapping: every child element with no
509
+ # explicit rule routes to one collection attribute. For
510
+ # parse-side consumption of dynamic vocabularies (semantic__
511
+ # trees); element_order preserves the original tag names.
512
+ # Serialization of a catch-all attribute emits the attribute's
513
+ # declared element name (not per-item originals).
514
+ def map_any_element(to:, render_nil: false, render_default: false)
515
+ validate!(nil, to, {}, render_nil, false, type: TYPES[:element])
516
+ @any_element_rule = MappingRule.new(
517
+ nil,
518
+ to: to,
519
+ render_nil: render_nil,
520
+ render_default: render_default,
521
+ )
522
+ @element_rules_index = nil if instance_variable_defined?(:@element_rules_index)
523
+ @any_element_rule
524
+ end
525
+
526
+ attr_reader :any_element_rule
527
+
507
528
  def map_instances(to:, polymorphic: {})
508
529
  map_element(to, to: to, polymorphic: polymorphic)
509
530
  end
@@ -1342,9 +1363,9 @@ module Lutaml
1342
1363
  elsif ["text", "#cdata-section"].include?(name.to_s) && type == "Text"
1343
1364
  content_mapping
1344
1365
  else
1345
- candidates = mappings.select do |rule|
1346
- rule.name == name.to_s || rule.name == name.to_sym
1347
- end
1366
+ candidates = element_rules_index[name.to_s]
1367
+ return @any_element_rule if candidates.nil? && @any_element_rule
1368
+ return nil if candidates.nil?
1348
1369
  return candidates.first if namespace_uri.nil? || candidates.one?
1349
1370
 
1350
1371
  candidates.find do |r|
@@ -1353,6 +1374,29 @@ module Lutaml
1353
1374
  end
1354
1375
  end
1355
1376
 
1377
+ # O(1) name -> rules lookup. Parsing consults this once per
1378
+ # child element; the linear scan over every rule made large
1379
+ # documents quadratic (155s on a 7MB vocabulary tree). Keyed on
1380
+ # the stable source hashes (@elements/@attributes mutate only at
1381
+ # DSL time), never on the per-call mappings array.
1382
+ def element_rules_index
1383
+ cache_key = [@elements.object_id, @attributes.object_id]
1384
+ cached = (@element_rules_index ||= {})
1385
+ idx = cached[cache_key]
1386
+ return idx if idx
1387
+
1388
+ idx = {}
1389
+ ([content_mapping, raw_mapping].compact + @elements.values +
1390
+ @attributes.values).flatten.each do |rule|
1391
+ next if rule.is_a?(::Hash)
1392
+
1393
+ (idx[rule.name.to_s] ||= []) << rule
1394
+ end
1395
+ cached[cache_key] = idx
1396
+ idx
1397
+ end
1398
+ private :element_rules_index
1399
+
1356
1400
  def find_by_to(to)
1357
1401
  mappings.detect { |rule| rule.to.to_s == to.to_s }
1358
1402
  end
@@ -263,6 +263,9 @@ module Lutaml
263
263
  # Performance: Resolve xml_mapping once — used for both rules and metadata
264
264
  xml_mapping = mappings_for(:xml, effective_register)
265
265
  mappings = options[:mappings] || xml_mapping.mappings(effective_register)
266
+ # Catch-all rule (map_any_element): iterated like any other,
267
+ # claiming every child no explicit rule matches.
268
+ mappings += [xml_mapping.any_element_rule].compact if options[:mappings].nil?
266
269
  default_namespace = options[:default_namespace]
267
270
  ordered_option = options[:ordered]
268
271
  mixed_content_option = options[:mixed_content]
@@ -361,7 +364,25 @@ module Lutaml
361
364
  options
362
365
  end
363
366
 
364
- value = if rule.raw_mapping?
367
+ value = if !rule.attribute? && rule.name.nil? &&
368
+ xml_mapping.any_element_rule.equal?(rule)
369
+ # map_any_element: every child element no explicit
370
+ # element rule claims, in document order. A name-less
371
+ # MappingRule is otherwise indistinguishable from a
372
+ # content rule (the dispatch consumed it as doc.text);
373
+ # name-derived lookup also cannot express this — it
374
+ # re-finds by name, losing order and double-picking.
375
+ # The pass walks the document children once; the
376
+ # normalize/cast pipeline below handles the rest.
377
+ claimed = Set.new(
378
+ xml_mapping.mappings(effective_register)
379
+ .select { |r| !r.attribute? && r.name }
380
+ .flat_map { |r| [r.name.to_s] },
381
+ )
382
+ doc.element_children.reject do |child|
383
+ claimed.include?(child.unprefixed_name)
384
+ end
385
+ elsif rule.raw_mapping?
365
386
  scoped_raw_inner_xml(doc, xml_mapping)
366
387
  elsif rule.content_mapping?
367
388
  rule.cdata ? doc.cdata : doc.text
@@ -369,6 +390,8 @@ module Lutaml
369
390
  group.size > 1 && attr&.collection?
370
391
  # First rule of a #765 group: match every spelling of
371
392
  # the group so the children merge in document order.
393
+ raise "GROUPED-BODY" if ENV["CATCHDBG"] && rule.name.nil?
394
+
372
395
  if child_names_set &&
373
396
  group.drop(1).none? do |s|
374
397
  child_matches_rule?(s, child_names_set,
@@ -385,9 +408,11 @@ module Lutaml
385
408
  value_for_rule(session, rule, new_opts, attr,
386
409
  extra_names)
387
410
  end
388
- elsif child_names_set && !rule.attribute? &&
411
+ elsif child_names_set && !rule.attribute? && rule.name &&
389
412
  !child_matches_rule?(rule, child_names_set,
390
413
  default_namespace)
414
+ raise "FASTSKIP-BODY" if ENV["CATCHDBG"] && rule.name.nil?
415
+
391
416
  # Pre-match: no child element matches this rule.
392
417
  # Skip expensive value_for_rule, handle defaults inline.
393
418
  # NOTE: do not "read the instance" here (to_value_for) —
@@ -400,6 +425,8 @@ module Lutaml
400
425
  ::Lutaml::Model::UninitializedClass.instance
401
426
  end
402
427
  else
428
+ raise "ELSE-BODY" if ENV["CATCHDBG"] && rule.name.nil?
429
+
403
430
  # Performance: Pass cached attr to avoid recomputing attribute_for_rule
404
431
  val = value_for_rule(session, rule, new_opts, attr)
405
432
 
@@ -24,6 +24,10 @@ module Lutaml
24
24
  @instance_is_serialize ||= instance.is_a?(::Lutaml::Model::Serialize)
25
25
  end
26
26
 
27
+ def mapping
28
+ @mapping ||= instance.class.mappings_for(:xml, register)
29
+ end
30
+
27
31
  # (element, rule name) -> resolved attribute name / matched
28
32
  # attribute VALUE for the lenient lookup paths. Both are pure
29
33
  # functions of the element and the name; ISO-13849-shaped
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "spec_helper"
4
+
5
+ class AnyElementNode < Lutaml::Model::Serializable
6
+ attribute :id, :string
7
+ attribute :children, "AnyElementNode", collection: true
8
+
9
+ xml do
10
+ element "root"
11
+ map_element "id", to: :id
12
+ map_any_element to: :children
13
+ end
14
+ end
15
+
16
+ RSpec.describe AnyElementNode do
17
+ it "routes unmapped child elements to the catch-all collection" do
18
+ doc = described_class.from_xml(<<~XML)
19
+ <root>
20
+ <id>r-1</id>
21
+ <semantic__p>hello</semantic__p>
22
+ <semantic__title>Nested</semantic__title>
23
+ <semantic__p><semantic__em>deep</semantic__em></semantic__p>
24
+ </root>
25
+ XML
26
+
27
+ expect(doc.id).to eq("r-1")
28
+ expect(doc.children.size).to eq(3)
29
+ end
30
+
31
+ it "keeps explicit rules ahead of the catch-all" do
32
+ doc = described_class.from_xml("<root><id>x</id></root>")
33
+ expect(doc.id).to eq("x")
34
+ expect(doc.children).to be_empty
35
+ end
36
+
37
+ it "nests catch-all children recursively" do
38
+ doc = described_class.from_xml("<root><semantic__p><semantic__em>deep</semantic__em></semantic__p></root>")
39
+ expect(doc.children.first.children.size).to eq(1)
40
+ end
41
+ end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: lutaml-model
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.8.85
4
+ version: 0.8.86
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ribose Inc.
@@ -1697,6 +1697,7 @@ files:
1697
1697
  - spec/lutaml/key_value/when_attribute_spec.rb
1698
1698
  - spec/lutaml/model/absent_value_contracts_spec.rb
1699
1699
  - spec/lutaml/model/adapter_resolver_configured_spec.rb
1700
+ - spec/lutaml/model/any_element_mapping_spec.rb
1700
1701
  - spec/lutaml/model/attribute_apply_value_map_spec.rb
1701
1702
  - spec/lutaml/model/attribute_collection_spec.rb
1702
1703
  - spec/lutaml/model/attribute_default_cache_spec.rb