lutaml-model 0.8.45 → 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 +52 -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/mapping/mapping.rb +30 -0
- data/lib/lutaml/model/version.rb +1 -1
- data/lib/lutaml/xml/mapping.rb +0 -29
- data/spec/lutaml/key_value/when_attribute_spec.rb +348 -0
- metadata +2 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: b2f28f3c9d10541b92778ec5ab996c7b41110bb5784b4832d08e650a758db5f8
|
|
4
|
+
data.tar.gz: 22b516d73a1b76acf9421226b3f00204c3dec0cc46e4e1a9f1531ee979b1f8a2
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 779f779b5b3d76947306eec00d8652c4d8ca5e41a1a5846df1b34d5b52c03cc7e6302a53a4280d921aaddc96172daed52e1476b0662b60af40ea49558cba87f5
|
|
7
|
+
data.tar.gz: 1e19049f5e0e8a57218bdd2d6a3366f018c37b53a773c6f660d18eb071fa7736db3521762040314097b0ab8b0d9507bd2a778a06af17719935c013639854ff82
|
|
@@ -645,3 +645,55 @@ end
|
|
|
645
645
|
Given `<component type="guidance">`, `<component type="purpose">`,
|
|
646
646
|
`<component type="guidance">` in the source, `to_xml` re-emits exactly
|
|
647
647
|
that guidance, purpose, guidance sequence.
|
|
648
|
+
|
|
649
|
+
|
|
650
|
+
==== Key-value formats
|
|
651
|
+
|
|
652
|
+
The same discriminator works for JSON, YAML, TOML and Hash: the wire
|
|
653
|
+
name is an object key, and the discriminator is a sibling key's value
|
|
654
|
+
inside each item.
|
|
655
|
+
|
|
656
|
+
[source,ruby]
|
|
657
|
+
----
|
|
658
|
+
class KvRequirement < Lutaml::Model::Serializable
|
|
659
|
+
attribute :guidance, Component, collection: true
|
|
660
|
+
attribute :purpose, Component, collection: true
|
|
661
|
+
|
|
662
|
+
json do
|
|
663
|
+
map "component", to: :guidance,
|
|
664
|
+
when_attribute: { "type" => "guidance" }
|
|
665
|
+
map "component", to: :purpose,
|
|
666
|
+
when_attribute: { "type" => "purpose" }
|
|
667
|
+
end
|
|
668
|
+
end
|
|
669
|
+
----
|
|
670
|
+
|
|
671
|
+
[source,json]
|
|
672
|
+
----
|
|
673
|
+
{ "component": [
|
|
674
|
+
{ "type": "guidance", "text": "g1" },
|
|
675
|
+
{ "type": "purpose", "text": "p1" }
|
|
676
|
+
] }
|
|
677
|
+
----
|
|
678
|
+
|
|
679
|
+
`guidance` holds `g1` and `purpose` holds `p1`. A bare object instead of
|
|
680
|
+
an array counts as one occurrence. Every semantics from the XML side
|
|
681
|
+
carries over: plain rules on the same key capture only unclaimed items,
|
|
682
|
+
`unmatched: :raise` fails the parse on an item no rule claims, and
|
|
683
|
+
string or symbol keys both match.
|
|
684
|
+
|
|
685
|
+
On serialization the rules merge back under the one key, each item
|
|
686
|
+
stamped with its rule's discriminator pair (unless the item's own
|
|
687
|
+
mapping already writes that key):
|
|
688
|
+
|
|
689
|
+
[source,json]
|
|
690
|
+
----
|
|
691
|
+
{ "component": [
|
|
692
|
+
{ "type": "guidance", "text": "g1" },
|
|
693
|
+
{ "type": "purpose", "text": "p1" }
|
|
694
|
+
] }
|
|
695
|
+
----
|
|
696
|
+
|
|
697
|
+
Items are grouped by rule in mapping-declaration order — object
|
|
698
|
+
documents carry no interleaving to preserve. `when_attribute` cannot be
|
|
699
|
+
combined with `with:` or `delegate:` in key-value mappings.
|
|
@@ -79,12 +79,21 @@ module Lutaml
|
|
|
79
79
|
polymorphic_map: {},
|
|
80
80
|
transform: {},
|
|
81
81
|
value_map: {},
|
|
82
|
+
when_attribute: {},
|
|
83
|
+
unmatched: :drop,
|
|
82
84
|
serialize: true
|
|
83
85
|
)
|
|
84
86
|
mapping_name = name_for_mapping(root_mappings, name)
|
|
85
87
|
validate!(mapping_name, to, with, render_nil, render_empty)
|
|
88
|
+
validate_when_attribute!(when_attribute) unless when_attribute.empty?
|
|
89
|
+
validate_unmatched!(unmatched, when_attribute)
|
|
90
|
+
if !when_attribute.empty? && (!with.empty? || delegate)
|
|
91
|
+
raise Lutaml::Model::IncorrectMappingArgumentsError,
|
|
92
|
+
"when_attribute cannot be combined with :with or :delegate " \
|
|
93
|
+
"in key-value mappings"
|
|
94
|
+
end
|
|
86
95
|
|
|
87
|
-
|
|
96
|
+
rule = MappingRule.new(
|
|
88
97
|
mapping_name,
|
|
89
98
|
to: to,
|
|
90
99
|
render_nil: render_nil,
|
|
@@ -101,8 +110,23 @@ module Lutaml
|
|
|
101
110
|
polymorphic_map: polymorphic_map,
|
|
102
111
|
transform: transform,
|
|
103
112
|
value_map: value_map,
|
|
113
|
+
when_attribute: when_attribute,
|
|
114
|
+
unmatched: unmatched,
|
|
104
115
|
serialize: serialize,
|
|
105
116
|
)
|
|
117
|
+
# lutaml-model#88: rules may share a wire key (when_attribute
|
|
118
|
+
# partitions) — store per-key arrays, like the XML mapping. A
|
|
119
|
+
# rule redefining the same target (`to:`) replaces its
|
|
120
|
+
# predecessor, so subclass and redeclaration overrides keep the
|
|
121
|
+
# pre-array replace semantics.
|
|
122
|
+
existing = @mappings[mapping_name]
|
|
123
|
+
if existing.nil?
|
|
124
|
+
@mappings[mapping_name] = [rule]
|
|
125
|
+
elsif (index = existing.index { |r| r.to == rule.to })
|
|
126
|
+
existing[index] = rule
|
|
127
|
+
else
|
|
128
|
+
existing << rule
|
|
129
|
+
end
|
|
106
130
|
end
|
|
107
131
|
|
|
108
132
|
alias map_element map
|
|
@@ -117,14 +141,14 @@ module Lutaml
|
|
|
117
141
|
@raw_mapping = true
|
|
118
142
|
validate!(Lutaml::Model::Constants::RAW_MAPPING_KEY, to, with,
|
|
119
143
|
render_nil, nil)
|
|
120
|
-
@mappings[Lutaml::Model::Constants::RAW_MAPPING_KEY] = MappingRule.new(
|
|
144
|
+
@mappings[Lutaml::Model::Constants::RAW_MAPPING_KEY] = [MappingRule.new(
|
|
121
145
|
Lutaml::Model::Constants::RAW_MAPPING_KEY,
|
|
122
146
|
to: to,
|
|
123
147
|
render_nil: render_nil,
|
|
124
148
|
render_default: render_default,
|
|
125
149
|
with: with,
|
|
126
150
|
delegate: delegate,
|
|
127
|
-
)
|
|
151
|
+
)]
|
|
128
152
|
end
|
|
129
153
|
|
|
130
154
|
alias map_all_content map_all
|
|
@@ -153,7 +177,9 @@ module Lutaml
|
|
|
153
177
|
return if !instance_mapping?
|
|
154
178
|
|
|
155
179
|
mapping_name = name_for_mapping(nil, key_name || @instance)
|
|
156
|
-
@mappings[mapping_name].
|
|
180
|
+
@mappings[mapping_name].each do |rule|
|
|
181
|
+
rule.child_mappings = @key_mapping.merge(@value_mapping)
|
|
182
|
+
end
|
|
157
183
|
end
|
|
158
184
|
|
|
159
185
|
def name_for_mapping(root_mappings, name)
|
|
@@ -164,7 +190,7 @@ module Lutaml
|
|
|
164
190
|
|
|
165
191
|
def mappings(register_id = nil)
|
|
166
192
|
ensure_mappings_imported!(register_id) if finalized?
|
|
167
|
-
mappings_hash(register_id).values
|
|
193
|
+
mappings_hash(register_id).values.flatten
|
|
168
194
|
end
|
|
169
195
|
|
|
170
196
|
def mappings_hash(register_id = nil)
|
|
@@ -271,7 +297,7 @@ module Lutaml
|
|
|
271
297
|
end
|
|
272
298
|
|
|
273
299
|
def find_by_name(name)
|
|
274
|
-
|
|
300
|
+
mappings.find { |m| m.name.to_s == name.to_s }
|
|
275
301
|
end
|
|
276
302
|
|
|
277
303
|
def polymorphic_mapping
|
|
@@ -4,6 +4,8 @@ module Lutaml
|
|
|
4
4
|
attr_accessor :child_mappings,
|
|
5
5
|
:root_mappings
|
|
6
6
|
|
|
7
|
+
attr_reader :unmatched
|
|
8
|
+
|
|
7
9
|
def initialize(
|
|
8
10
|
name,
|
|
9
11
|
to:,
|
|
@@ -23,6 +25,8 @@ module Lutaml
|
|
|
23
25
|
polymorphic_map: {},
|
|
24
26
|
transform: {},
|
|
25
27
|
value_map: {},
|
|
28
|
+
when_attribute: {},
|
|
29
|
+
unmatched: :drop,
|
|
26
30
|
serialize: true
|
|
27
31
|
)
|
|
28
32
|
super(
|
|
@@ -42,11 +46,14 @@ module Lutaml
|
|
|
42
46
|
polymorphic_map: polymorphic_map,
|
|
43
47
|
transform: transform,
|
|
44
48
|
value_map: value_map,
|
|
49
|
+
when_attribute: when_attribute,
|
|
45
50
|
serialize: serialize,
|
|
46
51
|
)
|
|
47
52
|
|
|
48
53
|
@child_mappings = child_mappings
|
|
49
54
|
@root_mappings = root_mappings
|
|
55
|
+
# lutaml-model#88: policy for occurrences claimed by no rule
|
|
56
|
+
@unmatched = unmatched
|
|
50
57
|
end
|
|
51
58
|
|
|
52
59
|
def hash_mappings
|
|
@@ -67,6 +74,8 @@ module Lutaml
|
|
|
67
74
|
child_mappings: Lutaml::Model::Utils.deep_dup(child_mappings),
|
|
68
75
|
root_mappings: Lutaml::Model::Utils.deep_dup(root_mappings),
|
|
69
76
|
value_map: Lutaml::Model::Utils.deep_dup(@value_map),
|
|
77
|
+
when_attribute: when_attribute,
|
|
78
|
+
unmatched: @unmatched,
|
|
70
79
|
serialize: serialize?,
|
|
71
80
|
)
|
|
72
81
|
end
|
|
@@ -18,8 +18,12 @@ module Lutaml
|
|
|
18
18
|
root_and_parent_assignment(instance, options)
|
|
19
19
|
mappings = extract_mappings(options, format)
|
|
20
20
|
|
|
21
|
-
mappings.mappings(lutaml_register)
|
|
22
|
-
|
|
21
|
+
rules = mappings.mappings(lutaml_register)
|
|
22
|
+
# lutaml-model#88: nil unless the mapping partitions a wire key
|
|
23
|
+
# with when_attribute rules — the common case pays one scan.
|
|
24
|
+
partition = kv_partition(rules)
|
|
25
|
+
rules.each do |rule|
|
|
26
|
+
process_mapping_rule(data, instance, format, rule, options, partition)
|
|
23
27
|
end
|
|
24
28
|
|
|
25
29
|
instance
|
|
@@ -48,11 +52,22 @@ module Lutaml
|
|
|
48
52
|
# This maintains backward compatibility for models without transformations
|
|
49
53
|
mappings = extract_mappings(options, format)
|
|
50
54
|
|
|
55
|
+
rules = mappings.mappings(lutaml_register)
|
|
56
|
+
partition = kv_partition(rules)
|
|
51
57
|
hash = {}
|
|
52
|
-
|
|
58
|
+
handled_groups = nil
|
|
59
|
+
rules.each do |rule|
|
|
53
60
|
next unless valid_mapping?(rule, options)
|
|
54
61
|
|
|
55
|
-
|
|
62
|
+
group = partition && kv_partition_group(rule, partition)
|
|
63
|
+
if group
|
|
64
|
+
next if handled_groups&.include?(group)
|
|
65
|
+
|
|
66
|
+
(handled_groups ||= []) << group
|
|
67
|
+
process_partition_group!(instance, group, hash, format, options)
|
|
68
|
+
else
|
|
69
|
+
process_rule!(instance, rule, hash, format, mappings, options)
|
|
70
|
+
end
|
|
56
71
|
end
|
|
57
72
|
|
|
58
73
|
hash.keys == [""] ? hash[""] : hash
|
|
@@ -263,7 +278,163 @@ format)
|
|
|
263
278
|
options[:mappings] || mappings_for(format, lutaml_register)
|
|
264
279
|
end
|
|
265
280
|
|
|
266
|
-
|
|
281
|
+
# ---- lutaml-model#88: when_attribute for key-value formats ----
|
|
282
|
+
|
|
283
|
+
# Wire keys partitioned by when_attribute rules: key -> { rules: all
|
|
284
|
+
# rules on the key in declaration order, discriminators: the subset
|
|
285
|
+
# carrying when_attribute }. nil unless any rule uses when_attribute.
|
|
286
|
+
def kv_partition(rules)
|
|
287
|
+
partitions = nil
|
|
288
|
+
rules.each do |rule|
|
|
289
|
+
next if rule.when_attribute.empty?
|
|
290
|
+
|
|
291
|
+
partitions ||= {}
|
|
292
|
+
key = kv_partition_key(rule)
|
|
293
|
+
entry = (partitions[key] ||= { rules: [], discriminators: [] })
|
|
294
|
+
entry[:discriminators] << rule
|
|
295
|
+
end
|
|
296
|
+
return nil unless partitions
|
|
297
|
+
|
|
298
|
+
rules.each do |rule|
|
|
299
|
+
key = kv_partition_key(rule)
|
|
300
|
+
partitions[key][:rules] << rule if partitions.key?(key)
|
|
301
|
+
end
|
|
302
|
+
partitions
|
|
303
|
+
end
|
|
304
|
+
|
|
305
|
+
def kv_partition_key(rule)
|
|
306
|
+
name = rule.name
|
|
307
|
+
name = name.first if name.is_a?(Array)
|
|
308
|
+
name.to_s
|
|
309
|
+
end
|
|
310
|
+
|
|
311
|
+
# The partition entry covering any of the rule's wire names.
|
|
312
|
+
def kv_partition_group(rule, partition)
|
|
313
|
+
return nil if partition.nil?
|
|
314
|
+
|
|
315
|
+
if rule.multiple_mappings?
|
|
316
|
+
rule.name.each do |name|
|
|
317
|
+
entry = partition[name.to_s]
|
|
318
|
+
return entry if entry
|
|
319
|
+
end
|
|
320
|
+
nil
|
|
321
|
+
else
|
|
322
|
+
partition[rule.name.to_s]
|
|
323
|
+
end
|
|
324
|
+
end
|
|
325
|
+
|
|
326
|
+
# A value at a partitioned key, filtered for one rule: a
|
|
327
|
+
# discriminator rule keeps its matches; a plain rule keeps the
|
|
328
|
+
# occurrences no discriminator claimed (single-capture, mirroring
|
|
329
|
+
# the XML side).
|
|
330
|
+
def apply_kv_when_attribute(value, rule, entry, attr = nil)
|
|
331
|
+
if rule.when_attribute.empty?
|
|
332
|
+
return kv_partition_value(value, attr) do |item|
|
|
333
|
+
entry[:discriminators].none? { |s| kv_item_matches?(item, s) }
|
|
334
|
+
end
|
|
335
|
+
end
|
|
336
|
+
|
|
337
|
+
plain_sibling = entry[:rules].any? { |r| r.when_attribute.empty? }
|
|
338
|
+
if rule.unmatched == :raise && !plain_sibling
|
|
339
|
+
check_unclaimed_kv_items!(value, rule, entry)
|
|
340
|
+
end
|
|
341
|
+
kv_partition_value(value, attr) { |item| kv_item_matches?(item, rule) }
|
|
342
|
+
end
|
|
343
|
+
|
|
344
|
+
def kv_item_matches?(item, rule)
|
|
345
|
+
return false unless item.is_a?(::Hash)
|
|
346
|
+
|
|
347
|
+
rule.when_attribute.all? do |name, expected|
|
|
348
|
+
actual = Lutaml::Model::Utils.fetch_str_or_sym(item, name.to_s)
|
|
349
|
+
!actual.nil? && actual.to_s == expected.to_s
|
|
350
|
+
end
|
|
351
|
+
end
|
|
352
|
+
|
|
353
|
+
# select/reject over the occurrence shape: an Array filters per
|
|
354
|
+
# item; a single occurrence is kept or dropped as a whole; nil
|
|
355
|
+
# passes through. A dropped occurrence existed on the wire, so a
|
|
356
|
+
# collection attribute reads it as [] rather than absent. A
|
|
357
|
+
# non-hash item satisfies no discriminator, so plain rules keep it
|
|
358
|
+
# and discriminator rules drop it.
|
|
359
|
+
def kv_partition_value(value, attr = nil, &block)
|
|
360
|
+
case value
|
|
361
|
+
when ::Array
|
|
362
|
+
value.select(&block)
|
|
363
|
+
when nil
|
|
364
|
+
nil
|
|
365
|
+
else
|
|
366
|
+
if yield(value)
|
|
367
|
+
value
|
|
368
|
+
else
|
|
369
|
+
attr&.collection? ? [] : nil
|
|
370
|
+
end
|
|
371
|
+
end
|
|
372
|
+
end
|
|
373
|
+
|
|
374
|
+
# lutaml-model#88: fail closed for `unmatched: :raise` — an item
|
|
375
|
+
# no rule on the key claims is exactly the data the default
|
|
376
|
+
# policy silently drops.
|
|
377
|
+
def check_unclaimed_kv_items!(value, rule, entry)
|
|
378
|
+
items = value.is_a?(::Array) ? value : [value]
|
|
379
|
+
tested = entry[:discriminators].flat_map { |s| s.when_attribute.keys }
|
|
380
|
+
.uniq.map(&:to_s)
|
|
381
|
+
items.each do |item|
|
|
382
|
+
next if entry[:discriminators].any? { |s| kv_item_matches?(item, s) }
|
|
383
|
+
|
|
384
|
+
values = tested.filter_map do |name|
|
|
385
|
+
v = item.is_a?(::Hash) &&
|
|
386
|
+
Lutaml::Model::Utils.fetch_str_or_sym(item, name)
|
|
387
|
+
"#{name}=#{v.inspect}" if v
|
|
388
|
+
end
|
|
389
|
+
raise ::Lutaml::Model::UnknownDiscriminatorError,
|
|
390
|
+
"Item at <#{kv_partition_key(rule)}> (#{values.join(', ')}) " \
|
|
391
|
+
"is claimed by no rule: it matches no when_attribute " \
|
|
392
|
+
"discriminator and no plain rule shares the key. Cover the " \
|
|
393
|
+
"value, add a plain rule, or opt out with unmatched: :drop"
|
|
394
|
+
end
|
|
395
|
+
nil
|
|
396
|
+
end
|
|
397
|
+
|
|
398
|
+
# Serialize a partitioned key as one wire value: every rule on the
|
|
399
|
+
# key contributes its items in declaration order, each stamped with
|
|
400
|
+
# its rule's discriminator pairs (unless the item already carries
|
|
401
|
+
# the key — the value's own mapping wins).
|
|
402
|
+
def process_partition_group!(instance, group, hash, format, options)
|
|
403
|
+
wire = rule_from_name(group[:rules].first)
|
|
404
|
+
items = []
|
|
405
|
+
present = false
|
|
406
|
+
group[:rules].each do |rule|
|
|
407
|
+
next unless valid_mapping?(rule, options)
|
|
408
|
+
|
|
409
|
+
scratch = {}
|
|
410
|
+
process_mapping_for_instance(instance, scratch, format, rule, options)
|
|
411
|
+
value = scratch[rule_from_name(rule)]
|
|
412
|
+
next if value.nil?
|
|
413
|
+
|
|
414
|
+
present = true
|
|
415
|
+
Array(value).each do |item|
|
|
416
|
+
items << kv_stamp_discriminator(item, rule)
|
|
417
|
+
end
|
|
418
|
+
end
|
|
419
|
+
return unless present
|
|
420
|
+
|
|
421
|
+
hash[wire] = items
|
|
422
|
+
end
|
|
423
|
+
|
|
424
|
+
def kv_stamp_discriminator(item, rule)
|
|
425
|
+
return item unless item.is_a?(::Hash)
|
|
426
|
+
|
|
427
|
+
missing = {}
|
|
428
|
+
rule.when_attribute.each do |name, expected|
|
|
429
|
+
unless Lutaml::Model::Utils.string_or_symbol_key?(item, name)
|
|
430
|
+
missing[name.to_s] = expected.to_s
|
|
431
|
+
end
|
|
432
|
+
end
|
|
433
|
+
missing.empty? ? item : item.merge(missing)
|
|
434
|
+
end
|
|
435
|
+
|
|
436
|
+
def process_mapping_rule(doc, instance, format, rule, options = {},
|
|
437
|
+
partition = nil)
|
|
267
438
|
attr = attribute_for_rule(rule)
|
|
268
439
|
return if attr&.derived?
|
|
269
440
|
|
|
@@ -271,7 +442,11 @@ format)
|
|
|
271
442
|
rule, attr
|
|
272
443
|
)
|
|
273
444
|
|
|
274
|
-
|
|
445
|
+
# lutaml-model#88: discriminator rules (and plain rules on a
|
|
446
|
+
# partitioned key) must filter the raw value, so they keep the
|
|
447
|
+
# interpretive path.
|
|
448
|
+
partitioned = partition && kv_partition_group(rule, partition)
|
|
449
|
+
if partitioned.nil? && (plan = kv_rule_plan(format, rule, attr)) &&
|
|
275
450
|
(value = kv_fast_extract(doc, plan))
|
|
276
451
|
rule.deserialize(instance, kv_fast_cast(value, plan, instance),
|
|
277
452
|
attributes, self, options[:context])
|
|
@@ -280,6 +455,9 @@ format)
|
|
|
280
455
|
|
|
281
456
|
value = rule_value_extractor_class.call(rule, doc, format, attr,
|
|
282
457
|
lutaml_register, options, instance)
|
|
458
|
+
if partitioned && !Lutaml::Model::Utils.uninitialized?(value)
|
|
459
|
+
value = apply_kv_when_attribute(value, rule, partitioned, attr)
|
|
460
|
+
end
|
|
283
461
|
value = apply_value_map(value, rule.value_map(:from, options), attr)
|
|
284
462
|
|
|
285
463
|
if rule.has_custom_method_for_deserialization?
|
|
@@ -328,7 +506,8 @@ format)
|
|
|
328
506
|
return nil if rule.delegate || rule.raw_mapping? || rule.root_mapping? ||
|
|
329
507
|
rule.hash_mappings || rule.child_mappings ||
|
|
330
508
|
rule.has_custom_method_for_serialization? ||
|
|
331
|
-
rule.
|
|
509
|
+
rule.when_attribute? || rule.multiple_mappings? ||
|
|
510
|
+
polymorphic_rule?(rule) ||
|
|
332
511
|
(rule.transform.is_a?(Hash) && !rule.transform.empty?) ||
|
|
333
512
|
rule.transform.is_a?(Class) || attr.nil? || attr.derived? ||
|
|
334
513
|
attr.union? || attr.polymorphic? || attr.custom_collection? ||
|
|
@@ -118,6 +118,37 @@ register = self.register)
|
|
|
118
118
|
end
|
|
119
119
|
end
|
|
120
120
|
|
|
121
|
+
# lutaml-model#88: rules sharing a wire key through when_attribute
|
|
122
|
+
# must serialize as ONE value — group them here, before freeze.
|
|
123
|
+
# Rules with custom `to` methods or delegates keep their own path;
|
|
124
|
+
# a key mixing them with discriminator rules is not a partition.
|
|
125
|
+
def after_compile
|
|
126
|
+
partitioned_names = nil
|
|
127
|
+
compiled_rules.each do |rule|
|
|
128
|
+
pairs = rule.option(:when_attribute)
|
|
129
|
+
next if pairs.nil? || pairs.empty?
|
|
130
|
+
|
|
131
|
+
(partitioned_names ||= {})[rule.serialized_name] = true
|
|
132
|
+
end
|
|
133
|
+
return unless partitioned_names
|
|
134
|
+
|
|
135
|
+
groups = {}
|
|
136
|
+
by_rule = {}.compare_by_identity
|
|
137
|
+
compiled_rules.each do |rule|
|
|
138
|
+
name = rule.serialized_name
|
|
139
|
+
next unless partitioned_names[name]
|
|
140
|
+
next if (rule.custom_methods || {}).key?(:to) || rule.option(:delegate)
|
|
141
|
+
|
|
142
|
+
entry = (groups[name] ||= [])
|
|
143
|
+
entry << rule
|
|
144
|
+
by_rule[rule] = entry
|
|
145
|
+
end
|
|
146
|
+
groups.each_value(&:freeze)
|
|
147
|
+
|
|
148
|
+
@partition_groups = groups.freeze
|
|
149
|
+
@partitioned_rules = by_rule
|
|
150
|
+
end
|
|
151
|
+
|
|
121
152
|
private
|
|
122
153
|
|
|
123
154
|
# Get the register ID, handling both Symbol and Register objects
|
|
@@ -180,12 +211,21 @@ register = self.register)
|
|
|
180
211
|
# Instead, we create an anonymous root that holds all attributes
|
|
181
212
|
root = Lutaml::KeyValue::DataModel::Element.new("__root__")
|
|
182
213
|
|
|
183
|
-
# Apply each compiled rule (with filtering support)
|
|
214
|
+
# Apply each compiled rule (with filtering support). A
|
|
215
|
+
# when_attribute partition applies once for its whole key.
|
|
216
|
+
handled_groups = nil
|
|
184
217
|
compiled_rules.each do |rule|
|
|
185
218
|
# Check if this rule should be applied based on only/except options
|
|
186
219
|
next unless valid_mapping?(rule, options)
|
|
187
220
|
|
|
188
|
-
|
|
221
|
+
if @partitioned_rules && (group = @partitioned_rules[rule])
|
|
222
|
+
next if handled_groups&.include?(group)
|
|
223
|
+
|
|
224
|
+
(handled_groups ||= []) << group
|
|
225
|
+
apply_partition_group(root, group, model_instance, options)
|
|
226
|
+
else
|
|
227
|
+
apply_rule(root, rule, model_instance, options)
|
|
228
|
+
end
|
|
189
229
|
end
|
|
190
230
|
|
|
191
231
|
if ENV["DEBUG_KEYED_COLLECTION"]
|
|
@@ -202,6 +242,77 @@ register = self.register)
|
|
|
202
242
|
|
|
203
243
|
private
|
|
204
244
|
|
|
245
|
+
# The rule's value on the instance, initializing an absent
|
|
246
|
+
# delegate target like apply_rule does.
|
|
247
|
+
def extract_rule_value(rule, model_instance)
|
|
248
|
+
delegate = rule.option(:delegate)
|
|
249
|
+
unless delegate
|
|
250
|
+
return model_instance.public_send(rule.attribute_name)
|
|
251
|
+
end
|
|
252
|
+
|
|
253
|
+
delegated_object = model_instance.public_send(delegate)
|
|
254
|
+
if delegated_object.nil? ||
|
|
255
|
+
Lutaml::Model::Utils.uninitialized?(delegated_object)
|
|
256
|
+
delegate_attr = model_class.attributes(register_id)&.[](delegate)
|
|
257
|
+
if delegate_attr
|
|
258
|
+
delegated_object = delegate_attr.type(register_id).new
|
|
259
|
+
model_instance.public_send(:"#{delegate}=", delegated_object)
|
|
260
|
+
end
|
|
261
|
+
end
|
|
262
|
+
delegated_object&.public_send(rule.attribute_name)
|
|
263
|
+
end
|
|
264
|
+
|
|
265
|
+
# Serialize one when_attribute partition: every rule on the key
|
|
266
|
+
# contributes its items in declaration order, each stamped with
|
|
267
|
+
# its rule's discriminator pairs (unless the item's own mapping
|
|
268
|
+
# already wrote the key). The wire value is an array whenever any
|
|
269
|
+
# rule had a present value.
|
|
270
|
+
def apply_partition_group(parent, group, model_instance, options)
|
|
271
|
+
items = []
|
|
272
|
+
present = false
|
|
273
|
+
group.each do |rule|
|
|
274
|
+
next unless valid_mapping?(rule, options)
|
|
275
|
+
|
|
276
|
+
value = extract_rule_value(rule, model_instance)
|
|
277
|
+
next if should_skip_value?(value, rule, model_instance,
|
|
278
|
+
rule.option(:delegate))
|
|
279
|
+
|
|
280
|
+
value = rule.transform_value(value, :export) if rule.value_transformer
|
|
281
|
+
|
|
282
|
+
present = true
|
|
283
|
+
Array(value).each do |item|
|
|
284
|
+
child = create_value_for_item(rule, item, options)
|
|
285
|
+
next if child.nil?
|
|
286
|
+
|
|
287
|
+
items << stamp_discriminator(child, rule)
|
|
288
|
+
end
|
|
289
|
+
end
|
|
290
|
+
return unless present
|
|
291
|
+
|
|
292
|
+
element = Lutaml::KeyValue::DataModel::Element.new(
|
|
293
|
+
group.first.serialized_name,
|
|
294
|
+
)
|
|
295
|
+
if items.empty?
|
|
296
|
+
element.value = []
|
|
297
|
+
else
|
|
298
|
+
items.each { |item| element.add_child(item) }
|
|
299
|
+
end
|
|
300
|
+
parent.add_child(element)
|
|
301
|
+
end
|
|
302
|
+
|
|
303
|
+
def stamp_discriminator(item, rule)
|
|
304
|
+
pairs = rule.option(:when_attribute)
|
|
305
|
+
return item if pairs.nil? || pairs.empty? || !item.is_a?(::Hash)
|
|
306
|
+
|
|
307
|
+
missing = {}
|
|
308
|
+
pairs.each do |name, expected|
|
|
309
|
+
unless Lutaml::Model::Utils.string_or_symbol_key?(item, name)
|
|
310
|
+
missing[name.to_s] = expected.to_s
|
|
311
|
+
end
|
|
312
|
+
end
|
|
313
|
+
missing.empty? ? item : item.merge(missing)
|
|
314
|
+
end
|
|
315
|
+
|
|
205
316
|
# Apply a single transformation rule
|
|
206
317
|
#
|
|
207
318
|
# @param parent [Lutaml::KeyValue::DataModel::Element] Parent element
|
|
@@ -106,6 +106,36 @@ 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
|
+
|
|
109
139
|
def register(register_id = nil)
|
|
110
140
|
register_id ||= Lutaml::Model::Config.default_register
|
|
111
141
|
Lutaml::Model::GlobalRegister.lookup(register_id)
|
data/lib/lutaml/model/version.rb
CHANGED
data/lib/lutaml/xml/mapping.rb
CHANGED
|
@@ -614,35 +614,6 @@ module Lutaml
|
|
|
614
614
|
end
|
|
615
615
|
end
|
|
616
616
|
|
|
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
617
|
def map_attribute(
|
|
647
618
|
name,
|
|
648
619
|
to: nil,
|
|
@@ -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
|
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.
|
|
4
|
+
version: 0.8.46
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Ribose Inc.
|
|
@@ -1688,6 +1688,7 @@ files:
|
|
|
1688
1688
|
- spec/lutaml/key_value/transformation/rule_compiler_spec.rb
|
|
1689
1689
|
- spec/lutaml/key_value/transformation/value_serializer_spec.rb
|
|
1690
1690
|
- spec/lutaml/key_value/transformation_spec.rb
|
|
1691
|
+
- spec/lutaml/key_value/when_attribute_spec.rb
|
|
1691
1692
|
- spec/lutaml/model/absent_value_contracts_spec.rb
|
|
1692
1693
|
- spec/lutaml/model/adapter_resolver_configured_spec.rb
|
|
1693
1694
|
- spec/lutaml/model/attribute_apply_value_map_spec.rb
|