lutaml-model 0.8.84 → 0.8.86

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.
@@ -0,0 +1,273 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Lutaml
4
+ module Xml
5
+ module Leptris
6
+ # Serialize-side mirror of the plan fast path: builds the output
7
+ # tree directly through leptris's fused create_child chain — no
8
+ # XmlElement tree, no DeclarationPlanner pass, no builder context
9
+ # stack, no moxml wrapper minting. Serialized shapes must be the
10
+ # plain ones (attributes, scalars, collections, nested models,
11
+ # raw fragments, content runs); custom-method, polymorphic,
12
+ # union, and multi-spelling rows keep the interpretive serializer
13
+ # (their output flows through hash-shaped custom APIs).
14
+ #
15
+ # Ordered/mixed models serialize from element_order when the
16
+ # instance carries one (parsed models): text runs, comments, and
17
+ # element entries in document order, mirroring the interpretive
18
+ # order applier — including its content-attr-first text reads
19
+ # (mutations to the model show up) and CDATA flattening. Models
20
+ # without element_order fall back interpretively (return nil).
21
+ module PlanSerializer
22
+ SERIALIZABLE_KINDS = %i[scalar collection_native collection_cb
23
+ nested ordered_deferred raw content
24
+ content_deferred].freeze
25
+ CONTENT_KINDS = %i[content content_deferred].freeze
26
+
27
+ class << self
28
+ # plan: the compiler's entry for instance's class
29
+ def call(instance, plan, register = nil)
30
+ # The child model's declared lutaml_default_register takes
31
+ # precedence over the ambient register — the resolve_for_child
32
+ # contract (#876); without it, child plans and attribute
33
+ # serialization resolve in the parent context.
34
+ register = Lutaml::Model::Register.resolve_for_child(
35
+ instance.class,
36
+ register || Lutaml::Model::Config.default_register,
37
+ )
38
+ return nil if plan[:ordered] && instance.element_order.nil?
39
+
40
+ doc = ::Leptris::XML::Document.create
41
+ root = doc.create_element(plan[:tree][:name])
42
+ doc.root = root
43
+ if plan[:ordered]
44
+ build_ordered(root, instance, plan, doc, register)
45
+ else
46
+ build(root, instance, plan, doc, register)
47
+ end
48
+ doc.to_xml(indent: 2, no_decl: true)
49
+ end
50
+
51
+ # Whether the compiled plan is serialize-shaped. Ordered/mixed
52
+ # models serialize through build_ordered from element_order;
53
+ # plain models through plan rows.
54
+ def serializable?(plan)
55
+ plan[:rows].all? do |_rule, _attr, kind, _sp, _del|
56
+ SERIALIZABLE_KINDS.include?(kind)
57
+ end
58
+ end
59
+
60
+ private
61
+
62
+ def register
63
+ Lutaml::Model::Config.default_register
64
+ end
65
+
66
+ def build(element, instance, plan, doc, reg = nil)
67
+ reg ||= register
68
+ write_attributes(element, instance, plan, reg)
69
+ plan[:rows].each do |rule, attr, kind, spelling, delegate|
70
+ value = value_of(instance, attr, delegate)
71
+ next unless rule.render?(value, instance)
72
+
73
+ case kind
74
+ when :scalar
75
+ # Multi-capture parity: an attribute that collected
76
+ # several occurrences serializes one element per item,
77
+ # as the interpretive writer does. A plain scalar skips
78
+ # the Array() wrap — the common case.
79
+ if value.is_a?(::Array)
80
+ value.each do |item|
81
+ add_leaf(element, row_name(rule, spelling),
82
+ attr.serialize(item, :xml, reg), doc,
83
+ attrs: rule.when_attribute)
84
+ end
85
+ else
86
+ add_leaf(element, row_name(rule, spelling),
87
+ attr.serialize(value, :xml, reg), doc,
88
+ attrs: rule.when_attribute)
89
+ end
90
+ when :collection_native, :collection_cb
91
+ Array(value).each do |item|
92
+ add_leaf(element, rule.name.to_s,
93
+ attr.serialize(item, :xml, reg), doc,
94
+ attrs: rule.when_attribute)
95
+ end
96
+ when :nested, :ordered_deferred
97
+ Array(value).each do |item|
98
+ child_plan = PlanCompiler.compile(item.class, reg)
99
+ child = element.create_child(child_plan[:tree][:name])
100
+ if child_plan[:ordered]
101
+ return nil unless build_ordered(child, item, child_plan,
102
+ doc, reg)
103
+
104
+ else
105
+ build(child, item, child_plan, doc, reg)
106
+ end
107
+ end
108
+ when :raw
109
+ Array(value).each { |raw| element.add_child(raw.to_s) }
110
+ when :content, :content_deferred
111
+ Array(value).each { |run| element.add_child(doc.create_text_node(run.to_s)) }
112
+ end
113
+ end
114
+ end
115
+
116
+ # Element_order-driven emission (the interpretive order
117
+ # applier's contract): text runs and comments verbatim, each
118
+ # element entry consuming the next item of its collection
119
+ # (document order). Text reads prefer the content attribute
120
+ # when it lines up with the text-node count, so mutations to
121
+ # the model are reflected; element_order text is the fallback.
122
+ # PIs drop (interpretive parity). Returns nil when a nested
123
+ # ordered child lacks element_order (caller falls back).
124
+ def build_ordered(element, instance, plan, doc, reg = nil)
125
+ reg ||= register
126
+ write_attributes(element, instance, plan, reg)
127
+ rows_by_name = {}
128
+ plan[:rows].each do |rule, attr, kind, spelling, delegate|
129
+ next unless rule.name
130
+
131
+ rows_by_name[rule.name.to_s] ||= [rule, attr, kind, spelling,
132
+ delegate]
133
+ end
134
+ content_entry = plan[:rows].find do |rule, _attr, kind, _sp, _del|
135
+ CONTENT_KINDS.include?(kind) && rule.name.nil?
136
+ end
137
+ order = instance.element_order
138
+ element_indices = ::Hash.new(0)
139
+ content_attr = content_entry && content_entry[1]
140
+ content_value = content_attr &&
141
+ value_of(instance, content_attr, content_entry[4])
142
+ text_node_count = order.count { |o| o.type == "Text" }
143
+ use_content_index = content_value.is_a?(Array) &&
144
+ content_value.length == text_node_count
145
+ content_cdata = content_entry && content_entry[0].cdata
146
+ text_node_index = 0
147
+
148
+ order.each do |object|
149
+ case object.type
150
+ when "Text"
151
+ text = if content_attr && !content_value.nil?
152
+ if use_content_index
153
+ content_value[text_node_index].to_s
154
+ elsif !content_value.is_a?(Array) && text_node_count <= 1
155
+ content_value.to_s
156
+ else
157
+ object.text_content || object.name
158
+ end
159
+ else
160
+ object.text_content || object.name
161
+ end
162
+ text_node_index += 1
163
+ next if text.nil?
164
+ # Mixed content keeps whitespace; ordered-only skips it
165
+ next if content_attr.nil? && text.strip.empty?
166
+
167
+ element.add_child(content_cdata ? doc.create_cdata(text) : doc.create_text_node(text))
168
+ when "Comment"
169
+ element.add_child(doc.create_comment(object.text_content))
170
+ when "Element"
171
+ entry = rows_by_name[object.name]
172
+ next unless entry
173
+
174
+ _, attr, kind, _spelling, delegate = entry
175
+ value = value_of(instance, attr, delegate)
176
+ case kind
177
+ when :collection_native, :collection_cb
178
+ index = element_indices[object.name]
179
+ items = Array(value)
180
+ next unless index < items.length
181
+
182
+ element_indices[object.name] += 1
183
+ add_leaf(element, object.name,
184
+ attr.serialize(items[index], :xml, reg), doc)
185
+ when :nested, :ordered_deferred
186
+ item = if attr.collection?
187
+ index = element_indices[object.name]
188
+ element_indices[object.name] += 1
189
+ Array(value)[index]
190
+ else
191
+ value
192
+ end
193
+ next unless item
194
+
195
+ child_plan = PlanCompiler.compile(item.class, reg)
196
+ child = element.create_child(child_plan[:tree][:name])
197
+ if child_plan[:ordered]
198
+ return nil unless build_ordered(child, item, child_plan,
199
+ doc, reg)
200
+ else
201
+ build(child, item, child_plan, doc, reg)
202
+ end
203
+ when :raw
204
+ element.add_child(value.to_s) unless value.nil?
205
+ when :scalar
206
+ add_leaf(element, object.name,
207
+ attr.serialize(value, :xml, reg), doc)
208
+ end
209
+ end
210
+ end
211
+ true
212
+ end
213
+
214
+ # Attributes in document order when the instance recorded it
215
+ # (parsed models), else plan row order.
216
+ def write_attributes(element, instance, plan, _reg = nil)
217
+ recorded = instance.attribute_order
218
+ if recorded && !recorded.empty?
219
+ by_name = {}
220
+ plan[:attr_rows].each do |rule, attr, _sp, delegate|
221
+ by_name[rule.name.to_s] = [rule, attr, delegate]
222
+ end
223
+ names = recorded |
224
+ plan[:attr_rows].map { |rule, _a, _s, _d| rule.name.to_s }
225
+ names.each do |name|
226
+ write_attribute(element, instance, name, by_name[name])
227
+ end
228
+ else
229
+ plan[:attr_rows].each do |rule, attr, _sp, delegate|
230
+ write_attribute(element, instance, rule.name.to_s,
231
+ [rule, attr, delegate])
232
+ end
233
+ end
234
+ end
235
+
236
+ def write_attribute(element, instance, name, entry, reg = nil)
237
+ return unless entry
238
+
239
+ _rule, attr, delegate = entry
240
+ value = value_of(instance, attr, delegate)
241
+ return if value.nil?
242
+
243
+ element.set_attribute(name,
244
+ attr.serialize(value, :xml, reg || register).to_s)
245
+ end
246
+
247
+ # Partition rows (#88) re-emit their discriminator: the wire
248
+ # element carries the when_attribute pairs so the round trip
249
+ # re-routes to the same attribute on reparse.
250
+ def add_leaf(element, name, value, doc, attrs: nil)
251
+ child = element.create_child(name)
252
+ attrs&.each { |k, v| child[k.to_s] = v.to_s }
253
+ child.add_child(doc.create_text_node(value.to_s)) unless value.nil?
254
+ child
255
+ end
256
+
257
+ def value_of(instance, attr, delegate)
258
+ if delegate
259
+ target = instance.public_send(delegate)
260
+ target&.public_send(attr.name)
261
+ else
262
+ instance.public_send(attr.name)
263
+ end
264
+ end
265
+
266
+ def row_name(rule, spelling)
267
+ (spelling || rule.name).to_s
268
+ end
269
+ end
270
+ end
271
+ end
272
+ end
273
+ end
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Lutaml
4
+ module Xml
5
+ module Leptris
6
+ # Public consumer surface for the plan fast path: one engine
7
+ # parse + descriptor walk and direct model hydration, bypassing
8
+ # the wrapper-tree construction that dominates large-document
9
+ # consumer paths. The returned object is the mapped model root;
10
+ # collection attributes remain collections on that model.
11
+ module PlanWalk
12
+ class << self
13
+ # Build the plan, walk the document, and hydrate the mapped
14
+ # root. Returns nil when the model is not plan-compilable.
15
+ def call(model_class, xml, _options = {})
16
+ register = Lutaml::Model::Config.default_register
17
+ plan = PlanCompiler.compile(model_class, register)
18
+ return nil unless plan
19
+
20
+ document = ::Leptris::XML.parse(xml)
21
+ root = document.root
22
+ return nil unless root && root.name == plan[:tree][:name]
23
+
24
+ PlanHydrator.call(model_class, plan,
25
+ plan[:descriptor].walk(root),
26
+ node: root)
27
+ end
28
+ end
29
+ end
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Lutaml
4
+ module Xml
5
+ # The leptris plan fast path: model mapping to libleptris descriptor
6
+ # plan, native walk, direct hydration. Everything under this
7
+ # namespace is bound to the leptris gem; nothing above it may
8
+ # reference leptris types directly (the moxml adapters and the
9
+ # interpretive path never enter here).
10
+ module Leptris
11
+ # The plan path requires the one-crossing child snapshot
12
+ # (leptris_plan_value_children_snapshot with child handles,
13
+ # PlanValue#children_snapshot), which shipped in leptris
14
+ # 1.9.273.0. Bundles resolving an older gem keep the
15
+ # interpretive path — PlanCompiler.compile answers nil — the
16
+ # existing opt-out protocol.
17
+ MIN_LEPTRIS_VERSION = "1.9.273.0"
18
+
19
+ def self.plan_path_compatible?
20
+ return false unless defined?(Gem)
21
+
22
+ spec = Gem.loaded_specs["leptris"]
23
+ spec && spec.version >= Gem::Version.new(MIN_LEPTRIS_VERSION)
24
+ end
25
+
26
+ autoload :PlanCompiler, "lutaml/xml/leptris/plan_compiler"
27
+ autoload :PlanHydrator, "lutaml/xml/leptris/plan_hydrator"
28
+ autoload :PlanOrder, "lutaml/xml/leptris/plan_order"
29
+ autoload :PlanSerializer, "lutaml/xml/leptris/plan_serializer"
30
+ autoload :PlanWalk, "lutaml/xml/leptris/plan_walk"
31
+ end
32
+ end
33
+ end
@@ -59,6 +59,7 @@ module Lutaml
59
59
 
