lutaml-model 0.8.45 → 0.8.47
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 +77 -11
- data/lib/lutaml/key_value/mapping.rb +65 -6
- data/lib/lutaml/key_value/mapping_rule.rb +9 -0
- data/lib/lutaml/key_value/transform.rb +186 -7
- data/lib/lutaml/key_value/transformation/rule_compiler.rb +2 -0
- data/lib/lutaml/key_value/transformation.rb +113 -2
- data/lib/lutaml/model/mapping/mapping.rb +71 -0
- data/lib/lutaml/model/version.rb +1 -1
- data/lib/lutaml/xml/mapping.rb +42 -29
- data/spec/lutaml/key_value/when_attribute_spec.rb +428 -0
- data/spec/lutaml/xml/when_attribute_spec.rb +93 -0
- metadata +2 -1
|
@@ -106,6 +106,77 @@ module Lutaml
|
|
|
106
106
|
|
|
107
107
|
attr_accessor :importable_mappings
|
|
108
108
|
|
|
109
|
+
# lutaml-model#88: attribute-value discriminator. Keys are wire
|
|
110
|
+
# names (an XML attribute, or an object key in key-value formats),
|
|
111
|
+
# values the expected string value. Shared by the XML and KV DSLs.
|
|
112
|
+
def validate_when_attribute!(when_attribute)
|
|
113
|
+
when_attribute.each do |k, v|
|
|
114
|
+
next if (k.is_a?(::String) || k.is_a?(::Symbol)) &&
|
|
115
|
+
(v.is_a?(::String) || v.is_a?(::Symbol))
|
|
116
|
+
|
|
117
|
+
raise Lutaml::Model::IncorrectMappingArgumentsError,
|
|
118
|
+
"when_attribute expects string/symbol attribute names " \
|
|
119
|
+
"mapped to string/symbol values, got " \
|
|
120
|
+
"#{k.inspect} => #{v.inspect}"
|
|
121
|
+
end
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
# lutaml-model#88: what a parse does with an occurrence that no
|
|
125
|
+
# rule on the wire name claims — :drop it (default) or :raise
|
|
126
|
+
# UnknownDiscriminatorError. Only meaningful on discriminator rules.
|
|
127
|
+
def validate_unmatched!(unmatched, when_attribute)
|
|
128
|
+
unless %i[drop raise].include?(unmatched)
|
|
129
|
+
raise Lutaml::Model::IncorrectMappingArgumentsError,
|
|
130
|
+
"unmatched expects :drop or :raise, got #{unmatched.inspect}"
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
return unless when_attribute.empty? && unmatched != :drop
|
|
134
|
+
|
|
135
|
+
raise Lutaml::Model::IncorrectMappingArgumentsError,
|
|
136
|
+
"unmatched only applies to rules declared with when_attribute"
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
# lutaml-model#88: grouped discriminator form.
|
|
140
|
+
# `when_attribute: "type"` names the discriminator key; `to:` maps
|
|
141
|
+
# each wire value to its target attribute. Validates the shape and
|
|
142
|
+
# returns the group pairs — the DSL then expands one rule per
|
|
143
|
+
# value, so the compiled rules are identical to the per-rule form.
|
|
144
|
+
def when_attribute_group(when_attribute, to)
|
|
145
|
+
if when_attribute.to_s.empty?
|
|
146
|
+
raise Lutaml::Model::IncorrectMappingArgumentsError,
|
|
147
|
+
"when_attribute group key cannot be empty"
|
|
148
|
+
end
|
|
149
|
+
unless to.is_a?(::Hash) && !to.empty?
|
|
150
|
+
raise Lutaml::Model::IncorrectMappingArgumentsError,
|
|
151
|
+
"when_attribute: #{when_attribute.inspect} is the grouped " \
|
|
152
|
+
"form and requires to: { value => attribute }, got " \
|
|
153
|
+
"to: #{to.inspect}"
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
to.each do |value, target|
|
|
157
|
+
next if (value.is_a?(::String) || value.is_a?(::Symbol)) &&
|
|
158
|
+
(target.is_a?(::String) || target.is_a?(::Symbol))
|
|
159
|
+
|
|
160
|
+
raise Lutaml::Model::IncorrectMappingArgumentsError,
|
|
161
|
+
"when_attribute group expects string/symbol values mapped " \
|
|
162
|
+
"to string/symbol attribute names, got " \
|
|
163
|
+
"#{value.inspect} => #{target.inspect}"
|
|
164
|
+
end
|
|
165
|
+
to
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
# The per-rule `{ name => value }` form maps ONE attribute; a Hash
|
|
169
|
+
# `to:` only makes sense with the grouped form.
|
|
170
|
+
def reject_ambiguous_when_attribute!(when_attribute, to)
|
|
171
|
+
return unless when_attribute.is_a?(::Hash) && !when_attribute.empty? &&
|
|
172
|
+
to.is_a?(::Hash)
|
|
173
|
+
|
|
174
|
+
raise Lutaml::Model::IncorrectMappingArgumentsError,
|
|
175
|
+
"to: must name a single attribute when when_attribute is the " \
|
|
176
|
+
"per-rule { name => value } form; the grouped form is " \
|
|
177
|
+
'when_attribute: "key", to: { value => attribute }'
|
|
178
|
+
end
|
|
179
|
+
|
|
109
180
|
def register(register_id = nil)
|
|
110
181
|
register_id ||= Lutaml::Model::Config.default_register
|
|
111
182
|
Lutaml::Model::GlobalRegister.lookup(register_id)
|
data/lib/lutaml/model/version.rb
CHANGED
data/lib/lutaml/xml/mapping.rb
CHANGED
|
@@ -539,6 +539,48 @@ module Lutaml
|
|
|
539
539
|
name, to, with, render_nil, render_empty, type: TYPES[:element]
|
|
540
540
|
)
|
|
541
541
|
|
|
542
|
+
# lutaml-model#88: grouped form — `when_attribute: "type"` plus
|
|
543
|
+
# `to: { value => attribute }` expands to one rule per value at
|
|
544
|
+
# DSL time, so the compiled rules are identical to the per-rule
|
|
545
|
+
# form and nothing downstream changes.
|
|
546
|
+
if when_attribute.is_a?(::String) || when_attribute.is_a?(::Symbol)
|
|
547
|
+
# These locals are `false` when their kwarg default ran and
|
|
548
|
+
# nil when the caller passed the kwarg explicitly (planned
|
|
549
|
+
# locals read as nil, so defined? is useless here).
|
|
550
|
+
if namespace_set.nil? || prefix_provided.nil? ||
|
|
551
|
+
xsd_type_provided.nil?
|
|
552
|
+
raise Lutaml::Model::IncorrectMappingArgumentsError,
|
|
553
|
+
"namespace/prefix/xsd_type are not supported in the " \
|
|
554
|
+
"grouped when_attribute form; declare rules individually"
|
|
555
|
+
end
|
|
556
|
+
|
|
557
|
+
when_attribute_group(when_attribute, to).each do |value, target|
|
|
558
|
+
map_element(
|
|
559
|
+
name,
|
|
560
|
+
to: target,
|
|
561
|
+
render_nil: render_nil,
|
|
562
|
+
render_default: render_default,
|
|
563
|
+
render_empty: render_empty,
|
|
564
|
+
treat_nil: treat_nil,
|
|
565
|
+
treat_empty: treat_empty,
|
|
566
|
+
treat_omitted: treat_omitted,
|
|
567
|
+
with: with,
|
|
568
|
+
delegate: delegate,
|
|
569
|
+
cdata: cdata,
|
|
570
|
+
polymorphic: polymorphic,
|
|
571
|
+
transform: transform,
|
|
572
|
+
value_map: value_map,
|
|
573
|
+
when_attribute: { when_attribute.to_s => value.to_s },
|
|
574
|
+
unmatched: unmatched,
|
|
575
|
+
form: form,
|
|
576
|
+
documentation: documentation,
|
|
577
|
+
raw: raw,
|
|
578
|
+
)
|
|
579
|
+
end
|
|
580
|
+
return
|
|
581
|
+
end
|
|
582
|
+
reject_ambiguous_when_attribute!(when_attribute, to)
|
|
583
|
+
|
|
542
584
|
validate_when_attribute!(when_attribute) unless when_attribute.empty?
|
|
543
585
|
validate_unmatched!(unmatched, when_attribute)
|
|
544
586
|
|
|
@@ -614,35 +656,6 @@ module Lutaml
|
|
|
614
656
|
end
|
|
615
657
|
end
|
|
616
658
|
|
|
617
|
-
# lutaml-model#88: attribute-value discriminator. Keys are wire
|
|
618
|
-
# attribute names, values the expected string value.
|
|
619
|
-
def validate_when_attribute!(when_attribute)
|
|
620
|
-
when_attribute.each do |k, v|
|
|
621
|
-
next if (k.is_a?(::String) || k.is_a?(::Symbol)) &&
|
|
622
|
-
(v.is_a?(::String) || v.is_a?(::Symbol))
|
|
623
|
-
|
|
624
|
-
raise Lutaml::Model::IncorrectMappingArgumentsError,
|
|
625
|
-
"when_attribute expects string/symbol attribute names " \
|
|
626
|
-
"mapped to string/symbol values, got " \
|
|
627
|
-
"#{k.inspect} => #{v.inspect}"
|
|
628
|
-
end
|
|
629
|
-
end
|
|
630
|
-
|
|
631
|
-
# lutaml-model#88: what a parse does with an occurrence that no
|
|
632
|
-
# rule on the wire name claims — :drop it (default) or :raise
|
|
633
|
-
# UnknownDiscriminatorError. Only meaningful on discriminator rules.
|
|
634
|
-
def validate_unmatched!(unmatched, when_attribute)
|
|
635
|
-
unless %i[drop raise].include?(unmatched)
|
|
636
|
-
raise Lutaml::Model::IncorrectMappingArgumentsError,
|
|
637
|
-
"unmatched expects :drop or :raise, got #{unmatched.inspect}"
|
|
638
|
-
end
|
|
639
|
-
|
|
640
|
-
return unless when_attribute.empty? && unmatched != :drop
|
|
641
|
-
|
|
642
|
-
raise Lutaml::Model::IncorrectMappingArgumentsError,
|
|
643
|
-
"unmatched only applies to rules declared with when_attribute"
|
|
644
|
-
end
|
|
645
|
-
|
|
646
659
|
def map_attribute(
|
|
647
660
|
name,
|
|
648
661
|
to: nil,
|
|
@@ -0,0 +1,428 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "spec_helper"
|
|
4
|
+
|
|
5
|
+
# lutaml-model#88: `when_attribute` partitioning for key-value formats.
|
|
6
|
+
# The discriminator is an object key: items of the array at a shared wire
|
|
7
|
+
# key are claimed by the rule whose pairs their key values satisfy.
|
|
8
|
+
# Serialization merges every rule's items back under the one key, each
|
|
9
|
+
# item stamped with its rule's discriminator pairs.
|
|
10
|
+
class KvWhenComponent < Lutaml::Model::Serializable
|
|
11
|
+
attribute :text, :string
|
|
12
|
+
|
|
13
|
+
json do
|
|
14
|
+
map "text", to: :text
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
yaml do
|
|
18
|
+
map "text", to: :text
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
toml do
|
|
22
|
+
map "text", to: :text
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
hsh do
|
|
26
|
+
map "text", to: :text
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
class KvWhenRequirement < Lutaml::Model::Serializable
|
|
31
|
+
attribute :guidance, KvWhenComponent, collection: true
|
|
32
|
+
attribute :purpose, KvWhenComponent, collection: true
|
|
33
|
+
|
|
34
|
+
json do
|
|
35
|
+
map "component", to: :guidance,
|
|
36
|
+
when_attribute: { "type" => "guidance" }
|
|
37
|
+
map "component", to: :purpose,
|
|
38
|
+
when_attribute: { "type" => "purpose" }
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
yaml do
|
|
42
|
+
map "component", to: :guidance,
|
|
43
|
+
when_attribute: { "type" => "guidance" }
|
|
44
|
+
map "component", to: :purpose,
|
|
45
|
+
when_attribute: { "type" => "purpose" }
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
hsh do
|
|
49
|
+
map "component", to: :guidance,
|
|
50
|
+
when_attribute: { "type" => "guidance" }
|
|
51
|
+
map "component", to: :purpose,
|
|
52
|
+
when_attribute: { "type" => "purpose" }
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
RSpec.describe "when_attribute for key-value formats" do
|
|
57
|
+
before do
|
|
58
|
+
stub_const("KvWhen::Component", KvWhenComponent)
|
|
59
|
+
stub_const("KvWhen::Requirement", KvWhenRequirement)
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
let(:payload) do
|
|
63
|
+
{
|
|
64
|
+
"component" => [
|
|
65
|
+
{ "type" => "guidance", "text" => "g1" },
|
|
66
|
+
{ "type" => "purpose", "text" => "p1" },
|
|
67
|
+
{ "type" => "guidance", "text" => "g2" },
|
|
68
|
+
],
|
|
69
|
+
}
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
it "partitions json by the discriminator key" do
|
|
73
|
+
req = KvWhenRequirement.from_json(payload.to_json)
|
|
74
|
+
|
|
75
|
+
expect(req.guidance.map(&:text)).to eq(%w[g1 g2])
|
|
76
|
+
expect(req.purpose.map(&:text)).to eq(["p1"])
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
it "partitions yaml arrays of mappings" do
|
|
80
|
+
req = KvWhenRequirement.from_yaml(payload.to_yaml)
|
|
81
|
+
|
|
82
|
+
expect(req.guidance.map(&:text)).to eq(%w[g1 g2])
|
|
83
|
+
expect(req.purpose.map(&:text)).to eq(["p1"])
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
it "partitions hashes passed directly with symbol keys" do
|
|
87
|
+
req = KvWhenRequirement.from_hash(
|
|
88
|
+
component: [
|
|
89
|
+
{ type: "guidance", text: "g1" },
|
|
90
|
+
{ type: "purpose", text: "p1" },
|
|
91
|
+
],
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
expect(req.guidance.map(&:text)).to eq(["g1"])
|
|
95
|
+
expect(req.purpose.map(&:text)).to eq(["p1"])
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
it "keeps a single object occurrence as one item" do
|
|
99
|
+
req = KvWhenRequirement.from_hash(
|
|
100
|
+
"component" => { "type" => "purpose", "text" => "p1" },
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
expect(req.purpose.map(&:text)).to eq(["p1"])
|
|
104
|
+
expect(req.guidance).to be_empty
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
it "serializes one merged array with the discriminators stamped" do
|
|
108
|
+
req = KvWhenRequirement.from_json(payload.to_json)
|
|
109
|
+
out = req.to_json
|
|
110
|
+
|
|
111
|
+
items = JSON.parse(out)["component"]
|
|
112
|
+
expect(items).to eq([
|
|
113
|
+
{ "type" => "guidance", "text" => "g1" },
|
|
114
|
+
{ "type" => "guidance", "text" => "g2" },
|
|
115
|
+
{ "type" => "purpose", "text" => "p1" },
|
|
116
|
+
])
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
it "round-trips through yaml" do
|
|
120
|
+
req = KvWhenRequirement.from_yaml(payload.to_yaml)
|
|
121
|
+
again = KvWhenRequirement.from_yaml(req.to_yaml)
|
|
122
|
+
|
|
123
|
+
expect(again.guidance.map(&:text)).to eq(%w[g1 g2])
|
|
124
|
+
expect(again.purpose.map(&:text)).to eq(["p1"])
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
it "does not stamp over a key the item's own mapping writes" do
|
|
128
|
+
holder = Class.new(Lutaml::Model::Serializable) do
|
|
129
|
+
attribute :items, KvWhenComponent, collection: true
|
|
130
|
+
|
|
131
|
+
json do
|
|
132
|
+
map "component", to: :items,
|
|
133
|
+
when_attribute: { "type" => "guidance" }
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
hsh do
|
|
137
|
+
map "component", to: :items,
|
|
138
|
+
when_attribute: { "type" => "guidance" }
|
|
139
|
+
end
|
|
140
|
+
end
|
|
141
|
+
stub_const("KvWhen::Holder", holder)
|
|
142
|
+
|
|
143
|
+
out = holder.from_hash(
|
|
144
|
+
"component" => [{ "type" => "guidance", "text" => "g" }],
|
|
145
|
+
).to_json
|
|
146
|
+
expect(JSON.parse(out)["component"]).to eq(
|
|
147
|
+
[{ "type" => "guidance", "text" => "g" }],
|
|
148
|
+
)
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
describe "plain rule complement" do
|
|
152
|
+
let(:holder) do
|
|
153
|
+
Class.new(Lutaml::Model::Serializable) do
|
|
154
|
+
attribute :special, KvWhenComponent, collection: true
|
|
155
|
+
attribute :plain, KvWhenComponent, collection: true
|
|
156
|
+
|
|
157
|
+
json do
|
|
158
|
+
map "item", to: :special, when_attribute: { "kind" => "special" }
|
|
159
|
+
map "item", to: :plain
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
hsh do
|
|
163
|
+
map "item", to: :special, when_attribute: { "kind" => "special" }
|
|
164
|
+
map "item", to: :plain
|
|
165
|
+
end
|
|
166
|
+
end
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
it "captures occurrences no discriminator claimed exactly once" do
|
|
170
|
+
stub_const("KvWhen::Both", holder)
|
|
171
|
+
doc = holder.from_hash(
|
|
172
|
+
"item" => [
|
|
173
|
+
{ "kind" => "special", "text" => "b" },
|
|
174
|
+
{ "text" => "a" },
|
|
175
|
+
{ "kind" => "other", "text" => "x" },
|
|
176
|
+
],
|
|
177
|
+
)
|
|
178
|
+
|
|
179
|
+
expect(doc.special.map(&:text)).to eq(["b"])
|
|
180
|
+
expect(doc.plain.map(&:text)).to eq(%w[a x])
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
it "merges plain and stamped items into the one key" do
|
|
184
|
+
stub_const("KvWhen::Both", holder)
|
|
185
|
+
doc = holder.from_hash(
|
|
186
|
+
"item" => [
|
|
187
|
+
{ "kind" => "special", "text" => "b" },
|
|
188
|
+
{ "text" => "a" },
|
|
189
|
+
],
|
|
190
|
+
)
|
|
191
|
+
items = JSON.parse(doc.to_json)["item"]
|
|
192
|
+
|
|
193
|
+
expect(items).to eq([
|
|
194
|
+
{ "kind" => "special", "text" => "b" },
|
|
195
|
+
{ "text" => "a" },
|
|
196
|
+
])
|
|
197
|
+
end
|
|
198
|
+
end
|
|
199
|
+
|
|
200
|
+
describe "unmatched policy" do
|
|
201
|
+
let(:strict) do
|
|
202
|
+
Class.new(Lutaml::Model::Serializable) do
|
|
203
|
+
attribute :guidance, KvWhenComponent, collection: true
|
|
204
|
+
|
|
205
|
+
json do
|
|
206
|
+
map "component", to: :guidance,
|
|
207
|
+
when_attribute: { "type" => "guidance" },
|
|
208
|
+
unmatched: :raise
|
|
209
|
+
end
|
|
210
|
+
end
|
|
211
|
+
end
|
|
212
|
+
|
|
213
|
+
it "raises on an unclaimed discriminator value" do
|
|
214
|
+
stub_const("KvWhen::Strict", strict)
|
|
215
|
+
|
|
216
|
+
expect do
|
|
217
|
+
strict.from_json({ "component" => [
|
|
218
|
+
{ "type" => "guidance", "text" => "g" },
|
|
219
|
+
{ "type" => "nope", "text" => "x" },
|
|
220
|
+
] }.to_json)
|
|
221
|
+
end.to raise_error(Lutaml::Model::UnknownDiscriminatorError, /type="nope"/)
|
|
222
|
+
end
|
|
223
|
+
|
|
224
|
+
it "raises on an item without the discriminator key" do
|
|
225
|
+
stub_const("KvWhen::Strict", strict)
|
|
226
|
+
|
|
227
|
+
expect do
|
|
228
|
+
strict.from_json({ "component" => [{ "text" => "x" }] }.to_json)
|
|
229
|
+
end.to raise_error(Lutaml::Model::UnknownDiscriminatorError, /<component>/)
|
|
230
|
+
end
|
|
231
|
+
|
|
232
|
+
it "drops unclaimed occurrences by default" do
|
|
233
|
+
lenient = Class.new(Lutaml::Model::Serializable) do
|
|
234
|
+
attribute :guidance, KvWhenComponent, collection: true
|
|
235
|
+
|
|
236
|
+
json do
|
|
237
|
+
map "component", to: :guidance,
|
|
238
|
+
when_attribute: { "type" => "guidance" }
|
|
239
|
+
end
|
|
240
|
+
end
|
|
241
|
+
stub_const("KvWhen::Lenient", lenient)
|
|
242
|
+
|
|
243
|
+
doc = lenient.from_json({ "component" => [
|
|
244
|
+
{ "type" => "guidance", "text" => "g" },
|
|
245
|
+
{ "type" => "other", "text" => "x" },
|
|
246
|
+
] }.to_json)
|
|
247
|
+
expect(doc.guidance.map(&:text)).to eq(["g"])
|
|
248
|
+
end
|
|
249
|
+
end
|
|
250
|
+
|
|
251
|
+
describe "toml arrays of tables" do
|
|
252
|
+
let(:toml_holder) do
|
|
253
|
+
Class.new(Lutaml::Model::Serializable) do
|
|
254
|
+
attribute :guidance, KvWhenComponent, collection: true
|
|
255
|
+
attribute :purpose, KvWhenComponent, collection: true
|
|
256
|
+
|
|
257
|
+
toml do
|
|
258
|
+
map "component", to: :guidance,
|
|
259
|
+
when_attribute: { "type" => "guidance" }
|
|
260
|
+
map "component", to: :purpose,
|
|
261
|
+
when_attribute: { "type" => "purpose" }
|
|
262
|
+
end
|
|
263
|
+
end
|
|
264
|
+
end
|
|
265
|
+
|
|
266
|
+
it "partitions and round-trips" do
|
|
267
|
+
stub_const("KvWhen::TomlHolder", toml_holder)
|
|
268
|
+
src = <<~TOML
|
|
269
|
+
[[component]]
|
|
270
|
+
type = "guidance"
|
|
271
|
+
text = "g1"
|
|
272
|
+
|
|
273
|
+
[[component]]
|
|
274
|
+
type = "purpose"
|
|
275
|
+
text = "p1"
|
|
276
|
+
TOML
|
|
277
|
+
|
|
278
|
+
doc = toml_holder.from_toml(src)
|
|
279
|
+
expect(doc.guidance.map(&:text)).to eq(["g1"])
|
|
280
|
+
expect(doc.purpose.map(&:text)).to eq(["p1"])
|
|
281
|
+
|
|
282
|
+
again = toml_holder.from_toml(doc.to_toml)
|
|
283
|
+
expect(again.guidance.map(&:text)).to eq(["g1"])
|
|
284
|
+
expect(again.purpose.map(&:text)).to eq(["p1"])
|
|
285
|
+
end
|
|
286
|
+
end
|
|
287
|
+
|
|
288
|
+
describe "grouped form" do
|
|
289
|
+
let(:grouped) do
|
|
290
|
+
Class.new(Lutaml::Model::Serializable) do
|
|
291
|
+
attribute :guidance, KvWhenComponent, collection: true
|
|
292
|
+
attribute :purpose, KvWhenComponent, collection: true
|
|
293
|
+
|
|
294
|
+
json do
|
|
295
|
+
map "component", when_attribute: "type",
|
|
296
|
+
to: { guidance: :guidance, purpose: :purpose },
|
|
297
|
+
unmatched: :raise
|
|
298
|
+
end
|
|
299
|
+
|
|
300
|
+
hsh do
|
|
301
|
+
map "component", when_attribute: "type",
|
|
302
|
+
to: { guidance: :guidance, purpose: :purpose }
|
|
303
|
+
end
|
|
304
|
+
end
|
|
305
|
+
end
|
|
306
|
+
|
|
307
|
+
it "expands to one rule per value with identical behavior" do
|
|
308
|
+
stub_const("KvWhen::Grouped", grouped)
|
|
309
|
+
req = grouped.from_json(payload.to_json)
|
|
310
|
+
|
|
311
|
+
expect(req.guidance.map(&:text)).to eq(%w[g1 g2])
|
|
312
|
+
expect(req.purpose.map(&:text)).to eq(["p1"])
|
|
313
|
+
expect(JSON.parse(req.to_json)["component"]).to eq([
|
|
314
|
+
{ "type" => "guidance", "text" => "g1" },
|
|
315
|
+
{ "type" => "guidance", "text" => "g2" },
|
|
316
|
+
{ "type" => "purpose", "text" => "p1" },
|
|
317
|
+
])
|
|
318
|
+
|
|
319
|
+
rules = grouped.mappings_for(:json).mappings.select { |r| r.name == "component" }
|
|
320
|
+
expect(rules.map(&:when_attribute)).to eq(
|
|
321
|
+
[{ "type" => "guidance" }, { "type" => "purpose" }],
|
|
322
|
+
)
|
|
323
|
+
end
|
|
324
|
+
|
|
325
|
+
it "works through the hash format" do
|
|
326
|
+
stub_const("KvWhen::Grouped", grouped)
|
|
327
|
+
req = grouped.from_hash(
|
|
328
|
+
"component" => [{ type: "purpose", text: "p1" }],
|
|
329
|
+
)
|
|
330
|
+
|
|
331
|
+
expect(req.purpose.map(&:text)).to eq(["p1"])
|
|
332
|
+
expect(req.guidance).to be_empty
|
|
333
|
+
end
|
|
334
|
+
|
|
335
|
+
it "forwards the unmatched policy to every rule" do
|
|
336
|
+
stub_const("KvWhen::Grouped", grouped)
|
|
337
|
+
|
|
338
|
+
expect do
|
|
339
|
+
grouped.from_json({ "component" => [{ "type" => "nope" }] }.to_json)
|
|
340
|
+
end.to raise_error(Lutaml::Model::UnknownDiscriminatorError, /type="nope"/)
|
|
341
|
+
end
|
|
342
|
+
|
|
343
|
+
it "rejects the grouped form without a to: map" do
|
|
344
|
+
expect do
|
|
345
|
+
Class.new(Lutaml::Model::Serializable) do
|
|
346
|
+
attribute :x, :string
|
|
347
|
+
|
|
348
|
+
json do
|
|
349
|
+
map "x", to: :x, when_attribute: "type"
|
|
350
|
+
end
|
|
351
|
+
end
|
|
352
|
+
end.to raise_error(Lutaml::Model::IncorrectMappingArgumentsError, /to:/)
|
|
353
|
+
end
|
|
354
|
+
|
|
355
|
+
it "rejects a Hash to: with the per-rule form" do
|
|
356
|
+
expect do
|
|
357
|
+
Class.new(Lutaml::Model::Serializable) do
|
|
358
|
+
attribute :x, :string
|
|
359
|
+
|
|
360
|
+
json do
|
|
361
|
+
map "x", when_attribute: { "type" => "a" }, to: { "a" => :x }
|
|
362
|
+
end
|
|
363
|
+
end
|
|
364
|
+
end.to raise_error(Lutaml::Model::IncorrectMappingArgumentsError, /single attribute/)
|
|
365
|
+
end
|
|
366
|
+
end
|
|
367
|
+
|
|
368
|
+
describe "DSL validation" do
|
|
369
|
+
it "rejects non-string/symbol pairs" do
|
|
370
|
+
expect do
|
|
371
|
+
Class.new(Lutaml::Model::Serializable) do
|
|
372
|
+
attribute :x, :string
|
|
373
|
+
|
|
374
|
+
json do
|
|
375
|
+
map "x", to: :x, when_attribute: { "type" => 42 }
|
|
376
|
+
end
|
|
377
|
+
end
|
|
378
|
+
end.to raise_error(Lutaml::Model::IncorrectMappingArgumentsError, /when_attribute/)
|
|
379
|
+
end
|
|
380
|
+
|
|
381
|
+
it "rejects unmatched without when_attribute" do
|
|
382
|
+
expect do
|
|
383
|
+
Class.new(Lutaml::Model::Serializable) do
|
|
384
|
+
attribute :x, :string
|
|
385
|
+
|
|
386
|
+
json do
|
|
387
|
+
map "x", to: :x, unmatched: :raise
|
|
388
|
+
end
|
|
389
|
+
end
|
|
390
|
+
end.to raise_error(Lutaml::Model::IncorrectMappingArgumentsError, /when_attribute/)
|
|
391
|
+
end
|
|
392
|
+
|
|
393
|
+
it "rejects unknown policy values" do
|
|
394
|
+
expect do
|
|
395
|
+
Class.new(Lutaml::Model::Serializable) do
|
|
396
|
+
attribute :x, :string
|
|
397
|
+
|
|
398
|
+
json do
|
|
399
|
+
map "x", to: :x, when_attribute: { "type" => "a" },
|
|
400
|
+
unmatched: :explode
|
|
401
|
+
end
|
|
402
|
+
end
|
|
403
|
+
end.to raise_error(Lutaml::Model::IncorrectMappingArgumentsError, /unmatched/)
|
|
404
|
+
end
|
|
405
|
+
|
|
406
|
+
it "rejects when_attribute combined with custom methods" do
|
|
407
|
+
expect do
|
|
408
|
+
Class.new(Lutaml::Model::Serializable) do
|
|
409
|
+
attribute :x, :string
|
|
410
|
+
|
|
411
|
+
json do
|
|
412
|
+
map "x", to: :x, when_attribute: { "type" => "a" },
|
|
413
|
+
with: { to: :x_to, from: :x_from }
|
|
414
|
+
end
|
|
415
|
+
end
|
|
416
|
+
end.to raise_error(Lutaml::Model::IncorrectMappingArgumentsError, /when_attribute/)
|
|
417
|
+
end
|
|
418
|
+
|
|
419
|
+
it "survives deep_dup of mappings" do
|
|
420
|
+
rule = KvWhenRequirement.mappings_for(:json).mappings.find do |r|
|
|
421
|
+
r.to == :guidance
|
|
422
|
+
end
|
|
423
|
+
|
|
424
|
+
expect(rule.when_attribute).to eq("type" => "guidance")
|
|
425
|
+
expect(rule.deep_dup.when_attribute).to eq("type" => "guidance")
|
|
426
|
+
end
|
|
427
|
+
end
|
|
428
|
+
end
|
|
@@ -131,6 +131,99 @@ RSpec.describe "when_attribute discriminator mappings" do
|
|
|
131
131
|
end.to raise_error(ArgumentError)
|
|
132
132
|
end
|
|
133
133
|
|
|
134
|
+
describe "grouped form" do
|
|
135
|
+
let(:grouped) do
|
|
136
|
+
Class.new(Lutaml::Model::Serializable) do
|
|
137
|
+
attribute :guidance, WhenAttrComponent, collection: true
|
|
138
|
+
attribute :purpose, WhenAttrComponent, collection: true
|
|
139
|
+
|
|
140
|
+
xml do
|
|
141
|
+
element "requirement"
|
|
142
|
+
map_element "component", when_attribute: "type",
|
|
143
|
+
to: { "guidance" => :guidance,
|
|
144
|
+
"purpose" => :purpose },
|
|
145
|
+
unmatched: :raise
|
|
146
|
+
end
|
|
147
|
+
end
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
let(:grouped_xml) do
|
|
151
|
+
<<~XML
|
|
152
|
+
<requirement>
|
|
153
|
+
<component type="guidance"><text>g1</text></component>
|
|
154
|
+
<component type="purpose"><text>p1</text></component>
|
|
155
|
+
<component type="guidance"><text>g2</text></component>
|
|
156
|
+
</requirement>
|
|
157
|
+
XML
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
it "expands to one rule per value with identical behavior" do
|
|
161
|
+
stub_const("WhenAttr::Grouped", grouped)
|
|
162
|
+
req = grouped.from_xml(grouped_xml)
|
|
163
|
+
|
|
164
|
+
expect(req.guidance.map(&:text)).to eq(%w[g1 g2])
|
|
165
|
+
expect(req.purpose.map(&:text)).to eq(["p1"])
|
|
166
|
+
expect(req.to_xml).to include('type="guidance"')
|
|
167
|
+
|
|
168
|
+
rules = grouped.mappings_for(:xml).mappings.select { |r| r.name == "component" }
|
|
169
|
+
expect(rules.map(&:when_attribute)).to eq(
|
|
170
|
+
[{ "type" => "guidance" }, { "type" => "purpose" }],
|
|
171
|
+
)
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
it "forwards the unmatched policy to every rule" do
|
|
175
|
+
stub_const("WhenAttr::Grouped", grouped)
|
|
176
|
+
|
|
177
|
+
expect do
|
|
178
|
+
grouped.from_xml(
|
|
179
|
+
'<requirement><component type="other"><text>x</text></component></requirement>',
|
|
180
|
+
)
|
|
181
|
+
end.to raise_error(Lutaml::Model::UnknownDiscriminatorError, /type="other"/)
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
it "rejects the grouped form without a to: map" do
|
|
185
|
+
expect do
|
|
186
|
+
Class.new(Lutaml::Model::Serializable) do
|
|
187
|
+
attribute :x, :string
|
|
188
|
+
|
|
189
|
+
xml do
|
|
190
|
+
element "x"
|
|
191
|
+
map_element "x", to: :x, when_attribute: "type"
|
|
192
|
+
end
|
|
193
|
+
end
|
|
194
|
+
end.to raise_error(Lutaml::Model::IncorrectMappingArgumentsError, /to:/)
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
it "rejects a Hash to: with the per-rule form" do
|
|
198
|
+
expect do
|
|
199
|
+
Class.new(Lutaml::Model::Serializable) do
|
|
200
|
+
attribute :x, :string
|
|
201
|
+
|
|
202
|
+
xml do
|
|
203
|
+
element "x"
|
|
204
|
+
map_element "x", when_attribute: { "type" => "a" },
|
|
205
|
+
to: { "a" => :x }
|
|
206
|
+
end
|
|
207
|
+
end
|
|
208
|
+
end.to raise_error(Lutaml::Model::IncorrectMappingArgumentsError, /single attribute/)
|
|
209
|
+
end
|
|
210
|
+
|
|
211
|
+
it "rejects namespace in the grouped form" do
|
|
212
|
+
expect do
|
|
213
|
+
Class.new(Lutaml::Model::Serializable) do
|
|
214
|
+
attribute :x, :string
|
|
215
|
+
|
|
216
|
+
xml do
|
|
217
|
+
element "x"
|
|
218
|
+
map_element "x", when_attribute: "type",
|
|
219
|
+
to: { "a" => :x },
|
|
220
|
+
namespace: "https://example.com"
|
|
221
|
+
end
|
|
222
|
+
end
|
|
223
|
+
end.to raise_error(Lutaml::Model::IncorrectMappingArgumentsError, /grouped/)
|
|
224
|
+
end
|
|
225
|
+
end
|
|
226
|
+
|
|
134
227
|
# Orthogonality guard: `when_attribute` partitions occurrences across
|
|
135
228
|
# attributes; `polymorphic` dispatches the class of each hydrated item.
|
|
136
229
|
# Different axes over the same discriminator concept — they compose on
|