jtd 0.1.1 → 0.1.6

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.
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: