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 +4 -4
- data/lib/lutaml/key_value/transform.rb +4 -0
- data/lib/lutaml/key_value/transformation.rb +10 -0
- data/lib/lutaml/model/attribute.rb +18 -0
- data/lib/lutaml/model/version.rb +1 -1
- data/lib/lutaml/xml/model_transform.rb +5 -0
- data/lutaml-model.gemspec +1 -1
- data/spec/lutaml/model/singular_tolerance_spec.rb +159 -0
- metadata +5 -4
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 381cba1519b14ff18965d44164e7d75e4564ac92c803815dad95277b7076ef70
|
|
4
|
+
data.tar.gz: acd3466cbc30ed27ce60ce62b181732ab957abde427da1e609003ced88c47a67
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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
|
|
data/lib/lutaml/model/version.rb
CHANGED
|
@@ -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.
|
|
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.
|
|
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-
|
|
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.
|
|
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.
|
|
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
|