moxml 0.5.165 → 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 +1 -0
- data/docs/_references/processing-model.adoc +8 -2
- data/lib/moxml/version.rb +1 -1
- data/lib/moxml/xsd/schema_rules.rb +178 -44
- data/spec/moxml/xsd_substitution_spec.rb +166 -0
- data/spec/support/model_coverage.rb +4 -0
- metadata +2 -1
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
|
|
@@ -43,6 +43,7 @@ Generated by `bundle exec rake docs:coverage` from the coverage matrix and the d
|
|
|
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
45
|
| validation regex dismissal | equivalent | equivalent | equivalent | equivalent | equivalent | equivalent | equivalent
|
|
46
|
+
| validation substitution dismissal | equivalent | equivalent | equivalent | equivalent | equivalent | equivalent | equivalent
|
|
46
47
|
| loud capability contracts | equivalent | equivalent | equivalent | equivalent | equivalent | equivalent | equivalent
|
|
47
48
|
|===
|
|
48
49
|
|
|
@@ -255,7 +255,7 @@ semantics.
|
|
|
255
255
|
|
|
256
256
|
=== Validation
|
|
257
257
|
|
|
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`
|
|
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
|
|
@@ -278,7 +278,13 @@ single-character escapes; subtraction, `\i`, `\c` do not), an
|
|
|
278
278
|
engine finding is dropped only when every same-named instance
|
|
279
279
|
matches the facet, and anything untranslatable keeps the engine's
|
|
280
280
|
answer, so a dismissal can only remove false rejections, never
|
|
281
|
-
create false acceptances.
|
|
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
|
|
282
288
|
version its source declares: root `vc:minVersion="1.1"` scopes the
|
|
283
289
|
document to 1.1, anything else is 1.0 (consumers scope 1.0-only
|
|
284
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,11 +125,34 @@ 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
132
|
:attributes, :contents, :version,
|
|
147
|
-
:element_patterns, :attribute_patterns
|
|
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
|
|
148
156
|
|
|
149
157
|
# The XSD 1.1 version-tagging namespace (Structures §C.3):
|
|
150
158
|
# vc:minVersion / vc:maxVersion gate 1.1 constructs.
|
|
@@ -243,7 +251,7 @@ module Moxml
|
|
|
243
251
|
root["targetNamespace"].to_s,
|
|
244
252
|
root["elementFormDefault"] == "qualified",
|
|
245
253
|
root["attributeFormDefault"] == "qualified",
|
|
246
|
-
{}, {}, {}, schema_version(root), {}, {}
|
|
254
|
+
{}, {}, {}, schema_version(root), {}, {}, {}
|
|
247
255
|
)
|
|
248
256
|
child_elements(root).each do |decl|
|
|
249
257
|
case decl.name
|
|
@@ -339,27 +347,36 @@ module Moxml
|
|
|
339
347
|
sink[name] = sink.key?(name) ? nil : patterns
|
|
340
348
|
end
|
|
341
349
|
|
|
342
|
-
# Dismisses engine
|
|
343
|
-
#
|
|
344
|
-
#
|
|
345
|
-
#
|
|
346
|
-
#
|
|
347
|
-
#
|
|
348
|
-
#
|
|
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.
|
|
350
363
|
def dismiss(engine_errors, doc, index)
|
|
351
364
|
return engine_errors unless index
|
|
352
365
|
|
|
353
366
|
vouched = vouch_table(doc, index)
|
|
354
367
|
engine_errors.reject do |error|
|
|
355
|
-
subject = lexical_subject(error.message)
|
|
356
|
-
|
|
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
|
|
357
373
|
end
|
|
358
374
|
end
|
|
359
375
|
|
|
360
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/
|
|
361
378
|
|
|
362
|
-
private_constant :LEXICAL_MESSAGE
|
|
379
|
+
private_constant :LEXICAL_MESSAGE, :CONTENT_MODEL_MESSAGE
|
|
363
380
|
|
|
364
381
|
def lexical_subject(message)
|
|
365
382
|
match = LEXICAL_MESSAGE.match(message.to_s)
|
|
@@ -368,6 +385,81 @@ module Moxml
|
|
|
368
385
|
[match[2], match[3]]
|
|
369
386
|
end
|
|
370
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
|
+
|
|
371
463
|
def vouch_table(doc, index)
|
|
372
464
|
root = doc.root
|
|
373
465
|
return {} unless root
|
|
@@ -434,6 +526,9 @@ module Moxml
|
|
|
434
526
|
name = decl["name"] or return
|
|
435
527
|
qname = "{#{index.target_namespace}}#{name}"
|
|
436
528
|
index.elements[qname] = { nillable: decl["nillable"] == "true" }
|
|
529
|
+
if (head = decl["substitutionGroup"])
|
|
530
|
+
register_substitution(head, decl, qname, index)
|
|
531
|
+
end
|
|
437
532
|
type_attr = decl.attribute("type")
|
|
438
533
|
model = if type_attr.nil? || type_attr.value.empty?
|
|
439
534
|
inline_model(decl, index)
|
|
@@ -443,6 +538,23 @@ module Moxml
|
|
|
443
538
|
index.contents[qname] = model if model
|
|
444
539
|
end
|
|
445
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
|
+
|
|
446
558
|
def index_global_attribute(decl, index)
|
|
447
559
|
name = decl["name"] or return
|
|
448
560
|
index.attributes["{#{index.target_namespace}}#{name}"] = true
|
|
@@ -468,7 +580,7 @@ module Moxml
|
|
|
468
580
|
end
|
|
469
581
|
|
|
470
582
|
def content_model(type_node, index)
|
|
471
|
-
model = ContentModel.new([], nil, [], nil)
|
|
583
|
+
model = ContentModel.new([], nil, [], nil, [], true)
|
|
472
584
|
collect_model(type_node, index, model)
|
|
473
585
|
model
|
|
474
586
|
end
|
|
@@ -477,36 +589,58 @@ module Moxml
|
|
|
477
589
|
# choice / all / complexContent wrappers flattened): we only
|
|
478
590
|
# ADD findings for strict-wildcard matches, so flattening
|
|
479
591
|
# can only under-report, never over-report. Cardinality and
|
|
480
|
-
# 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.
|
|
481
596
|
def collect_model(node, index, model)
|
|
482
597
|
child_elements(node).each do |child|
|
|
483
598
|
case child.name
|
|
484
599
|
when "element"
|
|
485
|
-
|
|
486
|
-
|
|
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
|
|
487
608
|
when "attribute"
|
|
488
|
-
|
|
489
|
-
|
|
609
|
+
if (qname = particle_qname(child, index))
|
|
610
|
+
model.declared_attributes << qname
|
|
611
|
+
end
|
|
490
612
|
when "any"
|
|
491
613
|
model.any ||= wildcard_from(child)
|
|
492
614
|
when "anyAttribute"
|
|
493
615
|
model.any_attribute ||= wildcard_from(child)
|
|
616
|
+
when "choice", "all"
|
|
617
|
+
model.pure_sequence = false
|
|
618
|
+
collect_model(child, index, model)
|
|
494
619
|
else
|
|
495
620
|
collect_model(child, index, model)
|
|
496
621
|
end
|
|
497
622
|
end
|
|
498
623
|
end
|
|
499
624
|
|
|
500
|
-
def
|
|
625
|
+
def particle_qname(particle, index)
|
|
501
626
|
ref = particle["ref"]
|
|
502
627
|
name = ref&.split(":")&.last || particle["name"]
|
|
503
628
|
return unless name
|
|
504
629
|
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
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
|
|
510
644
|
end
|
|
511
645
|
|
|
512
646
|
# Particles declare with form="qualified"/"unqualified",
|
|
@@ -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
|
|
@@ -93,6 +93,10 @@ module ModelCoverage
|
|
|
93
93
|
section: "Validation",
|
|
94
94
|
pin: "spec/moxml/xsd_regex_spec.rb",
|
|
95
95
|
},
|
|
96
|
+
"validation substitution dismissal" => {
|
|
97
|
+
section: "Validation",
|
|
98
|
+
pin: "spec/moxml/xsd_substitution_spec.rb",
|
|
99
|
+
},
|
|
96
100
|
"loud capability contracts" => {
|
|
97
101
|
section: "Errors and loud degradation",
|
|
98
102
|
pin: "spec/consistency/loud_degradation_spec.rb",
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
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.
|
|
@@ -634,6 +634,7 @@ files:
|
|
|
634
634
|
- spec/moxml/xsd_regex_spec.rb
|
|
635
635
|
- spec/moxml/xsd_release_spec.rb
|
|
636
636
|
- spec/moxml/xsd_spec.rb
|
|
637
|
+
- spec/moxml/xsd_substitution_spec.rb
|
|
637
638
|
- spec/moxml/xsd_underdetection_spec.rb
|
|
638
639
|
- spec/moxml/xsd_version_spec.rb
|
|
639
640
|
- spec/moxml/xslt/stylesheet_spec.rb
|