json_schemer 1.0.3 → 2.4.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 (59) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/ci.yml +7 -8
  3. data/CHANGELOG.md +96 -0
  4. data/Gemfile.lock +23 -14
  5. data/README.md +343 -20
  6. data/json_schemer.gemspec +8 -3
  7. data/lib/json_schemer/configuration.rb +31 -0
  8. data/lib/json_schemer/content.rb +18 -0
  9. data/lib/json_schemer/draft201909/meta.rb +320 -0
  10. data/lib/json_schemer/draft201909/vocab/applicator.rb +104 -0
  11. data/lib/json_schemer/draft201909/vocab/core.rb +45 -0
  12. data/lib/json_schemer/draft201909/vocab.rb +31 -0
  13. data/lib/json_schemer/draft202012/meta.rb +364 -0
  14. data/lib/json_schemer/draft202012/vocab/applicator.rb +382 -0
  15. data/lib/json_schemer/draft202012/vocab/content.rb +52 -0
  16. data/lib/json_schemer/draft202012/vocab/core.rb +160 -0
  17. data/lib/json_schemer/draft202012/vocab/format_annotation.rb +23 -0
  18. data/lib/json_schemer/draft202012/vocab/format_assertion.rb +23 -0
  19. data/lib/json_schemer/draft202012/vocab/meta_data.rb +30 -0
  20. data/lib/json_schemer/draft202012/vocab/unevaluated.rb +104 -0
  21. data/lib/json_schemer/draft202012/vocab/validation.rb +290 -0
  22. data/lib/json_schemer/draft202012/vocab.rb +105 -0
  23. data/lib/json_schemer/draft4/meta.rb +161 -0
  24. data/lib/json_schemer/draft4/vocab/validation.rb +38 -0
  25. data/lib/json_schemer/draft4/vocab.rb +18 -0
  26. data/lib/json_schemer/draft6/meta.rb +172 -0
  27. data/lib/json_schemer/draft6/vocab.rb +16 -0
  28. data/lib/json_schemer/draft7/meta.rb +183 -0
  29. data/lib/json_schemer/draft7/vocab/validation.rb +69 -0
  30. data/lib/json_schemer/draft7/vocab.rb +30 -0
  31. data/lib/json_schemer/errors.rb +1 -0
  32. data/lib/json_schemer/format/duration.rb +23 -0
  33. data/lib/json_schemer/format/json_pointer.rb +18 -0
  34. data/lib/json_schemer/format.rb +127 -106
  35. data/lib/json_schemer/keyword.rb +56 -0
  36. data/lib/json_schemer/location.rb +25 -0
  37. data/lib/json_schemer/openapi.rb +38 -0
  38. data/lib/json_schemer/openapi30/document.rb +1672 -0
  39. data/lib/json_schemer/openapi30/meta.rb +34 -0
  40. data/lib/json_schemer/openapi30/vocab/base.rb +18 -0
  41. data/lib/json_schemer/openapi30/vocab.rb +12 -0
  42. data/lib/json_schemer/openapi31/document.rb +1557 -0
  43. data/lib/json_schemer/openapi31/meta.rb +136 -0
  44. data/lib/json_schemer/openapi31/vocab/base.rb +127 -0
  45. data/lib/json_schemer/openapi31/vocab.rb +18 -0
  46. data/lib/json_schemer/output.rb +56 -0
  47. data/lib/json_schemer/resources.rb +24 -0
  48. data/lib/json_schemer/result.rb +242 -0
  49. data/lib/json_schemer/schema.rb +433 -0
  50. data/lib/json_schemer/version.rb +1 -1
  51. data/lib/json_schemer.rb +235 -32
  52. metadata +119 -18
  53. data/lib/json_schemer/schema/base.rb +0 -677
  54. data/lib/json_schemer/schema/draft4.json +0 -149
  55. data/lib/json_schemer/schema/draft4.rb +0 -44
  56. data/lib/json_schemer/schema/draft6.json +0 -155
  57. data/lib/json_schemer/schema/draft6.rb +0 -25
  58. data/lib/json_schemer/schema/draft7.json +0 -172
  59. data/lib/json_schemer/schema/draft7.rb +0 -32
@@ -1,126 +1,147 @@
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}-\h{4}-\h{12}\z/i
78
+ NIL_UUID = '00000000-0000-0000-0000-000000000000'
79
+ BINARY_TO_PERCENT_ENCODED = 256.times.each_with_object({}) do |byte, out|
80
+ out[-byte.chr(Encoding::BINARY)] = -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
+ binary = data.b
92
+ binary.gsub!(regexp, BINARY_TO_PERCENT_ENCODED)
93
+ binary.force_encoding(data.encoding)
94
+ end
73
95
 
