moxml 0.5.162 → 0.5.165
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 +1 -0
- data/docs/_references/processing-model.adoc +17 -4
- data/lib/moxml/version.rb +1 -1
- data/lib/moxml/xsd/schema_rules.rb +375 -41
- data/lib/moxml/xsd.rb +5 -3
- data/spec/moxml/xsd_regex_spec.rb +188 -0
- data/spec/moxml/xsd_underdetection_spec.rb +171 -5
- data/spec/support/model_coverage.rb +4 -0
- metadata +3 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 8dae9d144ec375224efd389406ed2cdb06c5409ae91d985e95221a972a1158ac
|
|
4
|
+
data.tar.gz: 308da2c1430d4f7f6b195eeec3d4ba9f651c9664f30c2a6c1381923572409fbd
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 794455e01d1021344dd5f792802d10403cc35a57b4fd167dee967d59c8079f2e3467a392433dff45c9959919569d2e3fc68b4f0db8535257bb646ce9f48821ea
|
|
7
|
+
data.tar.gz: a2e777a2b636b71432ee8baa0610254cb2b464b33843f57063d31a6688974394cfc25e8c6e91851448c97f563acd80261833c5542d5b9b962aa2dae240c8aa27
|
|
@@ -42,6 +42,7 @@ 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
|
|
45
46
|
| loud capability contracts | equivalent | equivalent | equivalent | equivalent | equivalent | equivalent | equivalent
|
|
46
47
|
|===
|
|
47
48
|
|
|
@@ -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_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
|
|
@@ -263,9 +263,22 @@ checks). `validate` returns accumulated `ValidationError` entries —
|
|
|
263
263
|
message always; line/column/path where the engine reports them, nil
|
|
264
264
|
where it does not; `[]` means valid. Documents outside the engine's
|
|
265
265
|
scope validate laxly, except that strict `xs:any` /
|
|
266
|
-
`xs:anyAttribute` wildcard matches
|
|
267
|
-
|
|
268
|
-
|
|
266
|
+
`xs:anyAttribute` wildcard matches, `xsi:nil`, and the XSD 1.1
|
|
267
|
+
negative wildcards (`notQName` / `notNamespace` — an item excluded
|
|
268
|
+
by them has no particle and the content model cannot accept it) are
|
|
269
|
+
re-checked from the schema source on every adapter — the re-check
|
|
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. A compiled schema reports the
|
|
269
282
|
version its source declares: root `vc:minVersion="1.1"` scopes the
|
|
270
283
|
document to 1.1, anything else is 1.0 (consumers scope 1.0-only
|
|
271
284
|
checks with it; applying 1.1 semantics is the engine-side program).
|
data/lib/moxml/version.rb
CHANGED
|
@@ -8,7 +8,11 @@ module Moxml
|
|
|
8
8
|
#
|
|
9
9
|
# * a strict `xs:any` wildcard with an undeclared element,
|
|
10
10
|
# * a strict `xs:anyAttribute` with an undeclared attribute,
|
|
11
|
-
# * `xsi:nil` on an element whose declaration is not nillable
|
|
11
|
+
# * `xsi:nil` on an element whose declaration is not nillable,
|
|
12
|
+
# * the XSD 1.1 negative wildcards — `notQName` /
|
|
13
|
+
# `notNamespace` on `xs:any` / `xs:anyAttribute` (#460):
|
|
14
|
+
# engines that compile them without enforcing them absorb
|
|
15
|
+
# excluded items the content model cannot accept.
|
|
12
16
|
#
|
|
13
17
|
# The rules run from an index of the schema SOURCE (this gem's
|
|
14
18
|
# own XML machinery — no engine involvement) and merge with the
|
|
@@ -20,22 +24,18 @@ module Moxml
|
|
|
20
24
|
module SchemaRules
|
|
21
25
|
XSI_NS = "http://www.w3.org/2001/XMLSchema-instance"
|
|
22
26
|
|
|
23
|
-
# A wildcard's namespace
|
|
24
|
-
# Structures §3.10.2 (##any default;
|
|
25
|
-
# strict
|
|
26
|
-
#
|
|
27
|
+
# A wildcard's namespace constraint and disallowed names, per
|
|
28
|
+
# XML Schema Structures §3.10.1–3.10.2 (##any default;
|
|
29
|
+
# processContents default strict; notNamespace / notQName are
|
|
30
|
+
# XSD 1.1). :tns / :local are resolved against the indexed
|
|
31
|
+
# schema at check time; notQName QNames are resolved against
|
|
32
|
+
# the schema element's in-scope bindings at index time.
|
|
27
33
|
class Wildcard
|
|
28
|
-
def initialize(value, process
|
|
34
|
+
def initialize(value, process, not_namespace: nil, not_qname: nil,
|
|
35
|
+
element: nil)
|
|
29
36
|
@process = (process || "strict").to_sym
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
@mode = :any
|
|
33
|
-
when "##other"
|
|
34
|
-
@mode = :other
|
|
35
|
-
else
|
|
36
|
-
@mode = :list
|
|
37
|
-
@namespaces = value.split(/\s+/).reject(&:empty?).freeze
|
|
38
|
-
end
|
|
37
|
+
@mode, @namespaces = namespace_constraint(value, not_namespace)
|
|
38
|
+
@disallowed = disallowed_names(not_qname, element).freeze
|
|
39
39
|
end
|
|
40
40
|
|
|
41
41
|
def strict?
|
|
@@ -47,13 +47,89 @@ module Moxml
|
|
|
47
47
|
when :any then true
|
|
48
48
|
when :other
|
|
49
49
|
namespace != :local && namespace != :tns
|
|
50
|
+
when :not
|
|
51
|
+
@namespaces.none? { |entry| entry_covers?(entry, namespace) }
|
|
50
52
|
else
|
|
51
53
|
@namespaces.any? { |entry| entry_covers?(entry, namespace) }
|
|
52
54
|
end
|
|
53
55
|
end
|
|
54
56
|
|
|
57
|
+
# Why the wildcard refuses an item outright (Structures
|
|
58
|
+
# §3.10.4.1 Item Valid): a refusal is a match-level failure,
|
|
59
|
+
# so it applies at every processContents — the content model
|
|
60
|
+
# has no particle for the item and cannot accept it. 1.0
|
|
61
|
+
# namespace lists stay the engine's enforcement and never
|
|
62
|
+
# name an exclusion here.
|
|
63
|
+
def negative_exclusion(qname, namespace, defined:,
|
|
64
|
+
sibling_names: [])
|
|
65
|
+
@disallowed.each do |entry|
|
|
66
|
+
case entry
|
|
67
|
+
when :defined
|
|
68
|
+
return :not_qname if defined
|
|
69
|
+
when :sibling
|
|
70
|
+
return :not_qname if sibling_names.include?(qname)
|
|
71
|
+
else
|
|
72
|
+
return :not_qname if entry == qname
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
return :not_namespace if @mode == :not &&
|
|
76
|
+
@namespaces.any? { |entry| entry_covers?(entry, namespace) }
|
|
77
|
+
|
|
78
|
+
nil
|
|
79
|
+
end
|
|
80
|
+
|
|
55
81
|
private
|
|
56
82
|
|
|
83
|
+
def namespace_constraint(value, not_namespace)
|
|
84
|
+
if not_namespace
|
|
85
|
+
[:not, not_namespace.split(/\s+/).reject(&:empty?)]
|
|
86
|
+
elsif value.nil? || value == "##any" || value == ""
|
|
87
|
+
[:any, []]
|
|
88
|
+
elsif value == "##other"
|
|
89
|
+
[:other, []]
|
|
90
|
+
else
|
|
91
|
+
[:list, value.split(/\s+/).reject(&:empty?)]
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
# notQName entries (§3.10.2): QName | ##defined |
|
|
96
|
+
# ##definedSibling (xs:any; ##defined only on
|
|
97
|
+
# xs:anyAttribute). Prefixed QNames resolve against the
|
|
98
|
+
# wildcard element's in-scope bindings; unprefixed QNames
|
|
99
|
+
# carry no namespace — XSD QName values take no default
|
|
100
|
+
# namespace. Unresolvable prefixes are dropped: the index
|
|
101
|
+
# under-reports, never false-reports.
|
|
102
|
+
def disallowed_names(not_qname, element)
|
|
103
|
+
not_qname.to_s.split(/\s+/).reject(&:empty?).filter_map do |token|
|
|
104
|
+
case token
|
|
105
|
+
when "##defined" then :defined
|
|
106
|
+
when "##definedSibling" then :sibling
|
|
107
|
+
when /\A([^:]+):(.+)\z/
|
|
108
|
+
uri = prefix_uri(Regexp.last_match(1), element)
|
|
109
|
+
"{#{uri}}#{Regexp.last_match(2)}" unless uri.empty?
|
|
110
|
+
else
|
|
111
|
+
"{}#{token}"
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
|
|
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
|
+
|
|
57
133
|
def entry_covers?(entry, namespace)
|
|
58
134
|
namespace == case entry
|
|
59
135
|
when "##targetNamespace" then :tns
|
|
@@ -67,12 +143,94 @@ module Moxml
|
|
|
67
143
|
:declared_attributes, :any_attribute)
|
|
68
144
|
SchemaIndex = Struct.new(:target_namespace, :element_default,
|
|
69
145
|
:attribute_default, :elements,
|
|
70
|
-
:attributes, :contents, :version
|
|
146
|
+
:attributes, :contents, :version,
|
|
147
|
+
:element_patterns, :attribute_patterns)
|
|
71
148
|
|
|
72
149
|
# The XSD 1.1 version-tagging namespace (Structures §C.3):
|
|
73
150
|
# vc:minVersion / vc:maxVersion gate 1.1 constructs.
|
|
74
151
|
VC_NS = "http://www.w3.org/2001/XMLSchema-versioning"
|
|
75
152
|
|
|
153
|
+
# XSD pattern-facet dialect → Ruby regex, bounded to the
|
|
154
|
+
# subset the facade vouches for (Datatypes Appendix F). Block
|
|
155
|
+
# escapes \p{Is…}/\P{Is…} map to Ruby's In-prefixed blocks
|
|
156
|
+
# (a few Unicode-renamed blocks carry an alias); general
|
|
157
|
+
# categories and the single-character escapes pass through;
|
|
158
|
+
# \i, \c and class subtraction are NOT translated. Anything
|
|
159
|
+
# unrecognized — or a translation Ruby itself rejects —
|
|
160
|
+
# returns nil: the engine's answer then stands.
|
|
161
|
+
module XsdRegex
|
|
162
|
+
BLOCK_ALIASES = {
|
|
163
|
+
"Greek" => "GreekandCoptic",
|
|
164
|
+
"CombiningMarksforSymbols" =>
|
|
165
|
+
"CombiningDiacriticalMarksforSymbols",
|
|
166
|
+
"Turkish" => "Latin-1Supplement",
|
|
167
|
+
}.freeze
|
|
168
|
+
|
|
169
|
+
PASS_THROUGH_ESCAPES = %w[
|
|
170
|
+
n r t \\ . | ? * + ( ) [ ] { } ^ $ -
|
|
171
|
+
].freeze
|
|
172
|
+
|
|
173
|
+
module_function
|
|
174
|
+
|
|
175
|
+
def translate(pattern)
|
|
176
|
+
translated = scan(pattern.to_s)
|
|
177
|
+
return nil unless translated
|
|
178
|
+
|
|
179
|
+
Regexp.new("\\A(?:#{translated})\\z")
|
|
180
|
+
rescue RegexpError
|
|
181
|
+
nil
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
def scan(pattern)
|
|
185
|
+
out = +""
|
|
186
|
+
index = 0
|
|
187
|
+
while index < pattern.length
|
|
188
|
+
char = pattern[index]
|
|
189
|
+
if char == "\\"
|
|
190
|
+
consumed = escape(pattern, index, out)
|
|
191
|
+
return nil unless consumed
|
|
192
|
+
|
|
193
|
+
index += consumed
|
|
194
|
+
else
|
|
195
|
+
return nil if char == "-" && pattern[index + 1] == "["
|
|
196
|
+
|
|
197
|
+
out << char
|
|
198
|
+
index += 1
|
|
199
|
+
end
|
|
200
|
+
end
|
|
201
|
+
out
|
|
202
|
+
end
|
|
203
|
+
|
|
204
|
+
# Appends the translation of the escape at index to out and
|
|
205
|
+
# returns the chars consumed, or nil when unrecognized.
|
|
206
|
+
def escape(pattern, index, out)
|
|
207
|
+
kind = pattern[index + 1]
|
|
208
|
+
case kind
|
|
209
|
+
when "p", "P"
|
|
210
|
+
close = pattern.index("}", index)
|
|
211
|
+
return nil unless close
|
|
212
|
+
|
|
213
|
+
body = pattern[index + 3, close - index - 3]
|
|
214
|
+
if body&.start_with?("Is")
|
|
215
|
+
mapped = body.delete_prefix("Is")
|
|
216
|
+
mapped = BLOCK_ALIASES.fetch(mapped, mapped)
|
|
217
|
+
begin
|
|
218
|
+
Regexp.new("\\p{In#{mapped}}")
|
|
219
|
+
rescue RegexpError
|
|
220
|
+
return nil
|
|
221
|
+
end
|
|
222
|
+
out << "\\#{kind}{In#{mapped}}"
|
|
223
|
+
else
|
|
224
|
+
out << pattern[index..close]
|
|
225
|
+
end
|
|
226
|
+
close - index + 1
|
|
227
|
+
when *PASS_THROUGH_ESCAPES
|
|
228
|
+
out << pattern[index, 2]
|
|
229
|
+
2
|
|
230
|
+
end
|
|
231
|
+
end
|
|
232
|
+
end
|
|
233
|
+
|
|
76
234
|
class << self
|
|
77
235
|
# Index one schema document. Returns nil when the source
|
|
78
236
|
# cannot be indexed — the engine's answer then stands alone
|
|
@@ -85,7 +243,7 @@ module Moxml
|
|
|
85
243
|
root["targetNamespace"].to_s,
|
|
86
244
|
root["elementFormDefault"] == "qualified",
|
|
87
245
|
root["attributeFormDefault"] == "qualified",
|
|
88
|
-
{}, {}, {}, schema_version(root)
|
|
246
|
+
{}, {}, {}, schema_version(root), {}, {}
|
|
89
247
|
)
|
|
90
248
|
child_elements(root).each do |decl|
|
|
91
249
|
case decl.name
|
|
@@ -93,6 +251,7 @@ module Moxml
|
|
|
93
251
|
when "attribute" then index_global_attribute(decl, index)
|
|
94
252
|
end
|
|
95
253
|
end
|
|
254
|
+
index_patterns(root, index)
|
|
96
255
|
index
|
|
97
256
|
rescue StandardError
|
|
98
257
|
nil
|
|
@@ -112,6 +271,142 @@ module Moxml
|
|
|
112
271
|
declared.to_s.start_with?("1.1") ? "1.1" : "1.0"
|
|
113
272
|
end
|
|
114
273
|
|
|
274
|
+
# Index the pattern facets the dismissal may vouch with
|
|
275
|
+
# (#464): named simpleTypes resolve through one type-ref
|
|
276
|
+
# hop, inline simpleTypes directly; keys are local names.
|
|
277
|
+
# A name declared twice with different pattern sets is
|
|
278
|
+
# ambiguous for name-based matching and is removed — an
|
|
279
|
+
# ambiguous vouch could mask a real violation.
|
|
280
|
+
def index_patterns(root, index)
|
|
281
|
+
named = {}
|
|
282
|
+
collect_named_patterns(root, named)
|
|
283
|
+
collect_decl_patterns(root, index, named)
|
|
284
|
+
end
|
|
285
|
+
|
|
286
|
+
def collect_named_patterns(node, named)
|
|
287
|
+
child_elements(node).each do |child|
|
|
288
|
+
if child.name == "simpleType" && child["name"] &&
|
|
289
|
+
(patterns = restriction_patterns(child))
|
|
290
|
+
named[child["name"]] = patterns
|
|
291
|
+
else
|
|
292
|
+
collect_named_patterns(child, named)
|
|
293
|
+
end
|
|
294
|
+
end
|
|
295
|
+
end
|
|
296
|
+
|
|
297
|
+
def collect_decl_patterns(node, index, named)
|
|
298
|
+
child_elements(node).each do |child|
|
|
299
|
+
case child.name
|
|
300
|
+
when "element"
|
|
301
|
+
register_pattern(index.element_patterns, child["name"],
|
|
302
|
+
decl_patterns(child, named))
|
|
303
|
+
when "attribute"
|
|
304
|
+
register_pattern(index.attribute_patterns, child["name"],
|
|
305
|
+
decl_patterns(child, named))
|
|
306
|
+
end
|
|
307
|
+
collect_decl_patterns(child, index, named)
|
|
308
|
+
end
|
|
309
|
+
end
|
|
310
|
+
|
|
311
|
+
def decl_patterns(decl, named)
|
|
312
|
+
inline = child_elements(decl).find do |child|
|
|
313
|
+
child.name == "simpleType"
|
|
314
|
+
end
|
|
315
|
+
if inline
|
|
316
|
+
restriction_patterns(inline)
|
|
317
|
+
elsif decl["type"]
|
|
318
|
+
named[decl["type"].split(":").last]
|
|
319
|
+
end
|
|
320
|
+
end
|
|
321
|
+
|
|
322
|
+
def restriction_patterns(simple_type)
|
|
323
|
+
restriction = child_elements(simple_type).find do |child|
|
|
324
|
+
child.name == "restriction"
|
|
325
|
+
end
|
|
326
|
+
return nil unless restriction
|
|
327
|
+
|
|
328
|
+
patterns = child_elements(restriction)
|
|
329
|
+
.select { |facet| facet.name == "pattern" }
|
|
330
|
+
.map { |facet| XsdRegex.translate(facet["value"].to_s) }
|
|
331
|
+
return nil if patterns.any?(&:nil?)
|
|
332
|
+
|
|
333
|
+
patterns
|
|
334
|
+
end
|
|
335
|
+
|
|
336
|
+
def register_pattern(sink, name, patterns)
|
|
337
|
+
return unless name && patterns && !patterns.empty?
|
|
338
|
+
|
|
339
|
+
sink[name] = sink.key?(name) ? nil : patterns
|
|
340
|
+
end
|
|
341
|
+
|
|
342
|
+
# Dismisses engine lexical findings the source's own
|
|
343
|
+
# pattern facets refute (#464): an engine that cannot
|
|
344
|
+
# evaluate a dialect construct rejects every occurrence of
|
|
345
|
+
# the facet — including values the facet accepts. A
|
|
346
|
+
# finding is dropped only when every same-named instance
|
|
347
|
+
# in the document matches; one non-matching value keeps
|
|
348
|
+
# the engine's answer, so a dismissal can only remove
|
|
349
|
+
# false rejections, never create false acceptances.
|
|
350
|
+
def dismiss(engine_errors, doc, index)
|
|
351
|
+
return engine_errors unless index
|
|
352
|
+
|
|
353
|
+
vouched = vouch_table(doc, index)
|
|
354
|
+
engine_errors.reject do |error|
|
|
355
|
+
subject = lexical_subject(error.message)
|
|
356
|
+
subject && vouched[subject]
|
|
357
|
+
end
|
|
358
|
+
end
|
|
359
|
+
|
|
360
|
+
LEXICAL_MESSAGE = /\A(?:element|attribute) '([^']+)' @\d+:\d+: (element|attribute) '([^']+)': lexical value does not match its type\z/
|
|
361
|
+
|
|
362
|
+
private_constant :LEXICAL_MESSAGE
|
|
363
|
+
|
|
364
|
+
def lexical_subject(message)
|
|
365
|
+
match = LEXICAL_MESSAGE.match(message.to_s)
|
|
366
|
+
return unless match
|
|
367
|
+
|
|
368
|
+
[match[2], match[3]]
|
|
369
|
+
end
|
|
370
|
+
|
|
371
|
+
def vouch_table(doc, index)
|
|
372
|
+
root = doc.root
|
|
373
|
+
return {} unless root
|
|
374
|
+
|
|
375
|
+
elements = Hash.new { |hash, key| hash[key] = [] }
|
|
376
|
+
attributes = Hash.new { |hash, key| hash[key] = [] }
|
|
377
|
+
collect_witnesses(root, index, elements, attributes)
|
|
378
|
+
vouched = {}
|
|
379
|
+
elements.each do |name, matches|
|
|
380
|
+
vouched[["element", name]] = !matches.empty? && matches.all?
|
|
381
|
+
end
|
|
382
|
+
attributes.each do |name, matches|
|
|
383
|
+
vouched[["attribute", name]] = !matches.empty? && matches.all?
|
|
384
|
+
end
|
|
385
|
+
vouched
|
|
386
|
+
end
|
|
387
|
+
|
|
388
|
+
def collect_witnesses(node, index, elements, attributes)
|
|
389
|
+
child_elements(node).each do |child|
|
|
390
|
+
if (patterns = index.element_patterns[child.name])
|
|
391
|
+
elements[child.name] << (text_only?(child) &&
|
|
392
|
+
patterns.any? { |regex| regex.match?(child.text.to_s) })
|
|
393
|
+
end
|
|
394
|
+
child.attributes.each do |attribute|
|
|
395
|
+
next if attribute.namespace&.uri == XSI_NS
|
|
396
|
+
|
|
397
|
+
if (patterns = index.attribute_patterns[attribute.name])
|
|
398
|
+
attributes[attribute.name] <<
|
|
399
|
+
patterns.any? { |regex| regex.match?(attribute.value.to_s) }
|
|
400
|
+
end
|
|
401
|
+
end
|
|
402
|
+
collect_witnesses(child, index, elements, attributes)
|
|
403
|
+
end
|
|
404
|
+
end
|
|
405
|
+
|
|
406
|
+
def text_only?(element)
|
|
407
|
+
child_elements(element).empty?
|
|
408
|
+
end
|
|
409
|
+
|
|
115
410
|
# Post-validation findings for one instance document.
|
|
116
411
|
def findings(doc, index)
|
|
117
412
|
return [] unless index
|
|
@@ -193,12 +488,9 @@ module Moxml
|
|
|
193
488
|
add_particle(child, index, model,
|
|
194
489
|
model.declared_attributes)
|
|
195
490
|
when "any"
|
|
196
|
-
model.any ||=
|
|
197
|
-
child["processContents"])
|
|
491
|
+
model.any ||= wildcard_from(child)
|
|
198
492
|
when "anyAttribute"
|
|
199
|
-
model.any_attribute ||=
|
|
200
|
-
Wildcard.new(child["namespace"],
|
|
201
|
-
child["processContents"])
|
|
493
|
+
model.any_attribute ||= wildcard_from(child)
|
|
202
494
|
else
|
|
203
495
|
collect_model(child, index, model)
|
|
204
496
|
end
|
|
@@ -234,19 +526,25 @@ module Moxml
|
|
|
234
526
|
end
|
|
235
527
|
end
|
|
236
528
|
|
|
529
|
+
def wildcard_from(node)
|
|
530
|
+
Wildcard.new(
|
|
531
|
+
node["namespace"], node["processContents"],
|
|
532
|
+
not_namespace: node["notNamespace"],
|
|
533
|
+
not_qname: node["notQName"],
|
|
534
|
+
element: node
|
|
535
|
+
)
|
|
536
|
+
end
|
|
537
|
+
|
|
237
538
|
def child_findings(child, model, index)
|
|
238
539
|
qname = qname(child)
|
|
239
540
|
findings = []
|
|
240
541
|
if model.declared_children.include?(qname)
|
|
241
542
|
declaration = index.elements[qname]
|
|
242
543
|
findings << nil_finding(child, declaration) if declaration
|
|
243
|
-
elsif
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
"wildcard but is not declared",
|
|
248
|
-
path: path_of(child),
|
|
249
|
-
)
|
|
544
|
+
elsif model.any &&
|
|
545
|
+
(finding = wildcard_child_finding(child, model, index,
|
|
546
|
+
qname))
|
|
547
|
+
findings << finding
|
|
250
548
|
end
|
|
251
549
|
findings.concat(
|
|
252
550
|
attribute_findings(child, index.contents[qname], index),
|
|
@@ -254,13 +552,34 @@ module Moxml
|
|
|
254
552
|
findings
|
|
255
553
|
end
|
|
256
554
|
|
|
257
|
-
def
|
|
555
|
+
def wildcard_child_finding(child, model, index, qname)
|
|
258
556
|
wildcard = model.any
|
|
259
|
-
return false unless wildcard&.strict?
|
|
260
|
-
return false if index.elements.key?(qname)
|
|
261
|
-
|
|
262
557
|
namespace = resolve_namespace(qname, index)
|
|
263
|
-
|
|
558
|
+
exclusion = wildcard.negative_exclusion(
|
|
559
|
+
qname, namespace,
|
|
560
|
+
defined: index.elements.key?(qname),
|
|
561
|
+
sibling_names: model.declared_children
|
|
562
|
+
)
|
|
563
|
+
return excluded_child_finding(child, qname, exclusion) if exclusion
|
|
564
|
+
|
|
565
|
+
return unless wildcard.covers?(namespace) && wildcard.strict?
|
|
566
|
+
return if index.elements.key?(qname)
|
|
567
|
+
|
|
568
|
+
Moxml::XSD::ValidationError.new(
|
|
569
|
+
message: "element <#{local(qname)}> matches a strict " \
|
|
570
|
+
"wildcard but is not declared",
|
|
571
|
+
path: path_of(child),
|
|
572
|
+
)
|
|
573
|
+
end
|
|
574
|
+
|
|
575
|
+
def excluded_child_finding(child, qname, exclusion)
|
|
576
|
+
reason = exclusion == :not_namespace ? "notNamespace" : "notQName"
|
|
577
|
+
Moxml::XSD::ValidationError.new(
|
|
578
|
+
message: "element <#{local(qname)}> is disallowed by a " \
|
|
579
|
+
"wildcard (#{reason}) and is not declared in the " \
|
|
580
|
+
"content model",
|
|
581
|
+
path: path_of(child),
|
|
582
|
+
)
|
|
264
583
|
end
|
|
265
584
|
|
|
266
585
|
def attribute_findings(element, model, index)
|
|
@@ -271,14 +590,19 @@ module Moxml
|
|
|
271
590
|
next if attribute.namespace&.uri == XSI_NS
|
|
272
591
|
|
|
273
592
|
qname = attribute_qname(attribute)
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
593
|
+
next if model.declared_attributes.include?(qname)
|
|
594
|
+
next unless any_attribute
|
|
595
|
+
|
|
596
|
+
namespace = resolve_namespace(qname, index)
|
|
597
|
+
exclusion = any_attribute.negative_exclusion(
|
|
598
|
+
qname, namespace,
|
|
599
|
+
defined: index.attributes.key?(qname)
|
|
281
600
|
)
|
|
601
|
+
next excluded_attribute_finding(element, qname, exclusion) if exclusion
|
|
602
|
+
|
|
603
|
+
next unless any_attribute.covers?(namespace)
|
|
604
|
+
next unless any_attribute.strict?
|
|
605
|
+
next if index.attributes.key?(qname)
|
|
282
606
|
|
|
283
607
|
Moxml::XSD::ValidationError.new(
|
|
284
608
|
message: "attribute #{local(qname)} matches a strict " \
|
|
@@ -288,6 +612,16 @@ module Moxml
|
|
|
288
612
|
end
|
|
289
613
|
end
|
|
290
614
|
|
|
615
|
+
def excluded_attribute_finding(element, qname, exclusion)
|
|
616
|
+
reason = exclusion == :not_namespace ? "notNamespace" : "notQName"
|
|
617
|
+
Moxml::XSD::ValidationError.new(
|
|
618
|
+
message: "attribute #{local(qname)} is disallowed by a " \
|
|
619
|
+
"wildcard (#{reason}) and is not declared in the " \
|
|
620
|
+
"content model",
|
|
621
|
+
path: path_of(element),
|
|
622
|
+
)
|
|
623
|
+
end
|
|
624
|
+
|
|
291
625
|
def nil_finding(element, declaration)
|
|
292
626
|
return if declaration[:nillable]
|
|
293
627
|
|
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
|
|
@@ -4,11 +4,11 @@ require "spec_helper"
|
|
|
4
4
|
|
|
5
5
|
# Processing model §Validation (normative): pinned here.
|
|
6
6
|
# #452: tier-1 engines can accept, inside their recognized scope,
|
|
7
|
-
# instances that violate strict wildcards or xsi:nil.
|
|
8
|
-
#
|
|
9
|
-
#
|
|
10
|
-
#
|
|
11
|
-
# corpus shapes.
|
|
7
|
+
# instances that violate strict wildcards or xsi:nil. #460: engines
|
|
8
|
+
# may compile the 1.1 negative wildcards without enforcing them.
|
|
9
|
+
# The Ruby-side SchemaRules post-validation closes those gaps from
|
|
10
|
+
# the schema source; findings merge with the engine's (duplicates
|
|
11
|
+
# dropped), so every adapter answers invalid for the corpus shapes.
|
|
12
12
|
RSpec.describe "Moxml::XSD tier-1 under-detections (#452)" do
|
|
13
13
|
%i[nokogiri leptris].each do |adapter_name|
|
|
14
14
|
describe "on #{adapter_name}" do
|
|
@@ -152,6 +152,172 @@ RSpec.describe "Moxml::XSD tier-1 under-detections (#452)" do
|
|
|
152
152
|
end
|
|
153
153
|
end
|
|
154
154
|
|
|
155
|
+
describe "negative wildcards (XSD 1.1, #460)" do
|
|
156
|
+
before do
|
|
157
|
+
if adapter_name == :nokogiri
|
|
158
|
+
skip "#{adapter_name} requires an XSD 1.1 wildcard-capable " \
|
|
159
|
+
"engine"
|
|
160
|
+
end
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
describe "xs:any notQName naming an excluded element" do
|
|
164
|
+
let(:schema) do
|
|
165
|
+
ctx.xsd(<<~XSD)
|
|
166
|
+
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema"
|
|
167
|
+
xmlns:w="http://example.com/w"
|
|
168
|
+
targetNamespace="http://example.com/w"
|
|
169
|
+
elementFormDefault="qualified">
|
|
170
|
+
<xs:element name="container">
|
|
171
|
+
<xs:complexType>
|
|
172
|
+
<xs:sequence>
|
|
173
|
+
<xs:any processContents="strict"
|
|
174
|
+
notQName="w:banned" minOccurs="0"
|
|
175
|
+
maxOccurs="unbounded"/>
|
|
176
|
+
</xs:sequence>
|
|
177
|
+
</xs:complexType>
|
|
178
|
+
</xs:element>
|
|
179
|
+
<xs:element name="banned" type="xs:string"/>
|
|
180
|
+
<xs:element name="known" type="xs:string"/>
|
|
181
|
+
</xs:schema>
|
|
182
|
+
XSD
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
it "rejects the excluded element although it is declared" do
|
|
186
|
+
doc = ctx.parse(
|
|
187
|
+
'<container xmlns="http://example.com/w"><banned>x</banned></container>',
|
|
188
|
+
)
|
|
189
|
+
expect(schema.valid?(doc)).to be(false)
|
|
190
|
+
expect(schema.validate(doc).map(&:message)).to include(
|
|
191
|
+
/disallowed by a wildcard \(notQName\)/,
|
|
192
|
+
)
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
it "accepts a wildcard-matched declared element" do
|
|
196
|
+
doc = ctx.parse(
|
|
197
|
+
'<container xmlns="http://example.com/w"><known>x</known></container>',
|
|
198
|
+
)
|
|
199
|
+
expect(schema.valid?(doc)).to be(true)
|
|
200
|
+
end
|
|
201
|
+
end
|
|
202
|
+
|
|
203
|
+
describe "xs:any notQName=##defined" do
|
|
204
|
+
let(:schema) do
|
|
205
|
+
ctx.xsd(<<~XSD)
|
|
206
|
+
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema"
|
|
207
|
+
targetNamespace="http://example.com/w"
|
|
208
|
+
elementFormDefault="qualified">
|
|
209
|
+
<xs:element name="container">
|
|
210
|
+
<xs:complexType>
|
|
211
|
+
<xs:sequence>
|
|
212
|
+
<xs:any processContents="lax"
|
|
213
|
+
notQName="##defined" minOccurs="0"
|
|
214
|
+
maxOccurs="unbounded"/>
|
|
215
|
+
</xs:sequence>
|
|
216
|
+
</xs:complexType>
|
|
217
|
+
</xs:element>
|
|
218
|
+
<xs:element name="known" type="xs:string"/>
|
|
219
|
+
</xs:schema>
|
|
220
|
+
XSD
|
|
221
|
+
end
|
|
222
|
+
|
|
223
|
+
it "rejects a declared element the ##defined wildcard excludes" do
|
|
224
|
+
doc = ctx.parse(
|
|
225
|
+
'<container xmlns="http://example.com/w"><known>x</known></container>',
|
|
226
|
+
)
|
|
227
|
+
expect(schema.valid?(doc)).to be(false)
|
|
228
|
+
expect(schema.validate(doc).map(&:message)).to include(
|
|
229
|
+
/disallowed by a wildcard \(notQName\)/,
|
|
230
|
+
)
|
|
231
|
+
end
|
|
232
|
+
|
|
233
|
+
it "absorbs an undeclared element laxly" do
|
|
234
|
+
doc = ctx.parse(
|
|
235
|
+
'<container xmlns="http://example.com/w"><unknown>x</unknown></container>',
|
|
236
|
+
)
|
|
237
|
+
expect(schema.valid?(doc)).to be(true)
|
|
238
|
+
end
|
|
239
|
+
end
|
|
240
|
+
|
|
241
|
+
describe "xs:any notNamespace" do
|
|
242
|
+
let(:schema) do
|
|
243
|
+
ctx.xsd(<<~XSD)
|
|
244
|
+
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema"
|
|
245
|
+
targetNamespace="http://example.com/w"
|
|
246
|
+
elementFormDefault="qualified">
|
|
247
|
+
<xs:element name="container">
|
|
248
|
+
<xs:complexType>
|
|
249
|
+
<xs:sequence>
|
|
250
|
+
<xs:any processContents="lax"
|
|
251
|
+
notNamespace="##targetNamespace"
|
|
252
|
+
minOccurs="0" maxOccurs="unbounded"/>
|
|
253
|
+
</xs:sequence>
|
|
254
|
+
</xs:complexType>
|
|
255
|
+
</xs:element>
|
|
256
|
+
</xs:schema>
|
|
257
|
+
XSD
|
|
258
|
+
end
|
|
259
|
+
|
|
260
|
+
it "rejects an element in the disallowed namespace" do
|
|
261
|
+
doc = ctx.parse(
|
|
262
|
+
'<container xmlns="http://example.com/w"><stray/></container>',
|
|
263
|
+
)
|
|
264
|
+
expect(schema.valid?(doc)).to be(false)
|
|
265
|
+
expect(schema.validate(doc).map(&:message)).to include(
|
|
266
|
+
/disallowed by a wildcard \(notNamespace\)/,
|
|
267
|
+
)
|
|
268
|
+
end
|
|
269
|
+
|
|
270
|
+
it "absorbs an element from an allowed namespace" do
|
|
271
|
+
doc = ctx.parse(
|
|
272
|
+
'<container xmlns="http://example.com/w">' \
|
|
273
|
+
'<stray xmlns="http://example.com/other"/></container>',
|
|
274
|
+
)
|
|
275
|
+
expect(schema.valid?(doc)).to be(true)
|
|
276
|
+
end
|
|
277
|
+
end
|
|
278
|
+
|
|
279
|
+
describe "xs:anyAttribute notQName=##defined" do
|
|
280
|
+
let(:schema) do
|
|
281
|
+
ctx.xsd(<<~XSD)
|
|
282
|
+
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema"
|
|
283
|
+
targetNamespace="http://example.com/w"
|
|
284
|
+
elementFormDefault="qualified"
|
|
285
|
+
attributeFormDefault="qualified">
|
|
286
|
+
<xs:element name="settings">
|
|
287
|
+
<xs:complexType>
|
|
288
|
+
<xs:complexContent>
|
|
289
|
+
<xs:restriction base="xs:anyType">
|
|
290
|
+
<xs:anyAttribute processContents="lax"
|
|
291
|
+
notQName="##defined"/>
|
|
292
|
+
</xs:restriction>
|
|
293
|
+
</xs:complexContent>
|
|
294
|
+
</xs:complexType>
|
|
295
|
+
</xs:element>
|
|
296
|
+
<xs:attribute name="marked" type="xs:string"/>
|
|
297
|
+
</xs:schema>
|
|
298
|
+
XSD
|
|
299
|
+
end
|
|
300
|
+
|
|
301
|
+
it "rejects a declared attribute the ##defined wildcard excludes" do
|
|
302
|
+
doc = ctx.parse(
|
|
303
|
+
'<settings xmlns="http://example.com/w" ' \
|
|
304
|
+
'xmlns:w="http://example.com/w" w:marked="x"/>',
|
|
305
|
+
)
|
|
306
|
+
expect(schema.valid?(doc)).to be(false)
|
|
307
|
+
expect(schema.validate(doc).map(&:message)).to include(
|
|
308
|
+
/attribute marked is disallowed by a wildcard \(notQName\)/,
|
|
309
|
+
)
|
|
310
|
+
end
|
|
311
|
+
|
|
312
|
+
it "absorbs an undeclared attribute laxly" do
|
|
313
|
+
doc = ctx.parse(
|
|
314
|
+
'<settings xmlns="http://example.com/w" free="x"/>',
|
|
315
|
+
)
|
|
316
|
+
expect(schema.valid?(doc)).to be(true)
|
|
317
|
+
end
|
|
318
|
+
end
|
|
319
|
+
end
|
|
320
|
+
|
|
155
321
|
describe "wrong-namespace root" do
|
|
156
322
|
let(:schema) do
|
|
157
323
|
ctx.xsd(<<~XSD)
|
|
@@ -89,6 +89,10 @@ 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
|
+
},
|
|
92
96
|
"loud capability contracts" => {
|
|
93
97
|
section: "Errors and loud degradation",
|
|
94
98
|
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.165
|
|
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,6 +631,7 @@ 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
|
|
636
637
|
- spec/moxml/xsd_underdetection_spec.rb
|