json_schemer 0.2.18 → 2.2.1

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 (64) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/ci.yml +7 -7
  3. data/CHANGELOG.md +89 -0
  4. data/Gemfile.lock +35 -10
  5. data/README.md +402 -6
  6. data/bin/hostname_character_classes +42 -0
  7. data/bin/rake +29 -0
  8. data/exe/json_schemer +62 -0
  9. data/json_schemer.gemspec +9 -12
  10. data/lib/json_schemer/cached_resolver.rb +16 -0
  11. data/lib/json_schemer/configuration.rb +31 -0
  12. data/lib/json_schemer/content.rb +18 -0
  13. data/lib/json_schemer/draft201909/meta.rb +320 -0
  14. data/lib/json_schemer/draft201909/vocab/applicator.rb +104 -0
  15. data/lib/json_schemer/draft201909/vocab/core.rb +45 -0
  16. data/lib/json_schemer/draft201909/vocab.rb +31 -0
  17. data/lib/json_schemer/draft202012/meta.rb +364 -0
  18. data/lib/json_schemer/draft202012/vocab/applicator.rb +382 -0
  19. data/lib/json_schemer/draft202012/vocab/content.rb +52 -0
  20. data/lib/json_schemer/draft202012/vocab/core.rb +160 -0
  21. data/lib/json_schemer/draft202012/vocab/format_annotation.rb +23 -0
  22. data/lib/json_schemer/draft202012/vocab/format_assertion.rb +23 -0
  23. data/lib/json_schemer/draft202012/vocab/meta_data.rb +30 -0
  24. data/lib/json_schemer/draft202012/vocab/unevaluated.rb +104 -0
  25. data/lib/json_schemer/draft202012/vocab/validation.rb +286 -0
  26. data/lib/json_schemer/draft202012/vocab.rb +105 -0
  27. data/lib/json_schemer/draft4/meta.rb +161 -0
  28. data/lib/json_schemer/draft4/vocab/validation.rb +39 -0
  29. data/lib/json_schemer/draft4/vocab.rb +18 -0
  30. data/lib/json_schemer/draft6/meta.rb +172 -0
  31. data/lib/json_schemer/draft6/vocab.rb +16 -0
  32. data/lib/json_schemer/draft7/meta.rb +183 -0
  33. data/lib/json_schemer/draft7/vocab/validation.rb +69 -0
  34. data/lib/json_schemer/draft7/vocab.rb +30 -0
  35. data/lib/json_schemer/ecma_regexp.rb +51 -0
  36. data/lib/json_schemer/errors.rb +1 -0
  37. data/lib/json_schemer/format/duration.rb +23 -0
  38. data/lib/json_schemer/format/email.rb +56 -0
  39. data/lib/json_schemer/format/hostname.rb +58 -0
  40. data/lib/json_schemer/format/json_pointer.rb +18 -0
  41. data/lib/json_schemer/format/uri_template.rb +34 -0
  42. data/lib/json_schemer/format.rb +128 -109
  43. data/lib/json_schemer/keyword.rb +56 -0
  44. data/lib/json_schemer/location.rb +25 -0
  45. data/lib/json_schemer/openapi.rb +38 -0
  46. data/lib/json_schemer/openapi30/document.rb +1672 -0
  47. data/lib/json_schemer/openapi30/meta.rb +32 -0
  48. data/lib/json_schemer/openapi30/vocab/base.rb +18 -0
  49. data/lib/json_schemer/openapi30/vocab.rb +12 -0
  50. data/lib/json_schemer/openapi31/document.rb +1557 -0
  51. data/lib/json_schemer/openapi31/meta.rb +136 -0
  52. data/lib/json_schemer/openapi31/vocab/base.rb +127 -0
  53. data/lib/json_schemer/openapi31/vocab.rb +18 -0
  54. data/lib/json_schemer/output.rb +56 -0
  55. data/lib/json_schemer/result.rb +242 -0
  56. data/lib/json_schemer/schema.rb +424 -0
  57. data/lib/json_schemer/version.rb +1 -1
  58. data/lib/json_schemer.rb +243 -29
  59. metadata +141 -25
  60. data/lib/json_schemer/cached_ref_resolver.rb +0 -14
  61. data/lib/json_schemer/schema/base.rb +0 -658
  62. data/lib/json_schemer/schema/draft4.rb +0 -44
  63. data/lib/json_schemer/schema/draft6.rb +0 -25
  64. data/lib/json_schemer/schema/draft7.rb +0 -32
