lutaml-model 0.8.41 → 0.8.43

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.
@@ -0,0 +1,173 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "spec_helper"
4
+
5
+ # lutaml-model#811: the parse path re-resolved per (element × rule) what is
6
+ # static per (attribute/rule, register). These guards pin the hoisting
7
+ # contracts: register-keyed type caching with replacement invalidation,
8
+ # the lock-free child-register resolution, per-rule treatment flags, and
9
+ # the transform dispatch verdicts.
10
+ RSpec.describe "parse-path dispatch hoisting" do
11
+ describe "Attribute#type register caching" do
12
+ let(:klass) do
13
+ Class.new(Lutaml::Model::Serializable) do
14
+ attribute :value, :string
15
+ end
16
+ end
17
+
18
+ it "returns the same resolved class for a repeated register" do
19
+ attr = klass.attributes[:value]
20
+ expect(attr.type(:default)).to equal(attr.type(:default))
21
+ end
22
+
23
+ it "does not pin an unregistered register to the thread default" do
24
+ attr = klass.attributes[:value]
25
+ expect(attr.type(:nonexistent_register)).to eq(Lutaml::Model::Type::String)
26
+ end
27
+
28
+ it "re-resolves when a context is replaced under the same id" do
29
+ attr = klass.attributes[:value]
30
+
31
+ build_context = lambda do |string_type|
32
+ Lutaml::Model::TypeContext.new(
33
+ id: :hoist_test,
34
+ registry: Lutaml::Model::TypeRegistry.new.tap do |r|
35
+ r.register(:string, string_type)
36
+ end,
37
+ )
38
+ end
39
+
40
+ Lutaml::Model::GlobalContext.register_context(
41
+ build_context.call(Lutaml::Model::Type::String),
42
+ )
43
+ expect(attr.type(:hoist_test)).to equal(Lutaml::Model::Type::String)
44
+ expect(attr.type(:hoist_test)).to equal(Lutaml::Model::Type::String)
45
+
46
+ Lutaml::Model::GlobalContext.register_context(
47
+ build_context.call(Lutaml::Model::Type::Integer),
48
+ )
49
+ expect(attr.type(:hoist_test)).to equal(Lutaml::Model::Type::Integer)
50
+
51
+ Lutaml::Model::GlobalContext.unregister_context(:hoist_test)
52
+ end
53
+ end
54
+
55
+ describe "Register.resolve_for_child" do
56
+ it "caches a nil answer without recomputing" do
57
+ klass = Class.new(Lutaml::Model::Serializable)
58
+ expect(Lutaml::Model::Register.resolve_for_child(klass, nil)).to be_nil
59
+ expect(Lutaml::Model::Register.resolve_for_child(klass, nil)).to be_nil
60
+ end
61
+
62
+ it "resolves the child's own default register over the parent's" do
63
+ klass = Class.new(Lutaml::Model::Serializable) do
64
+ def self.lutaml_default_register
65
+ :child_default
66
+ end
67
+ end
68
+ expect(Lutaml::Model::Register.resolve_for_child(klass, :other)).to eq(:child_default)
69
+ expect(Lutaml::Model::Register.resolve_for_child(klass, nil)).to eq(:child_default)
70
+ end
71
+ end
72
+
73
+ describe "MappingRule treatment flags" do
74
+ def rule_with(treat_nil: :nil, treat_empty: :empty, treat_omitted: :nil)
75
+ Lutaml::Model::MappingRule.new(
76
+ "name", to: :name,
77
+ treat_nil: treat_nil, treat_empty: treat_empty,
78
+ treat_omitted: treat_omitted
79
+ )
80
+ end
81
+
82
+ it "computes the same answers as the value_map walk" do
83
+ rule = rule_with(treat_omitted: :omitted, treat_nil: :omitted)
84
+ expect(rule.treat_omitted?).to be(false)
85
+ expect(rule.treat_nil?).to be(false)
86
+ expect(rule.treat_empty?).to be(true)
87
+
88
+ expect(rule.treat_omitted?({})).to eq(rule.treat_omitted?)
89
+ expect(rule.treat_nil?({})).to eq(rule.treat_nil?)
90
+ end
91
+
92
+ it "honors per-call overrides over the precomputed flags" do
93
+ rule = rule_with
94
+ expect(rule.treat_omitted?).to be(true)
95
+ expect(rule.treat_omitted?({ omitted: :omitted })).to be(false)
96
+ expect(rule.treat_nil?({ nil: :omitted })).to be(false)
97
+ expect(rule.treat_empty?({ empty: :omitted })).to be(false)
98
+ end
99
+ end
100
+
101
+ describe "transform dispatch" do
102
+ let(:base) do
103
+ Class.new(Lutaml::Model::Serializable) do
104
+ attribute :plain, :string
105
+ attribute :mapped, :string
106
+ end
107
+ end
108
+
109
+ it "assigns directly for a rule without transformers" do
110
+ rule = Lutaml::Model::MappingRule.new("mapped", to: :mapped)
111
+ expect(Lutaml::Model::MappingRule.transform_dispatch(
112
+ rule, base.attributes[:mapped]
113
+ )).to be(:assign)
114
+ end
115
+
116
+ it "routes hash transformers through ImportTransformer" do
117
+ rule = Lutaml::Model::MappingRule.new(
118
+ "mapped", to: :mapped,
119
+ transform: { import: ->(v) { v.to_s.upcase } }
120
+ )
121
+ expect(Lutaml::Model::MappingRule.transform_dispatch(
122
+ rule, base.attributes[:mapped]
123
+ )).to be(:import)
124
+ end
125
+
126
+ it "applies an import transform through a real round trip" do
127
+ klass = Class.new(Lutaml::Model::Serializable) do
128
+ attribute :shout, :string
129
+
130
+ xml do
131
+ element "doc"
132
+ map_element "shout", to: :shout,
133
+ transform: { import: ->(v) { v.to_s.upcase } }
134
+ end
135
+ end
136
+
137
+ expect(klass.from_xml("<doc><shout>soft</shout></doc>").shout).to eq("SOFT")
138
+ end
139
+ end
140
+
141
+ describe "uninitialized defaults are cached" do
142
+ let(:klass) do
143
+ Class.new(Lutaml::Model::Serializable) do
144
+ attribute :nothing, :string
145
+ end
146
+ end
147
+
148
+ it "returns the same uninitialized sentinel without re-casting" do
149
+ attr = klass.attributes[:nothing]
150
+ expect(attr.default(:default)).to equal(attr.default(:default))
151
+ expect(attr.default(:default)).to equal(
152
+ Lutaml::Model::UninitializedClass.instance,
153
+ )
154
+ end
155
+ end
156
+
157
+ describe "html-entities decode flag" do
158
+ it "memoizes per class without changing the answer" do
159
+ klass = Class.new(Lutaml::Model::Serializable) do
160
+ attribute :body, :string
161
+
162
+ xml do
163
+ element "doc"
164
+ map_element "body", to: :body
165
+ html_entities
166
+ end
167
+ end
168
+
169
+ expect(klass.from_xml("<doc><body>a &amp; b</body></doc>").body).to eq("a & b")
170
+ expect(klass.from_xml("<doc><body>a &amp; b</body></doc>").body).to eq("a & b")
171
+ end
172
+ end
173
+ end
@@ -0,0 +1,133 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "spec_helper"
4
+
5
+ # lutaml-model#88: attribute-value dispatch on a shared wire name.
6
+ # `map_element "component", to: :guidance, when_attribute: { "type" =>
7
+ # "guidance" }` selects only the occurrences whose sibling attribute carries
8
+ # the expected value, and re-emits the discriminator on serialization.
9
+ class WhenAttrComponent < Lutaml::Model::Serializable
10
+ attribute :text, :string
11
+
12
+ xml do
13
+ element "component"
14
+ map_element "text", to: :text
15
+ end
16
+ end
17
+
18
+ class WhenAttrRequirement < Lutaml::Model::Serializable
19
+ attribute :guidance, WhenAttrComponent, collection: true
20
+ attribute :purpose, WhenAttrComponent, collection: true
21
+ attribute :test_method, WhenAttrComponent, collection: true
22
+
23
+ xml do
24
+ element "requirement"
25
+ map_element "component", to: :guidance,
26
+ when_attribute: { "type" => "guidance" }
27
+ map_element "component", to: :purpose,
28
+ when_attribute: { "type" => "purpose" }
29
+ map_element "component", to: :test_method,
30
+ when_attribute: { type: "test-method" }
31
+ end
32
+ end
33
+
34
+ RSpec.describe "when_attribute discriminator mappings" do
35
+ before do
36
+ stub_const("WhenAttr::Component", WhenAttrComponent)
37
+ stub_const("WhenAttr::Requirement", WhenAttrRequirement)
38
+ end
39
+
40
+ let(:xml) do
41
+ <<~XML
42
+ <requirement>
43
+ <component type="guidance"><text>g1</text></component>
44
+ <component type="purpose"><text>p1</text></component>
45
+ <component type="guidance"><text>g2</text></component>
46
+ <component type="test-method"><text>t1</text></component>
47
+ </requirement>
48
+ XML
49
+ end
50
+
51
+ it "dispatches occurrences by the discriminator value" do
52
+ req = WhenAttr::Requirement.from_xml(xml)
53
+
54
+ expect(req.guidance.map(&:text)).to eq(%w[g1 g2])
55
+ expect(req.purpose.map(&:text)).to eq(["p1"])
56
+ expect(req.test_method.map(&:text)).to eq(["t1"])
57
+ end
58
+
59
+ it "keeps document order within each target attribute" do
60
+ req = WhenAttr::Requirement.from_xml(xml)
61
+ expect(req.guidance.map(&:text)).to eq(%w[g1 g2])
62
+ end
63
+
64
+ it "drops occurrences whose discriminator is not covered" do
65
+ doc = '<requirement><component type="unknown"><text>x</text></component></requirement>'
66
+ req = WhenAttr::Requirement.from_xml(doc)
67
+
68
+ expect(req.guidance).to be_empty
69
+ expect(req.purpose).to be_empty
70
+ end
71
+
72
+ it "re-emits the discriminator attribute on serialization" do
73
+ round = WhenAttr::Requirement.from_xml(WhenAttr::Requirement.from_xml(xml).to_xml)
74
+
75
+ expect(round.guidance.map(&:text)).to eq(%w[g1 g2])
76
+ expect(round.purpose.map(&:text)).to eq(["p1"])
77
+ expect(round.test_method.map(&:text)).to eq(["t1"])
78
+
79
+ serialized = WhenAttr::Requirement.from_xml(xml).to_xml
80
+ expect(serialized).to include('type="guidance"')
81
+ expect(serialized).to include('type="purpose"')
82
+ expect(serialized).to include('type="test-method"')
83
+ end
84
+
85
+ it "does not emit a discriminator for ordinary rules" do
86
+ plain = Class.new(Lutaml::Model::Serializable) do
87
+ attribute :text, :string
88
+
89
+ xml do
90
+ element "plain"
91
+ map_element "text", to: :text
92
+ end
93
+ end
94
+
95
+ expect(plain.from_xml("<plain><text>t</text></plain>").to_xml)
96
+ .not_to include("type=")
97
+ end
98
+
99
+ it "survives deep_dup of mappings" do
100
+ mapping = WhenAttr::Requirement.mappings_for(:xml)
101
+ duped = mapping.deep_dup.mappings.find do |r|
102
+ r.to == :guidance
103
+ end
104
+
105
+ expect(duped.when_attribute).to eq("type" => "guidance")
106
+ end
107
+
108
+ it "rejects non-string/symbol discriminator shapes" do
109
+ expect do
110
+ Class.new(Lutaml::Model::Serializable) do
111
+ attribute :x, :string
112
+
113
+ xml do
114
+ element "x"
115
+ map_element "x", to: :x, when_attribute: { "type" => 42 }
116
+ end
117
+ end
118
+ end.to raise_error(Lutaml::Model::IncorrectMappingArgumentsError, /when_attribute/)
119
+ end
120
+
121
+ it "is rejected on attribute mappings" do
122
+ expect do
123
+ Class.new(Lutaml::Model::Serializable) do
124
+ attribute :x, :string
125
+
126
+ xml do
127
+ element "x"
128
+ map_attribute "x", to: :x, when_attribute: { "type" => "guidance" }
129
+ end
130
+ end
131
+ end.to raise_error(ArgumentError)
132
+ end
133
+ end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: lutaml-model
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.8.41
4
+ version: 0.8.43
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ribose Inc.
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2026-09-19 00:00:00.000000000 Z
11
+ date: 2026-09-20 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: base64
@@ -1695,6 +1695,7 @@ files:
1695
1695
  - spec/lutaml/model/attribute_form_default_spec.rb
