lutaml-model 0.8.124 → 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
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
|
|
@@ -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,12 +745,14 @@ 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
|
|
@@ -757,11 +762,11 @@ partition = nil)
|
|
|
757
762
|
# or explicit-null key has nothing to translate — the attribute
|
|
758
763
|
# keeps its uninitialized/default state (a nil here previously
|
|
759
764
|
# crashed translate_mappings).
|
|
760
|
-
return if value.nil? &&
|
|
765
|
+
return if value.nil? && kind == :hash_map
|
|
761
766
|
|
|
762
767
|
value = translate_mappings(value, rule.hash_mappings, attr, format,
|
|
763
768
|
instance)
|
|
764
|
-
unless
|
|
769
|
+
unless kind == :hash_map
|
|
765
770
|
value = cast_value(value, attr, format, rule,
|
|
766
771
|
instance)
|
|
767
772
|
end
|
|
@@ -792,7 +797,8 @@ partition = nil)
|
|
|
792
797
|
# Guard + cached target — an inline cache per rule. Complex rules
|
|
793
798
|
# (custom methods, transforms, value maps, delegates, hash mappings,
|
|
794
799
|
# unions, polymorphism) return nil and keep the interpretive path.
|
|
795
|
-
|
|
800
|
+
WalkPlan = ::Struct.new(:kind, :attr, :valid, :wire, :klass,
|
|
801
|
+
:collection)
|
|
796
802
|
|
|
797
803
|
# Memoized (per transform) wire name for plain rules — the from
|
|
798
804
|
# spelling never changes after definition.
|
|
@@ -814,15 +820,48 @@ partition = nil)
|
|
|
814
820
|
@kv_serialize_plans[rule] ||= KvSerializePlan.new(rule_from_name(rule))
|
|
815
821
|
end
|
|
816
822
|
|
|
817
|
-
def
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
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
|
|
822
863
|
|
|
823
|
-
plan
|
|
824
|
-
KV_RULE_PLANS[key] = plan || false
|
|
825
|
-
plan
|
|
864
|
+
plan || WalkPlan.new(kind, attr, valid, nil, nil, false)
|
|
826
865
|
end
|
|
827
866
|
|
|
828
867
|
# `rule.polymorphic` defaults to an empty Hash, which is truthy —
|
|
@@ -849,7 +888,8 @@ partition = nil)
|
|
|
849
888
|
!attr.value_policy.whole_value?(type) &&
|
|
850
889
|
!Lutaml::Model::Attribute.custom_from_probe?(type)
|
|
851
890
|
|
|
852
|
-
|
|
891
|
+
WalkPlan.new(:scalar, attr, true, rule.name.to_s, type,
|
|
892
|
+
attr.collection?)
|
|
853
893
|
end
|
|
854
894
|
|
|
855
895
|
# Extraction fast path: plain hash fetch for a present, non-blank
|
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
|
@@ -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.
|
|
@@ -1872,6 +1872,7 @@ files:
|
|
|
1872
1872
|
- spec/lutaml/model/uninitialized_class_deep_dup_spec.rb
|
|
1873
1873
|
- spec/lutaml/model/uninitialized_class_spec.rb
|
|
1874
1874
|
- spec/lutaml/model/union_attribute_spec.rb
|
|
1875
|
+
- spec/lutaml/model/union_lenient_model_spec.rb
|
|
1875
1876
|
- spec/lutaml/model/union_spec.rb
|
|
1876
1877
|
- spec/lutaml/model/unregistered_context_resolution_spec.rb
|
|
1877
1878
|
- spec/lutaml/model/utils_spec.rb
|