60
60
  @elements = {}
61
61
  @attributes = {}
62
+ @any_element_rule = nil
62
63
  @element_sequence = []
63
64
  @content_mapping = nil
64
65
  @raw_mapping = nil
@@ -504,6 +505,26 @@ module Lutaml
504
505
  # @return [String, nil] the type name
505
506
  alias xsd_type type_name
506
507
 
508
+ # Catch-all element mapping: every child element with no
509
+ # explicit rule routes to one collection attribute. For
510
+ # parse-side consumption of dynamic vocabularies (semantic__
511
+ # trees); element_order preserves the original tag names.
512
+ # Serialization of a catch-all attribute emits the attribute's
513
+ # declared element name (not per-item originals).
514
+ def map_any_element(to:, render_nil: false, render_default: false)
515
+ validate!(nil, to, {}, render_nil, false, type: TYPES[:element])
516
+ @any_element_rule = MappingRule.new(
517
+ nil,
518
+ to: to,
519
+ render_nil: render_nil,
520
+ render_default: render_default,
521
+ )
522
+ @element_rules_index = nil if instance_variable_defined?(:@element_rules_index)
523
+ @any_element_rule
524
+ end
525
+
526
+ attr_reader :any_element_rule
527
+
507
528
  def map_instances(to:, polymorphic: {})
508
529
  map_element(to, to: to, polymorphic: polymorphic)
509
530
  end
@@ -1342,9 +1363,9 @@ module Lutaml
1342
1363
  elsif ["text", "#cdata-section"].include?(name.to_s) && type == "Text"
1343
1364
  content_mapping
1344
1365
  else
1345
- candidates = mappings.select do |rule|
1346
- rule.name == name.to_s || rule.name == name.to_sym
1347
- end
1366
+ candidates = element_rules_index[name.to_s]
1367
+ return @any_element_rule if candidates.nil? && @any_element_rule
1368
+ return nil if candidates.nil?
1348
1369
  return candidates.first if namespace_uri.nil? || candidates.one?
1349
1370
 
1350
1371
  candidates.find do |r|
@@ -1353,6 +1374,29 @@ module Lutaml
1353
1374
  end
1354
1375
  end
1355
1376
 
1377
+ # O(1) name -> rules lookup. Parsing consults this once per
1378
+ # child element; the linear scan over every rule made large
1379
+ # documents quadratic (155s on a 7MB vocabulary tree). Keyed on
1380
+ # the stable source hashes (@elements/@attributes mutate only at
1381
+ # DSL time), never on the per-call mappings array.
1382
+ def element_rules_index
1383
+ cache_key = [@elements.object_id, @attributes.object_id]
1384
+ cached = (@element_rules_index ||= {})
1385
+ idx = cached[cache_key]
1386
+ return idx if idx
1387
+
1388
+ idx = {}
1389
+ ([content_mapping, raw_mapping].compact + @elements.values +
1390
+ @attributes.values).flatten.each do |rule|
1391
+ next if rule.is_a?(::Hash)
1392
+
1393
+ (idx[rule.name.to_s] ||= []) << rule
1394
+ end
1395
+ cached[cache_key] = idx
1396
+ idx
1397
+ end
1398
+ private :element_rules_index
1399
+
1356
1400
  def find_by_to(to)
1357
1401
  mappings.detect { |rule| rule.to.to_s == to.to_s }
1358
1402
  end
@@ -263,6 +263,9 @@ module Lutaml
263
263
  # Performance: Resolve xml_mapping once — used for both rules and metadata
264
264
  xml_mapping = mappings_for(:xml, effective_register)
265
265
  mappings = options[:mappings] || xml_mapping.mappings(effective_register)
266
+ # Catch-all rule (map_any_element): iterated like any other,
267
+ # claiming every child no explicit rule matches.
268
+ mappings += [xml_mapping.any_element_rule].compact if options[:mappings].nil?
266
269
  default_namespace = options[:default_namespace]
267
270
  ordered_option = options[:ordered]
268
271
  mixed_content_option = options[:mixed_content]
@@ -361,7 +364,25 @@ module Lutaml
361
364
  options
362
365
  end
363
366
 
364
- value = if rule.raw_mapping?
367
+ value = if !rule.attribute? && rule.name.nil? &&
368
+ xml_mapping.any_element_rule.equal?(rule)
369
+ # map_any_element: every child element no explicit
370
+ # element rule claims, in document order. A name-less
371
+ # MappingRule is otherwise indistinguishable from a
372
+ # content rule (the dispatch consumed it as doc.text);
373
+ # name-derived lookup also cannot express this — it
374
+ # re-finds by name, losing order and double-picking.
375
+ # The pass walks the document children once; the
376
+ # normalize/cast pipeline below handles the rest.
377
+ claimed = Set.new(
378
+ xml_mapping.mappings(effective_register)
379
+ .select { |r| !r.attribute? && r.name }
380
+ .flat_map { |r| [r.name.to_s] },
381
+ )
382
+ doc.element_children.reject do |child|
383
+ claimed.include?(child.unprefixed_name)
384
+ end
385
+ elsif rule.raw_mapping?
365
386
  scoped_raw_inner_xml(doc, xml_mapping)
