jtd 0.1.1 → 0.1.6

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 38fab09375c0357e0c329bca34011de7f04eb7da7af137eea1c986534b47b2c5
4
- data.tar.gz: b99fee5bda367e455c45b530ae828ed429d7b6fde00348c246d555139c42bab1
3
+ metadata.gz: 48c509443ff1c585b82b2dad7e6177cb427b1109fb5f4561b8d13f5aea367732
4
+ data.tar.gz: ed30da3978bda75cdac5a726ccaec03516035e7b352f3d3a923719c934ba227f
5
5
  SHA512:
6
- metadata.gz: c0da77011abe60b2fdd3ee667c057a7218c8494342063202ff11489417fd04a2fe104126d0c5e96f984b95e0ed86453fdd3df24355e0efe372d6efe09d293941
7
- data.tar.gz: 98c043d9b4bcd4943b5e11d8767f02125fb7754ec026d782210a937a0b0a60232e376d87e722f7b5045fe91965bda21eedd631b5877c77bd4b2830f90b822696
6
+ metadata.gz: 211758c89db305e4e6b1fdafdb27043f0cee0c233bd14e358eeef901a76d1c06a732404e4d13ff4e00220b23d4ee577ae10f17c61ac167ff38a5910e51002245
7
+ data.tar.gz: 90bde12a921a518fe982cfc9fce5b2dd2610b2165bd891e1c450ef2dbc4b6094d32107bdc0f40621168c09f3a11814360d9659c6ed20c28acce093f6fcc853a7
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- jtd (0.1.1)
4
+ jtd (0.1.6)
5
5
 
6
6
  GEM
7
7
  remote: https://rubygems.org/
data/README.md CHANGED
@@ -1 +1,184 @@
1
1
  # jtd: JSON Validation for Ruby
