moxml 0.5.163 → 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 +13 -2
- data/lib/moxml/version.rb +1 -1
- data/lib/moxml/xsd/schema_rules.rb +221 -2
- data/lib/moxml/xsd.rb +5 -3
- data/spec/moxml/xsd_regex_spec.rb +188 -0
- 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
|
|
@@ -267,7 +267,18 @@ 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. A compiled schema reports the
|
|
271
282
|
version its source declares: root `vc:minVersion="1.1"` scopes the
|
|
272
283
|
document to 1.1, anything else is 1.0 (consumers scope 1.0-only
|
|
273
284
|
checks with it; applying 1.1 semantics is the engine-side program).
|
data/lib/moxml/version.rb
CHANGED
|
@@ -143,12 +143,94 @@ module Moxml
|
|
|
143
143
|
:declared_attributes, :any_attribute)
|
|
144
144
|
SchemaIndex = Struct.new(:target_namespace, :element_default,
|
|
145
145
|
:attribute_default, :elements,
|
|
146
|
-
:attributes, :contents, :version
|
|
146
|
+
:attributes, :contents, :version,
|
|
147
|
+
:element_patterns, :attribute_patterns)
|
|
147
148
|
|
|
148
149
|
# The XSD 1.1 version-tagging namespace (Structures §C.3):
|
|
149
150
|
# vc:minVersion / vc:maxVersion gate 1.1 constructs.
|
|
150
151
|
VC_NS = "http://www.w3.org/2001/XMLSchema-versioning"
|
|
151
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
|
+
|
|
152
234
|
class << self
|
|
153
235
|
# Index one schema document. Returns nil when the source
|
|
154
236
|
# cannot be indexed — the engine's answer then stands alone
|
|
@@ -161,7 +243,7 @@ module Moxml
|
|
|
161
243
|
root["targetNamespace"].to_s,
|
|
162
244
|
root["elementFormDefault"] == "qualified",
|
|
163
245
|
root["attributeFormDefault"] == "qualified",
|
|
164
|
-
{}, {}, {}, schema_version(root)
|
|
246
|
+
{}, {}, {}, schema_version(root), {}, {}
|
|
165
247
|
)
|
|
166
248
|
child_elements(root).each do |decl|
|
|
167
249
|
case decl.name
|
|
@@ -169,6 +251,7 @@ module Moxml
|
|
|
169
251
|
when "attribute" then index_global_attribute(decl, index)
|
|
170
252
|
end
|
|
171
253
|
end
|
|
254
|
+
index_patterns(root, index)
|
|
172
255
|
index
|
|
173
256
|
rescue StandardError
|
|
174
257
|
nil
|
|
@@ -188,6 +271,142 @@ module Moxml
|
|
|
188
271
|
declared.to_s.start_with?("1.1") ? "1.1" : "1.0"
|
|
189
272
|
end
|
|
190
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
|
+
|
|
191
410
|
# Post-validation findings for one instance document.
|
|
192
411
|
def findings(doc, index)
|
|
193
412
|
return [] unless index
|
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
|
|
@@ -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
|