366
387
  elsif rule.content_mapping?
367
388
  rule.cdata ? doc.cdata : doc.text
@@ -369,6 +390,8 @@ module Lutaml
369
390
  group.size > 1 && attr&.collection?
370
391
  # First rule of a #765 group: match every spelling of
371
392
  # the group so the children merge in document order.
393
+ raise "GROUPED-BODY" if ENV["CATCHDBG"] && rule.name.nil?
394
+
372
395
  if child_names_set &&
373
396
  group.drop(1).none? do |s|
374
397
  child_matches_rule?(s, child_names_set,
@@ -385,9 +408,11 @@ module Lutaml
385
408
  value_for_rule(session, rule, new_opts, attr,
386
409
  extra_names)
387
410
  end
388
- elsif child_names_set && !rule.attribute? &&
411
+ elsif child_names_set && !rule.attribute? && rule.name &&
389
412
  !child_matches_rule?(rule, child_names_set,
390
413
  default_namespace)
414
+ raise "FASTSKIP-BODY" if ENV["CATCHDBG"] && rule.name.nil?
415
+
391
416
  # Pre-match: no child element matches this rule.
392
417
  # Skip expensive value_for_rule, handle defaults inline.
393
418
  # NOTE: do not "read the instance" here (to_value_for) —
@@ -400,6 +425,8 @@ module Lutaml
400
425
  ::Lutaml::Model::UninitializedClass.instance
401
426
  end
402
427
  else
428
+ raise "ELSE-BODY" if ENV["CATCHDBG"] && rule.name.nil?
429
+
403
430
  # Performance: Pass cached attr to avoid recomputing attribute_for_rule
404
431
  val = value_for_rule(session, rule, new_opts, attr)
405
432
 
@@ -24,6 +24,10 @@ module Lutaml
24
24
  @instance_is_serialize ||= instance.is_a?(::Lutaml::Model::Serialize)
25
25
  end
26
26
 
27
+ def mapping
28
+ @mapping ||= instance.class.mappings_for(:xml, register)
29
+ end
30
+
27
31
  # (element, rule name) -> resolved attribute name / matched
28
32
  # attribute VALUE for the lenient lookup paths. Both are pure
29
33
  # functions of the element and the name; ISO-13849-shaped
@@ -63,7 +63,11 @@ module Lutaml
63
63
  #
64
64
  # @return [Array<String>]
65
65
  def xml_schema_paths
66
- inherited = if superclass.respond_to?(:xml_schema_paths)
66
+ # Type check, not a probe: every Serializable carries the
67
+ # class method; a plain (non-Serializable) superclass ends
68
+ # the chain.
69
+ inherited = if superclass.is_a?(Class) &&
70
+ superclass.include?(Lutaml::Model::Serialize)
67
71
  superclass.xml_schema_paths
68
72
  else
69
73
  []
@@ -428,7 +428,7 @@ text_node_count = 0, use_content_index = false)
428
428
  # reconciliation could not place
