lutaml-xsd 1.2.1 → 1.2.2
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/Gemfile.lock +3 -3
- data/TODO.conformance.md +8 -2
- data/lib/lutaml/xsd/validation/unique_particle_attribution.rb +302 -0
- data/lib/lutaml/xsd/version.rb +1 -1
- data/lutaml-xsd.gemspec +1 -1
- 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: a22b97347b4ad22fc7698a047a1037f4b16c5a6bad3ed0154701908f0052ea29
|
|
4
|
+
data.tar.gz: 0a3d2bd48d987524cbdc427adefcdc128ebcd089a2ed5e25203c28ff27ba4416
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 304b7555580c9bd6b79464b3b2d9ff73d9c69177790fbcd8bb97b9b0438fdc96b19ce19bdbaba4a9a3c2c6446402e81ee457614cd3e33c3dac35e02b2c948d74
|
|
7
|
+
data.tar.gz: 39593e00b27fb41c0fe4e374b682c4172dabcf56cd8250e2917f76c135e9f6421e1c65eaca4c0c3896a7d03bae1395fa18c8a1b6f8e38bb3e23cbfa4e195ef6a
|
data/Gemfile.lock
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
PATH
|
|
2
2
|
remote: .
|
|
3
3
|
specs:
|
|
4
|
-
lutaml-xsd (1.2.
|
|
4
|
+
lutaml-xsd (1.2.1)
|
|
5
5
|
liquid (>= 4.0, < 6.0)
|
|
6
|
-
lutaml-model (~> 0.8.
|
|
6
|
+
lutaml-model (~> 0.8.123)
|
|
7
7
|
lutaml-store (~> 0.3)
|
|
8
8
|
moxml (~> 0.5.147)
|
|
9
9
|
paint (~> 2.3)
|
|
@@ -264,7 +264,7 @@ CHECKSUMS
|
|
|
264
264
|
liquid (5.14.0) sha256=e1020c2b2ee58c7a4d3bfdc7a331537bd27a783353748bff0bf27c6ac0c17065
|
|
265
265
|
lutaml-model (0.8.123) sha256=475f391371965f5baee2a9d018a7c9da8361c3774d3802749aa8805ee1253fde
|
|
266
266
|
lutaml-store (0.3.2) sha256=9c455dbab32b4c635c48a27213f6f56fda191731c4574eb6187184ba0588a672
|
|
267
|
-
lutaml-xsd (1.2.
|
|
267
|
+
lutaml-xsd (1.2.1)
|
|
268
268
|
memo_wise (1.13.0) sha256=30220c38c4cef410849bc73553c58664dc2c91c6379e4a1df22aea02358b716b
|
|
269
269
|
moxml (0.5.153) sha256=561c6df843edd4a3b7f4c6785f9cfd6fef61050df7f1113fdcd45d8495704fc2
|
|
270
270
|
nokogiri (1.19.4-aarch64-linux-gnu) sha256=1269fb644a6de405057a53dd5c762b1209b43ca7424f839454d3dbc677c31a8f
|
data/TODO.conformance.md
CHANGED
|
@@ -89,7 +89,13 @@ Remaining engine-mode gaps (tracked, moxml/leptris-side):
|
|
|
89
89
|
resolution (unknown-type rejection; content walk follows the
|
|
90
90
|
override); the engine's valid-derived false positive is fixed in
|
|
91
91
|
leptris 1.9.380.2 (moxml#433)
|
|
92
|
-
- [
|
|
93
|
-
|
|
92
|
+
- [x] Particle checking: UPA detection (2026-10-22) —
|
|
93
|
+
`UniqueParticleAttribution` schema-side checker: first-set
|
|
94
|
+
computation over sequence/choice/all with wildcard/element
|
|
95
|
+
overlap, choice-alternative and sequence-junction conflict rules,
|
|
96
|
+
container occurrence folding. Standalone API
|
|
97
|
+
(`findings` → upa_violation findings); zero findings on the
|
|
98
|
+
conformance corpus schemas (no false positives). Deferred depth:
|
|
99
|
+
xs:group expansion, substitution-group-aware first-sets
|
|
94
100
|
- [ ] XSD 1.1 constructs behind a version flag — DEFERRED: 1.1 strategy
|
|
95
101
|
decision needed (see TODO.som.md)
|
|
@@ -0,0 +1,302 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Lutaml
|
|
4
|
+
module Xsd
|
|
5
|
+
module Validation
|
|
6
|
+
# Schema-side Unique Particle Attribution checking (XSD 1.0 §3.8.6).
|
|
7
|
+
#
|
|
8
|
+
# Reports content models where an instance element could match more
|
|
9
|
+
# than one particle, making a valid document's interpretation
|
|
10
|
+
# ambiguous:
|
|
11
|
+
# - xs:choice: overlapping first-sets between alternatives
|
|
12
|
+
# - xs:sequence: a nullable or repeatable particle followed by a
|
|
13
|
+
# particle matching the same first element
|
|
14
|
+
# - element vs wildcard and wildcard vs wildcard overlaps at the
|
|
15
|
+
# same junctions
|
|
16
|
+
#
|
|
17
|
+
# First-sets are computed recursively over nested
|
|
18
|
+
# sequence/choice/all containers; xs:group references are treated
|
|
19
|
+
# as opaque (not descended) and contribute no names.
|
|
20
|
+
#
|
|
21
|
+
# findings = UniqueParticleAttribution.new(schema).findings
|
|
22
|
+
# findings.each { |f| puts "#{f.code}: #{f.message}" }
|
|
23
|
+
class UniqueParticleAttribution
|
|
24
|
+
Finding = Struct.new(:code, :message, :path, keyword_init: true)
|
|
25
|
+
|
|
26
|
+
FirstSet = Struct.new(:names, :wildcards, :nullable, :repeatable) do
|
|
27
|
+
def merge(other)
|
|
28
|
+
FirstSet.new(
|
|
29
|
+
names | other.names,
|
|
30
|
+
wildcards | other.wildcards,
|
|
31
|
+
nullable || other.nullable,
|
|
32
|
+
)
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def overlaps?(other)
|
|
36
|
+
names.intersect?(other.names) ||
|
|
37
|
+
wildcards.any? { |w| other.wildcards.any? { |o| wildcard_overlap?(w, o) } } ||
|
|
38
|
+
wildcards.any? { |w| other.names.any? { |name| wildcard_admits?(w, name) } } ||
|
|
39
|
+
other.wildcards.any? { |w| names.any? { |name| wildcard_admits?(w, name) } }
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
private
|
|
43
|
+
|
|
44
|
+
# Conservative wildcard-pair overlap: ##any overlaps everything;
|
|
45
|
+
# distinct constraints are treated as overlapping only when
|
|
46
|
+
# identical (##local, ##other, lists, single namespaces).
|
|
47
|
+
def wildcard_overlap?(left, right)
|
|
48
|
+
left == right || left == "##any" || right == "##any"
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def wildcard_admits?(wildcard, (namespace, _name))
|
|
52
|
+
case wildcard
|
|
53
|
+
when "##any", "" then true
|
|
54
|
+
when "##local" then namespace.empty?
|
|
55
|
+
when "##other" then !namespace.empty?
|
|
56
|
+
when "##targetNamespace" then false
|
|
57
|
+
else wildcard.split(/\s+/).include?(namespace)
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
EMPTY_FIRST = FirstSet.new([].freeze, [].freeze, true).freeze
|
|
63
|
+
|
|
64
|
+
# @param schema [Lutaml::Xml::Schema::Xsd::Schema] parsed schema
|
|
65
|
+
def initialize(schema)
|
|
66
|
+
@schema = schema
|
|
67
|
+
@seen = {}.compare_by_identity
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
# @return [Array<Finding>] UPA violations, empty when deterministic
|
|
71
|
+
def findings
|
|
72
|
+
compute! unless @computed
|
|
73
|
+
@findings
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
private
|
|
77
|
+
|
|
78
|
+
def compute!
|
|
79
|
+
@findings = []
|
|
80
|
+
Array(@schema.element).each do |element|
|
|
81
|
+
check_type(element.complex_type, "/#{element.name}") if
|
|
82
|
+
element.complex_type
|
|
83
|
+
end
|
|
84
|
+
Array(@schema.complex_type).each do |complex_type|
|
|
85
|
+
check_type(complex_type, complex_type.name.to_s)
|
|
86
|
+
end
|
|
87
|
+
@computed = true
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def check_type(type, path)
|
|
91
|
+
return unless type.is_a?(Lutaml::Xml::Schema::Xsd::ComplexType)
|
|
92
|
+
|
|
93
|
+
container = type.sequence || type.choice || type.all
|
|
94
|
+
check_container(container, path) if container
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
# Walks a container: checks its own junctions, then descends.
|
|
98
|
+
def check_container(container, path)
|
|
99
|
+
return unless container
|
|
100
|
+
return if @seen[container]
|
|
101
|
+
|
|
102
|
+
@seen[container] = true
|
|
103
|
+
|
|
104
|
+
particles = ordered_particles(container)
|
|
105
|
+
check_junctions(particles, container, path)
|
|
106
|
+
particles.each do |particle|
|
|
107
|
+
case particle
|
|
108
|
+
when Lutaml::Xsd::Sequence, Lutaml::Xsd::Choice, Lutaml::Xsd::All
|
|
109
|
+
check_container(particle, "#{path}/#{particle_name(particle)}")
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
def check_junctions(particles, container, path)
|
|
115
|
+
firsts = particles.map { |p| first_set(p) }
|
|
116
|
+
|
|
117
|
+
if container.is_a?(Lutaml::Xsd::Choice)
|
|
118
|
+
check_choice_overlap(particles, firsts, path)
|
|
119
|
+
else
|
|
120
|
+
check_sequence_junctions(particles, firsts, path)
|
|
121
|
+
end
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
def check_choice_overlap(particles, firsts, path)
|
|
125
|
+
firsts.each_with_index do |first, i|
|
|
126
|
+
firsts[(i + 1)..].each_with_index do |other, j|
|
|
127
|
+
next unless first.overlaps?(other)
|
|
128
|
+
|
|
129
|
+
report(
|
|
130
|
+
path,
|
|
131
|
+
"ambiguous choice: '#{particle_name(particles[i])}' and " \
|
|
132
|
+
"'#{particle_name(particles[i + 1 + j])}' can match the " \
|
|
133
|
+
"same element",
|
|
134
|
+
)
|
|
135
|
+
end
|
|
136
|
+
end
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
def check_sequence_junctions(particles, firsts, path)
|
|
140
|
+
firsts.each_with_index do |first, i|
|
|
141
|
+
next if i.zero?
|
|
142
|
+
|
|
143
|
+
previous = firsts[i - 1]
|
|
144
|
+
next unless previous.nullable || previous.repeatable
|
|
145
|
+
next unless first.overlaps?(previous)
|
|
146
|
+
|
|
147
|
+
report(
|
|
148
|
+
path,
|
|
149
|
+
"ambiguous sequence: '#{particle_name(particles[i - 1])}' " \
|
|
150
|
+
"can be followed by '#{particle_name(particles[i])}' — both " \
|
|
151
|
+
"match the same first element",
|
|
152
|
+
)
|
|
153
|
+
end
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
def report(path, message)
|
|
157
|
+
@findings << Finding.new(
|
|
158
|
+
code: "upa_violation",
|
|
159
|
+
message: message,
|
|
160
|
+
path: path,
|
|
161
|
+
)
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
# First-set of a particle: the element names and wildcards it can
|
|
165
|
+
# match first, and whether it can be absent. Repeatable marks
|
|
166
|
+
# particles whose occurrence permits more than one match.
|
|
167
|
+
def first_set(particle)
|
|
168
|
+
case particle
|
|
169
|
+
when Lutaml::Xml::Schema::Xsd::Element
|
|
170
|
+
FirstSet.new([element_key(particle)].compact, [].freeze,
|
|
171
|
+
parse_min(particle).zero?).tap do |first|
|
|
172
|
+
first.repeatable = parse_max(particle) != 1
|
|
173
|
+
end
|
|
174
|
+
when Lutaml::Xsd::Any
|
|
175
|
+
FirstSet.new([].freeze, [wildcard_spec(particle)].compact,
|
|
176
|
+
parse_min(particle).zero?).tap do |first|
|
|
177
|
+
first.repeatable = parse_max(particle) != 1
|
|
178
|
+
end
|
|
179
|
+
when Lutaml::Xsd::Sequence
|
|
180
|
+
apply_occurrence(sequence_first(particle), particle)
|
|
181
|
+
when Lutaml::Xsd::Choice, Lutaml::Xsd::All
|
|
182
|
+
apply_occurrence(choice_first(particle), particle)
|
|
183
|
+
else
|
|
184
|
+
EMPTY_FIRST
|
|
185
|
+
end
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
# A container particle's own occurrence widens its computed
|
|
189
|
+
# first-set: minOccurs="0" makes it nullable, max≠1 repeatable.
|
|
190
|
+
def apply_occurrence(first, particle)
|
|
191
|
+
first.nullable ||= parse_min(particle).zero?
|
|
192
|
+
first.repeatable ||= parse_max(particle) != 1
|
|
193
|
+
first
|
|
194
|
+
end
|
|
195
|
+
|
|
196
|
+
# Leading names until the first required, exactly-once particle;
|
|
197
|
+
# nullable only when every particle can be absent.
|
|
198
|
+
def sequence_first(sequence)
|
|
199
|
+
names = []
|
|
200
|
+
wildcards = []
|
|
201
|
+
nullable = true
|
|
202
|
+
repeatable = false
|
|
203
|
+
ordered_particles(sequence).each do |particle|
|
|
204
|
+
first = first_set(particle)
|
|
205
|
+
names |= first.names
|
|
206
|
+
wildcards |= first.wildcards
|
|
207
|
+
repeatable ||= first.repeatable
|
|
208
|
+
unless first.nullable
|
|
209
|
+
nullable = false
|
|
210
|
+
break
|
|
211
|
+
end
|
|
212
|
+
end
|
|
213
|
+
FirstSet.new(names, wildcards, nullable).tap do |first|
|
|
214
|
+
first.repeatable = repeatable
|
|
215
|
+
end
|
|
216
|
+
end
|
|
217
|
+
|
|
218
|
+
def choice_first(choice)
|
|
219
|
+
names = []
|
|
220
|
+
wildcards = []
|
|
221
|
+
nullable = false
|
|
222
|
+
repeatable = false
|
|
223
|
+
ordered_particles(choice).each do |particle|
|
|
224
|
+
first = first_set(particle)
|
|
225
|
+
names |= first.names
|
|
226
|
+
wildcards |= first.wildcards
|
|
227
|
+
nullable ||= first.nullable
|
|
228
|
+
repeatable ||= first.repeatable
|
|
229
|
+
end
|
|
230
|
+
FirstSet.new(names, wildcards, nullable).tap do |first|
|
|
231
|
+
first.repeatable = repeatable
|
|
232
|
+
end
|
|
233
|
+
end
|
|
234
|
+
|
|
235
|
+
# Particles in declaration order — resolved_element_order preserves
|
|
236
|
+
# the source sequence including xs:any, which attribute-style
|
|
237
|
+
# accessor reads would reorder.
|
|
238
|
+
def ordered_particles(container)
|
|
239
|
+
ordered = container.resolved_element_order
|
|
240
|
+
return ordered.to_a if ordered.to_a.any?
|
|
241
|
+
|
|
242
|
+
case container
|
|
243
|
+
when Lutaml::Xml::Schema::Xsd::Sequence,
|
|
244
|
+
Lutaml::Xml::Schema::Xsd::Choice
|
|
245
|
+
%i[element any sequence choice].flat_map do |kind|
|
|
246
|
+
Array(container.public_send(kind))
|
|
247
|
+
end.compact
|
|
248
|
+
when Lutaml::Xml::Schema::Xsd::All
|
|
249
|
+
Array(container.element).compact
|
|
250
|
+
else
|
|
251
|
+
[]
|
|
252
|
+
end
|
|
253
|
+
end
|
|
254
|
+
|
|
255
|
+
def element_key(element)
|
|
256
|
+
name = element.name || element.ref
|
|
257
|
+
return nil if name.to_s.empty?
|
|
258
|
+
|
|
259
|
+
namespace = if element.name
|
|
260
|
+
element.target_namespace.to_s
|
|
261
|
+
else
|
|
262
|
+
@schema.target_namespace.to_s
|
|
263
|
+
end
|
|
264
|
+
[namespace, name.to_s]
|
|
265
|
+
end
|
|
266
|
+
|
|
267
|
+
def wildcard_spec(any_particle)
|
|
268
|
+
any_particle.namespace.to_s
|
|
269
|
+
end
|
|
270
|
+
|
|
271
|
+
def parse_min(particle)
|
|
272
|
+
value = particle.min_occurs.to_s
|
|
273
|
+
value.empty? ? 1 : value.to_i
|
|
274
|
+
rescue StandardError
|
|
275
|
+
1
|
|
276
|
+
end
|
|
277
|
+
|
|
278
|
+
def parse_max(particle)
|
|
279
|
+
value = particle.max_occurs.to_s
|
|
280
|
+
return Float::INFINITY if value == "unbounded"
|
|
281
|
+
return 1 if value.empty?
|
|
282
|
+
|
|
283
|
+
value.to_i
|
|
284
|
+
rescue StandardError
|
|
285
|
+
1
|
|
286
|
+
end
|
|
287
|
+
|
|
288
|
+
def particle_name(particle)
|
|
289
|
+
case particle
|
|
290
|
+
when Lutaml::Xml::Schema::Xsd::Element
|
|
291
|
+
particle.name || particle.ref || "element"
|
|
292
|
+
when Lutaml::Xsd::Any then "any"
|
|
293
|
+
when Lutaml::Xsd::Choice then "choice"
|
|
294
|
+
when Lutaml::Xsd::Sequence then "sequence"
|
|
295
|
+
when Lutaml::Xsd::All then "all"
|
|
296
|
+
else particle.class.name.split("::").last
|
|
297
|
+
end
|
|
298
|
+
end
|
|
299
|
+
end
|
|
300
|
+
end
|
|
301
|
+
end
|
|
302
|
+
end
|
data/lib/lutaml/xsd/version.rb
CHANGED
data/lutaml-xsd.gemspec
CHANGED
|
@@ -32,7 +32,7 @@ Gem::Specification.new do |spec|
|
|
|
32
32
|
end
|
|
33
33
|
|
|
34
34
|
spec.add_dependency "liquid", ">= 4.0", "< 6.0"
|
|
35
|
-
spec.add_dependency "lutaml-model", "~> 0.8.
|
|
35
|
+
spec.add_dependency "lutaml-model", "~> 0.8.123"
|
|
36
36
|
spec.add_dependency "lutaml-store", "~> 0.3"
|
|
37
37
|
spec.add_dependency "moxml", "~> 0.5.147"
|
|
38
38
|
spec.add_dependency "paint", "~> 2.3"
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: lutaml-xsd
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 1.2.
|
|
4
|
+
version: 1.2.2
|
|
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-23 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: liquid
|
|
@@ -36,14 +36,14 @@ dependencies:
|
|
|
36
36
|
requirements:
|
|
37
37
|
- - "~>"
|
|
38
38
|
- !ruby/object:Gem::Version
|
|
39
|
-
version: 0.8.
|
|
39
|
+
version: 0.8.123
|
|
40
40
|
type: :runtime
|
|
41
41
|
prerelease: false
|
|
42
42
|
version_requirements: !ruby/object:Gem::Requirement
|
|
43
43
|
requirements:
|
|
44
44
|
- - "~>"
|
|
45
45
|
- !ruby/object:Gem::Version
|
|
46
|
-
version: 0.8.
|
|
46
|
+
version: 0.8.123
|
|
47
47
|
- !ruby/object:Gem::Dependency
|
|
48
48
|
name: lutaml-store
|
|
49
49
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -576,6 +576,7 @@ files:
|
|
|
576
576
|
- lib/lutaml/xsd/validation/schema_definition_resolver.rb
|
|
577
577
|
- lib/lutaml/xsd/validation/schema_location_extractor.rb
|
|
578
578
|
- lib/lutaml/xsd/validation/schema_resolver.rb
|
|
579
|
+
- lib/lutaml/xsd/validation/unique_particle_attribution.rb
|
|
579
580
|
- lib/lutaml/xsd/validation/validation_configuration.rb
|
|
580
581
|
- lib/lutaml/xsd/validation/validation_error.rb
|
|
581
582
|
- lib/lutaml/xsd/validation/validation_job.rb
|