lutaml-model 0.8.134 → 0.8.136

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: 250473959d5b49e40ab480a2acf1e708d2cac1f8d5b092995be03821bc758122
4
- data.tar.gz: 59fc186602fae873d070d23baca950a2330a07abbb251acd16b3a63803877a93
3
+ metadata.gz: 78585d088d67236f3378f1860c5916e37a1bad8bde58bb48954fe8057ada72b9
4
+ data.tar.gz: afbf8b41d1b733685ad4d066c07e5217eff0674a1eb66bbbc75e6d9cdd2bf31e
5
5
  SHA512:
6
- metadata.gz: 82c2a11ef5c20e8342280054b0053e7d07fcca92b71effc4224d6b006889f3873026890d894b98a0791add12444e1cb87cddab51ece3eee47f2d08e8a53106c0
7
- data.tar.gz: 95ec3912247f014bc20a9bf2f3ec39a3fc1b0bdf5eeb2b932f55612446b5f3c45af21e6b37c28682b52541e842bc68b7099ce361b6b8184f93265e8254779873
6
+ metadata.gz: c2b9f8b00bfcc541d7ccebc179c9d0ce13fc530c594d605d7c4aeafd5dc1eb386c253eebdcc597a3257be3978e1fe74b8ba19b9bc05053d45a4662bd0bae7c63
7
+ data.tar.gz: 5e739f42a165ca2280473a03addd0418061f584e28d1f0d8fc7a015e0855a30e56dab47b47024bbc3fdc4ded50927654e2659a5d00ea5bd68b6c997cfca54224
@@ -28,6 +28,10 @@ NATIVE_ONLY_PATTERNS = [
28
28
  %r{/schema_builder/(nokogiri|oga)\b},
29
29
  %r{/schema/builder/(nokogiri|oga)\b},
30
30
  %r{\Alutaml/model/cli\z},
31
+ # leptris is an FFI wrapper over a native library — Opal::Builder dies
32
+ # on its requires (and FFI itself does not exist under Opal); the
33
+ # adapter probe degrades to moxml at runtime (#1008).
34
+ %r{\Alutaml/xml/leptris},
31
35
  ].freeze
32
36
 
33
37
  # Match `autoload :Name, "...anything..."` where the string argument may
@@ -148,7 +148,6 @@ require "lutaml/xml/format"
148
148
  require "lutaml/xml/format_chooser"
149
149
  require "lutaml/xml/hoisting_algorithm"
150
150
  require "lutaml/xml/html_entities"
151
- require "lutaml/xml/leptris"
152
151
  require "lutaml/xml/listener"
153
152
  require "lutaml/xml/mapping"
154
153
  require "lutaml/xml/mapping_rule"
@@ -399,12 +398,6 @@ require "lutaml/xml/error/namespace_mismatch_error"
399
398
  require "lutaml/xml/error/schema_validation_error"
400
399
  require "lutaml/xml/error/xml_configuration_error"
401
400
  require "lutaml/xml/error/xml_error"
402
- require "lutaml/xml/leptris/native_emission"
403
- require "lutaml/xml/leptris/plan_compiler"
404
- require "lutaml/xml/leptris/plan_hydrator"
405
- require "lutaml/xml/leptris/plan_order"
406
- require "lutaml/xml/leptris/plan_serializer"
407
- require "lutaml/xml/leptris/plan_walk"
408
401
  require "lutaml/xml/oga/element"
409
402
  require "lutaml/xml/ox/element"
410
403
  require "lutaml/xml/rexml/element"
@@ -98,7 +98,26 @@ module Lutaml
98
98
  return [member, casted] unless casted == :no_match
99
99
  end
100
100
 
101
- lenient_model_member(value, members, format, register)
101
+ widest_model_member(value, members, format, register) ||
102
+ lenient_model_member(value, members, format, register)
103
+ end
104
+
105
+ # No member maps every key, so the hash carries keys no rule claims
106
+ # (extension keywords, forward-compatible vocabularies): the model
107
+ # member mapping the most of its keys, the earlier one on a tie, as
108
+ # the keys no rule claims are ignored outside a union (#987).
109
+ def widest_model_member(value, members, format, register)
110
+ keys = input_keys(value, format)
111
+ return nil if keys.nil? || keys.empty?
112
+
113
+ counts = members.select { |member| model_member?(member) }.to_h do |member|
114
+ field_names = member_field_names(member, format, register)
115
+ [member, keys.count { |key| field_names.include?(key) }]
116
+ end
117
+ member, count = counts.max_by.with_index { |(_, mapped), index| [mapped, -index] }
118
+ return nil if member.nil? || count.zero?
119
+
120
+ [member, build_model(member, value, format, register)]
102
121
  end
103
122
 
104
123
  # With exactly one model member, a Hash that also carries unmapped
@@ -170,14 +189,12 @@ module Lutaml
170
189
  member.is_a?(::Class) && member.include?(Serialize)
171
190
  end
172
191
 