429
429
  def apply_remaining_rules(model_instance, options, compiled_rules,
430
430
  mapping, processed_text_nodes, fallback_rules)
431
- attr_order = model_instance.respond_to?(:attribute_order) &&
431
+ attr_order = model_instance.is_a?(Lutaml::Model::Serialize) &&
432
432
  model_instance.attribute_order
433
433
 
434
434
  rules_to_apply = if attr_order
@@ -47,7 +47,7 @@ end
47
47
 
48
48
  Lutaml::Model::Config.instance.xml_plan_fast_path = true
49
49
  measure.call("PlanWalk parse + 5000 model.new") do
50
- result = Lutaml::Xml::PlanWalk.call(PlanWalkProbe::BulkRoot, xml)
50
+ result = Lutaml::Xml::Leptris::PlanWalk.call(PlanWalkProbe::BulkRoot, xml)
51
51
  raise "PlanWalk returned nil" unless result&.item&.size == 5000
52
52
  end
53
53
 
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "spec_helper"
4
+
5
+ class AnyElementNode < Lutaml::Model::Serializable
6
+ attribute :id, :string
7
+ attribute :children, "AnyElementNode", collection: true
8
+
9
+ xml do
10
+ element "root"
11
+ map_element "id", to: :id
12
+ map_any_element to: :children
13
+ end
14
+ end
15
+
16
+ RSpec.describe AnyElementNode do
17
+ it "routes unmapped child elements to the catch-all collection" do
18
+ doc = described_class.from_xml(<<~XML)
19
+ <root>
20
+ <id>r-1</id>
21
+ <semantic__p>hello</semantic__p>
22
+ <semantic__title>Nested</semantic__title>
23
+ <semantic__p><semantic__em>deep</semantic__em></semantic__p>
24
+ </root>
25
+ XML
26
+
27
+ expect(doc.id).to eq("r-1")
28
+ expect(doc.children.size).to eq(3)
29
+ end
30
+
31
+ it "keeps explicit rules ahead of the catch-all" do
32
+ doc = described_class.from_xml("<root><id>x</id></root>")
33
+ expect(doc.id).to eq("x")
34
+ expect(doc.children).to be_empty
35
+ end
36
+
37
+ it "nests catch-all children recursively" do
38
+ doc = described_class.from_xml("<root><semantic__p><semantic__em>deep</semantic__em></semantic__p></root>")
39
+ expect(doc.children.first.children.size).to eq(1)
40
+ end
41
+ end
@@ -86,14 +86,18 @@ RSpec.describe Lutaml::Model::Store do
86
86
  end
87
87
 
88
88
  it "does not mix indices across different classes" do
89
- model_class.new(id: "a")
90
- other_class.new(id: "a")
89
+ # Hold strong references: Store keeps WeakRefs, so unreferenced
90
+ # instances can be collected between registration and resolve,
91
+ # making this example fail only under in-suite GC pressure.
92
+ a = model_class.new(id: "a")
93
+ b = other_class.new(id: "a")
91
94
 
92
95
  result = described_class.resolve(model_class, :id, "a")
93
96
  expect(result).to be_a(model_class)
94
97
 
95
98
  result2 = described_class.resolve(other_class, :id, "a")
96
99
  expect(result2).to be_a(other_class)
100
+ expect([a, b]).to all(be_a(Lutaml::Model::Serializable))
97
101
  end
98
102
 
99
103
  it "registering class B does not iterate class A's indices" do
@@ -57,7 +57,7 @@ RSpec.describe "XML plan fast path" do
57
57
  end
58
58
  stub_const("PlanFastPath::SelfRef", node)
59
59
 
