moxml 0.5.163 → 0.5.166
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/docs/_pages/model-coverage.adoc +2 -0
- data/docs/_references/processing-model.adoc +19 -2
- data/lib/moxml/version.rb +1 -1
- data/lib/moxml/xsd/schema_rules.rb +386 -33
- data/lib/moxml/xsd.rb +5 -3
- data/spec/moxml/xsd_regex_spec.rb +188 -0
- data/spec/moxml/xsd_substitution_spec.rb +166 -0
- data/spec/support/model_coverage.rb +8 -0
- metadata +4 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 932f907222ac657e72e53b9e5ac2e514bb6aa07a51da815caa7ee31c0b0a02fa
|
|
4
|
+
data.tar.gz: 11c82e15a42a80adb5bb19b4b819e9b4cfa1e7e508c6088d383dfdd793a33956
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 2ef7a9115c29ad09a1b3cb91e3c1c7c8cae297529de98cc772b6939b1e3a893c9820889b85b61f82cfd52c6e9536f688b841551c6f125c09aea0fee61ae12d10
|
|
7
|
+
data.tar.gz: c2822eb479197ddf8151820c1c28e3ce61f49b2a1b3263d1fa4ffc7bc581e387aa52799faef3fba303fb9113bed5ac51cb5b93b5736ed1f03884b398c5a6df15
|
|
@@ -42,6 +42,8 @@ Generated by `bundle exec rake docs:coverage` from the coverage matrix and the d
|
|
|
42
42
|
| validation | equivalent | equivalent | equivalent | equivalent | equivalent | equivalent | equivalent
|
|
43
43
|
| validation under-detections | equivalent | equivalent | equivalent | equivalent | equivalent | equivalent | equivalent
|
|
44
44
|
| validation version reporting | equivalent | equivalent | equivalent | equivalent | equivalent | equivalent | equivalent
|
|
45
|
+
| validation regex dismissal | equivalent | equivalent | equivalent | equivalent | equivalent | equivalent | equivalent
|
|
46
|
+
| validation substitution dismissal | equivalent | equivalent | equivalent | equivalent | equivalent | equivalent | equivalent
|
|
45
47
|
| loud capability contracts | equivalent | equivalent | equivalent | equivalent | equivalent | equivalent | equivalent
|
|
46
48
|
|===
|
|
47
49
|
|
|
@@ -255,7 +255,7 @@ semantics.
|
|
|
255
255
|
|
|
256
256
|
=== Validation
|
|
257
257
|
|
|
258
|
-
Pins:: `spec/moxml/xsd_spec.rb`, `spec/moxml/xsd_underdetection_spec.rb`, `spec/moxml/xsd_version_spec.rb`
|
|
258
|
+
Pins:: `spec/moxml/xsd_regex_spec.rb`, `spec/moxml/xsd_spec.rb`, `spec/moxml/xsd_substitution_spec.rb`, `spec/moxml/xsd_underdetection_spec.rb`, `spec/moxml/xsd_version_spec.rb`
|
|
259
259
|
|
|
260
260
|
XSD validation through engines that ship a validator (libxml2;
|
|
261
261
|
leptris tier-1: recognized roots, one-level content descent, lexical
|
|
@@ -267,7 +267,24 @@ scope validate laxly, except that strict `xs:any` /
|
|
|
267
267
|
negative wildcards (`notQName` / `notNamespace` — an item excluded
|
|
268
268
|
by them has no particle and the content model cannot accept it) are
|
|
269
269
|
re-checked from the schema source on every adapter — the re-check
|
|
270
|
-
can only under-report, never false-report.
|
|
270
|
+
can only under-report, never false-report. Engine lexical findings
|
|
271
|
+
that the source's own `pattern` facets refute are dismissed: an
|
|
272
|
+
engine that cannot evaluate a dialect construct (e.g. the
|
|
273
|
+
`\p{Is...}` block escapes) rejects every occurrence of the facet,
|
|
274
|
+
so the facade re-checks the value against the facet translated to
|
|
275
|
+
the host regex dialect — only whitelisted constructs translate
|
|
276
|
+
(block escapes with Unicode-rename aliases, general categories,
|
|
277
|
+
single-character escapes; subtraction, `\i`, `\c` do not), an
|
|
278
|
+
engine finding is dropped only when every same-named instance
|
|
279
|
+
matches the facet, and anything untranslatable keeps the engine's
|
|
280
|
+
answer, so a dismissal can only remove false rejections, never
|
|
281
|
+
create false acceptances. Engine content-model findings that reject
|
|
282
|
+
a substitution-group member are dismissed likewise: the parent's
|
|
283
|
+
model must be a pure sequence, the child sequence must match it
|
|
284
|
+
position-for-position with every particle exactly-once, and a
|
|
285
|
+
registered member must realize a position — any choice/all,
|
|
286
|
+
cardinality violation, or unknown name keeps the engine's answer.
|
|
287
|
+
A compiled schema reports the
|
|
271
288
|
version its source declares: root `vc:minVersion="1.1"` scopes the
|
|
272
289
|
document to 1.1, anything else is 1.0 (consumers scope 1.0-only
|
|
273
290
|
checks with it; applying 1.1 semantics is the engine-side program).
|
data/lib/moxml/version.rb
CHANGED
|
@@ -105,7 +105,9 @@ module Moxml
|
|
|
105
105
|
when "##defined" then :defined
|
|
106
106
|
when "##definedSibling" then :sibling
|
|
107
107
|
when /\A([^:]+):(.+)\z/
|
|
108
|
-
uri = prefix_uri(
|
|
108
|
+
uri = ::Moxml::XSD::SchemaRules.prefix_uri(
|
|
109
|
+
Regexp.last_match(1), element
|
|
110
|
+
)
|
|
109
111
|
"{#{uri}}#{Regexp.last_match(2)}" unless uri.empty?
|
|
110
112
|
else
|
|
111
113
|
"{}#{token}"
|
|
@@ -113,23 +115,6 @@ module Moxml
|
|
|
113
115
|
end
|
|
114
116
|
end
|
|
115
117
|
|
|
116
|
-
# node.namespaces is adapter-divergent (some engines return
|
|
117
|
-
# in-scope bindings, others own-node declarations only), so
|
|
118
|
-
# the in-scope map is walked out by hand — per-node
|
|
119
|
-
# definitions merged along the ancestor axis, deeper
|
|
120
|
-
# overriding shallower, identically on every adapter.
|
|
121
|
-
def prefix_uri(prefix, element)
|
|
122
|
-
bindings = {}
|
|
123
|
-
node = element
|
|
124
|
-
while node
|
|
125
|
-
node.namespaces.each do |namespace|
|
|
126
|
-
bindings[namespace.prefix] = namespace.uri
|
|
127
|
-
end
|
|
128
|
-
node = node.parent&.element? ? node.parent : nil
|
|
129
|
-
end
|
|
130
|
-
bindings[prefix].to_s
|
|
131
|
-
end
|
|
132
|
-
|
|
133
118
|
def entry_covers?(entry, namespace)
|
|
134
119
|
namespace == case entry
|
|
135
120
|
when "##targetNamespace" then :tns
|
|
@@ -140,15 +125,120 @@ module Moxml
|
|
|
140
125
|
end
|
|
141
126
|
|
|
142
127
|
ContentModel = Struct.new(:declared_children, :any,
|
|
143
|
-
:declared_attributes, :any_attribute
|
|
128
|
+
:declared_attributes, :any_attribute,
|
|
129
|
+
:particles, :pure_sequence)
|
|
144
130
|
SchemaIndex = Struct.new(:target_namespace, :element_default,
|
|
145
131
|
:attribute_default, :elements,
|
|
146
|
-
:attributes, :contents, :version
|
|
132
|
+
:attributes, :contents, :version,
|
|
133
|
+
:element_patterns, :attribute_patterns,
|
|
134
|
+
:substitution_members)
|
|
135
|
+
|
|
136
|
+
# Resolves a QName token against an element's in-scope
|
|
137
|
+
# bindings. node.namespaces is adapter-divergent (some
|
|
138
|
+
# engines return in-scope bindings, others own-node
|
|
139
|
+
# declarations only), so the in-scope map is walked out by
|
|
140
|
+
# hand — per-node definitions merged along the ancestor
|
|
141
|
+
# axis, deeper overriding shallower, identically on every
|
|
142
|
+
# adapter. Unresolvable prefixes yield "".
|
|
143
|
+
module_function
|
|
144
|
+
|
|
145
|
+
def prefix_uri(prefix, element)
|
|
146
|
+
bindings = {}
|
|
147
|
+
node = element
|
|
148
|
+
while node
|
|
149
|
+
node.namespaces.each do |namespace|
|
|
150
|
+
bindings[namespace.prefix] = namespace.uri
|
|
151
|
+
end
|
|
152
|
+
node = node.parent&.element? ? node.parent : nil
|
|
153
|
+
end
|
|
154
|
+
bindings[prefix].to_s
|
|
155
|
+
end
|
|
147
156
|
|
|
148
157
|
# The XSD 1.1 version-tagging namespace (Structures §C.3):
|
|
149
158
|
# vc:minVersion / vc:maxVersion gate 1.1 constructs.
|
|
150
159
|
VC_NS = "http://www.w3.org/2001/XMLSchema-versioning"
|
|
151
160
|
|
|
161
|
+
# XSD pattern-facet dialect → Ruby regex, bounded to the
|
|
162
|
+
# subset the facade vouches for (Datatypes Appendix F). Block
|
|
163
|
+
# escapes \p{Is…}/\P{Is…} map to Ruby's In-prefixed blocks
|
|
164
|
+
# (a few Unicode-renamed blocks carry an alias); general
|
|
165
|
+
# categories and the single-character escapes pass through;
|
|
166
|
+
# \i, \c and class subtraction are NOT translated. Anything
|
|
167
|
+
# unrecognized — or a translation Ruby itself rejects —
|
|
168
|
+
# returns nil: the engine's answer then stands.
|
|
169
|
+
module XsdRegex
|
|
170
|
+
BLOCK_ALIASES = {
|
|
171
|
+
"Greek" => "GreekandCoptic",
|
|
172
|
+
"CombiningMarksforSymbols" =>
|
|
173
|
+
"CombiningDiacriticalMarksforSymbols",
|
|
174
|
+
"Turkish" => "Latin-1Supplement",
|
|
175
|
+
}.freeze
|
|
176
|
+
|
|
177
|
+
PASS_THROUGH_ESCAPES = %w[
|
|
178
|
+
n r t \\ . | ? * + ( ) [ ] { } ^ $ -
|
|
179
|
+
].freeze
|
|
180
|
+
|
|
181
|
+
module_function
|
|
182
|
+
|
|
183
|
+
def translate(pattern)
|
|
184
|
+
translated = scan(pattern.to_s)
|
|
185
|
+
return nil unless translated
|
|
186
|
+
|
|
187
|
+
Regexp.new("\\A(?:#{translated})\\z")
|
|
188
|
+
rescue RegexpError
|
|
189
|
+
nil
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
def scan(pattern)
|
|
193
|
+
out = +""
|
|
194
|
+
index = 0
|
|
195
|
+
while index < pattern.length
|
|
196
|
+
char = pattern[index]
|
|
197
|
+
if char == "\\"
|
|
198
|
+
consumed = escape(pattern, index, out)
|
|
199
|
+
return nil unless consumed
|
|
200
|
+
|
|
201
|
+
index += consumed
|
|
202
|
+
else
|
|
203
|
+
return nil if char == "-" && pattern[index + 1] == "["
|
|
204
|
+
|
|
205
|
+
out << char
|
|
206
|
+
index += 1
|
|
207
|
+
end
|
|
208
|
+
end
|
|
209
|
+
out
|
|
210
|
+
end
|
|
211
|
+
|
|
212
|
+
# Appends the translation of the escape at index to out and
|
|
213
|
+
# returns the chars consumed, or nil when unrecognized.
|
|
214
|
+
def escape(pattern, index, out)
|
|
215
|
+
kind = pattern[index + 1]
|
|
216
|
+
case kind
|
|
217
|
+
when "p", "P"
|
|
218
|
+
close = pattern.index("}", index)
|
|
219
|
+
return nil unless close
|
|
220
|
+
|
|
221
|
+
body = pattern[index + 3, close - index - 3]
|
|
222
|
+
if body&.start_with?("Is")
|
|
223
|
+
mapped = body.delete_prefix("Is")
|
|
224
|
+
mapped = BLOCK_ALIASES.fetch(mapped, mapped)
|
|
225
|
+
begin
|
|
226
|
+
Regexp.new("\\p{In#{mapped}}")
|
|
227
|
+
rescue RegexpError
|
|
228
|
+
return nil
|
|
229
|
+
end
|
|
230
|
+
out << "\\#{kind}{In#{mapped}}"
|
|
231
|
+
else
|
|
232
|
+
out << pattern[index..close]
|
|
233
|
+
end
|
|
234
|
+
close - index + 1
|
|
235
|
+
when *PASS_THROUGH_ESCAPES
|
|
236
|
+
out << pattern[index, 2]
|
|
237
|
+
2
|
|
238
|
+
end
|
|
239
|
+
end
|
|
240
|
+
end
|
|
241
|
+
|
|
152
242
|
class << self
|
|
153
243
|
# Index one schema document. Returns nil when the source
|
|
154
244
|
# cannot be indexed — the engine's answer then stands alone
|
|
@@ -161,7 +251,7 @@ module Moxml
|
|
|
161
251
|
root["targetNamespace"].to_s,
|
|
162
252
|
root["elementFormDefault"] == "qualified",
|
|
163
253
|
root["attributeFormDefault"] == "qualified",
|
|
164
|
-
{}, {}, {}, schema_version(root)
|
|
254
|
+
{}, {}, {}, schema_version(root), {}, {}, {}
|
|
165
255
|
)
|
|
166
256
|
child_elements(root).each do |decl|
|
|
167
257
|
case decl.name
|
|
@@ -169,6 +259,7 @@ module Moxml
|
|
|
169
259
|
when "attribute" then index_global_attribute(decl, index)
|
|
170
260
|
end
|
|
171
261
|
end
|
|
262
|
+
index_patterns(root, index)
|
|
172
263
|
index
|
|
173
264
|
rescue StandardError
|
|
174
265
|
nil
|
|
@@ -188,6 +279,226 @@ module Moxml
|
|
|
188
279
|
declared.to_s.start_with?("1.1") ? "1.1" : "1.0"
|
|
189
280
|
end
|
|
190
281
|
|
|
282
|
+
# Index the pattern facets the dismissal may vouch with
|
|
283
|
+
# (#464): named simpleTypes resolve through one type-ref
|
|
284
|
+
# hop, inline simpleTypes directly; keys are local names.
|
|
285
|
+
# A name declared twice with different pattern sets is
|
|
286
|
+
# ambiguous for name-based matching and is removed — an
|
|
287
|
+
# ambiguous vouch could mask a real violation.
|
|
288
|
+
def index_patterns(root, index)
|
|
289
|
+
named = {}
|
|
290
|
+
collect_named_patterns(root, named)
|
|
291
|
+
collect_decl_patterns(root, index, named)
|
|
292
|
+
end
|
|
293
|
+
|
|
294
|
+
def collect_named_patterns(node, named)
|
|
295
|
+
child_elements(node).each do |child|
|
|
296
|
+
if child.name == "simpleType" && child["name"] &&
|
|
297
|
+
(patterns = restriction_patterns(child))
|
|
298
|
+
named[child["name"]] = patterns
|
|
299
|
+
else
|
|
300
|
+
collect_named_patterns(child, named)
|
|
301
|
+
end
|
|
302
|
+
end
|
|
303
|
+
end
|
|
304
|
+
|
|
305
|
+
def collect_decl_patterns(node, index, named)
|
|
306
|
+
child_elements(node).each do |child|
|
|
307
|
+
case child.name
|
|
308
|
+
when "element"
|
|
309
|
+
register_pattern(index.element_patterns, child["name"],
|
|
310
|
+
decl_patterns(child, named))
|
|
311
|
+
when "attribute"
|
|
312
|
+
register_pattern(index.attribute_patterns, child["name"],
|
|
313
|
+
decl_patterns(child, named))
|
|
314
|
+
end
|
|
315
|
+
collect_decl_patterns(child, index, named)
|
|
316
|
+
end
|
|
317
|
+
end
|
|
318
|
+
|
|
319
|
+
def decl_patterns(decl, named)
|
|
320
|
+
inline = child_elements(decl).find do |child|
|
|
321
|
+
child.name == "simpleType"
|
|
322
|
+
end
|
|
323
|
+
if inline
|
|
324
|
+
restriction_patterns(inline)
|
|
325
|
+
elsif decl["type"]
|
|
326
|
+
named[decl["type"].split(":").last]
|
|
327
|
+
end
|
|
328
|
+
end
|
|
329
|
+
|
|
330
|
+
def restriction_patterns(simple_type)
|
|
331
|
+
restriction = child_elements(simple_type).find do |child|
|
|
332
|
+
child.name == "restriction"
|
|
333
|
+
end
|
|
334
|
+
return nil unless restriction
|
|
335
|
+
|
|
336
|
+
patterns = child_elements(restriction)
|
|
337
|
+
.select { |facet| facet.name == "pattern" }
|
|
338
|
+
.map { |facet| XsdRegex.translate(facet["value"].to_s) }
|
|
339
|
+
return nil if patterns.any?(&:nil?)
|
|
340
|
+
|
|
341
|
+
patterns
|
|
342
|
+
end
|
|
343
|
+
|
|
344
|
+
def register_pattern(sink, name, patterns)
|
|
345
|
+
return unless name && patterns && !patterns.empty?
|
|
346
|
+
|
|
347
|
+
sink[name] = sink.key?(name) ? nil : patterns
|
|
348
|
+
end
|
|
349
|
+
|
|
350
|
+
# Dismisses engine findings the schema source refutes:
|
|
351
|
+
#
|
|
352
|
+
# * lexical findings pattern facets accept (#464) — dropped
|
|
353
|
+
# only when every same-named instance matches the facet;
|
|
354
|
+
# * content-model findings that reject a substitution-group
|
|
355
|
+
# member (#466 class 1) — dropped only when the parent's
|
|
356
|
+
# content model is a pure sequence, the child sequence
|
|
357
|
+
# matches it position-for-position with every particle
|
|
358
|
+
# exactly-once, and a substitution member resolves a
|
|
359
|
+
# position. One non-vouching instance — or any real
|
|
360
|
+
# mismatch — keeps the engine's answer, so a dismissal
|
|
361
|
+
# can only remove false rejections, never create false
|
|
362
|
+
# acceptances.
|
|
363
|
+
def dismiss(engine_errors, doc, index)
|
|
364
|
+
return engine_errors unless index
|
|
365
|
+
|
|
366
|
+
vouched = vouch_table(doc, index)
|
|
367
|
+
engine_errors.reject do |error|
|
|
368
|
+
if (subject = lexical_subject(error.message))
|
|
369
|
+
vouched[subject]
|
|
370
|
+
elsif (sequence = content_model_subject(error.message))
|
|
371
|
+
sequence_vouched?(doc, index, *sequence)
|
|
372
|
+
end
|
|
373
|
+
end
|
|
374
|
+
end
|
|
375
|
+
|
|
376
|
+
LEXICAL_MESSAGE = /\A(?:element|attribute) '([^']+)' @\d+:\d+: (element|attribute) '([^']+)': lexical value does not match its type\z/
|
|
377
|
+
CONTENT_MODEL_MESSAGE = /\Aelement '([^']+)' @\d+:\d+: element '([^']+)': children do not match the content model \(at '([^']+)'\)\z/
|
|
378
|
+
|
|
379
|
+
private_constant :LEXICAL_MESSAGE, :CONTENT_MODEL_MESSAGE
|
|
380
|
+
|
|
381
|
+
def lexical_subject(message)
|
|
382
|
+
match = LEXICAL_MESSAGE.match(message.to_s)
|
|
383
|
+
return unless match
|
|
384
|
+
|
|
385
|
+
[match[2], match[3]]
|
|
386
|
+
end
|
|
387
|
+
|
|
388
|
+
def content_model_subject(message)
|
|
389
|
+
match = CONTENT_MODEL_MESSAGE.match(message.to_s)
|
|
390
|
+
return unless match
|
|
391
|
+
|
|
392
|
+
[match[2], match[3]]
|
|
393
|
+
end
|
|
394
|
+
|
|
395
|
+
# Every same-named element in the document must vouch: a
|
|
396
|
+
# sequence the finding names could be valid while another
|
|
397
|
+
# same-named one is not. The root is included — the
|
|
398
|
+
# engine's finding can name it.
|
|
399
|
+
def sequence_vouched?(doc, index, name, offender)
|
|
400
|
+
root = doc.root
|
|
401
|
+
return false unless root
|
|
402
|
+
|
|
403
|
+
witnesses = []
|
|
404
|
+
yield_self = root.name == name
|
|
405
|
+
witnesses << sequence_valid?(root, index, offender) if yield_self
|
|
406
|
+
each_named_element(root, name) do |element|
|
|
407
|
+
witnesses << sequence_valid?(element, index, offender)
|
|
408
|
+
end
|
|
409
|
+
!witnesses.empty? && witnesses.all?
|
|
410
|
+
end
|
|
411
|
+
|
|
412
|
+
def each_named_element(node, name, &block)
|
|
413
|
+
child_elements(node).each do |child|
|
|
414
|
+
yield child if child.name == name
|
|
415
|
+
each_named_element(child, name, &block)
|
|
416
|
+
end
|
|
417
|
+
end
|
|
418
|
+
|
|
419
|
+
# The airtight witness: child count equals particle count,
|
|
420
|
+
# every position resolves to its particle's name or a
|
|
421
|
+
# registered member of it, every particle occurs exactly
|
|
422
|
+
# once, and the reported offender is a substitution member
|
|
423
|
+
# realized at some position. Anything else leaves the
|
|
424
|
+
# engine's answer standing.
|
|
425
|
+
def sequence_valid?(element, index, offender)
|
|
426
|
+
model = index.contents[qname(element)]
|
|
427
|
+
return false unless model&.pure_sequence
|
|
428
|
+
|
|
429
|
+
particles = model.particles
|
|
430
|
+
children = child_elements(element)
|
|
431
|
+
return false if children.empty? || children.size != particles.size
|
|
432
|
+
|
|
433
|
+
resolved = false
|
|
434
|
+
children.each_with_index do |child, position|
|
|
435
|
+
particle_name, min, max = particles[position]
|
|
436
|
+
child_qname = qname(child)
|
|
437
|
+
return false unless exactly_once?(min, max)
|
|
438
|
+
|
|
439
|
+
if child_qname == particle_name
|
|
440
|
+
next
|
|
441
|
+
elsif index.substitution_members.fetch(particle_name, [])
|
|
442
|
+
.include?(child_qname)
|
|
443
|
+
resolved = true
|
|
444
|
+
else
|
|
445
|
+
return false
|
|
446
|
+
end
|
|
447
|
+
end
|
|
448
|
+
resolved && offender_realized?(children, offender, index)
|
|
449
|
+
end
|
|
450
|
+
|
|
451
|
+
def exactly_once?(min, max)
|
|
452
|
+
min <= 1 && (max == :unbounded || max >= 1)
|
|
453
|
+
end
|
|
454
|
+
|
|
455
|
+
def offender_realized?(children, offender, index)
|
|
456
|
+
members = index.substitution_members.values.flatten
|
|
457
|
+
children.any? do |child|
|
|
458
|
+
child.name == offender &&
|
|
459
|
+
members.include?(qname(child))
|
|
460
|
+
end
|
|
461
|
+
end
|
|
462
|
+
|
|
463
|
+
def vouch_table(doc, index)
|
|
464
|
+
root = doc.root
|
|
465
|
+
return {} unless root
|
|
466
|
+
|
|
467
|
+
elements = Hash.new { |hash, key| hash[key] = [] }
|
|
468
|
+
attributes = Hash.new { |hash, key| hash[key] = [] }
|
|
469
|
+
collect_witnesses(root, index, elements, attributes)
|
|
470
|
+
vouched = {}
|
|
471
|
+
elements.each do |name, matches|
|
|
472
|
+
vouched[["element", name]] = !matches.empty? && matches.all?
|
|
473
|
+
end
|
|
474
|
+
attributes.each do |name, matches|
|
|
475
|
+
vouched[["attribute", name]] = !matches.empty? && matches.all?
|
|
476
|
+
end
|
|
477
|
+
vouched
|
|
478
|
+
end
|
|
479
|
+
|
|
480
|
+
def collect_witnesses(node, index, elements, attributes)
|
|
481
|
+
child_elements(node).each do |child|
|
|
482
|
+
if (patterns = index.element_patterns[child.name])
|
|
483
|
+
elements[child.name] << (text_only?(child) &&
|
|
484
|
+
patterns.any? { |regex| regex.match?(child.text.to_s) })
|
|
485
|
+
end
|
|
486
|
+
child.attributes.each do |attribute|
|
|
487
|
+
next if attribute.namespace&.uri == XSI_NS
|
|
488
|
+
|
|
489
|
+
if (patterns = index.attribute_patterns[attribute.name])
|
|
490
|
+
attributes[attribute.name] <<
|
|
491
|
+
patterns.any? { |regex| regex.match?(attribute.value.to_s) }
|
|
492
|
+
end
|
|
493
|
+
end
|
|
494
|
+
collect_witnesses(child, index, elements, attributes)
|
|
495
|
+
end
|
|
496
|
+
end
|
|
497
|
+
|
|
498
|
+
def text_only?(element)
|
|
499
|
+
child_elements(element).empty?
|
|
500
|
+
end
|
|
501
|
+
|
|
191
502
|
# Post-validation findings for one instance document.
|
|
192
503
|
def findings(doc, index)
|
|
193
504
|
return [] unless index
|
|
@@ -215,6 +526,9 @@ module Moxml
|
|
|
215
526
|
name = decl["name"] or return
|
|
216
527
|
qname = "{#{index.target_namespace}}#{name}"
|
|
217
528
|
index.elements[qname] = { nillable: decl["nillable"] == "true" }
|
|
529
|
+
if (head = decl["substitutionGroup"])
|
|
530
|
+
register_substitution(head, decl, qname, index)
|
|
531
|
+
end
|
|
218
532
|
type_attr = decl.attribute("type")
|
|
219
533
|
model = if type_attr.nil? || type_attr.value.empty?
|
|
220
534
|
inline_model(decl, index)
|
|
@@ -224,6 +538,23 @@ module Moxml
|
|
|
224
538
|
index.contents[qname] = model if model
|
|
225
539
|
end
|
|
226
540
|
|
|
541
|
+
# substitutionGroup is a QName resolved against the global
|
|
542
|
+
# declaration's in-scope bindings (#466 class 1); the
|
|
543
|
+
# member's own qname is target-namespace-qualified — global
|
|
544
|
+
# declarations always are.
|
|
545
|
+
def register_substitution(head, decl, member_qname, index)
|
|
546
|
+
if head.include?(":")
|
|
547
|
+
prefix, _, local = head.partition(":")
|
|
548
|
+
uri = prefix_uri(prefix, decl)
|
|
549
|
+
return if uri.empty?
|
|
550
|
+
|
|
551
|
+
head_qname = "{#{uri}}#{local}"
|
|
552
|
+
else
|
|
553
|
+
head_qname = "{}#{head}"
|
|
554
|
+
end
|
|
555
|
+
(index.substitution_members[head_qname] ||= []) << member_qname
|
|
556
|
+
end
|
|
557
|
+
|
|
227
558
|
def index_global_attribute(decl, index)
|
|
228
559
|
name = decl["name"] or return
|
|
229
560
|
index.attributes["{#{index.target_namespace}}#{name}"] = true
|
|
@@ -249,7 +580,7 @@ module Moxml
|
|
|
249
580
|
end
|
|
250
581
|
|
|
251
582
|
def content_model(type_node, index)
|
|
252
|
-
model = ContentModel.new([], nil, [], nil)
|
|
583
|
+
model = ContentModel.new([], nil, [], nil, [], true)
|
|
253
584
|
collect_model(type_node, index, model)
|
|
254
585
|
model
|
|
255
586
|
end
|
|
@@ -258,36 +589,58 @@ module Moxml
|
|
|
258
589
|
# choice / all / complexContent wrappers flattened): we only
|
|
259
590
|
# ADD findings for strict-wildcard matches, so flattening
|
|
260
591
|
# can only under-report, never over-report. Cardinality and
|
|
261
|
-
# order stay the engine's job
|
|
592
|
+
# order stay the engine's job — EXCEPT for the substitution
|
|
593
|
+
# dismissal (#466 class 1), which vouches only for pure
|
|
594
|
+
# sequences: any choice/all marks the model impure and the
|
|
595
|
+
# flattened order is then not a validity witness.
|
|
262
596
|
def collect_model(node, index, model)
|
|
263
597
|
child_elements(node).each do |child|
|
|
264
598
|
case child.name
|
|
265
599
|
when "element"
|
|
266
|
-
|
|
267
|
-
|
|
600
|
+
if (qname = particle_qname(child, index))
|
|
601
|
+
model.declared_children << qname
|
|
602
|
+
model.particles << [
|
|
603
|
+
qname,
|
|
604
|
+
occur_value(child, "minOccurs", 1),
|
|
605
|
+
occur_value(child, "maxOccurs", 1),
|
|
606
|
+
]
|
|
607
|
+
end
|
|
268
608
|
when "attribute"
|
|
269
|
-
|
|
270
|
-
|
|
609
|
+
if (qname = particle_qname(child, index))
|
|
610
|
+
model.declared_attributes << qname
|
|
611
|
+
end
|
|
271
612
|
when "any"
|
|
272
613
|
model.any ||= wildcard_from(child)
|
|
273
614
|
when "anyAttribute"
|
|
274
615
|
model.any_attribute ||= wildcard_from(child)
|
|
616
|
+
when "choice", "all"
|
|
617
|
+
model.pure_sequence = false
|
|
618
|
+
collect_model(child, index, model)
|
|
275
619
|
else
|
|
276
620
|
collect_model(child, index, model)
|
|
277
621
|
end
|
|
278
622
|
end
|
|
279
623
|
end
|
|
280
624
|
|
|
281
|
-
def
|
|
625
|
+
def particle_qname(particle, index)
|
|
282
626
|
ref = particle["ref"]
|
|
283
627
|
name = ref&.split(":")&.last || particle["name"]
|
|
284
628
|
return unless name
|
|
285
629
|
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
630
|
+
if ref
|
|
631
|
+
"{#{index.target_namespace}}#{name}"
|
|
632
|
+
else
|
|
633
|
+
"{#{form_namespace(particle, index)}}#{name}"
|
|
634
|
+
end
|
|
635
|
+
end
|
|
636
|
+
|
|
637
|
+
def occur_value(particle, attribute, default)
|
|
638
|
+
value = particle[attribute]
|
|
639
|
+
return :unbounded if value == "unbounded"
|
|
640
|
+
|
|
641
|
+
Integer(value || default)
|
|
642
|
+
rescue ArgumentError
|
|
643
|
+
default
|
|
291
644
|
end
|
|
292
645
|
|
|
293
646
|
# Particles declare with form="qualified"/"unqualified",
|
data/lib/moxml/xsd.rb
CHANGED
|
@@ -157,9 +157,11 @@ module Moxml
|
|
|
157
157
|
return engine_errors unless @rules_index
|
|
158
158
|
|
|
159
159
|
# The #452 post-validation rules only ever add findings for
|
|
160
|
-
# documents the engine accepted;
|
|
161
|
-
#
|
|
162
|
-
|
|
160
|
+
# documents the engine accepted; the #464 dismissal removes
|
|
161
|
+
# only engine lexical findings the source's own pattern
|
|
162
|
+
# facets refute. Exact duplicates are dropped for engines
|
|
163
|
+
# that already detect them.
|
|
164
|
+
(SchemaRules.dismiss(engine_errors, doc, @rules_index) +
|
|
163
165
|
SchemaRules.findings(doc, @rules_index)).uniq
|
|
164
166
|
end
|
|
165
167
|
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "spec_helper"
|
|
4
|
+
|
|
5
|
+
# Processing model §Validation (normative): pinned here.
|
|
6
|
+
# #464: an engine that cannot evaluate an XSD pattern-facet
|
|
7
|
+
# dialect construct (e.g. the \p{Is...} block escapes) rejects
|
|
8
|
+
# every occurrence of the facet — including values the facet
|
|
9
|
+
# accepts — false-positiving valid documents. The Ruby-side
|
|
10
|
+
# SchemaRules dismissal removes such engine findings only when the
|
|
11
|
+
# schema source's own pattern facets vouch for every same-named
|
|
12
|
+
# instance: one non-matching value keeps the engine's answer, so
|
|
13
|
+
# a dismissal can only remove false rejections, never create
|
|
14
|
+
# false acceptances. Constructs outside the translated dialect
|
|
15
|
+
# subset keep the engine's answer.
|
|
16
|
+
RSpec.describe "Moxml::XSD pattern-facet dismissal (#464)" do
|
|
17
|
+
describe "bounded dialect translator" do
|
|
18
|
+
let(:translator) { Moxml::XSD::SchemaRules::XsdRegex }
|
|
19
|
+
|
|
20
|
+
it "translates block escapes and categories" do
|
|
21
|
+
expect(translator.translate('\p{IsBasicLatin}+').source)
|
|
22
|
+
.to eq("\\A(?:\\p{InBasicLatin}+)\\z")
|
|
23
|
+
expect(translator.translate('\P{IsLatin-1Supplement}').source)
|
|
24
|
+
.to eq("\\A(?:\\P{InLatin-1Supplement})\\z")
|
|
25
|
+
expect(translator.translate('\p{IsGreek}').source)
|
|
26
|
+
.to eq("\\A(?:\\p{InGreekandCoptic})\\z")
|
|
27
|
+
expect(translator.translate('\p{L}').source)
|
|
28
|
+
.to eq("\\A(?:\\p{L})\\z")
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
it "refuses constructs outside the translated subset" do
|
|
32
|
+
expect(translator.translate('\i\c*')).to be_nil
|
|
33
|
+
expect(translator.translate('[\p{IsBasicLatin}-[abc]]+'))
|
|
34
|
+
.to be_nil
|
|
35
|
+
expect(translator.translate('\p{IsNoSuchBlock}')).to be_nil
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
%i[nokogiri leptris].each do |adapter_name|
|
|
40
|
+
describe "on #{adapter_name}" do
|
|
41
|
+
let(:ctx) { Moxml.new(adapter_name) }
|
|
42
|
+
|
|
43
|
+
before do
|
|
44
|
+
skip "#{adapter_name} requires an XSD-capable engine" unless
|
|
45
|
+
Moxml::XSD.supported?(ctx)
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
describe "block-escape pattern on an element" do
|
|
49
|
+
let(:schema) do
|
|
50
|
+
ctx.xsd(<<~'XSD')
|
|
51
|
+
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema">
|
|
52
|
+
<xs:element name="doc">
|
|
53
|
+
<xs:complexType>
|
|
54
|
+
<xs:sequence>
|
|
55
|
+
<xs:element name="elem" type="regex"
|
|
56
|
+
minOccurs="1" maxOccurs="unbounded"/>
|
|
57
|
+
</xs:sequence>
|
|
58
|
+
</xs:complexType>
|
|
59
|
+
</xs:element>
|
|
60
|
+
<xs:simpleType name="regex">
|
|
61
|
+
<xs:restriction base="xs:string">
|
|
62
|
+
<xs:pattern value="\p{IsBasicLatin}+"/>
|
|
63
|
+
</xs:restriction>
|
|
64
|
+
</xs:simpleType>
|
|
65
|
+
</xs:schema>
|
|
66
|
+
XSD
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
it "accepts the value the facet accepts" do
|
|
70
|
+
doc = ctx.parse(
|
|
71
|
+
"<doc><elem>	

 </elem></doc>",
|
|
72
|
+
)
|
|
73
|
+
expect(schema.valid?(doc)).to be(true)
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
it "rejects the value the facet rejects" do
|
|
77
|
+
doc = ctx.parse(
|
|
78
|
+
"<doc><elem>abc</elem><elem>abéc</elem></doc>",
|
|
79
|
+
)
|
|
80
|
+
expect(schema.valid?(doc)).to be(false)
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
describe "negated block escape" do
|
|
85
|
+
let(:schema) do
|
|
86
|
+
ctx.xsd(<<~'XSD')
|
|
87
|
+
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema">
|
|
88
|
+
<xs:element name="doc">
|
|
89
|
+
<xs:complexType>
|
|
90
|
+
<xs:sequence>
|
|
91
|
+
<xs:element name="elem" type="nonbasic"
|
|
92
|
+
minOccurs="1"/>
|
|
93
|
+
</xs:sequence>
|
|
94
|
+
</xs:complexType>
|
|
95
|
+
</xs:element>
|
|
96
|
+
<xs:simpleType name="nonbasic">
|
|
97
|
+
<xs:restriction base="xs:string">
|
|
98
|
+
<xs:pattern value="\P{IsBasicLatin}"/>
|
|
99
|
+
</xs:restriction>
|
|
100
|
+
</xs:simpleType>
|
|
101
|
+
</xs:schema>
|
|
102
|
+
XSD
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
it "accepts a value outside the negated block" do
|
|
106
|
+
doc = ctx.parse("<doc><elem>é</elem></doc>")
|
|
107
|
+
expect(schema.valid?(doc)).to be(true)
|
|
108
|
+
end
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
describe "block-escape pattern on an attribute" do
|
|
112
|
+
let(:schema) do
|
|
113
|
+
ctx.xsd(<<~'XSD')
|
|
114
|
+
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema">
|
|
115
|
+
<xs:element name="doc">
|
|
116
|
+
<xs:complexType>
|
|
117
|
+
<xs:sequence>
|
|
118
|
+
<xs:element name="elem" minOccurs="1"
|
|
119
|
+
maxOccurs="unbounded">
|
|
120
|
+
<xs:complexType>
|
|
121
|
+
<xs:simpleContent>
|
|
122
|
+
<xs:extension base="xs:string">
|
|
123
|
+
<xs:attribute name="code">
|
|
124
|
+
<xs:simpleType>
|
|
125
|
+
<xs:restriction base="xs:string">
|
|
126
|
+
<xs:pattern
|
|
127
|
+
value="\p{IsBasicLatin}{2}"/>
|
|
128
|
+
</xs:restriction>
|
|
129
|
+
</xs:simpleType>
|
|
130
|
+
</xs:attribute>
|
|
131
|
+
</xs:extension>
|
|
132
|
+
</xs:simpleContent>
|
|
133
|
+
</xs:complexType>
|
|
134
|
+
</xs:element>
|
|
135
|
+
</xs:sequence>
|
|
136
|
+
</xs:complexType>
|
|
137
|
+
</xs:element>
|
|
138
|
+
</xs:schema>
|
|
139
|
+
XSD
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
it "accepts when every instance matches the facet" do
|
|
143
|
+
doc = ctx.parse(
|
|
144
|
+
'<doc><elem code="ab">x</elem><elem code="z9">y</elem></doc>',
|
|
145
|
+
)
|
|
146
|
+
expect(schema.valid?(doc)).to be(true)
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
it "rejects when one instance mismatches" do
|
|
150
|
+
doc = ctx.parse(
|
|
151
|
+
'<doc><elem code="ab">x</elem><elem code="1">y</elem></doc>',
|
|
152
|
+
)
|
|
153
|
+
expect(schema.valid?(doc)).to be(false)
|
|
154
|
+
end
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
describe "class subtraction rides the engine unchanged" do
|
|
158
|
+
let(:schema) do
|
|
159
|
+
ctx.xsd(<<~'XSD')
|
|
160
|
+
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema">
|
|
161
|
+
<xs:element name="doc">
|
|
162
|
+
<xs:complexType>
|
|
163
|
+
<xs:sequence>
|
|
164
|
+
<xs:element name="elem" type="latinish"
|
|
165
|
+
minOccurs="1"/>
|
|
166
|
+
</xs:sequence>
|
|
167
|
+
</xs:complexType>
|
|
168
|
+
</xs:element>
|
|
169
|
+
<xs:simpleType name="latinish">
|
|
170
|
+
<xs:restriction base="xs:string">
|
|
171
|
+
<xs:pattern value="[\p{IsBasicLatin}-[abc]]+"/>
|
|
172
|
+
</xs:restriction>
|
|
173
|
+
</xs:simpleType>
|
|
174
|
+
</xs:schema>
|
|
175
|
+
XSD
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
it "keeps the engine's answer" do
|
|
179
|
+
# The translation refuses subtraction, so the dismissal
|
|
180
|
+
# never fires: both engines answer per their own
|
|
181
|
+
# dialect coverage, unmodified by the facade.
|
|
182
|
+
doc = ctx.parse("<doc><elem>xyz</elem></doc>")
|
|
183
|
+
expect(schema.valid?(doc)).to be(true)
|
|
184
|
+
end
|
|
185
|
+
end
|
|
186
|
+
end
|
|
187
|
+
end
|
|
188
|
+
end
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "spec_helper"
|
|
4
|
+
|
|
5
|
+
# Processing model §Validation (normative): pinned here.
|
|
6
|
+
# #466 class 1: the engine's content-model matcher matches
|
|
7
|
+
# particles by exact name only, so a valid substitution-group
|
|
8
|
+
# member is rejected with "children do not match the content
|
|
9
|
+
# model". The Ruby-side dismissal drops such a finding only when
|
|
10
|
+
# the parent's content model is a pure sequence, the child
|
|
11
|
+
# sequence matches it position-for-position with every particle
|
|
12
|
+
# exactly-once, and a registered member realizes a position —
|
|
13
|
+
# any choice/all, cardinality violation, or unknown name keeps
|
|
14
|
+
# the engine's answer, so the dismissal can only remove false
|
|
15
|
+
# rejections, never create false acceptances.
|
|
16
|
+
RSpec.describe "Moxml::XSD substitution-group dismissal (#466)" do
|
|
17
|
+
%i[nokogiri leptris].each do |adapter_name|
|
|
18
|
+
describe "on #{adapter_name}" do
|
|
19
|
+
let(:ctx) { Moxml.new(adapter_name) }
|
|
20
|
+
|
|
21
|
+
before do
|
|
22
|
+
skip "#{adapter_name} requires an XSD-capable engine" unless
|
|
23
|
+
Moxml::XSD.supported?(ctx)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
describe "member of a referenced head particle" do
|
|
27
|
+
let(:schema) do
|
|
28
|
+
ctx.xsd(<<~XSD)
|
|
29
|
+
<xsd:schema xmlns:xsd="http://www.w3.org/2001/XMLSchema"
|
|
30
|
+
xmlns:x="http://xsdtesting"
|
|
31
|
+
targetNamespace="http://xsdtesting"
|
|
32
|
+
elementFormDefault="qualified">
|
|
33
|
+
<xsd:element name="doc">
|
|
34
|
+
<xsd:complexType>
|
|
35
|
+
<xsd:sequence>
|
|
36
|
+
<xsd:element ref="x:elem1"/>
|
|
37
|
+
</xsd:sequence>
|
|
38
|
+
</xsd:complexType>
|
|
39
|
+
</xsd:element>
|
|
40
|
+
<xsd:element name="elem1" abstract="false"/>
|
|
41
|
+
<xsd:element name="elem2" substitutionGroup="x:elem1"/>
|
|
42
|
+
</xsd:schema>
|
|
43
|
+
XSD
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
it "accepts the member where the head is declared" do
|
|
47
|
+
doc = ctx.parse(
|
|
48
|
+
'<x:doc xmlns:x="http://xsdtesting"><x:elem2/></x:doc>',
|
|
49
|
+
)
|
|
50
|
+
expect(schema.valid?(doc)).to be(true)
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
it "accepts the head itself" do
|
|
54
|
+
doc = ctx.parse(
|
|
55
|
+
'<x:doc xmlns:x="http://xsdtesting"><x:elem1/></x:doc>',
|
|
56
|
+
)
|
|
57
|
+
expect(schema.valid?(doc)).to be(true)
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
it "rejects a stray non-member child" do
|
|
61
|
+
doc = ctx.parse(
|
|
62
|
+
'<x:doc xmlns:x="http://xsdtesting"><x:unknown/></x:doc>',
|
|
63
|
+
)
|
|
64
|
+
expect(schema.valid?(doc)).to be(false)
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
describe "member inside a nested sequence" do
|
|
69
|
+
let(:schema) do
|
|
70
|
+
ctx.xsd(<<~XSD)
|
|
71
|
+
<xsd:schema xmlns:xsd="http://www.w3.org/2001/XMLSchema"
|
|
72
|
+
xmlns:x="http://xsdtesting"
|
|
73
|
+
targetNamespace="http://xsdtesting"
|
|
74
|
+
elementFormDefault="qualified">
|
|
75
|
+
<xsd:element name="doc">
|
|
76
|
+
<xsd:complexType>
|
|
77
|
+
<xsd:sequence>
|
|
78
|
+
<xsd:element ref="x:mid"/>
|
|
79
|
+
</xsd:sequence>
|
|
80
|
+
</xsd:complexType>
|
|
81
|
+
</xsd:element>
|
|
82
|
+
<xsd:element name="mid">
|
|
83
|
+
<xsd:complexType>
|
|
84
|
+
<xsd:sequence>
|
|
85
|
+
<xsd:element ref="x:elem1"/>
|
|
86
|
+
</xsd:sequence>
|
|
87
|
+
</xsd:complexType>
|
|
88
|
+
</xsd:element>
|
|
89
|
+
<xsd:element name="elem1" abstract="false"/>
|
|
90
|
+
<xsd:element name="elem2" substitutionGroup="x:elem1"/>
|
|
91
|
+
</xsd:schema>
|
|
92
|
+
XSD
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
it "accepts the member at the inner position" do
|
|
96
|
+
doc = ctx.parse(
|
|
97
|
+
'<x:doc xmlns:x="http://xsdtesting">' \
|
|
98
|
+
"<x:mid><x:elem2/></x:mid></x:doc>",
|
|
99
|
+
)
|
|
100
|
+
expect(schema.valid?(doc)).to be(true)
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
describe "models the dismissal refuses" do
|
|
105
|
+
let(:schema) do
|
|
106
|
+
ctx.xsd(<<~XSD)
|
|
107
|
+
<xsd:schema xmlns:xsd="http://www.w3.org/2001/XMLSchema"
|
|
108
|
+
xmlns:x="http://xsdtesting"
|
|
109
|
+
targetNamespace="http://xsdtesting"
|
|
110
|
+
elementFormDefault="qualified">
|
|
111
|
+
<xsd:element name="doc">
|
|
112
|
+
<xsd:complexType>
|
|
113
|
+
<xsd:choice>
|
|
114
|
+
<xsd:element ref="x:elem1"/>
|
|
115
|
+
<xsd:element name="other"/>
|
|
116
|
+
</xsd:choice>
|
|
117
|
+
</xsd:complexType>
|
|
118
|
+
</xsd:element>
|
|
119
|
+
<xsd:element name="elem1" abstract="false"/>
|
|
120
|
+
<xsd:element name="elem2" substitutionGroup="x:elem1"/>
|
|
121
|
+
</xsd:schema>
|
|
122
|
+
XSD
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
it "keeps the engine's answer for a two-child choice match" do
|
|
126
|
+
# A choice takes one item; two children cannot be valid
|
|
127
|
+
# even though both names resolve — the flattened particle
|
|
128
|
+
# list is not a validity witness for non-sequences.
|
|
129
|
+
doc = ctx.parse(
|
|
130
|
+
'<x:doc xmlns:x="http://xsdtesting">' \
|
|
131
|
+
"<x:elem2/><x:other/></x:doc>",
|
|
132
|
+
)
|
|
133
|
+
expect(schema.valid?(doc)).to be(false)
|
|
134
|
+
end
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
describe "cardinality the dismissal refuses" do
|
|
138
|
+
let(:schema) do
|
|
139
|
+
ctx.xsd(<<~XSD)
|
|
140
|
+
<xsd:schema xmlns:xsd="http://www.w3.org/2001/XMLSchema"
|
|
141
|
+
xmlns:x="http://xsdtesting"
|
|
142
|
+
targetNamespace="http://xsdtesting"
|
|
143
|
+
elementFormDefault="qualified">
|
|
144
|
+
<xsd:element name="doc">
|
|
145
|
+
<xsd:complexType>
|
|
146
|
+
<xsd:sequence>
|
|
147
|
+
<xsd:element ref="x:elem1" minOccurs="2" maxOccurs="2"/>
|
|
148
|
+
</xsd:sequence>
|
|
149
|
+
</xsd:complexType>
|
|
150
|
+
</xsd:element>
|
|
151
|
+
<xsd:element name="elem1" abstract="false"/>
|
|
152
|
+
<xsd:element name="elem2" substitutionGroup="x:elem1"/>
|
|
153
|
+
</xsd:schema>
|
|
154
|
+
XSD
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
it "keeps the engine's answer below minOccurs" do
|
|
158
|
+
doc = ctx.parse(
|
|
159
|
+
'<x:doc xmlns:x="http://xsdtesting"><x:elem2/></x:doc>',
|
|
160
|
+
)
|
|
161
|
+
expect(schema.valid?(doc)).to be(false)
|
|
162
|
+
end
|
|
163
|
+
end
|
|
164
|
+
end
|
|
165
|
+
end
|
|
166
|
+
end
|
|
@@ -89,6 +89,14 @@ module ModelCoverage
|
|
|
89
89
|
section: "Validation",
|
|
90
90
|
pin: "spec/moxml/xsd_version_spec.rb",
|
|
91
91
|
},
|
|
92
|
+
"validation regex dismissal" => {
|
|
93
|
+
section: "Validation",
|
|
94
|
+
pin: "spec/moxml/xsd_regex_spec.rb",
|
|
95
|
+
},
|
|
96
|
+
"validation substitution dismissal" => {
|
|
97
|
+
section: "Validation",
|
|
98
|
+
pin: "spec/moxml/xsd_substitution_spec.rb",
|
|
99
|
+
},
|
|
92
100
|
"loud capability contracts" => {
|
|
93
101
|
section: "Errors and loud degradation",
|
|
94
102
|
pin: "spec/consistency/loud_degradation_spec.rb",
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: moxml
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.5.
|
|
4
|
+
version: 0.5.166
|
|
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-26 00:00:00.000000000 Z
|
|
12
12
|
dependencies: []
|
|
13
13
|
description: |
|
|
14
14
|
Moxml is a unified XML manipulation library that provides a common API
|
|
@@ -631,8 +631,10 @@ files:
|
|
|
631
631
|
- spec/moxml/xpath_spec.rb
|
|
632
632
|
- spec/moxml/xsd_error_contract_spec.rb
|
|
633
633
|
- spec/moxml/xsd_handle_mismatch_spec.rb
|
|
634
|
+
- spec/moxml/xsd_regex_spec.rb
|
|
634
635
|
- spec/moxml/xsd_release_spec.rb
|
|
635
636
|
- spec/moxml/xsd_spec.rb
|
|
637
|
+
- spec/moxml/xsd_substitution_spec.rb
|
|
636
638
|
- spec/moxml/xsd_underdetection_spec.rb
|
|
637
639
|
- spec/moxml/xsd_version_spec.rb
|
|
638
640
|
- spec/moxml/xslt/stylesheet_spec.rb
|