lutaml-model 0.8.112 → 0.8.114

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: 3f1d5fc3ccd267d7c8c8f4a7117db2f01b1a81da024724128402ca058584526b
4
- data.tar.gz: 0c5fa1399c3e9df481ee09480f1851493544c0c26cf1e0f8851c381dec5dfa64
3
+ metadata.gz: 381cba1519b14ff18965d44164e7d75e4564ac92c803815dad95277b7076ef70
4
+ data.tar.gz: acd3466cbc30ed27ce60ce62b181732ab957abde427da1e609003ced88c47a67
5
5
  SHA512:
6
- metadata.gz: 4809033fde8d06502a1cde12873b1d3214b2f9799c56e9e5e8d390979e0c370332a69d072f780d903349f6115028fb9c35fc0abbc1b84290d380a8ec9e0ed20c
7
- data.tar.gz: 7756798eb9c1484c509eeddef382a1566dbfbe5fb00dbc133f1d0912589cbd44e6fa539dd5f1ab428de9a37abd2c5a2aa752354df04e299ddd697bfe3ca80e60
6
+ metadata.gz: 8f48a3c4bc8a0242a714d945aae2aaca68ae6c0a8a87cedb038a1229363545bb41f1a3c24f833db86c89e3468cc216c77536af23385cce37e6ce20f8a2e3b142
7
+ data.tar.gz: 9ba5616b479da6952fd44284792ce708712ed6576ba90fa11507581aefd344f30ccd22e5887e3200c714feff15e7e8c95742149f49ac2a5618b2d5ea0f0b742a
@@ -687,6 +687,10 @@ partition = nil)
687
687
  # attribute is not a collection, so this guard defers exactly that case
688
688
  # to `.validate` while leaving declared ranges checked at parse. A mapped
689
689
  # PORO has no `.validate` to defer into, so it keeps the eager check.
690
+ # singular_tolerance: :first on the KV lane (#971): collapse an
691
+ # over-ingested collection before the writer stores it whole.
692
+ value = value.first if attr.tolerates_singular_overflow?(value)
693
+
690
694
  if attr.collection? || !instance.is_a?(Lutaml::Model::Serialize)
691
695
  attr.valid_collection!(value, context)
692
696
  end
@@ -1031,6 +1031,16 @@ child_mappings, options)
1031
1031
  rule.attribute_type < Lutaml::Model::Serialize
1032
1032
  end
1033
1033
 
1034
+ if is_nested_model && !value.is_a?(Lutaml::Model::Serialize)
1035
+ # The KV mirror of the XML lane's #971 collapse: a tolerated
1036
+ # singular attribute holding an over-assigned collection
1037
+ # serializes its leading value instead of raising.
1038
+ attr = if model_class.respond_to?(:attributes)
1039
+ model_class.attributes[rule.attribute_name.to_sym]
1040
+ end
1041
+ value = value.first if attr&.tolerates_singular_overflow?(value)
1042
+ end
1043
+
1034
1044
  if is_nested_model
1035
1045
  # Validate that value matches the expected type
1036
1046
  # There are three valid cases:
@@ -47,6 +47,7 @@ module Lutaml
47
47
  min_length
48
48
  max_length
49
49
  desc
50
+ singular_tolerance
50
51
  ].freeze
51
52
 
52
53
  # Per-type-class memo of the custom from_xml/from_json probe used by #cast.
@@ -827,6 +828,17 @@ instance_object = nil)
827
828
  end
828
829
  end
829
830
 
831
+ # Singular-overflow tolerance (#971): ingest can hand a singular
832
+ # attribute a collection — relaton's upstream documents carry
833
+ # multiple <formattedref> elements per bibitem. With
834
+ # singular_tolerance: :first the leading value is kept instead of
835
+ # raising CollectionTrueMissingError; the default stays strict.
836
+ def tolerates_singular_overflow?(value)
837
+ !collection? && @options[:singular_tolerance] == :first &&
838
+ collection_instance?(value) &&
839
+ !value_policy.whole_value?(unresolved_type)
840
+ end
841
+
830
842
  def valid_collection!(value, caller)
831
843
  if collection_instance?(value) && !collection? &&
832
844
  !value_policy.whole_value?(unresolved_type)
@@ -1041,6 +1053,12 @@ instance_object = nil)
1041
1053
  "Use map_element \"name\", to: :#{name}, raw: :content instead."
1042
1054
  end
1043
1055
  @validations = @options[:validations]
1056
+ tolerance = @options[:singular_tolerance]
1057
+ unless tolerance.nil? || tolerance == :first
1058
+ raise ArgumentError,
1059
+ "Invalid singular_tolerance: #{tolerance.inspect} for " \
1060
+ "attribute '#{name}'. Supported: :first"
1061
+ end
1044
1062
  set_default_for_collection if collection?
