json_schemer 1.0.3 → 2.1.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (57) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/ci.yml +2 -7
  3. data/CHANGELOG.md +51 -0
  4. data/Gemfile.lock +10 -3
  5. data/README.md +328 -14
  6. data/json_schemer.gemspec +3 -1
  7. data/lib/json_schemer/content.rb +18 -0
  8. data/lib/json_schemer/draft201909/meta.rb +320 -0
  9. data/lib/json_schemer/draft201909/vocab/applicator.rb +104 -0
  10. data/lib/json_schemer/draft201909/vocab/core.rb +45 -0
  11. data/lib/json_schemer/draft201909/vocab.rb +31 -0
  12. data/lib/json_schemer/draft202012/meta.rb +364 -0
  13. data/lib/json_schemer/draft202012/vocab/applicator.rb +382 -0
  14. data/lib/json_schemer/draft202012/vocab/content.rb +52 -0
  15. data/lib/json_schemer/draft202012/vocab/core.rb +160 -0
  16. data/lib/json_schemer/draft202012/vocab/format_annotation.rb +23 -0
  17. data/lib/json_schemer/draft202012/vocab/format_assertion.rb +23 -0
  18. data/lib/json_schemer/draft202012/vocab/meta_data.rb +30 -0
  19. data/lib/json_schemer/draft202012/vocab/unevaluated.rb +94 -0
  20. data/lib/json_schemer/draft202012/vocab/validation.rb +286 -0
  21. data/lib/json_schemer/draft202012/vocab.rb +105 -0
  22. data/lib/json_schemer/draft4/meta.rb +161 -0
  23. data/lib/json_schemer/draft4/vocab/validation.rb +39 -0
  24. data/lib/json_schemer/draft4/vocab.rb +18 -0
  25. data/lib/json_schemer/draft6/meta.rb +172 -0
  26. data/lib/json_schemer/draft6/vocab.rb +16 -0
  27. data/lib/json_schemer/draft7/meta.rb +183 -0
  28. data/lib/json_schemer/draft7/vocab/validation.rb +69 -0
  29. data/lib/json_schemer/draft7/vocab.rb +30 -0
  30. data/lib/json_schemer/errors.rb +1 -0
  31. data/lib/json_schemer/format/duration.rb +23 -0
  32. data/lib/json_schemer/format/json_pointer.rb +18 -0
  33. data/lib/json_schemer/format.rb +128 -106
  34. data/lib/json_schemer/keyword.rb +53 -0
  35. data/lib/json_schemer/location.rb +25 -0
  36. data/lib/json_schemer/openapi.rb +40 -0
  37. data/lib/json_schemer/openapi30/document.rb +1672 -0
  38. data/lib/json_schemer/openapi30/meta.rb +32 -0
  39. data/lib/json_schemer/openapi30/vocab/base.rb +18 -0
  40. data/lib/json_schemer/openapi30/vocab.rb +12 -0
  41. data/lib/json_schemer/openapi31/document.rb +1557 -0
  42. data/lib/json_schemer/openapi31/meta.rb +136 -0
  43. data/lib/json_schemer/openapi31/vocab/base.rb +127 -0
  44. data/lib/json_schemer/openapi31/vocab.rb +18 -0
  45. data/lib/json_schemer/output.rb +56 -0
  46. data/lib/json_schemer/result.rb +229 -0
  47. data/lib/json_schemer/schema.rb +423 -0
  48. data/lib/json_schemer/version.rb +1 -1
  49. data/lib/json_schemer.rb +198 -24
  50. metadata +71 -10
  51. data/lib/json_schemer/schema/base.rb +0 -677
  52. data/lib/json_schemer/schema/draft4.json +0 -149
  53. data/lib/json_schemer/schema/draft4.rb +0 -44
  54. data/lib/json_schemer/schema/draft6.json +0 -155
  55. data/lib/json_schemer/schema/draft6.rb +0 -25
  56. data/lib/json_schemer/schema/draft7.json +0 -172
  57. data/lib/json_schemer/schema/draft7.rb +0 -32
@@ -1,126 +1,148 @@
1
1
  # frozen_string_literal: true
2
2
  module JSONSchemer
3
3
  module Format