2
+
3
+ [![Gem](https://img.shields.io/gem/v/jtd)](https://rubygems.org/gems/jtd)
4
+
5
+ `jtd` is a Ruby implementation of [JSON Type Definition][jtd], a schema language
6
+ for JSON. `jtd` primarily gives you two things:
7
+
8
+ 1. Validating input data against JSON Typedef schemas.
9
+ 2. A Ruby representation of JSON Typedef schemas.
10
+
11
+ With this package, you can add JSON Typedef-powered validation to your
12
+ application, or you can build your own tooling on top of JSON Type Definition.
13
+
14
+ ## Installation
15
+
16
+ You can install this package with `gem`:
17
+
18
+ ```bash
19
+ gem install jtd
20
+ ```
21
+
22
+ ## Documentation
23
+
24
+ Detailed API documentation is available online at:
25
+
26
+ https://rubydoc.info/gems/jtd/JTD
27
+
28
+ For more high-level documentation about JSON Typedef in general, or JSON Typedef
29
+ in combination with Python in particular, see:
30
+
31
+ * [The JSON Typedef Website][jtd]
32
+ * ["Validating JSON in Ruby with JSON Typedef"][jtd-ruby-validation]
33
+
34
+ ## Basic Usage
35
+
36
+ > For a more detailed tutorial and guidance on how to integrate `jtd` in your
37
+ > application, see ["Validating JSON in Ruby with JSON
38
+ > Typedef"][jtd-ruby-validation] in the JSON Typedef docs.
39
+
40
+ Here's an example of how you can use this package to validate JSON data against
41
+ a JSON Typedef schema:
42
+
43
+ ```ruby
44
+ require 'jtd'
45
+
46
+ schema = JTD::Schema.from_hash({
47
+ 'properties' => {
48
+ 'name' => { 'type' => 'string' },
49
+ 'age' => { 'type' => 'uint32' },
50
+ 'phones' => {
51
+ 'elements' => {
52
+ 'type' => 'string'
53
+ }
54
+ }
55
+ }
56
+ })
57
+
58
+ # JTD::validate returns an array of validation errors. If there were no problems
59
+ # with the input, it returns an empty array.
60
+
61
+ # Outputs: []
62
+ p JTD::validate(schema, {
63
+ 'name' => 'John Doe',
64
+ 'age' => 43,
65
+ 'phones' => ['+44 1234567', '+44 2345678'],
66
+ })
67
+
68
+ # This next input has three problems with it:
69
+ #
70
+ # 1. It's missing "name", which is a required property.
71
+ # 2. "age" is a string, but it should be an integer.
72
+ # 3. "phones[1]" is a number, but it should be a string.
73
+ #
74
+ # Each of those errors corresponds to one of the errors returned by validate.
75
+
76
+ # Outputs:
77
+ #
78
+ # [
79
+ # #<struct JTD::ValidationError
80
+ # instance_path=[],
81
+ # schema_path=["properties", "name"]
82
+ # >,
83
+ # #<struct JTD::ValidationError
84
+ # instance_path=["age"],
85
+ # schema_path=["properties", "age", "type"]
86
+ # >,
87
+ # #<struct JTD::ValidationError
88
+ # instance_path=["phones", "1"],
89
+ # schema_path=["properties", "phones", "elements", "type"]
90
+ # >
91
+ # ]
92
+ p JTD::validate(schema, {
93
+ 'age' => '43',
94
+ 'phones' => ['+44 1234567', 442345678],
95
+ })
96
+ ```
97
+
98
+ ## Advanced Usage: Limiting Errors Returned
99
+
100
+ By default, `JTD::validate` returns every error it finds. If you just care about
101
+ whether there are any errors at all, or if you can't show more than some number
102
+ of errors, then you can get better performance out of `JTD::validate` using the
103
+ `max_errors` option.
104
+
105
+ For example, taking the same example from before, but limiting it to 1 error, we
106
+ get:
107
+
108
+ ```python
109
+ # Outputs:
110
+ #
111
+ # [#<struct JTD::ValidationError instance_path=[], schema_path=["properties", "name"]>]
112
+ options = JTD::ValidationOptions.new(max_errors: 1)
113
+ p JTD::validate(schema, {
114
+ 'age' => '43',
115
+ 'phones' => ['+44 1234567', 442345678],
116
+ }, options)
117
+ ```
118
+
119
+ ## Advanced Usage: Handling Untrusted Schemas
120
+
121
+ If you want to run `jtd` against a schema that you don't trust, then you should:
122
+
123
+ 1. Ensure the schema is well-formed, using the `#verify` method on
124
+ `JTD::Schema`. That will check things like making sure all `ref`s have
125
+ corresponding definitions.
126
+
127
+ 2. Call `JTD::validate` with the `max_depth` option. JSON Typedef lets you write
128
+ recursive schemas -- if you're evaluating against untrusted schemas, you
129
+ might go into an infinite loop when evaluating against a malicious input,
130
+ such as this one:
131
+
132
+ ```json
133
+ {
134
+ "ref": "loop",
135
+ "definitions": {
136
+ "loop": {
137
+ "ref": "loop"
138
+ }
139
+ }
140
+ }
141
+ ```
142
+
143
+ The `max_depth` option tells `JTD::validate` how many `ref`s to follow
144
+ recursively before giving up and raising `JTD::MaxDepthExceededError`.
145
+
146
+ Here's an example of how you can use `jtd` to evaluate data against an untrusted
147
+ schema:
148
+
149
+ ```ruby
150
+ require 'jtd'
151
+
152
+ # validate_untrusted returns true if `data` satisfies `schema`, and false if it
153
+ # does not. Throws an error if `schema` is invalid, or if validation goes in an
154
+ # infinite loop.
155
+ def validate_untrusted(schema, data)
156
+ schema.verify()
157
+
158
+ # You should tune max_depth to be high enough that most legitimate schemas
159
+ # evaluate without errors, but low enough that an attacker cannot cause a
160
+ # denial of service attack.
161
+ options = JTD::ValidationOptions.new(max_depth: 32)
162
+ JTD::validate(schema, data, options).empty?
163
+ end
164
+
165
+ # Returns true
166
+ validate_untrusted(JTD::Schema.from_hash({ 'type' => 'string' }), 'foo')
167
+
168
+ # Returns false
169
+ validate_untrusted(JTD::Schema.from_hash({ 'type' => 'string' }), nil)
170
+
171
+ # Raises ArgumentError (invalid type: nonsense)
172
+ validate_untrusted(JTD::Schema.from_hash({ 'type' => 'nonsense' }), 'foo')
173
+
174
+ # Raises JTD::MaxDepthExceededError (max depth exceeded during JTD::validate)
175
+ validate_untrusted(JTD::Schema.from_hash({
176
+ 'definitions' => {
177
+ 'loop' => { 'ref' => 'loop' },
178
+ },
179
+ 'ref' => 'loop',
180
+ }), nil)
181
+ ```
182
+
183
+ [jtd]: https://jsontypedef.com
184
+ [jtd-ruby-validation]: https://jsontypedef.com/docs/ruby/validation
data/lib/jtd.rb CHANGED
@@ -1,6 +1,7 @@
1
- require "jtd/version"
1
+ require 'jtd/schema'
2
+ require 'jtd/validate'
3
+ require 'jtd/version'
2
4
 
5
+ # JTD is an implementation of JSON Type Definition validation for Ruby.
3
6
  module JTD
4
- class Error < StandardError; end
5
- # Your code goes here...
6
7
  end
@@ -0,0 +1,291 @@
1
+ module JTD
2
+ # Represents a JSON Type Definition schema.
3
+ class Schema
4
+ attr_accessor *%i[
5
+ metadata
6
+ nullable
7
+ definitions
8
+ ref
9
+ type
10
+ enum
11
+ elements
12
+ properties
13
+ optional_properties
14
+ additional_properties
15
+ values
16
+ discriminator
17
+ mapping
18
+ ]
19
+
20
+ # Constructs a Schema from a Hash like the kind produced by JSON#parse.
21
+ #
22
+ # In other words, #from_hash is meant to be used to convert some parsed JSON
23
+ # into a Schema.
24
+ #
25
+ # If hash isn't a Hash or contains keys that are illegal for JSON Type
26
+ # Definition, then #from_hash will raise a TypeError.
27
+ #
28
+ # If the properties of hash are not of the correct type for a JSON Type
29
+ # Definition schema (for example, if the "elements" property of hash is
30
+ # non-nil, but not a hash), then #from_hash may raise a NoMethodError.
31
+ def self.from_hash(hash)
32
+ # Raising this error early makes for a much clearer error for the
33
+ # relatively common case of something that was expected to be an object
34
+ # (Hash), but was something else instead.
35
+ raise TypeError.new("expected hash, got: #{hash}") unless hash.is_a?(Hash)
36
+
37
+ illegal_keywords = hash.keys - KEYWORDS
38
+ unless illegal_keywords.empty?
39
+ raise TypeError.new("illegal schema keywords: #{illegal_keywords}")
40
+ end
41
+
42
+ s = Schema.new
43
+
44
+ if hash['metadata']
45
+ s.metadata = hash['metadata']
46
+ end
47
+
48
+ unless hash['nullable'].nil?
49
+ s.nullable = hash['nullable']
50
+ end
51
+
52
+ if hash['definitions']
53
+ s.definitions = Hash[hash['definitions'].map { |k, v| [k, from_hash(v) ]}]
54
+ end
55
+
56
+ s.ref = hash['ref']
57
+ s.type = hash['type']
58
+ s.enum = hash['enum']
59
+
60
+ if hash['elements']
61
+ s.elements = from_hash(hash['elements'])
62
+ end
63
+
64
+ if hash['properties']
65
+ s.properties = Hash[hash['properties'].map { |k, v| [k, from_hash(v) ]}]
66
+ end
67
+
68
+ if hash['optionalProperties']
69
+ s.optional_properties = Hash[hash['optionalProperties'].map { |k, v| [k, from_hash(v) ]}]
70
+ end
71
+
72
+ unless hash['additionalProperties'].nil?
73
+ s.additional_properties = hash['additionalProperties']
74
+ end
75
+
76
+ if hash['values']
77
+ s.values = from_hash(hash['values'])
78
+ end
79
+
80
+ s.discriminator = hash['discriminator']
81
+
82
+ if hash['mapping']
83
+ s.mapping = Hash[hash['mapping'].map { |k, v| [k, from_hash(v) ]}]
84
+ end
85
+
86
+ s
87
+ end
88
+
89
+ # Raises a TypeError or ArgumentError if the Schema is not correct according
90
+ # to the JSON Type Definition specification.
91
+ #
92
+ # See the JSON Type Definition specification for more details, but a high
93
+ # level #verify checks such things as:
94
+ #
95
+ # 1. Making sure each of the attributes of the Schema are of the right type,
96
+ # 2. The Schema uses a valid combination of JSON Type Definition keywords,
97
+ # 3. The Schema isn't ambiguous or unsatisfiable.
98
+ # 4. The Schema doesn't make references to nonexistent definitions.
99
+ #
100
+ # If root is specified, then that root is assumed to contain the schema
101
+ # being verified. By default, the Schema is considered its own root, which
102
+ # is usually the desired behavior.
103
+ def verify(root = self)
104
+ self.check_type('metadata', [Hash])
105
+ self.check_type('nullable', [TrueClass, FalseClass])
106
+ self.check_type('definitions', [Hash])
107
+ self.check_type('ref', [String])
108
+ self.check_type('type', [String])
109
+ self.check_type('enum', [Array])
110
+ self.check_type('elements', [Schema])
111
+ self.check_type('properties', [Hash])
112
+ self.check_type('optional_properties', [Hash])
113
+ self.check_type('additional_properties', [TrueClass, FalseClass])
114
+ self.check_type('values', [Schema])
115
+ self.check_type('discriminator', [String])
116
+ self.check_type('mapping', [Hash])
117
+
118
+ form_signature = [
119
+ !!ref,
120
+ !!type,
121
+ !!enum,
122
+ !!elements,
123
+ !!properties,
124
+ !!optional_properties,
125
+ !!additional_properties,
126
+ !!values,
127
+ !!discriminator,
128
+ !!mapping,
129
+ ]
130
+
131
+ unless VALID_FORMS.include?(form_signature)
132
+ raise ArgumentError.new("invalid schema form: #{self}")
133
+ end
134
+
135
+ if root != self && definitions && definitions.any?
136
+ raise ArgumentError.new("non-root definitions: #{definitions}")
137
+ end
138
+
139
+ if ref
140
+ if !root.definitions || !root.definitions.key?(ref)
141
+ raise ArgumentError.new("ref to non-existent definition: #{ref}")
142
+ end
143
+ end
144
+
145
+ if type && !TYPES.include?(type)
146
+ raise ArgumentError.new("invalid type: #{type}")
147
+ end
148
+
149
+ if enum
150
+ if enum.empty?
151
+ raise ArgumentError.new("enum must not be empty: #{self}")
152
+ end
153
+
154
+ if enum.any? { |v| !v.is_a?(String) }
155
+ raise ArgumentError.new("enum must contain only strings: #{enum}")
156
+ end
157
+
158
+ if enum.size != enum.uniq.size
159
+ raise ArgumentError.new("enum must not contain duplicates: #{enum}")
160
+ end
161
+ end
162
+
163
+ if properties && optional_properties
164
+ shared_keys = properties.keys & optional_properties.keys
165
+ if shared_keys.any?
166
+ raise ArgumentError.new("properties and optional_properties share keys: #{shared_keys}")
167
+ end
168
+ end
169
+
170
+ if mapping
171
+ mapping.values.each do |s|
172
+ if s.form != :properties
173
+ raise ArgumentError.new("mapping values must be of properties form: #{s}")
174
+ end
175
+
176
+ if s.nullable
177
+ raise ArgumentError.new("mapping values must not be nullable: #{s}")
178
+ end
179
+
180
+ contains_discriminator = ArgumentError.new("mapping values must not contain discriminator (#{discriminator}): #{s}")
181
+
182
+ if s.properties && s.properties.key?(discriminator)
183
+ raise contains_discriminator
184
+ end
185
+
186
+ if s.optional_properties && s.optional_properties.key?(discriminator)
187
+ raise contains_discriminator
188
+ end
189
+ end
190
+ end
191
+
192
+ definitions.values.each { |s| s.verify(root) } if definitions
193
+ elements.verify(root) if elements
194
+ properties.values.each { |s| s.verify(root) } if properties
195
+ optional_properties.values.each { |s| s.verify(root) } if optional_properties
196
+ values.verify(root) if values
197
+ mapping.values.each { |s| s.verify(root) } if mapping
198
+
199
+ self
200
+ end
201
+
202
+ # Returns the form that the schema takes on.
203
+ #
204
+ # The return value will be one of :empty, :ref:, :type, :enum, :elements,
205
+ # :properties, :values, or :discriminator.
206
+ #
207
+ # If the schema is not well-formed, i.e. calling #verify on it raises an
208
+ # error, then the return value of #form is not well-defined.
209
+ def form
210
+ return :ref if ref
211
+ return :type if type
212
+ return :enum if enum
213
+ return :elements if elements
214
+ return :properties if properties || optional_properties
215
+ return :values if values
216
+ return :discriminator if discriminator
217
+
218
+ :empty
219
+ end
220
+
221
+ private
222
+
223
+ KEYWORDS = %w[
224
+ metadata
225
+ nullable
226
+ definitions
227
+ ref
228
+ type
229
+ enum
230
+ elements
231
+ properties
232
+ optionalProperties
233
+ additionalProperties
234
+ values
235
+ discriminator
236
+ mapping
237
+ ]
238
+
239
+ private_constant :KEYWORDS
240
+
241
+ TYPES = %w[
242
+ boolean
243
+ int8
244
+ uint8
245
+ int16
246
+ uint16
247
+ int32
248
+ uint32
249
+ float32
250
+ float64
251
+ string
252
+ timestamp
253
+ ]
254
+
255
+ private_constant :TYPES
256
+
257
+ VALID_FORMS = [
258
+ # Empty form
259
+ [false, false, false, false, false, false, false, false, false, false],
260
+ # Ref form
261
+ [true, false, false, false, false, false, false, false, false, false],
262
+ # Type form
263
+ [false, true, false, false, false, false, false, false, false, false],
264
+ # Enum form
265
+ [false, false, true, false, false, false, false, false, false, false],
266
+ # Elements form
267
+ [false, false, false, true, false, false, false, false, false, false],
268
+ # Properties form -- properties or optional properties or both, and
269
+ # never additional properties on its own
270
+ [false, false, false, false, true, false, false, false, false, false],
271
+ [false, false, false, false, false, true, false, false, false, false],
272
+ [false, false, false, false, true, true, false, false, false, false],
273
+ [false, false, false, false, true, false, true, false, false, false],
274
+ [false, false, false, false, false, true, true, false, false, false],
275
+ [false, false, false, false, true, true, true, false, false, false],
276
+ # Values form
277
+ [false, false, false, false, false, false, false, true, false, false],
278
+ # Discriminator form
279
+ [false, false, false, false, false, false, false, false, true, true],
280
+ ]
281
+
282
+ private_constant :VALID_FORMS
283
+
284
+ def check_type(key, classes)
285
+ val = self.send(key)
286
+ unless val.nil? || classes.include?(val.class)
287
+ raise TypeError.new("#{key} must be one of #{classes}, got: #{val}")
288
+ end
289
+ end
290
+ end
291
+ end
@@ -0,0 +1,363 @@
1
+ require 'time'
2
+
3
+ module JTD
4
+ # Validates +instance+ against +schema+ according to the JSON Type Definition
5
+ # specification.
6
+ #
7
+ # Returns a list of ValidationError. If there are no validation errors, then
8
+ # the returned list will be empty.
9
+ #
10
+ # By default, all errors are returned, and an unlimited number of references
11
+ # will be followed. If you are running #validate against schemas that may
12
+ # return a lot of errors, or which may contain circular references, then this
13
+ # can cause performance issues or stack overflows.
14
+ #
15
+ # To mitigate this risk, consider using +options+, which must be an instance
16
+ # of ValidationOptions, to limit the number of errors returned or references
17
+ # followed.
18
+ #
19
+ # If ValidationOptions#max_depth is reached, then #validate will raise a
20
+ # MaxDepthExceededError.
21
+ #
22
+ # The return value of #validate is not well-defined if the schema is not
23
+ # valid, i.e. Schema#verify raises an error.
24
+ def self.validate(schema, instance, options = ValidationOptions.new)
25
+ state = ValidationState.new
26
+ state.options = options
27
+ state.root_schema = schema
28
+ state.instance_tokens = []
29
+ state.schema_tokens = [[]]
30
+ state.errors = []
31
+
32
+ begin
33
+ validate_with_state(state, schema, instance)
34
+ rescue MaxErrorsReachedError
35
+ # This is just a dummy error to immediately stop validation. We swallow
36
+ # the error here, and return the abridged set of errors.
37
+ end
38
+
39
+ state.errors
40
+ end
41
+
42
+ # Options you can pass to JTD::validate.
43
+ class ValidationOptions
44
+ # The maximum number of references to follow before aborting validation. You
45
+ # can use this to prevent a stack overflow when validating schemas that
46
+ # potentially have infinite loops, such as this one:
47
+ #
48
+ # {
49
+ # "definitions": {
50
+ # "loop": { "ref": "loop" }
51
+ # },
52
+ # "ref": "loop"
53
+ # }
54
+ #
55
+ # The default value for +max_depth+ is 0, which indicates that no max depth
56
+ # should be imposed at all.
57
+ attr_accessor :max_depth
58
+
59
+ # The maximum number of errors to return. You can use this to have
60
+ # JTD::validate have better performance if you don't have any use for errors
61
+ # beyond a certain count.
62
+ #
63
+ # For instance, if all you care about is whether or not there are any
64
+ # validation errors at all, you can set +max_errors+ to 1. If you're
65
+ # presenting validation errors in an interface that can't show more than 5
66
+ # errors, set +max_errors+ to 5.
67
+ #
68
+ # The default value for +max_errors+ is 0, which indicates that all errors
69
+ # will be returned.
70
+ attr_accessor :max_errors
71
+
72
+ # Construct a new set of ValidationOptions with the given +max_depth+ and
73
+ # +max_errors+.
74
+ #
75
+ # See the documentation for +max_depth+ and +max_errors+ for what their
76
+ # default values of 0 mean.
77
+ def initialize(max_depth: 0, max_errors: 0)
78
+ @max_depth = max_depth
79
+ @max_errors = max_errors
80
+ end
81
+ end
82
+
83
+ # Represents a single JSON Type Definition validation error.
84
+ #
85
+ # ValidationError does not extend StandardError; it is not a Ruby exception.
86
+ # It is a plain old Ruby object.
87
+ #
88
+ # Every ValidationError has two attributes:
89
+ #
90
+ # * +instance_path+ is an array of strings. It represents the path to the part
91
+ # of the +instance+ passed to JTD::validate that was rejected.
92
+ #
93
+ # * +schema_path+ is an array of strings. It represents the path to the part
94
+ # of the +schema+ passed to JTD::validate that rejected the instance at
95
+ # +instance_path+.
96
+ class ValidationError < Struct.new(:instance_path, :schema_path)
97
+
98
+ # Constructs a new ValidationError from the standard JSON representation of
99
+ # a validation error in JSON Type Definition.
100
+ def self.from_hash(hash)
101
+ instance_path = hash['instancePath']
102
+ schema_path = hash['schemaPath']
103
+
104
+ ValidationError.new(instance_path, schema_path)
105
+ end
106
+ end
107
+
108
+ # Error raised from JTD::validate if the number of references followed exceeds
109
+ # ValidationOptions#max_depth.
110
+ class MaxDepthExceededError < StandardError
111
+ # Constructs a new MaxDepthExceededError.
112
+ def initialize(msg = 'max depth exceeded during JTD::validate')
113
+ super
114
+ end
115
+ end
116
+
117
+ private
118
+
119
+ class ValidationState # :nodoc:
120
+ attr_accessor :options, :root_schema, :instance_tokens, :schema_tokens, :errors
121
+
122
+ def push_instance_token(token)
123
+ instance_tokens << token
124
+ end
125
+
126
+ def pop_instance_token
127
+ instance_tokens.pop
128
+ end
129
+
130
+ def push_schema_token(token)
131
+ schema_tokens.last << token
132
+ end
133
+
134
+ def pop_schema_token
135
+ schema_tokens.last.pop
136
+ end
137
+
138
+ def push_error
139
+ errors << ValidationError.new(instance_tokens.clone, schema_tokens.last.clone)
140
+
141
+ raise MaxErrorsReachedError.new if errors.size == options.max_errors
142
+ end
143
+ end
144
+
145
+ private_constant :ValidationState
146
+
147
+ class MaxErrorsReachedError < StandardError # :nodoc:
148
+ end
149
+
150
+ private_constant :MaxErrorsReachedError
151
+
152
+ def self.validate_with_state(state, schema, instance, parent_tag = nil)
153
+ return if schema.nullable && instance.nil?
154
+
155
+ case schema.form
156
+ when :ref
157
+ state.schema_tokens << ['definitions', schema.ref]
158
+ raise MaxDepthExceededError.new if state.schema_tokens.length == state.options.max_depth
159
+
160
+ validate_with_state(state, state.root_schema.definitions[schema.ref], instance)
161
+ state.schema_tokens.pop
162
+
163
+ when :type
164
+ state.push_schema_token('type')
165
+
166
+ case schema.type
167
+ when 'boolean'
168
+ state.push_error unless instance == true || instance == false
169
+ when 'float32', 'float64'
170
+ state.push_error unless instance.is_a?(Numeric)
171
+ when 'int8'
172
+ validate_int(state, instance, -128, 127)
173
+ when 'uint8'
174
+ validate_int(state, instance, 0, 255)
175
+ when 'int16'
176
+ validate_int(state, instance, -32_768, 32_767)
177
+ when 'uint16'
178
+ validate_int(state, instance, 0, 65_535)
179
+ when 'int32'
180
+ validate_int(state, instance, -2_147_483_648, 2_147_483_647)
181
+ when 'uint32'
182
+ validate_int(state, instance, 0, 4_294_967_295)
183
+ when 'string'
184
+ state.push_error unless instance.is_a?(String)
185
+ when 'timestamp'
186
+ begin
187
+ DateTime.rfc3339(instance)
188
+ rescue TypeError, ArgumentError
189
+ state.push_error
190
+ end
191
+ end
192
+
193
+ state.pop_schema_token
194
+
195
+ when :enum
196
+ state.push_schema_token('enum')
197
+ state.push_error unless schema.enum.include?(instance)
198
+ state.pop_schema_token
199
+
200
+ when :elements
201
+ state.push_schema_token('elements')
202
+
203
+ if instance.is_a?(Array)
204
+ instance.each_with_index do |sub_instance, index|
205
+ state.push_instance_token(index.to_s)
206
+ validate_with_state(state, schema.elements, sub_instance)
207
+ state.pop_instance_token
208
+ end
209
+ else
210
+ state.push_error
211
+ end
212
+
213
+ state.pop_schema_token
214
+
215
+ when :properties
216
+ # The properties form is a little weird. The JSON Typedef spec always
217
+ # works out so that the schema path points to a part of the schema that
218
+ # really exists, and there's no guarantee that a schema of the properties
219
+ # form has the properties keyword.
220
+ #
221
+ # To deal with this, we handle the "instance isn't even an object" case
222
+ # separately.
223
+ unless instance.is_a?(Hash)
224
+ if schema.properties
225
+ state.push_schema_token('properties')
226
+ else
227
+ state.push_schema_token('optionalProperties')
228
+ end
229
+
230
+ state.push_error
231
+ state.pop_schema_token
232
+
233
+ return
234
+ end
235
+
236
+ # Check the required properties.
237
+ if schema.properties
238
+ state.push_schema_token('properties')
239
+
240
+ schema.properties.each do |property, sub_schema|
241
+ state.push_schema_token(property)
242
+
243
+ if instance.key?(property)
244
+ state.push_instance_token(property)
245
+ validate_with_state(state, sub_schema, instance[property])
246
+ state.pop_instance_token
247
+ else
248
+ state.push_error
249
+ end
250
+
251
+ state.pop_schema_token
252
+ end
253
+
254
+ state.pop_schema_token
255
+ end
256
+
257
+ # Check the optional properties. This is almost identical to the previous
258
+ # case, except we don't raise an error if the property isn't present on
259
+ # the instance.
260
+ if schema.optional_properties
261
+ state.push_schema_token('optionalProperties')
262
+
263
+ schema.optional_properties.each do |property, sub_schema|
264
+ state.push_schema_token(property)
265
+
266
+ if instance.key?(property)
267
+ state.push_instance_token(property)
268
+ validate_with_state(state, sub_schema, instance[property])
269
+ state.pop_instance_token
270
+ end
271
+
272
+ state.pop_schema_token
273
+ end
274
+
275
+ state.pop_schema_token
276
+ end
277
+
278
+ # Check for unallowed additional properties.
279
+ unless schema.additional_properties
280
+ properties = (schema.properties || {}).keys
281
+ optional_properties = (schema.optional_properties || {}).keys
282
+ parent_tags = [parent_tag]
283
+
284
+ additional_keys = instance.keys - properties - optional_properties - parent_tags
285
+ additional_keys.each do |property|
286
+ state.push_instance_token(property)
287
+ state.push_error
288
+ state.pop_instance_token
289
+ end
290
+ end
291
+
292
+ when :values
293
+ state.push_schema_token('values')
294
+
295
+ if instance.is_a?(Hash)
296
+ instance.each do |property, sub_instance|
297
+ state.push_instance_token(property)
298
+ validate_with_state(state, schema.values, sub_instance)
299
+ state.pop_instance_token
300
+ end
301
+ else
302
+ state.push_error
303
+ end
304
+
305
+ state.pop_schema_token
306
+
307
+ when :discriminator
308
+ unless instance.is_a?(Hash)
309
+ state.push_schema_token('discriminator')
310
+ state.push_error
311
+ state.pop_schema_token
312
+
313
+ return
314
+ end
315
+
316
+ unless instance.key?(schema.discriminator)
317
+ state.push_schema_token('discriminator')
318
+ state.push_error
319
+ state.pop_schema_token
320
+
321
+ return
322
+ end
323
+
324
+ unless instance[schema.discriminator].is_a?(String)
325
+ state.push_schema_token('discriminator')
326
+ state.push_instance_token(schema.discriminator)
327
+ state.push_error
328
+ state.pop_instance_token
329
+ state.pop_schema_token
330
+
331
+ return
332
+ end
333
+
334
+ unless schema.mapping.key?(instance[schema.discriminator])
335
+ state.push_schema_token('mapping')
336
+ state.push_instance_token(schema.discriminator)
337
+ state.push_error
338
+ state.pop_instance_token
339
+ state.pop_schema_token
340
+
341
+ return
342
+ end
343
+
344
+ sub_schema = schema.mapping[instance[schema.discriminator]]
345
+
346
+ state.push_schema_token('mapping')
347
+ state.push_schema_token(instance[schema.discriminator])
348
+ validate_with_state(state, sub_schema, instance, schema.discriminator)
349
+ state.pop_schema_token
350
+ state.pop_schema_token
351
+ end
352
+ end
353
+
354
+ def self.validate_int(state, instance, min, max)
355
+ if instance.is_a?(Numeric)
356
+ if instance.modulo(1).nonzero? || instance < min || instance > max
357
+ state.push_error
358
+ end
359
+ else
360
+ state.push_error
361
+ end
362
+ end
363
+ end
@@ -1,3 +1,4 @@
1
1
  module JTD
2
- VERSION = "0.1.1"
2
+ # The version of the +jtd+ gem you are using.
3
+ VERSION = '0.1.6'
3
4
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: jtd
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.1
4
+ version: 0.1.6
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ulysse Carion
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2020-09-18 00:00:00.000000000 Z
11
+ date: 2020-09-21 00:00:00.000000000 Z
12
12
  dependencies: []
13
13
  description:
14
14
  email:
@@ -32,6 +32,8 @@ files:
32
32
  - bin/setup
33
33
  - jtd.gemspec
34
34
  - lib/jtd.rb
35
+ - lib/jtd/schema.rb
36
+ - lib/jtd/validate.rb
35
37
  - lib/jtd/version.rb
36
38
  homepage: https://github.com/jsontypedef/json-typedef-ruby
37
39
  licenses: