moxml 0.5.166 → 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/Gemfile +1 -1
- data/benchmark/README.md +9 -9
- data/benchmark/scoreboard.json +32 -32
- data/benchmark/scoreboard.rb +11 -0
- data/docs/_pages/divergences.adoc +10 -0
- data/docs/_pages/model-coverage.adoc +2 -0
- data/docs/_pages/performance.adoc +10 -10
- data/docs/_references/processing-model.adoc +17 -4
- data/lib/moxml/adapter/base.rb +0 -5
- data/lib/moxml/adapter/leptris.rb +121 -13
- data/lib/moxml/config.rb +6 -1
- data/lib/moxml/document.rb +17 -0
- data/lib/moxml/node_set.rb +6 -1
- 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 +46 -11
- data/spec/consistency/xpath_idioms_spec.rb +41 -0
- data/spec/moxml/adapter/leptris_crossdoc_replace_spec.rb +37 -0
- data/spec/moxml/adapter/leptris_native_parent_axis_spec.rb +46 -0
- data/spec/moxml/xsd_builtin_lexical_spec.rb +139 -0
- data/spec/moxml/xsd_engine_meta_spec.rb +66 -0
- data/spec/support/model_coverage.rb +8 -0
- metadata +7 -2
|
@@ -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
|
|
|
@@ -177,15 +178,49 @@ module Moxml
|
|
|
177
178
|
@context.config.adapter.xsd_declarations(@handle)
|
|
178
179
|
end
|
|
179
180
|
|
|
180
|
-
# The effective schema version
|
|
181
|
-
#
|
|
182
|
-
#
|
|
183
|
-
#
|
|
184
|
-
#
|
|
181
|
+
# The effective schema version (#460 item 2): "1.1" or
|
|
182
|
+
# "1.0". The engine's answer (root @version) can only
|
|
183
|
+
# UPGRADE to 1.1 — it never downgrades the source-derived
|
|
184
|
+
# answer, because vc:minVersion="1.1" selects 1.1 semantics
|
|
185
|
+
# even when @version is absent, and @version="1.1" alongside
|
|
186
|
+
# vc:minVersion="1.0" is an authoring contradiction the
|
|
187
|
+
# source annotation wins.
|
|
185
188
|
def version
|
|
189
|
+
adapter = @context.config.adapter
|
|
190
|
+
engine = if adapter.respond_to?(:xsd_version)
|
|
191
|
+
adapter.xsd_version(@handle)
|
|
192
|
+
end
|
|
193
|
+
return "1.1" if engine == "1.1"
|
|
194
|
+
|
|
186
195
|
@rules_index ? @rules_index.version : "1.0"
|
|
187
196
|
end
|
|
188
197
|
|
|
198
|
+
# One row per engine finding with the structured fields the
|
|
199
|
+
# #1843 report carries (moxml#460): kind, component, line,
|
|
200
|
+
# column, message. Nil when the binding predates the face —
|
|
201
|
+
# callers keep using #validate/#errors for the flat shape.
|
|
202
|
+
EngineFinding = Struct.new(:kind, :component, :line, :column,
|
|
203
|
+
:message)
|
|
204
|
+
|
|
205
|
+
def structured_errors(node_or_xml)
|
|
206
|
+
check_released!
|
|
207
|
+
doc = case node_or_xml
|
|
208
|
+
when Moxml::Document then node_or_xml
|
|
209
|
+
when Moxml::Node then node_or_xml.document
|
|
210
|
+
else @context.parse(node_or_xml.to_s)
|
|
211
|
+
end
|
|
212
|
+
adapter = @context.config.adapter
|
|
213
|
+
rows = if adapter.respond_to?(:xsd_structured_errors)
|
|
214
|
+
adapter.xsd_structured_errors(@handle, doc.native)
|
|
215
|
+
end
|
|
216
|
+
return nil unless rows
|
|
217
|
+
|
|
218
|
+
rows.map do |r|
|
|
219
|
+
EngineFinding.new(r.kind, r.component, r.line, r.column,
|
|
220
|
+
r.message)
|
|
221
|
+
end
|
|
222
|
+
end
|
|
223
|
+
|
|
189
224
|
# Deterministic drop (#362.3): true when the adapter freed
|
|
190
225
|
# the engine-side schema NOW; false when the adapter can only
|
|
191
226
|
# wait for GC (documented per adapter). After a true release
|
|
@@ -122,6 +122,19 @@ RSpec.describe "XPath idioms across adapters" do
|
|
|
122
122
|
expect(doc.at_xpath("count(//a)")).to eq(1.0)
|
|
123
123
|
end
|
|
124
124
|
|
|
125
|
+
# The parent of an attribute node is its OWNER element
|
|
126
|
+
# (XPath 1.0 section 2.2) — the load-bearing arm of standoc's
|
|
127
|
+
# svg id disambiguation (#474).
|
|
128
|
+
it "answers the parent-of-attribute step with the owner element" do
|
|
129
|
+
doc = ctx.parse("<r><svg id='s'><use id='u'/></svg></r>")
|
|
130
|
+
svg = doc.xpath("//*[local-name()='svg']").first
|
|
131
|
+
expect(svg.xpath("./@id/..").map(&:name)).to eq(["svg"])
|
|
132
|
+
expect(svg.xpath("./@id/parent::*").map(&:name)).to eq(["svg"])
|
|
133
|
+
expect(
|
|
134
|
+
svg.xpath(".//*[@id] | ./@id/..").map(&:name).sort,
|
|
135
|
+
).to eq(%w[svg use])
|
|
136
|
+
end
|
|
137
|
+
|
|
125
138
|
# The live-tree contract: a sibling inserted after an XPath
|
|
126
139
|
# evaluation must be visible to at_xpath and xpath. leptris's
|
|
127
140
|
# node-set lane caches a document index that sibling insertions
|
|
@@ -136,6 +149,34 @@ RSpec.describe "XPath idioms across adapters" do
|
|
|
136
149
|
expect(doc.xpath("//c").map(&:name)).to eq(["c"])
|
|
137
150
|
expect(doc.at_xpath("count(//c)")).to eq(1.0)
|
|
138
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
|
|
139
180
|
end
|
|
140
181
|
end
|
|
141
182
|
end
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "spec_helper"
|
|
4
|
+
|
|
5
|
+
# leptris#1866: a cross-document replace used to hand back the
|
|
6
|
+
# SCRATCH document's wrapper — the adapter's insert deep-copies the
|
|
7
|
+
# subtree into the target, so the returned wrapper's native pointer
|
|
8
|
+
# belonged to the scratch arena. Callers stored it (svg cleanup
|
|
9
|
+
# keeps `new_svg` for id bookkeeping), the scratch was collected,
|
|
10
|
+
# and every later read of the stored wrapper walked freed memory:
|
|
11
|
+
# serializer segfaults and `<use ="">` empty-attribute names in
|
|
12
|
+
# the output.
|
|
13
|
+
RSpec.describe "leptris cross-document replace survival" do
|
|
14
|
+
let(:context) { Moxml.new(:leptris) }
|
|
15
|
+
|
|
16
|
+
it "keeps the installed wrapper valid after the scratch is collected" do
|
|
17
|
+
host = context.parse(
|
|
18
|
+
'<root xmlns:xlink="http://www.w3.org/1999/xlink">' \
|
|
19
|
+
'<svg xmlns="http://www.w3.org/2000/svg" id="s1">' \
|
|
20
|
+
'<use xlink:href="#a"/><use href="#b"/></svg></root>',
|
|
21
|
+
)
|
|
22
|
+
scratch = context.parse(
|
|
23
|
+
"<svg xmlns=\"http://www.w3.org/2000/svg\" id=\"fresh\">" \
|
|
24
|
+
"<use xlink:href=\"#fx\"/><use href=\"#b\"/></svg>",
|
|
25
|
+
)
|
|
26
|
+
olds = host.root.children.select { |c| c.name == "svg" }
|
|
27
|
+
olds[0].replace(scratch.root)
|
|
28
|
+
GC.start(full_mark: true, immediate_sweep: true)
|
|
29
|
+
out = host.root.to_xml
|
|
30
|
+
expect(out).not_to include("<use =")
|
|
31
|
+
# the installed copy's OWN content survives the scratch GC
|
|
32
|
+
expect(out).to include("fx").and include("#b")
|
|
33
|
+
# NOTE: pre-existing divergence D-22 (materialized-wrapper
|
|
34
|
+
# lane) — the UNREPLACED siblings of a cross-doc replace are
|
|
35
|
+
# dropped; reproduced on the stock adapter before this fix.
|
|
36
|
+
end
|
|
37
|
+
end
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "spec_helper"
|
|
4
|
+
|
|
5
|
+
# The parent step answers the OWNER element natively on bindings
|
|
6
|
+
# serving the fixed engine (1.9.385: leptris#1869 parent-of-attribute
|
|
7
|
+
# names the owner; the cross-doc adopt keeps its attribute chain) —
|
|
8
|
+
# moxml#474: the reverse-axis gate used to route every parent-axis
|
|
9
|
+
# expression to the Ruby engine, so the fast consumers (standoc's
|
|
10
|
+
# svg id cleanup runs `.//*[@id] | ./@id/..` per styled svg) paid
|
|
11
|
+
# the interpreter. Other reverse axes keep the Ruby lane (their
|
|
12
|
+
# native results are reverse-ordered; document order is the
|
|
13
|
+
# model's contract).
|
|
14
|
+
RSpec.describe "leptris native parent-axis lane" do
|
|
15
|
+
let(:ctx) { Moxml.new(:leptris) }
|
|
16
|
+
let(:doc) do
|
|
17
|
+
ctx.parse('<root><svg id="s"><use id="u"/></svg></root>')
|
|
18
|
+
end
|
|
19
|
+
let(:svg) { doc.xpath("//*[local-name()='svg']").first }
|
|
20
|
+
|
|
21
|
+
it "routes parent-of-attribute expressions to the native lane" do
|
|
22
|
+
native = svg.adapter.native_xpath(
|
|
23
|
+
svg.native, "./@id/..", {}
|
|
24
|
+
)
|
|
25
|
+
expect(native).not_to be_nil
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
it "answers the owner element through the facade" do
|
|
29
|
+
expect(svg.xpath("./@id/..").map(&:name)).to eq(["svg"])
|
|
30
|
+
expect(svg.xpath("./@id/..").first).to equal(svg)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
it "serves the two-arm union natively" do
|
|
34
|
+
expect(
|
|
35
|
+
svg.xpath(".//*[@id] | ./@id/..").map(&:name).sort,
|
|
36
|
+
).to eq(%w[svg use])
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
it "keeps non-parent reverse axes on the Ruby lane" do
|
|
40
|
+
doc2 = ctx.parse('<r><e a="1" b="2" c="3"/></r>')
|
|
41
|
+
e = doc2.xpath("//e").first
|
|
42
|
+
expect(e.xpath("ancestor::*").map(&:name)).to eq(["r"])
|
|
43
|
+
native = e.adapter.native_xpath(e.native, "ancestor::*", {})
|
|
44
|
+
expect(native).to be_nil
|
|
45
|
+
end
|
|
46
|
+
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
|