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 +4 -4
- data/docs/_guides/grammar-backed-formats.adoc +69 -0
- data/docs/_guides/index.adoc +2 -0
- data/lib/lutaml/model/serialize/attribute_definition.rb +133 -7
- data/lib/lutaml/model/serialize/format_conversion.rb +6 -1
- data/lib/lutaml/model/serialize/model_import.rb +5 -0
- data/lib/lutaml/model/version.rb +1 -1
- data/lib/lutaml/model.rb +7 -0
- data/lib/lutaml/xml/format.rb +8 -10
- data/lib/lutaml/xml/leptris/plan_compiler.rb +36 -18
- data/lib/lutaml/xml/leptris/plan_hydrator.rb +34 -1
- data/lib/lutaml/xml/leptris/plan_serializer.rb +114 -18
- data/lib/lutaml/xml/leptris.rb +26 -0
- data/spec/lutaml/model/opal_smoke_spec.rb +17 -0
- data/spec/lutaml/model/serialize/attribute_definition_block_form_spec.rb +244 -0
- data/spec/lutaml/xml/opal_xml_spec.rb +47 -0
- data/spec/lutaml/xml/plan_namespaced_serialize_spec.rb +157 -0
- data/spec/lutaml/xml/plan_type_ns_attribute_spec.rb +2 -1
- data/spec/lutaml/xml/plan_wildcard_any_spec.rb +105 -0
- metadata +5 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 7b4bd8b10dc92c3a53f615bb3e13c907d1cc4976eb547a5a2da41aa2837bef17
|
|
4
|
+
data.tar.gz: e1e5eacb6d80dc8108d65a5264d5e9d872d4259103672f6a88e0d912b437002f
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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)
|
data/docs/_guides/index.adoc
CHANGED
|
@@ -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
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
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
|
|
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
|
|
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 &&
|
|
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
|
data/lib/lutaml/model/version.rb
CHANGED
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
|
data/lib/lutaml/xml/format.rb
CHANGED
|
@@ -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.
|
|
247
|
-
#
|
|
248
|
-
#
|
|
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
|
-
|
|
277
|
-
|
|
278
|
-
|
|
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
|
|
296
|
-
#
|
|
297
|
-
#
|
|
298
|
-
#
|
|
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
|
-
#
|
|
353
|
-
#
|
|
354
|
-
#
|
|
355
|
-
|
|
356
|
-
|
|
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
|
|
433
|
+
return :none if uri.nil? || uri.to_s.empty?
|
|
419
434
|
|
|
420
|
-
:
|
|
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
|
-
|
|
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 :
|
|
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,
|