1045
1063
  end
1046
1064
 
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Lutaml
4
4
  module Model
5
- VERSION = "0.8.112"
5
+ VERSION = "0.8.114"
6
6
  end
7
7
  end
@@ -479,6 +479,11 @@ module Lutaml
479
479
  # violation entirely — check it while there is still someone to tell.
480
480
  if !instance_is_serialize && attr && !attr.collection? &&
481
481
  !rule.content_mapping?
482
+ # singular_tolerance: :first collapses an over-ingested
483
+ # collection to its leading value before the writer sees it
484
+ # (#971) — the reduced scalar then flows through
485
+ # deserialize and serializes as a single element.
486
+ value = value.first if attr.tolerates_singular_overflow?(value)
482
487
  attr.valid_collection!(value, context)
483
488
  end
484
489
  rule.deserialize(instance, value, deserialize_attributes, context,
data/lutaml-model.gemspec CHANGED
@@ -41,7 +41,7 @@ Gem::Specification.new do |spec|
41
41
  # 0.5.84 always passes XML_PARSE_HUGE to libxml2: very large documents
42
42
  # parse completely instead of being silently truncated at the 10 MB
43
43
  # input-buffer cap (lutaml-model#871).
44
- spec.add_dependency "moxml", "~> 0.5.84"
44
+ spec.add_dependency "moxml", "~> 0.5.139"
45
45
  spec.add_dependency "ostruct"
46
46
  spec.add_dependency "rubyzip", "~> 3.7"
47
47
  spec.add_dependency "thor", "~> 1.4"
@@ -0,0 +1,159 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "spec_helper"
4
+
5
+ # #971: a singular attribute may ask ingest to keep the leading value
6
+ # when the mapper collects more than one element (relaton's upstream
7
+ # bibitems carry multiple <formattedref> elements). Default stays
8
+ # strict: the over-count remains a CollectionTrueMissingError.
9
+ RSpec.describe "singular_tolerance attribute option" do
10
+ let(:item_class) do
11
+ Class.new(Lutaml::Model::Serializable) do
12
+ attribute :content, :string
13
+ end
14
+ end
15
+
16
+ before do
17
+ stub_const("ToleranceItem", item_class)
18
+ end
19
+
20
+ describe "option validation" do
21
+ it "accepts :first" do
22
+ expect do
23
+ Lutaml::Model::Attribute.new(:ref, "ToleranceItem",
24
+ singular_tolerance: :first)
25
+ end.not_to raise_error
26
+ end
27
+
28
+ it "accepts omission (strict default)" do
29
+ expect do
30
+ Lutaml::Model::Attribute.new(:ref, "ToleranceItem")
31
+ end.not_to raise_error
32
+ end
33
+
34
+ it "rejects unsupported tolerances" do
35
+ expect do
36
+ Lutaml::Model::Attribute.new(:ref, "ToleranceItem",
37
+ singular_tolerance: :last)
38
+ end.to raise_error(ArgumentError, /singular_tolerance: :last/)
39
+ end
40
+ end
41
+
42
+ describe "#tolerates_singular_overflow?" do
43
+ let(:tolerant) do
44
+ Lutaml::Model::Attribute.new(:ref, "ToleranceItem",
45
+ singular_tolerance: :first)
46
+ end
47
+
48
+ let(:strict) { Lutaml::Model::Attribute.new(:ref, "ToleranceItem") }
49
+
50
+ let(:collection_attr) do
51
+ Lutaml::Model::Attribute.new(:refs, "ToleranceItem",
52
+ collection: true,
53
+ singular_tolerance: :first)
54
+ end
55
+
56
+ it "is true for an over-ingested collection on a tolerant singular" do
57
+ expect(tolerant.tolerates_singular_overflow?([1, 2])).to be(true)
58
+ end
59
+
60
+ it "is false without the option" do
61
+ expect(strict.tolerates_singular_overflow?([1, 2])).to be(false)
62
+ end
63
+
64
+ it "is false for a scalar on a tolerant singular" do
65
+ expect(tolerant.tolerates_singular_overflow?(1)).to be(false)
66
+ end
67
+
68
+ it "is false for a collection attribute — the option does not apply" do
69
+ expect(collection_attr.tolerates_singular_overflow?([1, 2])).to be(false)
70
+ end
71
+ end
72
+
73
+ describe "strict default" do
74
+ it "still raises CollectionTrueMissingError on over-ingest" do
75
+ attr = Lutaml::Model::Attribute.new(:ref, "ToleranceItem")
76
+
77
+ expect do
78
+ attr.valid_collection!([item_class.new, item_class.new], String)
79
+ end.to raise_error(Lutaml::Model::CollectionTrueMissingError)
80
+ end
81
+ end
82
+
83
+ describe "tolerant ingest" do
84
+ it "lets valid_collection! pass after the transform reduces to first" do
85
+ attr = Lutaml::Model::Attribute.new(:ref, "ToleranceItem",
86
+ singular_tolerance: :first)
87
+ value = [item_class.new, item_class.new]
88
+
89
+ reduced = value.first if attr.tolerates_singular_overflow?(value)
90
+ expect(attr.valid_collection!(reduced, String)).to be(true)
91
+ expect(reduced).to eq(value.first)
92
+ end
93
+ end
94
+
95
+ describe "on the KeyValue lane" do
96
+ let(:marked_class) do
97
+ Class.new(Lutaml::Model::Serializable) do
98
+ attribute :content, :string
99
+
100
+ key_value do
101
+ map "content", to: :content
102
+ end
103
+ end
104
+ end
105
+
106
+ let(:relation_class) do
107
+ Class.new(Lutaml::Model::Serializable) do
108
+ attribute :rel_type, :string
109
+ attribute :description, "ToleranceMarked", singular_tolerance: :first
110
+
111
+ key_value do
112
+ map "type", to: :rel_type
113
+ map "description", to: :description
114
+ end
115
+ end
116
+ end
117
+
118
+ before do
119
+ stub_const("ToleranceMarked", marked_class)
120
+ stub_const("ToleranceRelation", relation_class)
121
+ end
122
+
123
+ it "keeps the leading value on ingest instead of the whole array" do
124
+ model = ToleranceRelation.from_yaml(
125
+ "---\ntype: cites\ndescription:\n - content: First\n - content: Second\n",
126
+ )
127
+
128
+ expect(model.description).to be_a(ToleranceMarked)
129
+ expect(model.description.content).to eq("First")
130
+ end
131
+
132
+ it "round-trips the collapsed singular through to_yaml" do
133
+ model = ToleranceRelation.from_yaml(
134
+ "---\ntype: cites\ndescription:\n - content: First\n - content: Second\n",
135
+ )
136
+
137
+ yaml = model.to_yaml
138
+ expect(yaml).to include("First")
139
+ expect(yaml).not_to include("Second")
140
+ expect(ToleranceRelation.from_yaml(yaml).description.content).to eq("First")
141
+ end
142
+
143
+ it "collapses an over-assigned collection at serialization" do
144
+ model = ToleranceRelation.new(
145
+ rel_type: "cites",
146
+ description: [ToleranceMarked.new(content: "One"),
147
+ ToleranceMarked.new(content: "Two")],
148
+ )
149
+
150
+ yaml = model.to_yaml
151
+ expect(yaml).to include("One")
152
+ expect(yaml).not_to include("Two")
153
+ end
154
+ end
155
+ end
156
+
157
+ # The KV mirror of the XML-lane cases: the YAML lane never consulted
158
+ # the tolerance — ingest stored the whole array and re-serialization
159
+ # raised the mapped-PORO type check.
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: lutaml-model
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.8.112
4
+ version: 0.8.114
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ribose Inc.
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2026-10-17 00:00:00.000000000 Z
11
+ date: 2026-10-18 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: base64
@@ -92,14 +92,14 @@ dependencies:
92
92
  requirements:
93
93
  - - "~>"
94
94
  - !ruby/object:Gem::Version
95
- version: 0.5.84
95
+ version: 0.5.139
96
96
  type: :runtime
97
97
  prerelease: false
98
98
  version_requirements: !ruby/object:Gem::Requirement
99
99
  requirements:
100
100
  - - "~>"
101
101
  - !ruby/object:Gem::Version
102
- version: 0.5.84
102
+ version: 0.5.139
103
103
  - !ruby/object:Gem::Dependency
104
104
  name: ostruct
105
105
  requirement: !ruby/object:Gem::Requirement
@@ -1824,6 +1824,7 @@ files:
1824
1824
  - spec/lutaml/model/serialize_perf_guard_spec.rb
1825
1825
  - spec/lutaml/model/services/transformer_spec.rb
1826
1826
  - spec/lutaml/model/simple_model_spec.rb
1827
+ - spec/lutaml/model/singular_tolerance_spec.rb
1827
1828
  - spec/lutaml/model/store_spec.rb
1828
1829
  - spec/lutaml/model/toml_adapter_spec.rb
1829
1830
  - spec/lutaml/model/toml_spec.rb