schemurai 1.0.0
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 +206 -0
- data/lib/schemurai/dialect.rb +118 -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/evaluation.rb +46 -0
- data/lib/schemurai/evaluator.rb +857 -0
- data/lib/schemurai/formats.rb +403 -0
- data/lib/schemurai/meta_schemas/draft2019_09.rb +48 -0
- data/lib/schemurai/meta_schemas/draft2020_12.rb +48 -0
- data/lib/schemurai/meta_schemas/draft7.rb +121 -0
- data/lib/schemurai/meta_schemas.rb +28 -0
- data/lib/schemurai/schema_graph.rb +481 -0
- data/lib/schemurai/schema_node.rb +55 -0
- data/lib/schemurai/version.rb +5 -0
- data/lib/schemurai.rb +110 -0
- metadata +76 -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,48 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../meta_schemas"
|
|
4
|
+
|
|
5
|
+
module Schemurai
|
|
6
|
+
module Internal
|
|
7
|
+
module MetaSchemas
|
|
8
|
+
module Draft201909
|
|
9
|
+
META_SCHEMA_URI = "https://json-schema.org/draft/2019-09/schema"
|
|
10
|
+
SCHEMA = {
|
|
11
|
+
"$schema" => META_SCHEMA_URI,
|
|
12
|
+
"$id" => META_SCHEMA_URI,
|
|
13
|
+
"$recursiveAnchor" => true,
|
|
14
|
+
"type" => ["object", "boolean"],
|
|
15
|
+
"properties" => {
|
|
16
|
+
"$defs" => {"type" => "object", "additionalProperties" => {"$recursiveRef" => "#"}},
|
|
17
|
+
"definitions" => {"type" => "object", "additionalProperties" => {"$recursiveRef" => "#"}},
|
|
18
|
+
"type" => {
|
|
19
|
+
"anyOf" => [
|
|
20
|
+
{"enum" => ["null", "boolean", "object", "array", "number", "integer", "string"]},
|
|
21
|
+
{
|
|
22
|
+
"type" => "array",
|
|
23
|
+
"items" => {"enum" => ["null", "boolean", "object", "array", "number", "integer", "string"]},
|
|
24
|
+
"minItems" => 1,
|
|
25
|
+
"uniqueItems" => true
|
|
26
|
+
}
|
|
27
|
+
]
|
|
28
|
+
},
|
|
29
|
+
"minLength" => {"type" => "integer", "minimum" => 0},
|
|
30
|
+
"maxLength" => {"type" => "integer", "minimum" => 0},
|
|
31
|
+
"minItems" => {"type" => "integer", "minimum" => 0},
|
|
32
|
+
"maxItems" => {"type" => "integer", "minimum" => 0},
|
|
33
|
+
"minContains" => {"type" => "integer", "minimum" => 0},
|
|
34
|
+
"maxContains" => {"type" => "integer", "minimum" => 0},
|
|
35
|
+
"minProperties" => {"type" => "integer", "minimum" => 0},
|
|
36
|
+
"maxProperties" => {"type" => "integer", "minimum" => 0}
|
|
37
|
+
}
|
|
38
|
+
}.freeze
|
|
39
|
+
|
|
40
|
+
MetaSchemas.register(META_SCHEMA_URI, SCHEMA)
|
|
41
|
+
|
|
42
|
+
private_constant :META_SCHEMA_URI, :SCHEMA
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
private_constant :Internal
|
|
48
|
+
end
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../meta_schemas"
|
|
4
|
+
|
|
5
|
+
module Schemurai
|
|
6
|
+
module Internal
|
|
7
|
+
module MetaSchemas
|
|
8
|
+
module Draft202012
|
|
9
|
+
META_SCHEMA_URI = "https://json-schema.org/draft/2020-12/schema"
|
|
10
|
+
SCHEMA = {
|
|
11
|
+
"$schema" => META_SCHEMA_URI,
|
|
12
|
+
"$id" => META_SCHEMA_URI,
|
|
13
|
+
"$dynamicAnchor" => "meta",
|
|
14
|
+
"type" => ["object", "boolean"],
|
|
15
|
+
"properties" => {
|
|
16
|
+
"$defs" => {"type" => "object", "additionalProperties" => {"$dynamicRef" => "#meta"}},
|
|
17
|
+
"definitions" => {"type" => "object", "additionalProperties" => {"$dynamicRef" => "#meta"}},
|
|
18
|
+
"type" => {
|
|
19
|
+
"anyOf" => [
|
|
20
|
+
{"enum" => ["null", "boolean", "object", "array", "number", "integer", "string"]},
|
|
21
|
+
{
|
|
22
|
+
"type" => "array",
|
|
23
|
+
"items" => {"enum" => ["null", "boolean", "object", "array", "number", "integer", "string"]},
|
|
24
|
+
"minItems" => 1,
|
|
25
|
+
"uniqueItems" => true
|
|
26
|
+
}
|
|
27
|
+
]
|
|
28
|
+
},
|
|
29
|
+
"minLength" => {"type" => "integer", "minimum" => 0},
|
|
30
|
+
"maxLength" => {"type" => "integer", "minimum" => 0},
|
|
31
|
+
"minItems" => {"type" => "integer", "minimum" => 0},
|
|
32
|
+
"maxItems" => {"type" => "integer", "minimum" => 0},
|
|
33
|
+
"minContains" => {"type" => "integer", "minimum" => 0},
|
|
34
|
+
"maxContains" => {"type" => "integer", "minimum" => 0},
|
|
35
|
+
"minProperties" => {"type" => "integer", "minimum" => 0},
|
|
36
|
+
"maxProperties" => {"type" => "integer", "minimum" => 0}
|
|
37
|
+
}
|
|
38
|
+
}.freeze
|
|
39
|
+
|
|
40
|
+
MetaSchemas.register(META_SCHEMA_URI, SCHEMA)
|
|
41
|
+
|
|
42
|
+
private_constant :META_SCHEMA_URI, :SCHEMA
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
private_constant :Internal
|
|
48
|
+
end
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../meta_schemas"
|
|
4
|
+
|
|
5
|
+
module Schemurai
|
|
6
|
+
module Internal
|
|
7
|
+
module MetaSchemas
|
|
8
|
+
module Draft7
|
|
9
|
+
META_SCHEMA_URI = "http://json-schema.org/draft-07/schema"
|
|
10
|
+
SCHEMA = {
|
|
11
|
+
"$schema" => "#{META_SCHEMA_URI}#",
|
|
12
|
+
"$id" => "#{META_SCHEMA_URI}#",
|
|
13
|
+
"definitions" => {
|
|
14
|
+
"schemaArray" => {"type" => "array", "minItems" => 1, "items" => {"$ref" => "#"}},
|
|
15
|
+
"nonNegativeInteger" => {"type" => "integer", "minimum" => 0},
|
|
16
|
+
"nonNegativeIntegerDefault0" => {
|
|
17
|
+
"allOf" => [
|
|
18
|
+
{"$ref" => "#/definitions/nonNegativeInteger"},
|
|
19
|
+
{"default" => 0}
|
|
20
|
+
]
|
|
21
|
+
},
|
|
22
|
+
"simpleTypes" => {"enum" => %w[array boolean integer null number object string]},
|
|
23
|
+
"stringArray" => {
|
|
24
|
+
"type" => "array",
|
|
25
|
+
"items" => {"type" => "string"},
|
|
26
|
+
"uniqueItems" => true,
|
|
27
|
+
"default" => []
|
|
28
|
+
}
|
|
29
|
+
},
|
|
30
|
+
"type" => ["object", "boolean"],
|
|
31
|
+
"properties" => {
|
|
32
|
+
"$id" => {"type" => "string", "format" => "uri-reference"},
|
|
33
|
+
"$schema" => {"type" => "string", "format" => "uri"},
|
|
34
|
+
"$ref" => {"type" => "string", "format" => "uri-reference"},
|
|
35
|
+
"$comment" => {"type" => "string"},
|
|
36
|
+
"title" => {"type" => "string"},
|
|
37
|
+
"description" => {"type" => "string"},
|
|
38
|
+
"default" => true,
|
|
39
|
+
"readOnly" => {"type" => "boolean", "default" => false},
|
|
40
|
+
"writeOnly" => {"type" => "boolean", "default" => false},
|
|
41
|
+
"examples" => {"type" => "array", "items" => true},
|
|
42
|
+
"multipleOf" => {"type" => "number", "exclusiveMinimum" => 0},
|
|
43
|
+
"maximum" => {"type" => "number"},
|
|
44
|
+
"exclusiveMaximum" => {"type" => "number"},
|
|
45
|
+
"minimum" => {"type" => "number"},
|
|
46
|
+
"exclusiveMinimum" => {"type" => "number"},
|
|
47
|
+
"maxLength" => {"$ref" => "#/definitions/nonNegativeInteger"},
|
|
48
|
+
"minLength" => {"$ref" => "#/definitions/nonNegativeIntegerDefault0"},
|
|
49
|
+
"pattern" => {"type" => "string", "format" => "regex"},
|
|
50
|
+
"additionalItems" => {"$ref" => "#"},
|
|
51
|
+
"items" => {
|
|
52
|
+
"anyOf" => [{"$ref" => "#"}, {"$ref" => "#/definitions/schemaArray"}],
|
|
53
|
+
"default" => true
|
|
54
|
+
},
|
|
55
|
+
"maxItems" => {"$ref" => "#/definitions/nonNegativeInteger"},
|
|
56
|
+
"minItems" => {"$ref" => "#/definitions/nonNegativeIntegerDefault0"},
|
|
57
|
+
"uniqueItems" => {"type" => "boolean", "default" => false},
|
|
58
|
+
"contains" => {"$ref" => "#"},
|
|
59
|
+
"maxProperties" => {"$ref" => "#/definitions/nonNegativeInteger"},
|
|
60
|
+
"minProperties" => {"$ref" => "#/definitions/nonNegativeIntegerDefault0"},
|
|
61
|
+
"required" => {"$ref" => "#/definitions/stringArray"},
|
|
62
|
+
"additionalProperties" => {"$ref" => "#"},
|
|
63
|
+
"definitions" => {
|
|
64
|
+
"type" => "object",
|
|
65
|
+
"additionalProperties" => {"$ref" => "#"},
|
|
66
|
+
"default" => {}
|
|
67
|
+
},
|
|
68
|
+
"properties" => {
|
|
69
|
+
"type" => "object",
|
|
70
|
+
"additionalProperties" => {"$ref" => "#"},
|
|
71
|
+
"default" => {}
|
|
72
|
+
},
|
|
73
|
+
"patternProperties" => {
|
|
74
|
+
"type" => "object",
|
|
75
|
+
"additionalProperties" => {"$ref" => "#"},
|
|
76
|
+
"propertyNames" => {"format" => "regex"},
|
|
77
|
+
"default" => {}
|
|
78
|
+
},
|
|
79
|
+
"dependencies" => {
|
|
80
|
+
"type" => "object",
|
|
81
|
+
"additionalProperties" => {
|
|
82
|
+
"anyOf" => [{"$ref" => "#"}, {"$ref" => "#/definitions/stringArray"}]
|
|
83
|
+
}
|
|
84
|
+
},
|
|
85
|
+
"propertyNames" => {"$ref" => "#"},
|
|
86
|
+
"const" => true,
|
|
87
|
+
"enum" => {"type" => "array", "items" => true, "minItems" => 1, "uniqueItems" => true},
|
|
88
|
+
"type" => {
|
|
89
|
+
"anyOf" => [
|
|
90
|
+
{"$ref" => "#/definitions/simpleTypes"},
|
|
91
|
+
{
|
|
92
|
+
"type" => "array",
|
|
93
|
+
"items" => {"$ref" => "#/definitions/simpleTypes"},
|
|
94
|
+
"minItems" => 1,
|
|
95
|
+
"uniqueItems" => true
|
|
96
|
+
}
|
|
97
|
+
]
|
|
98
|
+
},
|
|
99
|
+
"format" => {"type" => "string"},
|
|
100
|
+
"contentMediaType" => {"type" => "string"},
|
|
101
|
+
"contentEncoding" => {"type" => "string"},
|
|
102
|
+
"if" => {"$ref" => "#"},
|
|
103
|
+
"then" => {"$ref" => "#"},
|
|
104
|
+
"else" => {"$ref" => "#"},
|
|
105
|
+
"allOf" => {"$ref" => "#/definitions/schemaArray"},
|
|
106
|
+
"anyOf" => {"$ref" => "#/definitions/schemaArray"},
|
|
107
|
+
"oneOf" => {"$ref" => "#/definitions/schemaArray"},
|
|
108
|
+
"not" => {"$ref" => "#"}
|
|
109
|
+
},
|
|
110
|
+
"default" => true
|
|
111
|
+
}.freeze
|
|
112
|
+
|
|
113
|
+
MetaSchemas.register(META_SCHEMA_URI, SCHEMA)
|
|
114
|
+
|
|
115
|
+
private_constant :META_SCHEMA_URI, :SCHEMA
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
private_constant :Internal
|
|
121
|
+
end
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Schemurai
|
|
4
|
+
module Internal
|
|
5
|
+
module MetaSchemas
|
|
6
|
+
class << self
|
|
7
|
+
def register(uri, schema)
|
|
8
|
+
registry[normalize_uri(uri)] = schema
|
|
9
|
+
schema
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def resolve(uri)
|
|
13
|
+
registry[normalize_uri(uri)]
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
private def registry
|
|
17
|
+
@registry ||= {}
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
private def normalize_uri(uri)
|
|
21
|
+
uri.to_s.delete_suffix("#")
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
private_constant :Internal
|
|
28
|
+
end
|