lutaml-model 0.8.95 → 0.8.97
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 +13 -2
- data/lib/lutaml/model/serialize/initialization.rb +25 -7
- data/lib/lutaml/model/serialize/model_import.rb +39 -4
- 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 +137 -22
- data/lib/lutaml/xml/leptris/plan_hydrator.rb +34 -1
- data/lib/lutaml/xml/leptris/plan_serializer.rb +174 -18
- data/lib/lutaml/xml/leptris/plan_walk.rb +1 -0
- data/lib/lutaml/xml/leptris.rb +47 -0
- data/lib/lutaml/xml/model_transform.rb +38 -14
- data/spec/lutaml/model/model_import_reentry_spec.rb +46 -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 +7 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 2dcefe56e003b241812971d37fc9d76d6b6945cb5a138a9fa15f9ce2aa4fbd7d
|
|
4
|
+
data.tar.gz: 835dfecf768169c9fbc6ab772c2e11dbe593f3f5e9deb7016b09c8088706117c
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 7efe4b0bc24f20b9d48e37c33b5e23cf23af7397900a7d788912275a4b396318d916c721b21bc6373de53e99f3f4f0dc29e459c94bba2e2baaf8c4da67232c14
|
|
7
|
+
data.tar.gz: 59d5412393469b1904c4c6e29bc0ff58b0c6333b5e3710e531d9e7a8ae465e7762993288f04e6147a12a204a8fb581059a24491d9dc90d7f309e78b1bb39b97a
|
|
@@ -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
|
|
@@ -95,7 +95,12 @@ module Lutaml
|
|
|
95
95
|
options.key?(:mappings) || options.key?(:adapter) ||
|
|
96
96
|
options.key?(:_adapter_override) ||
|
|
97
97
|
options.key?(:indent) || options.key?(:xml_declaration) ||
|
|
98
|
-
options.key?(:declaration) || options.key?(:doctype)
|
|
98
|
+
options.key?(:declaration) || options.key?(:doctype) ||
|
|
99
|
+
# prefix requests ride the interpretive writer's use_prefix
|
|
100
|
+
# option (planner Tier-1) — the plan emission has no
|
|
101
|
+
# option-aware spelling. The instance-level option prep
|
|
102
|
+
# rewrites :prefix to :use_prefix before this gate.
|
|
103
|
+
options.key?(:prefix) || options.key?(:use_prefix)
|
|
99
104
|
|
|
100
105
|
adapter_name = Lutaml::Model::Config.adapter_for(:xml)
|
|
101
106
|
adapter_name &&= adapter_name.name
|
|
@@ -103,9 +108,14 @@ module Lutaml
|
|
|
103
108
|
|
|
104
109
|
register = Lutaml::Model::Config.default_register
|
|
105
110
|
plan = Lutaml::Xml::Leptris::PlanCompiler.compile(self, register)
|
|
106
|
-
return nil unless plan &&
|
|
111
|
+
return nil unless plan &&
|
|
107
112
|
Lutaml::Xml::Leptris::PlanSerializer.serializable?(plan)
|
|
108
113
|
|
|
114
|
+
# Namespaced plans ride the plan serializer too: the DOM
|
|
115
|
+
# emission spells prefixed names and hoists declarations
|
|
116
|
+
# (the #847 xmlns drop is fixed in PlanSerializer); any
|
|
117
|
+
# unspellable shape answers nil there and falls back
|
|
118
|
+
# interpretively.
|
|
109
119
|
Lutaml::Xml::Leptris::PlanSerializer.call(instance, plan)
|
|
110
120
|
end
|
|
111
121
|
|
|
@@ -128,6 +138,7 @@ module Lutaml
|
|
|
128
138
|
register = Lutaml::Model::Config.default_register
|
|
129
139
|
plan = Lutaml::Xml::Leptris::PlanCompiler.compile(self, register)
|
|
130
140
|
return nil unless plan
|
|
141
|
+
return nil unless Lutaml::Xml::Leptris::PlanCompiler.engine_walk_safe?(plan)
|
|
131
142
|
|
|
132
143
|
root = ::Leptris::XML.parse(data.to_s).root
|
|
133
144
|
return nil if root.nil?
|
|
@@ -172,12 +172,30 @@ module Lutaml
|
|
|
172
172
|
#
|
|
173
173
|
# @param register [Symbol, nil] The register context
|
|
174
174
|
def ensure_imports!(register = nil)
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
#
|
|
179
|
-
#
|
|
180
|
-
|
|
175
|
+
# Bounded re-entry for the resolution chain. Each leg's
|
|
176
|
+
# record_import_attempt tail calls attributes(), which calls
|
|
177
|
+
# back into ensure_imports!; nested passes converge imports
|
|
178
|
+
# whose targets only become resolvable after an earlier leg
|
|
179
|
+
# (so they must not be dropped), but with a permanently
|
|
180
|
+
# pending import the tail re-entered unboundedly and crashed
|
|
181
|
+
# with SystemStackError (#938). Two levels allow one nested
|
|
182
|
+
# convergence pass; deeper re-entry is suppressed per
|
|
183
|
+
# register and the counter unwinds on error.
|
|
184
|
+
register_id = extract_register_id(register)
|
|
185
|
+
(@ensure_imports_depth ||= {})[register_id] ||= 0
|
|
186
|
+
return if @ensure_imports_depth[register_id] >= 2
|
|
187
|
+
|
|
188
|
+
@ensure_imports_depth[register_id] += 1
|
|
189
|
+
begin
|
|
190
|
+
ensure_model_imports!(register)
|
|
191
|
+
ensure_choice_imports!(register)
|
|
192
|
+
ensure_restrict_attributes!(register)
|
|
193
|
+
# Hook for format-specific mapping import resolution.
|
|
194
|
+
# XML overrides this to call mappings[:xml]&.ensure_mappings_imported!(register)
|
|
195
|
+
ensure_format_mapping_imports!(register)
|
|
196
|
+
ensure
|
|
197
|
+
@ensure_imports_depth[register_id] -= 1
|
|
198
|
+
end
|
|
181
199
|
end
|
|
182
200
|
|
|
183
201
|
# Hook for format-specific mapping import resolution.
|
|
@@ -357,7 +375,7 @@ module Lutaml
|
|
|
357
375
|
# chain's frames per level pushed that recursion past the
|
|
358
376
|
# stack limit — models without pending deferred imports skip
|
|
359
377
|
# it entirely.
|
|
360
|
-
ensure_imports!(register_id) if finalized? && deferred_imports_pending?
|
|
378
|
+
ensure_imports!(register_id) if finalized? && deferred_imports_pending?(register_id)
|
|
361
379
|
instance = allocate
|
|
362
380
|
instance.finalize_deserialization(register_id)
|
|
363
381
|
instance
|
|
@@ -162,11 +162,39 @@ module Lutaml
|
|
|
162
162
|
# @param register_id [Symbol, nil] The register context
|
|
163
163
|
# Whether deferred (symbol-form) imports are still pending —
|
|
164
164
|
# gates the allocation-time ensure chain (#933).
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
165
|
+
# The attribute-set fingerprint at the moment an import
|
|
166
|
+
# resolution attempt ran — deferred_imports_pending? compares
|
|
167
|
+
# against it to skip re-resolution on stable sets.
|
|
168
|
+
def record_import_attempt(register_id)
|
|
169
|
+
# Raw stores only — the lazy attributes() re-enters
|
|
170
|
+
# ensure_imports! and, when @models_imported was just set to
|
|
171
|
+
# false (unresolved imports), cycles through this very
|
|
172
|
+
# method forever (the uniword register_in SystemStackError).
|
|
173
|
+
register_attrs = (@register_records ||= {})[register_id]
|
|
174
|
+
(@import_attempt_fingerprints ||= {})[register_id] =
|
|
175
|
+
@attributes.size + @choice_attributes.to_a.size +
|
|
176
|
+
(register_attrs ? register_attrs[:attributes].to_a.size : 0)
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
def deferred_imports_pending?(register_id = nil)
|
|
180
|
+
register_id ||= Lutaml::Model::Config.default_register
|
|
181
|
+
has_importables = importable_models.any? { |_m, models| models.any? } ||
|
|
182
|
+
importable_choices.any? do |_c, imports|
|
|
183
|
+
imports.any? { |_m, models| models.any? }
|
|
169
184
|
end || restrict_attributes.any?
|
|
185
|
+
return false unless has_importables
|
|
186
|
+
|
|
187
|
+
# Unresolvable imports stay pending forever; re-running the
|
|
188
|
+
# ensure chain (and its state-defaults invalidation) on every
|
|
189
|
+
# allocation thrashed import-heavy models quadratically.
|
|
190
|
+
# Resolution re-arms only when the attribute set CHANGED
|
|
191
|
+
# since the last attempt — a late declaration after a failed
|
|
192
|
+
# resolution re-attempts; a stable set skips.
|
|
193
|
+
attempted = (@import_attempt_fingerprints ||= {})[register_id]
|
|
194
|
+
return true if attempted.nil?
|
|
195
|
+
|
|
196
|
+
current = attributes(register_id).size + @choice_attributes.to_a.size
|
|
197
|
+
current != attempted
|
|
170
198
|
end
|
|
171
199
|
|
|
172
200
|
def ensure_model_imports!(register_id = nil)
|
|
@@ -200,6 +228,7 @@ module Lutaml
|
|
|
200
228
|
end
|
|
201
229
|
|
|
202
230
|
@models_imported[register_id] = all_resolved
|
|
231
|
+
record_import_attempt(register_id)
|
|
203
232
|
end
|
|
204
233
|
|
|
205
234
|
# Ensure all choice imports are resolved for a specific register
|
|
@@ -239,6 +268,7 @@ module Lutaml
|
|
|
239
268
|
end
|
|
240
269
|
|
|
241
270
|
@choices_imported[register_id] = true if all_resolved
|
|
271
|
+
record_import_attempt(register_id)
|
|
242
272
|
# Choice imports can add attributes without passing through
|
|
243
273
|
# define_attribute_methods — recompile state defaults so the
|
|
244
274
|
# seeding and raw readers cover them (#933). Only when an
|
|
@@ -268,6 +298,11 @@ module Lutaml
|
|
|
268
298
|
# Uses TracePoint to detect when a class definition is complete,
|
|
269
299
|
# allowing deferred import resolution.
|
|
270
300
|
def setup_trace_point
|
|
301
|
+
# Opal has no TracePoint. Symbol imports still resolve lazily
|
|
302
|
+
# on first use (ensure_imports!), so there the class is marked
|
|
303
|
+
# finalized at once instead of at the end of its body.
|
|
304
|
+
return @finalized = true if Lutaml::Model.opal?
|
|
305
|
+
|
|
271
306
|
@trace ||= TracePoint.new(:end) do |_tp|
|
|
272
307
|
if include?(Lutaml::Model::Serialize)
|
|
273
308
|
@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
|