74
- def valid_ip?(data, family)
75
- IPAddr.new(data, family)
76
- IP_REGEX.match?(data)
77
- rescue IPAddr::Error
78
- false
79
- end
96
+ def valid_date_time?(data)
97
+ return false if HOUR_24_REGEX.match?(data)
98
+ datetime = DateTime.rfc3339(data)
99
+ return false if LEAP_SECOND_REGEX.match?(data) && datetime.new_offset.strftime('%H:%M') != '23:59'
100
+ DATE_TIME_OFFSET_REGEX.match?(data)
101
+ rescue ArgumentError
102
+ false
103
+ end
80
104
 
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
105
+ def valid_ip?(data, family)
106
+ IPAddr.new(data, family)
107
+ IP_REGEX.match?(data)
108
+ rescue IPAddr::Error
109
+ false
110
+ end
87
111
 
88
- def valid_uri?(data)
89
- !!parse_uri_scheme(data)
90
- rescue URI::InvalidURIError
91
- false
92
- end
112
+ def parse_uri_scheme(data)
113
+ scheme, _userinfo, _host, _port, _registry, _path, opaque, query, _fragment = ::URI::RFC3986_PARSER.split(data)
114
+ # ::URI::RFC3986_PARSER.parse allows spaces in these and I don't think it should
115
+ raise ::URI::InvalidURIError if INVALID_QUERY_REGEX.match?(query) || INVALID_QUERY_REGEX.match?(opaque)
116
+ scheme
117
+ end
93
118
 
94
- def valid_uri_reference?(data)
95
- parse_uri_scheme(data)
96
- true
97
- rescue URI::InvalidURIError
98
- false
99
- end
119
+ def valid_uri?(data)
120
+ !!parse_uri_scheme(data)
121
+ rescue ::URI::InvalidURIError
122
+ false
123
+ end
100
124
 
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
125
+ def valid_uri_reference?(data)
126
+ parse_uri_scheme(data)
127
+ true
128
+ rescue ::URI::InvalidURIError
129
+ false
130
+ end
111
131
 
112
- def valid_json_pointer?(data)
113
- JSON_POINTER_REGEX.match?(data)
114
- end
132
+ def iri_escape(data)
133
+ Format.percent_encode(data, IRI_ESCAPE_REGEX)
134
+ end
115
135
 
116
- def valid_relative_json_pointer?(data)
117
- RELATIVE_JSON_POINTER_REGEX.match?(data)
118
- end
136
+ def valid_regex?(data)
137
+ !!EcmaRegexp.ruby_equivalent(data)
138
+ rescue InvalidEcmaRegexp
139
+ false
140
+ end
119
141
 
120
- def valid_regex?(data)
121
- !!EcmaRegexp.ruby_equivalent(data)
122
- rescue InvalidEcmaRegexp
123
- false
142
+ def valid_uuid?(data)
143
+ UUID_REGEX.match?(data) || NIL_UUID == data
144
+ end
124
145
  end
125
146
  end
126
147
  end
@@ -0,0 +1,56 @@
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[:configuration] ||= schema.configuration
49
+ options[:base_uri] ||= schema.base_uri
50
+ options[:meta_schema] ||= schema.meta_schema
51
+ options[:ref_resolver] ||= schema.ref_resolver
52
+ options[:regexp_resolver] ||= schema.regexp_resolver
53
+ Schema.new(value, self, root, keyword, **options)
54
+ end
55
+ end
56
+ 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,38 @@
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
+ meta_schema = document.fetch('jsonSchemaDialect') { OpenAPI31::BASE_URI.to_s }
12
+ when /\A3\.0\.\d+\z/
13
+ @document_schema = JSONSchemer.openapi30_document
14
+ meta_schema = OpenAPI30::BASE_URI.to_s
15
+ else
16
+ raise UnsupportedOpenAPIVersion, version
17
+ end
18
+
19
+ @schema = JSONSchemer.schema(@document, :meta_schema => meta_schema, **options)
20
+ end
21
+
22
+ def valid?
23
+ @document_schema.valid?(@document)
24
+ end
25
+
26
+ def validate(**options)
27
+ @document_schema.validate(@document, **options)
28
+ end
29
+
30
+ def ref(value)
31
+ @schema.ref(value)
32
+ end
33
+
34
+ def schema(name)
35
+ ref("#/components/schemas/#{name}")
36
+ end
37
+ end
38
+ end