lutaml-model 0.8.95 → 0.8.96

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: c5933a3971f4f39e30541f504636641cd059e7a97751ba67a710203dbfdcc45a
4
- data.tar.gz: c23b70227e80d99427b9d4125a09e33d9751112bc72a5e6abaca73d6a3897c60
3
+ metadata.gz: 7b4bd8b10dc92c3a53f615bb3e13c907d1cc4976eb547a5a2da41aa2837bef17
4
+ data.tar.gz: e1e5eacb6d80dc8108d65a5264d5e9d872d4259103672f6a88e0d912b437002f
5
5
  SHA512:
6
- metadata.gz: 473b9970246c24172d6040a9c5062fad06d0fea413a5e04956ac98c0990324f2543668436c9259ad2a30b8647555d1e00102f6346afd3018814d68c2841ca8e5
7
- data.tar.gz: f48b568bdc715a7649a262ca5757474f56ecc52d775121be99f18f067570d48d5401ff29cd7141ff768e9ea0a3e0aef5996d32129b8907c386cfd3c72e9f5370
6
+ metadata.gz: efa1a2368c6f2f0352dd928eb33c4c45c9580a431ca3847fe709fcb1026f29deaf2f55e7ec52eafaebdac537ef0c5d55588b22d37d6f6bb27d332165ad9a795a
7
+ data.tar.gz: 5380981def9666c4ec1fa0f4cb7f977dcba141a5278003a96486d814e7c812d2083339baa3d9d8079c3029d7aef0e84226be40eb188315bd5270c974baed1dda
@@ -0,0 +1,69 @@
1
+ = Grammar-backed string formats
2
+
3
+ A serialization format whose input is a *grammar*, not a data structure:
4
+ identifier strings, query languages, DSLs — text that is parsed, not
5
+ decoded. lutaml-model supports registering these as first-class formats
6
+ through the same `FormatRegistry` extension point as xml/json/yaml; the
7
+ parse side is implemented by a grammar engine (the reference
8
+ implementation is https://github.com/parsanol/parsanol-ruby[Parsanol]'s
9
+ `Parsanol::PARG::Lutaml.register`, which consumes a compiled PARG
10
+ artifact plus its bindings).
11
+
12
+ == The contract
13
+
14
+ A grammar-backed format needs three things:
15
+
16
+ * a *parse* — text → attribute hash (the grammar engine's job)
17
+ * *bindings* — capture names → model attributes (the artifact's
18
+ bindings section)
19
+ * a *model class* — a regular `Serializable` with attributes; no
20
+ hand-written `from_h`/`to_h` anywhere
21
+
22
+ == Reference implementation (Parsanol)
23
+
24
+ [source,ruby]
25
+ ----
26
+ require "parsanol/parg/lutaml"
27
+
28
+ Parsanol::PARG::Lutaml.register(
29
+ IsoIdentifier,
30
+ format_name: :pubid_iso,
31
+ artifact: "artifacts/iso.json", # compiled grammar + bindings + tests
32
+ entry: "identifier",
33
+ )
34
+
35
+ IsoIdentifier.from_pubid_iso("ISO/IEC 12345-1:2020")
36
+ # => #<IsoIdentifier number: "12345-1", year: 2020, ...>
37
+ ----
38
+
39
+ What `register` does:
40
+
41
+ . loads the checksummed artifact (parse failures raise the engine's
42
+ error type — register it via `error_types:` so users can rescue it);
43
+ . registers the format through `FormatRegistry.register` with an
44
+ adapter whose `parse` is artifact-parse-then-bind;
45
+ . defines `from_<format>` on the model class.
46
+
47
+ The artifact is the parser of record: it carries the grammar, the
48
+ bindings, and the conformance tests (the grammar's own test block is
49
+ baked in and can be re-run by consumers), and its checksum pins the
50
+ pinned version of the language.
51
+
52
+ == Writing the grammar side
53
+
54
+ Grammars are authored in PARG (`.parg` — ABNF-flavoured surface, PEG
55
+ semantics, one rule per line) and compiled with the `parsanol` CLI:
56
+
57
+ ----
58
+ parsanol compile grammars/iso.parg --tables tables
59
+ ----
60
+
61
+ The compile-time lint rejects left recursion and shadowed
62
+ alternatives; the embedded test block runs at compile time, so a
63
+ grammar that stops accepting its documented inputs fails the build
64
+ rather than the consumer.
65
+
66
+ == See also
67
+
68
+ * parsanol-ruby `docs/pg-language-guide.md` — the PARG language
69
+ * `Parsanol::PARG::Lutaml.register` — the bridge (reference implementation)
@@ -26,6 +26,7 @@ Task-oriented guides for accomplishing specific goals with Lutaml::Model.
26
26
  == Advanced mapping
27
27
 
28
28
  * link:../advanced-mapping[Advanced Attribute Mapping] - Same-name dispatch on attribute values (`when_attribute`), grouped declarations, custom methods, delegation
29
+ * link:../grammar-backed-formats[Grammar-Backed String Formats] - Register a grammar engine as a serialization format (identifier strings, DSLs)
29
30
 
30
31
  == Performance
31
32
 
@@ -47,6 +48,7 @@ See link:../references/rdf-namespaces[RDF Namespaces] for namespace classes,
47
48
  * link:../value-transformations[Value Transformations] - Transform values during serialization
48
49
  * link:../missing-values-handling[Missing Values Handling] - Handle nil/empty/omitted
49
50
  * link:../advanced-mapping[Advanced Mapping] - Custom methods and delegation
51
+ * link:../grammar-backed-formats[Grammar-Backed String Formats] - Grammar engines as serialization formats
50
52
  * link:../schema-generation[Schema Generation] - Generate schemas from models
51
53
  * link:../creating-xsd[Creating XSD Schemas] - Complete reference for XSD generation from LutaML models
52
54
  * link:../schema-import[Schema Import] - Import schemas to create models
@@ -8,6 +8,24 @@ module Lutaml
8
8
  # Extracted from serialize.rb to improve code organization.
9
9
  # Provides methods for defining and validating model attributes.
10
10
  module AttributeDefinition
11
+ # Whether the per-class accessors and the state-defaults seed are
12
+ # compiled from Ruby source with a String class_eval (the fast
13
+ # path). Opal has no runtime compiler unless opal-parser is
14
+ # bundled, so there every such method is defined from a block
15
+ # with define_method instead.
16
+ # Core dispatch for the block-form methods, bound per call so an
17
+ # attribute or method a model defines with one of these names
18
+ # (send, __send__, instance_variable_get, const_get, ...) is never invoked;
19
+ # the compiled form reads ivars and calls methods directly.
20
+ IVAR_GET = ::Kernel.instance_method(:instance_variable_get)
21
+ IVAR_SET = ::Kernel.instance_method(:instance_variable_set)
22
+ SEND = ::BasicObject.instance_method(:__send__)
23
+ CONST_GET = ::Module.instance_method(:const_get)
24
+
25
+ def self.source_compilation?
26
+ !Lutaml::Model.opal?
27
+ end
28
+
11
29
  # Define attribute methods on the model class
12
30
  #
13
31
  # @param attr [Attribute] The attribute to define methods for
@@ -44,6 +62,26 @@ module Lutaml
44
62
  return
45
63
  end
46
64
 
65
+ unless AttributeDefinition.source_compilation?
66
+ seeds = attrs.map do |name, attr|
67
+ sentinel = if attr.collection?
68
+ Lutaml::Model::Serialize::LAZY_EMPTY_COLLECTION
69
+ else
70
+ Lutaml::Model::UninitializedClass.instance
71
+ end
72
+ [:"@#{name}", sentinel]
73
+ end
74
+ # Raw readers for every attribute the register sees, as the
75
+ # compiled form below does (#933).
76
+ attrs.each_key { |name| define_raw_state_reader(name) }
77
+ define_method(method_name) do
78
+ # Returns the last seeded value, as the compiled form's final
79
+ # assignment does.
80
+ seeds.reduce(nil) { |_, (ivar, sentinel)| IVAR_SET.bind_call(self, ivar, sentinel) }
81
+ end
82
+ return
83
+ end
84
+
47
85
  lines = attrs.map do |name, attr|
48
86
  if attr.collection?
49
87
  "@#{name} = Lutaml::Model::Serialize::LAZY_EMPTY_COLLECTION"
@@ -82,6 +120,14 @@ module Lutaml
82
120
  RUBY
83
121
  end
84
122
 
123
+ # Block-form raw-state reader (no String eval), defined on self.
124
+ def define_raw_state_reader(name)
125
+ ivar = :"@#{name}"
126
+ define_method(:"__lutaml_raw_#{name}") do
127
+ IVAR_GET.bind_call(self, ivar)
128
+ end
129
+ end
130
+
85
131
  # Historical getter shape for punctuation-named attributes and
86
132
  # any name the compiled form cannot express.
87
133
  def define_reflective_attribute_methods(name, attr)
@@ -145,11 +191,16 @@ module Lutaml
145
191
  # the state-defaults compilation) so late declarations get it
146
192
  # too -- XMI's extension loader declares attributes after
147
193
  # first parse.
148
- if name.to_s.match?(PLAIN_NAME)
149
- # class_eval on self: a bare `model` receiver resolves
150
- # through the host class's own namespace (omml's Base
151
- # answers a Store model) and defined the reader on the
152
- # wrong object (#933).
194
+ # Without a source compiler, compile_state_defaults! defines
195
+ # its raw readers from blocks; define one for every name here
196
+ # too (define_method takes any name, not only plain ones).
197
+ # Both forms define on self: a bare `model` receiver resolves
198
+ # through the host class's own namespace (omml's Base answers
199
+ # a Store model) and defined the reader on the wrong object
200
+ # (#933).
201
+ if !AttributeDefinition.source_compilation?
202
+ define_raw_state_reader(name)
203
+ elsif name.to_s.match?(PLAIN_NAME)
153
204
  class_eval(<<~RUBY, __FILE__, __LINE__ + 1) # rubocop:disable Style/DocumentDynamicEvalDefinition
154
205
  def __lutaml_raw_#{name}
155
206
  @#{name}
@@ -263,7 +314,9 @@ module Lutaml
263
314
  # Array on EVERY read — the largest per-call allocation source
264
315
  # on instance-heavy parses (TODO.max-perf/06). The optional-arg
265
316
  # form allocates nothing, and the read is a direct @ivar.
266
- if attr.collection?
317
+ if !AttributeDefinition.source_compilation?
318
+ define_block_attribute_getter(name, attr)
319
+ elsif attr.collection?
267
320
  # class_eval interpolates, e.g.:
268
321
  # def items(arg = Lutaml::Model::Serialize::NO_ARG)
269
322
  # if arg.equal?(Lutaml::Model::Serialize::NO_ARG)
@@ -319,7 +372,9 @@ module Lutaml
319
372
  end
320
373
  define_method(attr_reader_method) { attr } unless reader_defined
321
374
 
322
- if attr.collection?
375
+ if !AttributeDefinition.source_compilation?
376
+ define_block_attribute_setters(name, attr)
377
+ elsif attr.collection?
323
378
  # class_eval interpolates, e.g.:
324
379
  # def items=(value)
325
380
  # value_set_for(:items)
@@ -379,6 +434,77 @@ module Lutaml
379
434
  end
380
435
  end
381
436
 
437
+ # Block form of the compiled getter in define_regular_attribute_methods,
438
+ # for runtimes without a source compiler (see source_compilation?).
439
+ def define_block_attribute_getter(name, attr)
440
+ name = name.to_sym # the compiled form passes :name literals
441
+ ivar = :"@#{name}"
442
+ if attr.collection?
443
+ # The compiled getter resolves Array in the model class's scope.
444
+ scope = self
445
+ define_method(name) do |arg = Lutaml::Model::Serialize::NO_ARG|
446
+ if arg.equal?(Lutaml::Model::Serialize::NO_ARG)
447
+ materialize_lazy_collection(name)
448
+ else
449
+ current = IVAR_GET.bind_call(self, ivar) || []
450
+ new_value = current.is_a?(CONST_GET.bind_call(scope, :Array)) ? current + [arg] : arg
451
+ IVAR_SET.bind_call(self, ivar, new_value)
452
+ record_mutation(name, arg)
453
+ arg
454
+ end
455
+ end
456
+ else
457
+ define_method(name) do |arg = Lutaml::Model::Serialize::NO_ARG|
458
+ if arg.equal?(Lutaml::Model::Serialize::NO_ARG)
459
+ IVAR_GET.bind_call(self, ivar)
460
+ else
461
+ public_send(:"#{name}=", arg)
462
+ arg
463
+ end
464
+ end
465
+ end
466
+ end
467
+
468
+ # Block form of the compiled setters in define_regular_attribute_methods.
469
+ # The Attribute is read through the same hidden accessor the
470
+ # compiled form calls, not captured: a setter redefined over an
471
+ # enum shorthand keeps casting with the accessor's Attribute.
472
+ def define_block_attribute_setters(name, attr)
473
+ name = name.to_sym # the compiled form passes :name literals
474
+ ivar = :"@#{name}"
475
+ handle = :"__attribute_definition_#{name}"
476
+ if attr.collection?
477
+ assign = lambda do |model, value|
478
+ current = IVAR_GET.bind_call(model, ivar)
479
+ unless current.equal?(Lutaml::Model::Serialize::LAZY_EMPTY_COLLECTION) &&
480
+ (value.nil? || Lutaml::Model::Utils.uninitialized?(value))
481
+ IVAR_SET.bind_call(model, ivar, value)
482
+ end
483
+ SEND.bind_call(model, :record_mutation_collection, name, value)
484
+ end
485
+ define_method(:"#{name}=") do |value|
486
+ value_set_for(name)
487
+ assign.call(self, SEND.bind_call(self, handle).cast_value(value, lutaml_register))
488
+ end
489
+ define_method(:"__assign_parsed_#{name}=") do |value|
490
+ value_set_for(name)
491
+ assign.call(self, value)
492
+ end
493
+ else
494
+ define_method(:"#{name}=") do |value|
495
+ value_set_for(name)
496
+ value = SEND.bind_call(self, handle).cast_value(value, lutaml_register)
497
+ IVAR_SET.bind_call(self, ivar, value)
498
+ record_mutation(name, value)
499
+ end
500
+ define_method(:"__assign_parsed_#{name}=") do |value|
501
+ value_set_for(name)
502
+ IVAR_SET.bind_call(self, ivar, value)
503
+ record_mutation(name, value)
504
+ end
505
+ end
506
+ end
507
+
382
508
  # Define an attribute for the model
383
509
  #
384
510
  # @param name [Symbol] The attribute name
@@ -103,9 +103,14 @@ module Lutaml
103
103
 
104
104
  register = Lutaml::Model::Config.default_register
105
105
  plan = Lutaml::Xml::Leptris::PlanCompiler.compile(self, register)
106
- return nil unless plan && !plan[:namespaced] &&
106
+ return nil unless plan &&
107
107
  Lutaml::Xml::Leptris::PlanSerializer.serializable?(plan)
108
108
 
109
+ # Namespaced plans ride the plan serializer too: the DOM
110
+ # emission spells prefixed names and hoists declarations
111
+ # (the #847 xmlns drop is fixed in PlanSerializer); any
112
+ # unspellable shape answers nil there and falls back
113
+ # interpretively.
109
114
  Lutaml::Xml::Leptris::PlanSerializer.call(instance, plan)
110
115
  end
111
116
 
@@ -268,6 +268,11 @@ module Lutaml
268
268
  # Uses TracePoint to detect when a class definition is complete,
269
269
  # allowing deferred import resolution.
270
270
  def setup_trace_point
271
+ # Opal has no TracePoint. Symbol imports still resolve lazily
272
+ # on first use (ensure_imports!), so there the class is marked
273
+ # finalized at once instead of at the end of its body.
274
+ return @finalized = true if Lutaml::Model.opal?
275
+
271
276
  @trace ||= TracePoint.new(:end) do |_tp|
272
277
  if include?(Lutaml::Model::Serialize)
273
278
  @finalized = true
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Lutaml
4
4
  module Model
5
- VERSION = "0.8.95"
5
+ VERSION = "0.8.96"
6
6
  end
7
7
  end
data/lib/lutaml/model.rb CHANGED
@@ -294,3 +294,10 @@ require "#{__dir__}/xml/format"
294
294
  # Builder must be prepended AFTER XML so its initialize runs first
295
295
  # (Builder -> XML InstanceMethods -> Serialize)
296
296
  Lutaml::Model::Serialize.prepend(Lutaml::Model::Serialize::Builder)
297
+
298
+ # Opal does not propagate a module prepended into an already-included
299
+ # module to classes that included it earlier (see lib/lutaml/xml/format.rb).
300
+ # Serializable included Serialize at boot, so Builder goes onto it directly.
301
+ if Lutaml::Model.opal?
302
+ Lutaml::Model::Serializable.prepend(Lutaml::Model::Serialize::Builder)
303
+ end
@@ -243,9 +243,11 @@ Lutaml::Model::Serialize.prepend(
243
243
  #
244
244
  # Anonymous model classes (Class.new { include Serialize }) extend
245
245
  # Serialize::ClassMethods rather than inheriting from Serializable, so the
246
- # ModelImportExt override (root?) must land on ClassMethods too. The other
247
- # two (FormatConversion, InstanceMethods) are already prepended unconditionally
248
- # above, so Opal's no-double-prepend rule means we don't repeat them here.
246
+ # ModelImportExt override (root?) must land on ClassMethods too.
247
+ #
248
+ # On Opal (and TruffleRuby, below) the ClassMethods.prepend(FormatConversion)
249
+ # above does not reach Serializable's singleton chain, which extended
250
+ # ClassMethods before it ran, so it is prepended onto that singleton directly.
249
251
  #
250
252
  # TruffleRuby (34.0.1) has the same propagation gap, on both the include
251
253
  # side and the extend/singleton side — a module prepended into an
@@ -273,13 +275,9 @@ if Lutaml::Model.opal? || RUBY_ENGINE == "truffleruby"
273
275
  Lutaml::Xml::Serialization::ModelImportExt,
274
276
  )
275
277
 
276
- # TruffleRuby only: Opal's no-double-prepend rule makes the unconditional
277
- # ClassMethods.prepend(FormatConversion) above sufficient there.
278
- if RUBY_ENGINE == "truffleruby"
279
- Lutaml::Model::Serializable.singleton_class.prepend(
280
- Lutaml::Xml::Serialization::FormatConversion,
281
- )
282
- end
278
+ Lutaml::Model::Serializable.singleton_class.prepend(
279
+ Lutaml::Xml::Serialization::FormatConversion,
280
+ )
283
281
  end
284
282
 
285
283
  # Register XML-specific attribute override warning names
@@ -292,14 +292,28 @@ module Lutaml
292
292
  needs_nodes = true unless ns_attr_names.empty?
293
293
 
294
294
  # A model combining exact-URI attribute rows with nested
295
- # child rows stays interpretive: the engine's walk drops
296
- # nested-children attribute capture under such plans
297
- # (FontTable's w:name under an mc:Ignorable ns row —
298
- # observed 1.9.311.3; filed upstream). Leaf models keep the
299
- # lift — that is the measured win (uniword's deep property
300
- # models are leaves).
295
+ # child rows needs the walk's nested attribute capture
296
+ # (leptris#1563, fixed 1.9.313.0): older engines drop the
297
+ # children's attribute capture under such plans, so they
298
+ # stay interpretive. Leaf models are unaffected.
301
299
  return opt_out!(:attr_type_ns_nested) if plan_attrs.any? { |a| a[:ns] } &&
302
- rows.any?
300
+ rows.any? && !Leptris.nested_attr_capture_compatible?
301
+
302
+ # The catch-all row: kind :wildcard with an explicit :any ns
303
+ # form (pad0) — the two-pass walk routes every child no
304
+ # named row claimed here, tagged for hydrator routing.
305
+ # Document order recovery and bridging happen in the
306
+ # hydrator against the source node.
307
+ if (any_rule = mapping.any_element_rule)
308
+ any_attr = model_class.attributes(register)[any_rule.to]
309
+ return opt_out!(:attr_nil) if any_attr.nil?
310
+
311
+ needs_nodes = true
312
+ wildcard_tag = tag += 1
313
+ compiled << [any_rule, any_attr, :wildcard_any, nil, nil]
314
+ rows << { name: "__lutaml_any__", kind: :wildcard, ns: :any,
315
+ type_tag: wildcard_tag }
316
+ end
303
317
 
304
318
  { descriptor: descriptor, tree: tree, rows: compiled,
305
319
  attr_rows: attr_rows, mapping: mapping,
@@ -307,7 +321,8 @@ module Lutaml
307
321
  ordered: mapping.ordered? || mapping.mixed_content?,
308
322
  needs_nodes: needs_nodes,
309
323
  collection_defaults: collection_defaults,
310
- ns_attr_names: ns_attr_names.empty? ? nil : ns_attr_names }
324
+ ns_attr_names: ns_attr_names.empty? ? nil : ns_attr_names,
325
+ wildcard_tag: (wildcard_tag if defined?(wildcard_tag)) }
311
326
  end