4
- include Email
5
- include Hostname
6
- include URITemplate
4
+ # https://datatracker.ietf.org/doc/html/draft-bhutton-json-schema-validation-01#section-7.3
5
+ DATE_TIME = proc do |instance, _format|
6
+ !instance.is_a?(String) || valid_date_time?(instance)
7
+ end
8
+ DATE = proc do |instance, _format|
9
+ !instance.is_a?(String) || valid_date_time?("#{instance}T04:05:06.123456789+07:00")
10
+ end
11
+ TIME = proc do |instance, _format|
12
+ !instance.is_a?(String) || valid_date_time?("2001-02-03T#{instance}")
13
+ end
14
+ DURATION = proc do |instance, _format|
15
+ !instance.is_a?(String) || valid_duration?(instance)
16
+ end
17
+ # https://datatracker.ietf.org/doc/html/draft-bhutton-json-schema-validation-01#section-7.3.2
18
+ EMAIL = proc do |instance, _format|
19
+ !instance.is_a?(String) || instance.ascii_only? && valid_email?(instance)
20
+ end
21
+ IDN_EMAIL = proc do |instance, _format|
22
+ !instance.is_a?(String) || valid_email?(instance)
23
+ end
24
+ # https://datatracker.ietf.org/doc/html/draft-bhutton-json-schema-validation-01#section-7.3.3
25
+ HOSTNAME = proc do |instance, _format|
26
+ !instance.is_a?(String) || instance.ascii_only? && valid_hostname?(instance)
27
+ end
28
+ IDN_HOSTNAME = proc do |instance, _format|
29
+ !instance.is_a?(String) || valid_hostname?(instance)
30
+ end
31
+ # https://datatracker.ietf.org/doc/html/draft-bhutton-json-schema-validation-01#section-7.3.4
32
+ IPV4 = proc do |instance, _format|
33
+ !instance.is_a?(String) || valid_ip?(instance, Socket::AF_INET)
34
+ end
35
+ IPV6 = proc do |instance, _format|
36
+ !instance.is_a?(String) || valid_ip?(instance, Socket::AF_INET6)
37
+ end
38
+ # https://datatracker.ietf.org/doc/html/draft-bhutton-json-schema-validation-01#section-7.3.5
39
+ URI = proc do |instance, _format|
40
+ !instance.is_a?(String) || valid_uri?(instance)
41
+ end
42
+ URI_REFERENCE = proc do |instance, _format|
43
+ !instance.is_a?(String) || valid_uri_reference?(instance)
44
+ end
45
+ IRI = proc do |instance, _format|
46
+ !instance.is_a?(String) || valid_uri?(iri_escape(instance))
47
+ end
48
+ IRI_REFERENCE = proc do |instance, _format|
49
+ !instance.is_a?(String) || valid_uri_reference?(iri_escape(instance))
50
+ end
51
+ UUID = proc do |instance, _format|
52
+ !instance.is_a?(String) || valid_uuid?(instance)
53
+ end
54
+ # https://datatracker.ietf.org/doc/html/draft-bhutton-json-schema-validation-01#section-7.3.6
55
+ URI_TEMPLATE = proc do |instance, _format|
56
+ !instance.is_a?(String) || valid_uri_template?(instance)
57
+ end
58
+ # https://datatracker.ietf.org/doc/html/draft-bhutton-json-schema-validation-01#section-7.3.7
59
+ JSON_POINTER = proc do |instance, _format|
60
+ !instance.is_a?(String) || valid_json_pointer?(instance)
61
+ end
62
+ RELATIVE_JSON_POINTER = proc do |instance, _format|
63
+ !instance.is_a?(String) || valid_relative_json_pointer?(instance)
64
+ end
65
+ # https://datatracker.ietf.org/doc/html/draft-bhutton-json-schema-validation-01#section-7.3.8
66
+ REGEX = proc do |instance, _format|
67
+ !instance.is_a?(String) || valid_regex?(instance)
68
+ end
7
69
 
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
70
  DATE_TIME_OFFSET_REGEX = /(Z|[\+\-]([01][0-9]|2[0-3]):[0-5][0-9])\z/i.freeze
12
- HOUR_24_REGEX = /T24/.freeze
13
- LEAP_SECOND_REGEX = /T\d{2}:\d{2}:6/.freeze
71
+ DATE_TIME_SEPARATOR_CHARACTER_CLASS = '[Tt\s]'
72
+ HOUR_24_REGEX = /#{DATE_TIME_SEPARATOR_CHARACTER_CLASS}24:/.freeze
73
+ LEAP_SECOND_REGEX = /#{DATE_TIME_SEPARATOR_CHARACTER_CLASS}\d{2}:\d{2}:6/.freeze
14
74
  IP_REGEX = /\A[\h:.]+\z/.freeze
15
75
  INVALID_QUERY_REGEX = /\s/.freeze
76
+ IRI_ESCAPE_REGEX = /[^[:ascii:]]/
77
+ UUID_REGEX = /\A\h{8}-\h{4}-\h{4}-[89AB]\h{3}-\h{12}\z/i
78
+ NIL_UUID = '00000000-0000-0000-0000-000000000000'
79
+ ASCII_8BIT_TO_PERCENT_ENCODED = 256.times.each_with_object({}) do |byte, out|
80
+ out[-byte.chr] = -sprintf('%%%02X', byte)
81
+ end.freeze
16
82
 
