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.
@@ -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