312
327
 
313
328
  # Attribute rows: identity is (URI, local) (lutaml-model#744).
@@ -341,7 +356,8 @@ module Lutaml
341
356
 
342
357
  claim[:ns] += 1
343
358
  plan_attrs << { name: rule.name.to_s,
344
- ns: { exact: type_ns.uri.to_s } }
359
+ ns: { exact: type_ns.uri.to_s,
360
+ prefix: type_ns.prefix_default&.to_s } }
345
361
  ns_attr_names << rule.name.to_s
346
362
  nil
347
363
  end
@@ -349,12 +365,11 @@ module Lutaml
349
365
  def compilable_mapping?(mapping)
350
366
  # Mapping's uniform interface: root_mappings defaults to
351
367
  # false on the base (KeyValue overrides with its own).
352
- # A map_any_element catch-all is not plan-shaped yet — the
353
- # compiled plan has no row for it, so plan-path models
354
- # would silently drop every catch-all child (0.8.86's
355
- # smoke catch). The interpretive path owns them until the
356
- # plan learns the catch-all row.
357
- return false if mapping.any_element_rule
368
+ # The map_any_element catch-all rides the engine's wildcard
369
+ # row (leptris#1552, named rows take precedence); engines
370
+ # without the kind keep the interpretive refusal.
371
+ return false if mapping.any_element_rule &&
372
+ !Leptris.wildcard_rows_compatible?
358
373
 