1696
1696
  - spec/lutaml/model/attribute_restriction_spec.rb
1697
1697
  - spec/lutaml/model/attribute_spec.rb
1698
+ - spec/lutaml/model/attributes_memo_clear_race_spec.rb
1698
1699
  - spec/lutaml/model/boot_manifest_spec.rb
1699
1700
  - spec/lutaml/model/cached_type_resolver_spec.rb
1700
1701
  - spec/lutaml/model/cardinality_strict_spec.rb
@@ -1737,6 +1738,7 @@ files:
1737
1738
  - spec/lutaml/model/global_register_spec.rb
1738
1739
  - spec/lutaml/model/group_spec.rb
1739
1740
  - spec/lutaml/model/hash/adapter_spec.rb
1741
+ - spec/lutaml/model/hydrate_only_mapping_spec.rb
1740
1742
  - spec/lutaml/model/import_registry_spec.rb
1741
1743
  - spec/lutaml/model/included_spec.rb
1742
1744
  - spec/lutaml/model/inheritance_spec.rb
@@ -1765,6 +1767,7 @@ files:
1765
1767
  - spec/lutaml/model/opal_smoke_spec.rb
1766
1768
  - spec/lutaml/model/optimization_spec.rb
1767
1769
  - spec/lutaml/model/ordered_content_spec.rb
1770
+ - spec/lutaml/model/parse_dispatch_hoist_spec.rb
1768
1771
  - spec/lutaml/model/parsed_model_mutation_spec.rb
1769
1772
  - spec/lutaml/model/phantom_value_spec.rb
1770
1773
  - spec/lutaml/model/polymorphic_spec.rb
@@ -2003,6 +2006,7 @@ files:
2003
2006
  - spec/lutaml/xml/validate_xml_with_constructs_spec.rb
2004
2007
  - spec/lutaml/xml/validate_xml_with_spec.rb
2005
2008
  - spec/lutaml/xml/w3c_types_spec.rb
2009
+ - spec/lutaml/xml/when_attribute_spec.rb
2006
2010
  - spec/lutaml/xml/xml_adapter_spec.rb
2007
2011
  - spec/lutaml/xml/xml_declaration_spec.rb
2008
2012
  - spec/lutaml/xml/xml_element_guard_spec.rb