17
- def valid_spec_format?(data, format)
18
- case format
19
- when 'date-time'
20
- valid_date_time?(data)
21
- when 'date'
22
- valid_date_time?("#{data}T04:05:06.123456789+07:00")
23
- when 'time'
24
- valid_date_time?("2001-02-03T#{data}")
25
- when 'email'
26
- data.ascii_only? && valid_email?(data)
27
- when 'idn-email'
28
- valid_email?(data)
29
- when 'hostname'
30
- data.ascii_only? && valid_hostname?(data)
31
- when 'idn-hostname'
32
- valid_hostname?(data)
33
- when 'ipv4'
34
- valid_ip?(data, Socket::AF_INET)
35
- when 'ipv6'
36
- valid_ip?(data, Socket::AF_INET6)
37
- when 'uri'
38
- valid_uri?(data)
39
- when 'uri-reference'
40
- valid_uri_reference?(data)
41
- when 'iri'
42
- valid_uri?(iri_escape(data))
43
- when 'iri-reference'
44
- valid_uri_reference?(iri_escape(data))
45
- when 'uri-template'
46
- valid_uri_template?(data)
47
- when 'json-pointer'
48
- valid_json_pointer?(data)
49
- when 'relative-json-pointer'
50
- valid_relative_json_pointer?(data)
51
- when 'regex'
52
- valid_regex?(data)
53
- else
54
- raise UnknownFormat, format
55
- end
56
- end
57
-
58
- def valid_json?(data)
59
- JSON.parse(data)
60
- true
61
- rescue JSON::ParserError
62
- false
63
- end
83
+ class << self
84
+ include Duration
85
+ include Email
86
+ include Hostname
87
+ include JSONPointer
88
+ include URITemplate
64
89
 
65
- def valid_date_time?(data)
66
- return false if HOUR_24_REGEX.match?(data)
67
- datetime = DateTime.rfc3339(data)
68
- return false if LEAP_SECOND_REGEX.match?(data) && datetime.new_offset.strftime('%H:%M') != '23:59'
69
- DATE_TIME_OFFSET_REGEX.match?(data)
70
- rescue ArgumentError
71
- false
72
- end
90
+ def percent_encode(data, regexp)
91
+ data = data.dup
92
+ data.force_encoding(Encoding::ASCII_8BIT)
93
+ data.gsub!(regexp, ASCII_8BIT_TO_PERCENT_ENCODED)
94
+ data.force_encoding(Encoding::US_ASCII)
95
+ end
73
96
 
74
- def valid_ip?(data, family)
75
- IPAddr.new(data, family)
76
- IP_REGEX.match?(data)
77
- rescue IPAddr::Error
78
- false
79
- end
97
+ def valid_date_time?(data)
98
+ return false if HOUR_24_REGEX.match?(data)
99
+ datetime = DateTime.rfc3339(data)
100
+ return false if LEAP_SECOND_REGEX.match?(data) && datetime.new_offset.strftime('%H:%M') != '23:59'
101
+ DATE_TIME_OFFSET_REGEX.match?(data)
102
+ rescue ArgumentError
103
+ false
104
+ end
80
105
 
81
- def parse_uri_scheme(data)
82
- scheme, _userinfo, _host, _port, _registry, _path, opaque, query, _fragment = URI::RFC3986_PARSER.split(data)
83
- # URI::RFC3986_PARSER.parse allows spaces in these and I don't think it should
84
- raise URI::InvalidURIError if INVALID_QUERY_REGEX.match?(query) || INVALID_QUERY_REGEX.match?(opaque)
85
- scheme
86
- end
106
+ def valid_ip?(data, family)
107
+ IPAddr.new(data, family)
108
+ IP_REGEX.match?(data)
109
+ rescue IPAddr::Error
110
+ false
111
+ end
87
112
 
88
- def valid_uri?(data)
89
- !!parse_uri_scheme(data)
90
- rescue URI::InvalidURIError
91
- false
92
- end
113
+ def parse_uri_scheme(data)
114
+ scheme, _userinfo, _host, _port, _registry, _path, opaque, query, _fragment = ::URI::RFC3986_PARSER.split(data)
115
+ # ::URI::RFC3986_PARSER.parse allows spaces in these and I don't think it should
116
+ raise ::URI::InvalidURIError if INVALID_QUERY_REGEX.match?(query) || INVALID_QUERY_REGEX.match?(opaque)
117
+ scheme
118
+ end
93
119
 