359
374
  mapping.root_element && !mapping.root_mappings?
360
375
  end
@@ -415,9 +430,9 @@ module Lutaml
415
430
  # prefix. Blank-namespace rules (xmlns="") match :none.
416
431
  def child_ns(rule, _model_ns)
417
432
  uri = rule.namespace
418
- return { exact: uri.to_s } if uri && !uri.to_s.empty?
433
+ return :none if uri.nil? || uri.to_s.empty?
419
434
 
420
- :none
435
+ { exact: uri.to_s, prefix: rule.prefix&.to_s }
421
436
  end
422
437
 
423
438
  def content_rows(rows)
@@ -431,7 +446,10 @@ module Lutaml
431
446
  # Mapping's uniform interface: namespace_class defaults
432
447
  # to nil on the base (Xml overrides).
433
448
  ns_class = mapping.namespace_class
434
- ns_class&.uri ? { exact: ns_class.uri.to_s } : nil
449
+ return nil unless ns_class&.uri
450
+
451
+ { exact: ns_class.uri.to_s,
452
+ prefix: ns_class.prefix_default&.to_s }
435
453
  end
436
454
 
437
455
  def scalar_type?(attr, register)
@@ -201,7 +201,7 @@ module Lutaml
201
201
  v = rule.transform_value(attr, v, :from, :xml) if rule.transform.is_a?(Class)
