moxml 0.5.167 → 0.5.168
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 +9 -2
- data/lib/moxml/adapter/base.rb +0 -5
- data/lib/moxml/adapter/leptris.rb +11 -9
- data/lib/moxml/version.rb +1 -1
- data/lib/moxml/xpath/ordering.rb +11 -0
- data/lib/moxml/xsd/builtin_lexical.rb +251 -0
- data/lib/moxml/xsd.rb +7 -6
- data/spec/consistency/xpath_idioms_spec.rb +28 -0
- data/spec/moxml/xsd_builtin_lexical_spec.rb +139 -0
- data/spec/support/model_coverage.rb +4 -0
- metadata +3 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 5b55c7bbd0cbaa945ab4404c9fe40b354ae5b291672804f874b33318abd26dc3
|
|
4
|
+
data.tar.gz: 33ac60d77afebe105bf6ccfcd4f07482f1b33c42cf573c6e169a2b2aacc0b2df
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 33d2df43783702c43372335cfe88fb0449cb04984889f3932fcd085592c26f33ad68303c0d4019a6ed01831a325bd1070d2d261a47cfc604867b9b221452a69a
|
|
7
|
+
data.tar.gz: 5417a78c416bbbc737a13e9c369f7a7064e5946f44da6e80dad795fee279c1d355f86d10dd5f95bfcbe61b44eba1a7c7d3f6cd462197b7a00698a1f74f81fa35
|
|
@@ -44,6 +44,7 @@ Generated by `bundle exec rake docs:coverage` from the coverage matrix and the d
|
|
|
44
44
|
| validation version reporting | equivalent | equivalent | equivalent | equivalent | equivalent | equivalent | equivalent
|
|
45
45
|
| validation engine metadata | equivalent | equivalent | equivalent | equivalent | equivalent | equivalent | equivalent
|
|
46
46
|
| validation regex dismissal | equivalent | equivalent | equivalent | equivalent | equivalent | equivalent | equivalent
|
|
47
|
+
| validation builtin lexical table | equivalent | equivalent | equivalent | equivalent | equivalent | equivalent | equivalent
|
|
47
48
|
| validation substitution dismissal | equivalent | equivalent | equivalent | equivalent | equivalent | equivalent | equivalent
|
|
48
49
|
| loud capability contracts | equivalent | equivalent | equivalent | equivalent | equivalent | equivalent | equivalent
|
|
49
50
|
|===
|
|
@@ -255,11 +255,18 @@ semantics.
|
|
|
255
255
|
|
|
256
256
|
=== Validation
|
|
257
257
|
|
|
258
|
-
Pins:: `spec/moxml/xsd_engine_meta_spec.rb`, `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`
|
|
258
|
+
Pins:: `spec/moxml/xsd_builtin_lexical_spec.rb`, `spec/moxml/xsd_engine_meta_spec.rb`, `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
|
|
262
|
-
checks).
|
|
262
|
+
checks). The built-in lexical answer is facade-owned:
|
|
263
|
+
`Moxml::XSD.builtin_valid?` evaluates the Datatypes Part 2 lexical
|
|
264
|
+
spaces (collapse for the token family, the unbounded integer family
|
|
265
|
+
with bignum bounds, calendar and duration grammars, binary, QName
|
|
266
|
+
with the reserved `xmlns` prefix) from the facade's own table on
|
|
267
|
+
every adapter — an engine table converges to it, never the reverse;
|
|
268
|
+
an unknown name raises `ArgumentError`. `validate` returns
|
|
269
|
+
accumulated `ValidationError` entries —
|
|
263
270
|
message always; line/column/path where the engine reports them, nil
|
|
264
271
|
where it does not; `[]` means valid. Documents outside the engine's
|
|
265
272
|
scope validate laxly, except that strict `xs:any` /
|
data/lib/moxml/adapter/base.rb
CHANGED
|
@@ -373,11 +373,6 @@ module Moxml
|
|
|
373
373
|
"XSD validation requires an adapter with an XSD engine"
|
|
374
374
|
end
|
|
375
375
|
|
|
376
|
-
def xsd_builtin_valid?(_builtin, _lexical)
|
|
377
|
-
raise Moxml::NotImplementedError,
|
|
378
|
-
"XSD validation requires an adapter with an XSD engine"
|
|
379
|
-
end
|
|
380
|
-
|
|
381
376
|
def xsd_declarations(_xsd_handle); end
|
|
382
377
|
|
|
383
378
|
def xsd_compile_error(_xsd_handle); end
|
|
@@ -406,15 +406,6 @@ module Moxml
|
|
|
406
406
|
handle.valid?(to_binding(document))
|
|
407
407
|
end
|
|
408
408
|
|
|
409
|
-
def xsd_builtin_valid?(builtin, lexical)
|
|
410
|
-
unless XSD_SUPPORTED
|
|
411
|
-
raise Moxml::NotImplementedError,
|
|
412
|
-
"XSD validation requires an adapter with an XSD engine"
|
|
413
|
-
end
|
|
414
|
-
|
|
415
|
-
::Leptris::XML::XSD.builtin_valid?(builtin, lexical)
|
|
416
|
-
end
|
|
417
|
-
|
|
418
409
|
def xsd_declarations(xsd_handle)
|
|
419
410
|
xsd_handle.declaration_count
|
|
420
411
|
end
|
|
@@ -939,9 +930,20 @@ module Moxml
|
|
|
939
930
|
# Protocol-level node equality: one engine can hand out
|
|
940
931
|
# two wrapper classes over the same C node (native layer +
|
|
941
932
|
# binding), so raw == is not identity across the seam.
|
|
933
|
+
# Attributes are (name, value, element) triples without a
|
|
934
|
+
# node pointer; identity is the owning element plus the
|
|
935
|
+
# attribute name.
|
|
942
936
|
def same_node?(one, other)
|
|
943
937
|
return true if one.equal?(other)
|
|
944
938
|
|
|
939
|
+
if one.is_a?(::Leptris::XML::Attr) ||
|
|
940
|
+
other.is_a?(::Leptris::XML::Attr)
|
|
941
|
+
return one.is_a?(::Leptris::XML::Attr) &&
|
|
942
|
+
other.is_a?(::Leptris::XML::Attr) &&
|
|
943
|
+
one.name == other.name &&
|
|
944
|
+
same_node?(one.element, other.element)
|
|
945
|
+
end
|
|
946
|
+
|
|
945
947
|
if NATIVE_READ_LAYER
|
|
946
948
|
native_one = one.is_a?(::Leptris::XML::NativeNode)
|
|
947
949
|
native_other = other.is_a?(::Leptris::XML::NativeNode)
|
data/lib/moxml/version.rb
CHANGED
data/lib/moxml/xpath/ordering.rb
CHANGED
|
@@ -75,7 +75,18 @@ module Moxml
|
|
|
75
75
|
[kind_rank, ancestry_path(node)]
|
|
76
76
|
end
|
|
77
77
|
|
|
78
|
+
# Attributes are not children of their element, so their
|
|
79
|
+
# document position is the parent's; same-parent attributes
|
|
80
|
+
# keep their relative order (stable sort). A parentless
|
|
81
|
+
# attribute stays unplaceable.
|
|
78
82
|
def ancestry_path(node)
|
|
83
|
+
if node.is_a?(Moxml::Attribute)
|
|
84
|
+
parent = node.parent
|
|
85
|
+
return [2_000_000] unless parent
|
|
86
|
+
|
|
87
|
+
return ancestry_path(parent)
|
|
88
|
+
end
|
|
89
|
+
|
|
79
90
|
path = []
|
|
80
91
|
current = node
|
|
81
92
|
while current && !current.is_a?(Moxml::Document)
|
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Moxml
|
|
4
|
+
module XSD
|
|
5
|
+
# Facade-owned lexical validation for the XSD built-in simple
|
|
6
|
+
# types (Datatypes Part 2). One committed answer for every
|
|
7
|
+
# adapter: engines with their own tables converge to this one,
|
|
8
|
+
# never the reverse (#482). The check is lexical only — pattern,
|
|
9
|
+
# enumeration and other constraining facets belong to the
|
|
10
|
+
# schema, not to the built-in's own space.
|
|
11
|
+
module BuiltinLexical
|
|
12
|
+
module_function
|
|
13
|
+
|
|
14
|
+
# Answers for the "xs:NAME" reference spelling. Unknown names
|
|
15
|
+
# raise ArgumentError — the facade never guesses.
|
|
16
|
+
def valid?(builtin, lexical)
|
|
17
|
+
checker = CHECKERS.fetch(builtin) do
|
|
18
|
+
raise ArgumentError, "unknown built-in type #{builtin.inspect}"
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
checker.call(lexical.to_s)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
# XML 1.0 5th ed. name productions. NCName excludes ":";
|
|
25
|
+
# xs:Name and xs:NMTOKEN admit it.
|
|
26
|
+
NCNAME_START_CLASS =
|
|
27
|
+
"A-Z_a-z\u{C0}-\u{D6}\u{D8}-\u{F6}\u{F8}-\u{2FF}\u{370}-\u{37D}" \
|
|
28
|
+
"\u{37F}-\u{1FFF}\u{200C}-\u{200D}\u{2070}-\u{218F}\u{2C00}-\u{2FEF}" \
|
|
29
|
+
"\u{3001}-\u{D7FF}\u{F900}-\u{FDCF}\u{FDF0}-\u{FFFD}"
|
|
30
|
+
NCNAME_CHAR_CLASS =
|
|
31
|
+
"#{NCNAME_START_CLASS}0-9.\\-\u{B7}\u{300}-\u{36F}\u{203F}-\u{2040}".freeze
|
|
32
|
+
|
|
33
|
+
NCNAME = /\A[#{NCNAME_START_CLASS}][#{NCNAME_CHAR_CLASS}]*\z/
|
|
34
|
+
NAME = /\A[:#{NCNAME_START_CLASS}][:#{NCNAME_CHAR_CLASS}]*\z/
|
|
35
|
+
NMTOKEN = /\A[:#{NCNAME_CHAR_CLASS}]+\z/
|
|
36
|
+
|
|
37
|
+
INTEGER_LEXICAL = /\A[+-]?[0-9]+\z/
|
|
38
|
+
DECIMAL_LEXICAL = /\A[+-]?(?:[0-9]+(?:\.[0-9]*)?|\.[0-9]+)\z/
|
|
39
|
+
FLOAT_LEXICAL = /\A(?:[+-]?(?:[0-9]+(?:\.[0-9]*)?|\.[0-9]+)(?:[Ee][+-]?[0-9]+)?|[+-]?INF|NaN)\z/
|
|
40
|
+
BOOLEAN_LEXICAL = /\A(?:true|false|0|1)\z/
|
|
41
|
+
LANGUAGE_LEXICAL = /\A[A-Za-z]{1,8}(?:-[A-Za-z0-9]{1,8})*\z/
|
|
42
|
+
HEX_LEXICAL = /\A(?:[0-9A-Fa-f]{2})*\z/
|
|
43
|
+
BASE64_LEXICAL = /\A(?:[A-Za-z0-9+\/]{4})*(?:[A-Za-z0-9+\/]{2}==|[A-Za-z0-9+\/]{3}=)?\z/
|
|
44
|
+
|
|
45
|
+
DATE_PART = /-?(?:[1-9][0-9]{3,}|0[0-9]{3})-(?:0[1-9]|1[0-2])-(?:0[1-9]|[12][0-9]|3[01])/
|
|
46
|
+
TIME_PART = /(?:[01][0-9]|2[0-3]):[0-5][0-9]:[0-5][0-9](?:\.[0-9]+)?/
|
|
47
|
+
ZONE = /(?:Z|[+-](?:0[0-9]|1[0-3]):[0-5][0-9]|14:00)/
|
|
48
|
+
DATE_LEXICAL = /\A#{DATE_PART}#{ZONE}?\z/
|
|
49
|
+
TIME_LEXICAL = /\A#{TIME_PART}#{ZONE}?\z/
|
|
50
|
+
DATE_TIME_LEXICAL = /\A#{DATE_PART}T#{TIME_PART}#{ZONE}?\z/
|
|
51
|
+
DATE_TIME_STAMP_LEXICAL = /\A#{DATE_PART}T#{TIME_PART}#{ZONE}\z/
|
|
52
|
+
G_YEAR_LEXICAL = /\A-?(?:[1-9][0-9]{3,}|0[0-9]{3})#{ZONE}?\z/
|
|
53
|
+
G_YEAR_MONTH_LEXICAL = /\A-?(?:[1-9][0-9]{3,}|0[0-9]{3})-(?:0[1-9]|1[0-2])#{ZONE}?\z/
|
|
54
|
+
G_MONTH_LEXICAL = /\A--(?:0[1-9]|1[0-2])#{ZONE}?\z/
|
|
55
|
+
G_DAY_LEXICAL = /\A---(?:0[1-9]|[12][0-9]|3[01])#{ZONE}?\z/
|
|
56
|
+
G_MONTH_DAY_LEXICAL = /\A--(?:0[1-9]|1[0-2])-(?:0[1-9]|[12][0-9]|3[01])#{ZONE}?\z/
|
|
57
|
+
DURATION_LEXICAL = /\A-?P(?:[0-9]+Y)?(?:[0-9]+M)?(?:[0-9]+D)?(?:T(?:[0-9]+H)?(?:[0-9]+M)?(?:[0-9]+(?:\.[0-9]+)?S)?)?\z/
|
|
58
|
+
|
|
59
|
+
MONTH_DAYS = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31].freeze
|
|
60
|
+
|
|
61
|
+
private_constant :NCNAME_START_CLASS, :NCNAME_CHAR_CLASS, :NCNAME,
|
|
62
|
+
:NAME, :NMTOKEN, :INTEGER_LEXICAL, :DECIMAL_LEXICAL,
|
|
63
|
+
:FLOAT_LEXICAL, :BOOLEAN_LEXICAL, :LANGUAGE_LEXICAL,
|
|
64
|
+
:HEX_LEXICAL, :BASE64_LEXICAL, :DATE_PART,
|
|
65
|
+
:TIME_PART, :ZONE, :DATE_LEXICAL, :TIME_LEXICAL,
|
|
66
|
+
:DATE_TIME_LEXICAL, :DATE_TIME_STAMP_LEXICAL,
|
|
67
|
+
:G_YEAR_LEXICAL, :G_YEAR_MONTH_LEXICAL,
|
|
68
|
+
:G_MONTH_LEXICAL, :G_DAY_LEXICAL,
|
|
69
|
+
:G_MONTH_DAY_LEXICAL, :DURATION_LEXICAL, :MONTH_DAYS
|
|
70
|
+
|
|
71
|
+
# whiteSpace=collapse, shared by token and its derivatives.
|
|
72
|
+
def collapsed(lexical)
|
|
73
|
+
lexical.gsub(/[ \t\r\n]+/, " ").strip
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def ncname?(lexical)
|
|
77
|
+
NCNAME.match?(lexical)
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
# A QName is an optional NCName prefix, a colon, and an NCName
|
|
81
|
+
# local part. The "xmlns" prefix is reserved (Namespaces in
|
|
82
|
+
# XML) and never a legal QName prefix.
|
|
83
|
+
def qname?(lexical)
|
|
84
|
+
prefix, colon, local = lexical.partition(":")
|
|
85
|
+
return ncname?(lexical) if colon.empty?
|
|
86
|
+
|
|
87
|
+
return false if prefix == "xmlns"
|
|
88
|
+
|
|
89
|
+
ncname?(prefix) && ncname?(local)
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
# List lexical spaces are whitespace-separated items, at least
|
|
93
|
+
# one; every item must be a valid item-type lexical.
|
|
94
|
+
def list?(lexical, &block)
|
|
95
|
+
items = lexical.split(/[ \t\r\n]+/).reject(&:empty?)
|
|
96
|
+
!items.empty? && items.all?(&block)
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
# The grammar admits a bare "T" and the empty body; the
|
|
100
|
+
# lexical space requires at least one component, and a "T"
|
|
101
|
+
# must be followed by one.
|
|
102
|
+
def duration?(lexical)
|
|
103
|
+
return false unless DURATION_LEXICAL.match?(lexical)
|
|
104
|
+
|
|
105
|
+
body = lexical.sub(/\A-?P/, "")
|
|
106
|
+
if (time_at = body.index("T"))
|
|
107
|
+
/[0-9]/.match?(body[(time_at + 1)..].to_s)
|
|
108
|
+
else
|
|
109
|
+
/[0-9]/.match?(body)
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
def day_time_duration?(lexical)
|
|
114
|
+
return false unless duration?(lexical)
|
|
115
|
+
|
|
116
|
+
head = lexical.split("T", 2).first.to_s
|
|
117
|
+
!/[0-9]+[YM]/.match?(head)
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
def year_month_duration?(lexical)
|
|
121
|
+
duration?(lexical) && !/[0-9]+D/.match?(lexical) &&
|
|
122
|
+
!lexical.include?("T")
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
def leap_year?(year)
|
|
126
|
+
((year % 4).zero? && !(year % 100).zero?) || (year % 400).zero?
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
# Dates carry calendar validity in their lexical space: the
|
|
130
|
+
# day must exist for the month and year (Gregorian, leap
|
|
131
|
+
# rules on the absolute year).
|
|
132
|
+
def calendar_date?(year, month, day)
|
|
133
|
+
return false unless month.between?(1, 12)
|
|
134
|
+
|
|
135
|
+
limit = MONTH_DAYS[month - 1]
|
|
136
|
+
limit += 1 if month == 2 && leap_year?(year.abs)
|
|
137
|
+
day.between?(1, limit)
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
def date_fields(lexical)
|
|
141
|
+
match = /\A(-?[0-9]{4,})-([0-9]{2})-([0-9]{2})/.match(lexical)
|
|
142
|
+
return unless match
|
|
143
|
+
|
|
144
|
+
match.captures.map { |field| Integer(field) }
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
def with_date_fields(lexical)
|
|
148
|
+
year, month, day = date_fields(lexical)
|
|
149
|
+
year && yield(year, month, day)
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
def integer?(lexical, range: nil)
|
|
153
|
+
return false unless INTEGER_LEXICAL.match?(lexical)
|
|
154
|
+
|
|
155
|
+
range.nil? || range.cover?(Integer(lexical))
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
def signed(range)
|
|
159
|
+
->(lexical) { integer?(lexical, range: range) }
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
private_class_method :collapsed, :ncname?, :qname?, :list?,
|
|
163
|
+
:duration?, :day_time_duration?,
|
|
164
|
+
:year_month_duration?, :leap_year?,
|
|
165
|
+
:calendar_date?, :date_fields,
|
|
166
|
+
:with_date_fields, :integer?, :signed
|
|
167
|
+
|
|
168
|
+
CHECKERS = {
|
|
169
|
+
"xs:anySimpleType" => ->(_lexical) { true },
|
|
170
|
+
"xs:anyAtomicType" => ->(_lexical) { true },
|
|
171
|
+
"xs:anyType" => ->(_lexical) { true },
|
|
172
|
+
"xs:string" => ->(_lexical) { true },
|
|
173
|
+
"xs:normalizedString" => ->(_lexical) { true },
|
|
174
|
+
"xs:token" => ->(_lexical) { true },
|
|
175
|
+
"xs:language" => ->(lexical) { LANGUAGE_LEXICAL.match?(collapsed(lexical)) },
|
|
176
|
+
"xs:Name" => ->(lexical) { NAME.match?(collapsed(lexical)) },
|
|
177
|
+
"xs:NCName" => ->(lexical) { ncname?(collapsed(lexical)) },
|
|
178
|
+
"xs:ID" => ->(lexical) { ncname?(collapsed(lexical)) },
|
|
179
|
+
"xs:IDREF" => ->(lexical) { ncname?(collapsed(lexical)) },
|
|
180
|
+
"xs:ENTITY" => ->(lexical) { ncname?(collapsed(lexical)) },
|
|
181
|
+
"xs:NMTOKEN" => ->(lexical) { NMTOKEN.match?(collapsed(lexical)) },
|
|
182
|
+
"xs:IDREFS" => ->(lexical) { list?(lexical) { |item| ncname?(item) } },
|
|
183
|
+
"xs:ENTITIES" => ->(lexical) { list?(lexical) { |item| ncname?(item) } },
|
|
184
|
+
"xs:NMTOKENS" => ->(lexical) { list?(lexical) { |item| NMTOKEN.match?(item) } },
|
|
185
|
+
"xs:boolean" => ->(lexical) { BOOLEAN_LEXICAL.match?(lexical) },
|
|
186
|
+
"xs:decimal" => ->(lexical) { DECIMAL_LEXICAL.match?(lexical) },
|
|
187
|
+
"xs:precisionDecimal" => ->(lexical) { FLOAT_LEXICAL.match?(lexical) },
|
|
188
|
+
"xs:float" => ->(lexical) { FLOAT_LEXICAL.match?(lexical) },
|
|
189
|
+
"xs:double" => ->(lexical) { FLOAT_LEXICAL.match?(lexical) },
|
|
190
|
+
"xs:integer" => ->(lexical) { integer?(lexical) },
|
|
191
|
+
"xs:nonPositiveInteger" => signed(-Float::INFINITY..0),
|
|
192
|
+
"xs:negativeInteger" => signed(-Float::INFINITY...0),
|
|
193
|
+
"xs:long" => signed(-(2**63)..((2**63) - 1)),
|
|
194
|
+
"xs:int" => signed(-(2**31)..((2**31) - 1)),
|
|
195
|
+
"xs:short" => signed(-32_768..32_767),
|
|
196
|
+
"xs:byte" => signed(-128..127),
|
|
197
|
+
"xs:nonNegativeInteger" => signed(0..Float::INFINITY),
|
|
198
|
+
"xs:positiveInteger" => signed(1..Float::INFINITY),
|
|
199
|
+
"xs:unsignedLong" => signed(0..((2**64) - 1)),
|
|
200
|
+
"xs:unsignedInt" => signed(0..((2**32) - 1)),
|
|
201
|
+
"xs:unsignedShort" => signed(0..65_535),
|
|
202
|
+
"xs:unsignedByte" => signed(0..255),
|
|
203
|
+
"xs:duration" => ->(lexical) { duration?(lexical) },
|
|
204
|
+
"xs:dayTimeDuration" => ->(lexical) { day_time_duration?(lexical) },
|
|
205
|
+
"xs:yearMonthDuration" => ->(lexical) { year_month_duration?(lexical) },
|
|
206
|
+
"xs:dateTime" => ->(lexical) do
|
|
207
|
+
with_date_fields(lexical) do |y, m, d|
|
|
208
|
+
DATE_TIME_LEXICAL.match?(lexical) && calendar_date?(y, m, d)
|
|
209
|
+
end
|
|
210
|
+
end,
|
|
211
|
+
"xs:dateTimeStamp" => ->(lexical) do
|
|
212
|
+
with_date_fields(lexical) do |y, m, d|
|
|
213
|
+
DATE_TIME_STAMP_LEXICAL.match?(lexical) && calendar_date?(y, m, d)
|
|
214
|
+
end
|
|
215
|
+
end,
|
|
216
|
+
"xs:time" => ->(lexical) { TIME_LEXICAL.match?(lexical) },
|
|
217
|
+
"xs:date" => ->(lexical) do
|
|
218
|
+
with_date_fields(lexical) do |y, m, d|
|
|
219
|
+
DATE_LEXICAL.match?(lexical) && calendar_date?(y, m, d)
|
|
220
|
+
end
|
|
221
|
+
end,
|
|
222
|
+
"xs:gYear" => ->(lexical) { G_YEAR_LEXICAL.match?(lexical) },
|
|
223
|
+
"xs:gYearMonth" => ->(lexical) do
|
|
224
|
+
G_YEAR_MONTH_LEXICAL.match?(lexical) &&
|
|
225
|
+
with_date_fields("#{lexical.sub(/(?:Z|[+-][0-9]{2}:[0-9]{2})\z/, '')}-01") do |y, m, d|
|
|
226
|
+
calendar_date?(y, m, d)
|
|
227
|
+
end
|
|
228
|
+
end,
|
|
229
|
+
"xs:gMonth" => ->(lexical) { G_MONTH_LEXICAL.match?(lexical) },
|
|
230
|
+
"xs:gDay" => ->(lexical) { G_DAY_LEXICAL.match?(lexical) },
|
|
231
|
+
"xs:gMonthDay" => ->(lexical) do
|
|
232
|
+
match = /\A--([0-9]{2})-([0-9]{2})/.match(lexical)
|
|
233
|
+
next false unless match
|
|
234
|
+
|
|
235
|
+
month = Integer(match[1])
|
|
236
|
+
limit = MONTH_DAYS[month - 1]
|
|
237
|
+
limit += 1 if month == 2
|
|
238
|
+
G_MONTH_DAY_LEXICAL.match?(lexical) &&
|
|
239
|
+
Integer(match[2]).between?(1, limit)
|
|
240
|
+
end,
|
|
241
|
+
"xs:hexBinary" => ->(lexical) { HEX_LEXICAL.match?(lexical) },
|
|
242
|
+
"xs:base64Binary" => ->(lexical) { BASE64_LEXICAL.match?(lexical.delete(" \t\r\n")) },
|
|
243
|
+
"xs:anyURI" => ->(_lexical) { true },
|
|
244
|
+
"xs:QName" => ->(lexical) { qname?(collapsed(lexical)) },
|
|
245
|
+
"xs:NOTATION" => ->(lexical) { qname?(collapsed(lexical)) },
|
|
246
|
+
}.freeze
|
|
247
|
+
|
|
248
|
+
private_constant :CHECKERS
|
|
249
|
+
end
|
|
250
|
+
end
|
|
251
|
+
end
|
data/lib/moxml/xsd.rb
CHANGED
|
@@ -33,6 +33,7 @@ module Moxml
|
|
|
33
33
|
SUPPORTED_ADAPTERS = %i[leptris nokogiri].freeze
|
|
34
34
|
|
|
35
35
|
autoload :SchemaRules, "moxml/xsd/schema_rules"
|
|
36
|
+
autoload :BuiltinLexical, "moxml/xsd/builtin_lexical"
|
|
36
37
|
|
|
37
38
|
# One validation finding, harmonized across adapters (#362.2):
|
|
38
39
|
# +message+ always present; +line+ / +column+ when the engine
|
|
@@ -76,12 +77,12 @@ module Moxml
|
|
|
76
77
|
adapter.const_get(:XSD_SUPPORTED)
|
|
77
78
|
end
|
|
78
79
|
|
|
79
|
-
# Lexical validation against the built-in type
|
|
80
|
-
# "xs:NAME" reference spelling).
|
|
81
|
-
#
|
|
82
|
-
#
|
|
83
|
-
def builtin_valid?(
|
|
84
|
-
|
|
80
|
+
# Lexical validation against the facade-owned built-in type
|
|
81
|
+
# table (the "xs:NAME" reference spelling). One committed
|
|
82
|
+
# answer on every adapter (#482); raises ArgumentError when
|
|
83
|
+
# the name is not in the table.
|
|
84
|
+
def builtin_valid?(_context, builtin, lexical)
|
|
85
|
+
BuiltinLexical.valid?(builtin, lexical)
|
|
85
86
|
end
|
|
86
87
|
end
|
|
87
88
|
|
|
@@ -149,6 +149,34 @@ RSpec.describe "XPath idioms across adapters" do
|
|
|
149
149
|
expect(doc.xpath("//c").map(&:name)).to eq(["c"])
|
|
150
150
|
expect(doc.at_xpath("count(//c)")).to eq(1.0)
|
|
151
151
|
end
|
|
152
|
+
|
|
153
|
+
# Attribute wrappers participate in node equality and document
|
|
154
|
+
# ordering: standoc's svg cleanup compares and sorts result
|
|
155
|
+
# sets that carry parent-wired attributes. Equality across an
|
|
156
|
+
# attribute and a node must answer (false), never raise, and
|
|
157
|
+
# two wrappers over one attribute are the same node.
|
|
158
|
+
it "compares attribute identity and orders attribute results" do
|
|
159
|
+
doc = ctx.parse("<r><svg id='a'><p>x</p></svg><svg id='b'/></r>")
|
|
160
|
+
|
|
161
|
+
attr_a = doc.xpath("//*[@id='a']").first.attributes.to_a.first
|
|
162
|
+
attr_b = doc.xpath("//*[@id='b']").first.attributes.to_a.first
|
|
163
|
+
expect(doc.root.children.to_a.first == attr_a).to be(false)
|
|
164
|
+
expect(
|
|
165
|
+
attr_a == doc.xpath("//*[@id='a']").first.attributes.to_a.first,
|
|
166
|
+
).to be(true)
|
|
167
|
+
expect(attr_a == attr_b).to be(false)
|
|
168
|
+
|
|
169
|
+
expect(doc.xpath("//@id").map(&:value)).to eq(%w[a b])
|
|
170
|
+
|
|
171
|
+
# XPath document order: an attribute sorts after its owner
|
|
172
|
+
# element and before the element's children.
|
|
173
|
+
svg = doc.xpath("//*[@id='a']").first
|
|
174
|
+
expect(
|
|
175
|
+
svg.xpath("./@* | ./p").map do |node|
|
|
176
|
+
node.is_a?(Moxml::Attribute) ? "@#{node.name}" : node.name
|
|
177
|
+
end,
|
|
178
|
+
).to eq(%w[@id p])
|
|
179
|
+
end
|
|
152
180
|
end
|
|
153
181
|
end
|
|
154
182
|
end
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Processing model §Validation (normative): pinned here.
|
|
4
|
+
|
|
5
|
+
require "spec_helper"
|
|
6
|
+
|
|
7
|
+
RSpec.describe Moxml::XSD do
|
|
8
|
+
let(:ctx) { Moxml.new(:leptris) }
|
|
9
|
+
|
|
10
|
+
def valid_lexical?(type, lexical)
|
|
11
|
+
described_class.builtin_valid?(ctx, type, lexical)
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
describe ".builtin_valid?" do
|
|
15
|
+
it "answers the facade-owned table identically without an engine" do
|
|
16
|
+
expect(valid_lexical?("xs:integer", "3")).to be(true)
|
|
17
|
+
expect(valid_lexical?("xs:integer", "x")).to be(false)
|
|
18
|
+
expect do
|
|
19
|
+
described_class.builtin_valid?(ctx, "xs:nope", "3")
|
|
20
|
+
end.to raise_error(ArgumentError)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
it "applies whiteSpace=collapse before token-family checks" do
|
|
24
|
+
expect(valid_lexical?("xs:token", "a b")).to be(true)
|
|
25
|
+
expect(valid_lexical?("xs:ID", " id ")).to be(true)
|
|
26
|
+
expect(valid_lexical?("xs:NMTOKEN", "\tx ")).to be(true)
|
|
27
|
+
expect(valid_lexical?("xs:ID", "1d")).to be(false)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
it "keeps unbounded integers unbounded" do
|
|
31
|
+
big = "1" * 29
|
|
32
|
+
expect(valid_lexical?("xs:integer", big)).to be(true)
|
|
33
|
+
expect(valid_lexical?("xs:positiveInteger", big)).to be(true)
|
|
34
|
+
expect(valid_lexical?("xs:nonNegativeInteger", big)).to be(true)
|
|
35
|
+
expect(valid_lexical?("xs:negativeInteger", "-1#{'0' * 20}")).to be(true)
|
|
36
|
+
expect(valid_lexical?("xs:nonPositiveInteger", "0")).to be(true)
|
|
37
|
+
expect(valid_lexical?("xs:nonPositiveInteger", "-0")).to be(true)
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
it "stops bounded integers at their own bounds" do
|
|
41
|
+
expect(valid_lexical?("xs:long", "9223372036854775807")).to be(true)
|
|
42
|
+
expect(valid_lexical?("xs:long", "9223372036854775808")).to be(false)
|
|
43
|
+
expect(valid_lexical?("xs:unsignedLong", "18446744073709551615")).to be(true)
|
|
44
|
+
expect(valid_lexical?("xs:unsignedLong", "18446744073709551616")).to be(false)
|
|
45
|
+
expect(valid_lexical?("xs:byte", "128")).to be(false)
|
|
46
|
+
expect(valid_lexical?("xs:unsignedByte", "256")).to be(false)
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
it "admits the float spellings the datatype spec defines" do
|
|
50
|
+
expect(valid_lexical?("xs:float", "+INF")).to be(true)
|
|
51
|
+
expect(valid_lexical?("xs:double", "-INF")).to be(true)
|
|
52
|
+
expect(valid_lexical?("xs:float", "NaN")).to be(true)
|
|
53
|
+
expect(valid_lexical?("xs:float", "1.5E-3")).to be(true)
|
|
54
|
+
expect(valid_lexical?("xs:decimal", "5.")).to be(true)
|
|
55
|
+
expect(valid_lexical?("xs:decimal", ".5")).to be(true)
|
|
56
|
+
expect(valid_lexical?("xs:decimal", "1e3")).to be(false)
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
it "checks date lexicals against the Gregorian rules" do
|
|
60
|
+
expect(valid_lexical?("xs:date", "2024-02-29")).to be(true)
|
|
61
|
+
expect(valid_lexical?("xs:date", "1999-02-29")).to be(false)
|
|
62
|
+
expect(valid_lexical?("xs:date", "2024-04-31")).to be(false)
|
|
63
|
+
expect(valid_lexical?("xs:date", "2024-13-01")).to be(false)
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
it "checks dateTime and time lexicals" do
|
|
67
|
+
expect(valid_lexical?("xs:dateTime", "2024-02-29T23:59:59Z")).to be(true)
|
|
68
|
+
expect(valid_lexical?("xs:dateTime", "1999-02-29T00:00:00")).to be(false)
|
|
69
|
+
expect(valid_lexical?("xs:time", "23:59:59.999+05:30")).to be(true)
|
|
70
|
+
expect(valid_lexical?("xs:time", "24:00:00")).to be(false)
|
|
71
|
+
expect(valid_lexical?("xs:time", "25:20:00")).to be(false)
|
|
72
|
+
expect(valid_lexical?("xs:time", "13:20:60")).to be(false)
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
it "checks the gregorian-day lexicals" do
|
|
76
|
+
expect(valid_lexical?("xs:gYearMonth", "2024-02Z")).to be(true)
|
|
77
|
+
expect(valid_lexical?("xs:gYearMonth", "2024-13")).to be(false)
|
|
78
|
+
expect(valid_lexical?("xs:gMonthDay", "--02-29")).to be(true)
|
|
79
|
+
expect(valid_lexical?("xs:gMonthDay", "--02-30")).to be(false)
|
|
80
|
+
expect(valid_lexical?("xs:gDay", "---31")).to be(true)
|
|
81
|
+
expect(valid_lexical?("xs:gDay", "---32")).to be(false)
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
it "enforces the duration component rules" do
|
|
85
|
+
expect(valid_lexical?("xs:duration", "P1Y2M3DT4H5M6.5S")).to be(true)
|
|
86
|
+
expect(valid_lexical?("xs:duration", "PT0S")).to be(true)
|
|
87
|
+
expect(valid_lexical?("xs:duration", "-P1D")).to be(true)
|
|
88
|
+
expect(valid_lexical?("xs:duration", "P")).to be(false)
|
|
89
|
+
expect(valid_lexical?("xs:duration", "PT")).to be(false)
|
|
90
|
+
expect(valid_lexical?("xs:duration", "P1Y2MT")).to be(false)
|
|
91
|
+
expect(valid_lexical?("xs:duration", "P200.5Y")).to be(false)
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
it "restricts the duration subtypes to their components" do
|
|
95
|
+
expect(valid_lexical?("xs:dayTimeDuration", "P1DT2H")).to be(true)
|
|
96
|
+
expect(valid_lexical?("xs:dayTimeDuration", "P1Y")).to be(false)
|
|
97
|
+
expect(valid_lexical?("xs:yearMonthDuration", "P1Y2M")).to be(true)
|
|
98
|
+
expect(valid_lexical?("xs:yearMonthDuration", "P1D")).to be(false)
|
|
99
|
+
expect(valid_lexical?("xs:yearMonthDuration", "P1YT1H")).to be(false)
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
it "checks binary lexical spaces" do
|
|
103
|
+
expect(valid_lexical?("xs:base64Binary", "aGVsbG8=")).to be(true)
|
|
104
|
+
expect(valid_lexical?("xs:base64Binary", "abcd")).to be(true)
|
|
105
|
+
expect(valid_lexical?("xs:base64Binary", "abcde")).to be(false)
|
|
106
|
+
expect(valid_lexical?("xs:base64Binary", "a b")).to be(false)
|
|
107
|
+
expect(valid_lexical?("xs:hexBinary", "0A")).to be(true)
|
|
108
|
+
expect(valid_lexical?("xs:hexBinary", "0AB")).to be(false)
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
it "rejects the reserved xmlns prefix in QName-family lexicals" do
|
|
112
|
+
expect(valid_lexical?("xs:QName", "foo")).to be(true)
|
|
113
|
+
expect(valid_lexical?("xs:QName", "ns:foo")).to be(true)
|
|
114
|
+
expect(valid_lexical?("xs:QName", "xmlns:xsi")).to be(false)
|
|
115
|
+
expect(valid_lexical?("xs:QName", ":foo")).to be(false)
|
|
116
|
+
expect(valid_lexical?("xs:QName", "a:b:c")).to be(false)
|
|
117
|
+
expect(valid_lexical?("xs:NOTATION", "foo")).to be(true)
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
it "counts list items, not characters" do
|
|
121
|
+
expect(valid_lexical?("xs:IDREFS", "a b")).to be(true)
|
|
122
|
+
expect(valid_lexical?("xs:IDREFS", "a")).to be(true)
|
|
123
|
+
expect(valid_lexical?("xs:IDREFS", "")).to be(false)
|
|
124
|
+
expect(valid_lexical?("xs:NMTOKENS", "a.b-c:d e")).to be(true)
|
|
125
|
+
expect(valid_lexical?("xs:ENTITIES", "a 1")).to be(false)
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
it "keeps the open lexical spaces open" do
|
|
129
|
+
expect(valid_lexical?("xs:string", "anything <>&")).to be(true)
|
|
130
|
+
expect(valid_lexical?("xs:anyURI", "http://example.com/a b")).to be(true)
|
|
131
|
+
expect(valid_lexical?("xs:boolean", "true")).to be(true)
|
|
132
|
+
expect(valid_lexical?("xs:boolean", "TRUE")).to be(false)
|
|
133
|
+
expect(valid_lexical?("xs:language", "en-US")).to be(true)
|
|
134
|
+
expect(valid_lexical?("xs:language", "en_US")).to be(false)
|
|
135
|
+
expect(valid_lexical?("xs:Name", "a:b")).to be(true)
|
|
136
|
+
expect(valid_lexical?("xs:NCName", "a:b")).to be(false)
|
|
137
|
+
end
|
|
138
|
+
end
|
|
139
|
+
end
|
|
@@ -97,6 +97,10 @@ module ModelCoverage
|
|
|
97
97
|
section: "Validation",
|
|
98
98
|
pin: "spec/moxml/xsd_regex_spec.rb",
|
|
99
99
|
},
|
|
100
|
+
"validation builtin lexical table" => {
|
|
101
|
+
section: "Validation",
|
|
102
|
+
pin: "spec/moxml/xsd_builtin_lexical_spec.rb",
|
|
103
|
+
},
|
|
100
104
|
"validation substitution dismissal" => {
|
|
101
105
|
section: "Validation",
|
|
102
106
|
pin: "spec/moxml/xsd_substitution_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.168
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Ribose Inc.
|
|
@@ -318,6 +318,7 @@ files:
|
|
|
318
318
|
- lib/moxml/xpath/ruby/generator.rb
|
|
319
319
|
- lib/moxml/xpath/ruby/node.rb
|
|
320
320
|
- lib/moxml/xsd.rb
|
|
321
|
+
- lib/moxml/xsd/builtin_lexical.rb
|
|
321
322
|
- lib/moxml/xsd/schema_rules.rb
|
|
322
323
|
- lib/moxml/xslt.rb
|
|
323
324
|
- lib/moxml/xslt/stylesheet.rb
|
|
@@ -631,6 +632,7 @@ files:
|
|
|
631
632
|
- spec/moxml/xpath/union_spec.rb
|
|
632
633
|
- spec/moxml/xpath_capabilities_spec.rb
|
|
633
634
|
- spec/moxml/xpath_spec.rb
|
|
635
|
+
- spec/moxml/xsd_builtin_lexical_spec.rb
|
|
634
636
|
- spec/moxml/xsd_engine_meta_spec.rb
|
|
635
637
|
- spec/moxml/xsd_error_contract_spec.rb
|
|
636
638
|
- spec/moxml/xsd_handle_mismatch_spec.rb
|