@@ -0,0 +1,39 @@
1
+ # frozen_string_literal: true
2
+ module JSONSchemer
3
+ module Draft4
4
+ module Vocab
5
+ module Validation
6
+ class Type < Draft202012::Vocab::Validation::Type
7
+ private
8
+ def valid_type(type, instance)
9
+ type == 'integer' ? instance.is_a?(Integer) : super
10
+ end
11
+ end
12
+
13
+ class ExclusiveMaximum < Keyword
14
+ def error(formatted_instance_location:, **)
15
+ "number at #{formatted_instance_location} is greater than or equal to `maximum`"
16
+ end
17
+
18
+ def validate(instance, instance_location, keyword_location, _context)
19
+ maximum = schema.parsed.fetch('maximum').parsed
20
+ valid = !instance.is_a?(Numeric) || !value || !maximum || instance < maximum
21
+ result(instance, instance_location, keyword_location, valid)
22
+ end
23
+ end
24
+
25
+ class ExclusiveMinimum < Keyword
26
+ def error(formatted_instance_location:, **)
27
+ "number at #{formatted_instance_location} is less than or equal to `minimum`"
28
+ end
29
+
30
+ def validate(instance, instance_location, keyword_location, _context)
31
+ minimum = schema.parsed.fetch('minimum').parsed
32
+ valid = !instance.is_a?(Numeric) || !value || !minimum || instance > minimum
33
+ result(instance, instance_location, keyword_location, valid)
34
+ end
35
+ end
36
+ end
37
+ end
38
+ end
39
+ end
@@ -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,172 @@
1
+ # frozen_string_literal: true
2
+ module JSONSchemer
3
+ module Draft6
4
+ BASE_URI = URI('http://json-schema.org/draft-06/schema#')
5
+ FORMATS = Draft7::FORMATS.dup
6
+ FORMATS.delete('date')
7
+ FORMATS.delete('time')
8
+ FORMATS.delete('idn-email')
9
+ FORMATS.delete('idn-hostname')
10
+ FORMATS.delete('iri')
11
+ FORMATS.delete('iri-reference')
12
+ FORMATS.delete('relative-json-pointer')
13
+ FORMATS.delete('regex')
14
+ CONTENT_ENCODINGS = Draft7::CONTENT_ENCODINGS
15
+ CONTENT_MEDIA_TYPES = Draft7::CONTENT_MEDIA_TYPES
16
+ SCHEMA = {
17
+ '$schema' => 'http://json-schema.org/draft-06/schema#',
18
+ '$id' => 'http://json-schema.org/draft-06/schema#',
19
+ 'title' => 'Core schema meta-schema',
20
+ 'definitions' => {
21
+ 'schemaArray' => {
22
+ 'type' => 'array',
23
+ 'minItems' => 1,
24
+ 'items' => { '$ref' => '#' }
25
+ },
26
+ 'nonNegativeInteger' => {
27
+ 'type' => 'integer',
28
+ 'minimum' => 0
29
+ },
30
+ 'nonNegativeIntegerDefault0' => {
31
+ 'allOf' => [
32
+ { '$ref' => '#/definitions/nonNegativeInteger' },
33
+ { 'default' => 0 }
34
+ ]
35
+ },
36
+ 'simpleTypes' => {
37
+ 'enum' => [
38
+ 'array',
39
+ 'boolean',
40
+ 'integer',
41
+ 'null',
42
+ 'number',
43
+ 'object',
44
+ 'string'
45
+ ]
46
+ },
47
+ 'stringArray' => {
48
+ 'type' => 'array',
49
+ 'items' => { 'type' => 'string' },
50
+ 'uniqueItems' => true,
51
+ 'default' => []
52
+ }
53
+ },
54
+ 'type' => ['object', 'boolean'],
55
+ 'properties' => {
56
+ '$id' => {
57
+ 'type' => 'string',
58
+ 'format' => 'uri-reference'
59
+ },
60
+ '$schema' => {
61
+ 'type' => 'string',
62
+ 'format' => 'uri'
63
+ },
64
+ '$ref' => {
65
+ 'type' => 'string',
66
+ 'format' => 'uri-reference'
67
+ },
68
+ 'title' => {
69
+ 'type' => 'string'
70
+ },
71
+ 'description' => {
72
+ 'type' => 'string'
73
+ },
74
+ 'default' => {},
75
+ 'examples' => {
76
+ 'type' => 'array',
77
+ 'items' => {}
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' => {}
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' => {},
147
+ 'enum' => {
148
+ 'type' => 'array',
149
+ 'minItems' => 1,
150
+ 'uniqueItems' => true
151
+ },
152
+ 'type' => {
153
+ 'anyOf' => [
154
+ { '$ref' => '#/definitions/simpleTypes' },
155
+ {
156
+ 'type' => 'array',
157
+ 'items' => { '$ref' => '#/definitions/simpleTypes' },
158
+ 'minItems' => 1,
159
+ 'uniqueItems' => true
160
+ }
161
+ ]
162
+ },
163
+ 'format' => { 'type' => 'string' },
164
+ 'allOf' => { '$ref' => '#/definitions/schemaArray' },
165
+ 'anyOf' => { '$ref' => '#/definitions/schemaArray' },
166
+ 'oneOf' => { '$ref' => '#/definitions/schemaArray' },
167
+ 'not' => { '$ref' => '#' }
168
+ },
169
+ 'default' => {}
170
+ }
171
+ end
172
+ 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,183 @@
1
+ # frozen_string_literal: true
2
+ module JSONSchemer
3
+ module Draft7
4
+ BASE_URI = URI('http://json-schema.org/draft-07/schema#')
5
+ FORMATS = Draft201909::FORMATS.dup
6
+ FORMATS.delete('duration')
7
+ FORMATS.delete('uuid')
8
+ CONTENT_ENCODINGS = Draft201909::CONTENT_ENCODINGS
9
+ CONTENT_MEDIA_TYPES = Draft201909::CONTENT_MEDIA_TYPES
10
+ SCHEMA = {
11
+ '$schema' => 'http://json-schema.org/draft-07/schema#',
12
+ '$id' => 'http://json-schema.org/draft-07/schema#',
13
+ 'title' => 'Core schema meta-schema',
14
+ 'definitions' => {
15
+ 'schemaArray' => {
16
+ 'type' => 'array',
17
+ 'minItems' => 1,
18
+ 'items' => { '$ref' => '#' }
19
+ },
20
+ 'nonNegativeInteger' => {
21
+ 'type' => 'integer',
22
+ 'minimum' => 0
23
+ },
24
+ 'nonNegativeIntegerDefault0' => {
25
+ 'allOf' => [
26
+ { '$ref' => '#/definitions/nonNegativeInteger' },
27
+ { 'default' => 0 }
28
+ ]
29
+ },
30
+ 'simpleTypes' => {
31
+ 'enum' => [
32
+ 'array',
33
+ 'boolean',
34
+ 'integer',
35
+ 'null',
36
+ 'number',
37
+ 'object',
38
+ 'string'
39
+ ]
40
+ },
41
+ 'stringArray' => {
42
+ 'type' => 'array',
43
+ 'items' => { 'type' => 'string' },
44
+ 'uniqueItems' => true,
45
+ 'default' => []
46
+ }
47
+ },
48
+ 'type' => ['object', 'boolean'],
49
+ 'properties' => {
50
+ '$id' => {
51
+ 'type' => 'string',
52
+ 'format' => 'uri-reference'
53
+ },
54
+ '$schema' => {
55
+ 'type' => 'string',
56
+ 'format' => 'uri'
57
+ },
58
+ '$ref' => {
59
+ 'type' => 'string',
60
+ 'format' => 'uri-reference'
61
+ },
62
+ '$comment' => {
63
+ 'type' => 'string'
64
+ },
65
+ 'title' => {
66
+ 'type' => 'string'
67
+ },
68
+ 'description' => {
69
+ 'type' => 'string'
70
+ },
71
+ 'default' => true,
72
+ 'readOnly' => {
73
+ 'type' => 'boolean',
74
+ 'default' => false
75
+ },
76
+ 'writeOnly' => {
77
+ 'type' => 'boolean',
78
+ 'default' => false
79
+ },
80
+ 'examples' => {
81
+ 'type' => 'array',
82
+ 'items' => true
83
+ },
84
+ 'multipleOf' => {
85
+ 'type' => 'number',
86
+ 'exclusiveMinimum' => 0
87
+ },
88
+ 'maximum' => {
89
+ 'type' => 'number'
90
+ },
91
+ 'exclusiveMaximum' => {
92
+ 'type' => 'number'
93
+ },
94
+ 'minimum' => {
95
+ 'type' => 'number'
96
+ },
97
+ 'exclusiveMinimum' => {
98
+ 'type' => 'number'
99
+ },
100
+ 'maxLength' => { '$ref' => '#/definitions/nonNegativeInteger' },
101
+ 'minLength' => { '$ref' => '#/definitions/nonNegativeIntegerDefault0' },
102
+ 'pattern' => {
103
+ 'type' => 'string',
104
+ 'format' => 'regex'
105
+ },
106
+ 'additionalItems' => { '$ref' => '#' },
107
+ 'items' => {
108
+ 'anyOf' => [
109
+ { '$ref' => '#' },
110
+ { '$ref' => '#/definitions/schemaArray' }
111
+ ],
112
+ 'default' => true
113
+ },
114
+ 'maxItems' => { '$ref' => '#/definitions/nonNegativeInteger' },
115
+ 'minItems' => { '$ref' => '#/definitions/nonNegativeIntegerDefault0' },
116
+ 'uniqueItems' => {
117
+ 'type' => 'boolean',
118
+ 'default' => false
119
+ },
120
+ 'contains' => { '$ref' => '#' },
121
+ 'maxProperties' => { '$ref' => '#/definitions/nonNegativeInteger' },
122
+ 'minProperties' => { '$ref' => '#/definitions/nonNegativeIntegerDefault0' },
123
+ 'required' => { '$ref' => '#/definitions/stringArray' },
124
+ 'additionalProperties' => { '$ref' => '#' },
125
+ 'definitions' => {
126
+ 'type' => 'object',
127
+ 'additionalProperties' => { '$ref' => '#' },
128
+ 'default' => {}
129
+ },
130
+ 'properties' => {
131
+ 'type' => 'object',
132
+ 'additionalProperties' => { '$ref' => '#' },
133
+ 'default' => {}
134
+ },
135
+ 'patternProperties' => {
136
+ 'type' => 'object',
137
+ 'additionalProperties' => { '$ref' => '#' },
138
+ 'propertyNames' => { 'format' => 'regex' },
139
+ 'default' => {}
140
+ },
141
+ 'dependencies' => {
142
+ 'type' => 'object',
143
+ 'additionalProperties' => {
144
+ 'anyOf' => [
145
+ { '$ref' => '#' },
146
+ { '$ref' => '#/definitions/stringArray' }
147
+ ]
148
+ }
149
+ },
150
+ 'propertyNames' => { '$ref' => '#' },
151
+ 'const' => true,
152
+ 'enum' => {
153
+ 'type' => 'array',
154
+ 'items' => true,
155
+ 'minItems' => 1,
156
+ 'uniqueItems' => true
157
+ },
158
+ 'type' => {
159
+ 'anyOf' => [
160
+ { '$ref' => '#/definitions/simpleTypes' },
161
+ {
162
+ 'type' => 'array',
163
+ 'items' => { '$ref' => '#/definitions/simpleTypes' },
164
+ 'minItems' => 1,
165
+ 'uniqueItems' => true
166
+ }
167
+ ]
168
+ },
169
+ 'format' => { 'type' => 'string' },
170
+ 'contentMediaType' => { 'type' => 'string' },
171
+ 'contentEncoding' => { 'type' => 'string' },
172
+ 'if' => { '$ref' => '#' },
173
+ 'then' => { '$ref' => '#' },
174
+ 'else' => { '$ref' => '#' },
175
+ 'allOf' => { '$ref' => '#/definitions/schemaArray' },
176
+ 'anyOf' => { '$ref' => '#/definitions/schemaArray' },
177
+ 'oneOf' => { '$ref' => '#/definitions/schemaArray' },
178
+ 'not' => { '$ref' => '#' }
179
+ },
180
+ 'default' => true
181
+ }
182
+ end
183
+ 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 < Draft202012::Vocab::Content::ContentEncoding
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 = parsed.call(instance)
47
+
48
+ result(instance, instance_location, keyword_location, valid, :annotation => annotation)
49
+ end
50
+ end
51
+
52
+ class ContentMediaType < Draft202012::Vocab::Content::ContentMediaType
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 = parsed.call(decoded_instance)
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
@@ -0,0 +1,51 @@
1
+ # frozen_string_literal: true
2
+ module JSONSchemer
3
+ class EcmaRegexp
4
+ class Syntax < Regexp::Syntax::Base
5
+ # regexp_parser >= 2.3.0 uses syntax classes directly instead of instances
6
+ # :nocov:
7
+ SYNTAX = respond_to?(:implements) ? self : new
8
+ # :nocov:
9
+ SYNTAX.implements :anchor, Anchor::Extended
10
+ SYNTAX.implements :assertion, Assertion::All
11
+ # literal %i[number] to support regexp_parser < 2.2.0 (Backreference::Plain)
12
+ SYNTAX.implements :backref, %i[number] + Backreference::Name
13
+ # :meta_sequence, :bell, and :escape are not supported in ecma
14
+ SYNTAX.implements :escape, Escape::Basic + (Escape::Control - %i[meta_sequence]) + (Escape::ASCII - %i[bell escape]) + Escape::Unicode + Escape::Meta + Escape::Hex + Escape::Octal
15
+ SYNTAX.implements :property, UnicodeProperty::All
16
+ SYNTAX.implements :nonproperty, UnicodeProperty::All
17
+ # :comment is not supported in ecma
18
+ SYNTAX.implements :free_space, (FreeSpace::All - %i[comment])
19
+ SYNTAX.implements :group, Group::Basic + Group::Named + Group::Passive
20
+ SYNTAX.implements :literal, Literal::All
21
+ SYNTAX.implements :meta, Meta::Extended
22
+ SYNTAX.implements :quantifier, Quantifier::Greedy + Quantifier::Reluctant + Quantifier::Interval + Quantifier::IntervalReluctant
23
+ SYNTAX.implements :set, CharacterSet::Basic
24
+ SYNTAX.implements :type, CharacterType::Extended
25
+ end
26
+
27
+ RUBY_EQUIVALENTS = {
28
+ :anchor => {
29
+ :bol => '\A',
30
+ :eol => '\z'
31
+ },
32
+ :type => {
33
+ :space => '[\t\r\n\f\v\uFEFF\u2029\p{Zs}]',
34
+ :nonspace => '[^\t\r\n\f\v\uFEFF\u2029\p{Zs}]'
35
+ }
36
+ }.freeze
37
+
38
+ class << self
39
+ def ruby_equivalent(pattern)
40
+ Regexp::Scanner.scan(pattern).map do |type, token, text|
41
+ Syntax::SYNTAX.check!(*Syntax::SYNTAX.normalize(type, token))
42
+ RUBY_EQUIVALENTS.dig(type, token) || text
43
+ rescue Regexp::Syntax::NotImplementedError
44
+ raise InvalidEcmaRegexp, "invalid token #{text.inspect} (#{type}:#{token}) in #{pattern.inspect}"
45
+ end.join
46
+ rescue Regexp::Scanner::ScannerError
47
+ raise InvalidEcmaRegexp, "invalid pattern #{pattern.inspect}"
48
+ end
49
+ end
50
+ end
51
+ 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,56 @@
1
+ # frozen_string_literal: true
2
+ module JSONSchemer
3
+ module Format
4
+ module Email
5
+ # https://datatracker.ietf.org/doc/html/rfc6531#section-3.3
6
+ # I think this is the same as "UTF8-non-ascii"? (https://datatracker.ietf.org/doc/html/rfc6532#section-3.1)
7
+ UTF8_NON_ASCII = '[^[:ascii:]]'
8
+ # https://datatracker.ietf.org/doc/html/rfc5321#section-4.1.2
9
+ A_TEXT = "([\\w!#$%&'*+\\-/=?\\^`{|}~]|#{UTF8_NON_ASCII})" # atext = ALPHA / DIGIT / ; Printable US-ASCII
10
+ # "!" / "#" / ; characters not including
11
+ # "$" / "%" / ; specials. Used for atoms.
12
+ # "&" / "'" /
13
+ # "*" / "+" /
14
+ # "-" / "/" /
15
+ # "=" / "?" /
16
+ # "^" / "_" /
17
+ # "`" / "{" /
18
+ # "|" / "}" /
19
+ # "~"
20
+ Q_TEXT_SMTP = "([\\x20-\\x21\\x23-\\x5B\\x5D-\\x7E]|#{UTF8_NON_ASCII})" # qtextSMTP = %d32-33 / %d35-91 / %d93-126
21
+ # ; i.e., within a quoted string, any
22
+ # ; ASCII graphic or space is permitted
23
+ # ; without blackslash-quoting except
24
+ # ; double-quote and the backslash itself.
25
+ QUOTED_PAIR_SMTP = '\x5C[\x20-\x7E]' # quoted-pairSMTP = %d92 %d32-126
26
+ # ; i.e., backslash followed by any ASCII
27
+ # ; graphic (including itself) or SPace
28
+ Q_CONTENT_SMTP = "#{Q_TEXT_SMTP}|#{QUOTED_PAIR_SMTP}" # QcontentSMTP = qtextSMTP / quoted-pairSMTP
29
+ QUOTED_STRING = "\"(#{Q_CONTENT_SMTP})*\"" # Quoted-string = DQUOTE *QcontentSMTP DQUOTE
30
+ ATOM = "#{A_TEXT}+" # Atom = 1*atext
31
+ DOT_STRING = "#{ATOM}(\\.#{ATOM})*" # Dot-string = Atom *("." Atom)
32
+ LOCAL_PART = "#{DOT_STRING}|#{QUOTED_STRING}" # Local-part = Dot-string / Quoted-string
33
+ # ; MAY be case-sensitive
34
+ # IPv4-address-literal = Snum 3("." Snum)
35
+ # using `valid_id?` to check ip addresses because it's complicated. # IPv6-address-literal = "IPv6:" IPv6-addr
36
+ ADDRESS_LITERAL = '\[(IPv6:(?<ipv6>[\h:]+)|(?<ipv4>[\d.]+))\]' # address-literal = "[" ( IPv4-address-literal /
37
+ # IPv6-address-literal /
38
+ # General-address-literal ) "]"
39
+ # ; See Section 4.1.3
40
+ # using `valid_hostname?` to check domain because it's complicated
41
+ MAILBOX = "(#{LOCAL_PART})@(#{ADDRESS_LITERAL}|(?<domain>.+))" # Mailbox = Local-part "@" ( Domain / address-literal )
42
+ EMAIL_REGEX = /\A#{MAILBOX}\z/
43
+
44
+ def valid_email?(data)
45
+ return false unless match = EMAIL_REGEX.match(data)
46
+ if ipv4 = match.named_captures.fetch('ipv4')
47
+ valid_ip?(ipv4, Socket::AF_INET)
48
+ elsif ipv6 = match.named_captures.fetch('ipv6')
49
+ valid_ip?(ipv6, Socket::AF_INET6)
50
+ else
51
+ valid_hostname?(match.named_captures.fetch('domain'))
52
+ end
53
+ end
54
+ end
55
+ end
56
+ end