lutaml-model 0.8.44 → 0.8.46
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 +69 -0
- data/lib/compat/opal/lutaml_model_boot.rb +1 -0
- data/lib/lutaml/key_value/mapping.rb +32 -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/error/unknown_discriminator_error.rb +11 -0
- data/lib/lutaml/model/mapping/mapping.rb +30 -0
- data/lib/lutaml/model/version.rb +1 -1
- data/lib/lutaml/model.rb +2 -0
- data/lib/lutaml/xml/mapping.rb +3 -14
- data/lib/lutaml/xml/mapping_rule.rb +7 -1
- data/lib/lutaml/xml/model_transform.rb +32 -0
- data/lib/lutaml/xml/parse_session.rb +36 -14
- data/spec/lutaml/key_value/when_attribute_spec.rb +348 -0
- data/spec/lutaml/xml/when_attribute_unmatched_spec.rb +169 -0
- metadata +4 -1
data/lib/lutaml/model.rb
CHANGED
|
@@ -138,6 +138,8 @@ module Lutaml
|
|
|
138
138
|
autoload :ElementCountOutOfRangeError,
|
|
139
139
|
"#{__dir__}/model/error/element_count_out_of_range_error"
|
|
140
140
|
autoload :ValidationError, "#{__dir__}/model/error/validation_error"
|
|
141
|
+
autoload :UnknownDiscriminatorError,
|
|
142
|
+
"#{__dir__}/model/error/unknown_discriminator_error"
|
|
141
143
|
autoload :TypeNotEnabledError,
|
|
142
144
|
"#{__dir__}/model/error/type_not_enabled_error"
|
|
143
145
|
autoload :TypeError, "#{__dir__}/model/error/type_error"
|
data/lib/lutaml/xml/mapping.rb
CHANGED
|
@@ -528,6 +528,7 @@ module Lutaml
|
|
|
528
528
|
transform: {},
|
|
529
529
|
value_map: {},
|
|
530
530
|
when_attribute: {},
|
|
531
|
+
unmatched: :drop,
|
|
531
532
|
form: nil,
|
|
532
533
|
documentation: nil,
|
|
533
534
|
xsd_type: (xsd_type_provided = false
|
|
@@ -539,6 +540,7 @@ module Lutaml
|
|
|
539
540
|
)
|
|
540
541
|
|
|
541
542
|
validate_when_attribute!(when_attribute) unless when_attribute.empty?
|
|
543
|
+
validate_unmatched!(unmatched, when_attribute)
|
|
542
544
|
|
|
543
545
|
# Warn if prefix parameter is provided
|
|
544
546
|
if prefix_provided != false
|
|
@@ -588,6 +590,7 @@ module Lutaml
|
|
|
588
590
|
documentation: documentation,
|
|
589
591
|
raw: raw,
|
|
590
592
|
when_attribute: when_attribute,
|
|
593
|
+
unmatched: unmatched,
|
|
591
594
|
)
|
|
592
595
|
# Store rules with the same element name in an array to support
|
|
593
596
|
# multiple mapping rules for the same element name with different target types
|
|
@@ -611,20 +614,6 @@ module Lutaml
|
|
|
611
614
|
end
|
|
612
615
|
end
|
|
613
616
|
|
|
614
|
-
# lutaml-model#88: attribute-value discriminator. Keys are wire
|
|
615
|
-
# attribute names, values the expected string value.
|
|
616
|
-
def validate_when_attribute!(when_attribute)
|
|
617
|
-
when_attribute.each do |k, v|
|
|
618
|
-
next if (k.is_a?(::String) || k.is_a?(::Symbol)) &&
|
|
619
|
-
(v.is_a?(::String) || v.is_a?(::Symbol))
|
|
620
|
-
|
|
621
|
-
raise Lutaml::Model::IncorrectMappingArgumentsError,
|
|
622
|
-
"when_attribute expects string/symbol attribute names " \
|
|
623
|
-
"mapped to string/symbol values, got " \
|
|
624
|
-
"#{k.inspect} => #{v.inspect}"
|
|
625
|
-
end
|
|
626
|
-
end
|
|
627
|
-
|
|
628
617
|
def map_attribute(
|
|
629
618
|
name,
|
|
630
619
|
to: nil,
|
|
@@ -11,7 +11,8 @@ module Lutaml
|
|
|
11
11
|
:delimiter,
|
|
12
12
|
:form,
|
|
13
13
|
:documentation,
|
|
14
|
-
:raw
|
|
14
|
+
:raw,
|
|
15
|
+
:unmatched
|
|
15
16
|
|
|
16
17
|
# Writers for deep_dup (preserves exact object references)
|
|
17
18
|
attr_accessor :namespace, :prefix, :namespace_class
|
|
@@ -38,6 +39,7 @@ module Lutaml
|
|
|
38
39
|
transform: {},
|
|
39
40
|
value_map: {},
|
|
40
41
|
when_attribute: {},
|
|
42
|
+
unmatched: :drop,
|
|
41
43
|
as_list: nil,
|
|
42
44
|
delimiter: nil,
|
|
43
45
|
form: nil,
|
|
@@ -66,6 +68,9 @@ module Lutaml
|
|
|
66
68
|
# Store original namespace parameter to preserve :inherit symbol
|
|
67
69
|
@namespace_param = namespace
|
|
68
70
|
|
|
71
|
+
# lutaml-model#88: policy for occurrences claimed by no rule
|
|
72
|
+
@unmatched = unmatched
|
|
73
|
+
|
|
69
74
|
# Normalize namespace to XmlNamespace class
|
|
70
75
|
@namespace_class = normalize_namespace(namespace)
|
|
71
76
|
@namespace = @namespace_class == :blank ? nil : @namespace_class&.uri
|
|
@@ -273,6 +278,7 @@ module Lutaml
|
|
|
273
278
|
documentation: @documentation,
|
|
274
279
|
raw: @raw,
|
|
275
280
|
when_attribute: when_attribute,
|
|
281
|
+
unmatched: @unmatched,
|
|
276
282
|
).tap do |dup_rule|
|
|
277
283
|
# Manually preserve the exact @namespace_class object to avoid
|
|
278
284
|
# recreating anonymous classes (which would have different object_ids)
|
|
@@ -859,6 +859,35 @@ _effective_register)
|
|
|
859
859
|
cache[type_class] = type_class.mappings_for(:xml)&.namespace_class
|
|
860
860
|
end
|
|
861
861
|
|
|
862
|
+
# lutaml-model#88: fail closed for `unmatched: :raise`. An
|
|
863
|
+
# occurrence no rule on the wire name claims — no discriminator
|
|
864
|
+
# match, no plain sibling — is exactly the data the default policy
|
|
865
|
+
# silently drops.
|
|
866
|
+
def check_unmatched_discriminators!(children, rule, rule_names, session)
|
|
867
|
+
plain_names = session.plain_element_rule_names
|
|
868
|
+
return if rule_names.any? { |name| plain_names[name] }
|
|
869
|
+
|
|
870
|
+
group = rule_names.filter_map do |name|
|
|
871
|
+
session.when_attribute_siblings_by_name[name]
|
|
872
|
+
end.flatten
|
|
873
|
+
tested = group.flat_map { |sibling| sibling.when_attribute.keys }
|
|
874
|
+
.uniq.map(&:to_s)
|
|
875
|
+
children.each do |child|
|
|
876
|
+
next if group.any? { |sibling| sibling.matches_when_attribute?(child) }
|
|
877
|
+
|
|
878
|
+
values = tested.filter_map do |name|
|
|
879
|
+
value = child.find_attribute_value(name)
|
|
880
|
+
"#{name}=#{value.inspect}" if value
|
|
881
|
+
end
|
|
882
|
+
raise ::Lutaml::Model::UnknownDiscriminatorError,
|
|
883
|
+
"Element <#{rule.name}> (#{values.join(', ')}) is claimed by " \
|
|
884
|
+
"no rule: it matches no when_attribute discriminator and no " \
|
|
885
|
+
"plain rule shares the name. Cover the value, add a plain " \
|
|
886
|
+
"rule, or opt out with unmatched: :drop"
|
|
887
|
+
end
|
|
888
|
+
nil
|
|
889
|
+
end
|
|
890
|
+
|
|
862
891
|
def value_for_rule(session, rule, options, cached_attr = nil,
|
|
863
892
|
extra_rule_names = nil)
|
|
864
893
|
doc = session.doc
|
|
@@ -1058,6 +1087,9 @@ _effective_register)
|
|
|
1058
1087
|
# no discriminator claimed — so each occurrence is captured
|
|
1059
1088
|
# exactly once, mirroring ordered-serialization routing.
|
|
1060
1089
|
if rule.when_attribute?
|
|
1090
|
+
if rule.unmatched == :raise
|
|
1091
|
+
check_unmatched_discriminators!(children, rule, rule_names, session)
|
|
1092
|
+
end
|
|
1061
1093
|
children = children.select do |child|
|
|
1062
1094
|
rule.matches_when_attribute?(child)
|
|
1063
1095
|
end
|
|
@@ -66,27 +66,49 @@ module Lutaml
|
|
|
66
66
|
end
|
|
67
67
|
|
|
68
68
|
# lutaml-model#88: element wire names partitioned by `when_attribute`
|
|
69
|
-
# rules
|
|
70
|
-
#
|
|
71
|
-
#
|
|
72
|
-
#
|
|
73
|
-
#
|
|
74
|
-
#
|
|
75
|
-
#
|
|
76
|
-
|
|
77
|
-
|
|
69
|
+
# rules. One pass builds two views over the mapping's element rules:
|
|
70
|
+
#
|
|
71
|
+
# - name -> discriminator rules. A plain rule on a partitioned name
|
|
72
|
+
# captures only occurrences no discriminator claimed — the mirror
|
|
73
|
+
# of ordered-serialization routing, so each occurrence is captured
|
|
74
|
+
# exactly once instead of double-captured by the plain rule and
|
|
75
|
+
# its discriminator sibling.
|
|
76
|
+
# - name -> true when a plain element rule also holds the name.
|
|
77
|
+
# Everything on the name is then claimed, so `unmatched: :raise`
|
|
78
|
+
# never fires.
|
|
79
|
+
#
|
|
80
|
+
# Empty unless the mapping uses when_attribute at all; non-Serialize
|
|
81
|
+
# custom models have no mappings to partition.
|
|
82
|
+
def when_attribute_partition
|
|
83
|
+
@when_attribute_partition ||=
|
|
78
84
|
if instance_is_serialize && (mapping = xml_mapping)
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
85
|
+
siblings = {}
|
|
86
|
+
plain_names = {}
|
|
87
|
+
mapping.mappings.each do |rule|
|
|
88
|
+
next if rule.attribute? || rule.content_mapping? ||
|
|
89
|
+
rule.raw_mapping? || rule.cdata
|
|
82
90
|
|
|
83
|
-
|
|
91
|
+
pairs = rule.when_attribute
|
|
92
|
+
if pairs && !pairs.empty?
|
|
93
|
+
(siblings[rule.name.to_s] ||= []) << rule
|
|
94
|
+
else
|
|
95
|
+
plain_names[rule.name.to_s] = true
|
|
96
|
+
end
|
|
84
97
|
end
|
|
98
|
+
[siblings, plain_names]
|
|
85
99
|
else
|
|
86
|
-
{}
|
|
100
|
+
[{}, {}]
|
|
87
101
|
end
|
|
88
102
|
end
|
|
89
103
|
|
|
104
|
+
def when_attribute_siblings_by_name
|
|
105
|
+
when_attribute_partition[0]
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
def plain_element_rule_names
|
|
109
|
+
when_attribute_partition[1]
|
|
110
|
+
end
|
|
111
|
+
|
|
90
112
|
def model_class
|
|
91
113
|
@model_class ||= instance.class
|
|
92
114
|
end
|
|
@@ -0,0 +1,348 @@
|
|
|
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 "DSL validation" do
|
|
289
|
+
it "rejects non-string/symbol pairs" do
|
|
290
|
+
expect do
|
|
291
|
+
Class.new(Lutaml::Model::Serializable) do
|
|
292
|
+
attribute :x, :string
|
|
293
|
+
|
|
294
|
+
json do
|
|
295
|
+
map "x", to: :x, when_attribute: { "type" => 42 }
|
|
296
|
+
end
|
|
297
|
+
end
|
|
298
|
+
end.to raise_error(Lutaml::Model::IncorrectMappingArgumentsError, /when_attribute/)
|
|
299
|
+
end
|
|
300
|
+
|
|
301
|
+
it "rejects unmatched without when_attribute" do
|
|
302
|
+
expect do
|
|
303
|
+
Class.new(Lutaml::Model::Serializable) do
|
|
304
|
+
attribute :x, :string
|
|
305
|
+
|
|
306
|
+
json do
|
|
307
|
+
map "x", to: :x, unmatched: :raise
|
|
308
|
+
end
|
|
309
|
+
end
|
|
310
|
+
end.to raise_error(Lutaml::Model::IncorrectMappingArgumentsError, /when_attribute/)
|
|
311
|
+
end
|
|
312
|
+
|
|
313
|
+
it "rejects unknown policy values" do
|
|
314
|
+
expect do
|
|
315
|
+
Class.new(Lutaml::Model::Serializable) do
|
|
316
|
+
attribute :x, :string
|
|
317
|
+
|
|
318
|
+
json do
|
|
319
|
+
map "x", to: :x, when_attribute: { "type" => "a" },
|
|
320
|
+
unmatched: :explode
|
|
321
|
+
end
|
|
322
|
+
end
|
|
323
|
+
end.to raise_error(Lutaml::Model::IncorrectMappingArgumentsError, /unmatched/)
|
|
324
|
+
end
|
|
325
|
+
|
|
326
|
+
it "rejects when_attribute combined with custom methods" do
|
|
327
|
+
expect do
|
|
328
|
+
Class.new(Lutaml::Model::Serializable) do
|
|
329
|
+
attribute :x, :string
|
|
330
|
+
|
|
331
|
+
json do
|
|
332
|
+
map "x", to: :x, when_attribute: { "type" => "a" },
|
|
333
|
+
with: { to: :x_to, from: :x_from }
|
|
334
|
+
end
|
|
335
|
+
end
|
|
336
|
+
end.to raise_error(Lutaml::Model::IncorrectMappingArgumentsError, /when_attribute/)
|
|
337
|
+
end
|
|
338
|
+
|
|
339
|
+
it "survives deep_dup of mappings" do
|
|
340
|
+
rule = KvWhenRequirement.mappings_for(:json).mappings.find do |r|
|
|
341
|
+
r.to == :guidance
|
|
342
|
+
end
|
|
343
|
+
|
|
344
|
+
expect(rule.when_attribute).to eq("type" => "guidance")
|
|
345
|
+
expect(rule.deep_dup.when_attribute).to eq("type" => "guidance")
|
|
346
|
+
end
|
|
347
|
+
end
|
|
348
|
+
end
|