json_schemer 1.0.3 → 2.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.
Files changed (56) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/ci.yml +1 -6
  3. data/CHANGELOG.md +25 -0
  4. data/Gemfile.lock +1 -1
  5. data/README.md +137 -14
  6. data/json_schemer.gemspec +1 -1
  7. data/lib/json_schemer/draft201909/meta.rb +335 -0
  8. data/lib/json_schemer/draft201909/vocab/applicator.rb +104 -0
  9. data/lib/json_schemer/draft201909/vocab/core.rb +45 -0
  10. data/lib/json_schemer/draft201909/vocab.rb +31 -0
  11. data/lib/json_schemer/draft202012/meta.rb +361 -0
  12. data/lib/json_schemer/draft202012/vocab/applicator.rb +382 -0
  13. data/lib/json_schemer/draft202012/vocab/content.rb +44 -0
  14. data/lib/json_schemer/draft202012/vocab/core.rb +154 -0
  15. data/lib/json_schemer/draft202012/vocab/format_annotation.rb +31 -0
  16. data/lib/json_schemer/draft202012/vocab/format_assertion.rb +29 -0
  17. data/lib/json_schemer/draft202012/vocab/meta_data.rb +30 -0
  18. data/lib/json_schemer/draft202012/vocab/unevaluated.rb +94 -0
  19. data/lib/json_schemer/draft202012/vocab/validation.rb +286 -0
  20. data/lib/json_schemer/draft202012/vocab.rb +103 -0
  21. data/lib/json_schemer/draft4/meta.rb +155 -0
  22. data/lib/json_schemer/draft4/vocab/validation.rb +39 -0
  23. data/lib/json_schemer/draft4/vocab.rb +18 -0
  24. data/lib/json_schemer/draft6/meta.rb +161 -0
  25. data/lib/json_schemer/draft6/vocab.rb +16 -0
  26. data/lib/json_schemer/draft7/meta.rb +178 -0
  27. data/lib/json_schemer/draft7/vocab/validation.rb +69 -0
  28. data/lib/json_schemer/draft7/vocab.rb +30 -0
  29. data/lib/json_schemer/errors.rb +1 -0
  30. data/lib/json_schemer/format/duration.rb +23 -0
  31. data/lib/json_schemer/format/json_pointer.rb +18 -0
  32. data/lib/json_schemer/format.rb +52 -26
  33. data/lib/json_schemer/keyword.rb +41 -0
  34. data/lib/json_schemer/location.rb +25 -0
  35. data/lib/json_schemer/openapi.rb +40 -0
  36. data/lib/json_schemer/openapi30/document.rb +1673 -0
  37. data/lib/json_schemer/openapi30/meta.rb +26 -0
  38. data/lib/json_schemer/openapi30/vocab/base.rb +18 -0
  39. data/lib/json_schemer/openapi30/vocab.rb +12 -0
  40. data/lib/json_schemer/openapi31/document.rb +1559 -0
  41. data/lib/json_schemer/openapi31/meta.rb +128 -0
  42. data/lib/json_schemer/openapi31/vocab/base.rb +89 -0
  43. data/lib/json_schemer/openapi31/vocab.rb +18 -0
  44. data/lib/json_schemer/output.rb +55 -0
  45. data/lib/json_schemer/result.rb +168 -0
  46. data/lib/json_schemer/schema.rb +390 -0
  47. data/lib/json_schemer/version.rb +1 -1
  48. data/lib/json_schemer.rb +197 -24
  49. metadata +42 -10
  50. data/lib/json_schemer/schema/base.rb +0 -677
  51. data/lib/json_schemer/schema/draft4.json +0 -149
  52. data/lib/json_schemer/schema/draft4.rb +0 -44
  53. data/lib/json_schemer/schema/draft6.json +0 -155
  54. data/lib/json_schemer/schema/draft6.rb +0 -25
  55. data/lib/json_schemer/schema/draft7.json +0 -172
  56. data/lib/json_schemer/schema/draft7.rb +0 -32
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+ module JSONSchemer
3
+ module Draft4
4
+ module Vocab
5
+ ALL = Draft6::Vocab::ALL.dup
6
+ ALL.transform_keys! { |key| key == '$id' ? 'id' : key }
7
+ ALL.delete('contains')
8
+ ALL.delete('propertyNames')
9
+ ALL.delete('const')
10
+ ALL.delete('examples')
11
+ ALL.merge!(
12
+ 'type' => Validation::Type,
13
+ 'exclusiveMaximum' => Validation::ExclusiveMaximum,
14
+ 'exclusiveMinimum' => Validation::ExclusiveMinimum
15
+ )
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,161 @@
1
+ # frozen_string_literal: true
2
+ module JSONSchemer
3
+ module Draft6
4
+ BASE_URI = URI('http://json-schema.org/draft-06/schema#')
5
+ SCHEMA = {
6
+ '$schema' => 'http://json-schema.org/draft-06/schema#',
7
+ '$id' => 'http://json-schema.org/draft-06/schema#',
8
+ 'title' => 'Core schema meta-schema',
9
+ 'definitions' => {
10
+ 'schemaArray' => {
11
+ 'type' => 'array',
12
+ 'minItems' => 1,
13
+ 'items' => { '$ref' => '#' }
14
+ },
15
+ 'nonNegativeInteger' => {
16
+ 'type' => 'integer',
17
+ 'minimum' => 0
18
+ },
19
+ 'nonNegativeIntegerDefault0' => {
20
+ 'allOf' => [
21
+ { '$ref' => '#/definitions/nonNegativeInteger' },
22
+ { 'default' => 0 }
23
+ ]
24
+ },
25
+ 'simpleTypes' => {
26
+ 'enum' => [
27
+ 'array',
28
+ 'boolean',
29
+ 'integer',
30
+ 'null',
31
+ 'number',
32
+ 'object',
33
+ 'string'
34
+ ]
35
+ },
36
+ 'stringArray' => {
37
+ 'type' => 'array',
38
+ 'items' => { 'type' => 'string' },
39
+ 'uniqueItems' => true,
40
+ 'default' => []
41
+ }
42
+ },
43
+ 'type' => ['object', 'boolean'],
44
+ 'properties' => {
45
+ '$id' => {
46
+ 'type' => 'string',
47
+ 'format' => 'uri-reference'
48
+ },
49
+ '$schema' => {
50
+ 'type' => 'string',
51
+ 'format' => 'uri'
52
+ },
53
+ '$ref' => {
54
+ 'type' => 'string',
55
+ 'format' => 'uri-reference'
56
+ },
57
+ 'title' => {
58
+ 'type' => 'string'
59
+ },
60
+ 'description' => {
61
+ 'type' => 'string'
62
+ },
63
+ 'default' => {},
64
+ 'examples' => {
65
+ 'type' => 'array',
66
+ 'items' => {}
67
+ },
68
+ 'multipleOf' => {
69
+ 'type' => 'number',
70
+ 'exclusiveMinimum' => 0
71
+ },
72
+ 'maximum' => {
73
+ 'type' => 'number'
74
+ },
75
+ 'exclusiveMaximum' => {
76
+ 'type' => 'number'
77
+ },
78
+ 'minimum' => {
79
+ 'type' => 'number'
80
+ },
81
+ 'exclusiveMinimum' => {
82
+ 'type' => 'number'
83
+ },
84
+ 'maxLength' => { '$ref' => '#/definitions/nonNegativeInteger' },
85
+ 'minLength' => { '$ref' => '#/definitions/nonNegativeIntegerDefault0' },
86
+ 'pattern' => {
87
+ 'type' => 'string',
88
+ 'format' => 'regex'
89
+ },
90
+ 'additionalItems' => { '$ref' => '#' },
91
+ 'items' => {
92
+ 'anyOf' => [
93
+ { '$ref' => '#' },
94
+ { '$ref' => '#/definitions/schemaArray' }
95
+ ],
96
+ 'default' => {}
97
+ },
98
+ 'maxItems' => { '$ref' => '#/definitions/nonNegativeInteger' },
99
+ 'minItems' => { '$ref' => '#/definitions/nonNegativeIntegerDefault0' },
100
+ 'uniqueItems' => {
101
+ 'type' => 'boolean',
102
+ 'default' => false
103
+ },
104
+ 'contains' => { '$ref' => '#' },
105
+ 'maxProperties' => { '$ref' => '#/definitions/nonNegativeInteger' },
106
+ 'minProperties' => { '$ref' => '#/definitions/nonNegativeIntegerDefault0' },
107
+ 'required' => { '$ref' => '#/definitions/stringArray' },
108
+ 'additionalProperties' => { '$ref' => '#' },
109
+ 'definitions' => {
110
+ 'type' => 'object',
111
+ 'additionalProperties' => { '$ref' => '#' },
112
+ 'default' => {}
113
+ },
114
+ 'properties' => {
115
+ 'type' => 'object',
116
+ 'additionalProperties' => { '$ref' => '#' },
117
+ 'default' => {}
118
+ },
119
+ 'patternProperties' => {
120
+ 'type' => 'object',
121
+ 'additionalProperties' => { '$ref' => '#' },
122
+ 'propertyNames' => { 'format' => 'regex' },
123
+ 'default' => {}
124
+ },
125
+ 'dependencies' => {
126
+ 'type' => 'object',
127
+ 'additionalProperties' => {
128
+ 'anyOf' => [
129
+ { '$ref' => '#' },
130
+ { '$ref' => '#/definitions/stringArray' }
131
+ ]
132
+ }
133
+ },
134
+ 'propertyNames' => { '$ref' => '#' },
135
+ 'const' => {},
136
+ 'enum' => {
137
+ 'type' => 'array',
138
+ 'minItems' => 1,
139
+ 'uniqueItems' => true
140
+ },
141
+ 'type' => {
142
+ 'anyOf' => [
143
+ { '$ref' => '#/definitions/simpleTypes' },
144
+ {
145
+ 'type' => 'array',
146
+ 'items' => { '$ref' => '#/definitions/simpleTypes' },
147
+ 'minItems' => 1,
148
+ 'uniqueItems' => true
149
+ }
150
+ ]
151
+ },
152
+ 'format' => { 'type' => 'string' },
153
+ 'allOf' => { '$ref' => '#/definitions/schemaArray' },
154
+ 'anyOf' => { '$ref' => '#/definitions/schemaArray' },
155
+ 'oneOf' => { '$ref' => '#/definitions/schemaArray' },
156
+ 'not' => { '$ref' => '#' }
157
+ },
158
+ 'default' => {}
159
+ }
160
+ end
161
+ end
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+ module JSONSchemer
3
+ module Draft6
4
+ module Vocab
5
+ ALL = Draft7::Vocab::ALL.dup
6
+ ALL.delete('$comment')
7
+ ALL.delete('if')
8
+ ALL.delete('then')
9
+ ALL.delete('else')
10
+ ALL.delete('readOnly')
11
+ ALL.delete('writeOnly')
12
+ ALL.delete('contentMediaType')
13
+ ALL.delete('contentEncoding')
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,178 @@
1
+ # frozen_string_literal: true
2
+ module JSONSchemer
3
+ module Draft7
4
+ BASE_URI = URI('http://json-schema.org/draft-07/schema#')
5
+ SCHEMA = {
6
+ '$schema' => 'http://json-schema.org/draft-07/schema#',
7
+ '$id' => 'http://json-schema.org/draft-07/schema#',
8
+ 'title' => 'Core schema meta-schema',
9
+ 'definitions' => {
10
+ 'schemaArray' => {
11
+ 'type' => 'array',
12
+ 'minItems' => 1,
13
+ 'items' => { '$ref' => '#' }
14
+ },
15
+ 'nonNegativeInteger' => {
16
+ 'type' => 'integer',
17
+ 'minimum' => 0
18
+ },
19
+ 'nonNegativeIntegerDefault0' => {
20
+ 'allOf' => [
21
+ { '$ref' => '#/definitions/nonNegativeInteger' },
22
+ { 'default' => 0 }
23
+ ]
24
+ },
25
+ 'simpleTypes' => {
26
+ 'enum' => [
27
+ 'array',
28
+ 'boolean',
29
+ 'integer',
30
+ 'null',
31
+ 'number',
32
+ 'object',
33
+ 'string'
34
+ ]
35
+ },
36
+ 'stringArray' => {
37
+ 'type' => 'array',
38
+ 'items' => { 'type' => 'string' },
39
+ 'uniqueItems' => true,
40
+ 'default' => []
41
+ }
42
+ },
43
+ 'type' => ['object', 'boolean'],
44
+ 'properties' => {
45
+ '$id' => {
46
+ 'type' => 'string',
47
+ 'format' => 'uri-reference'
48
+ },
49
+ '$schema' => {
50
+ 'type' => 'string',
51
+ 'format' => 'uri'
52
+ },
53
+ '$ref' => {
54
+ 'type' => 'string',
55
+ 'format' => 'uri-reference'
56
+ },
57
+ '$comment' => {
58
+ 'type' => 'string'
59
+ },
60
+ 'title' => {
61
+ 'type' => 'string'
62
+ },
63
+ 'description' => {
64
+ 'type' => 'string'
65
+ },
66
+ 'default' => true,
67
+ 'readOnly' => {
68
+ 'type' => 'boolean',
69
+ 'default' => false
70
+ },
71
+ 'writeOnly' => {
72
+ 'type' => 'boolean',
73
+ 'default' => false
74
+ },
75
+ 'examples' => {
76
+ 'type' => 'array',
77
+ 'items' => true
78
+ },
79
+ 'multipleOf' => {
80
+ 'type' => 'number',
81
+ 'exclusiveMinimum' => 0
82
+ },
83
+ 'maximum' => {
84
+ 'type' => 'number'
85
+ },
86
+ 'exclusiveMaximum' => {
87
+ 'type' => 'number'
88
+ },
89
+ 'minimum' => {
90
+ 'type' => 'number'
91
+ },
92
+ 'exclusiveMinimum' => {
93
+ 'type' => 'number'
94
+ },
95
+ 'maxLength' => { '$ref' => '#/definitions/nonNegativeInteger' },
96
+ 'minLength' => { '$ref' => '#/definitions/nonNegativeIntegerDefault0' },
97
+ 'pattern' => {
98
+ 'type' => 'string',
99
+ 'format' => 'regex'
100
+ },
101
+ 'additionalItems' => { '$ref' => '#' },
102
+ 'items' => {
103
+ 'anyOf' => [
104
+ { '$ref' => '#' },
105
+ { '$ref' => '#/definitions/schemaArray' }
106
+ ],
107
+ 'default' => true
108
+ },
109
+ 'maxItems' => { '$ref' => '#/definitions/nonNegativeInteger' },
110
+ 'minItems' => { '$ref' => '#/definitions/nonNegativeIntegerDefault0' },
111
+ 'uniqueItems' => {
112
+ 'type' => 'boolean',
113
+ 'default' => false
114
+ },
115
+ 'contains' => { '$ref' => '#' },
116
+ 'maxProperties' => { '$ref' => '#/definitions/nonNegativeInteger' },
117
+ 'minProperties' => { '$ref' => '#/definitions/nonNegativeIntegerDefault0' },
118
+ 'required' => { '$ref' => '#/definitions/stringArray' },
119
+ 'additionalProperties' => { '$ref' => '#' },
120
+ 'definitions' => {
121
+ 'type' => 'object',
122
+ 'additionalProperties' => { '$ref' => '#' },
123
+ 'default' => {}
124
+ },
125
+ 'properties' => {
126
+ 'type' => 'object',
127
+ 'additionalProperties' => { '$ref' => '#' },
128
+ 'default' => {}
129
+ },
130
+ 'patternProperties' => {
131
+ 'type' => 'object',
132
+ 'additionalProperties' => { '$ref' => '#' },
133
+ 'propertyNames' => { 'format' => 'regex' },
134
+ 'default' => {}
135
+ },
136
+ 'dependencies' => {
137
+ 'type' => 'object',
138
+ 'additionalProperties' => {
139
+ 'anyOf' => [
140
+ { '$ref' => '#' },
141
+ { '$ref' => '#/definitions/stringArray' }
142
+ ]
143
+ }
144
+ },
145
+ 'propertyNames' => { '$ref' => '#' },
146
+ 'const' => true,
147
+ 'enum' => {
148
+ 'type' => 'array',
149
+ 'items' => true,
150
+ 'minItems' => 1,
151
+ 'uniqueItems' => true
152
+ },
153
+ 'type' => {
154
+ 'anyOf' => [
155
+ { '$ref' => '#/definitions/simpleTypes' },
156
+ {
157
+ 'type' => 'array',
158
+ 'items' => { '$ref' => '#/definitions/simpleTypes' },
159
+ 'minItems' => 1,
160
+ 'uniqueItems' => true
161
+ }
162
+ ]
163
+ },
164
+ 'format' => { 'type' => 'string' },
165
+ 'contentMediaType' => { 'type' => 'string' },
166
+ 'contentEncoding' => { 'type' => 'string' },
167
+ 'if' => { '$ref' => '#' },
168
+ 'then' => { '$ref' => '#' },
169
+ 'else' => { '$ref' => '#' },
170
+ 'allOf' => { '$ref' => '#/definitions/schemaArray' },
171
+ 'anyOf' => { '$ref' => '#/definitions/schemaArray' },
172
+ 'oneOf' => { '$ref' => '#/definitions/schemaArray' },
173
+ 'not' => { '$ref' => '#' }
174
+ },
175
+ 'default' => true
176
+ }
177
+ end
178
+ end
@@ -0,0 +1,69 @@
1
+ # frozen_string_literal: true
2
+ module JSONSchemer
3
+ module Draft7
4
+ module Vocab
5
+ module Validation
6
+ class Ref < Draft202012::Vocab::Core::Ref
7
+ def self.exclusive?
8
+ true
9
+ end
10
+ end
11
+
12
+ class AdditionalItems < Keyword
13
+ def error(formatted_instance_location:, **)
14
+ "array items at #{formatted_instance_location} do not match `additionalItems` schema"
15
+ end
16
+
17
+ def parse
18
+ subschema(value)
19
+ end
20
+
21
+ def validate(instance, instance_location, keyword_location, context)
22
+ items = schema.parsed['items']&.parsed
23
+
24
+ if !instance.is_a?(Array) || !items.is_a?(Array) || items.size >= instance.size
25
+ return result(instance, instance_location, keyword_location, true)
26
+ end
27
+
28
+ offset = items.size
29
+
30
+ nested = instance.slice(offset..-1).map.with_index do |item, index|
31
+ parsed.validate_instance(item, join_location(instance_location, (offset + index).to_s), keyword_location, context)
32
+ end
33
+
34
+ result(instance, instance_location, keyword_location, nested.all?(&:valid), nested, :annotation => nested.any?)
35
+ end
36
+ end
37
+
38
+ class ContentEncoding < Keyword
39
+ def error(formatted_instance_location:, **)
40
+ "string at #{formatted_instance_location} could not be decoded using encoding: #{value}"
41
+ end
42
+
43
+ def validate(instance, instance_location, keyword_location, _context)
44
+ return result(instance, instance_location, keyword_location, true) unless instance.is_a?(String)
45
+
46
+ valid, annotation = Format.decode_content_encoding(instance, value)
47
+
48
+ result(instance, instance_location, keyword_location, valid, :annotation => annotation)
49
+ end
50
+ end
51
+
52
+ class ContentMediaType < Keyword
53
+ def error(formatted_instance_location:, **)
54
+ "string at #{formatted_instance_location} could not be parsed using media type: #{value}"
55
+ end
56
+
57
+ def validate(instance, instance_location, keyword_location, context)
58
+ return result(instance, instance_location, keyword_location, true) unless instance.is_a?(String)
59
+
60
+ decoded_instance = context.adjacent_results[ContentEncoding]&.annotation || instance
61
+ valid, annotation = Format.parse_content_media_type(decoded_instance, value)
62
+
63
+ result(instance, instance_location, keyword_location, valid, :annotation => annotation)
64
+ end
65
+ end
66
+ end
67
+ end
68
+ end
69
+ end
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+ module JSONSchemer
3
+ module Draft7
4
+ module Vocab
5
+ ALL = Draft201909::Vocab::CORE.dup
6
+ ALL.delete('$recursiveAnchor')
7
+ ALL.delete('$recursiveRef')
8
+ ALL.delete('$vocabulary')
9
+ ALL.delete('$anchor')
10
+ ALL.delete('$defs')
11
+ ALL.merge!(Draft201909::Vocab::APPLICATOR)
12
+ ALL.delete('dependentSchemas')
13
+ ALL.delete('unevaluatedItems')
14
+ ALL.delete('unevaluatedProperties')
15
+ ALL.merge!(Draft201909::Vocab::VALIDATION)
16
+ ALL.delete('dependentRequired')
17
+ ALL.delete('maxContains')
18
+ ALL.delete('minContains')
19
+ ALL.merge!(Draft202012::Vocab::FORMAT_ANNOTATION)
20
+ ALL.merge!(Draft201909::Vocab::META_DATA)
21
+ ALL.delete('deprecated')
22
+ ALL.merge!(
23
+ '$ref' => Validation::Ref,
24
+ 'additionalItems' => Validation::AdditionalItems,
25
+ 'contentEncoding' => Validation::ContentEncoding,
26
+ 'contentMediaType' => Validation::ContentMediaType
27
+ )
28
+ end
29
+ end
30
+ end
@@ -1,3 +1,4 @@
1
+ # frozen_string_literal: true
1
2
  # Based on code from @robacarp found in issue 48:
2
3
  # https://github.com/davishmcclurg/json_schemer/issues/48
3
4
  #
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+ module JSONSchemer
3
+ module Format
4
+ module Duration
5
+ # https://datatracker.ietf.org/doc/html/rfc3339#appendix-A
6
+ DUR_SECOND = '\d+S' # dur-second = 1*DIGIT "S"
7
+ DUR_MINUTE = "\\d+M(#{DUR_SECOND})?" # dur-minute = 1*DIGIT "M" [dur-second]
8
+ DUR_HOUR = "\\d+H(#{DUR_MINUTE})?" # dur-hour = 1*DIGIT "H" [dur-minute]
9
+ DUR_TIME = "T(#{DUR_HOUR}|#{DUR_MINUTE}|#{DUR_SECOND})" # dur-time = "T" (dur-hour / dur-minute / dur-second)
10
+ DUR_DAY = '\d+D' # dur-day = 1*DIGIT "D"
11
+ DUR_WEEK = '\d+W' # dur-week = 1*DIGIT "W"
12
+ DUR_MONTH = "\\d+M(#{DUR_DAY})?" # dur-month = 1*DIGIT "M" [dur-day]
13
+ DUR_YEAR = "\\d+Y(#{DUR_MONTH})?" # dur-year = 1*DIGIT "Y" [dur-month]
14
+ DUR_DATE = "(#{DUR_DAY}|#{DUR_MONTH}|#{DUR_YEAR})(#{DUR_TIME})?" # dur-date = (dur-day / dur-month / dur-year) [dur-time]
15
+ DURATION = "P(#{DUR_DATE}|#{DUR_TIME}|#{DUR_WEEK})" # duration = "P" (dur-date / dur-time / dur-week)
16
+ DURATION_REGEX = /\A#{DURATION}\z/
17
+
18
+ def valid_duration?(data)
19
+ DURATION_REGEX.match?(data)
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+ module JSONSchemer
3
+ module Format
4
+ module JSONPointer
5
+ JSON_POINTER_REGEX_STRING = '(\/([^~\/]|~[01])*)*'
6
+ JSON_POINTER_REGEX = /\A#{JSON_POINTER_REGEX_STRING}\z/.freeze
7
+ RELATIVE_JSON_POINTER_REGEX = /\A(0|[1-9]\d*)(#|#{JSON_POINTER_REGEX_STRING})?\z/.freeze
8
+
9
+ def valid_json_pointer?(data)
10
+ JSON_POINTER_REGEX.match?(data)
11
+ end
12
+
13
+ def valid_relative_json_pointer?(data)
14
+ RELATIVE_JSON_POINTER_REGEX.match?(data)
15
+ end
16
+ end
17
+ end
18
+ end
@@ -1,18 +1,58 @@
1
1
  # frozen_string_literal: true
2
2
  module JSONSchemer
3
3
  module Format
4
+ include Duration
4
5
  include Email
5
6
  include Hostname
7
+ include JSONPointer
6
8
  include URITemplate
7
9
 
8
- JSON_POINTER_REGEX_STRING = '(\/([^~\/]|~[01])*)*'
9
- JSON_POINTER_REGEX = /\A#{JSON_POINTER_REGEX_STRING}\z/.freeze
10
- RELATIVE_JSON_POINTER_REGEX = /\A(0|[1-9]\d*)(#|#{JSON_POINTER_REGEX_STRING})?\z/.freeze
11
10
  DATE_TIME_OFFSET_REGEX = /(Z|[\+\-]([01][0-9]|2[0-3]):[0-5][0-9])\z/i.freeze
12
11
  HOUR_24_REGEX = /T24/.freeze
13
12
  LEAP_SECOND_REGEX = /T\d{2}:\d{2}:6/.freeze
14
13
  IP_REGEX = /\A[\h:.]+\z/.freeze
15
14
  INVALID_QUERY_REGEX = /\s/.freeze
15
+ IRI_ESCAPE_REGEX = /[^[:ascii:]]/
16
+ UUID_REGEX = /\A\h{8}-\h{4}-\h{4}-[89AB]\h{3}-\h{12}\z/i
17
+ NIL_UUID = '00000000-0000-0000-0000-000000000000'
18
+ ASCII_8BIT_TO_PERCENT_ENCODED = 256.times.each_with_object({}) do |byte, out|
19
+ out[-byte.chr] = -sprintf('%%%02X', byte)
20
+ end.freeze
21
+
22
+ class << self
23
+ def percent_encode(data, regexp)
24
+ data = data.dup
25
+ data.force_encoding(Encoding::ASCII_8BIT)
26
+ data.gsub!(regexp, ASCII_8BIT_TO_PERCENT_ENCODED)
27
+ data.force_encoding(Encoding::US_ASCII)
28
+ end
29
+
30
+ def decode_content_encoding(data, content_encoding)
31
+ case content_encoding
32
+ when 'base64'
33
+ begin
34
+ [true, Base64.strict_decode64(data)]
35
+ rescue
36
+ [false, nil]
37
+ end
38
+ else
39
+ raise UnknownContentEncoding, content_encoding
40
+ end
41
+ end
42
+
43
+ def parse_content_media_type(data, content_media_type)
44
+ case content_media_type
45
+ when 'application/json'
46
+ begin
47
+ [true, JSON.parse(data)]
48
+ rescue
49
+ [false, nil]
50
+ end
51
+ else
52
+ raise UnknownContentMediaType, content_media_type
53
+ end
54
+ end
55
+ end
16
56
 
17
57
  def valid_spec_format?(data, format)
18
58
  case format
@@ -50,18 +90,15 @@ module JSONSchemer
50
90
  valid_relative_json_pointer?(data)
51
91
  when 'regex'
52
92
  valid_regex?(data)
93
+ when 'duration'
94
+ valid_duration?(data)
95
+ when 'uuid'
96
+ valid_uuid?(data)
53
97
  else
54
98
  raise UnknownFormat, format
55
99
  end
56
100
  end
57
101
 
58
- def valid_json?(data)
59
- JSON.parse(data)
60
- true
61
- rescue JSON::ParserError
62
- false
63
- end
64
-
65
102
  def valid_date_time?(data)
66
103
  return false if HOUR_24_REGEX.match?(data)
67
104
  datetime = DateTime.rfc3339(data)
@@ -99,22 +136,7 @@ module JSONSchemer
99
136
  end
100
137
 
101
138
  def iri_escape(data)
102
- data.gsub(/[^[:ascii:]]/) do |match|
103
- us = match
104
- tmp = +''
105
- us.each_byte do |uc|
106
- tmp << sprintf('%%%02X', uc)
107
- end
108
- tmp
109
- end.force_encoding(Encoding::US_ASCII)
110
- end
111
-
112
- def valid_json_pointer?(data)
113
- JSON_POINTER_REGEX.match?(data)
114
- end
115
-
116
- def valid_relative_json_pointer?(data)
117
- RELATIVE_JSON_POINTER_REGEX.match?(data)
139
+ Format.percent_encode(data, IRI_ESCAPE_REGEX)
118
140
  end
119
141
 
120
142
  def valid_regex?(data)
@@ -122,5 +144,9 @@ module JSONSchemer
122
144
  rescue InvalidEcmaRegexp
123
145
  false
124
146
  end
147
+
148
+ def valid_uuid?(data)
149
+ UUID_REGEX.match?(data) || NIL_UUID == data
150
+ end
125
151
  end
126
152
  end
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+ module JSONSchemer
3
+ class Keyword
4
+ include Output
5
+
6
+ attr_reader :value, :parent, :root, :parsed
7
+
8
+ def initialize(value, parent, keyword, schema = parent)
9
+ @value = value
10
+ @parent = parent
11
+ @root = parent.root
12
+ @keyword = keyword
13
+ @schema = schema
14
+ @parsed = parse
15
+ end
16
+
17
+ def validate(_instance, _instance_location, _keyword_location, _context)
18
+ nil
19
+ end
20
+
21
+ def absolute_keyword_location
22
+ @absolute_keyword_location ||= "#{parent.absolute_keyword_location}/#{fragment_encode(escaped_keyword)}"
23
+ end
24
+
25
+ def schema_pointer
26
+ @schema_pointer ||= "#{parent.schema_pointer}/#{escaped_keyword}"
27
+ end
28
+
29
+ private
30
+
31
+ def parse
32
+ value
33
+ end
34
+
35
+ def subschema(value, keyword = nil, **options)
36
+ options[:base_uri] ||= schema.base_uri
37
+ options[:meta_schema] ||= schema.meta_schema
38
+ Schema.new(value, self, root, keyword, **options)
39
+ end
40
+ end
41
+ end