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.
- checksums.yaml +4 -4
- data/docs/_guides/advanced-mapping.adoc +32 -0
- data/lib/lutaml/key_value/mapping.rb +3 -1
- data/lib/lutaml/key_value/mapping_rule.rb +4 -1
- data/lib/lutaml/key_value/transform.rb +2 -0
- data/lib/lutaml/key_value/transformation/rule_compiler.rb +3 -0
- data/lib/lutaml/model/attribute.rb +71 -32
- data/lib/lutaml/model/collection_handler.rb +14 -2
- data/lib/lutaml/model/global_context.rb +25 -1
- data/lib/lutaml/model/mapping/mapping_rule.rb +86 -20
- data/lib/lutaml/model/register.rb +7 -1
- data/lib/lutaml/model/serialize/initialization.rb +17 -3
- data/lib/lutaml/model/serialize.rb +12 -3
- data/lib/lutaml/model/type/string.rb +1 -1
- data/lib/lutaml/model/version.rb +1 -1
- data/lib/lutaml/xml/mapping.rb +20 -1
- data/lib/lutaml/xml/mapping_rule.rb +13 -1
- data/lib/lutaml/xml/model_transform.rb +90 -14
- data/lib/lutaml/xml/parse_session.rb +11 -2
- data/lib/lutaml/xml/transformation/element_builder.rb +25 -6
- data/lib/lutaml/xml/transformation/rule_compiler.rb +2 -0
- data/spec/lutaml/key_value/transformation/rule_compiler_spec.rb +16 -6
- data/spec/lutaml/key_value/transformation_spec.rb +4 -1
- data/spec/lutaml/model/attributes_memo_clear_race_spec.rb +90 -0
- data/spec/lutaml/model/hydrate_only_mapping_spec.rb +47 -0
- data/spec/lutaml/model/parse_dispatch_hoist_spec.rb +173 -0
- data/spec/lutaml/xml/when_attribute_spec.rb +133 -0
- metadata +6 -2
|
@@ -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 & b</body></doc>").body).to eq("a & b")
|
|
170
|
+
expect(klass.from_xml("<doc><body>a & 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.
|
|
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-
|
|
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
|