moxml 0.5.167 → 0.5.169

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: cfb972cf79eaf0a41c6764ceed1ce1739f6c6e639bf753120418bbf8ef5c3082
4
- data.tar.gz: fb1759136a3091e0e998708489ef4849c5cea4e2df6009a9a319cf26ed8540bf
3
+ metadata.gz: c6d162a122e231644310d60b9f27eef7f3fa89728a6c135a9eb855d56952af5d
4
+ data.tar.gz: 7c3c92e7ffe80f35484e364d75808d1a74f4c62db7c1e2222b0d1d2921d3ccac
5
5
  SHA512:
6
- metadata.gz: 0c0da22a0e0d37ec5db5b3e76845bc042583c005964cfa7499be07227618125b65e04b8d8ed7b11c7754c8403a0d6ceecbb9dffa07cf683bed7e037c72b125c4
7
- data.tar.gz: ef0ff906785959fe2f93719df3d3deb49e8b27f6097ecd836d8ab296200cb8b2ef24aabb60ef3924b5fbc54c6da517c44bd18d5bb213d8b0042546b1d65cfe7a
6
+ metadata.gz: 4872ca2eb4456ee02a511196840379610f6c66b590d64ba1a4715b6107c33a912056bd6fe48055d1270c274e9aec62193f88778180f57ebe32da3f27961f871c
7
+ data.tar.gz: 4811ea416c2ebd77ff2cbedf4f640a755424e5ae67d3590c63220fd182eff8ad2fb0c58335ea82f90bb8b9c5bca9f408cb4989256a9c74bb6af5b568ecfc457b
@@ -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). `validate` returns accumulated `ValidationError` entries —
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` /
@@ -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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Moxml
4
- VERSION = "0.5.167"
4
+ VERSION = "0.5.169"
5
5
  end
@@ -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,261 @@
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
+ # The answer is total: a lexical whose date part does not
148
+ # parse is invalid (false), never nil — consumers pin strict
149
+ # booleans on this face (#485 follow-up).
150
+ def with_date_fields(lexical)
151
+ year, month, day = date_fields(lexical)
152
+ return false unless year
153
+
154
+ yield(year, month, day)
155
+ end
156
+
157
+ # The supported "xs:NAME" spellings (the CHECKERS keys).
158
+ def names
159
+ CHECKERS.keys
160
+ end
161
+
162
+ def integer?(lexical, range: nil)
163
+ return false unless INTEGER_LEXICAL.match?(lexical)
164
+
165
+ range.nil? || range.cover?(Integer(lexical))
166
+ end
167
+
168
+ def signed(range)
169
+ ->(lexical) { integer?(lexical, range: range) }
170
+ end
171
+
172
+ private_class_method :collapsed, :ncname?, :qname?, :list?,
173
+ :duration?, :day_time_duration?,
174
+ :year_month_duration?, :leap_year?,
175
+ :calendar_date?, :date_fields,
176
+ :with_date_fields, :integer?, :signed
177
+
178
+ CHECKERS = {
179
+ "xs:anySimpleType" => ->(_lexical) { true },
180
+ "xs:anyAtomicType" => ->(_lexical) { true },
181
+ "xs:anyType" => ->(_lexical) { true },
182
+ "xs:string" => ->(_lexical) { true },
183
+ "xs:normalizedString" => ->(_lexical) { true },
184
+ "xs:token" => ->(_lexical) { true },
185
+ "xs:language" => ->(lexical) { LANGUAGE_LEXICAL.match?(collapsed(lexical)) },
186
+ "xs:Name" => ->(lexical) { NAME.match?(collapsed(lexical)) },
187
+ "xs:NCName" => ->(lexical) { ncname?(collapsed(lexical)) },
188
+ "xs:ID" => ->(lexical) { ncname?(collapsed(lexical)) },
189
+ "xs:IDREF" => ->(lexical) { ncname?(collapsed(lexical)) },
190
+ "xs:ENTITY" => ->(lexical) { ncname?(collapsed(lexical)) },
191
+ "xs:NMTOKEN" => ->(lexical) { NMTOKEN.match?(collapsed(lexical)) },
192
+ "xs:IDREFS" => ->(lexical) { list?(lexical) { |item| ncname?(item) } },
193
+ "xs:ENTITIES" => ->(lexical) { list?(lexical) { |item| ncname?(item) } },
194
+ "xs:NMTOKENS" => ->(lexical) { list?(lexical) { |item| NMTOKEN.match?(item) } },
195
+ "xs:boolean" => ->(lexical) { BOOLEAN_LEXICAL.match?(lexical) },
196
+ "xs:decimal" => ->(lexical) { DECIMAL_LEXICAL.match?(lexical) },
197
+ "xs:precisionDecimal" => ->(lexical) { FLOAT_LEXICAL.match?(lexical) },
198
+ "xs:float" => ->(lexical) { FLOAT_LEXICAL.match?(lexical) },
199
+ "xs:double" => ->(lexical) { FLOAT_LEXICAL.match?(lexical) },
200
+ "xs:integer" => ->(lexical) { integer?(lexical) },
201
+ "xs:nonPositiveInteger" => signed(-Float::INFINITY..0),
202
+ "xs:negativeInteger" => signed(-Float::INFINITY...0),
203
+ "xs:long" => signed(-(2**63)..((2**63) - 1)),
204
+ "xs:int" => signed(-(2**31)..((2**31) - 1)),
205
+ "xs:short" => signed(-32_768..32_767),
206
+ "xs:byte" => signed(-128..127),
207
+ "xs:nonNegativeInteger" => signed(0..Float::INFINITY),
208
+ "xs:positiveInteger" => signed(1..Float::INFINITY),
209
+ "xs:unsignedLong" => signed(0..((2**64) - 1)),
210
+ "xs:unsignedInt" => signed(0..((2**32) - 1)),
211
+ "xs:unsignedShort" => signed(0..65_535),
212
+ "xs:unsignedByte" => signed(0..255),
213
+ "xs:duration" => ->(lexical) { duration?(lexical) },
214
+ "xs:dayTimeDuration" => ->(lexical) { day_time_duration?(lexical) },
215
+ "xs:yearMonthDuration" => ->(lexical) { year_month_duration?(lexical) },
216
+ "xs:dateTime" => ->(lexical) do
217
+ with_date_fields(lexical) do |y, m, d|
218
+ DATE_TIME_LEXICAL.match?(lexical) && calendar_date?(y, m, d)
219
+ end
220
+ end,
221
+ "xs:dateTimeStamp" => ->(lexical) do
222
+ with_date_fields(lexical) do |y, m, d|
223
+ DATE_TIME_STAMP_LEXICAL.match?(lexical) && calendar_date?(y, m, d)
224
+ end
225
+ end,
226
+ "xs:time" => ->(lexical) { TIME_LEXICAL.match?(lexical) },
227
+ "xs:date" => ->(lexical) do
228
+ with_date_fields(lexical) do |y, m, d|
229
+ DATE_LEXICAL.match?(lexical) && calendar_date?(y, m, d)
230
+ end
231
+ end,
232
+ "xs:gYear" => ->(lexical) { G_YEAR_LEXICAL.match?(lexical) },
233
+ "xs:gYearMonth" => ->(lexical) do
234
+ G_YEAR_MONTH_LEXICAL.match?(lexical) &&
235
+ with_date_fields("#{lexical.sub(/(?:Z|[+-][0-9]{2}:[0-9]{2})\z/, '')}-01") do |y, m, d|
236
+ calendar_date?(y, m, d)
237
+ end
238
+ end,
239
+ "xs:gMonth" => ->(lexical) { G_MONTH_LEXICAL.match?(lexical) },
240
+ "xs:gDay" => ->(lexical) { G_DAY_LEXICAL.match?(lexical) },
241
+ "xs:gMonthDay" => ->(lexical) do
242
+ match = /\A--([0-9]{2})-([0-9]{2})/.match(lexical)
243
+ next false unless match
244
+
245
+ month = Integer(match[1])
246
+ limit = MONTH_DAYS[month - 1]
247
+ limit += 1 if month == 2
248
+ G_MONTH_DAY_LEXICAL.match?(lexical) &&
249
+ Integer(match[2]).between?(1, limit)
250
+ end,
251
+ "xs:hexBinary" => ->(lexical) { HEX_LEXICAL.match?(lexical) },
252
+ "xs:base64Binary" => ->(lexical) { BASE64_LEXICAL.match?(lexical.delete(" \t\r\n")) },
253
+ "xs:anyURI" => ->(_lexical) { true },
254
+ "xs:QName" => ->(lexical) { qname?(collapsed(lexical)) },
255
+ "xs:NOTATION" => ->(lexical) { qname?(collapsed(lexical)) },
256
+ }.freeze
257
+
258
+ private_constant :CHECKERS
259
+ end
260
+ end
261
+ 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 table (the
80
- # "xs:NAME" reference spelling). Raises ArgumentError when the
81
- # name is not in the table; NotImplementedError on adapters
82
- # without an engine.
83
- def builtin_valid?(context, builtin, lexical)
84
- context.config.adapter.xsd_builtin_valid?(builtin, lexical)
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,163 @@
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
+
139
+ # lutaml-xsd's engine context pins strict booleans on this face:
140
+ # a grammar-invalid calendar lexical must answer false, never nil
141
+ # (the with_date_fields short-circuit used to leak nil).
142
+ it "answers a strict boolean for every supported name" do
143
+ garbage = ["not-a-date", "", " ", "12x", "9999-99-99Tzz"]
144
+
145
+ Moxml::XSD::BuiltinLexical.names.each do |name|
146
+ garbage.each do |lexical|
147
+ answer = valid_lexical?(name, lexical)
148
+ expect(answer.eql?(true) || answer.eql?(false)).to be(true),
149
+ "#{name} #{lexical.inspect} answered " \
150
+ "#{answer.inspect}, want a boolean"
151
+ end
152
+ end
153
+ end
154
+
155
+ it "answers false for the calendar faces the engine context pins" do
156
+ expect(valid_lexical?("xs:date", "not-a-date")).to be(false)
157
+ expect(valid_lexical?("xs:dateTime", "not-a-date")).to be(false)
158
+ expect(valid_lexical?("xs:dateTimeStamp", "not-a-date")).to be(false)
159
+ expect(valid_lexical?("xs:dateTimeStamp", "2024-02-29T00:00:00")).to be(false)
160
+ expect(valid_lexical?("xs:dateTimeStamp", "2024-02-29T00:00:00Z")).to be(true)
161
+ end
162
+ end
163
+ 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.167
4
+ version: 0.5.169
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