60
- expect(Lutaml::Xml::PlanCompiler.compile(node, :default)).to be_nil
60
+ expect(Lutaml::Xml::Leptris::PlanCompiler.compile(node, :default)).to be_nil
61
61
 
62
62
  xml = "<n>#{'<n>t</n>' * 200}</n>"
63
63
  parsed = node.from_xml(xml)
@@ -94,7 +94,7 @@ RSpec.describe "XML plan fast path" do
94
94
 
95
95
  expect(parsed.xmi_type).to eq("uml:Parameter")
96
96
  expect(parsed.classifier_type).to eq("EAnone_void")
97
- expect(Lutaml::Xml::PlanCompiler.compile(parameter, :default)).to be_nil
97
+ expect(Lutaml::Xml::Leptris::PlanCompiler.compile(parameter, :default)).to be_nil
98
98
  end
99
99
 
100
100
  it "is on by default and can be disabled" do
@@ -156,7 +156,7 @@ RSpec.describe "XML plan fast path" do
156
156
  end
157
157
  end
158
158
 
159
- expect(Lutaml::Xml::PlanCompiler.compile(klass,
159
+ expect(Lutaml::Xml::Leptris::PlanCompiler.compile(klass,
160
160
  Lutaml::Model::Config.default_register)).not_to be_nil
161
161
  expect(klass.from_xml("<doc><note>hi</note></doc>").note).to eq("HI")
162
162
  end
@@ -223,14 +223,14 @@ RSpec.describe "XML plan fast path" do
223
223
  end
224
224
  end
225
225
 
226
- expect(Lutaml::Xml::PlanCompiler.compile(klass,
226
+ expect(Lutaml::Xml::Leptris::PlanCompiler.compile(klass,
227
227
  Lutaml::Model::Config.default_register)).not_to be_nil
228
228
  parsed = klass.from_xml("<doc><note>hi</note></doc>")
229
229
  expect(parsed.tgt.note).to eq("hi")
230
230
  end
231
231
 
232
232
  it "routes a single collection row natively (hybrid routing)" do
233
- plan = Lutaml::Xml::PlanCompiler.compile(
233
+ plan = Lutaml::Xml::Leptris::PlanCompiler.compile(
234
234
  Class.new(Lutaml::Model::Serializable) do
235
235
  attribute :xs, :string, collection: true
236
236
  attribute :name, :string
@@ -400,7 +400,7 @@ RSpec.describe "XML plan fast path" do
400
400
  map_element "para", to: :para
401
401
  end
402
402
  end
403
- plan = Lutaml::Xml::PlanCompiler.compile(
403
+ plan = Lutaml::Xml::Leptris::PlanCompiler.compile(
404
404
  doc_class, Lutaml::Model::Config.default_register
405
405
  )
406
406
  expect(plan).not_to be_nil
@@ -623,7 +623,7 @@ RSpec.describe "XML plan fast path" do
623
623
  expect(req.guidance.map(&:text)).to eq(%w[g1 g2])
624
624
  expect(req.purpose.map(&:text)).to eq(["p1"])
625
625
 
626
- expect(Lutaml::Xml::PlanCompiler.compile(partitioned,
626
+ expect(Lutaml::Xml::Leptris::PlanCompiler.compile(partitioned,
627
627
  :default)).to be_nil
628
628
  end
629
629
 
@@ -708,7 +708,7 @@ RSpec.describe "XML plan fast path" do
708
708
  stub_const("PlanFastPath::UpdateFields847", klass)
709
709
 
710
710
  expect(klass.new(value: false).to_xml).to include("http://example.com/ord847")
711
- plan = Lutaml::Xml::PlanCompiler.compile(
711
+ plan = Lutaml::Xml::Leptris::PlanCompiler.compile(
712
712
  klass, Lutaml::Model::Config.default_register
713
713
  )
714
714
  expect(plan[:namespaced]).to be(true)
@@ -37,7 +37,7 @@ RSpec.describe "Plan fast path with when_attribute partitions" do
37
37
  end
38
38
 
39
39
  it "compiles the partitioned model with row tags" do
40
- plan = Lutaml::Xml::PlanCompiler.compile(
40
+ plan = Lutaml::Xml::Leptris::PlanCompiler.compile(
41
41
  model, Lutaml::Model::Config.default_register
42
42
  )
43
43
  expect(plan).not_to be_nil