173
- # Structural selection: at least one input key maps to one of the
174
- # member's mapped fields. Keys no rule claims are ignored exactly
175
- # as they are when the same hash reaches the model outside a
176
- # union — extension keys (x-** vendor keywords, forward-compatible
177
- # vocabularies) must not defeat dispatch (#987). Once selected,
178
- # field values cast leniently via the member's normal path. An
179
- # existing member instance conforms as-is, keeping conformance
180
- # idempotent — values can re-enter casting (e.g. a value
192
+ # Structural (key-coverage) selection: every input key maps to one of
193
+ # the member's mapped fields, which is what tells members sharing a
194
+ # key apart. A hash no member covers falls to widest_model_member.
195
+ # Once selected, field values cast leniently via the member's normal
196
+ # path. An existing member instance conforms as-is, keeping
197
+ # conformance idempotent — values can re-enter casting (e.g. a value
181
198
  # transformer builds the member before the cast runs).
182
199
  def conform_model(member, value, format, register)
183
200
  return value if value.is_a?(member)
@@ -186,13 +203,17 @@ module Lutaml
186
203
  return :no_match if keys.nil? || keys.empty?
187
204
 
188
205
  field_names = member_field_names(member, format, register)
189
- return :no_match unless keys.any? do |key|
206
+ return :no_match unless keys.all? do |key|
190
207
  field_names.include?(key)
191
208
  end
192
209
 
193
- # Plain-Ruby assignment (format nil) builds straight from the
194
- # attribute hash, mirroring how non-union model attributes accept
195
- # `Model.new(child: { ... })`; a format builds via the mappings.
210
+ build_model(member, value, format, register)
211
+ end
212
+
213
+ # Plain-Ruby assignment (format nil) builds straight from the
214
+ # attribute hash, mirroring how non-union model attributes accept
215
+ # `Model.new(child: { ... })`; a format builds via the mappings.
216
+ def build_model(member, value, format, register)
196
217
  return member.new(value) if format.nil?
197
218
 
198
219
  member.apply_mappings(value, format, register: register)
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Lutaml
4
4
  module Model
5
- VERSION = "0.8.134"
5
+ VERSION = "0.8.136"
6
6
  end
7
7
  end
@@ -710,6 +710,76 @@ RSpec.describe "Union-typed attributes (issue #190)" do
710
710
  end
711
711
  end
712
712
 
713
+ describe "model members that share a key" do
714
+ # Two members both map "source"; only their other keys tell them
715
+ # apart. The member mapping every key wins over an earlier one that
716
+ # maps only the shared key, as before #987; with an extension key
717
+ # neither maps every key, so the member mapping the most keys wins.
718
+ let(:report) do
719
+ Class.new(Lutaml::Model::Serializable) do
720
+ attribute :source, :string
721
+ attribute :issues, :string, collection: true
722
+ key_value do
723
+ map "source", to: :source
724
+ map "issues", to: :issues
725
+ end
726
+ end
727
+ end
728
+
729
+ let(:conversion) do
730
+ Class.new(Lutaml::Model::Serializable) do
731
+ attribute :source, :string
732
+ attribute :target, :string
733
+ key_value do
734
+ map "source", to: :source
735
+ map "target", to: :target
736
+ end
737
+ end
738
+ end
739
+
740
+ let(:holder) do
741
+ members = [report, conversion]
742
+ Class.new(Lutaml::Model::Serializable) do
743
+ attribute :result, members
744
+ key_value { map "result", to: :result }
745
+ end
746
+ end
747
+
748
+ before do
749
+ stub_const("SharedKeyReport", report)
750
+ stub_const("SharedKeyConversion", conversion)
751
+ stub_const("SharedKeyHolder", holder)
752
+ end
753
+
754
+ it "selects the later member whose fields cover every key" do
755
+ parsed = holder.from_json('{"result":{"source":"a.emf","target":"svg"}}')
756
+
757
+ expect(parsed.result).to be_a(conversion)
758
+ expect(parsed.result.target).to eq("svg")
759
+ end
760
+
761
+ it "keeps the earlier member when it covers every key" do
762
+ parsed = holder.from_json('{"result":{"source":"a.emf","issues":["m"]}}')
763
+
764
+ expect(parsed.result).to be_a(report)
765
+ expect(parsed.result.issues).to eq(["m"])
766
+ end
767
+
768
+ it "selects the member mapping the most keys when an extension key is present" do
769
+ parsed = holder.from_json(
770
+ '{"result":{"source":"a.emf","target":"svg","x-ext":1}}',
771
+ )
772
+
773
+ expect(parsed.result).to be_a(conversion)
774
+ end
775
+
776
+ it "keeps declared order when members map equally many keys" do
777
+ parsed = holder.from_json('{"result":{"source":"a.emf","x-ext":1}}')
778
+
779
+ expect(parsed.result).to be_a(report)
780
+ end
781
+ end
782
+
713
783
  describe "value transformers (cast idempotence)" do
714
784
  it "keeps a model member built by transform: before the cast runs" do
715
785
  reading = UnionAttributeSpec::TransformedReading.from_yaml(<<~YAML)
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.134
4
+ version: 0.8.136
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ribose Inc.