lutaml-model 0.8.121 → 0.8.123
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/lib/lutaml/key_value/mapping.rb +19 -0
- data/lib/lutaml/key_value/transform.rb +85 -3
- data/lib/lutaml/key_value/transformation.rb +17 -0
- data/lib/lutaml/model/services/rule_value_extractor.rb +14 -1
- data/lib/lutaml/model/type/boolean.rb +7 -3
- data/lib/lutaml/model/type/float.rb +1 -1
- data/lib/lutaml/model/type/integer.rb +1 -1
- data/lib/lutaml/model/version.rb +1 -1
- data/lib/lutaml/xml/schema/xsd/complex_type.rb +2 -0
- data/lib/lutaml/xml/schema/xsd/extension_complex_content.rb +1 -1
- data/lib/lutaml/xml/schema/xsd/extension_simple_content.rb +1 -1
- data/spec/lutaml/model/map_unmapped_spec.rb +139 -0
- data/spec/lutaml/xml/schema/xsd/any_attribute_spec.rb +65 -0
- metadata +4 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 6d77a19ece999a4de97c03776b3f56bc31eb217fb14904362eef4c98b4623be6
|
|
4
|
+
data.tar.gz: b76ef3e5de721b3e94a02dbac605c61fe3ca44fe97388b8fca1ec4645c3ab494
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 3a03373b96d83e69500cbd2371557f5ced955e85964b3a92340e8bf3cb4da0b45ea3f83ed59fc6f47e410661c5a2e0ca0b9a5d2db6d890ce9d192ed35ccda2c1
|
|
7
|
+
data.tar.gz: 6675bdd426008a94d3e1367b584d3ce92e242b6d6e6e3a85c1e7afc544ab57cce7da847a7af53c9a3f11bd1bfb4f3b49cefb1032eb8157a5e2ddee32b4761545
|
|
@@ -187,6 +187,24 @@ module Lutaml
|
|
|
187
187
|
|
|
188
188
|
alias map_element map
|
|
189
189
|
|
|
190
|
+
# Collects every document key no declared rule claims into the given
|
|
191
|
+
# attribute (#985): vendor extensions and forward-compatible keywords
|
|
192
|
+
# survive the round-trip instead of being dropped. Allowed alongside
|
|
193
|
+
# normal mappings; keys claimed by any rule are never collected, and
|
|
194
|
+
# on serialization the collected hash is merged after the mapped
|
|
195
|
+
# rules so mapped keys win.
|
|
196
|
+
def map_unmapped(to:)
|
|
197
|
+
@unmapped_attribute = to
|
|
198
|
+
end
|
|
199
|
+
|
|
200
|
+
def unmapped_attribute
|
|
201
|
+
@unmapped_attribute
|
|
202
|
+
end
|
|
203
|
+
|
|
204
|
+
# Writer used by deep_dup to carry the catch-all across mapping
|
|
205
|
+
# copies (registers, value-map specializations).
|
|
206
|
+
attr_writer :unmapped_attribute
|
|
207
|
+
|
|
190
208
|
def map_all(
|
|
191
209
|
to: nil,
|
|
192
210
|
render_nil: false,
|
|
@@ -333,6 +351,7 @@ module Lutaml
|
|
|
333
351
|
dup_instance.tap do |new_mapping|
|
|
334
352
|
new_mapping.mappings = duplicate_mappings
|
|
335
353
|
new_mapping.register_mappings = Lutaml::Model::Utils.deep_dup(@register_mappings)
|
|
354
|
+
new_mapping.unmapped_attribute = @unmapped_attribute
|
|
336
355
|
end
|
|
337
356
|
end
|
|
338
357
|
|
|
@@ -43,10 +43,56 @@ module Lutaml
|
|
|
43
43
|
rules.each do |rule|
|
|
44
44
|
process_mapping_rule(data, instance, format, rule, options, partition)
|
|
45
45
|
end
|
|
46
|
+
collect_unmapped_keys(data, instance, mappings, rules)
|
|
46
47
|
|
|
47
48
|
instance
|
|
48
49
|
end
|
|
49
50
|
|
|
51
|
+
# #985: with `map_unmapped to:` declared, document keys no rule
|
|
52
|
+
# claims are collected verbatim into the target attribute, exactly
|
|
53
|
+
# as the model would have kept them outside a mapping.
|
|
54
|
+
def collect_unmapped_keys(data, instance, mappings, rules)
|
|
55
|
+
return unless data.is_a?(::Hash)
|
|
56
|
+
|
|
57
|
+
target = validated_unmapped_target(mappings)
|
|
58
|
+
return if target.nil?
|
|
59
|
+
|
|
60
|
+
claimed = {}
|
|
61
|
+
rules.each do |rule|
|
|
62
|
+
Array(rule.name).each { |name| claimed[name.to_s] = true } if rule.name
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
extras = nil
|
|
66
|
+
data.each do |key, value|
|
|
67
|
+
next if claimed[key.to_s]
|
|
68
|
+
|
|
69
|
+
(extras ||= {})[key] = value
|
|
70
|
+
end
|
|
71
|
+
return if extras.nil?
|
|
72
|
+
|
|
73
|
+
existing = instance.public_send(target)
|
|
74
|
+
merged = existing.is_a?(::Hash) ? existing.merge(extras) : extras
|
|
75
|
+
instance.public_send(:"#{target}=", merged)
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
# Memoized on the cached transform (one instance per model class
|
|
79
|
+
# and register): a typo'd map_unmapped target raises at first
|
|
80
|
+
# parse, naming the class and the target, instead of NoMethodError
|
|
81
|
+
# from inside the walk.
|
|
82
|
+
def validated_unmapped_target(mappings)
|
|
83
|
+
target = mappings.unmapped_attribute
|
|
84
|
+
return if target.nil?
|
|
85
|
+
return @lutaml_unmapped_target if @lutaml_unmapped_target == target
|
|
86
|
+
|
|
87
|
+
unless model_class.attributes(lutaml_register).key?(target)
|
|
88
|
+
raise ArgumentError,
|
|
89
|
+
"map_unmapped target :#{target} is not a declared " \
|
|
90
|
+
"attribute of #{model_class}"
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
@lutaml_unmapped_target = target
|
|
94
|
+
end
|
|
95
|
+
|
|
50
96
|
def model_to_data(instance, format, options = {})
|
|
51
97
|
# NEW ARCHITECTURE: Use KeyValue::Transformation if available
|
|
52
98
|
# This provides symmetric OOP architecture with XmlDataModel
|
|
@@ -83,10 +129,23 @@ module Lutaml
|
|
|
83
129
|
process_rule!(instance, rule, hash, format, mappings, options)
|
|
84
130
|
end
|
|
85
131
|
end
|
|
132
|
+
merge_unmapped_on_serialize(instance, mappings, hash)
|
|
86
133
|
|
|
87
134
|
hash.keys == [""] ? hash[""] : hash
|
|
88
135
|
end
|
|
89
136
|
|
|
137
|
+
# #985: re-emit the collected unknown keys after the mapped rules,
|
|
138
|
+
# so a mapped key colliding with a collected one wins. The
|
|
139
|
+
# attribute was declared in the mapping and validated at compile,
|
|
140
|
+
# so the accessor exists on every instance shape.
|
|
141
|
+
def merge_unmapped_on_serialize(instance, mappings, hash)
|
|
142
|
+
target = mappings.unmapped_attribute
|
|
143
|
+
return if target.nil?
|
|
144
|
+
|
|
145
|
+
extras = instance.public_send(target)
|
|
146
|
+
extras.each { |key, value| hash[key] ||= value } if extras.is_a?(::Hash)
|
|
147
|
+
end
|
|
148
|
+
|
|
90
149
|
# ---- TODO.max-perf/37: group-then-instantiate fast path ----
|
|
91
150
|
|
|
92
151
|
# [model class, format, register] -> rows or false (ineligible).
|
|
@@ -106,6 +165,21 @@ module Lutaml
|
|
|
106
165
|
Concurrent::Map.new
|
|
107
166
|
end
|
|
108
167
|
|
|
168
|
+
# [model class, register, format, rule, generation] -> KvRulePlan,
|
|
169
|
+
# or false for ineligible rules. Class-level: the interpretive
|
|
170
|
+
# walk re-derived the same eligibility chain for the same rule at
|
|
171
|
+
# every node (~12 predicate allocations x nodes per heroku.json
|
|
172
|
+
# parse). Same idempotent-write, generation-retirement pattern as
|
|
173
|
+
# KV_GROUP_PLANS; false (not nil) marks ineligible so a miss never
|
|
174
|
+
# rebuilds.
|
|
175
|
+
KV_RULE_PLANS = if Lutaml::Model.opal?
|
|
176
|
+
{}
|
|
177
|
+
else
|
|
178
|
+
Lutaml::Model::RuntimeCompatibility
|
|
179
|
+
.require_native("concurrent")
|
|
180
|
+
Concurrent::Map.new
|
|
181
|
+
end
|
|
182
|
+
|
|
109
183
|
def self.kv_group_plan(model_class, format, register)
|
|
110
184
|
# Context generation: specs (and apps) reset registers between
|
|
111
185
|
# parses; a plan cached across a reset references dead attribute
|
|
@@ -138,6 +212,9 @@ module Lutaml
|
|
|
138
212
|
|
|
139
213
|
mapping = model_class.mappings_for(format, register)
|
|
140
214
|
return false if mapping.nil?
|
|
215
|
+
# The catch-all collect runs on the interpretive walk only —
|
|
216
|
+
# the compiled hydration must not skip unknown keys silently.
|
|
217
|
+
return false if mapping.unmapped_attribute
|
|
141
218
|
|
|
142
219
|
attrs = model_class.attributes(register)
|
|
143
220
|
rows = nil
|
|
@@ -732,9 +809,14 @@ partition = nil)
|
|
|
732
809
|
end
|
|
733
810
|
|
|
734
811
|
def kv_rule_plan(format, rule, attr)
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
812
|
+
key = [model_class, lutaml_register, format, rule,
|
|
813
|
+
Lutaml::Model::GlobalContext.context_generation]
|
|
814
|
+
cached = KV_RULE_PLANS[key]
|
|
815
|
+
return cached == false ? nil : cached unless cached.nil?
|
|
816
|
+
|
|
817
|
+
plan = build_kv_rule_plan(format, rule, attr)
|
|
818
|
+
KV_RULE_PLANS[key] = plan || false
|
|
819
|
+
plan
|
|
738
820
|
end
|
|
739
821
|
|
|
740
822
|
# `rule.polymorphic` defaults to an empty Hash, which is truthy —
|
|
@@ -45,6 +45,18 @@ module Lutaml
|
|
|
45
45
|
def initialize(model_class, mapping_dsl, format, register)
|
|
46
46
|
# Extract register_id before super
|
|
47
47
|
register_id = extract_register_id(register)
|
|
48
|
+
# #985: the catch-all target (collected unknown keys) — merged
|
|
49
|
+
# after the compiled rules on serialize; mapped keys win. The
|
|
50
|
+
# DSL object is a KeyValue::Mapping by contract, and the target
|
|
51
|
+
# must be a declared attribute: a typo fails here, at compile
|
|
52
|
+
# time, not as a silent no-op on the wire.
|
|
53
|
+
@unmapped_attribute = mapping_dsl.unmapped_attribute
|
|
54
|
+
if @unmapped_attribute &&
|
|
55
|
+
!model_class.attributes(register_id).key?(@unmapped_attribute)
|
|
56
|
+
raise ArgumentError,
|
|
57
|
+
"map_unmapped target :#{@unmapped_attribute} is not a " \
|
|
58
|
+
"declared attribute of #{model_class}"
|
|
59
|
+
end
|
|
48
60
|
transformation_factory = ->(type_class) {
|
|
49
61
|
build_child_transformation(type_class)
|
|
50
62
|
}
|
|
@@ -231,6 +243,11 @@ register = self.register)
|
|
|
231
243
|
end
|
|
232
244
|
end
|
|
233
245
|
|
|
246
|
+
if @unmapped_attribute
|
|
247
|
+
extras = model_instance.public_send(@unmapped_attribute)
|
|
248
|
+
extras.each { |key, value| root[key] ||= value } if extras.is_a?(::Hash)
|
|
249
|
+
end
|
|
250
|
+
|
|
234
251
|
if ENV["DEBUG_KEYED_COLLECTION"]
|
|
235
252
|
puts "=== TRANSFORM COMPLETE ==="
|
|
236
253
|
puts "root: #{root.inspect}"
|
|
@@ -1,6 +1,18 @@
|
|
|
1
1
|
module Lutaml
|
|
2
2
|
module Model
|
|
3
3
|
class RuleValueExtractor < Services::Base
|
|
4
|
+
# Memoized per rule object: the extractor is fresh per node, so
|
|
5
|
+
# the name array rebuilt per node is pure churn (~145k
|
|
6
|
+
# allocations per heroku.json parse). Rules are stable per
|
|
7
|
+
# (class, register); the bounded cache tracks their arrays.
|
|
8
|
+
RULE_NAMES_CACHE = if Lutaml::Model.opal?
|
|
9
|
+
{}
|
|
10
|
+
else
|
|
11
|
+
Lutaml::Model::RuntimeCompatibility
|
|
12
|
+
.require_native("concurrent")
|
|
13
|
+
Concurrent::Map.new
|
|
14
|
+
end
|
|
15
|
+
|
|
4
16
|
def initialize(rule, doc, format, attr, register, options,
|
|
5
17
|
instance_object)
|
|
6
18
|
super()
|
|
@@ -29,7 +41,8 @@ instance_object)
|
|
|
29
41
|
:instance_object
|
|
30
42
|
|
|
31
43
|
def rule_names
|
|
32
|
-
|
|
44
|
+
RULE_NAMES_CACHE[rule] ||=
|
|
45
|
+
rule.multiple_mappings? ? rule.name : [rule.name]
|
|
33
46
|
end
|
|
34
47
|
|
|
35
48
|
def rule_value_for(name)
|
|
@@ -4,11 +4,15 @@ module Lutaml
|
|
|
4
4
|
module Model
|
|
5
5
|
module Type
|
|
6
6
|
class Boolean < Value
|
|
7
|
-
def self.cast(value, _options =
|
|
7
|
+
def self.cast(value, _options = EMPTY_OPTIONS)
|
|
8
8
|
return nil if value.nil?
|
|
9
9
|
return value if Utils.uninitialized?(value)
|
|
10
|
-
|
|
11
|
-
|
|
10
|
+
# Actual booleans are the dominant input (default resolution,
|
|
11
|
+
# parsed JSON/YAML literals): answer without the two to_s +
|
|
12
|
+
# match? allocations the lexical path costs.
|
|
13
|
+
return value if value.equal?(true) || value.equal?(false)
|
|
14
|
+
return true if value.to_s.match?(/^(true|t|yes|y|1)$/i)
|
|
15
|
+
return false if value.to_s.match?(/^(false|f|no|n|0)$/i)
|
|
12
16
|
|
|
13
17
|
value
|
|
14
18
|
end
|
|
@@ -5,7 +5,7 @@ module Lutaml
|
|
|
5
5
|
module Type
|
|
6
6
|
class Integer < Value
|
|
7
7
|
# Performance-optimized cast with short-circuit for already-correct types
|
|
8
|
-
def self.cast(value, options =
|
|
8
|
+
def self.cast(value, options = EMPTY_OPTIONS)
|
|
9
9
|
return nil if value.nil?
|
|
10
10
|
return value if Utils.uninitialized?(value)
|
|
11
11
|
# Short-circuit: return immediately if already an Integer
|
data/lib/lutaml/model/version.rb
CHANGED
|
@@ -30,6 +30,7 @@ module Lutaml
|
|
|
30
30
|
initialize_empty: true
|
|
31
31
|
attribute :attribute_group, :attribute_group, collection: true,
|
|
32
32
|
initialize_empty: true
|
|
33
|
+
attribute :any_attribute, :any_attribute
|
|
33
34
|
|
|
34
35
|
xml do
|
|
35
36
|
element "complexType"
|
|
@@ -50,6 +51,7 @@ module Lutaml
|
|
|
50
51
|
map_element :attribute, to: :attribute
|
|
51
52
|
map_element :annotation, to: :annotation
|
|
52
53
|
map_element :attributeGroup, to: :attribute_group
|
|
54
|
+
map_element :anyAttribute, to: :any_attribute
|
|
53
55
|
map_element :simpleContent, to: :simple_content
|
|
54
56
|
map_element :complexContent, to: :complex_content
|
|
55
57
|
end
|
|
@@ -31,7 +31,7 @@ module Lutaml
|
|
|
31
31
|
map_element :sequence, to: :sequence
|
|
32
32
|
map_element :attribute, to: :attribute
|
|
33
33
|
map_element :annotation, to: :annotation
|
|
34
|
-
map_element :
|
|
34
|
+
map_element :anyAttribute, to: :any_attribute
|
|
35
35
|
map_element :attributeGroup, to: :attribute_group
|
|
36
36
|
end
|
|
37
37
|
|
|
@@ -23,7 +23,7 @@ module Lutaml
|
|
|
23
23
|
map_attribute :base, to: :base
|
|
24
24
|
map_element :attribute, to: :attribute
|
|
25
25
|
map_element :annotation, to: :annotation
|
|
26
|
-
map_element :
|
|
26
|
+
map_element :anyAttribute, to: :any_attribute
|
|
27
27
|
map_element :attributeGroup, to: :attribute_group
|
|
28
28
|
end
|
|
29
29
|
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "spec_helper"
|
|
4
|
+
|
|
5
|
+
# #985: `map_unmapped to:` collects document keys no declared rule claims
|
|
6
|
+
# into the given attribute, so vendor extensions and forward-compatible
|
|
7
|
+
# keywords survive the round-trip. Opt-in: models without the directive
|
|
8
|
+
# keep the drop behavior. Compiled fast paths are ineligible for such
|
|
9
|
+
# models — the collect runs on the interpretive walk.
|
|
10
|
+
RSpec.describe "key-value catch-all mapping (map_unmapped)" do
|
|
11
|
+
let(:schema_class) do
|
|
12
|
+
Class.new(Lutaml::Model::Serializable) do
|
|
13
|
+
attribute :type, :string
|
|
14
|
+
attribute :description, :string
|
|
15
|
+
attribute :extensions, :hash, default: -> { {} }
|
|
16
|
+
|
|
17
|
+
key_value do
|
|
18
|
+
map "type", to: :type
|
|
19
|
+
map "description", to: :description
|
|
20
|
+
map_unmapped to: :extensions
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
let(:plain_class) do
|
|
26
|
+
Class.new(Lutaml::Model::Serializable) do
|
|
27
|
+
attribute :type, :string
|
|
28
|
+
|
|
29
|
+
key_value do
|
|
30
|
+
map "type", to: :type
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
before do
|
|
36
|
+
stub_const("MapUnmappedSchema", schema_class)
|
|
37
|
+
stub_const("MapUnmappedPlain", plain_class)
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
it "collects unmapped keys on parse" do
|
|
41
|
+
model = MapUnmappedSchema.from_json(
|
|
42
|
+
'{"type":"object","x-enumDescriptions":{"a":"A"},"future":true}',
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
expect(model.type).to eq("object")
|
|
46
|
+
expect(model.extensions).to eq(
|
|
47
|
+
"x-enumDescriptions" => { "a" => "A" }, "future" => true,
|
|
48
|
+
)
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
it "never duplicates keys claimed by a rule" do
|
|
52
|
+
model = MapUnmappedSchema.from_json('{"type":"object"}')
|
|
53
|
+
|
|
54
|
+
expect(model.extensions).to eq({})
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
it "re-emits collected keys verbatim on serialization" do
|
|
58
|
+
model = MapUnmappedSchema.from_json(
|
|
59
|
+
'{"type":"object","x-enumDescriptions":{"a":"A"},"future":true}',
|
|
60
|
+
)
|
|
61
|
+
out = JSON.parse(model.to_json)
|
|
62
|
+
|
|
63
|
+
expect(out["x-enumDescriptions"]).to eq("a" => "A")
|
|
64
|
+
expect(out["future"]).to be(true)
|
|
65
|
+
expect(out["type"]).to eq("object")
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
it "lets mapped keys win on collision with collected keys" do
|
|
69
|
+
model = MapUnmappedSchema.new
|
|
70
|
+
model.type = "object"
|
|
71
|
+
model.extensions = { "type" => "extension-wins-only-if-unmapped" }
|
|
72
|
+
|
|
73
|
+
out = JSON.parse(model.to_json)
|
|
74
|
+
|
|
75
|
+
expect(out["type"]).to eq("object")
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
it "round-trips in yaml through the same contract" do
|
|
79
|
+
model = MapUnmappedSchema.from_yaml(
|
|
80
|
+
<<~YAML,
|
|
81
|
+
type: object
|
|
82
|
+
x-vendor: kept
|
|
83
|
+
YAML
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
expect(model.extensions).to eq("x-vendor" => "kept")
|
|
87
|
+
expect(YAML.safe_load(model.to_yaml)["x-vendor"]).to eq("kept")
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
it "fails fast when the target is not a declared attribute" do
|
|
91
|
+
typo_class = Class.new(Lutaml::Model::Serializable) do
|
|
92
|
+
attribute :type, :string
|
|
93
|
+
|
|
94
|
+
key_value do
|
|
95
|
+
map "type", to: :type
|
|
96
|
+
map_unmapped to: :extenshuns
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
stub_const("MapUnmappedTypo", typo_class)
|
|
100
|
+
|
|
101
|
+
# from_json wraps the raise in the format-error contract, message
|
|
102
|
+
# intact; to_json surfaces the bare ArgumentError at compile.
|
|
103
|
+
expect do
|
|
104
|
+
typo_class.from_json('{"type":"object","x-a":1}')
|
|
105
|
+
end.to raise_error(
|
|
106
|
+
Lutaml::Model::InvalidFormatError,
|
|
107
|
+
/map_unmapped target :extenshuns is not a declared attribute of/,
|
|
108
|
+
)
|
|
109
|
+
expect do
|
|
110
|
+
typo_class.new.to_json
|
|
111
|
+
end.to raise_error(
|
|
112
|
+
ArgumentError,
|
|
113
|
+
/map_unmapped target :extenshuns is not a declared attribute of/,
|
|
114
|
+
)
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
it "keeps the drop behavior when no catch-all is declared" do
|
|
118
|
+
model = MapUnmappedPlain.from_json('{"type":"object","x-lost":1}')
|
|
119
|
+
|
|
120
|
+
expect(model.to_json).not_to include("x-lost")
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
it "collects per instance in nested models" do
|
|
124
|
+
container = Class.new(Lutaml::Model::Serializable) do
|
|
125
|
+
attribute :child, "MapUnmappedSchema"
|
|
126
|
+
|
|
127
|
+
key_value do
|
|
128
|
+
map "child", to: :child
|
|
129
|
+
end
|
|
130
|
+
end
|
|
131
|
+
stub_const("MapUnmappedContainer", container)
|
|
132
|
+
|
|
133
|
+
model = MapUnmappedContainer.from_json(
|
|
134
|
+
'{"child":{"type":"object","x-inner":1}}',
|
|
135
|
+
)
|
|
136
|
+
|
|
137
|
+
expect(model.child.extensions).to eq("x-inner" => 1)
|
|
138
|
+
end
|
|
139
|
+
end
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "spec_helper"
|
|
4
|
+
|
|
5
|
+
RSpec.describe "xs:anyAttribute mappings" do
|
|
6
|
+
it "maps anyAttribute on a complexType from the parsed XSD" do
|
|
7
|
+
xsd = <<~XSD
|
|
8
|
+
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema">
|
|
9
|
+
<xs:complexType name="t">
|
|
10
|
+
<xs:attribute name="a"/>
|
|
11
|
+
<xs:anyAttribute namespace="##other" processContents="strict"/>
|
|
12
|
+
</xs:complexType>
|
|
13
|
+
</xs:schema>
|
|
14
|
+
XSD
|
|
15
|
+
|
|
16
|
+
schema = Lutaml::Xml::Schema::Xsd.parse(xsd)
|
|
17
|
+
wildcard = schema.complex_type.first.any_attribute
|
|
18
|
+
|
|
19
|
+
expect(wildcard).not_to be_nil
|
|
20
|
+
expect(wildcard.namespace).to eq("##other")
|
|
21
|
+
expect(wildcard.process_contents).to eq("strict")
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
it "maps anyAttribute inside a simpleContent extension" do
|
|
25
|
+
xsd = <<~XSD
|
|
26
|
+
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema">
|
|
27
|
+
<xs:complexType name="t">
|
|
28
|
+
<xs:simpleContent>
|
|
29
|
+
<xs:extension base="xs:string">
|
|
30
|
+
<xs:anyAttribute namespace="##local"/>
|
|
31
|
+
</xs:extension>
|
|
32
|
+
</xs:simpleContent>
|
|
33
|
+
</xs:complexType>
|
|
34
|
+
</xs:schema>
|
|
35
|
+
XSD
|
|
36
|
+
|
|
37
|
+
schema = Lutaml::Xml::Schema::Xsd.parse(xsd)
|
|
38
|
+
extension = schema.complex_type.first.simple_content.extension
|
|
39
|
+
wildcard = extension.any_attribute
|
|
40
|
+
|
|
41
|
+
expect(wildcard).not_to be_nil
|
|
42
|
+
expect(wildcard.namespace).to eq("##local")
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
it "maps anyAttribute inside a complexContent extension" do
|
|
46
|
+
xsd = <<~XSD
|
|
47
|
+
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema">
|
|
48
|
+
<xs:complexType name="t">
|
|
49
|
+
<xs:complexContent>
|
|
50
|
+
<xs:extension base="xs:anyType">
|
|
51
|
+
<xs:anyAttribute namespace="##targetNamespace"/>
|
|
52
|
+
</xs:extension>
|
|
53
|
+
</xs:complexContent>
|
|
54
|
+
</xs:complexType>
|
|
55
|
+
</xs:schema>
|
|
56
|
+
XSD
|
|
57
|
+
|
|
58
|
+
schema = Lutaml::Xml::Schema::Xsd.parse(xsd)
|
|
59
|
+
extension = schema.complex_type.first.complex_content.extension
|
|
60
|
+
wildcard = extension.any_attribute
|
|
61
|
+
|
|
62
|
+
expect(wildcard).not_to be_nil
|
|
63
|
+
expect(wildcard.namespace).to eq("##targetNamespace")
|
|
64
|
+
end
|
|
65
|
+
end
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: lutaml-model
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.8.
|
|
4
|
+
version: 0.8.123
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Ribose Inc.
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: exe
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-10-
|
|
11
|
+
date: 2026-10-22 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: base64
|
|
@@ -1778,6 +1778,7 @@ files:
|
|
|
1778
1778
|
- spec/lutaml/model/logger_spec.rb
|
|
1779
1779
|
- spec/lutaml/model/map_all_spec.rb
|
|
1780
1780
|
- spec/lutaml/model/map_content_spec.rb
|
|
1781
|
+
- spec/lutaml/model/map_unmapped_spec.rb
|
|
1781
1782
|
- spec/lutaml/model/mapping_rule_perf_guard_spec.rb
|
|
1782
1783
|
- spec/lutaml/model/mapping_type_validation_spec.rb
|
|
1783
1784
|
- spec/lutaml/model/mixed_content_spec.rb
|
|
@@ -2018,6 +2019,7 @@ files:
|
|
|
2018
2019
|
- spec/lutaml/xml/schema/facet_round_trip_spec.rb
|
|
2019
2020
|
- spec/lutaml/xml/schema/rnc_compiler_spec.rb
|
|
2020
2021
|
- spec/lutaml/xml/schema/rng_compiler_spec.rb
|
|
2022
|
+
- spec/lutaml/xml/schema/xsd/any_attribute_spec.rb
|
|
2021
2023
|
- spec/lutaml/xml/schema/xsd/errors/error_context_spec.rb
|
|
2022
2024
|
- spec/lutaml/xml/schema/xsd/errors/suggestion_spec.rb
|
|
2023
2025
|
- spec/lutaml/xml/schema/xsd/file_validation_result_spec.rb
|