94
- def valid_uri_reference?(data)
95
- parse_uri_scheme(data)
96
- true
97
- rescue URI::InvalidURIError
98
- false
99
- end
120
+ def valid_uri?(data)
121
+ !!parse_uri_scheme(data)
122
+ rescue ::URI::InvalidURIError
123
+ false
124
+ end
100
125
 
101
- 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
126
+ def valid_uri_reference?(data)
127
+ parse_uri_scheme(data)
128
+ true
129
+ rescue ::URI::InvalidURIError
130
+ false
131
+ end
111
132
 
112
- def valid_json_pointer?(data)
113
- JSON_POINTER_REGEX.match?(data)
114
- end
133
+ def iri_escape(data)
134
+ Format.percent_encode(data, IRI_ESCAPE_REGEX)
135
+ end
115
136
 
116
- def valid_relative_json_pointer?(data)
117
- RELATIVE_JSON_POINTER_REGEX.match?(data)
118
- end
137
+ def valid_regex?(data)
138
+ !!EcmaRegexp.ruby_equivalent(data)
139
+ rescue InvalidEcmaRegexp
140
+ false
141
+ end
119
142
 
120
- def valid_regex?(data)
121
- !!EcmaRegexp.ruby_equivalent(data)
122
- rescue InvalidEcmaRegexp
123
- false
143
+ def valid_uuid?(data)
144
+ UUID_REGEX.match?(data) || NIL_UUID == data
145
+ end
124
146
  end
125
147
  end
126
148
  end
@@ -0,0 +1,53 @@
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
+ def error_key
30
+ keyword
31
+ end
32
+
33
+ def fetch(key)
34
+ parsed.fetch(parsed.is_a?(Array) ? key.to_i : key)
35
+ end
36
+
37
+ def parsed_schema
38
+ parsed.is_a?(Schema) ? parsed : nil
39
+ end
40
+
41
+ private
42
+
43
+ def parse
44
+ value
45
+ end
46
+
47
+ def subschema(value, keyword = nil, **options)
48
+ options[:base_uri] ||= schema.base_uri
49
+ options[:meta_schema] ||= schema.meta_schema
50
+ Schema.new(value, self, root, keyword, **options)
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+ module JSONSchemer
3
+ module Location
4
+ JSON_POINTER_TOKEN_ESCAPE_CHARS = { '~' => '~0', '/' => '~1' }
5
+ JSON_POINTER_TOKEN_ESCAPE_REGEX = Regexp.union(JSON_POINTER_TOKEN_ESCAPE_CHARS.keys)
6
+
7
+ class << self
8
+ def root
9
+ {}
10
+ end
11
+
12
+ def join(location, name)
13
+ location[name] ||= { :name => name, :parent => location }
14
+ end
15
+
16
+ def resolve(location)
17
+ location[:resolve] ||= location[:parent] ? "#{resolve(location[:parent])}/#{escape_json_pointer_token(location[:name])}" : ''
18
+ end
19
+
20
+ def escape_json_pointer_token(token)
21
+ token.gsub(JSON_POINTER_TOKEN_ESCAPE_REGEX, JSON_POINTER_TOKEN_ESCAPE_CHARS)
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,40 @@
1
+ # frozen_string_literal: true
2
+ module JSONSchemer
3
+ class OpenAPI
4
+ def initialize(document, **options)
5
+ @document = document
6
+
7
+ version = document['openapi']
8
+ case version
9
+ when /\A3\.1\.\d+\z/
10
+ @document_schema = JSONSchemer.openapi31_document
11
+ json_schema_dialect = document.fetch('jsonSchemaDialect') { OpenAPI31::BASE_URI.to_s }
12
+ when /\A3\.0\.\d+\z/
13
+ @document_schema = JSONSchemer.openapi30_document
14
+ json_schema_dialect = OpenAPI30::BASE_URI.to_s
15
+ else
16
+ raise UnsupportedOpenAPIVersion, version
17
+ end
18
+
19
+ meta_schema = META_SCHEMAS_BY_BASE_URI_STR[json_schema_dialect] || raise(UnsupportedMetaSchema, json_schema_dialect)
20
+
21
+ @schema = JSONSchemer.schema(@document, :meta_schema => meta_schema, **options)
22
+ end
23
+
24
+ def valid?
25
+ @document_schema.valid?(@document)
26
+ end
27
+
28
+ def validate(**options)
29
+ @document_schema.validate(@document, **options)
30
+ end
31
+
32
+ def ref(value)
33
+ @schema.ref(value)
34
+ end
35
+
36
+ def schema(name)
37
+ ref("#/components/schemas/#{name}")
38
+ end
39
+ end
40
+ end