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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 8dae9d144ec375224efd389406ed2cdb06c5409ae91d985e95221a972a1158ac
4
- data.tar.gz: 308da2c1430d4f7f6b195eeec3d4ba9f651c9664f30c2a6c1381923572409fbd
3
+ metadata.gz: 932f907222ac657e72e53b9e5ac2e514bb6aa07a51da815caa7ee31c0b0a02fa
4
+ data.tar.gz: 11c82e15a42a80adb5bb19b4b819e9b4cfa1e7e508c6088d383dfdd793a33956
5
5
  SHA512:
6
- metadata.gz: 794455e01d1021344dd5f792802d10403cc35a57b4fd167dee967d59c8079f2e3467a392433dff45c9959919569d2e3fc68b4f0db8535257bb646ce9f48821ea
7
- data.tar.gz: a2e777a2b636b71432ee8baa0610254cb2b464b33843f57063d31a6688974394cfc25e8c6e91851448c97f563acd80261833c5542d5b9b962aa2dae240c8aa27
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. A compiled schema reports the
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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Moxml
4
- VERSION = "0.5.165"
4
+ VERSION = "0.5.166"
5
5
  end
@@ -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(Regexp.last_match(1), element)
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 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
+ # 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
- subject && vouched[subject]
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
- add_particle(child, index, model,
486
- model.declared_children)
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
- add_particle(child, index, model,
489
- model.declared_attributes)
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 add_particle(particle, index, _model, sink)
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
- sink << if ref
506
- "{#{index.target_namespace}}#{name}"
507
- else
508
- "{#{form_namespace(particle, index)}}#{name}"
509
- end
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.165
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