json_schemer 1.0.3 → 2.1.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 (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