lutaml-model 0.8.141 → 0.8.142
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/PERFORMANCE_METHODOLOGY.md +34 -5
- data/lib/lutaml/model/choice.rb +1 -3
- data/lib/lutaml/model/liquefiable.rb +15 -4
- data/lib/lutaml/model/serialize/attribute_definition.rb +3 -0
- data/lib/lutaml/model/serialize/format_conversion.rb +10 -4
- data/lib/lutaml/model/serialize/initialization.rb +154 -8
- data/lib/lutaml/model/serialize/model_import.rb +9 -2
- data/lib/lutaml/model/version.rb +1 -1
- data/lib/lutaml/xml/leptris/plan_hydrator.rb +5 -2
- data/lib/lutaml/xml/mapping.rb +12 -0
- data/lib/lutaml/xml/model_transform.rb +4 -2
- data/lib/lutaml/xml/namespace_collector.rb +22 -11
- data/lib/lutaml/xml/schema/xsd/redefine.rb +1 -1
- data/lib/lutaml/xml/serialization/format_conversion.rb +1 -0
- data/lib/lutaml/xml/serialization/model_import_ext.rb +1 -1
- data/spec/lutaml/model/inherited_state_isolation_spec.rb +202 -0
- data/spec/lutaml/xml/namespace_collector_child_register_spec.rb +71 -0
- data/spec/lutaml/xml/schema/xsd/redefine_schema_location_spec.rb +65 -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: 4ddbdf9b831babcd7fd6bd41314ea8b4f2352503f0302b2420942d9d144dc0bb
|
|
4
|
+
data.tar.gz: 4704a4bac05aef07a9a0a0a939180cd10d15ad4a8ab086dc218b126c68f97d2f
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 817089bc912e7a85d912ecdffc09e8d894cadb3ca416450f6b5cab6d3b2463e4d9210ccf8ca0af1710a4de088562ef5628ec5896ff9585dc0ad6798444897a77
|
|
7
|
+
data.tar.gz: b1196728958dfc2e8be4ed6da1a70c4b5e09079c7c1d3940398e4c1c97adbd5bfa34d74a829ae51c9ad8a85d09ddcad5028050eca1de432f897b98fbdce5ff60
|
|
@@ -328,8 +328,9 @@ end
|
|
|
328
328
|
```
|
|
329
329
|
|
|
330
330
|
**Invalidation:** `finalize()` clears all caches before setting `@finalized`.
|
|
331
|
-
This handles the case where
|
|
332
|
-
|
|
331
|
+
This handles the case where a finalized mapping is copied into a child class
|
|
332
|
+
(eagerly, or on first write under the inherited-state copy-on-write scheme),
|
|
333
|
+
then the child adds its own mappings and re-finalizes.
|
|
333
334
|
|
|
334
335
|
```ruby
|
|
335
336
|
def finalize(mapper_class)
|
|
@@ -429,6 +430,31 @@ classes caused 2-5x time regression despite reducing allocations.
|
|
|
429
430
|
**Rule:** Keep caching inline with `@ivar` access. Guard specs that exercise
|
|
430
431
|
the code path (not the cache implementation) are the correct tradeoff.
|
|
431
432
|
|
|
433
|
+
### Pattern G: Copy-on-Write Inherited State
|
|
434
|
+
|
|
435
|
+
**What happened:** every subclass used to `deep_dup` its parent's mappings,
|
|
436
|
+
attributes, choices and register records at inheritance time — about 272
|
|
437
|
+
allocations for a two-attribute parent, paid whether or not the subclass ever
|
|
438
|
+
declared anything of its own.
|
|
439
|
+
|
|
440
|
+
**Now:** a subclass shares the parent's structures by reference. A 2-bit
|
|
441
|
+
status register per structure (`mappings`, `attributes`, `choices`,
|
|
442
|
+
`registers`) records whether it is still inherited-shared or already owned,
|
|
443
|
+
and whether the owned copy is itself shared with a further descendant. The
|
|
444
|
+
first DSL write on the child forks that one structure (deep-dupping it and
|
|
445
|
+
invalidating the merged-attribute cache); reads keep using the parent's
|
|
446
|
+
objects. `choices` fork eagerly because imports mutate them in place.
|
|
447
|
+
|
|
448
|
+
A DSL write on the parent forks the parent's own copy instead of mutating the
|
|
449
|
+
structure its children share, so an already-defined child keeps exactly the
|
|
450
|
+
state it inherited — the same snapshot semantics the eager copy provided, at
|
|
451
|
+
none of the up-front cost.
|
|
452
|
+
|
|
453
|
+
**Rule:** never mutate one of these structures directly. Go through the
|
|
454
|
+
fork accessor (`lutaml_cow_fork!`, `lutaml_cow_mapping_for_dsl!`,
|
|
455
|
+
`register_record_for_dsl!`); a bare `<<` on an inherited hash or array
|
|
456
|
+
mutates the whole ancestry.
|
|
457
|
+
|
|
432
458
|
---
|
|
433
459
|
|
|
434
460
|
## 7. Profiling Guide
|
|
@@ -564,11 +590,14 @@ name, unprefixed name, or through the default namespace. Any optimization
|
|
|
564
590
|
that changes the matching loop must preserve all three match paths. Test
|
|
565
591
|
with Uniword (10+ sub-namespaces) and XMI (namespace aliases).
|
|
566
592
|
|
|
567
|
-
### Pitfall 4:
|
|
593
|
+
### Pitfall 4: Inherited-State Copies + Finalize Cache Interaction
|
|
568
594
|
|
|
569
|
-
**What happened:**
|
|
595
|
+
**What happened:** an inherited copy of a finalized mapping carried its
|
|
570
596
|
frozen caches. The child class then added new mappings, but the cached
|
|
571
|
-
elements/attributes were stale.
|
|
597
|
+
elements/attributes were stale. (The copy used to be an eager `deep_dup` on
|
|
598
|
+
every subclass; it is now copy-on-write — the parent's structures stay shared
|
|
599
|
+
until the child first mutates one — but the cache hazard is the same at the
|
|
600
|
+
moment the copy is materialized.)
|
|
572
601
|
|
|
573
602
|
**Rule:** `finalize()` must clear all caches before setting `@finalized`.
|
|
574
603
|
This ensures the first access after finalization builds correct caches.
|
data/lib/lutaml/model/choice.rb
CHANGED
|
@@ -128,9 +128,7 @@ register = nil)
|
|
|
128
128
|
|
|
129
129
|
if register_id != :default
|
|
130
130
|
# Non-default register: store in register-specific storage only
|
|
131
|
-
current_record = @model.
|
|
132
|
-
attributes: {}, choice_attributes: []
|
|
133
|
-
}
|
|
131
|
+
current_record = @model.register_record_for_dsl!(register_id)
|
|
134
132
|
imported_attributes = Utils.deep_dup(model.attributes(register_id))
|
|
135
133
|
imported_attributes.each_value do |attr|
|
|
136
134
|
attr.options[:choice] = self
|
|
@@ -14,7 +14,12 @@ module Lutaml
|
|
|
14
14
|
def inherited(child)
|
|
15
15
|
super
|
|
16
16
|
child.register_class_if_liquid_defined
|
|
17
|
-
|
|
17
|
+
# Serialize models share mappings copy-on-write; a standalone
|
|
18
|
+
# Liquefiable class has no such state, so the eager copy is
|
|
19
|
+
# its only isolation from the parent.
|
|
20
|
+
unless child <= Lutaml::Model::Serialize
|
|
21
|
+
child.mappings = Utils.deep_dup(mappings) || {}
|
|
22
|
+
end
|
|
18
23
|
end
|
|
19
24
|
|
|
20
25
|
def register_class_if_liquid_defined
|
|
@@ -154,9 +159,15 @@ module Lutaml
|
|
|
154
159
|
end
|
|
155
160
|
|
|
156
161
|
def liquid(&block)
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
162
|
+
if self <= Lutaml::Model::Serialize
|
|
163
|
+
mapping = lutaml_cow_mapping_for_dsl!(:liquid) ||
|
|
164
|
+
::Lutaml::Model::Liquid::Mapping.new
|
|
165
|
+
mappings[:liquid] = mapping
|
|
166
|
+
else
|
|
167
|
+
mapping = mappings[:liquid] ||= ::Lutaml::Model::Liquid::Mapping.new
|
|
168
|
+
end
|
|
169
|
+
mapping.instance_eval(&block) if block
|
|
170
|
+
mapping
|
|
160
171
|
end
|
|
161
172
|
|
|
162
173
|
def liquid_mappings
|
|
@@ -833,6 +833,7 @@ module Lutaml
|
|
|
833
833
|
end
|
|
834
834
|
|
|
835
835
|
attr = Attribute.new(name, type, options)
|
|
836
|
+
lutaml_cow_fork!(:attributes)
|
|
836
837
|
@attributes[name] = attr
|
|
837
838
|
@merged_attributes_cache = nil
|
|
838
839
|
# Attributes can be declared after the model has been
|
|
@@ -853,6 +854,8 @@ module Lutaml
|
|
|
853
854
|
# @return [Symbol] The attribute name
|
|
854
855
|
def restrict(name, options = {})
|
|
855
856
|
register_id = options.delete(:register) || Lutaml::Model::Config.default_register
|
|
857
|
+
lutaml_cow_fork!(:attributes)
|
|
858
|
+
lutaml_cow_fork!(:registers)
|
|
856
859
|
|
|
857
860
|
if !@attributes.key?(name) && !register_record(register_id)&.dig(
|
|
858
861
|
:attributes, name
|
|
@@ -22,7 +22,7 @@ module Lutaml
|
|
|
22
22
|
end
|
|
23
23
|
|
|
24
24
|
klass = ::Lutaml::Model::Config.mappings_class_for(format)
|
|
25
|
-
existing =
|
|
25
|
+
existing = lutaml_cow_mapping_for_dsl!(format)
|
|
26
26
|
mappings[format] = if existing.nil? || !existing.is_a?(klass)
|
|
27
27
|
klass.new
|
|
28
28
|
else
|
|
@@ -476,9 +476,14 @@ module Lutaml
|
|
|
476
476
|
end
|
|
477
477
|
|
|
478
478
|
formats.each do |format|
|
|
479
|
-
mappings
|
|
480
|
-
|
|
481
|
-
|
|
479
|
+
# The helper may replace @mappings mid-flight (CoW
|
|
480
|
+
# fork); bind it to a local before indexing, or the
|
|
481
|
+
# element assignment lands on the abandoned hash.
|
|
482
|
+
mapping = lutaml_cow_mapping_for_dsl!(format) ||
|
|
483
|
+
Lutaml::KeyValue::Mapping.new(format)
|
|
484
|
+
mappings[format] = mapping
|
|
485
|
+
mapping.instance_eval(&block)
|
|
486
|
+
mapping.finalize(self)
|
|
482
487
|
end
|
|
483
488
|
end
|
|
484
489
|
|
|
@@ -488,6 +493,7 @@ module Lutaml
|
|
|
488
493
|
# @param block [Proc] The DSL block
|
|
489
494
|
def rdf(&block)
|
|
490
495
|
Lutaml::Model::FormatRegistry.rdf_formats.each do |format|
|
|
496
|
+
lutaml_cow_fork!(:mappings)
|
|
491
497
|
mappings[format] = Lutaml::Rdf::Mapping.new
|
|
492
498
|
mappings[format].instance_eval(&block)
|
|
493
499
|
end
|
|
@@ -86,21 +86,159 @@ module Lutaml
|
|
|
86
86
|
base.initialize_attrs(self)
|
|
87
87
|
end
|
|
88
88
|
|
|
89
|
+
# Copy-on-write structure status. Two bits per structure
|
|
90
|
+
# (mappings, attributes, choices, registers): 0 = inherited and
|
|
91
|
+
# shared with the source class, 1 = owned and private (safe to
|
|
92
|
+
# mutate in place), 2 = owned but shared with subclasses (fork
|
|
93
|
+
# before mutating so they keep the pre-mutation state).
|
|
94
|
+
LUTAML_COW_STRUCTURES = %i[mappings attributes choices registers].freeze
|
|
95
|
+
|
|
89
96
|
# Initialize class attributes from a source class
|
|
90
97
|
#
|
|
91
98
|
# @param source_class [Class] The source class to copy from
|
|
92
99
|
def initialize_attrs(source_class)
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
100
|
+
if source_class.equal?(self)
|
|
101
|
+
# A subclass that says `include Lutaml::Model::Serialize`
|
|
102
|
+
# again re-enters this hook with itself as the source. It
|
|
103
|
+
# already carries the state `inherited` installed, and the
|
|
104
|
+
# eager implementation re-copied that state; here it must
|
|
105
|
+
# survive untouched, so only a brand-new class initializes.
|
|
106
|
+
initialize_empty_attrs if @attributes.nil?
|
|
107
|
+
else
|
|
108
|
+
@mappings = source_class.mappings
|
|
109
|
+
@attributes = source_class.class_attributes
|
|
110
|
+
@choice_attributes = source_class.class_choice_attributes
|
|
111
|
+
@register_records = source_class.register_records
|
|
112
|
+
@lutaml_cow_status = 0
|
|
113
|
+
lutaml_cow_mark_ancestors_shared!(source_class)
|
|
114
|
+
# A choice binds its owning class and stamps itself onto
|
|
115
|
+
# the attribute options, so a subclass must rebind its
|
|
116
|
+
# choices to itself. Models without choices — the vast
|
|
117
|
+
# majority — keep sharing the empty array.
|
|
118
|
+
lutaml_cow_fork!(:choices) if @choice_attributes&.any?
|
|
119
|
+
end
|
|
101
120
|
model(self)
|
|
102
121
|
end
|
|
103
122
|
|
|
123
|
+
def initialize_empty_attrs
|
|
124
|
+
# `Liquefiable` may already have created the mapping store
|
|
125
|
+
# before this class included `Serialize`; that state is the
|
|
126
|
+
# class's own and must survive initialization.
|
|
127
|
+
@mappings ||= {}
|
|
128
|
+
@attributes ||= {}
|
|
129
|
+
@choice_attributes ||= []
|
|
130
|
+
@register_records ||= ::Hash.new do |hash, key|
|
|
131
|
+
hash[key] = { attributes: {}, choice_attributes: [] }
|
|
132
|
+
end
|
|
133
|
+
@lutaml_cow_status = Lutaml::Model::Serialize::Initialization
|
|
134
|
+
.lutaml_cow_status_for(1)
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
# Status value for every structure, encoded per structure
|
|
138
|
+
def self.lutaml_cow_status_for(state)
|
|
139
|
+
LUTAML_COW_STRUCTURES.each_index.sum do |i|
|
|
140
|
+
state << (i * 2)
|
|
141
|
+
end
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
def lutaml_cow_status(structure)
|
|
145
|
+
shift = LUTAML_COW_STRUCTURES.index(structure) * 2
|
|
146
|
+
((@lutaml_cow_status || 0) >> shift) & 3
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
# Pack one structure's state back into the status word
|
|
150
|
+
def lutaml_cow_set_status!(structure, state)
|
|
151
|
+
shift = LUTAML_COW_STRUCTURES.index(structure) * 2
|
|
152
|
+
@lutaml_cow_status = ((@lutaml_cow_status || 0) & ~(3 << shift)) |
|
|
153
|
+
(state << shift)
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
# The structures this class inherited are about to gain a second
|
|
157
|
+
# referencer (the new subclass). Every ancestor whose current
|
|
158
|
+
# object for a structure is the object being shared must flip
|
|
159
|
+
# from owned-private to owned-shared, or its next in-place
|
|
160
|
+
# mutation would leak into the new subclass.
|
|
161
|
+
def lutaml_cow_mark_ancestors_shared!(source_class)
|
|
162
|
+
LUTAML_COW_STRUCTURES.each do |structure|
|
|
163
|
+
shared = lutaml_cow_structure(structure)
|
|
164
|
+
klass = source_class
|
|
165
|
+
loop do
|
|
166
|
+
if klass.lutaml_cow_structure(structure).equal?(shared) &&
|
|
167
|
+
klass.lutaml_cow_status(structure) == 1
|
|
168
|
+
klass.lutaml_cow_set_status!(structure, 2)
|
|
169
|
+
end
|
|
170
|
+
break unless klass.is_a?(Class)
|
|
171
|
+
|
|
172
|
+
klass = klass.superclass
|
|
173
|
+
break unless klass.include?(Lutaml::Model::Serialize)
|
|
174
|
+
end
|
|
175
|
+
end
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
def lutaml_cow_structure(structure)
|
|
179
|
+
case structure
|
|
180
|
+
when :mappings then @mappings
|
|
181
|
+
when :attributes then @attributes
|
|
182
|
+
when :choices then @choice_attributes
|
|
183
|
+
when :registers then @register_records
|
|
184
|
+
end
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
private
|
|
188
|
+
|
|
189
|
+
# Replace a shared structure with a class-owned copy. The copy
|
|
190
|
+
# becomes the snapshot for every existing subclass, which is what
|
|
191
|
+
# keeps post-fork mutations invisible to them (the eager-copy
|
|
192
|
+
# semantics).
|
|
193
|
+
def lutaml_cow_fork!(structure)
|
|
194
|
+
return if lutaml_cow_status(structure) == 1
|
|
195
|
+
|
|
196
|
+
# The merged attribute view is built from the structures
|
|
197
|
+
# below; a replacement makes any cached merge stale.
|
|
198
|
+
@merged_attributes_cache = nil
|
|
199
|
+
case structure
|
|
200
|
+
when :mappings
|
|
201
|
+
# Entries are shared with the source class as well as the
|
|
202
|
+
# hash itself: a sibling inheriting the same object must
|
|
203
|
+
# not see this class's DSL. Copying every entry here is
|
|
204
|
+
# the lazy equivalent of the eager snapshot the previous
|
|
205
|
+
# implementation took in `inherited`.
|
|
206
|
+
@mappings = Utils.deep_dup(@mappings) || {}
|
|
207
|
+
when :attributes
|
|
208
|
+
@attributes = Utils.deep_dup(@attributes) || {}
|
|
209
|
+
when :choices
|
|
210
|
+
# Choice duplication rebinds Attribute objects through
|
|
211
|
+
# `attributes`, so the attributes fork must land first.
|
|
212
|
+
lutaml_cow_fork!(:attributes)
|
|
213
|
+
@choice_attributes = deep_duplicate_choice_attributes(self)
|
|
214
|
+
when :registers
|
|
215
|
+
@register_records = Utils.deep_dup(@register_records) ||
|
|
216
|
+
::Hash.new do |hash, key|
|
|
217
|
+
hash[key] = { attributes: {}, choice_attributes: [] }
|
|
218
|
+
end
|
|
219
|
+
end
|
|
220
|
+
lutaml_cow_set_status!(structure, 1)
|
|
221
|
+
end
|
|
222
|
+
|
|
223
|
+
public
|
|
224
|
+
|
|
225
|
+
# Return the mapping object a format DSL may mutate. Forking
|
|
226
|
+
# the mappings structure copies every entry, so the object
|
|
227
|
+
# returned here is this class's own.
|
|
228
|
+
def lutaml_cow_mapping_for_dsl!(format)
|
|
229
|
+
lutaml_cow_fork!(:mappings)
|
|
230
|
+
@mappings[format]
|
|
231
|
+
end
|
|
232
|
+
|
|
233
|
+
# Return the register record a format DSL may mutate. The
|
|
234
|
+
# register store is copy-on-write, so write access forks it
|
|
235
|
+
# away from the parent class before handing out the record.
|
|
236
|
+
def register_record_for_dsl!(register_id)
|
|
237
|
+
lutaml_cow_fork!(:registers)
|
|
238
|
+
@register_records[register_id] ||= { attributes: {},
|
|
239
|
+
choice_attributes: [] }
|
|
240
|
+
end
|
|
241
|
+
|
|
104
242
|
# Deep duplicate choice attributes from a source class
|
|
105
243
|
#
|
|
106
244
|
# @param source_class [Class] The source class
|
|
@@ -153,6 +291,13 @@ module Lutaml
|
|
|
153
291
|
@attributes
|
|
154
292
|
end
|
|
155
293
|
|
|
294
|
+
# Raw class-level choice attributes without register merging.
|
|
295
|
+
# Used by initialize_attrs during class inheritance.
|
|
296
|
+
# @return [Array] The raw choice attributes array
|
|
297
|
+
def class_choice_attributes
|
|
298
|
+
@choice_attributes
|
|
299
|
+
end
|
|
300
|
+
|
|
156
301
|
# Get all choice attributes for this model
|
|
157
302
|
#
|
|
158
303
|
# Merges class-level choice attributes with register-specific choice attributes.
|
|
@@ -331,6 +476,7 @@ module Lutaml
|
|
|
331
476
|
# @param max [Integer] Maximum number of choices
|
|
332
477
|
# @param block [Proc] The choice definition block
|
|
333
478
|
def choice(min: 1, max: 1, format: nil, &block)
|
|
479
|
+
lutaml_cow_fork!(:choices)
|
|
334
480
|
@choice_attributes << Choice.new(self, min, max,
|
|
335
481
|
format: format).tap do |c|
|
|
336
482
|
c.instance_eval(&block)
|
|
@@ -57,6 +57,8 @@ module Lutaml
|
|
|
57
57
|
end
|
|
58
58
|
|
|
59
59
|
# Default register: merge into class-level storage
|
|
60
|
+
lutaml_cow_fork!(:attributes)
|
|
61
|
+
lutaml_cow_fork!(:choices)
|
|
60
62
|
model.attributes.each_value { |attr| define_attribute_methods(attr) }
|
|
61
63
|
@attributes.merge!(Utils.deep_dup(model.attributes))
|
|
62
64
|
@choice_attributes.concat(deep_duplicate_choice_attributes(model))
|
|
@@ -81,8 +83,8 @@ module Lutaml
|
|
|
81
83
|
next unless model.mappings.key?(format)
|
|
82
84
|
|
|
83
85
|
klass = ::Lutaml::Model::Config.mappings_class_for(format)
|
|
84
|
-
|
|
85
|
-
|
|
86
|
+
mapping = lutaml_cow_mapping_for_dsl!(format) || klass.new
|
|
87
|
+
@mappings[format] = mapping
|
|
86
88
|
if mapping.is_a?(Mapping)
|
|
87
89
|
mapping.import_model_mappings(model,
|
|
88
90
|
register_id)
|
|
@@ -115,6 +117,8 @@ module Lutaml
|
|
|
115
117
|
# @param model [Class] The model to import from
|
|
116
118
|
# @param register_id [Symbol] The register context (must be non-default)
|
|
117
119
|
def register_only_import_model_attributes(model, register_id)
|
|
120
|
+
lutaml_cow_fork!(:registers)
|
|
121
|
+
lutaml_cow_fork!(:choices)
|
|
118
122
|
model.attributes(register_id).each_value do |attr|
|
|
119
123
|
define_attribute_methods(attr, register_id)
|
|
120
124
|
end
|
|
@@ -166,6 +170,7 @@ module Lutaml
|
|
|
166
170
|
# resolution attempt ran — deferred_imports_pending? compares
|
|
167
171
|
# against it to skip re-resolution on stable sets.
|
|
168
172
|
def record_import_attempt(register_id)
|
|
173
|
+
lutaml_cow_fork!(:registers)
|
|
169
174
|
# Raw stores only — the lazy attributes() re-enters
|
|
170
175
|
# ensure_imports! and, when @models_imported was just set to
|
|
171
176
|
# false (unresolved imports), cycles through this very
|
|
@@ -199,6 +204,7 @@ module Lutaml
|
|
|
199
204
|
|
|
200
205
|
def ensure_model_imports!(register_id = nil)
|
|
201
206
|
register_id ||= Lutaml::Model::Config.default_register
|
|
207
|
+
lutaml_cow_fork!(:registers)
|
|
202
208
|
@models_imported = {} if @models_imported.nil? || @models_imported == false
|
|
203
209
|
return if @models_imported[register_id] || Utils.present?(@register_records[register_id][:attributes])
|
|
204
210
|
|
|
@@ -236,6 +242,7 @@ module Lutaml
|
|
|
236
242
|
# @param register_id [Symbol, nil] The register context
|
|
237
243
|
def ensure_choice_imports!(register_id = nil)
|
|
238
244
|
register_id ||= Lutaml::Model::Config.default_register
|
|
245
|
+
lutaml_cow_fork!(:registers)
|
|
239
246
|
@choices_imported = {} if @choices_imported.nil? || @choices_imported == false
|
|
240
247
|
return if @choices_imported[register_id] || Utils.present?(@register_records[register_id][:choice_attributes])
|
|
241
248
|
|
data/lib/lutaml/model/version.rb
CHANGED
|
@@ -82,7 +82,7 @@ module Lutaml
|
|
|
82
82
|
# interpretive set_schema_location contract).
|
|
83
83
|
if parent.nil? && node
|
|
84
84
|
record_declaration_plan(instance, node, model_class) if declaration_plan_mode != :skip
|
|
85
|
-
record_schema_location(instance, node)
|
|
85
|
+
record_schema_location(instance, node, model_class, register)
|
|
86
86
|
end
|
|
87
87
|
children.each do |child|
|
|
88
88
|
child.lutaml_parent = instance
|
|
@@ -108,7 +108,10 @@ module Lutaml
|
|
|
108
108
|
|
|
109
109
|
# The interpretive parse stores the root's xsi:schemaLocation
|
|
110
110
|
# verbatim (set_schema_location); the writer re-emits it.
|
|
111
|
-
def record_schema_location(instance, node)
|
|
111
|
+
def record_schema_location(instance, node, model_class, register)
|
|
112
|
+
xml_mapping = model_class.mappings_for(:xml, register)
|
|
113
|
+
return if xml_mapping&.schema_location_mapped?(register)
|
|
114
|
+
|
|
112
115
|
schema_loc = node.attributes.each_value.find do |a|
|
|
113
116
|
a.name.to_s.split(":", 2).last == "schemaLocation"
|
|
114
117
|
end
|
data/lib/lutaml/xml/mapping.rb
CHANGED
|
@@ -1455,6 +1455,18 @@ module Lutaml
|
|
|
1455
1455
|
end
|
|
1456
1456
|
end
|
|
1457
1457
|
|
|
1458
|
+
# Whether the model maps the engine-reserved schemaLocation name
|
|
1459
|
+
# itself (the XSD schema objects map it as a plain XSD attribute).
|
|
1460
|
+
# Raw xsi:schemaLocation capture must stay out of the way, or the
|
|
1461
|
+
# writer re-emits the value beside the mapped attribute (#1042).
|
|
1462
|
+
def schema_location_mapped?(register_id = nil)
|
|
1463
|
+
mapping_attributes_hash(register_id).each_value.any? do |rule_or_array|
|
|
1464
|
+
Array(rule_or_array).any? do |rule|
|
|
1465
|
+
rule.name.to_s == "schemaLocation"
|
|
1466
|
+
end
|
|
1467
|
+
end
|
|
1468
|
+
end
|
|
1469
|
+
|
|
1458
1470
|
# Raw hash access for inheritance and deep_dup operations.
|
|
1459
1471
|
# Callers may mutate the returned hash (e.g., setting keys).
|
|
1460
1472
|
def elements_hash
|
|
@@ -301,7 +301,7 @@ module Lutaml
|
|
|
301
301
|
set_instance_ordering(instance, doc, ordered_option,
|
|
302
302
|
mixed_content_option, xml_mapping,
|
|
303
303
|
instance_is_serialize)
|
|
304
|
-
set_schema_location(instance, doc)
|
|
304
|
+
set_schema_location(instance, doc, xml_mapping, effective_register)
|
|
305
305
|
|
|
306
306
|
defaults_used = []
|
|
307
307
|
|
|
@@ -585,7 +585,9 @@ instance_is_serialize = nil)
|
|
|
585
585
|
end
|
|
586
586
|
end
|
|
587
587
|
|
|
588
|
-
def set_schema_location(instance, doc)
|
|
588
|
+
def set_schema_location(instance, doc, xml_mapping, register)
|
|
589
|
+
return if xml_mapping&.schema_location_mapped?(register)
|
|
590
|
+
|
|
589
591
|
schema_location = doc.attributes.values.find do |a|
|
|
590
592
|
a.unprefixed_name == "schemaLocation"
|
|
591
593
|
end
|
|
@@ -68,7 +68,7 @@ module Lutaml
|
|
|
68
68
|
if instance_rule
|
|
69
69
|
attr_def = collection.class.attributes[instance_rule.to]
|
|
70
70
|
if attr_def
|
|
71
|
-
instance_type = attr_def.type(
|
|
71
|
+
instance_type = attr_def.type(effective_register_for(collection.class))
|
|
72
72
|
if instance_type.is_a?(Class) &&
|
|
73
73
|
instance_type < Lutaml::Model::Serialize
|
|
74
74
|
instance_mapping = instance_type.mappings_for(:xml)
|
|
@@ -241,6 +241,17 @@ module Lutaml
|
|
|
241
241
|
end
|
|
242
242
|
end
|
|
243
243
|
|
|
244
|
+
# The register a model class's own attribute types resolve in: the
|
|
245
|
+
# class's lutaml_default_register takes precedence over the ambient
|
|
246
|
+
# register, unless the ambient register is a derived context carrying
|
|
247
|
+
# substitutions (Register.resolve_for_child encodes that contract).
|
|
248
|
+
def effective_register_for(mapper_class)
|
|
249
|
+
return @register unless mapper_class.is_a?(Class)
|
|
250
|
+
|
|
251
|
+
(@register_for ||= {})[mapper_class] ||=
|
|
252
|
+
Lutaml::Model::Register.resolve_for_child(mapper_class, @register)
|
|
253
|
+
end
|
|
254
|
+
|
|
244
255
|
# Internal collection method
|
|
245
256
|
#
|
|
246
257
|
# @param element [Object] the model instance (can be nil for type analysis)
|
|
@@ -277,6 +288,13 @@ module Lutaml
|
|
|
277
288
|
{}
|
|
278
289
|
end
|
|
279
290
|
|
|
291
|
+
# Attribute types belong to the model declaring them: a nested
|
|
292
|
+
# model with its own lutaml_default_register (an mml element
|
|
293
|
+
# inside an STS document) declares symbolic types only that
|
|
294
|
+
# register knows. Resolving them in the ambient register raises
|
|
295
|
+
# UnknownTypeError while collecting namespaces.
|
|
296
|
+
attribute_register = effective_register_for(mapper_class)
|
|
297
|
+
|
|
280
298
|
# ==================================================================
|
|
281
299
|
# PHASE 1: OWN NAMESPACE COLLECTION (for non-type-only models)
|
|
282
300
|
# ==================================================================
|
|
@@ -370,7 +388,7 @@ module Lutaml
|
|
|
370
388
|
attr_def = attributes[elem_rule.to]
|
|
371
389
|
next unless attr_def
|
|
372
390
|
|
|
373
|
-
child_type = attr_def.type(
|
|
391
|
+
child_type = attr_def.type(attribute_register)
|
|
374
392
|
next unless child_type
|
|
375
393
|
|
|
376
394
|
# Get child instance BEFORE adding Type ref
|
|
@@ -579,16 +597,9 @@ module Lutaml
|
|
|
579
597
|
# Get child's mapper_class and mapping
|
|
580
598
|
attr_def = attributes[matching_rule.to]
|
|
581
599
|
if attr_def
|
|
582
|
-
# Resolve type using
|
|
600
|
+
# Resolve type using the owning model's register context
|
|
583
601
|
mapper_class = options[:mapper_class]
|
|
584
|
-
child_register =
|
|
585
|
-
(@register_for ||= {})[mapper_class] ||=
|
|
586
|
-
Lutaml::Model::Register.resolve_for_child(
|
|
587
|
-
mapper_class, @register
|
|
588
|
-
)
|
|
589
|
-
else
|
|
590
|
-
@register
|
|
591
|
-
end
|
|
602
|
+
child_register = effective_register_for(mapper_class)
|
|
592
603
|
child_type = attr_def.type(child_register)
|
|
593
604
|
if child_type.is_a?(Class) && child_type < Lutaml::Model::Serialize
|
|
594
605
|
child_mapping = child_type.mappings_for(:xml)
|
|
@@ -19,7 +19,7 @@ module Lutaml
|
|
|
19
19
|
namespace Lutaml::Xml::Schema::XsdNamespace
|
|
20
20
|
|
|
21
21
|
map_attribute :id, to: :id
|
|
22
|
-
map_attribute
|
|
22
|
+
map_attribute :schemaLocation, to: :schema_path
|
|
23
23
|
map_element :group, to: :group
|
|
24
24
|
map_element :annotation, to: :annotation
|
|
25
25
|
map_element :simpleType, to: :simple_type
|
|
@@ -107,6 +107,7 @@ module Lutaml
|
|
|
107
107
|
# @param block [Proc] The DSL block
|
|
108
108
|
def process_mapping(format, *args, &block)
|
|
109
109
|
if format == :xml && args.any? && args.first.is_a?(Class) && args.first < Lutaml::Xml::Mapping
|
|
110
|
+
lutaml_cow_fork!(:mappings)
|
|
110
111
|
process_xml_mapping_class_inheritance(args.first, &block)
|
|
111
112
|
else
|
|
112
113
|
super
|
|
@@ -23,7 +23,7 @@ module Lutaml
|
|
|
23
23
|
#
|
|
24
24
|
# @param register [Symbol, nil] The register context
|
|
25
25
|
def ensure_format_mapping_imports!(register = nil)
|
|
26
|
-
|
|
26
|
+
lutaml_cow_mapping_for_dsl!(:xml)&.ensure_mappings_imported!(register)
|
|
27
27
|
end
|
|
28
28
|
|
|
29
29
|
# Check for conflicting sort configurations with XML ordered mapping
|
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../../spec_helper"
|
|
4
|
+
|
|
5
|
+
# Pin the inheritance-state isolation contract between a model class, its
|
|
6
|
+
# superclass and its siblings. These are the semantics the eager copy in
|
|
7
|
+
# `initialize_attrs` has always provided; they must hold for any
|
|
8
|
+
# copy-on-write replacement of that copy.
|
|
9
|
+
RSpec.describe "inherited model state isolation" do
|
|
10
|
+
let(:parent) do
|
|
11
|
+
klass = Class.new(Lutaml::Model::Serializable) do
|
|
12
|
+
attribute :name, :string
|
|
13
|
+
attribute :size, :integer
|
|
14
|
+
|
|
15
|
+
xml do
|
|
16
|
+
element "parent"
|
|
17
|
+
map_attribute "name", to: :name
|
|
18
|
+
map_element "size", to: :size
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
stub_const("IsoParent", klass)
|
|
22
|
+
klass
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
it "gives the child the parent's attributes and mappings immediately" do
|
|
26
|
+
child = Class.new(parent)
|
|
27
|
+
stub_const("IsoChildSeen", child)
|
|
28
|
+
|
|
29
|
+
expect(IsoChildSeen.attributes.key?(:name)).to be(true)
|
|
30
|
+
expect(IsoChildSeen.attributes.key?(:size)).to be(true)
|
|
31
|
+
expect(IsoChildSeen.mappings[:xml].elements_hash).not_to be_empty
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
it "hides a child's attribute declaration from the parent" do
|
|
35
|
+
child = Class.new(parent) do
|
|
36
|
+
attribute :extra, :string
|
|
37
|
+
end
|
|
38
|
+
stub_const("IsoChildExtra", child)
|
|
39
|
+
|
|
40
|
+
expect(IsoChildExtra.attributes.key?(:extra)).to be(true)
|
|
41
|
+
expect(parent.attributes.key?(:extra)).to be(false)
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
it "hides a child's mapping declaration from the parent" do
|
|
45
|
+
child = Class.new(parent) do
|
|
46
|
+
attribute :extra, :string
|
|
47
|
+
xml do
|
|
48
|
+
map_element "extra", to: :extra
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
stub_const("IsoChildMapping", child)
|
|
52
|
+
|
|
53
|
+
model = child.new(name: "x", extra: "e")
|
|
54
|
+
expect(model.to_xml).to include("<extra>e</extra>")
|
|
55
|
+
expect(parent.mappings[:xml].elements_hash.key?("extra")).to be(false)
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
it "hides a parent attribute declared after subclassing from the child" do
|
|
59
|
+
child = Class.new(parent)
|
|
60
|
+
stub_const("IsoChildLateParent", child)
|
|
61
|
+
|
|
62
|
+
parent.attribute :late, :string
|
|
63
|
+
|
|
64
|
+
expect(parent.attributes.key?(:late)).to be(true)
|
|
65
|
+
expect(child.attributes.key?(:late)).to be(false)
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
it "hides a parent mapping declared after subclassing from the child" do
|
|
69
|
+
child = Class.new(parent) do
|
|
70
|
+
attribute :late_element, :string
|
|
71
|
+
end
|
|
72
|
+
stub_const("IsoChildLateMapping", child)
|
|
73
|
+
|
|
74
|
+
parent.xml do
|
|
75
|
+
map_element "late_element", to: :late_element
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
expect(parent.mappings[:xml].elements_hash.key?("late_element")).to be(true)
|
|
79
|
+
expect(child.mappings[:xml].elements_hash.key?("late_element")).to be(false)
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
it "isolates restrict between parent, child and sibling" do
|
|
83
|
+
child_a = Class.new(parent)
|
|
84
|
+
stub_const("IsoRestrictA", child_a)
|
|
85
|
+
child_b = Class.new(parent)
|
|
86
|
+
stub_const("IsoRestrictB", child_b)
|
|
87
|
+
|
|
88
|
+
child_a.restrict :name, default: "a-default"
|
|
89
|
+
parent.restrict :size, default: 42
|
|
90
|
+
|
|
91
|
+
expect(parent.attributes[:name].default).to eq(IsoRestrictB.attributes[:name].default)
|
|
92
|
+
expect(IsoRestrictA.attributes[:name].default).to eq("a-default")
|
|
93
|
+
expect(IsoRestrictB.attributes[:size].default).not_to eq(42)
|
|
94
|
+
expect(IsoRestrictA.attributes[:size].default).not_to eq(42)
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
it "isolates choice declarations from the parent" do
|
|
98
|
+
child = Class.new(parent) do
|
|
99
|
+
choice(min: 0, max: 1) do
|
|
100
|
+
attribute :a, :string
|
|
101
|
+
attribute :b, :string
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
stub_const("IsoChoiceChild", child)
|
|
105
|
+
|
|
106
|
+
expect(child.choice_attributes.size).to eq(1)
|
|
107
|
+
expect(parent.choice_attributes).to be_empty
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
it "isolates a register-only import from the parent" do
|
|
111
|
+
source = Class.new(Lutaml::Model::Serializable) do
|
|
112
|
+
attribute :imported, :string
|
|
113
|
+
end
|
|
114
|
+
stub_const("IsoImportSource", source)
|
|
115
|
+
|
|
116
|
+
child = Class.new(parent) do
|
|
117
|
+
xml do
|
|
118
|
+
element "child"
|
|
119
|
+
map_attribute "name", to: :name
|
|
120
|
+
end
|
|
121
|
+
import_model IsoImportSource, :custom_reg
|
|
122
|
+
end
|
|
123
|
+
stub_const("IsoImportChild", child)
|
|
124
|
+
|
|
125
|
+
expect(child.register_record(:custom_reg)[:attributes].key?(:imported))
|
|
126
|
+
.to be(true)
|
|
127
|
+
expect(parent.register_record(:custom_reg)[:attributes]).to be_empty
|
|
128
|
+
expect(source.attributes.key?(:imported)).to be(true)
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
it "keeps a default-register import out of the parent" do
|
|
132
|
+
source = Class.new(Lutaml::Model::Serializable) do
|
|
133
|
+
attribute :def_imported, :string
|
|
134
|
+
end
|
|
135
|
+
stub_const("IsoImportDefaultSource", source)
|
|
136
|
+
|
|
137
|
+
child = Class.new(parent) do
|
|
138
|
+
import_model IsoImportDefaultSource
|
|
139
|
+
end
|
|
140
|
+
stub_const("IsoImportDefaultChild", child)
|
|
141
|
+
|
|
142
|
+
expect(child.attributes.key?(:def_imported)).to be(true)
|
|
143
|
+
expect(parent.attributes.key?(:def_imported)).to be(false)
|
|
144
|
+
expect(source.attributes.key?(:def_imported)).to be(true)
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
it "serializes a trio byte-identically to independently declared models" do
|
|
148
|
+
child = Class.new(parent) do
|
|
149
|
+
attribute :note, :string
|
|
150
|
+
xml do
|
|
151
|
+
map_element "note", to: :note
|
|
152
|
+
end
|
|
153
|
+
end
|
|
154
|
+
stub_const("IsoTrioChild", child)
|
|
155
|
+
sibling = Class.new(parent) do
|
|
156
|
+
attribute :flag, :string
|
|
157
|
+
xml do
|
|
158
|
+
map_element "flag", to: :flag
|
|
159
|
+
end
|
|
160
|
+
end
|
|
161
|
+
stub_const("IsoTrioSibling", sibling)
|
|
162
|
+
|
|
163
|
+
inherited = child.from_xml("<parent name=\"n\"><size>1</size><note>x</note></parent>")
|
|
164
|
+
standalone = Class.new(Lutaml::Model::Serializable) do
|
|
165
|
+
attribute :name, :string
|
|
166
|
+
attribute :size, :integer
|
|
167
|
+
attribute :note, :string
|
|
168
|
+
xml do
|
|
169
|
+
element "parent"
|
|
170
|
+
map_attribute "name", to: :name
|
|
171
|
+
map_element "size", to: :size
|
|
172
|
+
map_element "note", to: :note
|
|
173
|
+
end
|
|
174
|
+
end
|
|
175
|
+
stub_const("IsoTrioStandalone", standalone)
|
|
176
|
+
|
|
177
|
+
expect(inherited.to_xml).to include("<size>1</size>")
|
|
178
|
+
expect(inherited.to_xml).to include("<note>x</note>")
|
|
179
|
+
expect(standalone.from_xml(inherited.to_xml).to_xml).to eq(inherited.to_xml)
|
|
180
|
+
expect(sibling.from_xml("<parent name=\"s\"><flag>1</flag></parent>").to_xml)
|
|
181
|
+
.to include("<flag>1</flag>")
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
it "keeps grandparent chains isolated in both directions" do
|
|
185
|
+
mid = Class.new(parent) do
|
|
186
|
+
attribute :mid_attr, :string
|
|
187
|
+
end
|
|
188
|
+
stub_const("IsoMid", mid)
|
|
189
|
+
leaf = Class.new(mid)
|
|
190
|
+
stub_const("IsoLeaf", leaf)
|
|
191
|
+
|
|
192
|
+
parent.attribute :gp_late, :string
|
|
193
|
+
mid.attribute :mid_late, :string
|
|
194
|
+
|
|
195
|
+
expect(leaf.attributes.key?(:name)).to be(true)
|
|
196
|
+
expect(leaf.attributes.key?(:mid_attr)).to be(true)
|
|
197
|
+
expect(leaf.attributes.key?(:gp_late)).to be(false)
|
|
198
|
+
expect(leaf.attributes.key?(:mid_late)).to be(false)
|
|
199
|
+
expect(mid.attributes.key?(:gp_late)).to be(false)
|
|
200
|
+
expect(parent.attributes.key?(:mid_attr)).to be(false)
|
|
201
|
+
end
|
|
202
|
+
end
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "spec_helper"
|
|
4
|
+
|
|
5
|
+
# A nested model may declare its own `lutaml_default_register` (mml does:
|
|
6
|
+
# every MathML element class resolves its symbolic child types in the
|
|
7
|
+
# per-version MML context). Namespace collection walks the child's XML
|
|
8
|
+
# mapping; it must resolve the child's *own* attribute types in the
|
|
9
|
+
# child's register, not in the ambient (parent) register.
|
|
10
|
+
RSpec.describe "NamespaceCollector nested child model registers" do
|
|
11
|
+
before do
|
|
12
|
+
ctx = Lutaml::Model::GlobalContext.create_context(
|
|
13
|
+
id: :nscollectorchild, registry: Lutaml::Model::TypeRegistry.new,
|
|
14
|
+
)
|
|
15
|
+
ctx.registry.register(:part, widget_part_class)
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
let(:widget_part_class) do
|
|
19
|
+
Class.new(Lutaml::Model::Serializable) do
|
|
20
|
+
xml do
|
|
21
|
+
element "part"
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# The symbolic attribute type is declared on a base class; the concrete
|
|
27
|
+
# model is a subclass that carries the register hook. This mirrors
|
|
28
|
+
# Mml::V3::Math < Mml::CommonElements.
|
|
29
|
+
let(:widget_class) do
|
|
30
|
+
Class.new(widget_base_class) do
|
|
31
|
+
def self.lutaml_default_register
|
|
32
|
+
:nscollectorchild
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
let(:widget_base_class) do
|
|
38
|
+
Class.new(Lutaml::Model::Serializable) do
|
|
39
|
+
attribute :part_value, :part, collection: true
|
|
40
|
+
|
|
41
|
+
xml do
|
|
42
|
+
element "widget"
|
|
43
|
+
map_element "part", to: :part_value
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
let(:host_class) do
|
|
49
|
+
widget = widget_class
|
|
50
|
+
Class.new(Lutaml::Model::Serializable) do
|
|
51
|
+
attribute :widget, widget
|
|
52
|
+
|
|
53
|
+
xml do
|
|
54
|
+
element "host"
|
|
55
|
+
map_element "widget", to: :widget
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
let(:instance) do
|
|
61
|
+
host_class.from_xml("<host><widget><part/></widget></host>")
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
it "resolves the child's symbolic attribute types in the child's register" do
|
|
65
|
+
collector = Lutaml::Xml::NamespaceCollector.new(:default)
|
|
66
|
+
|
|
67
|
+
expect do
|
|
68
|
+
collector.collect(instance, host_class.mappings_for(:xml))
|
|
69
|
+
end.not_to raise_error
|
|
70
|
+
end
|
|
71
|
+
end
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../../../../spec_helper"
|
|
4
|
+
require "open3"
|
|
5
|
+
require "lutaml/xml/schema/xsd"
|
|
6
|
+
|
|
7
|
+
# #1042: the XSD schema-object classes map the plain `schemaLocation`
|
|
8
|
+
# attribute — an XSD attribute on xs:redefine, not xsi:schemaLocation.
|
|
9
|
+
# The class used to warn about its own mapping ("handled by default"),
|
|
10
|
+
# and the engine's raw xsi capture then re-emitted the value as
|
|
11
|
+
# xsi:schemaLocation beside the mapped attribute.
|
|
12
|
+
RSpec.describe "Xsd::Redefine schemaLocation" do
|
|
13
|
+
let(:xsd) do
|
|
14
|
+
<<~XSD
|
|
15
|
+
<xsd:schema xmlns:xsd="http://www.w3.org/2001/XMLSchema">
|
|
16
|
+
<xsd:redefine schemaLocation="base.xsd">
|
|
17
|
+
<xsd:group name="g">
|
|
18
|
+
<xsd:choice><xsd:element name="e1"/></xsd:choice>
|
|
19
|
+
</xsd:group>
|
|
20
|
+
</xsd:redefine>
|
|
21
|
+
</xsd:schema>
|
|
22
|
+
XSD
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
it "reads schemaLocation into schema_path" do
|
|
26
|
+
doc = Lutaml::Xml::Schema::Xsd.parse(xsd)
|
|
27
|
+
|
|
28
|
+
expect(doc.redefine.first.schema_path).to eq("base.xsd")
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
it "round-trips it as the plain attribute without an xsi copy" do
|
|
32
|
+
xml = Lutaml::Xml::Schema::Xsd.parse(xsd).to_xml
|
|
33
|
+
|
|
34
|
+
expect(xml).to include('schemaLocation="base.xsd"')
|
|
35
|
+
expect(xml).not_to include("xsi:schemaLocation")
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
it "does not warn while defining its own mapping" do
|
|
39
|
+
probe = 'require "lutaml/model"; require "lutaml/xml/schema/xsd"; ' \
|
|
40
|
+
"Lutaml::Xml::Schema::Xsd::Redefine"
|
|
41
|
+
|
|
42
|
+
_out, err, status = Open3.capture3(RbConfig.ruby, "-I", "lib", "-e", probe)
|
|
43
|
+
|
|
44
|
+
expect(status).to be_success, err
|
|
45
|
+
expect(err).not_to include("handled by default")
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
it "still captures xsi:schemaLocation on models that do not map it" do
|
|
49
|
+
model = Class.new(Lutaml::Model::Serializable) do
|
|
50
|
+
attribute :name, :string
|
|
51
|
+
xml do
|
|
52
|
+
element "r"
|
|
53
|
+
map_attribute "name", to: :name
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
stub_const("AutoSchemaLocationModel", model)
|
|
57
|
+
|
|
58
|
+
parsed = AutoSchemaLocationModel.from_xml(
|
|
59
|
+
'<r xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" ' \
|
|
60
|
+
'name="n" xsi:schemaLocation="urn:a a.xsd"/>',
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
expect(parsed.raw_schema_location).to eq("urn:a a.xsd")
|
|
64
|
+
end
|
|
65
|
+
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.142
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Ribose Inc.
|
|
@@ -1782,6 +1782,7 @@ files:
|
|
|
1782
1782
|
- spec/lutaml/model/import_registry_spec.rb
|
|
1783
1783
|
- spec/lutaml/model/included_spec.rb
|
|
1784
1784
|
- spec/lutaml/model/inheritance_spec.rb
|
|
1785
|
+
- spec/lutaml/model/inherited_state_isolation_spec.rb
|
|
1785
1786
|
- spec/lutaml/model/json_adapter_spec.rb
|
|
1786
1787
|
- spec/lutaml/model/json_spec.rb
|
|
1787
1788
|
- spec/lutaml/model/json_state_nesting_spec.rb
|
|
@@ -2001,6 +2002,7 @@ files:
|
|
|
2001
2002
|
- spec/lutaml/xml/namespace_aware_parsing_spec.rb
|
|
2002
2003
|
- spec/lutaml/xml/namespace_backward_compatibility_spec.rb
|
|
2003
2004
|
- spec/lutaml/xml/namespace_bound_element_roundtrip_spec.rb
|
|
2005
|
+
- spec/lutaml/xml/namespace_collector_child_register_spec.rb
|
|
2004
2006
|
- spec/lutaml/xml/namespace_constant_spec.rb
|
|
2005
2007
|
- spec/lutaml/xml/namespace_force_prefix_spec.rb
|
|
2006
2008
|
- spec/lutaml/xml/namespace_format_preservation_spec.rb
|
|
@@ -2060,6 +2062,7 @@ files:
|
|
|
2060
2062
|
- spec/lutaml/xml/schema/xsd/list_simple_type_spec.rb
|
|
2061
2063
|
- spec/lutaml/xml/schema/xsd/mapping_integrity_spec.rb
|
|
2062
2064
|
- spec/lutaml/xml/schema/xsd/namespace_uri_remapping_spec.rb
|
|
2065
|
+
- spec/lutaml/xml/schema/xsd/redefine_schema_location_spec.rb
|
|
2063
2066
|
- spec/lutaml/xml/schema/xsd/schema_helper_methods_spec.rb
|
|
2064
2067
|
- spec/lutaml/xml/schema/xsd/schema_mapping_spec.rb
|
|
2065
2068
|
- spec/lutaml/xml/schema/xsd/schema_validator_spec.rb
|