lutaml-model 0.8.94 → 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/.github/workflows/downstream-performance.yml +17 -6
- data/bench/bench_merge_attempts.rb +49 -0
- data/docs/_guides/grammar-backed-formats.adoc +69 -0
- data/docs/_guides/index.adoc +2 -0
- data/lib/lutaml/model/serialize/attribute_definition.rb +145 -16
- data/lib/lutaml/model/serialize/format_conversion.rb +6 -1
- data/lib/lutaml/model/serialize/initialization.rb +13 -1
- data/lib/lutaml/model/serialize/model_import.rb +30 -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 +110 -21
- data/lib/lutaml/xml/leptris/plan_hydrator.rb +124 -10
- data/lib/lutaml/xml/leptris/plan_serializer.rb +114 -18
- data/lib/lutaml/xml/leptris.rb +38 -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/model/writer_assigned_attribute_spec.rb +2 -2
- data/spec/lutaml/xml/opal_xml_spec.rb +47 -0
- data/spec/lutaml/xml/plan_fast_path_spec.rb +6 -4
- data/spec/lutaml/xml/plan_namespaced_serialize_spec.rb +157 -0
- data/spec/lutaml/xml/plan_ns_less_document_spec.rb +86 -0
- data/spec/lutaml/xml/plan_type_ns_attribute_spec.rb +142 -0
- data/spec/lutaml/xml/plan_wildcard_any_spec.rb +105 -0
- data/spec/person_spec.rb +8 -8
- metadata +9 -2
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
|
|
@@ -116,9 +116,16 @@ jobs:
|
|
|
116
116
|
File.write("Gemfile", out.join)
|
|
117
117
|
' /tmp/main-checkout
|
|
118
118
|
bundle install --jobs=4
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
119
|
+
# lutaml-model#834: min-of-N across attempts — contention
|
|
120
|
+
# inflates single-shot timings, so run three attempts and let
|
|
121
|
+
# the merge keep the least-interfered measurement per fixture.
|
|
122
|
+
for i in 1 2 3; do
|
|
123
|
+
bundle exec ruby ${{ github.workspace }}/bench/bench_${{ matrix.downstream }}.rb || \
|
|
124
|
+
echo "::warning::baseline attempt $i errored (#834) — continuing with remaining attempts"
|
|
125
|
+
cp "$BENCH_JSON" "$BENCH_JSON.attempt$i" 2>/dev/null || true
|
|
126
|
+
done
|
|
127
|
+
ruby ${{ github.workspace }}/bench/bench_merge_attempts.rb \
|
|
128
|
+
"$BENCH_JSON" "$BENCH_JSON.attempt1" "$BENCH_JSON.attempt2" "$BENCH_JSON.attempt3"
|
|
122
129
|
|
|
123
130
|
- name: Run current benchmark (PR branch)
|
|
124
131
|
env:
|
|
@@ -146,9 +153,13 @@ jobs:
|
|
|
146
153
|
File.write("Gemfile", out.join)
|
|
147
154
|
' ${{ github.workspace }}
|
|
148
155
|
bundle install --jobs=4
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
156
|
+
for i in 1 2 3; do
|
|
157
|
+
bundle exec ruby ${{ github.workspace }}/bench/bench_${{ matrix.downstream }}.rb || \
|
|
158
|
+
echo "::warning::current attempt $i errored (#834) — continuing with remaining attempts"
|
|
159
|
+
cp "$BENCH_JSON" "$BENCH_JSON.attempt$i" 2>/dev/null || true
|
|
160
|
+
done
|
|
161
|
+
ruby ${{ github.workspace }}/bench/bench_merge_attempts.rb \
|
|
162
|
+
"$BENCH_JSON" "$BENCH_JSON.attempt1" "$BENCH_JSON.attempt2" "$BENCH_JSON.attempt3"
|
|
152
163
|
|
|
153
164
|
- name: Compare baseline vs current
|
|
154
165
|
run: |
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# lutaml-model#834: merge N benchmark-attempt JSONs into one result set,
|
|
4
|
+
# taking the per-fixture minimum of every metric. Contention inflates
|
|
5
|
+
# timings; the minimum across attempts is the least-interfered
|
|
6
|
+
# measurement, so gates evaluated on merged output misfire only when
|
|
7
|
+
# EVERY attempt was slow.
|
|
8
|
+
#
|
|
9
|
+
# Usage: ruby bench_merge_attempts.rb <merged.json> <attempt1.json> [attempt2.json ...]
|
|
10
|
+
#
|
|
11
|
+
# Attempt files may be missing (crashed attempt): missing files are
|
|
12
|
+
# skipped; if none exist, nothing is written and the caller's compare
|
|
13
|
+
# step fails on the missing merged file.
|
|
14
|
+
if ARGV.length < 3
|
|
15
|
+
warn "Usage: #{$PROGRAM_NAME} <merged.json> <attempt1.json> [attempt2.json ...]"
|
|
16
|
+
exit 1
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
merged_path = ARGV[0]
|
|
20
|
+
attempt_paths = ARGV[1..]
|
|
21
|
+
|
|
22
|
+
require "json"
|
|
23
|
+
|
|
24
|
+
best = {}
|
|
25
|
+
attempt_paths.each do |path|
|
|
26
|
+
next unless File.exist?(path)
|
|
27
|
+
|
|
28
|
+
results = JSON.parse(File.read(path))
|
|
29
|
+
results.each do |label, metrics|
|
|
30
|
+
cur = best[label]
|
|
31
|
+
if cur
|
|
32
|
+
cur["avg_time"] = [cur["avg_time"], metrics["avg_time"]].min
|
|
33
|
+
cur["min_time"] = [cur["min_time"], metrics["min_time"]].min
|
|
34
|
+
cur["max_time"] = [cur["max_time"], metrics["max_time"]].min
|
|
35
|
+
cur["allocations"] = [cur["allocations"], metrics["allocations"]].min
|
|
36
|
+
cur["ips"] = [cur["ips"], metrics["ips"]].max
|
|
37
|
+
else
|
|
38
|
+
best[label] = metrics.dup
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
if best.nil? || best.empty?
|
|
44
|
+
warn "No attempt JSONs found among: #{attempt_paths.join(', ')}"
|
|
45
|
+
exit 1
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
File.write(merged_path, JSON.pretty_generate(best))
|
|
49
|
+
puts "Merged #{attempt_paths.size} attempt file(s) into #{merged_path} (#{best.size} fixtures)"
|
|
@@ -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"
|
|
@@ -63,18 +101,14 @@ module Lutaml
|
|
|
63
101
|
end
|
|
64
102
|
RUBY
|
|
65
103
|
|
|
66
|
-
#
|
|
67
|
-
#
|
|
68
|
-
#
|
|
69
|
-
#
|
|
70
|
-
#
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
# initialized marker (sentinel / lazy collection) is unset;
|
|
75
|
-
# anything else was written since init and must never be
|
|
76
|
-
# overwritten by an unmatched rule's default assignment.
|
|
77
|
-
raw_lines = attrs.map do |name, _attr|
|
|
104
|
+
# Raw readers compiled alongside the seeding so every
|
|
105
|
+
# attribute the register can see — including late-merged
|
|
106
|
+
# imports that bypass define_attribute_methods — carries
|
|
107
|
+
# one (#933). Idempotent with the declaration-time
|
|
108
|
+
# definition.
|
|
109
|
+
raw_lines = attrs.filter_map do |name, _attr|
|
|
110
|
+
next unless name.to_s.match?(PLAIN_NAME)
|
|
111
|
+
|
|
78
112
|
<<~RUBY
|
|
79
113
|
def __lutaml_raw_#{name}
|
|
80
114
|
@#{name}
|
|
@@ -86,6 +120,14 @@ module Lutaml
|
|
|
86
120
|
RUBY
|
|
87
121
|
end
|
|
88
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
|
+
|
|
89
131
|
# Historical getter shape for punctuation-named attributes and
|
|
90
132
|
# any name the compiled form cannot express.
|
|
91
133
|
def define_reflective_attribute_methods(name, attr)
|
|
@@ -126,6 +168,9 @@ module Lutaml
|
|
|
126
168
|
end
|
|
127
169
|
remove_method(compiled) if defined_now
|
|
128
170
|
end
|
|
171
|
+
# The names cache must go with the methods: the memoized
|
|
172
|
+
# lookup answers stale names for removed methods otherwise.
|
|
173
|
+
@state_defaults_names.clear
|
|
129
174
|
end
|
|
130
175
|
|
|
131
176
|
def define_attribute_methods(attr, register = nil)
|
|
@@ -146,8 +191,17 @@ module Lutaml
|
|
|
146
191
|
# the state-defaults compilation) so late declarations get it
|
|
147
192
|
# too -- XMI's extension loader declares attributes after
|
|
148
193
|
# first parse.
|
|
149
|
-
|
|
150
|
-
|
|
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)
|
|
204
|
+
class_eval(<<~RUBY, __FILE__, __LINE__ + 1) # rubocop:disable Style/DocumentDynamicEvalDefinition
|
|
151
205
|
def __lutaml_raw_#{name}
|
|
152
206
|
@#{name}
|
|
153
207
|
end
|
|
@@ -260,7 +314,9 @@ module Lutaml
|
|
|
260
314
|
# Array on EVERY read — the largest per-call allocation source
|
|
261
315
|
# on instance-heavy parses (TODO.max-perf/06). The optional-arg
|
|
262
316
|
# form allocates nothing, and the read is a direct @ivar.
|
|
263
|
-
if
|
|
317
|
+
if !AttributeDefinition.source_compilation?
|
|
318
|
+
define_block_attribute_getter(name, attr)
|
|
319
|
+
elsif attr.collection?
|
|
264
320
|
# class_eval interpolates, e.g.:
|
|
265
321
|
# def items(arg = Lutaml::Model::Serialize::NO_ARG)
|
|
266
322
|
# if arg.equal?(Lutaml::Model::Serialize::NO_ARG)
|
|
@@ -316,7 +372,9 @@ module Lutaml
|
|
|
316
372
|
end
|
|
317
373
|
define_method(attr_reader_method) { attr } unless reader_defined
|
|
318
374
|
|
|
319
|
-
if
|
|
375
|
+
if !AttributeDefinition.source_compilation?
|
|
376
|
+
define_block_attribute_setters(name, attr)
|
|
377
|
+
elsif attr.collection?
|
|
320
378
|
# class_eval interpolates, e.g.:
|
|
321
379
|
# def items=(value)
|
|
322
380
|
# value_set_for(:items)
|
|
@@ -376,6 +434,77 @@ module Lutaml
|
|
|
376
434
|
end
|
|
377
435
|
end
|
|
378
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
|
+
|
|
379
508
|
# Define an attribute for the model
|
|
380
509
|
#
|
|
381
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
|
|
|
@@ -345,8 +345,20 @@ module Lutaml
|
|
|
345
345
|
# @param register [Symbol, nil] The register context
|
|
346
346
|
# @return [Object] The allocated instance
|
|
347
347
|
def allocate_for_deserialization(register = nil)
|
|
348
|
-
instance = allocate
|
|
349
348
|
register_id = extract_register_id(register)
|
|
349
|
+
# Deferred imports (symbol-form import_model_attributes) must
|
|
350
|
+
# resolve before state defaults compile: the compiled seeding
|
|
351
|
+
# and the raw readers cover the post-import attribute set, and
|
|
352
|
+
# the transform's records already carry the mapping's rules
|
|
353
|
+
# for imported names — CTR's <m:br> parsed before resolution
|
|
354
|
+
# had no reader (#933). The constructor path ensures the same
|
|
355
|
+
# way (initialize_attributes). Guarded: deep object graphs
|
|
356
|
+
# (XSD schema trees) allocate recursively, and the ensure
|
|
357
|
+
# chain's frames per level pushed that recursion past the
|
|
358
|
+
# stack limit — models without pending deferred imports skip
|
|
359
|
+
# it entirely.
|
|
360
|
+
ensure_imports!(register_id) if finalized? && deferred_imports_pending?
|
|
361
|
+
instance = allocate
|
|
350
362
|
instance.finalize_deserialization(register_id)
|
|
351
363
|
instance
|
|
352
364
|
end
|
|
@@ -60,6 +60,10 @@ module Lutaml
|
|
|
60
60
|
model.attributes.each_value { |attr| define_attribute_methods(attr) }
|
|
61
61
|
@attributes.merge!(Utils.deep_dup(model.attributes))
|
|
62
62
|
@choice_attributes.concat(deep_duplicate_choice_attributes(model))
|
|
63
|
+
# Imported attributes grow the state-defaults surface (the
|
|
64
|
+
# raw readers live there) — a first use may already have
|
|
65
|
+
# compiled it for the pre-import attribute set (#933).
|
|
66
|
+
invalidate_state_defaults!
|
|
63
67
|
# Ensure @models_imported is a hash; migrate from legacy nil/false state
|
|
64
68
|
@models_imported ||= {}
|
|
65
69
|
@models_imported[reg] = true
|
|
@@ -122,6 +126,9 @@ module Lutaml
|
|
|
122
126
|
deep_duplicate_choice_attributes(model, register_id),
|
|
123
127
|
)
|
|
124
128
|
@merged_attributes_cache = nil
|
|
129
|
+
# See import_model_attributes: late-registered attributes
|
|
130
|
+
# must recompile the register's state defaults (#933).
|
|
131
|
+
invalidate_state_defaults!
|
|
125
132
|
end
|
|
126
133
|
private :register_only_import_model_attributes
|
|
127
134
|
|
|
@@ -153,6 +160,15 @@ module Lutaml
|
|
|
153
160
|
# Ensure all model imports are resolved for a specific register
|
|
154
161
|
#
|
|
155
162
|
# @param register_id [Symbol, nil] The register context
|
|
163
|
+
# Whether deferred (symbol-form) imports are still pending —
|
|
164
|
+
# gates the allocation-time ensure chain (#933).
|
|
165
|
+
def deferred_imports_pending?
|
|
166
|
+
importable_models.any? { |_method, models| models.any? } ||
|
|
167
|
+
importable_choices.any? do |_choice, imports|
|
|
168
|
+
imports.any? { |_method, models| models.any? }
|
|
169
|
+
end || restrict_attributes.any?
|
|
170
|
+
end
|
|
171
|
+
|
|
156
172
|
def ensure_model_imports!(register_id = nil)
|
|
157
173
|
register_id ||= Lutaml::Model::Config.default_register
|
|
158
174
|
@models_imported = {} if @models_imported.nil? || @models_imported == false
|
|
@@ -197,6 +213,7 @@ module Lutaml
|
|
|
197
213
|
@choices_imported[register_id] = true
|
|
198
214
|
all_resolved = true
|
|
199
215
|
|
|
216
|
+
resolved_any = false
|
|
200
217
|
importable_choices.each do |choice, choice_imports|
|
|
201
218
|
choice_imports.each do |method, models|
|
|
202
219
|
models.uniq.each do |model|
|
|
@@ -216,11 +233,19 @@ module Lutaml
|
|
|
216
233
|
end
|
|
217
234
|
|
|
218
235
|
choice.public_send(method, model_class, register_id)
|
|
236
|
+
resolved_any = true
|
|
219
237
|
end
|
|
220
238
|
end
|
|
221
239
|
end
|
|
222
240
|
|
|
223
241
|
@choices_imported[register_id] = true if all_resolved
|
|
242
|
+
# Choice imports can add attributes without passing through
|
|
243
|
+
# define_attribute_methods — recompile state defaults so the
|
|
244
|
+
# seeding and raw readers cover them (#933). Only when an
|
|
245
|
+
# import actually resolved: ensure_choice_imports! runs on
|
|
246
|
+
# every ensure_imports! (allocation-time), and a recompile
|
|
247
|
+
# per allocation would churn.
|
|
248
|
+
invalidate_state_defaults! if resolved_any
|
|
224
249
|
end
|
|
225
250
|
|
|
226
251
|
# Ensure all restrict attributes are applied
|
|
@@ -243,6 +268,11 @@ module Lutaml
|
|
|
243
268
|
# Uses TracePoint to detect when a class definition is complete,
|
|
244
269
|
# allowing deferred import resolution.
|
|
245
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
|
+
|
|
246
276
|
@trace ||= TracePoint.new(:end) do |_tp|
|
|
247
277
|
if include?(Lutaml::Model::Serialize)
|
|
248
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
|