202
202
  assign(kwargs, delegates, delegate, rule, attr, v)
203
203
  end
204
- when :raw, :custom_method, :polymorphic, :content_deferred
204
+ when :wildcard_any
205
205
  # interpreted post-instance (interpret_deferred)
206
206
  when :content
207
207
  runs = content_runs(value)
@@ -352,6 +352,39 @@ module Lutaml
352
352
 
353
353
  instance.public_send(:"#{attr.name}=",
354
354
  attr.collection? ? results : results.first)
355
+ when :wildcard_any
356
+ # map_any_element: every child element no named row
357
+ # claims, in document order, bridged as wrappers — the
358
+ # interpretive pass's contract. The wildcard row's
359
+ # type_tag marks its captures in the walk.
360
+ next unless node
361
+
362
+ claimed = plan[:rows].filter_map do |wr, _wa, _wk, spelling, _wd|
363
+ next if wr.equal?(rule) || wr.name.nil?
364
+
365
+ spelling ? spelling.to_s : wr.name.to_s
366
+ end.uniq
367
+ elements = []
368
+ node.children.each do |child|
369
+ next unless child.is_a?(::Leptris::XML::Element)
370
+ next if claimed.include?(child.name)
371
+
372
+ elements << bridge_element(child)
373
+ end
374
+ next if elements.empty?
375
+
376
+ # The interpretive pass casts each collected element
377
+ # through the attribute: String keeps the wrapper,
378
+ # Serializable types build model instances.
379
+ results = elements.map do |element|
380
+ attr.cast(element, :xml, register,
381
+ lutaml_parent: instance,
382
+ lutaml_root: instance.lutaml_root || instance)
383
+ end
384
+ instance.public_send(
385
+ :"#{attr.name}=",
386
+ attr.collection? ? results : results.first,
387
+ )
355
388
  when :polymorphic
356
389
  results = deferred_elements(value, rule, buckets).map do |element|
357
390
  attr.cast(element, :xml, register,