lutaml-model 0.8.47 → 0.8.48
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/README.adoc +35 -0
- data/docs/_guides/index.adoc +4 -0
- data/docs/_guides/keyvalue-serialization.adoc +34 -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
- 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: a482362c928135d0733413ae200b20e5a9a7f05af8faf57696bc4461b093d691
|
|
4
|
+
data.tar.gz: 3b22e8a6881ad4a832d90a6b064d6999a5e5b8a969d507fd467d44c66f7c0279
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: a6047ddb9f43fa9a7012e794070891a0d997ca261f2c76f324223a1c1414bab39f1f05b5f627ecde1e21a083b971c4cc002d15928db410e97411b5dcca9a5b97
|
|
7
|
+
data.tar.gz: b849ae1daa34c19f6f570d1389a2ea43cf3af5ffba5db32bdef33e53a0b7ba960736b511ecfc5f520a926690134a4247227b1fa7a437923a8774a511569f6e88
|
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.
|
|
@@ -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
|