schemurai 3.1.0-aarch64-linux-gnu
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 +7 -0
- data/LICENSE +21 -0
- data/README.md +395 -0
- data/lib/schemurai/backend.rb +34 -0
- data/lib/schemurai/container_checks/array_diagnostics.rb +119 -0
- data/lib/schemurai/container_checks/object_diagnostics.rb +152 -0
- data/lib/schemurai/container_checks.rb +534 -0
- data/lib/schemurai/decimal.rb +11 -0
- data/lib/schemurai/dialect.rb +106 -0
- data/lib/schemurai/dialect_keywords.rb +91 -0
- data/lib/schemurai/dialects/draft2019_09.rb +27 -0
- data/lib/schemurai/dialects/draft2020_12.rb +27 -0
- data/lib/schemurai/dialects/draft7.rb +27 -0
- data/lib/schemurai/ecma_regexp.rb +46 -0
- data/lib/schemurai/error_message.rb +102 -0
- data/lib/schemurai/evaluation.rb +63 -0
- data/lib/schemurai/evaluation_limits.rb +65 -0
- data/lib/schemurai/evaluation_pool.rb +66 -0
- data/lib/schemurai/evaluator.rb +861 -0
- data/lib/schemurai/formats.rb +403 -0
- data/lib/schemurai/json_pointer.rb +11 -0
- data/lib/schemurai/limit_budget.rb +78 -0
- data/lib/schemurai/limit_exceeded.rb +18 -0
- data/lib/schemurai/limited_evaluator.rb +43 -0
- data/lib/schemurai/meta_schemas/draft2019_09.rb +149 -0
- data/lib/schemurai/meta_schemas/draft2020_12.rb +149 -0
- data/lib/schemurai/meta_schemas/draft7.rb +121 -0
- data/lib/schemurai/meta_schemas.rb +28 -0
- data/lib/schemurai/native/4.0/schemurai_native.so +0 -0
- data/lib/schemurai/native/diagnostics.rb +53 -0
- data/lib/schemurai/native/evaluator.rb +87 -0
- data/lib/schemurai/native/null_numeric.rb +23 -0
- data/lib/schemurai/number_checks.rb +128 -0
- data/lib/schemurai/schema_domain.rb +95 -0
- data/lib/schemurai/schema_graph.rb +538 -0
- data/lib/schemurai/schema_node.rb +55 -0
- data/lib/schemurai/string_checks.rb +166 -0
- data/lib/schemurai/type_checks.rb +59 -0
- data/lib/schemurai/unique_items.rb +68 -0
- data/lib/schemurai/value_equality.rb +72 -0
- data/lib/schemurai/version.rb +6 -0
- data/lib/schemurai/vm/compiler.rb +410 -0
- data/lib/schemurai/vm/evaluator.rb +663 -0
- data/lib/schemurai/vm/limited_evaluator.rb +27 -0
- data/lib/schemurai.rb +307 -0
- data/lib/schemurai_native.rb +4 -0
- data/sig/schemurai.rbs +108 -0
- metadata +121 -0
|
@@ -0,0 +1,403 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Schemurai
|
|
4
|
+
module Internal
|
|
5
|
+
module Formats
|
|
6
|
+
class Format
|
|
7
|
+
attr_reader :name
|
|
8
|
+
|
|
9
|
+
def initialize(name = nil)
|
|
10
|
+
@name = name
|
|
11
|
+
freeze
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def call(_value)
|
|
15
|
+
true
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
private def valid_date_at?(value, start)
|
|
19
|
+
return false unless value.getbyte(start + 4) == 45 && value.getbyte(start + 7) == 45
|
|
20
|
+
|
|
21
|
+
century = two_digits(value, start)
|
|
22
|
+
year_in_century = two_digits(value, start + 2)
|
|
23
|
+
month = two_digits(value, start + 5)
|
|
24
|
+
day = two_digits(value, start + 8)
|
|
25
|
+
return false unless century && year_in_century
|
|
26
|
+
|
|
27
|
+
year = (century * 100) + year_in_century
|
|
28
|
+
return false unless year && month && day && month.between?(1, 12)
|
|
29
|
+
|
|
30
|
+
last_day = if month == 2
|
|
31
|
+
leap_year?(year) ? 29 : 28
|
|
32
|
+
elsif month == 4 || month == 6 || month == 9 || month == 11
|
|
33
|
+
30
|
|
34
|
+
else
|
|
35
|
+
31
|
|
36
|
+
end
|
|
37
|
+
day.between?(1, last_day)
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
private def valid_time_at?(value, start)
|
|
41
|
+
return false if value.bytesize < start + 9
|
|
42
|
+
return false unless value.getbyte(start + 2) == 58 && value.getbyte(start + 5) == 58
|
|
43
|
+
|
|
44
|
+
hour = two_digits(value, start)
|
|
45
|
+
minute = two_digits(value, start + 3)
|
|
46
|
+
second = two_digits(value, start + 6)
|
|
47
|
+
return false unless hour&.between?(0, 23) && minute&.between?(0, 59) && second&.between?(0, 60)
|
|
48
|
+
|
|
49
|
+
index = start + 8
|
|
50
|
+
if value.getbyte(index) == 46
|
|
51
|
+
index += 1
|
|
52
|
+
fraction_start = index
|
|
53
|
+
while (byte = value.getbyte(index)) && byte >= 48 && byte <= 57
|
|
54
|
+
index += 1
|
|
55
|
+
end
|
|
56
|
+
return false if index == fraction_start
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
zone = value.getbyte(index)
|
|
60
|
+
if zone == 90 || zone == 122
|
|
61
|
+
return false unless index + 1 == value.bytesize
|
|
62
|
+
offset = 0
|
|
63
|
+
else
|
|
64
|
+
return false unless zone == 43 || zone == 45
|
|
65
|
+
return false unless index + 6 == value.bytesize
|
|
66
|
+
return false unless value.getbyte(index + 3) == 58
|
|
67
|
+
|
|
68
|
+
offset_hour = two_digits(value, index + 1)
|
|
69
|
+
offset_minute = two_digits(value, index + 4)
|
|
70
|
+
return false unless offset_hour&.between?(0, 23) && offset_minute&.between?(0, 59)
|
|
71
|
+
|
|
72
|
+
offset = (offset_hour * 60) + offset_minute
|
|
73
|
+
offset = -offset if zone == 45
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
second != 60 || ((hour * 60) + minute - offset) % 1_440 == 1_439
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
private def valid_duration_time_at?(value, index)
|
|
80
|
+
length = value.bytesize
|
|
81
|
+
number_end = ascii_digits_end(value, index)
|
|
82
|
+
return false unless number_end
|
|
83
|
+
|
|
84
|
+
case value.getbyte(number_end)
|
|
85
|
+
when 72 # H
|
|
86
|
+
index = number_end + 1
|
|
87
|
+
if (number_end = ascii_digits_end(value, index))
|
|
88
|
+
return false unless value.getbyte(number_end) == 77 # M
|
|
89
|
+
|
|
90
|
+
index = number_end + 1
|
|
91
|
+
if (number_end = ascii_digits_end(value, index))
|
|
92
|
+
return false unless value.getbyte(number_end) == 83 # S
|
|
93
|
+
|
|
94
|
+
index = number_end + 1
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
when 77 # M
|
|
98
|
+
index = number_end + 1
|
|
99
|
+
if (number_end = ascii_digits_end(value, index))
|
|
100
|
+
return false unless value.getbyte(number_end) == 83 # S
|
|
101
|
+
|
|
102
|
+
index = number_end + 1
|
|
103
|
+
end
|
|
104
|
+
when 83 # S
|
|
105
|
+
index = number_end + 1
|
|
106
|
+
else
|
|
107
|
+
return false
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
index == length
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
private def valid_json_pointer_at?(value, index)
|
|
114
|
+
length = value.bytesize
|
|
115
|
+
return true if index == length
|
|
116
|
+
return false unless value.getbyte(index) == 47 # /
|
|
117
|
+
|
|
118
|
+
while index < length
|
|
119
|
+
if value.getbyte(index) == 126 # ~
|
|
120
|
+
index += 1
|
|
121
|
+
return false unless value.getbyte(index) == 48 || value.getbyte(index) == 49 # 0 or 1
|
|
122
|
+
end
|
|
123
|
+
index += 1
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
true
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
private def ascii_digits_end(value, index)
|
|
130
|
+
start = index
|
|
131
|
+
index += 1 while value.getbyte(index)&.between?(48, 57)
|
|
132
|
+
index unless index == start
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
private def two_digits(value, start)
|
|
136
|
+
tens = value.getbyte(start) - 48
|
|
137
|
+
ones = value.getbyte(start + 1) - 48
|
|
138
|
+
return unless tens.between?(0, 9) && ones.between?(0, 9)
|
|
139
|
+
|
|
140
|
+
(tens * 10) + ones
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
private def leap_year?(year)
|
|
144
|
+
(year % 4).zero? && (!(year % 100).zero? || (year % 400).zero?)
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
private def valid_ipv4_at?(value, index)
|
|
148
|
+
length = value.bytesize
|
|
149
|
+
|
|
150
|
+
4.times do |octet|
|
|
151
|
+
byte = value.getbyte(index)
|
|
152
|
+
return false unless byte&.between?(48, 57)
|
|
153
|
+
return false if byte == 48 && value.getbyte(index + 1)&.between?(48, 57)
|
|
154
|
+
|
|
155
|
+
number = 0
|
|
156
|
+
digits = 0
|
|
157
|
+
while (byte = value.getbyte(index))&.between?(48, 57)
|
|
158
|
+
number = (number * 10) + byte - 48
|
|
159
|
+
digits += 1
|
|
160
|
+
return false if digits > 3
|
|
161
|
+
|
|
162
|
+
index += 1
|
|
163
|
+
end
|
|
164
|
+
return false if number > 255
|
|
165
|
+
|
|
166
|
+
if octet == 3
|
|
167
|
+
return false unless index == length
|
|
168
|
+
else
|
|
169
|
+
return false unless value.getbyte(index) == 46
|
|
170
|
+
|
|
171
|
+
index += 1
|
|
172
|
+
end
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
true
|
|
176
|
+
end
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
class DateFormat < Format
|
|
180
|
+
def initialize = super("date")
|
|
181
|
+
|
|
182
|
+
def call(value)
|
|
183
|
+
value.bytesize == 10 && valid_date_at?(value, 0)
|
|
184
|
+
end
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
class TimeFormat < Format
|
|
188
|
+
def initialize = super("time")
|
|
189
|
+
|
|
190
|
+
def call(value)
|
|
191
|
+
valid_time_at?(value, 0)
|
|
192
|
+
end
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
class DateTimeFormat < Format
|
|
196
|
+
def initialize = super("date-time")
|
|
197
|
+
|
|
198
|
+
def call(value)
|
|
199
|
+
value.bytesize >= 20 &&
|
|
200
|
+
valid_date_at?(value, 0) &&
|
|
201
|
+
(value.getbyte(10) == 84 || value.getbyte(10) == 116) &&
|
|
202
|
+
valid_time_at?(value, 11)
|
|
203
|
+
end
|
|
204
|
+
end
|
|
205
|
+
|
|
206
|
+
class DurationFormat < Format
|
|
207
|
+
def initialize = super("duration")
|
|
208
|
+
|
|
209
|
+
def call(value)
|
|
210
|
+
length = value.bytesize
|
|
211
|
+
return false unless value.getbyte(0) == 80 && length > 1
|
|
212
|
+
|
|
213
|
+
index = 1
|
|
214
|
+
return valid_duration_time_at?(value, index + 1) if value.getbyte(index) == 84 # T
|
|
215
|
+
|
|
216
|
+
number_end = ascii_digits_end(value, index)
|
|
217
|
+
return false unless number_end
|
|
218
|
+
|
|
219
|
+
case value.getbyte(number_end)
|
|
220
|
+
when 87 # W
|
|
221
|
+
return number_end + 1 == length
|
|
222
|
+
when 89 # Y
|
|
223
|
+
index = number_end + 1
|
|
224
|
+
if (number_end = ascii_digits_end(value, index))
|
|
225
|
+
return false unless value.getbyte(number_end) == 77 # M
|
|
226
|
+
|
|
227
|
+
index = number_end + 1
|
|
228
|
+
if (number_end = ascii_digits_end(value, index))
|
|
229
|
+
return false unless value.getbyte(number_end) == 68 # D
|
|
230
|
+
|
|
231
|
+
index = number_end + 1
|
|
232
|
+
end
|
|
233
|
+
end
|
|
234
|
+
when 77 # M
|
|
235
|
+
index = number_end + 1
|
|
236
|
+
if (number_end = ascii_digits_end(value, index))
|
|
237
|
+
return false unless value.getbyte(number_end) == 68 # D
|
|
238
|
+
|
|
239
|
+
index = number_end + 1
|
|
240
|
+
end
|
|
241
|
+
when 68 # D
|
|
242
|
+
index = number_end + 1
|
|
243
|
+
else
|
|
244
|
+
return false
|
|
245
|
+
end
|
|
246
|
+
|
|
247
|
+
return true if index == length
|
|
248
|
+
return false unless value.getbyte(index) == 84 # T
|
|
249
|
+
|
|
250
|
+
valid_duration_time_at?(value, index + 1)
|
|
251
|
+
end
|
|
252
|
+
end
|
|
253
|
+
|
|
254
|
+
class Ipv4Format < Format
|
|
255
|
+
def initialize = super("ipv4")
|
|
256
|
+
|
|
257
|
+
def call(value)
|
|
258
|
+
valid_ipv4_at?(value, 0)
|
|
259
|
+
end
|
|
260
|
+
end
|
|
261
|
+
|
|
262
|
+
class Ipv6Format < Format
|
|
263
|
+
def initialize = super("ipv6")
|
|
264
|
+
|
|
265
|
+
def call(value)
|
|
266
|
+
length = value.bytesize
|
|
267
|
+
return false if length.zero?
|
|
268
|
+
|
|
269
|
+
index = 0
|
|
270
|
+
groups = 0
|
|
271
|
+
compressed = false
|
|
272
|
+
|
|
273
|
+
if value.getbyte(index) == 58 # :
|
|
274
|
+
return false unless value.getbyte(index + 1) == 58
|
|
275
|
+
|
|
276
|
+
compressed = true
|
|
277
|
+
index += 2
|
|
278
|
+
return true if index == length
|
|
279
|
+
end
|
|
280
|
+
|
|
281
|
+
while index < length
|
|
282
|
+
group_start = index
|
|
283
|
+
hex_digits = 0
|
|
284
|
+
while (byte = value.getbyte(index)) &&
|
|
285
|
+
(byte.between?(48, 57) || byte.between?(65, 70) || byte.between?(97, 102))
|
|
286
|
+
hex_digits += 1
|
|
287
|
+
return false if hex_digits > 4
|
|
288
|
+
|
|
289
|
+
index += 1
|
|
290
|
+
end
|
|
291
|
+
return false if hex_digits.zero?
|
|
292
|
+
|
|
293
|
+
if value.getbyte(index) == 46 # .
|
|
294
|
+
return false unless groups <= 6 && valid_ipv4_at?(value, group_start)
|
|
295
|
+
|
|
296
|
+
groups += 2
|
|
297
|
+
index = length
|
|
298
|
+
break
|
|
299
|
+
end
|
|
300
|
+
|
|
301
|
+
groups += 1
|
|
302
|
+
return false if groups > 8
|
|
303
|
+
break if index == length
|
|
304
|
+
return false unless value.getbyte(index) == 58 # :
|
|
305
|
+
|
|
306
|
+
index += 1
|
|
307
|
+
if value.getbyte(index) == 58 # :
|
|
308
|
+
return false if compressed
|
|
309
|
+
|
|
310
|
+
compressed = true
|
|
311
|
+
index += 1
|
|
312
|
+
break if index == length
|
|
313
|
+
elsif index == length
|
|
314
|
+
return false
|
|
315
|
+
end
|
|
316
|
+
end
|
|
317
|
+
|
|
318
|
+
compressed ? groups < 8 : groups == 8
|
|
319
|
+
end
|
|
320
|
+
end
|
|
321
|
+
|
|
322
|
+
class JsonPointerFormat < Format
|
|
323
|
+
def initialize = super("json-pointer")
|
|
324
|
+
|
|
325
|
+
def call(value)
|
|
326
|
+
valid_json_pointer_at?(value, 0)
|
|
327
|
+
end
|
|
328
|
+
end
|
|
329
|
+
|
|
330
|
+
class RelativeJsonPointerFormat < Format
|
|
331
|
+
def initialize = super("relative-json-pointer")
|
|
332
|
+
|
|
333
|
+
def call(value)
|
|
334
|
+
length = value.bytesize
|
|
335
|
+
first = value.getbyte(0)
|
|
336
|
+
return false unless first&.between?(48, 57)
|
|
337
|
+
|
|
338
|
+
index = 1
|
|
339
|
+
return false if first == 48 && value.getbyte(index)&.between?(48, 57)
|
|
340
|
+
|
|
341
|
+
index += 1 while value.getbyte(index)&.between?(48, 57)
|
|
342
|
+
return true if index == length
|
|
343
|
+
return index + 1 == length if value.getbyte(index) == 35 # #
|
|
344
|
+
|
|
345
|
+
valid_json_pointer_at?(value, index)
|
|
346
|
+
end
|
|
347
|
+
end
|
|
348
|
+
|
|
349
|
+
class UuidFormat < Format
|
|
350
|
+
def initialize = super("uuid")
|
|
351
|
+
|
|
352
|
+
def call(value)
|
|
353
|
+
return false unless value.bytesize == 36
|
|
354
|
+
|
|
355
|
+
36.times do |index|
|
|
356
|
+
byte = value.getbyte(index)
|
|
357
|
+
if index == 8 || index == 13 || index == 18 || index == 23
|
|
358
|
+
return false unless byte == 45 # -
|
|
359
|
+
else
|
|
360
|
+
return false unless byte&.between?(48, 57) || byte&.between?(65, 70) || byte&.between?(97, 102)
|
|
361
|
+
end
|
|
362
|
+
end
|
|
363
|
+
|
|
364
|
+
true
|
|
365
|
+
end
|
|
366
|
+
end
|
|
367
|
+
|
|
368
|
+
UNKNOWN = Format.new
|
|
369
|
+
DATE = DateFormat.new
|
|
370
|
+
TIME = TimeFormat.new
|
|
371
|
+
DATE_TIME = DateTimeFormat.new
|
|
372
|
+
DURATION = DurationFormat.new
|
|
373
|
+
IPV4 = Ipv4Format.new
|
|
374
|
+
IPV6 = Ipv6Format.new
|
|
375
|
+
JSON_POINTER = JsonPointerFormat.new
|
|
376
|
+
RELATIVE_JSON_POINTER = RelativeJsonPointerFormat.new
|
|
377
|
+
UUID = UuidFormat.new
|
|
378
|
+
|
|
379
|
+
class << self
|
|
380
|
+
def resolve(format)
|
|
381
|
+
case format
|
|
382
|
+
when "date" then DATE
|
|
383
|
+
when "time" then TIME
|
|
384
|
+
when "date-time" then DATE_TIME
|
|
385
|
+
when "duration" then DURATION
|
|
386
|
+
when "ipv4" then IPV4
|
|
387
|
+
when "ipv6" then IPV6
|
|
388
|
+
when "json-pointer" then JSON_POINTER
|
|
389
|
+
when "relative-json-pointer" then RELATIVE_JSON_POINTER
|
|
390
|
+
when "uuid" then UUID
|
|
391
|
+
else UNKNOWN
|
|
392
|
+
end
|
|
393
|
+
end
|
|
394
|
+
end
|
|
395
|
+
|
|
396
|
+
private_constant :Format, :DateFormat, :TimeFormat, :DateTimeFormat, :DurationFormat,
|
|
397
|
+
:Ipv4Format, :Ipv6Format, :JsonPointerFormat, :RelativeJsonPointerFormat, :UuidFormat, :UNKNOWN,
|
|
398
|
+
:DATE, :TIME, :DATE_TIME, :DURATION, :IPV4, :IPV6, :JSON_POINTER, :RELATIVE_JSON_POINTER, :UUID
|
|
399
|
+
end
|
|
400
|
+
end
|
|
401
|
+
|
|
402
|
+
private_constant :Internal
|
|
403
|
+
end
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Schemurai
|
|
4
|
+
module Internal
|
|
5
|
+
# Mutable state owned by one validation call, including native/VM fallback.
|
|
6
|
+
class LimitBudget
|
|
7
|
+
CHECK_INTERVAL = 256
|
|
8
|
+
|
|
9
|
+
def self.validate_options!(max_errors, timeout)
|
|
10
|
+
valid_errors = max_errors.nil? || (max_errors.is_a?(Integer) && max_errors >= 0)
|
|
11
|
+
unless valid_errors
|
|
12
|
+
raise ArgumentError, "max_errors must be a nonnegative Integer or nil"
|
|
13
|
+
end
|
|
14
|
+
valid_timeout = timeout.nil? || ((timeout.is_a?(Integer) || timeout.is_a?(Float)) && timeout >= 0 && timeout.to_f.finite?)
|
|
15
|
+
unless valid_timeout
|
|
16
|
+
raise ArgumentError, "timeout must be finite nonnegative seconds or nil"
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def initialize(max_errors: nil, timeout: nil)
|
|
21
|
+
@max_errors = max_errors
|
|
22
|
+
@timeout = timeout
|
|
23
|
+
@errors = 0
|
|
24
|
+
@checks = 0
|
|
25
|
+
if timeout
|
|
26
|
+
@started = monotonic_time
|
|
27
|
+
@deadline = @started + timeout
|
|
28
|
+
check_deadline!
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# Synchronous polling only; never inject exceptions into another thread.
|
|
33
|
+
def checkpoint
|
|
34
|
+
return unless @deadline
|
|
35
|
+
|
|
36
|
+
@checks += 1
|
|
37
|
+
check_deadline! if (@checks % CHECK_INTERVAL).zero?
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def check_deadline!
|
|
41
|
+
return unless @deadline
|
|
42
|
+
|
|
43
|
+
now = monotonic_time
|
|
44
|
+
raise LimitExceeded.new(:timeout, @timeout, now - @started) if now >= @deadline
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# Ruby retains the original deadline through native execution and fallback.
|
|
48
|
+
def native_duration
|
|
49
|
+
check_deadline!
|
|
50
|
+
@deadline && [@deadline - monotonic_time, 0.0].max
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def native_max_errors
|
|
54
|
+
@max_errors && [@max_errors, (1 << (0.size * 8)) - 1].min
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def raise_native_limit(kind, attempted)
|
|
58
|
+
if kind == 2
|
|
59
|
+
raise LimitExceeded.new(:timeout, @timeout, monotonic_time - @started)
|
|
60
|
+
end
|
|
61
|
+
raise LimitExceeded.new(:max_errors, @max_errors, attempted)
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
# Check before constructing the next message and JSON Pointers.
|
|
65
|
+
def reserve_error
|
|
66
|
+
checkpoint
|
|
67
|
+
attempted = @errors + 1
|
|
68
|
+
raise LimitExceeded.new(:max_errors, @max_errors, attempted) if @max_errors && attempted > @max_errors
|
|
69
|
+
|
|
70
|
+
@errors = attempted
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
private def monotonic_time
|
|
74
|
+
Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
end
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Schemurai
|
|
4
|
+
# Raised when validation cannot finish within an application-supplied limit.
|
|
5
|
+
# This is an incomplete evaluation, not a JSON Schema validation failure.
|
|
6
|
+
class LimitExceeded < Error
|
|
7
|
+
# Limit name, configured maximum and attempted count (or elapsed seconds).
|
|
8
|
+
attr_reader :limit, :maximum, :attempted
|
|
9
|
+
|
|
10
|
+
# Creates an interruption error without embedding the input or partial errors.
|
|
11
|
+
def initialize(limit, maximum, attempted)
|
|
12
|
+
@limit = limit
|
|
13
|
+
@maximum = maximum
|
|
14
|
+
@attempted = attempted
|
|
15
|
+
super("validation exceeded #{limit} (maximum #{maximum}, attempted #{attempted})")
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "limit_exceeded"
|
|
4
|
+
require_relative "evaluation_limits"
|
|
5
|
+
require_relative "evaluator"
|
|
6
|
+
|
|
7
|
+
module Schemurai
|
|
8
|
+
module Internal
|
|
9
|
+
# Ruby evaluation with invocation-local limits.
|
|
10
|
+
class LimitedEvaluator < Evaluator
|
|
11
|
+
include EvaluationLimits
|
|
12
|
+
|
|
13
|
+
private def add_error(keyword, message = nil, append_keyword: true, &block)
|
|
14
|
+
@budget.reserve_error if @error_callback
|
|
15
|
+
super
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
private def decimal(value)
|
|
19
|
+
@budget.check_deadline!
|
|
20
|
+
result = super
|
|
21
|
+
@budget.check_deadline!
|
|
22
|
+
result
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
private def ecma_regexp(pattern)
|
|
26
|
+
@budget.check_deadline!
|
|
27
|
+
result = super
|
|
28
|
+
@budget.check_deadline!
|
|
29
|
+
result
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
private def json_equal?(left, right)
|
|
33
|
+
@budget.checkpoint
|
|
34
|
+
super
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
private def json_fingerprint(value)
|
|
38
|
+
@budget.checkpoint
|
|
39
|
+
super
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|