lutaml-model 0.8.43 → 0.8.45
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 +53 -2
- data/docs/_tutorials/polymorphic-models-tutorial.adoc +2 -0
- data/lib/compat/opal/lutaml_model_boot.rb +1 -0
- data/lib/lutaml/model/error/unknown_discriminator_error.rb +11 -0
- data/lib/lutaml/model/version.rb +1 -1
- data/lib/lutaml/model.rb +2 -0
- data/lib/lutaml/xml/adapter_element.rb +2 -1
- data/lib/lutaml/xml/element.rb +8 -2
- data/lib/lutaml/xml/mapping.rb +18 -0
- data/lib/lutaml/xml/mapping_rule.rb +7 -1
- data/lib/lutaml/xml/model_transform.rb +46 -1
- data/lib/lutaml/xml/parse_session.rb +44 -0
- data/lib/lutaml/xml/transformation/order_reconciler.rb +16 -1
- data/lib/lutaml/xml/transformation/ordered_applier.rb +73 -2
- data/lib/lutaml/xml/xml_element.rb +20 -1
- data/spec/lutaml/xml/when_attribute_ordered_spec.rb +148 -0
- data/spec/lutaml/xml/when_attribute_spec.rb +92 -0
- data/spec/lutaml/xml/when_attribute_unmatched_spec.rb +169 -0
- metadata +4 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 07e6ecf8e0047cd91862259a5a9db3b1cfb09122e752cdd17a4523d7169c89c5
|
|
4
|
+
data.tar.gz: 5fd97454986a0d0f4bd1f13d75af01d0a692618acd9fafcc702bfaea7a73827f
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: d95d9d885d0fbc63200900751b75c62282b63970f3fe4ebb1707ff134452bd57d714b8aad22f2253b70ea9509d1456dfd6bbee2a43f3cb292de8ae87959ab617
|
|
7
|
+
data.tar.gz: 455d7af1865469474d7f11f623dede9fbd65311dabaa30c0546a0193bdfbab4e99356b50ca6e8ffa2deb5f108ca0d5e2de9bf112e03ce607328c9ff4e6de0a8d
|
|
@@ -592,5 +592,56 @@ writes that attribute).
|
|
|
592
592
|
|
|
593
593
|
Co-occurrence semantics: several `when_attribute` rules may share the
|
|
594
594
|
element name; each target attribute fills independently, so declare each
|
|
595
|
-
as `collection: true` when the element repeats.
|
|
596
|
-
`when_attribute`
|
|
595
|
+
as `collection: true` when the element repeats. A rule without
|
|
596
|
+
`when_attribute` that shares the name only claims occurrences no
|
|
597
|
+
discriminator rule claimed, so every occurrence is captured exactly once.
|
|
598
|
+
|
|
599
|
+
By default an occurrence no rule claims — an uncovered discriminator
|
|
600
|
+
value, or a missing discriminator attribute — is silently dropped.
|
|
601
|
+
Declare `unmatched: :raise` on a discriminator rule to fail the parse
|
|
602
|
+
instead: `Lutaml::Model::UnknownDiscriminatorError` names the element and
|
|
603
|
+
the unclaimed discriminator values. A plain rule sharing the name claims
|
|
604
|
+
everything left, so the strict policy never fires when one is present.
|
|
605
|
+
|
|
606
|
+
[source,ruby]
|
|
607
|
+
----
|
|
608
|
+
xml do
|
|
609
|
+
element "requirement"
|
|
610
|
+
map_element "component", to: :guidance,
|
|
611
|
+
when_attribute: { "type" => "guidance" },
|
|
612
|
+
unmatched: :raise
|
|
613
|
+
end
|
|
614
|
+
----
|
|
615
|
+
|
|
616
|
+
`when_attribute` and `polymorphic:` are two different dispatch axes over
|
|
617
|
+
the same discriminator idea — pick by what the discriminator selects:
|
|
618
|
+
|
|
619
|
+
* `polymorphic: { attribute:, class_map: }` — the value selects the
|
|
620
|
+
**class** each occurrence hydrates into; ONE attribute holds every
|
|
621
|
+
occurrence. See the polymorphic models tutorial.
|
|
622
|
+
* `when_attribute:` — the value selects **which attribute** claims the
|
|
623
|
+
occurrence; the declared type stays fixed.
|
|
624
|
+
|
|
625
|
+
They compose: a rule may partition with `when_attribute` while its
|
|
626
|
+
attribute dispatches classes with `polymorphic`.
|
|
627
|
+
|
|
628
|
+
Under an `ordered` mapping, serialization preserves the original
|
|
629
|
+
interleaving: `element_order` records each occurrence's attributes, and
|
|
630
|
+
the ordered walk routes every occurrence back to the rule whose
|
|
631
|
+
discriminator it satisfies.
|
|
632
|
+
|
|
633
|
+
[source,ruby]
|
|
634
|
+
----
|
|
635
|
+
xml do
|
|
636
|
+
element "requirement"
|
|
637
|
+
ordered
|
|
638
|
+
map_element "component", to: :guidance,
|
|
639
|
+
when_attribute: { "type" => "guidance" }
|
|
640
|
+
map_element "component", to: :purpose,
|
|
641
|
+
when_attribute: { "type" => "purpose" }
|
|
642
|
+
end
|
|
643
|
+
----
|
|
644
|
+
|
|
645
|
+
Given `<component type="guidance">`, `<component type="purpose">`,
|
|
646
|
+
`<component type="guidance">` in the source, `to_xml` re-emits exactly
|
|
647
|
+
that guidance, purpose, guidance sequence.
|
|
@@ -9,6 +9,8 @@ nav_order: 8
|
|
|
9
9
|
|
|
10
10
|
Polymorphic attributes allow an attribute to accept multiple types of values. This is useful when attributes share common characteristics but have type-specific behaviors.
|
|
11
11
|
|
|
12
|
+
NOTE: Polymorphism dispatches the *class* of each hydrated item into one attribute. If the discriminator instead needs to route occurrences of a shared element name into *different attributes* (e.g. `component[@type='guidance']` → `guidance`, `component[@type='purpose']` → `purpose`), that is `when_attribute:` in the advanced mapping guide — the two compose on one rule.
|
|
13
|
+
|
|
12
14
|
== Understanding polymorphism
|
|
13
15
|
|
|
14
16
|
A polymorphic attribute can accept instances of different classes that share a common base class.
|
|
@@ -283,6 +283,7 @@ require "lutaml/model/error/type_only_namespace_error"
|
|
|
283
283
|
require "lutaml/model/error/undefined_attribute_error"
|
|
284
284
|
require "lutaml/model/error/union_schema_unsupported_error"
|
|
285
285
|
require "lutaml/model/error/unknown_adapter_type_error"
|
|
286
|
+
require "lutaml/model/error/unknown_discriminator_error"
|
|
286
287
|
require "lutaml/model/error/unknown_sequence_mapping_error"
|
|
287
288
|
require "lutaml/model/error/unknown_type_error"
|
|
288
289
|
require "lutaml/model/error/unresolvable_type_error"
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Lutaml
|
|
4
|
+
module Model
|
|
5
|
+
# Raised when a `when_attribute` rule declared `unmatched: :raise`
|
|
6
|
+
# parses an occurrence that no rule claims: it matches no
|
|
7
|
+
# discriminator and no plain rule shares the wire name.
|
|
8
|
+
class UnknownDiscriminatorError < Error
|
|
9
|
+
end
|
|
10
|
+
end
|
|
11
|
+
end
|
data/lib/lutaml/model/version.rb
CHANGED
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"
|
|
@@ -302,7 +302,8 @@ module Lutaml
|
|
|
302
302
|
Lutaml::Xml::Element.new("Element", child.unprefixed_name,
|
|
303
303
|
node_type: :element,
|
|
304
304
|
namespace_uri: child.namespace_uri,
|
|
305
|
-
namespace_prefix: child.namespace_prefix
|
|
305
|
+
namespace_prefix: child.namespace_prefix,
|
|
306
|
+
attributes: order_entry_attributes(child))
|
|
306
307
|
end
|
|
307
308
|
end
|
|
308
309
|
end.each(&:freeze).freeze
|
data/lib/lutaml/xml/element.rb
CHANGED
|
@@ -4,7 +4,7 @@ module Lutaml
|
|
|
4
4
|
include Lutaml::Model::Liquefiable
|
|
5
5
|
|
|
6
6
|
attr_reader :type, :name, :text_content, :node_type, :namespace_uri,
|
|
7
|
-
:namespace_prefix
|
|
7
|
+
:namespace_prefix, :attributes
|
|
8
8
|
|
|
9
9
|
# Create a new Element for order tracking
|
|
10
10
|
#
|
|
@@ -14,8 +14,13 @@ module Lutaml
|
|
|
14
14
|
# @param node_type [Symbol, nil] The node type (:text, :cdata, :element, :comment, :processing_instruction)
|
|
15
15
|
# @param namespace_uri [String, nil] The namespace URI of this element
|
|
16
16
|
# @param namespace_prefix [String, nil] The namespace prefix of this element
|
|
17
|
+
# @param attributes [::Hash{String=>String}, nil] The element's
|
|
18
|
+
# attributes keyed by namespaced name. Only element entries parsed
|
|
19
|
+
# from a document carry them, so `when_attribute` rules can tell
|
|
20
|
+
# same-name entries apart on ordered serialization.
|
|
17
21
|
def initialize(type, name, text_content: nil, node_type: nil,
|
|
18
|
-
namespace_uri: nil, namespace_prefix: nil
|
|
22
|
+
namespace_uri: nil, namespace_prefix: nil,
|
|
23
|
+
attributes: nil)
|
|
19
24
|
@type = type # "Text" or "Element" - deprecated, kept for backward compatibility
|
|
20
25
|
@name = name
|
|
21
26
|
# Infer node_type from type for backward compatibility if not provided
|
|
@@ -26,6 +31,7 @@ module Lutaml
|
|
|
26
31
|
@text_content ||= name if text? || cdata?
|
|
27
32
|
@namespace_uri = namespace_uri
|
|
28
33
|
@namespace_prefix = namespace_prefix
|
|
34
|
+
@attributes = attributes&.freeze
|
|
29
35
|
end
|
|
30
36
|
|
|
31
37
|
# Check if this is a text content node (not CDATA)
|
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
|
|
@@ -625,6 +628,21 @@ module Lutaml
|
|
|
625
628
|
end
|
|
626
629
|
end
|
|
627
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
|
+
|
|
628
646
|
def map_attribute(
|
|
629
647
|
name,
|
|
630
648
|
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
|
|
@@ -1053,11 +1082,27 @@ _effective_register)
|
|
|
1053
1082
|
|
|
1054
1083
|
# lutaml-model#88: attribute-value discriminator — among the
|
|
1055
1084
|
# name-matched occurrences, keep only those whose sibling
|
|
1056
|
-
# attributes carry the expected values.
|
|
1085
|
+
# attributes carry the expected values. A plain rule sharing the
|
|
1086
|
+
# name with discriminator rules gets the complement — occurrences
|
|
1087
|
+
# no discriminator claimed — so each occurrence is captured
|
|
1088
|
+
# exactly once, mirroring ordered-serialization routing.
|
|
1057
1089
|
if rule.when_attribute?
|
|
1090
|
+
if rule.unmatched == :raise
|
|
1091
|
+
check_unmatched_discriminators!(children, rule, rule_names, session)
|
|
1092
|
+
end
|
|
1058
1093
|
children = children.select do |child|
|
|
1059
1094
|
rule.matches_when_attribute?(child)
|
|
1060
1095
|
end
|
|
1096
|
+
else
|
|
1097
|
+
siblings = session.when_attribute_siblings_by_name
|
|
1098
|
+
unless siblings.empty?
|
|
1099
|
+
claimed = rule_names.filter_map { |n| siblings[n] }.flatten
|
|
1100
|
+
unless claimed.empty?
|
|
1101
|
+
children = children.reject do |child|
|
|
1102
|
+
claimed.any? { |sibling| sibling.matches_when_attribute?(child) }
|
|
1103
|
+
end
|
|
1104
|
+
end
|
|
1105
|
+
end
|
|
1061
1106
|
end
|
|
1062
1107
|
|
|
1063
1108
|
# Performance: Cache rule method check before loop to avoid repeated method calls
|
|
@@ -65,6 +65,50 @@ module Lutaml
|
|
|
65
65
|
end
|
|
66
66
|
end
|
|
67
67
|
|
|
68
|
+
# lutaml-model#88: element wire names partitioned by `when_attribute`
|
|
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 ||=
|
|
84
|
+
if instance_is_serialize && (mapping = xml_mapping)
|
|
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
|
|
90
|
+
|
|
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
|
|
97
|
+
end
|
|
98
|
+
[siblings, plain_names]
|
|
99
|
+
else
|
|
100
|
+
[{}, {}]
|
|
101
|
+
end
|
|
102
|
+
end
|
|
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
|
+
|
|
68
112
|
def model_class
|
|
69
113
|
@model_class ||= instance.class
|
|
70
114
|
end
|
|
@@ -220,25 +220,40 @@ module Lutaml
|
|
|
220
220
|
# accepts a nil rule namespace, namespace aliases and name aliases.
|
|
221
221
|
# When more than one rule would claim the entry, reconciling would
|
|
222
222
|
# feed the value to the wrong rule, so leave the rule alone.
|
|
223
|
+
# Rules distinguished by `when_attribute` are separate rules: their
|
|
224
|
+
# inserted entries carry the discriminator attributes, so they
|
|
225
|
+
# resolve back correctly.
|
|
223
226
|
def unambiguous?(rule, element_rules)
|
|
227
|
+
discriminator = rule_discriminator(rule)
|
|
224
228
|
matches = element_rules.select do |candidate|
|
|
225
229
|
matches_element_rule?(candidate, rule.serialized_name,
|
|
226
|
-
rule.namespace_class&.uri)
|
|
230
|
+
rule.namespace_class&.uri) &&
|
|
231
|
+
rule_discriminator(candidate) == discriminator
|
|
227
232
|
end
|
|
228
233
|
|
|
229
234
|
matches == [rule]
|
|
230
235
|
end
|
|
231
236
|
|
|
232
237
|
def new_order_entry(rule)
|
|
238
|
+
discriminator = rule_discriminator(rule)
|
|
233
239
|
::Lutaml::Xml::Element.new(
|
|
234
240
|
"Element",
|
|
235
241
|
rule.serialized_name,
|
|
236
242
|
node_type: :element,
|
|
237
243
|
namespace_uri: rule.namespace_class&.uri,
|
|
238
244
|
namespace_prefix: nil,
|
|
245
|
+
attributes: discriminator && discriminator_attributes(discriminator),
|
|
239
246
|
)
|
|
240
247
|
end
|
|
241
248
|
|
|
249
|
+
# The discriminator pairs as order-entry attributes, so an inserted
|
|
250
|
+
# entry routes back to its own rule at emit time and re-parses.
|
|
251
|
+
def discriminator_attributes(pairs)
|
|
252
|
+
pairs.each_with_object({}) do |(name, value), attrs|
|
|
253
|
+
attrs[name.to_s] = value.to_s
|
|
254
|
+
end
|
|
255
|
+
end
|
|
256
|
+
|
|
242
257
|
# Where a rule's new entries go:
|
|
243
258
|
# - after the last entry that already resolves to it, so extra
|
|
244
259
|
# collection items follow the existing ones and any interleaved
|
|
@@ -122,6 +122,14 @@ model_class, register_id)
|
|
|
122
122
|
|
|
123
123
|
# Find the mapping rule for an element from element_order
|
|
124
124
|
#
|
|
125
|
+
# Same-name entries under different `when_attribute` discriminators
|
|
126
|
+
# (all `<component>`s, distinguished by `type="guidance"` vs
|
|
127
|
+
# `type="purpose"`) route to the rule whose discriminator pairs the
|
|
128
|
+
# entry's recorded attributes satisfy. Entries carrying no
|
|
129
|
+
# attributes — built before the entry format carried them, or
|
|
130
|
+
# inserted by hand — fall back to the first plain rule, which is
|
|
131
|
+
# the pre-discriminator behavior.
|
|
132
|
+
#
|
|
125
133
|
# @param object [Xml::Element] Element from element_order
|
|
126
134
|
# @param compiled_rules [Array<CompiledRule>] The compiled rules
|
|
127
135
|
# @return [CompiledRule, nil] The matching rule or nil
|
|
@@ -131,11 +139,23 @@ model_class, register_id)
|
|
|
131
139
|
|
|
132
140
|
object_ns_uri = object.namespace_uri # nil if old element_order (backward compat)
|
|
133
141
|
|
|
134
|
-
|
|
135
|
-
|
|
142
|
+
first = nil
|
|
143
|
+
shared_name = false
|
|
144
|
+
compiled_rules.each do |r|
|
|
145
|
+
next unless r.is_a?(::Lutaml::Model::CompiledRule) &&
|
|
136
146
|
r.option(:mapping_type) == :element &&
|
|
137
147
|
matches_element_rule?(r, object.name, object_ns_uri)
|
|
148
|
+
|
|
149
|
+
if first.nil?
|
|
150
|
+
first = r
|
|
151
|
+
else
|
|
152
|
+
shared_name = true
|
|
153
|
+
break
|
|
154
|
+
end
|
|
138
155
|
end
|
|
156
|
+
return first unless shared_name
|
|
157
|
+
|
|
158
|
+
resolve_shared_name_rule(object, compiled_rules, object_ns_uri)
|
|
139
159
|
end
|
|
140
160
|
|
|
141
161
|
private
|
|
@@ -175,6 +195,57 @@ model_class, register_id)
|
|
|
175
195
|
rule.matches_name?(name) && rule_ns_matches
|
|
176
196
|
end
|
|
177
197
|
|
|
198
|
+
# Pick among rules sharing a serialized name when the entry's
|
|
199
|
+
# recorded attributes are the only thing that tells them apart.
|
|
200
|
+
#
|
|
201
|
+
# A discriminator rule whose pairs the entry satisfies wins over
|
|
202
|
+
# any plain rule regardless of declaration order, because a plain
|
|
203
|
+
# rule would claim EVERY same-name entry including discriminated
|
|
204
|
+
# ones. Entries matching no discriminator pair — an unknown value,
|
|
205
|
+
# or an entry without attributes — fall back to the first plain
|
|
206
|
+
# rule.
|
|
207
|
+
def resolve_shared_name_rule(object, compiled_rules, object_ns_uri)
|
|
208
|
+
plain = nil
|
|
209
|
+
compiled_rules.each do |r|
|
|
210
|
+
next unless r.is_a?(::Lutaml::Model::CompiledRule) &&
|
|
211
|
+
r.option(:mapping_type) == :element &&
|
|
212
|
+
matches_element_rule?(r, object.name, object_ns_uri)
|
|
213
|
+
|
|
214
|
+
pairs = rule_discriminator(r)
|
|
215
|
+
return r if pairs && entry_matches_when_attribute?(object, pairs)
|
|
216
|
+
|
|
217
|
+
plain ||= r if pairs.nil?
|
|
218
|
+
end
|
|
219
|
+
plain
|
|
220
|
+
end
|
|
221
|
+
|
|
222
|
+
# The rule's `when_attribute` discriminator pairs, or nil for a
|
|
223
|
+
# plain rule. Shared by OrderedApplier and OrderReconciler since
|
|
224
|
+
# both run on the same transform host.
|
|
225
|
+
#
|
|
226
|
+
# @param rule [CompiledRule]
|
|
227
|
+
# @return [::Hash, nil]
|
|
228
|
+
def rule_discriminator(rule)
|
|
229
|
+
pairs = rule.option(:when_attribute)
|
|
230
|
+
pairs && !pairs.empty? ? pairs : nil
|
|
231
|
+
end
|
|
232
|
+
|
|
233
|
+
# Whether an order entry's recorded attributes satisfy a rule's
|
|
234
|
+
# discriminator pairs. Mirrors Xml::MappingRule#matches_when_attribute?
|
|
235
|
+
# — non-nil and string-equal — against the namespaced-name keying
|
|
236
|
+
# the entries record.
|
|
237
|
+
def entry_matches_when_attribute?(object, pairs)
|
|
238
|
+
return false unless object.is_a?(::Lutaml::Xml::Element)
|
|
239
|
+
|
|
240
|
+
attrs = object.attributes
|
|
241
|
+
return false if attrs.nil?
|
|
242
|
+
|
|
243
|
+
pairs.all? do |name, expected|
|
|
244
|
+
actual = attrs[name.to_s]
|
|
245
|
+
!actual.nil? && actual.to_s == expected.to_s
|
|
246
|
+
end
|
|
247
|
+
end
|
|
248
|
+
|
|
178
249
|
def process_element_order_item(object, root, model_instance, options,
|
|
179
250
|
compiled_rules, _mapping, element_indices,
|
|
180
251
|
content_rule = nil, text_node_index = 0,
|
|
@@ -284,11 +284,30 @@ module Lutaml
|
|
|
284
284
|
Lutaml::Xml::Element.new("Element", child.unprefixed_name,
|
|
285
285
|
node_type: :element,
|
|
286
286
|
namespace_uri: child.namespace_uri,
|
|
287
|
-
namespace_prefix: child.namespace_prefix
|
|
287
|
+
namespace_prefix: child.namespace_prefix,
|
|
288
|
+
attributes: order_entry_attributes(child))
|
|
288
289
|
end
|
|
289
290
|
end.each(&:freeze).freeze
|
|
290
291
|
end
|
|
291
292
|
|
|
293
|
+
# The attributes an element-order entry records, keyed by namespaced
|
|
294
|
+
# name — the same key find_attribute_value indexes. Same-name entries
|
|
295
|
+
# under different `when_attribute` discriminators need them to route
|
|
296
|
+
# back to the right rule on ordered serialization. nil for attribute-
|
|
297
|
+
# less children keeps the common entry allocation-free.
|
|
298
|
+
#
|
|
299
|
+
# @param child [XmlElement] An element child of this node
|
|
300
|
+
# @return [::Hash{String=>String}, nil]
|
|
301
|
+
def order_entry_attributes(child)
|
|
302
|
+
return if child.attributes.empty?
|
|
303
|
+
|
|
304
|
+
attrs = {}
|
|
305
|
+
child.attributes.each_value do |attr|
|
|
306
|
+
attrs[attr.namespaced_name] = attr.value
|
|
307
|
+
end
|
|
308
|
+
attrs.freeze
|
|
309
|
+
end
|
|
310
|
+
|
|
292
311
|
def root
|
|
293
312
|
self
|
|
294
313
|
end
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "spec_helper"
|
|
4
|
+
|
|
5
|
+
# lutaml-model#88: `when_attribute` partitioning under `ordered` mappings.
|
|
6
|
+
# The ordered walk routes each element_order entry by its recorded
|
|
7
|
+
# attributes, so `<component type="guidance">` and `<component
|
|
8
|
+
# type="purpose">` interleave on to_xml exactly as the source document did.
|
|
9
|
+
class WhenOrderedComponent < 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 WhenOrderedRequirement < Lutaml::Model::Serializable
|
|
19
|
+
attribute :guidance, WhenOrderedComponent, collection: true
|
|
20
|
+
attribute :purpose, WhenOrderedComponent, collection: true
|
|
21
|
+
attribute :test_method, WhenOrderedComponent, collection: true
|
|
22
|
+
|
|
23
|
+
xml do
|
|
24
|
+
element "requirement"
|
|
25
|
+
ordered
|
|
26
|
+
map_element "component", to: :guidance,
|
|
27
|
+
when_attribute: { "type" => "guidance" }
|
|
28
|
+
map_element "component", to: :purpose,
|
|
29
|
+
when_attribute: { "type" => "purpose" }
|
|
30
|
+
map_element "component", to: :test_method,
|
|
31
|
+
when_attribute: { type: "test-method" }
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
RSpec.describe "when_attribute under ordered mappings" do
|
|
36
|
+
before do
|
|
37
|
+
stub_const("WhenOrdered::Component", WhenOrderedComponent)
|
|
38
|
+
stub_const("WhenOrdered::Requirement", WhenOrderedRequirement)
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
let(:xml) do
|
|
42
|
+
<<~XML
|
|
43
|
+
<requirement>
|
|
44
|
+
<component type="guidance"><text>g1</text></component>
|
|
45
|
+
<component type="purpose"><text>p1</text></component>
|
|
46
|
+
<component type="guidance"><text>g2</text></component>
|
|
47
|
+
<component type="test-method"><text>t1</text></component>
|
|
48
|
+
</requirement>
|
|
49
|
+
XML
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def serialized_types(doc)
|
|
53
|
+
doc.scan(/<component type="([^"]+)">/).flatten
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
it "parses partitioned values" do
|
|
57
|
+
req = WhenOrderedRequirement.from_xml(xml)
|
|
58
|
+
|
|
59
|
+
expect(req.guidance.map(&:text)).to eq(%w[g1 g2])
|
|
60
|
+
expect(req.purpose.map(&:text)).to eq(["p1"])
|
|
61
|
+
expect(req.test_method.map(&:text)).to eq(["t1"])
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
it "records the discriminator attributes on element_order entries" do
|
|
65
|
+
req = WhenOrderedRequirement.from_xml(xml)
|
|
66
|
+
entries = req.element_order.select { |e| e.type == "Element" }
|
|
67
|
+
|
|
68
|
+
expect(entries.map(&:attributes)).to eq([
|
|
69
|
+
{ "type" => "guidance" },
|
|
70
|
+
{ "type" => "purpose" },
|
|
71
|
+
{ "type" => "guidance" },
|
|
72
|
+
{ "type" => "test-method" },
|
|
73
|
+
])
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
it "round-trips the original interleaving" do
|
|
77
|
+
round = WhenOrderedRequirement.from_xml(xml).to_xml
|
|
78
|
+
|
|
79
|
+
expect(serialized_types(round)).to eq(
|
|
80
|
+
%w[guidance purpose guidance test-method],
|
|
81
|
+
)
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
it "survives a re-parse of the ordered output" do
|
|
85
|
+
once = WhenOrderedRequirement.from_xml(xml)
|
|
86
|
+
twice = WhenOrderedRequirement.from_xml(once.to_xml)
|
|
87
|
+
|
|
88
|
+
expect(twice.guidance.map(&:text)).to eq(%w[g1 g2])
|
|
89
|
+
expect(twice.purpose.map(&:text)).to eq(["p1"])
|
|
90
|
+
expect(twice.test_method.map(&:text)).to eq(["t1"])
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
it "routes appended items to the right attribute on ordered output" do
|
|
94
|
+
req = WhenOrderedRequirement.from_xml(xml)
|
|
95
|
+
req.guidance << WhenOrderedComponent.new(text: "g3")
|
|
96
|
+
|
|
97
|
+
out = req.to_xml
|
|
98
|
+
# Reconciliation places new entries after the rule's last existing
|
|
99
|
+
# occurrence (the OrderReconciler contract), not at document end.
|
|
100
|
+
expect(serialized_types(out)).to eq(
|
|
101
|
+
%w[guidance purpose guidance guidance test-method],
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
reparsed = WhenOrderedRequirement.from_xml(out)
|
|
105
|
+
expect(reparsed.guidance.map(&:text)).to eq(%w[g1 g2 g3])
|
|
106
|
+
expect(reparsed.purpose.map(&:text)).to eq(["p1"])
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
it "drops occurrences with an unknown discriminator from ordered output" do
|
|
110
|
+
with_unknown = WhenOrderedRequirement.from_xml(<<~XML)
|
|
111
|
+
<requirement>
|
|
112
|
+
<component type="guidance"><text>g1</text></component>
|
|
113
|
+
<component type="unknown"><text>x</text></component>
|
|
114
|
+
<component type="purpose"><text>p1</text></component>
|
|
115
|
+
</requirement>
|
|
116
|
+
XML
|
|
117
|
+
|
|
118
|
+
expect(with_unknown.guidance.map(&:text)).to eq(["g1"])
|
|
119
|
+
expect(with_unknown.purpose.map(&:text)).to eq(["p1"])
|
|
120
|
+
expect(serialized_types(with_unknown.to_xml)).to eq(%w[guidance purpose])
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
it "falls back to the plain rule for legacy attribute-less entries" do
|
|
124
|
+
both = Class.new(Lutaml::Model::Serializable) do
|
|
125
|
+
attribute :plain, :string
|
|
126
|
+
attribute :special, :string
|
|
127
|
+
|
|
128
|
+
xml do
|
|
129
|
+
element "holder"
|
|
130
|
+
ordered
|
|
131
|
+
map_element "item", to: :plain
|
|
132
|
+
map_element "item", to: :special, when_attribute: { "kind" => "special" }
|
|
133
|
+
end
|
|
134
|
+
end
|
|
135
|
+
stub_const("WhenOrdered::Holder", both)
|
|
136
|
+
|
|
137
|
+
doc = both.from_xml(
|
|
138
|
+
"<holder><item>a</item><item kind=\"special\">b</item></holder>",
|
|
139
|
+
)
|
|
140
|
+
|
|
141
|
+
expect(doc.plain).to eq("a")
|
|
142
|
+
expect(doc.special).to eq("b")
|
|
143
|
+
|
|
144
|
+
out = doc.to_xml
|
|
145
|
+
expect(out.scan(%r{<item[^>]*>([^<]*)</item>})).to eq([["a"], ["b"]])
|
|
146
|
+
expect(out).to include('kind="special"')
|
|
147
|
+
end
|
|
148
|
+
end
|
|
@@ -130,4 +130,96 @@ RSpec.describe "when_attribute discriminator mappings" do
|
|
|
130
130
|
end
|
|
131
131
|
end.to raise_error(ArgumentError)
|
|
132
132
|
end
|
|
133
|
+
|
|
134
|
+
# Orthogonality guard: `when_attribute` partitions occurrences across
|
|
135
|
+
# attributes; `polymorphic` dispatches the class of each hydrated item.
|
|
136
|
+
# Different axes over the same discriminator concept — they compose on
|
|
137
|
+
# one rule rather than substitute for each other.
|
|
138
|
+
describe "vs polymorphic dispatch" do
|
|
139
|
+
before do
|
|
140
|
+
stub_const("AxisComp", Class.new(Lutaml::Model::Serializable) do
|
|
141
|
+
attribute :text, :string
|
|
142
|
+
end)
|
|
143
|
+
stub_const("AxisGuidanceComp", Class.new(AxisComp))
|
|
144
|
+
stub_const("AxisPurposeComp", Class.new(AxisComp))
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
let(:axes_xml) do
|
|
148
|
+
<<~XML
|
|
149
|
+
<req>
|
|
150
|
+
<component type="guidance"><text>g1</text></component>
|
|
151
|
+
<component type="purpose"><text>p1</text></component>
|
|
152
|
+
<component type="guidance"><text>g2</text></component>
|
|
153
|
+
</req>
|
|
154
|
+
XML
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
it "polymorphic keeps one attribute and dispatches classes" do
|
|
158
|
+
poly = Class.new(Lutaml::Model::Serializable) do
|
|
159
|
+
attribute :components, AxisComp, collection: true
|
|
160
|
+
|
|
161
|
+
xml do
|
|
162
|
+
element "req"
|
|
163
|
+
map_element "component", to: :components, polymorphic: {
|
|
164
|
+
attribute: "type",
|
|
165
|
+
class_map: {
|
|
166
|
+
"guidance" => "AxisGuidanceComp",
|
|
167
|
+
"purpose" => "AxisPurposeComp",
|
|
168
|
+
},
|
|
169
|
+
}
|
|
170
|
+
end
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
parsed = poly.from_xml(axes_xml)
|
|
174
|
+
expect(parsed.components.map(&:class))
|
|
175
|
+
.to eq([AxisGuidanceComp, AxisPurposeComp, AxisGuidanceComp])
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
it "when_attribute keeps one class and partitions attributes" do
|
|
179
|
+
partitioned = Class.new(Lutaml::Model::Serializable) do
|
|
180
|
+
attribute :guidance, AxisComp, collection: true
|
|
181
|
+
attribute :purpose, AxisComp, collection: true
|
|
182
|
+
|
|
183
|
+
xml do
|
|
184
|
+
element "req"
|
|
185
|
+
map_element "component", to: :guidance,
|
|
186
|
+
when_attribute: { "type" => "guidance" }
|
|
187
|
+
map_element "component", to: :purpose,
|
|
188
|
+
when_attribute: { "type" => "purpose" }
|
|
189
|
+
end
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
parsed = partitioned.from_xml(axes_xml)
|
|
193
|
+
expect(parsed.guidance.map(&:text)).to eq(%w[g1 g2])
|
|
194
|
+
expect(parsed.purpose.map(&:text)).to eq(["p1"])
|
|
195
|
+
expect(parsed.guidance.first.class).to eq(AxisComp)
|
|
196
|
+
end
|
|
197
|
+
|
|
198
|
+
it "composes: partition and class dispatch on the same rule" do
|
|
199
|
+
both = Class.new(Lutaml::Model::Serializable) do
|
|
200
|
+
attribute :guidance, AxisComp, collection: true
|
|
201
|
+
attribute :purpose, AxisComp, collection: true
|
|
202
|
+
|
|
203
|
+
xml do
|
|
204
|
+
element "req"
|
|
205
|
+
map_element "component", to: :guidance,
|
|
206
|
+
when_attribute: { "type" => "guidance" },
|
|
207
|
+
polymorphic: {
|
|
208
|
+
attribute: "type",
|
|
209
|
+
class_map: { "guidance" => "AxisGuidanceComp" },
|
|
210
|
+
}
|
|
211
|
+
map_element "component", to: :purpose,
|
|
212
|
+
when_attribute: { "type" => "purpose" },
|
|
213
|
+
polymorphic: {
|
|
214
|
+
attribute: "type",
|
|
215
|
+
class_map: { "purpose" => "AxisPurposeComp" },
|
|
216
|
+
}
|
|
217
|
+
end
|
|
218
|
+
end
|
|
219
|
+
|
|
220
|
+
parsed = both.from_xml(axes_xml)
|
|
221
|
+
expect(parsed.guidance.map(&:class)).to eq([AxisGuidanceComp, AxisGuidanceComp])
|
|
222
|
+
expect(parsed.purpose.map(&:class)).to eq([AxisPurposeComp])
|
|
223
|
+
end
|
|
224
|
+
end
|
|
133
225
|
end
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "spec_helper"
|
|
4
|
+
|
|
5
|
+
# lutaml-model#88: `unmatched: :raise` on a when_attribute rule fails the
|
|
6
|
+
# parse when an occurrence is claimed by no rule — no discriminator match
|
|
7
|
+
# and no plain sibling on the wire name. The default (:drop) keeps the
|
|
8
|
+
# silent-drop behavior.
|
|
9
|
+
class UnmatchedComponent < 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 UnmatchedRequirement < Lutaml::Model::Serializable
|
|
19
|
+
attribute :guidance, UnmatchedComponent, collection: true
|
|
20
|
+
attribute :purpose, UnmatchedComponent, collection: true
|
|
21
|
+
|
|
22
|
+
xml do
|
|
23
|
+
element "requirement"
|
|
24
|
+
map_element "component", to: :guidance,
|
|
25
|
+
when_attribute: { "type" => "guidance" },
|
|
26
|
+
unmatched: :raise
|
|
27
|
+
map_element "component", to: :purpose,
|
|
28
|
+
when_attribute: { "type" => "purpose" }
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
RSpec.describe "when_attribute unmatched policy" do
|
|
33
|
+
before do
|
|
34
|
+
stub_const("Unmatched::Component", UnmatchedComponent)
|
|
35
|
+
stub_const("Unmatched::Requirement", UnmatchedRequirement)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
it "parses when every occurrence is covered" do
|
|
39
|
+
req = UnmatchedRequirement.from_xml(<<~XML)
|
|
40
|
+
<requirement>
|
|
41
|
+
<component type="guidance"><text>g1</text></component>
|
|
42
|
+
<component type="purpose"><text>p1</text></component>
|
|
43
|
+
</requirement>
|
|
44
|
+
XML
|
|
45
|
+
|
|
46
|
+
expect(req.guidance.map(&:text)).to eq(["g1"])
|
|
47
|
+
expect(req.purpose.map(&:text)).to eq(["p1"])
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
it "raises on an unknown discriminator value" do
|
|
51
|
+
xml = <<~XML
|
|
52
|
+
<requirement>
|
|
53
|
+
<component type="guidance"><text>g1</text></component>
|
|
54
|
+
<component type="unknown"><text>x</text></component>
|
|
55
|
+
</requirement>
|
|
56
|
+
XML
|
|
57
|
+
|
|
58
|
+
expect { UnmatchedRequirement.from_xml(xml) }
|
|
59
|
+
.to raise_error(Lutaml::Model::UnknownDiscriminatorError, /type="unknown"/)
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
it "raises when the discriminator attribute is missing entirely" do
|
|
63
|
+
xml = "<requirement><component><text>x</text></component></requirement>"
|
|
64
|
+
|
|
65
|
+
expect { UnmatchedRequirement.from_xml(xml) }
|
|
66
|
+
.to raise_error(Lutaml::Model::UnknownDiscriminatorError, /<component>/)
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
it "names the tested attributes of the unclaimed occurrence" do
|
|
70
|
+
xml = '<requirement><component type="later"><text>x</text></component></requirement>'
|
|
71
|
+
|
|
72
|
+
expect { UnmatchedRequirement.from_xml(xml) }
|
|
73
|
+
.to raise_error(Lutaml::Model::UnknownDiscriminatorError, /unmatched: :drop/)
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
it "does not raise when a plain rule claims the leftovers" do
|
|
77
|
+
both = Class.new(Lutaml::Model::Serializable) do
|
|
78
|
+
attribute :special, UnmatchedComponent, collection: true
|
|
79
|
+
attribute :plain, UnmatchedComponent, collection: true
|
|
80
|
+
|
|
81
|
+
xml do
|
|
82
|
+
element "holder"
|
|
83
|
+
map_element "item", to: :special,
|
|
84
|
+
when_attribute: { "kind" => "special" },
|
|
85
|
+
unmatched: :raise
|
|
86
|
+
map_element "item", to: :plain
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
stub_const("Unmatched::Holder", both)
|
|
90
|
+
|
|
91
|
+
doc = both.from_xml(
|
|
92
|
+
'<holder><item kind="special"><text>b</text></item><item><text>a</text></item></holder>',
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
expect(doc.special.map(&:text)).to eq(["b"])
|
|
96
|
+
expect(doc.plain.map(&:text)).to eq(["a"])
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
it "drops unmatched occurrences by default" do
|
|
100
|
+
lenient = Class.new(Lutaml::Model::Serializable) do
|
|
101
|
+
attribute :guidance, UnmatchedComponent, collection: true
|
|
102
|
+
|
|
103
|
+
xml do
|
|
104
|
+
element "requirement"
|
|
105
|
+
map_element "component", to: :guidance,
|
|
106
|
+
when_attribute: { "type" => "guidance" }
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
stub_const("Unmatched::Lenient", lenient)
|
|
110
|
+
|
|
111
|
+
doc = lenient.from_xml(
|
|
112
|
+
'<requirement><component type="guidance"><text>g</text></component>' \
|
|
113
|
+
'<component type="other"><text>x</text></component></requirement>',
|
|
114
|
+
)
|
|
115
|
+
expect(doc.guidance.map(&:text)).to eq(["g"])
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
it "survives deep_dup of mappings" do
|
|
119
|
+
rule = UnmatchedRequirement.mappings_for(:xml).mappings.find do |r|
|
|
120
|
+
r.to == :guidance
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
expect(rule.unmatched).to eq(:raise)
|
|
124
|
+
duped = rule.deep_dup
|
|
125
|
+
expect(duped.unmatched).to eq(:raise)
|
|
126
|
+
expect(duped.when_attribute).to eq("type" => "guidance")
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
it "rejects unknown policy values" do
|
|
130
|
+
expect do
|
|
131
|
+
Class.new(Lutaml::Model::Serializable) do
|
|
132
|
+
attribute :x, :string
|
|
133
|
+
|
|
134
|
+
xml do
|
|
135
|
+
element "x"
|
|
136
|
+
map_element "x", to: :x,
|
|
137
|
+
when_attribute: { "type" => "a" },
|
|
138
|
+
unmatched: :explode
|
|
139
|
+
end
|
|
140
|
+
end
|
|
141
|
+
end.to raise_error(Lutaml::Model::IncorrectMappingArgumentsError, /unmatched/)
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
it "rejects unmatched without when_attribute" do
|
|
145
|
+
expect do
|
|
146
|
+
Class.new(Lutaml::Model::Serializable) do
|
|
147
|
+
attribute :x, :string
|
|
148
|
+
|
|
149
|
+
xml do
|
|
150
|
+
element "x"
|
|
151
|
+
map_element "x", to: :x, unmatched: :raise
|
|
152
|
+
end
|
|
153
|
+
end
|
|
154
|
+
end.to raise_error(Lutaml::Model::IncorrectMappingArgumentsError, /when_attribute/)
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
it "is rejected on attribute mappings" do
|
|
158
|
+
expect do
|
|
159
|
+
Class.new(Lutaml::Model::Serializable) do
|
|
160
|
+
attribute :x, :string
|
|
161
|
+
|
|
162
|
+
xml do
|
|
163
|
+
element "x"
|
|
164
|
+
map_attribute "x", to: :x, unmatched: :raise
|
|
165
|
+
end
|
|
166
|
+
end
|
|
167
|
+
end.to raise_error(ArgumentError)
|
|
168
|
+
end
|
|
169
|
+
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.45
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Ribose Inc.
|
|
@@ -489,6 +489,7 @@ files:
|
|
|
489
489
|
- lib/lutaml/model/error/undefined_attribute_error.rb
|
|
490
490
|
- lib/lutaml/model/error/union_schema_unsupported_error.rb
|
|
491
491
|
- lib/lutaml/model/error/unknown_adapter_type_error.rb
|
|
492
|
+
- lib/lutaml/model/error/unknown_discriminator_error.rb
|
|
492
493
|
- lib/lutaml/model/error/unknown_sequence_mapping_error.rb
|
|
493
494
|
- lib/lutaml/model/error/unknown_type_error.rb
|
|
494
495
|
- lib/lutaml/model/error/unresolvable_type_error.rb
|
|
@@ -2006,7 +2007,9 @@ files:
|
|
|
2006
2007
|
- spec/lutaml/xml/validate_xml_with_constructs_spec.rb
|
|
2007
2008
|
- spec/lutaml/xml/validate_xml_with_spec.rb
|
|
2008
2009
|
- spec/lutaml/xml/w3c_types_spec.rb
|
|
2010
|
+
- spec/lutaml/xml/when_attribute_ordered_spec.rb
|
|
2009
2011
|
- spec/lutaml/xml/when_attribute_spec.rb
|
|
2012
|
+
- spec/lutaml/xml/when_attribute_unmatched_spec.rb
|
|
2010
2013
|
- spec/lutaml/xml/xml_adapter_spec.rb
|
|
2011
2014
|
- spec/lutaml/xml/xml_declaration_spec.rb
|
|
2012
2015
|
- spec/lutaml/xml/xml_element_guard_spec.rb
|