lutaml-model 0.8.123 → 0.8.125
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/lib/compat/opal/lutaml_model_boot.rb +2 -0
- data/lib/lutaml/key_value/transform.rb +76 -30
- data/lib/lutaml/model/error/unsupported_version_error.rb +14 -0
- data/lib/lutaml/model/evolution_steps.rb +85 -0
- data/lib/lutaml/model/serialize/attribute_definition.rb +161 -0
- data/lib/lutaml/model/serialize.rb +12 -0
- data/lib/lutaml/model/union.rb +17 -1
- data/lib/lutaml/model/version.rb +1 -1
- data/lib/lutaml/model.rb +3 -0
- data/spec/lutaml/model/child_mappings_absent_key_spec.rb +53 -0
- data/spec/lutaml/model/evolution_spec.rb +204 -0
- data/spec/lutaml/model/union_attribute_spec.rb +15 -2
- data/spec/lutaml/model/union_lenient_model_spec.rb +78 -0
- metadata +6 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: bd52cd1acd33af2b1f8abe39a9d57c4412618d63ee65b516204ef4fa9f7ff047
|
|
4
|
+
data.tar.gz: 903e6d8ed42b95b0c5f79616f362d856b79ef54c90b3bbc1ba854c3345e79bd2
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 125f630b5d0816ad0dbbf98e1650f9a221a19c83793fe05c38e384fa884ec251774e97b0ce9636be1a014f84f678fc2c4d42d9cbea108fc615051fbd1bf4112c
|
|
7
|
+
data.tar.gz: 9eec80327bca08b7cc3bb649c77c1b956356baa190d434ae86c5eebacaacfda95c5e2ed2392d6c1d2db7a98fa4a7e32d6d416eb74185d5f0ba8ba416cef66d2a
|
|
@@ -59,6 +59,7 @@ require "lutaml/model/context_registry"
|
|
|
59
59
|
require "lutaml/model/deep_dupable"
|
|
60
60
|
require "lutaml/model/error"
|
|
61
61
|
require "lutaml/model/errors"
|
|
62
|
+
require "lutaml/model/evolution_steps"
|
|
62
63
|
require "lutaml/model/finalization_cache"
|
|
63
64
|
require "lutaml/model/format_registry"
|
|
64
65
|
require "lutaml/model/global_context"
|
|
@@ -283,6 +284,7 @@ require "lutaml/model/error/unknown_discriminator_error"
|
|
|
283
284
|
require "lutaml/model/error/unknown_sequence_mapping_error"
|
|
284
285
|
require "lutaml/model/error/unknown_type_error"
|
|
285
286
|
require "lutaml/model/error/unresolvable_type_error"
|
|
287
|
+
require "lutaml/model/error/unsupported_version_error"
|
|
286
288
|
require "lutaml/model/error/validation_error"
|
|
287
289
|
require "lutaml/model/error/validation_failed_error"
|
|
288
290
|
require "lutaml/model/liquid/indexed_access"
|
|
@@ -165,14 +165,15 @@ module Lutaml
|
|
|
165
165
|
Concurrent::Map.new
|
|
166
166
|
end
|
|
167
167
|
|
|
168
|
-
# [model class, register, format,
|
|
169
|
-
#
|
|
170
|
-
#
|
|
171
|
-
#
|
|
172
|
-
#
|
|
173
|
-
#
|
|
174
|
-
#
|
|
175
|
-
|
|
168
|
+
# [model class, register, format, generation] -> { rule => plan }.
|
|
169
|
+
# Class-level, generation-retired: the whole dispatch table for a
|
|
170
|
+
# model mapping compiles once — kind (scalar/custom/hash_map/
|
|
171
|
+
# interpretive/skip/invalid), target attribute and validity — so
|
|
172
|
+
# process_mapping_rule stops re-probing rule predicates at every
|
|
173
|
+
# node (~12 predicate calls plus a key array per rule per node;
|
|
174
|
+
# 154k+154k allocations per heroku.json parse). Same
|
|
175
|
+
# idempotent-write pattern as KV_GROUP_PLANS.
|
|
176
|
+
KV_WALK_PLANS = if Lutaml::Model.opal?
|
|
176
177
|
{}
|
|
177
178
|
else
|
|
178
179
|
Lutaml::Model::RuntimeCompatibility
|
|
@@ -717,18 +718,20 @@ format)
|
|
|
717
718
|
|
|
718
719
|
def process_mapping_rule(doc, instance, format, rule, options = {},
|
|
719
720
|
partition = nil)
|
|
720
|
-
|
|
721
|
-
|
|
721
|
+
plans = walk_plans_for(format)
|
|
722
|
+
plan = plans[rule] ||= build_walk_plan(format, rule)
|
|
723
|
+
kind = plan.kind
|
|
724
|
+
return if kind == :skip
|
|
722
725
|
|
|
723
|
-
raise "Attribute '#{rule.to}' not found in #{self}" unless
|
|
724
|
-
|
|
725
|
-
|
|
726
|
+
raise "Attribute '#{rule.to}' not found in #{self}" unless plan.valid
|
|
727
|
+
|
|
728
|
+
attr = plan.attr
|
|
726
729
|
|
|
727
730
|
# lutaml-model#88: discriminator rules (and plain rules on a
|
|
728
731
|
# partitioned key) must filter the raw value, so they keep the
|
|
729
732
|
# interpretive path.
|
|
730
733
|
partitioned = partition && kv_partition_group(rule, partition)
|
|
731
|
-
if partitioned.nil? &&
|
|
734
|
+
if partitioned.nil? && kind == :scalar &&
|
|
732
735
|
(value = kv_fast_extract(doc, plan))
|
|
733
736
|
rule.deserialize(instance, kv_fast_cast(value, plan, instance),
|
|
734
737
|
attributes, self, options[:context], pre_cast: true)
|
|
@@ -742,20 +745,28 @@ partition = nil)
|
|
|
742
745
|
end
|
|
743
746
|
value = apply_value_map(value, rule.value_map(:from, options), attr)
|
|
744
747
|
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
return
|
|
748
|
+
# An empty value ("", [], {}) is a present value per the
|
|
749
|
+
# missing-values semantics and must reach the custom method;
|
|
750
|
+
# only nil (non-existent) skips it (lutaml-model#746).
|
|
751
|
+
if (kind == :custom) && value.nil?
|
|
752
|
+
return
|
|
753
|
+
end
|
|
750
754
|
|
|
755
|
+
if kind == :custom
|
|
751
756
|
return rule.deserialize(instance, value, attributes, model_class,
|
|
752
757
|
options[:context])
|
|
753
758
|
end
|
|
754
759
|
|
|
755
760
|
value = rule.transform_value(attr, value, :from, format)
|
|
761
|
+
# Hash/child-mapping rules translate a hash document; an absent
|
|
762
|
+
# or explicit-null key has nothing to translate — the attribute
|
|
763
|
+
# keeps its uninitialized/default state (a nil here previously
|
|
764
|
+
# crashed translate_mappings).
|
|
765
|
+
return if value.nil? && kind == :hash_map
|
|
766
|
+
|
|
756
767
|
value = translate_mappings(value, rule.hash_mappings, attr, format,
|
|
757
768
|
instance)
|
|
758
|
-
unless
|
|
769
|
+
unless kind == :hash_map
|
|
759
770
|
value = cast_value(value, attr, format, rule,
|
|
760
771
|
instance)
|
|
761
772
|
end
|
|
@@ -786,7 +797,8 @@ partition = nil)
|
|
|
786
797
|
# Guard + cached target — an inline cache per rule. Complex rules
|
|
787
798
|
# (custom methods, transforms, value maps, delegates, hash mappings,
|
|
788
799
|
# unions, polymorphism) return nil and keep the interpretive path.
|
|
789
|
-
|
|
800
|
+
WalkPlan = ::Struct.new(:kind, :attr, :valid, :wire, :klass,
|
|
801
|
+
:collection)
|
|
790
802
|
|
|
791
803
|
# Memoized (per transform) wire name for plain rules — the from
|
|
792
804
|
# spelling never changes after definition.
|
|
@@ -808,15 +820,48 @@ partition = nil)
|
|
|
808
820
|
@kv_serialize_plans[rule] ||= KvSerializePlan.new(rule_from_name(rule))
|
|
809
821
|
end
|
|
810
822
|
|
|
811
|
-
def
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
823
|
+
def walk_plans_for(format)
|
|
824
|
+
generation = Lutaml::Model::GlobalContext.context_generation
|
|
825
|
+
if @lutaml_walk_plans.nil? || @lutaml_walk_generation != generation ||
|
|
826
|
+
@lutaml_walk_format != format
|
|
827
|
+
key = [model_class, lutaml_register, format, generation]
|
|
828
|
+
@lutaml_walk_plans =
|
|
829
|
+
(KV_WALK_PLANS[key] ||= build_walk_plans(format))
|
|
830
|
+
@lutaml_walk_generation = generation
|
|
831
|
+
@lutaml_walk_format = format
|
|
832
|
+
end
|
|
833
|
+
@lutaml_walk_plans
|
|
834
|
+
end
|
|
835
|
+
|
|
836
|
+
def build_walk_plans(format)
|
|
837
|
+
plans = {}.compare_by_identity
|
|
838
|
+
mappings_for(format).mappings(lutaml_register).each do |rule|
|
|
839
|
+
plans[rule] = build_walk_plan(format, rule)
|
|
840
|
+
end
|
|
841
|
+
plans
|
|
842
|
+
end
|
|
843
|
+
|
|
844
|
+
# The per-rule dispatch compile. Kind precedence mirrors the
|
|
845
|
+
# interpretive method exactly: skip (derived) > invalid >
|
|
846
|
+
# custom (deserialization custom method) > hash_map
|
|
847
|
+
# (hash_mappings truthy — translate, never cast) > scalar (the
|
|
848
|
+
# fast-extract eligibility) > interpretive.
|
|
849
|
+
def build_walk_plan(format, rule, _attrs = nil)
|
|
850
|
+
attr = attribute_for_rule(rule)
|
|
851
|
+
return WalkPlan.new(:skip, nil, false, nil, nil, false) if attr&.derived?
|
|
852
|
+
|
|
853
|
+
valid = valid_rule?(rule, attr)
|
|
854
|
+
kind =
|
|
855
|
+
if rule.has_custom_method_for_deserialization?
|
|
856
|
+
:custom
|
|
857
|
+
elsif rule.hash_mappings
|
|
858
|
+
:hash_map
|
|
859
|
+
else
|
|
860
|
+
plan = build_kv_rule_plan(format, rule, attr)
|
|
861
|
+
plan ? :scalar : :interpretive
|
|
862
|
+
end
|
|
816
863
|
|
|
817
|
-
plan
|
|
818
|
-
KV_RULE_PLANS[key] = plan || false
|
|
819
|
-
plan
|
|
864
|
+
plan || WalkPlan.new(kind, attr, valid, nil, nil, false)
|
|
820
865
|
end
|
|
821
866
|
|
|
822
867
|
# `rule.polymorphic` defaults to an empty Hash, which is truthy —
|
|
@@ -843,7 +888,8 @@ partition = nil)
|
|
|
843
888
|
!attr.value_policy.whole_value?(type) &&
|
|
844
889
|
!Lutaml::Model::Attribute.custom_from_probe?(type)
|
|
845
890
|
|
|
846
|
-
|
|
891
|
+
WalkPlan.new(:scalar, attr, true, rule.name.to_s, type,
|
|
892
|
+
attr.collection?)
|
|
847
893
|
end
|
|
848
894
|
|
|
849
895
|
# Extraction fast path: plain hash fetch for a present, non-blank
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
module Lutaml
|
|
2
|
+
module Model
|
|
3
|
+
# Raised when version detection reports a model version that no class
|
|
4
|
+
# in the detected model's family declares (#265).
|
|
5
|
+
class UnsupportedVersionError < Error
|
|
6
|
+
def initialize(model_class, version)
|
|
7
|
+
super(
|
|
8
|
+
"no model in the #{model_class} family declares version " \
|
|
9
|
+
"#{version.inspect}",
|
|
10
|
+
)
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
end
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Lutaml
|
|
4
|
+
module Model
|
|
5
|
+
# Step collector for the `evolution from:` DSL (#265). Each declared
|
|
6
|
+
# step is a closure applied in declaration order to the source
|
|
7
|
+
# document's wire hash; the evolved hash is then parsed by the
|
|
8
|
+
# target model. All steps are wire-level: they relocate, merge and
|
|
9
|
+
# drop keys of the old document — the model pipeline never sees a
|
|
10
|
+
# half-migrated shape.
|
|
11
|
+
class EvolutionSteps
|
|
12
|
+
def initialize
|
|
13
|
+
@steps = []
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
attr_reader :steps
|
|
17
|
+
|
|
18
|
+
# Accept a document written with a former wire name for the
|
|
19
|
+
# current attribute. The current name wins when both appear.
|
|
20
|
+
def wire_rename(from_key, to:)
|
|
21
|
+
from_key = from_key.to_s
|
|
22
|
+
to = to.to_s
|
|
23
|
+
@steps << lambda do |hash|
|
|
24
|
+
next hash unless hash.key?(from_key)
|
|
25
|
+
|
|
26
|
+
hash[to] = hash.delete(from_key) unless hash.key?(to)
|
|
27
|
+
hash
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# Relocate a former top-level key into a nested position, given
|
|
32
|
+
# as a dot path ("metadata.name") or an array of segments.
|
|
33
|
+
# Intermediate objects are created as needed.
|
|
34
|
+
def wire_move(from_key, to:)
|
|
35
|
+
path = to.is_a?(::Array) ? to.map(&:to_s) : to.split(".").map(&:to_s)
|
|
36
|
+
from_key = from_key.to_s
|
|
37
|
+
@steps << lambda do |hash|
|
|
38
|
+
value = hash.delete(from_key)
|
|
39
|
+
next hash if value.nil?
|
|
40
|
+
|
|
41
|
+
target = hash
|
|
42
|
+
path[0...-1].each do |segment|
|
|
43
|
+
target = (target[segment] ||= {})
|
|
44
|
+
end
|
|
45
|
+
target[path[-1]] = value
|
|
46
|
+
hash
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# Combine former keys into one. With `with:`, it receives the
|
|
51
|
+
# source values in declared order and returns the new value;
|
|
52
|
+
# without it, the sources must all be hashes and are merged in
|
|
53
|
+
# declared order.
|
|
54
|
+
def wire_merge(from_keys, to:, with: nil)
|
|
55
|
+
keys = from_keys.map(&:to_s)
|
|
56
|
+
to = to.to_s
|
|
57
|
+
@steps << lambda do |hash|
|
|
58
|
+
values = keys.map { |key| hash.delete(key) }
|
|
59
|
+
hash[to] =
|
|
60
|
+
if with
|
|
61
|
+
with.call(*values)
|
|
62
|
+
elsif values.all?(::Hash)
|
|
63
|
+
values.reduce { |merged, value| merged.merge(value) }
|
|
64
|
+
else
|
|
65
|
+
raise ArgumentError,
|
|
66
|
+
"wire_merge requires with: when the merged values " \
|
|
67
|
+
"are not all hashes"
|
|
68
|
+
end
|
|
69
|
+
hash
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
# Retire former keys explicitly. Documentation as much as
|
|
74
|
+
# behavior: an undeclared unknown key keeps the map_unmapped /
|
|
75
|
+
# drop semantics; a declared one states the vocabulary removed it.
|
|
76
|
+
def wire_drop(*keys)
|
|
77
|
+
dropped = keys.map(&:to_s)
|
|
78
|
+
@steps << lambda do |hash|
|
|
79
|
+
dropped.each { |key| hash.delete(key) }
|
|
80
|
+
hash
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
end
|
|
@@ -521,6 +521,167 @@ module Lutaml
|
|
|
521
521
|
end
|
|
522
522
|
end
|
|
523
523
|
|
|
524
|
+
# Model evolution (#265): `version 1` (or a semver string)
|
|
525
|
+
# stamps the model schema version. Subclasses inherit the
|
|
526
|
+
# nearest ancestor's version; a family is the ancestor chain of
|
|
527
|
+
# version-declared classes, and detection routes within it.
|
|
528
|
+
def version(value = nil)
|
|
529
|
+
return @lutaml_model_version = value if value
|
|
530
|
+
|
|
531
|
+
return @lutaml_model_version if defined?(@lutaml_model_version)
|
|
532
|
+
|
|
533
|
+
if superclass.is_a?(::Class) &&
|
|
534
|
+
superclass < ::Lutaml::Model::Serializable
|
|
535
|
+
return superclass.version
|
|
536
|
+
end
|
|
537
|
+
|
|
538
|
+
nil
|
|
539
|
+
end
|
|
540
|
+
|
|
541
|
+
alias model_version version
|
|
542
|
+
|
|
543
|
+
# Wire-level version detection for the family (#265): the block
|
|
544
|
+
# receives the parsed document hash and returns the version the
|
|
545
|
+
# document was written against. Declared on the family head;
|
|
546
|
+
# `from_<format>` routes to the detected version's class before
|
|
547
|
+
# parsing. Opt-in — without a detector, parsing is unchanged.
|
|
548
|
+
def detect_version(&block)
|
|
549
|
+
return @lutaml_version_detector = block if block
|
|
550
|
+
|
|
551
|
+
return @lutaml_version_detector if defined?(@lutaml_version_detector)
|
|
552
|
+
|
|
553
|
+
if superclass.is_a?(::Class) &&
|
|
554
|
+
superclass < ::Lutaml::Model::Serializable
|
|
555
|
+
return superclass.detect_version
|
|
556
|
+
end
|
|
557
|
+
|
|
558
|
+
nil
|
|
559
|
+
end
|
|
560
|
+
|
|
561
|
+
# Declares wire-level migration steps from a former model
|
|
562
|
+
# version (#265). The block is evaluated on an EvolutionSteps
|
|
563
|
+
# collector (wire_rename, wire_move, wire_merge, wire_drop);
|
|
564
|
+
# the steps are applied by `evolve`, in declaration order, to a
|
|
565
|
+
# copy of the source document's wire hash before the target
|
|
566
|
+
# model parses it.
|
|
567
|
+
def evolution(from:, &block)
|
|
568
|
+
unless from.is_a?(::Class) && from < ::Lutaml::Model::Serializable
|
|
569
|
+
raise ArgumentError,
|
|
570
|
+
"evolution from: must be a Lutaml::Model::Serializable " \
|
|
571
|
+
"model class"
|
|
572
|
+
end
|
|
573
|
+
|
|
574
|
+
collector = Lutaml::Model::EvolutionSteps.new
|
|
575
|
+
collector.instance_eval(&block)
|
|
576
|
+
lutaml_evolutions[from] = collector.steps.freeze
|
|
577
|
+
nil
|
|
578
|
+
end
|
|
579
|
+
|
|
580
|
+
def lutaml_evolutions
|
|
581
|
+
@lutaml_evolutions ||= {}
|
|
582
|
+
end
|
|
583
|
+
|
|
584
|
+
# The class of the family that declares `version == value`.
|
|
585
|
+
# Walks the ancestor chain nearest-first; raises
|
|
586
|
+
# UnsupportedVersionError when no family member declares it.
|
|
587
|
+
def version_class_for(value)
|
|
588
|
+
klass = self
|
|
589
|
+
while klass.is_a?(::Class) &&
|
|
590
|
+
klass < ::Lutaml::Model::Serializable
|
|
591
|
+
return klass if klass.version == value
|
|
592
|
+
|
|
593
|
+
klass = klass.superclass
|
|
594
|
+
end
|
|
595
|
+
|
|
596
|
+
raise ::Lutaml::Model::UnsupportedVersionError.new(self, value)
|
|
597
|
+
end
|
|
598
|
+
|
|
599
|
+
# Migrate a document or instance of a former model version into
|
|
600
|
+
# this one (#265). The source's wire hash (its own `to_hash`
|
|
601
|
+
# for an instance) passes through the declared evolution chain
|
|
602
|
+
# — every family link between the source version and this
|
|
603
|
+
# class, in order — and the result parses as this model.
|
|
604
|
+
#
|
|
605
|
+
# @param source [Hash, Serializable] the old document or model
|
|
606
|
+
# @param from [Class] the source model class
|
|
607
|
+
# @param format [Symbol] the key-value format used to parse
|
|
608
|
+
# the evolved hash
|
|
609
|
+
def evolve(source, from:)
|
|
610
|
+
chain = evolution_chain_from(from)
|
|
611
|
+
wire = if source.is_a?(::Hash)
|
|
612
|
+
Lutaml::Model::Utils.deep_dup(source)
|
|
613
|
+
else
|
|
614
|
+
source.to_hash
|
|
615
|
+
end
|
|
616
|
+
chain.each do |link_source|
|
|
617
|
+
steps = evolution_steps_for(link_source)
|
|
618
|
+
wire = steps.reduce(wire) { |hash, step| step.call(hash) }
|
|
619
|
+
end
|
|
620
|
+
from_hash(wire)
|
|
621
|
+
end
|
|
622
|
+
|
|
623
|
+
# Family members nearest-first, self included.
|
|
624
|
+
def evolution_family
|
|
625
|
+
members = []
|
|
626
|
+
klass = self
|
|
627
|
+
while klass.is_a?(::Class) &&
|
|
628
|
+
klass < ::Lutaml::Model::Serializable
|
|
629
|
+
members << klass
|
|
630
|
+
klass = klass.superclass
|
|
631
|
+
end
|
|
632
|
+
members
|
|
633
|
+
end
|
|
634
|
+
|
|
635
|
+
# Shortest declared-evolution path from the source class to
|
|
636
|
+
# self, as the list of link source classes. Edges are the
|
|
637
|
+
# family's `evolution from:` declarations. (Public: also serves
|
|
638
|
+
# introspection of declared family links.)
|
|
639
|
+
def evolution_chain_from(source_class)
|
|
640
|
+
return [] if source_class == self
|
|
641
|
+
|
|
642
|
+
edges = {}
|
|
643
|
+
evolution_family.each do |member|
|
|
644
|
+
member.lutaml_evolutions.each_key do |source|
|
|
645
|
+
(edges[source] ||= []) << member
|
|
646
|
+
end
|
|
647
|
+
end
|
|
648
|
+
|
|
649
|
+
parents = { source_class => nil }
|
|
650
|
+
frontier = [source_class]
|
|
651
|
+
until frontier.empty? || parents[self]
|
|
652
|
+
current = frontier.shift
|
|
653
|
+
(edges[current] || []).each do |target|
|
|
654
|
+
next if parents.key?(target)
|
|
655
|
+
|
|
656
|
+
parents[target] = current
|
|
657
|
+
frontier << target
|
|
658
|
+
end
|
|
659
|
+
end
|
|
660
|
+
|
|
661
|
+
unless parents[self]
|
|
662
|
+
raise ArgumentError,
|
|
663
|
+
"no evolution path from #{source_class} to #{self} " \
|
|
664
|
+
"is declared"
|
|
665
|
+
end
|
|
666
|
+
|
|
667
|
+
path = []
|
|
668
|
+
cursor = self
|
|
669
|
+
while cursor != source_class
|
|
670
|
+
path.unshift(cursor)
|
|
671
|
+
cursor = parents[cursor]
|
|
672
|
+
end
|
|
673
|
+
path.unshift(source_class)[0...-1]
|
|
674
|
+
end
|
|
675
|
+
|
|
676
|
+
# The family member (nearest-first) that declared steps for the
|
|
677
|
+
# given link source.
|
|
678
|
+
def evolution_steps_for(link_source)
|
|
679
|
+
member = evolution_family.find do |klass|
|
|
680
|
+
klass.lutaml_evolutions.key?(link_source)
|
|
681
|
+
end
|
|
682
|
+
member.lutaml_evolutions[link_source]
|
|
683
|
+
end
|
|
684
|
+
|
|
524
685
|
# Define an attribute for the model
|
|
525
686
|
#
|
|
526
687
|
# @param name [Symbol] The attribute name
|
|
@@ -75,6 +75,18 @@ module Lutaml
|
|
|
75
75
|
|
|
76
76
|
def self.register_from_format_method(format)
|
|
77
77
|
ClassMethods.define_method(:"from_#{format}") do |data, options = {}|
|
|
78
|
+
# #265 version routing: with a family detector declared, the
|
|
79
|
+
# document's version picks the family class that parses it.
|
|
80
|
+
# Terminates because the routed class resolves the same
|
|
81
|
+
# detected version to itself and parses directly.
|
|
82
|
+
detector = detect_version
|
|
83
|
+
if detector
|
|
84
|
+
parsed = ::Lutaml::Model::Config.adapter_for(format)
|
|
85
|
+
.parse(data, options)
|
|
86
|
+
detected = detector.call(parsed)
|
|
87
|
+
routed = version_class_for(detected) unless detected.nil?
|
|
88
|
+
return routed.public_send(:"from_#{format}", data, options) if routed && routed != self
|
|
89
|
+
end
|
|
78
90
|
from(format, data, options)
|
|
79
91
|
end
|
|
80
92
|
|
data/lib/lutaml/model/union.rb
CHANGED
|
@@ -97,7 +97,23 @@ module Lutaml
|
|
|
97
97
|
end
|
|
98
98
|
return [member, casted] unless casted == :no_match
|
|
99
99
|
end
|
|
100
|
-
|
|
100
|
+
|
|
101
|
+
lenient_model_member(value, members, format, register)
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
# With exactly one model member, a Hash that also carries unmapped
|
|
105
|
+
# keys still conforms: unknown keys are ignored by every non-union
|
|
106
|
+
# model attribute, and strict coverage would silently drop the
|
|
107
|
+
# whole value. With several model members the strict key-coverage
|
|
108
|
+
# check stays in charge — coverage is what discriminates between
|
|
109
|
+
# them. Plain-Ruby assignment keeps the strict constructor path.
|
|
110
|
+
def lenient_model_member(value, members, format, register)
|
|
111
|
+
return nil unless value.is_a?(::Hash)
|
|
112
|
+
return nil if format.nil?
|
|
113
|
+
return nil unless members.one? { |member| model_member?(member) }
|
|
114
|
+
|
|
115
|
+
member = members.find { |candidate| model_member?(candidate) }
|
|
116
|
+
[member, member.apply_mappings(value, format, register: register)]
|
|
101
117
|
end
|
|
102
118
|
|
|
103
119
|
# Whether a compiled mapping rule's attribute is a union — the single
|
data/lib/lutaml/model/version.rb
CHANGED
data/lib/lutaml/model.rb
CHANGED
|
@@ -46,6 +46,7 @@ module Lutaml
|
|
|
46
46
|
autoload :NamespaceBinding, "#{__dir__}/model/register/namespace_binding"
|
|
47
47
|
autoload :ModelTreeImporter, "#{__dir__}/model/register/model_tree_importer"
|
|
48
48
|
autoload :Transformation, "#{__dir__}/model/transformation"
|
|
49
|
+
autoload :EvolutionSteps, "#{__dir__}/model/evolution_steps"
|
|
49
50
|
autoload :CompiledRule, "#{__dir__}/model/compiled_rule"
|
|
50
51
|
autoload :FormatRegistry, "#{__dir__}/model/format_registry"
|
|
51
52
|
autoload :OneEntryCache, "#{__dir__}/model/one_entry_cache"
|
|
@@ -161,6 +162,8 @@ module Lutaml
|
|
|
161
162
|
autoload :NoRootMappingError, "#{__dir__}/model/error/no_root_mapping_error"
|
|
162
163
|
autoload :ImportModelWithRootError,
|
|
163
164
|
"#{__dir__}/model/error/import_model_with_root_error"
|
|
165
|
+
autoload :UnsupportedVersionError,
|
|
166
|
+
"#{__dir__}/model/error/unsupported_version_error"
|
|
164
167
|
autoload :InvalidChoiceRangeError,
|
|
165
168
|
"#{__dir__}/model/error/invalid_choice_range_error"
|
|
166
169
|
autoload :UnknownSequenceMappingError,
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "spec_helper"
|
|
4
|
+
|
|
5
|
+
# A child_mappings rule whose wire key is absent crashed translate_mappings
|
|
6
|
+
# with an internal NoMethodError (nil.map) wrapped as InvalidFormatError.
|
|
7
|
+
# An absent key means the attribute keeps its uninitialized/default state,
|
|
8
|
+
# exactly like every other rule kind.
|
|
9
|
+
RSpec.describe "child_mappings rule with an absent key" do
|
|
10
|
+
let(:schema_class) do
|
|
11
|
+
Class.new(Lutaml::Model::Serializable) do
|
|
12
|
+
attribute :id, :string
|
|
13
|
+
attribute :path, :string
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
let(:model_class) do
|
|
18
|
+
schemas = schema_class
|
|
19
|
+
Class.new(Lutaml::Model::Serializable) do
|
|
20
|
+
attribute :type, :string
|
|
21
|
+
attribute :schemas, schemas, collection: true
|
|
22
|
+
|
|
23
|
+
key_value do
|
|
24
|
+
map "type", to: :type
|
|
25
|
+
map "schemas", to: :schemas,
|
|
26
|
+
child_mappings: {
|
|
27
|
+
id: :key,
|
|
28
|
+
path: %i[path link],
|
|
29
|
+
}
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
before { stub_const("ChildMapAbsentModel", model_class) }
|
|
35
|
+
|
|
36
|
+
it "parses without the key, leaving the attribute unset" do
|
|
37
|
+
model = ChildMapAbsentModel.from_json('{"type":"object"}')
|
|
38
|
+
|
|
39
|
+
expect(model.type).to eq("object")
|
|
40
|
+
expect(model.schemas).to be_nil
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
it "still translates when the key is present" do
|
|
44
|
+
model = ChildMapAbsentModel.from_json(
|
|
45
|
+
'{"type":"object","schemas":{' \
|
|
46
|
+
'"sch1":{"path":{"link":"https://example.com"}}}}',
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
expect(model.schemas.size).to eq(1)
|
|
50
|
+
expect(model.schemas.first.id).to eq("sch1")
|
|
51
|
+
expect(model.schemas.first.path).to eq("https://example.com")
|
|
52
|
+
end
|
|
53
|
+
end
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "spec_helper"
|
|
4
|
+
|
|
5
|
+
# #265: model evolution — version metadata, wire-level migration steps
|
|
6
|
+
# between model versions, and family version detection. The migration
|
|
7
|
+
# steps operate on the source document's wire hash (deep-copied); the
|
|
8
|
+
# evolved hash parses as the target model through the hash format.
|
|
9
|
+
RSpec.describe "model evolution" do
|
|
10
|
+
let(:v1_class) do
|
|
11
|
+
Class.new(Lutaml::Model::Serializable) do
|
|
12
|
+
version 1
|
|
13
|
+
|
|
14
|
+
attribute :author, :string
|
|
15
|
+
attribute :name, :string
|
|
16
|
+
attribute :first_name, :string
|
|
17
|
+
attribute :last_name, :string
|
|
18
|
+
attribute :tag, :string
|
|
19
|
+
attribute :deprecated_field, :string
|
|
20
|
+
|
|
21
|
+
key_value do
|
|
22
|
+
map "author", to: :author
|
|
23
|
+
map "name", to: :name
|
|
24
|
+
map "first_name", to: :first_name
|
|
25
|
+
map "last_name", to: :last_name
|
|
26
|
+
map "tag", to: :tag
|
|
27
|
+
map "deprecated_field", to: :deprecated_field
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
let(:v2_class) do
|
|
33
|
+
v1 = v1_class
|
|
34
|
+
Class.new(v1) do
|
|
35
|
+
version 2
|
|
36
|
+
|
|
37
|
+
attribute :creator, :string
|
|
38
|
+
attribute :full_name, :string
|
|
39
|
+
attribute :tags, :string, collection: true
|
|
40
|
+
attribute :metadata_name, :string
|
|
41
|
+
|
|
42
|
+
key_value do
|
|
43
|
+
map "creator", to: :creator
|
|
44
|
+
map "full_name", to: :full_name
|
|
45
|
+
map "tags", to: :tags
|
|
46
|
+
map "metadata", to: :metadata_name,
|
|
47
|
+
child_mappings: { metadata_name: :name }
|
|
48
|
+
map "deprecated_field", to: :deprecated_field, serialize: false
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
evolution from: v1 do
|
|
52
|
+
wire_rename "author", to: "creator"
|
|
53
|
+
wire_rename "tag", to: "tags"
|
|
54
|
+
wire_merge %w[first_name last_name], to: "full_name",
|
|
55
|
+
with: ->(first, last) { [first, last].compact.join(" ") }
|
|
56
|
+
wire_drop "first_name", "last_name", "deprecated_field"
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
before do
|
|
62
|
+
stub_const("EvolutionDocV1", v1_class)
|
|
63
|
+
stub_const("EvolutionDocV2", v2_class)
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
it "stamps and inherits the model version" do
|
|
67
|
+
expect(EvolutionDocV1.model_version).to eq(1)
|
|
68
|
+
expect(EvolutionDocV2.model_version).to eq(2)
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
it "migrates a v1 hash into a v2 model through declared steps" do
|
|
72
|
+
v2 = EvolutionDocV2.evolve(
|
|
73
|
+
{ "author" => "Ruth", "first_name" => "Ada", "last_name" => "L" },
|
|
74
|
+
from: EvolutionDocV1,
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
expect(v2).to be_a(EvolutionDocV2)
|
|
78
|
+
expect(v2.creator).to eq("Ruth")
|
|
79
|
+
expect(v2.full_name).to eq("Ada L")
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
it "migrates a v1 instance through its own wire hash" do
|
|
83
|
+
old = EvolutionDocV1.from_json(
|
|
84
|
+
'{"author":"Ruth","tag":"core","deprecated_field":"gone"}',
|
|
85
|
+
)
|
|
86
|
+
v2 = EvolutionDocV2.evolve(old, from: EvolutionDocV1)
|
|
87
|
+
|
|
88
|
+
expect(v2.creator).to eq("Ruth")
|
|
89
|
+
expect(v2.tags).to eq(["core"])
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
it "does not mutate the caller's source hash" do
|
|
93
|
+
source = { "author" => "Ruth", "first_name" => "Ada" }
|
|
94
|
+
EvolutionDocV2.evolve(source, from: EvolutionDocV1)
|
|
95
|
+
|
|
96
|
+
expect(source).to eq("author" => "Ruth", "first_name" => "Ada")
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
it "relocates keys with wire_move" do
|
|
100
|
+
mover = Class.new(EvolutionDocV1) do
|
|
101
|
+
version 3
|
|
102
|
+
attribute :meta, :hash
|
|
103
|
+
|
|
104
|
+
key_value do
|
|
105
|
+
map "meta", to: :meta
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
evolution from: EvolutionDocV2 do
|
|
109
|
+
wire_move "creator", to: "meta.creator"
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
stub_const("EvolutionDocV3", mover)
|
|
113
|
+
|
|
114
|
+
v3 = mover.evolve(
|
|
115
|
+
{ "creator" => "Ruth", "name" => "n" },
|
|
116
|
+
from: EvolutionDocV2,
|
|
117
|
+
)
|
|
118
|
+
|
|
119
|
+
expect(v3.meta).to eq("creator" => "Ruth")
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
it "composes chains v1 -> v2 -> v3" do
|
|
123
|
+
mover = Class.new(EvolutionDocV2) do
|
|
124
|
+
version 3
|
|
125
|
+
|
|
126
|
+
evolution from: EvolutionDocV2 do
|
|
127
|
+
wire_drop "tag"
|
|
128
|
+
end
|
|
129
|
+
end
|
|
130
|
+
stub_const("EvolutionDocV3Chain", mover)
|
|
131
|
+
|
|
132
|
+
v3 = mover.evolve(
|
|
133
|
+
{ "author" => "Ruth", "first_name" => "Ada", "tag" => "x" },
|
|
134
|
+
from: EvolutionDocV1,
|
|
135
|
+
)
|
|
136
|
+
|
|
137
|
+
expect(v3).to be_a(EvolutionDocV3Chain)
|
|
138
|
+
expect(v3.creator).to eq("Ruth")
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
it "raises when no evolution path is declared" do
|
|
142
|
+
unrelated = Class.new(Lutaml::Model::Serializable) do
|
|
143
|
+
version 9
|
|
144
|
+
attribute :x, :string
|
|
145
|
+
key_value { map "x", to: :x }
|
|
146
|
+
end
|
|
147
|
+
stub_const("EvolutionUnrelated", unrelated)
|
|
148
|
+
|
|
149
|
+
expect do
|
|
150
|
+
EvolutionUnrelated.evolve({ "x" => 1 }, from: EvolutionDocV1)
|
|
151
|
+
end.to raise_error(
|
|
152
|
+
ArgumentError,
|
|
153
|
+
/no evolution path from EvolutionDocV1 to EvolutionUnrelated/,
|
|
154
|
+
)
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
describe "version detection" do
|
|
158
|
+
let(:detecting_class) do
|
|
159
|
+
Class.new(v2_class) do
|
|
160
|
+
detect_version do |doc|
|
|
161
|
+
doc["schema_version"] || 1
|
|
162
|
+
end
|
|
163
|
+
end
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
before { stub_const("EvolutionDetecting", detecting_class) }
|
|
167
|
+
|
|
168
|
+
it "routes an older document to its own version's model" do
|
|
169
|
+
parsed = EvolutionDetecting.from_json(
|
|
170
|
+
'{"schema_version":1,"author":"Ruth"}',
|
|
171
|
+
)
|
|
172
|
+
|
|
173
|
+
expect(parsed).to be_a(EvolutionDocV1)
|
|
174
|
+
expect(parsed.author).to eq("Ruth")
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
it "parses a current document as itself" do
|
|
178
|
+
parsed = EvolutionDetecting.from_json(
|
|
179
|
+
'{"schema_version":2,"creator":"Ruth"}',
|
|
180
|
+
)
|
|
181
|
+
|
|
182
|
+
expect(parsed).to be_a(EvolutionDetecting)
|
|
183
|
+
expect(parsed.creator).to eq("Ruth")
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
it "raises on a version no family member declares" do
|
|
187
|
+
expect do
|
|
188
|
+
EvolutionDetecting.from_json('{"schema_version":42}')
|
|
189
|
+
end.to raise_error(
|
|
190
|
+
Lutaml::Model::UnsupportedVersionError,
|
|
191
|
+
/version 42/,
|
|
192
|
+
)
|
|
193
|
+
end
|
|
194
|
+
end
|
|
195
|
+
|
|
196
|
+
describe "models without evolution declarations" do
|
|
197
|
+
it "parses unchanged" do
|
|
198
|
+
model = v1_class.from_json('{"author":"Ruth"}')
|
|
199
|
+
|
|
200
|
+
expect(model.author).to eq("Ruth")
|
|
201
|
+
expect(v1_class.model_version).to eq(1)
|
|
202
|
+
end
|
|
203
|
+
end
|
|
204
|
+
end
|
|
@@ -686,14 +686,27 @@ RSpec.describe "Union-typed attributes (issue #190)" do
|
|
|
686
686
|
expect(parsed.value).to be(true)
|
|
687
687
|
end
|
|
688
688
|
|
|
689
|
-
it "
|
|
689
|
+
it "builds the lone model member even when no key maps (#989: " \
|
|
690
|
+
"non-union parity)" do
|
|
690
691
|
holder = Class.new(Lutaml::Model::Serializable) do
|
|
691
692
|
attribute :value, [ExtendedShape, :boolean]
|
|
692
693
|
key_value { map "value", to: :value }
|
|
693
694
|
end
|
|
694
695
|
stub_const("ExtendedHolderNinetyEight", holder)
|
|
695
696
|
|
|
696
|
-
|
|
697
|
+
value = holder.from_json('{"value":{"unrelated":1}}').value
|
|
698
|
+
|
|
699
|
+
expect(value).to be_a(ExtendedShape)
|
|
700
|
+
end
|
|
701
|
+
|
|
702
|
+
it "returns nil when no member conforms at all" do
|
|
703
|
+
holder = Class.new(Lutaml::Model::Serializable) do
|
|
704
|
+
attribute :value, [ExtendedShape, :boolean]
|
|
705
|
+
key_value { map "value", to: :value }
|
|
706
|
+
end
|
|
707
|
+
stub_const("ExtendedHolderNinetyEightB", holder)
|
|
708
|
+
|
|
709
|
+
expect(holder.from_json('{"value":3}').value).to be_nil
|
|
697
710
|
end
|
|
698
711
|
end
|
|
699
712
|
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "spec_helper"
|
|
4
|
+
|
|
5
|
+
# Regression spec for issue #987: with a single model member in a union,
|
|
6
|
+
# a Hash carrying keys the model does not map must still conform. Unknown
|
|
7
|
+
# keys are ignored by every non-union model attribute; strict key
|
|
8
|
+
# coverage silently returned nil for the whole attribute instead.
|
|
9
|
+
module UnionLenientModelSpec
|
|
10
|
+
class Extensible < Lutaml::Model::Serializable
|
|
11
|
+
attribute :type, :string
|
|
12
|
+
attribute :strict_properties, :boolean
|
|
13
|
+
|
|
14
|
+
key_value do
|
|
15
|
+
map "type", to: :type
|
|
16
|
+
map "strictProperties", to: :strict_properties
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
class Holder < Lutaml::Model::Serializable
|
|
21
|
+
attribute :value, [Extensible, :boolean]
|
|
22
|
+
|
|
23
|
+
key_value do
|
|
24
|
+
map "value", to: :value
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
class Other < Lutaml::Model::Serializable
|
|
29
|
+
attribute :celsius, :float
|
|
30
|
+
|
|
31
|
+
key_value do
|
|
32
|
+
map "celsius", to: :celsius
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
class TwoModelHolder < Lutaml::Model::Serializable
|
|
37
|
+
attribute :value, [Extensible, Other]
|
|
38
|
+
|
|
39
|
+
key_value do
|
|
40
|
+
map "value", to: :value
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
RSpec.describe "Union with a single model member accepts unmapped keys" do
|
|
45
|
+
it "builds the model when the hash carries unmapped keys" do
|
|
46
|
+
value = Holder.from_json('{"value":{"type":"string","x-ext":1}}').value
|
|
47
|
+
|
|
48
|
+
expect(value).to be_a(Extensible)
|
|
49
|
+
expect(value.type).to eq("string")
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
it "still accepts hashes with mapped keys only" do
|
|
53
|
+
value = Holder.from_json('{"value":{"type":"string"}}').value
|
|
54
|
+
|
|
55
|
+
expect(value).to be_a(Extensible)
|
|
56
|
+
expect(value.type).to eq("string")
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
it "keeps boolean members working" do
|
|
60
|
+
expect(Holder.from_json('{"value":true}').value).to be(true)
|
|
61
|
+
expect(Holder.from_json('{"value":false}').value).to be(false)
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
it "dispatches to the first model member with a mapped key across " \
|
|
65
|
+
"several members" do
|
|
66
|
+
json = '{"value":{"type":"string","celsius":1.0}}'
|
|
67
|
+
|
|
68
|
+
value = TwoModelHolder.from_json(json).value
|
|
69
|
+
|
|
70
|
+
expect(value).to be_a(Extensible)
|
|
71
|
+
expect(value.type).to eq("string")
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
it "still requires a match when nothing conforms" do
|
|
75
|
+
expect(Holder.from_json('{"value":3}').value).to be_nil
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
end
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: lutaml-model
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.8.
|
|
4
|
+
version: 0.8.125
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Ribose Inc.
|
|
@@ -500,9 +500,11 @@ files:
|
|
|
500
500
|
- lib/lutaml/model/error/unknown_sequence_mapping_error.rb
|
|
501
501
|
- lib/lutaml/model/error/unknown_type_error.rb
|
|
502
502
|
- lib/lutaml/model/error/unresolvable_type_error.rb
|
|
503
|
+
- lib/lutaml/model/error/unsupported_version_error.rb
|
|
503
504
|
- lib/lutaml/model/error/validation_error.rb
|
|
504
505
|
- lib/lutaml/model/error/validation_failed_error.rb
|
|
505
506
|
- lib/lutaml/model/errors.rb
|
|
507
|
+
- lib/lutaml/model/evolution_steps.rb
|
|
506
508
|
- lib/lutaml/model/finalization_cache.rb
|
|
507
509
|
- lib/lutaml/model/format_registry.rb
|
|
508
510
|
- lib/lutaml/model/global_context.rb
|
|
@@ -1716,6 +1718,7 @@ files:
|
|
|
1716
1718
|
- spec/lutaml/model/cardinality_strict_spec.rb
|
|
1717
1719
|
- spec/lutaml/model/cast_element_form_spec.rb
|
|
1718
1720
|
- spec/lutaml/model/cdata_spec.rb
|
|
1721
|
+
- spec/lutaml/model/child_mappings_absent_key_spec.rb
|
|
1719
1722
|
- spec/lutaml/model/choice_restrict_spec.rb
|
|
1720
1723
|
- spec/lutaml/model/choice_spec.rb
|
|
1721
1724
|
- spec/lutaml/model/cli_spec.rb
|
|
@@ -1750,6 +1753,7 @@ files:
|
|
|
1750
1753
|
- spec/lutaml/model/dynamic_attribute_spec.rb
|
|
1751
1754
|
- spec/lutaml/model/encoding_spec.rb
|
|
1752
1755
|
- spec/lutaml/model/enum_spec.rb
|
|
1756
|
+
- spec/lutaml/model/evolution_spec.rb
|
|
1753
1757
|
- spec/lutaml/model/except_spec.rb
|
|
1754
1758
|
- spec/lutaml/model/finalization_cache_spec.rb
|
|
1755
1759
|
- spec/lutaml/model/from_json_scalar_cast_spec.rb
|
|
@@ -1868,6 +1872,7 @@ files:
|
|
|
1868
1872
|
- spec/lutaml/model/uninitialized_class_deep_dup_spec.rb
|
|
1869
1873
|
- spec/lutaml/model/uninitialized_class_spec.rb
|
|
1870
1874
|
- spec/lutaml/model/union_attribute_spec.rb
|
|
1875
|
+
- spec/lutaml/model/union_lenient_model_spec.rb
|
|
1871
1876
|
- spec/lutaml/model/union_spec.rb
|
|
1872
1877
|
- spec/lutaml/model/unregistered_context_resolution_spec.rb
|
|
1873
1878
|
- spec/lutaml/model/utils_spec.rb
|