lutaml-model 0.8.47 → 0.8.49
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/performance.yml +47 -1
- data/README.adoc +35 -0
- data/docs/_guides/index.adoc +4 -0
- data/docs/_guides/keyvalue-serialization.adoc +34 -0
- data/docs/_guides/native-engines.adoc +30 -0
- data/lib/lutaml/key_value/transform.rb +11 -4
- data/lib/lutaml/model/mapping/mapping_rule.rb +41 -4
- data/lib/lutaml/model/serialize/attribute_definition.rb +22 -0
- data/lib/lutaml/model/version.rb +1 -1
- data/lib/lutaml/xml/model_transform.rb +6 -3
- data/lib/lutaml/xml/plan_compiler.rb +8 -1
- data/spec/lutaml/xml/plan_fast_path_spec.rb +77 -0
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 2dab1dcbc2ea77cfd1c61d9ffba4717521b9e97e1f3f6c8dd6f9ce6c7014222f
|
|
4
|
+
data.tar.gz: ed16efadd94ce85b9e52a5c7de822f9c7de7e3c581203280120630e8cdb389ff
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: cd6bbdebb2c812eb849774ac168fe2c194bc21113521be6b4ca8b389ab5f6ebb2743b951478077332a18a2c214f458962c0a89d54e1a26447b3b103b7c2bb153
|
|
7
|
+
data.tar.gz: fddad44bad334a640facc5f6869b91c2a12134da6c79e58b7bed1787e14c03702d5ffd0ffcece27e435fe2f9387a7d13040e45794d178424cd7e56acbf9bb36c
|
|
@@ -55,4 +55,50 @@ jobs:
|
|
|
55
55
|
run: rake vendor:compile
|
|
56
56
|
|
|
57
57
|
- name: Run performance benchmarks
|
|
58
|
-
run: bundle exec rake performance:compare
|
|
58
|
+
run: bundle exec rake performance:compare
|
|
59
|
+
# TODO.max-perf/36: YJIT moves exactly the interpretive paths the
|
|
60
|
+
# model layer still runs. Sequential off/on probes on one runner give
|
|
61
|
+
# a directly comparable pair per commit - a measurement leg, not a
|
|
62
|
+
# gate. The plan fast path shrinks the YJIT win; watch both.
|
|
63
|
+
performance-yjit:
|
|
64
|
+
runs-on: ubuntu-latest
|
|
65
|
+
timeout-minutes: 15
|
|
66
|
+
steps:
|
|
67
|
+
- uses: actions/checkout@v6
|
|
68
|
+
with:
|
|
69
|
+
submodules: "recursive"
|
|
70
|
+
|
|
71
|
+
- name: Set up Ruby
|
|
72
|
+
uses: ruby/setup-ruby@v1
|
|
73
|
+
with:
|
|
74
|
+
ruby-version: '3.4'
|
|
75
|
+
bundler-cache: false
|
|
76
|
+
|
|
77
|
+
- name: Install ragel
|
|
78
|
+
run: sudo apt-get update && sudo apt-get install -y ragel
|
|
79
|
+
|
|
80
|
+
- name: Install ruby-ll (for rake vendor:prepare)
|
|
81
|
+
run: gem install ruby-ll --no-document
|
|
82
|
+
|
|
83
|
+
- name: Generate ragel / ruby-ll outputs in vendored forks
|
|
84
|
+
run: rake vendor:prepare
|
|
85
|
+
|
|
86
|
+
- name: Configure bundler
|
|
87
|
+
run: |
|
|
88
|
+
mkdir -p .bundle
|
|
89
|
+
echo "---" > .bundle/config
|
|
90
|
+
echo 'BUNDLE_BUILD__RUBY___LL: "--with-cflags=-std=gnu17"' >> .bundle/config
|
|
91
|
+
|
|
92
|
+
- name: Install dependencies
|
|
93
|
+
run: bundle install
|
|
94
|
+
|
|
95
|
+
- name: Compile liboga / libll native extensions
|
|
96
|
+
run: rake vendor:compile
|
|
97
|
+
|
|
98
|
+
- name: Probe YJIT off
|
|
99
|
+
run: bundle exec rake performance:probe
|
|
100
|
+
|
|
101
|
+
- name: Probe YJIT on
|
|
102
|
+
env:
|
|
103
|
+
RUBY_YJIT_ENABLE: "1"
|
|
104
|
+
run: bundle exec rake performance:probe
|
data/README.adoc
CHANGED
|
@@ -2693,6 +2693,41 @@ references:
|
|
|
2693
2693
|
====
|
|
2694
2694
|
|
|
2695
2695
|
|
|
2696
|
+
==== Attribute-value dispatch on a shared name (`when_attribute`)
|
|
2697
|
+
|
|
2698
|
+
Polymorphism selects the *class* of each item. The other dispatch axis
|
|
2699
|
+
selects *which attribute* an occurrence belongs to, keyed by a sibling
|
|
2700
|
+
value — every `<component>` below goes to a different attribute by its
|
|
2701
|
+
`type`:
|
|
2702
|
+
|
|
2703
|
+
[source,ruby]
|
|
2704
|
+
----
|
|
2705
|
+
class Requirement < Lutaml::Model::Serializable
|
|
2706
|
+
attribute :guidance, Component, collection: true
|
|
2707
|
+
attribute :purpose, Component, collection: true
|
|
2708
|
+
|
|
2709
|
+
xml do
|
|
2710
|
+
element "requirement"
|
|
2711
|
+
map_element "component", when_attribute: "type",
|
|
2712
|
+
to: { "guidance" => :guidance,
|
|
2713
|
+
"purpose" => :purpose }
|
|
2714
|
+
end
|
|
2715
|
+
end
|
|
2716
|
+
----
|
|
2717
|
+
|
|
2718
|
+
The same declaration works for key-value formats (`json`/`yaml`/`toml`/
|
|
2719
|
+
`hsh` blocks use `map`), where the discriminator is an object key's
|
|
2720
|
+
value. Every occurrence is captured exactly once, the discriminator is
|
|
2721
|
+
re-emitted on serialization, `unmatched: :raise` fails the parse on
|
|
2722
|
+
values no rule claims, and `ordered` XML mappings round-trip the
|
|
2723
|
+
original interleaving. The two axes compose on one rule — partition by
|
|
2724
|
+
value while dispatching classes with `polymorphic`.
|
|
2725
|
+
|
|
2726
|
+
See
|
|
2727
|
+
link:docs/_guides/advanced-mapping.adoc[the advanced mapping guide]
|
|
2728
|
+
for the full semantics.
|
|
2729
|
+
|
|
2730
|
+
|
|
2696
2731
|
|
|
2697
2732
|
=== Union attributes
|
|
2698
2733
|
|
data/docs/_guides/index.adoc
CHANGED
|
@@ -23,6 +23,10 @@ Task-oriented guides for accomplishing specific goals with Lutaml::Model.
|
|
|
23
23
|
* link:../keyvalue-serialization[Key-Value Serialization] - JSON/YAML/TOML/Hash
|
|
24
24
|
* link:../collection-serialization[Collection Serialization] - JSONL and YAML Stream
|
|
25
25
|
|
|
26
|
+
== Advanced mapping
|
|
27
|
+
|
|
28
|
+
* link:../advanced-mapping[Advanced Attribute Mapping] - Same-name dispatch on attribute values (`when_attribute`), grouped declarations, custom methods, delegation
|
|
29
|
+
|
|
26
30
|
== Performance
|
|
27
31
|
|
|
28
32
|
* link:../native-engines[Native engines (leptris / yeptris / teptris)] - Drop-in XML/YAML/JSON/TOML engine upgrades via bundle opt-in
|
|
@@ -116,6 +116,40 @@ desc: A ceramic with a navy blue color and clear glaze.
|
|
|
116
116
|
----
|
|
117
117
|
====
|
|
118
118
|
|
|
119
|
+
=== Dispatching a shared key by a discriminator value
|
|
120
|
+
|
|
121
|
+
When one wire key holds a list of objects distinguished by a sibling
|
|
122
|
+
key's value, `when_attribute` routes each item to its own attribute —
|
|
123
|
+
one declaration covers the whole partition:
|
|
124
|
+
|
|
125
|
+
[source,ruby]
|
|
126
|
+
----
|
|
127
|
+
class Requirement < Lutaml::Model::Serializable
|
|
128
|
+
attribute :guidance, Component, collection: true
|
|
129
|
+
attribute :purpose, Component, collection: true
|
|
130
|
+
|
|
131
|
+
json do
|
|
132
|
+
map "component", when_attribute: "type",
|
|
133
|
+
to: { "guidance" => :guidance, "purpose" => :purpose }
|
|
134
|
+
end
|
|
135
|
+
end
|
|
136
|
+
----
|
|
137
|
+
|
|
138
|
+
[source,json]
|
|
139
|
+
----
|
|
140
|
+
{ "component": [
|
|
141
|
+
{ "type": "guidance", "text": "g1" },
|
|
142
|
+
{ "type": "purpose", "text": "p1" }
|
|
143
|
+
] }
|
|
144
|
+
----
|
|
145
|
+
|
|
146
|
+
`guidance` holds `g1`, `purpose` holds `p1`. Serialization merges the
|
|
147
|
+
items back under the one key, each stamped with its discriminator.
|
|
148
|
+
`unmatched: :raise` fails the parse on items no rule claims. Works for
|
|
149
|
+
all key-value formats. See
|
|
150
|
+
link:advanced-mapping.adoc[the advanced mapping guide] for the full
|
|
151
|
+
semantics, including the per-rule form.
|
|
152
|
+
|
|
119
153
|
=== Specific format mappings
|
|
120
154
|
|
|
121
155
|
Specific key value formats can be mapping independently of other formats.
|
|
@@ -31,6 +31,36 @@ leptris and yeptris ship prebuilt platform gems for Linux and macOS
|
|
|
31
31
|
every platform, including Windows. When a gem is absent, lutaml-model
|
|
32
32
|
automatically falls back to the standard engine for that format.
|
|
33
33
|
|
|
34
|
+
== YJIT doubles the model pipeline
|
|
35
|
+
|
|
36
|
+
The model layer's own work — mapping dispatch, casts, hydration — is
|
|
37
|
+
plain Ruby and exactly the code shape YJIT compiles well; the native
|
|
38
|
+
engines' C work is unaffected either way. On a controlled runner
|
|
39
|
+
(ubuntu-latest, Ruby 3.4, the repository's performance probe):
|
|
40
|
+
|
|
41
|
+
|===
|
|
42
|
+
| op | YJIT off | YJIT on | change
|
|
43
|
+
|
|
44
|
+
| from_yaml | 75.1 i/s | 129.7 i/s | +73%
|
|
45
|
+
| to_yaml | 67.3 i/s | 145.1 i/s | +116%
|
|
46
|
+
| from_json | 73.2 i/s | 128.4 i/s | +75%
|
|
47
|
+
| to_json | 115.4 i/s | 255.5 i/s | +121%
|
|
48
|
+
|===
|
|
49
|
+
|
|
50
|
+
Enable it like any Ruby application does — this is the application's
|
|
51
|
+
call, never a gem's:
|
|
52
|
+
|
|
53
|
+
[source,sh]
|
|
54
|
+
----
|
|
55
|
+
RUBY_YJIT_ENABLE=1 ruby app.rb
|
|
56
|
+
# or in code, before heavy work:
|
|
57
|
+
RubyVM::YJIT.enable
|
|
58
|
+
----
|
|
59
|
+
|
|
60
|
+
The opt-in XML plan fast path (see below) moves work out of Ruby and
|
|
61
|
+
into the engine, which shrinks the YJIT win — enabling both is still
|
|
62
|
+
net faster than either alone.
|
|
63
|
+
|
|
34
64
|
== Selecting adapters explicitly
|
|
35
65
|
|
|
36
66
|
The engines register as ordinary adapter types, so explicit selection and
|
|
@@ -5,9 +5,12 @@ module Lutaml
|
|
|
5
5
|
# Use child's own default register if it has one
|
|
6
6
|
# This ensures versioned schemas (e.g., MML v2 with lutaml_default_register = :mml_v2)
|
|
7
7
|
# are instantiated with their native context
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
8
|
+
# TODO.max-perf/32: constant per (model class, register) — the
|
|
9
|
+
# transform itself is cached per that pair, so resolve once.
|
|
10
|
+
child_register = @kv_child_register ||= Lutaml::Model::Register
|
|
11
|
+
.resolve_for_child(
|
|
12
|
+
model_class, lutaml_register
|
|
13
|
+
)
|
|
11
14
|
|
|
12
15
|
if model_class.include?(Lutaml::Model::Serialize)
|
|
13
16
|
instance = model_class.new(lutaml_register: child_register)
|
|
@@ -449,7 +452,7 @@ partition = nil)
|
|
|
449
452
|
if partitioned.nil? && (plan = kv_rule_plan(format, rule, attr)) &&
|
|
450
453
|
(value = kv_fast_extract(doc, plan))
|
|
451
454
|
rule.deserialize(instance, kv_fast_cast(value, plan, instance),
|
|
452
|
-
attributes, self, options[:context])
|
|
455
|
+
attributes, self, options[:context], pre_cast: true)
|
|
453
456
|
return
|
|
454
457
|
end
|
|
455
458
|
|
|
@@ -485,6 +488,10 @@ partition = nil)
|
|
|
485
488
|
if attr.collection? || !instance.is_a?(Lutaml::Model::Serialize)
|
|
486
489
|
attr.valid_collection!(value, context)
|
|
487
490
|
end
|
|
491
|
+
# No pre_cast here: the interpretive attr.cast is not the full
|
|
492
|
+
# cast chain — whole-value Type policies (e.g. a custom
|
|
493
|
+
# Type::Value that receives the whole hash) are shaped by the
|
|
494
|
+
# setter's cast_value, which must still run.
|
|
488
495
|
rule.deserialize(instance, value, attributes, self,
|
|
489
496
|
options[:context])
|
|
490
497
|
end
|
|
@@ -324,13 +324,15 @@ module Lutaml
|
|
|
324
324
|
end
|
|
325
325
|
|
|
326
326
|
def deserialize(model, value, attributes, mapper_class = nil,
|
|
327
|
-
context = nil)
|
|
327
|
+
context = nil, pre_cast: false)
|
|
328
328
|
if @needs_full_deserialize
|
|
329
329
|
handle_custom_method(model, value, mapper_class, context) ||
|
|
330
330
|
handle_delegate(model, value, attributes) ||
|
|
331
|
-
handle_transform_method(model, value, attributes, context
|
|
331
|
+
handle_transform_method(model, value, attributes, context,
|
|
332
|
+
pre_cast: pre_cast)
|
|
332
333
|
else
|
|
333
|
-
handle_transform_method(model, value, attributes, context
|
|
334
|
+
handle_transform_method(model, value, attributes, context,
|
|
335
|
+
pre_cast: pre_cast)
|
|
334
336
|
end
|
|
335
337
|
end
|
|
336
338
|
|
|
@@ -446,6 +448,24 @@ context = nil)
|
|
|
446
448
|
# :import — hash/proc transformers go through ImportTransformer.
|
|
447
449
|
TRANSFORM_DISPATCH = {}.compare_by_identity
|
|
448
450
|
|
|
451
|
+
def self.parsed_assign_writer(model_class, target)
|
|
452
|
+
key = [model_class, target]
|
|
453
|
+
writer = PARSED_ASSIGN_WRITERS[key]
|
|
454
|
+
return writer unless writer.nil?
|
|
455
|
+
|
|
456
|
+
name = :"__assign_parsed_#{target}="
|
|
457
|
+
writer = model_class.method_defined?(name) ? name : false
|
|
458
|
+
PARSED_ASSIGN_WRITERS[key] = writer
|
|
459
|
+
writer
|
|
460
|
+
end
|
|
461
|
+
|
|
462
|
+
# [model class, target] -> the parsed-assign writer compiled for
|
|
463
|
+
# that class's attribute, or nil when only a casting writer
|
|
464
|
+
# exists (custom writers, reflective names, enum shorthands).
|
|
465
|
+
# Class-level because mapping rules may be frozen (see the
|
|
466
|
+
# name-string note above TRANSFORM_DISPATCH).
|
|
467
|
+
PARSED_ASSIGN_WRITERS = {}.compare_by_identity
|
|
468
|
+
|
|
449
469
|
def self.transform_dispatch(rule, attr)
|
|
450
470
|
per_attr = TRANSFORM_DISPATCH[rule]
|
|
451
471
|
if per_attr.nil?
|
|
@@ -556,7 +576,8 @@ context = nil)
|
|
|
556
576
|
attributes[delegate].type(model.lutaml_register).new)
|
|
557
577
|
end
|
|
558
578
|
|
|
559
|
-
def handle_transform_method(model, value, attributes, context = nil
|
|
579
|
+
def handle_transform_method(model, value, attributes, context = nil,
|
|
580
|
+
pre_cast: false)
|
|
560
581
|
attr = attributes[to]
|
|
561
582
|
# The transform verdict (none / class-based already applied /
|
|
562
583
|
# hash-proc via ImportTransformer) is static per (rule, attr) —
|
|
@@ -566,12 +587,28 @@ context = nil)
|
|
|
566
587
|
transformed = ImportTransformer.call(value, self, attr,
|
|
567
588
|
context: context)
|
|
568
589
|
assign_value(model, transformed)
|
|
590
|
+
elsif pre_cast && collection_shaped?(value, attr) &&
|
|
591
|
+
(writer = self.class.parsed_assign_writer(model.class, to))
|
|
592
|
+
# TODO.max-perf/31: the deserializing transform already cast
|
|
593
|
+
# the value through the format-aware path — assign without
|
|
594
|
+
# repeating the cast. Custom writers and reflective names
|
|
595
|
+
# resolve nil here and keep the casting setter.
|
|
596
|
+
model.public_send(writer, value)
|
|
569
597
|
else
|
|
570
598
|
assign_value(model, value)
|
|
571
599
|
end
|
|
572
600
|
true
|
|
573
601
|
end
|
|
574
602
|
|
|
603
|
+
# Whether the value already carries the attribute's final shape:
|
|
604
|
+
# a bare single occurrence still owes the setter's one-element
|
|
605
|
+
# collection coercion, so only collection instances (and every
|
|
606
|
+
# singular value, whose cast is complete) take the no-recast lane.
|
|
607
|
+
def collection_shaped?(value, attr)
|
|
608
|
+
attr.nil? || !attr.collection? || value.is_a?(::Array) ||
|
|
609
|
+
value.is_a?(Lutaml::Model::Collection)
|
|
610
|
+
end
|
|
611
|
+
|
|
575
612
|
def assign_value(model, value)
|
|
576
613
|
model.public_send(@setter_name, value)
|
|
577
614
|
end
|
|
@@ -297,6 +297,21 @@ module Lutaml
|
|
|
297
297
|
end
|
|
298
298
|
record_mutation_collection(:#{name}, value)
|
|
299
299
|
end
|
|
300
|
+
|
|
301
|
+
# TODO.max-perf/31: the deserialization transforms already
|
|
302
|
+
# cast through the format-aware path; this writer skips the
|
|
303
|
+
# setter's re-cast while keeping value_set_for marking and
|
|
304
|
+
# mutation recording. Public assignment keeps the caster.
|
|
305
|
+
def __assign_parsed_#{name}=(value)
|
|
306
|
+
value_set_for(:#{name})
|
|
307
|
+
current = @#{name}
|
|
308
|
+
if current.equal?(Lutaml::Model::Serialize::LAZY_EMPTY_COLLECTION) &&
|
|
309
|
+
(value.nil? || Lutaml::Model::Utils.uninitialized?(value))
|
|
310
|
+
else
|
|
311
|
+
@#{name} = value
|
|
312
|
+
end
|
|
313
|
+
record_mutation_collection(:#{name}, value)
|
|
314
|
+
end
|
|
300
315
|
RUBY
|
|
301
316
|
else
|
|
302
317
|
class_eval(<<~RUBY, __FILE__, __LINE__ + 1) # rubocop:disable Style/DocumentDynamicEvalDefinition
|
|
@@ -306,6 +321,13 @@ module Lutaml
|
|
|
306
321
|
@#{name} = value
|
|
307
322
|
record_mutation(:#{name}, value)
|
|
308
323
|
end
|
|
324
|
+
|
|
325
|
+
# TODO.max-perf/31: see the collection branch above.
|
|
326
|
+
def __assign_parsed_#{name}=(value)
|
|
327
|
+
value_set_for(:#{name})
|
|
328
|
+
@#{name} = value
|
|
329
|
+
record_mutation(:#{name}, value)
|
|
330
|
+
end
|
|
309
331
|
RUBY
|
|
310
332
|
end
|
|
311
333
|
end
|
data/lib/lutaml/model/version.rb
CHANGED
|
@@ -48,9 +48,12 @@ module Lutaml
|
|
|
48
48
|
# Use child's own default register if it has one
|
|
49
49
|
# This ensures versioned schemas (e.g., MML v2 with lutaml_default_register = :mml_v2)
|
|
50
50
|
# are instantiated with their native context
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
51
|
+
# TODO.max-perf/32: constant per (model class, register) — the
|
|
52
|
+
# transform itself is cached per that pair, so resolve once.
|
|
53
|
+
child_register = @xml_child_register ||= Lutaml::Model::Register
|
|
54
|
+
.resolve_for_child(
|
|
55
|
+
model_class, lutaml_register
|
|
56
|
+
)
|
|
54
57
|
|
|
55
58
|
instance_is_serialize = model_class.include?(::Lutaml::Model::Serialize)
|
|
56
59
|
if instance_is_serialize
|
|
@@ -200,7 +200,14 @@ module Lutaml
|
|
|
200
200
|
|
|
201
201
|
def compilable_mapping?(mapping)
|
|
202
202
|
mapping.root_element &&
|
|
203
|
-
!(mapping.respond_to?(:root_mappings) && mapping.root_mappings)
|
|
203
|
+
!(mapping.respond_to?(:root_mappings) && mapping.root_mappings) &&
|
|
204
|
+
# lutaml-model#88: when_attribute partitions route same-name
|
|
205
|
+
# occurrences by attribute value — name-keyed plan rows would
|
|
206
|
+
# hydrate every occurrence into EVERY partition attribute
|
|
207
|
+
# (verified double-capture under the flag). The interpretive
|
|
208
|
+
# filter is the only correct path until the descriptor ABI
|
|
209
|
+
# grows row predicates.
|
|
210
|
+
mapping.mappings.none?(&:when_attribute?)
|
|
204
211
|
end
|
|
205
212
|
|
|
206
213
|
# Attribute for a rule — delegate rules resolve against their
|
|
@@ -517,6 +517,83 @@ RSpec.describe "XML plan fast path" do
|
|
|
517
517
|
expect(model.to_xml).to eq(interpretive)
|
|
518
518
|
end
|
|
519
519
|
|
|
520
|
+
# lutaml-model#88: name-keyed plan rows would hydrate every
|
|
521
|
+
# same-name occurrence into EVERY partition attribute (verified
|
|
522
|
+
# double-capture) — discriminator models must take the interpretive
|
|
523
|
+
# path, and a partitioned CHILD model must opt its parent out too.
|
|
524
|
+
it "falls back to the interpretive path for when_attribute models" do
|
|
525
|
+
component = Class.new(Lutaml::Model::Serializable) do
|
|
526
|
+
attribute :text, :string
|
|
527
|
+
|
|
528
|
+
xml do
|
|
529
|
+
element "component"
|
|
530
|
+
map_element "text", to: :text
|
|
531
|
+
end
|
|
532
|
+
end
|
|
533
|
+
stub_const("PlanFastPath::Component", component)
|
|
534
|
+
|
|
535
|
+
partitioned = Class.new(Lutaml::Model::Serializable) do
|
|
536
|
+
attribute :guidance, component, collection: true
|
|
537
|
+
attribute :purpose, component, collection: true
|
|
538
|
+
|
|
539
|
+
xml do
|
|
540
|
+
element "requirement"
|
|
541
|
+
map_element "component", when_attribute: "type",
|
|
542
|
+
to: { "guidance" => :guidance,
|
|
543
|
+
"purpose" => :purpose }
|
|
544
|
+
end
|
|
545
|
+
end
|
|
546
|
+
stub_const("PlanFastPath::Partitioned", partitioned)
|
|
547
|
+
|
|
548
|
+
req = partitioned.from_xml(<<~XML)
|
|
549
|
+
<requirement>
|
|
550
|
+
<component type="guidance"><text>g1</text></component>
|
|
551
|
+
<component type="purpose"><text>p1</text></component>
|
|
552
|
+
<component type="guidance"><text>g2</text></component>
|
|
553
|
+
</requirement>
|
|
554
|
+
XML
|
|
555
|
+
|
|
556
|
+
expect(req.guidance.map(&:text)).to eq(%w[g1 g2])
|
|
557
|
+
expect(req.purpose.map(&:text)).to eq(["p1"])
|
|
558
|
+
|
|
559
|
+
expect(Lutaml::Xml::PlanCompiler.compile(partitioned,
|
|
560
|
+
:default)).to be_nil
|
|
561
|
+
end
|
|
562
|
+
|
|
563
|
+
it "opts a parent out when a child model partitions with when_attribute" do
|
|
564
|
+
component = Class.new(Lutaml::Model::Serializable) do
|
|
565
|
+
attribute :text, :string
|
|
566
|
+
|
|
567
|
+
xml do
|
|
568
|
+
element "component"
|
|
569
|
+
map_element "text", to: :text
|
|
570
|
+
end
|
|
571
|
+
end
|
|
572
|
+
inner = Class.new(Lutaml::Model::Serializable) do
|
|
573
|
+
attribute :guidance, component, collection: true
|
|
574
|
+
|
|
575
|
+
xml do
|
|
576
|
+
element "req"
|
|
577
|
+
map_element "component", to: :guidance,
|
|
578
|
+
when_attribute: { "type" => "guidance" }
|
|
579
|
+
end
|
|
580
|
+
end
|
|
581
|
+
outer = Class.new(Lutaml::Model::Serializable) do
|
|
582
|
+
attribute :req, inner
|
|
583
|
+
|
|
584
|
+
xml do
|
|
585
|
+
element "holder"
|
|
586
|
+
map_element "req", to: :req
|
|
587
|
+
end
|
|
588
|
+
end
|
|
589
|
+
stub_const("PlanFastPath::Nested", outer)
|
|
590
|
+
|
|
591
|
+
doc = outer.from_xml(
|
|
592
|
+
"<holder><req><component type=\"guidance\"><text>g1</text></component></req></holder>",
|
|
593
|
+
)
|
|
594
|
+
expect(doc.req.guidance.map(&:text)).to eq(["g1"])
|
|
595
|
+
end
|
|
596
|
+
|
|
520
597
|
it "round-trips through both fast paths" do
|
|
521
598
|
item = Class.new(Lutaml::Model::Serializable) do
|
|
522
599
|
attribute :id, :integer
|