jtd 0.1.1 → 0.1.2
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/Gemfile.lock +1 -1
- data/lib/jtd.rb +3 -3
- data/lib/jtd/schema.rb +258 -0
- data/lib/jtd/validate.rb +293 -0
- data/lib/jtd/version.rb +1 -1
- metadata +4 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: f28bef81d86fe4efad4f6e1398ce5b1d7d462de7e99590297b14a7a92bf2b40d
|
4
|
+
data.tar.gz: 8f25271a45816681d57d13dcb1cd8436cfc93d5a16aeadfa7a1946b199bc489f
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 0bcc179000832d0d22833fec826df648f1b40d2cbc8610d138a479144260fedae3c507a28aa986ef9392be6a777f871607bf27bb99bbaee8486ca6f5538d7e55
|
7
|
+
data.tar.gz: 8749c6e7e26072097069e4977b7b817210a2f70815f4a6706d728ae4b629169429856eaa0c24ec11dcee171e196b4effcc1ce2185c9ae028eb6da24a2d5df9d6
|
data/Gemfile.lock
CHANGED
data/lib/jtd.rb
CHANGED
data/lib/jtd/schema.rb
ADDED
@@ -0,0 +1,258 @@
|
|
1
|
+
module JTD
|
2
|
+
class Schema
|
3
|
+
attr_accessor *%i[
|
4
|
+
metadata
|
5
|
+
nullable
|
6
|
+
definitions
|
7
|
+
ref
|
8
|
+
type
|
9
|
+
enum
|
10
|
+
elements
|
11
|
+
properties
|
12
|
+
optional_properties
|
13
|
+
additional_properties
|
14
|
+
values
|
15
|
+
discriminator
|
16
|
+
mapping
|
17
|
+
]
|
18
|
+
|
19
|
+
def self.from_hash(hash)
|
20
|
+
# Raising this error early makes for a much clearer error for the
|
21
|
+
# relatively common case of something that was expected to be an object
|
22
|
+
# (Hash), but was something else instead.
|
23
|
+
raise TypeError.new("expected hash, got: #{hash}") unless hash.is_a?(Hash)
|
24
|
+
|
25
|
+
illegal_keywords = hash.keys - KEYWORDS
|
26
|
+
unless illegal_keywords.empty?
|
27
|
+
raise TypeError.new("illegal schema keywords: #{illegal_keywords}")
|
28
|
+
end
|
29
|
+
|
30
|
+
s = Schema.new
|
31
|
+
|
32
|
+
if hash['metadata']
|
33
|
+
s.metadata = hash['metadata']
|
34
|
+
end
|
35
|
+
|
36
|
+
unless hash['nullable'].nil?
|
37
|
+
s.nullable = hash['nullable']
|
38
|
+
end
|
39
|
+
|
40
|
+
if hash['definitions']
|
41
|
+
s.definitions = Hash[hash['definitions'].map { |k, v| [k, from_hash(v) ]}]
|
42
|
+
end
|
43
|
+
|
44
|
+
s.ref = hash['ref']
|
45
|
+
s.type = hash['type']
|
46
|
+
s.enum = hash['enum']
|
47
|
+
|
48
|
+
if hash['elements']
|
49
|
+
s.elements = from_hash(hash['elements'])
|
50
|
+
end
|
51
|
+
|
52
|
+
if hash['properties']
|
53
|
+
s.properties = Hash[hash['properties'].map { |k, v| [k, from_hash(v) ]}]
|
54
|
+
end
|
55
|
+
|
56
|
+
if hash['optionalProperties']
|
57
|
+
s.optional_properties = Hash[hash['optionalProperties'].map { |k, v| [k, from_hash(v) ]}]
|
58
|
+
end
|
59
|
+
|
60
|
+
unless hash['additionalProperties'].nil?
|
61
|
+
s.additional_properties = hash['additionalProperties']
|
62
|
+
end
|
63
|
+
|
64
|
+
if hash['values']
|
65
|
+
s.values = from_hash(hash['values'])
|
66
|
+
end
|
67
|
+
|
68
|
+
s.discriminator = hash['discriminator']
|
69
|
+
|
70
|
+
if hash['mapping']
|
71
|
+
s.mapping = Hash[hash['mapping'].map { |k, v| [k, from_hash(v) ]}]
|
72
|
+
end
|
73
|
+
|
74
|
+
s
|
75
|
+
end
|
76
|
+
|
77
|
+
def verify(root = self)
|
78
|
+
self.check_type('metadata', [Hash])
|
79
|
+
self.check_type('nullable', [TrueClass, FalseClass])
|
80
|
+
self.check_type('definitions', [Hash])
|
81
|
+
self.check_type('ref', [String])
|
82
|
+
self.check_type('type', [String])
|
83
|
+
self.check_type('enum', [Array])
|
84
|
+
self.check_type('elements', [Schema])
|
85
|
+
self.check_type('properties', [Hash])
|
86
|
+
self.check_type('optional_properties', [Hash])
|
87
|
+
self.check_type('additional_properties', [TrueClass, FalseClass])
|
88
|
+
self.check_type('values', [Schema])
|
89
|
+
self.check_type('discriminator', [String])
|
90
|
+
self.check_type('mapping', [Hash])
|
91
|
+
|
92
|
+
form_signature = [
|
93
|
+
!!ref,
|
94
|
+
!!type,
|
95
|
+
!!enum,
|
96
|
+
!!elements,
|
97
|
+
!!properties,
|
98
|
+
!!optional_properties,
|
99
|
+
!!additional_properties,
|
100
|
+
!!values,
|
101
|
+
!!discriminator,
|
102
|
+
!!mapping,
|
103
|
+
]
|
104
|
+
|
105
|
+
unless VALID_FORMS.include?(form_signature)
|
106
|
+
raise ArgumentError.new("invalid schema form: #{self}")
|
107
|
+
end
|
108
|
+
|
109
|
+
if root != self && definitions && definitions.any?
|
110
|
+
raise ArgumentError.new("non-root definitions: #{definitions}")
|
111
|
+
end
|
112
|
+
|
113
|
+
if ref
|
114
|
+
if !root.definitions || !root.definitions.key?(ref)
|
115
|
+
raise ArgumentError.new("ref to non-existent definition: #{ref}")
|
116
|
+
end
|
117
|
+
end
|
118
|
+
|
119
|
+
if type && !TYPES.include?(type)
|
120
|
+
raise ArgumentError.new("invalid type: #{type}")
|
121
|
+
end
|
122
|
+
|
123
|
+
if enum
|
124
|
+
if enum.empty?
|
125
|
+
raise ArgumentError.new("enum must not be empty: #{self}")
|
126
|
+
end
|
127
|
+
|
128
|
+
if enum.any? { |v| !v.is_a?(String) }
|
129
|
+
raise ArgumentError.new("enum must contain only strings: #{enum}")
|
130
|
+
end
|
131
|
+
|
132
|
+
if enum.size != enum.uniq.size
|
133
|
+
raise ArgumentError.new("enum must not contain duplicates: #{enum}")
|
134
|
+
end
|
135
|
+
end
|
136
|
+
|
137
|
+
if properties && optional_properties
|
138
|
+
shared_keys = properties.keys & optional_properties.keys
|
139
|
+
if shared_keys.any?
|
140
|
+
raise ArgumentError.new("properties and optional_properties share keys: #{shared_keys}")
|
141
|
+
end
|
142
|
+
end
|
143
|
+
|
144
|
+
if mapping
|
145
|
+
mapping.values.each do |s|
|
146
|
+
if s.form != :properties
|
147
|
+
raise ArgumentError.new("mapping values must be of properties form: #{s}")
|
148
|
+
end
|
149
|
+
|
150
|
+
if s.nullable
|
151
|
+
raise ArgumentError.new("mapping values must not be nullable: #{s}")
|
152
|
+
end
|
153
|
+
|
154
|
+
contains_discriminator = ArgumentError.new("mapping values must not contain discriminator (#{discriminator}): #{s}")
|
155
|
+
|
156
|
+
if s.properties && s.properties.key?(discriminator)
|
157
|
+
raise contains_discriminator
|
158
|
+
end
|
159
|
+
|
160
|
+
if s.optional_properties && s.optional_properties.key?(discriminator)
|
161
|
+
raise contains_discriminator
|
162
|
+
end
|
163
|
+
end
|
164
|
+
end
|
165
|
+
|
166
|
+
definitions.values.each { |s| s.verify(root) } if definitions
|
167
|
+
elements.verify(root) if elements
|
168
|
+
properties.values.each { |s| s.verify(root) } if properties
|
169
|
+
optional_properties.values.each { |s| s.verify(root) } if optional_properties
|
170
|
+
values.verify(root) if values
|
171
|
+
mapping.values.each { |s| s.verify(root) } if mapping
|
172
|
+
|
173
|
+
self
|
174
|
+
end
|
175
|
+
|
176
|
+
def form
|
177
|
+
return :ref if ref
|
178
|
+
return :type if type
|
179
|
+
return :enum if enum
|
180
|
+
return :elements if elements
|
181
|
+
return :properties if properties || optional_properties
|
182
|
+
return :values if values
|
183
|
+
return :discriminator if discriminator
|
184
|
+
|
185
|
+
:empty
|
186
|
+
end
|
187
|
+
|
188
|
+
private
|
189
|
+
|
190
|
+
KEYWORDS = %w[
|
191
|
+
metadata
|
192
|
+
nullable
|
193
|
+
definitions
|
194
|
+
ref
|
195
|
+
type
|
196
|
+
enum
|
197
|
+
elements
|
198
|
+
properties
|
199
|
+
optionalProperties
|
200
|
+
additionalProperties
|
201
|
+
values
|
202
|
+
discriminator
|
203
|
+
mapping
|
204
|
+
]
|
205
|
+
|
206
|
+
private_constant :KEYWORDS
|
207
|
+
|
208
|
+
TYPES = %w[
|
209
|
+
boolean
|
210
|
+
int8
|
211
|
+
uint8
|
212
|
+
int16
|
213
|
+
uint16
|
214
|
+
int32
|
215
|
+
uint32
|
216
|
+
float32
|
217
|
+
float64
|
218
|
+
string
|
219
|
+
timestamp
|
220
|
+
]
|
221
|
+
|
222
|
+
private_constant :TYPES
|
223
|
+
|
224
|
+
VALID_FORMS = [
|
225
|
+
# Empty form
|
226
|
+
[false, false, false, false, false, false, false, false, false, false],
|
227
|
+
# Ref form
|
228
|
+
[true, false, false, false, false, false, false, false, false, false],
|
229
|
+
# Type form
|
230
|
+
[false, true, false, false, false, false, false, false, false, false],
|
231
|
+
# Enum form
|
232
|
+
[false, false, true, false, false, false, false, false, false, false],
|
233
|
+
# Elements form
|
234
|
+
[false, false, false, true, false, false, false, false, false, false],
|
235
|
+
# Properties form -- properties or optional properties or both, and
|
236
|
+
# never additional properties on its own
|
237
|
+
[false, false, false, false, true, false, false, false, false, false],
|
238
|
+
[false, false, false, false, false, true, false, false, false, false],
|
239
|
+
[false, false, false, false, true, true, false, false, false, false],
|
240
|
+
[false, false, false, false, true, false, true, false, false, false],
|
241
|
+
[false, false, false, false, false, true, true, false, false, false],
|
242
|
+
[false, false, false, false, true, true, true, false, false, false],
|
243
|
+
# Values form
|
244
|
+
[false, false, false, false, false, false, false, true, false, false],
|
245
|
+
# Discriminator form
|
246
|
+
[false, false, false, false, false, false, false, false, true, true],
|
247
|
+
]
|
248
|
+
|
249
|
+
private_constant :VALID_FORMS
|
250
|
+
|
251
|
+
def check_type(key, classes)
|
252
|
+
val = self.send(key)
|
253
|
+
unless val.nil? || classes.include?(val.class)
|
254
|
+
raise TypeError.new("#{key} must be one of #{classes}, got: #{val}")
|
255
|
+
end
|
256
|
+
end
|
257
|
+
end
|
258
|
+
end
|
data/lib/jtd/validate.rb
ADDED
@@ -0,0 +1,293 @@
|
|
1
|
+
require 'time'
|
2
|
+
|
3
|
+
module JTD
|
4
|
+
def self.validate(schema, instance, options = ValidationOptions.new)
|
5
|
+
state = ValidationState.new
|
6
|
+
state.options = options
|
7
|
+
state.root_schema = schema
|
8
|
+
state.instance_tokens = []
|
9
|
+
state.schema_tokens = [[]]
|
10
|
+
state.errors = []
|
11
|
+
|
12
|
+
begin
|
13
|
+
validate_with_state(state, schema, instance)
|
14
|
+
rescue MaxErrorsReachedError
|
15
|
+
# This is just a dummy error to immediately stop validation. We swallow
|
16
|
+
# the error here, and return the abridged set of errors.
|
17
|
+
end
|
18
|
+
|
19
|
+
state.errors
|
20
|
+
end
|
21
|
+
|
22
|
+
class ValidationOptions
|
23
|
+
attr_accessor :max_depth, :max_errors
|
24
|
+
|
25
|
+
def initialize(max_depth: 0, max_errors: 0)
|
26
|
+
@max_depth = max_depth
|
27
|
+
@max_errors = max_errors
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
class ValidationError < Struct.new(:instance_path, :schema_path)
|
32
|
+
def self.from_hash(hash)
|
33
|
+
instance_path = hash['instancePath']
|
34
|
+
schema_path = hash['schemaPath']
|
35
|
+
|
36
|
+
ValidationError.new(instance_path, schema_path)
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
class MaxDepthExceededError < StandardError
|
41
|
+
def initialize(msg = 'max depth exceeded during JTD::validate')
|
42
|
+
super
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
private
|
47
|
+
|
48
|
+
class ValidationState
|
49
|
+
attr_accessor :options, :root_schema, :instance_tokens, :schema_tokens, :errors
|
50
|
+
|
51
|
+
def push_instance_token(token)
|
52
|
+
instance_tokens << token
|
53
|
+
end
|
54
|
+
|
55
|
+
def pop_instance_token
|
56
|
+
instance_tokens.pop
|
57
|
+
end
|
58
|
+
|
59
|
+
def push_schema_token(token)
|
60
|
+
schema_tokens.last << token
|
61
|
+
end
|
62
|
+
|
63
|
+
def pop_schema_token
|
64
|
+
schema_tokens.last.pop
|
65
|
+
end
|
66
|
+
|
67
|
+
def push_error
|
68
|
+
errors << ValidationError.new(instance_tokens.clone, schema_tokens.last.clone)
|
69
|
+
|
70
|
+
raise MaxErrorsReachedError.new if errors.size == options.max_errors
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
private_constant :ValidationState
|
75
|
+
|
76
|
+
class MaxErrorsReachedError < StandardError
|
77
|
+
end
|
78
|
+
|
79
|
+
private_constant :MaxErrorsReachedError
|
80
|
+
|
81
|
+
def self.validate_with_state(state, schema, instance, parent_tag = nil)
|
82
|
+
return if schema.nullable && instance.nil?
|
83
|
+
|
84
|
+
case schema.form
|
85
|
+
when :ref
|
86
|
+
state.schema_tokens << ['definitions', schema.ref]
|
87
|
+
p state.schema_tokens.length, state.options, state.options.max_depth
|
88
|
+
raise MaxDepthExceededError.new if state.schema_tokens.length == state.options.max_depth
|
89
|
+
|
90
|
+
validate_with_state(state, state.root_schema.definitions[schema.ref], instance)
|
91
|
+
state.schema_tokens.pop
|
92
|
+
|
93
|
+
when :type
|
94
|
+
state.push_schema_token('type')
|
95
|
+
|
96
|
+
case schema.type
|
97
|
+
when 'boolean'
|
98
|
+
state.push_error unless instance == true || instance == false
|
99
|
+
when 'float32', 'float64'
|
100
|
+
state.push_error unless instance.is_a?(Numeric)
|
101
|
+
when 'int8'
|
102
|
+
validate_int(state, instance, -128, 127)
|
103
|
+
when 'uint8'
|
104
|
+
validate_int(state, instance, 0, 255)
|
105
|
+
when 'int16'
|
106
|
+
validate_int(state, instance, -32_768, 32_767)
|
107
|
+
when 'uint16'
|
108
|
+
validate_int(state, instance, 0, 65_535)
|
109
|
+
when 'int32'
|
110
|
+
validate_int(state, instance, -2_147_483_648, 2_147_483_647)
|
111
|
+
when 'uint32'
|
112
|
+
validate_int(state, instance, 0, 4_294_967_295)
|
113
|
+
when 'string'
|
114
|
+
state.push_error unless instance.is_a?(String)
|
115
|
+
when 'timestamp'
|
116
|
+
begin
|
117
|
+
DateTime.rfc3339(instance)
|
118
|
+
rescue TypeError, ArgumentError
|
119
|
+
state.push_error
|
120
|
+
end
|
121
|
+
end
|
122
|
+
|
123
|
+
state.pop_schema_token
|
124
|
+
|
125
|
+
when :enum
|
126
|
+
state.push_schema_token('enum')
|
127
|
+
state.push_error unless schema.enum.include?(instance)
|
128
|
+
state.pop_schema_token
|
129
|
+
|
130
|
+
when :elements
|
131
|
+
state.push_schema_token('elements')
|
132
|
+
|
133
|
+
if instance.is_a?(Array)
|
134
|
+
instance.each_with_index do |sub_instance, index|
|
135
|
+
state.push_instance_token(index.to_s)
|
136
|
+
validate_with_state(state, schema.elements, sub_instance)
|
137
|
+
state.pop_instance_token
|
138
|
+
end
|
139
|
+
else
|
140
|
+
state.push_error
|
141
|
+
end
|
142
|
+
|
143
|
+
state.pop_schema_token
|
144
|
+
|
145
|
+
when :properties
|
146
|
+
# The properties form is a little weird. The JSON Typedef spec always
|
147
|
+
# works out so that the schema path points to a part of the schema that
|
148
|
+
# really exists, and there's no guarantee that a schema of the properties
|
149
|
+
# form has the properties keyword.
|
150
|
+
#
|
151
|
+
# To deal with this, we handle the "instance isn't even an object" case
|
152
|
+
# separately.
|
153
|
+
unless instance.is_a?(Hash)
|
154
|
+
if schema.properties
|
155
|
+
state.push_schema_token('properties')
|
156
|
+
else
|
157
|
+
state.push_schema_token('optionalProperties')
|
158
|
+
end
|
159
|
+
|
160
|
+
state.push_error
|
161
|
+
state.pop_schema_token
|
162
|
+
|
163
|
+
return
|
164
|
+
end
|
165
|
+
|
166
|
+
# Check the required properties.
|
167
|
+
if schema.properties
|
168
|
+
state.push_schema_token('properties')
|
169
|
+
|
170
|
+
schema.properties.each do |property, sub_schema|
|
171
|
+
state.push_schema_token(property)
|
172
|
+
|
173
|
+
if instance.key?(property)
|
174
|
+
state.push_instance_token(property)
|
175
|
+
validate_with_state(state, sub_schema, instance[property])
|
176
|
+
state.pop_instance_token
|
177
|
+
else
|
178
|
+
state.push_error
|
179
|
+
end
|
180
|
+
|
181
|
+
state.pop_schema_token
|
182
|
+
end
|
183
|
+
|
184
|
+
state.pop_schema_token
|
185
|
+
end
|
186
|
+
|
187
|
+
# Check the optional properties. This is almost identical to the previous
|
188
|
+
# case, except we don't raise an error if the property isn't present on
|
189
|
+
# the instance.
|
190
|
+
if schema.optional_properties
|
191
|
+
state.push_schema_token('optionalProperties')
|
192
|
+
|
193
|
+
schema.optional_properties.each do |property, sub_schema|
|
194
|
+
state.push_schema_token(property)
|
195
|
+
|
196
|
+
if instance.key?(property)
|
197
|
+
state.push_instance_token(property)
|
198
|
+
validate_with_state(state, sub_schema, instance[property])
|
199
|
+
state.pop_instance_token
|
200
|
+
end
|
201
|
+
|
202
|
+
state.pop_schema_token
|
203
|
+
end
|
204
|
+
|
205
|
+
state.pop_schema_token
|
206
|
+
end
|
207
|
+
|
208
|
+
# Check for unallowed additional properties.
|
209
|
+
unless schema.additional_properties
|
210
|
+
properties = (schema.properties || {}).keys
|
211
|
+
optional_properties = (schema.optional_properties || {}).keys
|
212
|
+
parent_tags = [parent_tag]
|
213
|
+
|
214
|
+
additional_keys = instance.keys - properties - optional_properties - parent_tags
|
215
|
+
additional_keys.each do |property|
|
216
|
+
state.push_instance_token(property)
|
217
|
+
state.push_error
|
218
|
+
state.pop_instance_token
|
219
|
+
end
|
220
|
+
end
|
221
|
+
|
222
|
+
when :values
|
223
|
+
state.push_schema_token('values')
|
224
|
+
|
225
|
+
if instance.is_a?(Hash)
|
226
|
+
instance.each do |property, sub_instance|
|
227
|
+
state.push_instance_token(property)
|
228
|
+
validate_with_state(state, schema.values, sub_instance)
|
229
|
+
state.pop_instance_token
|
230
|
+
end
|
231
|
+
else
|
232
|
+
state.push_error
|
233
|
+
end
|
234
|
+
|
235
|
+
state.pop_schema_token
|
236
|
+
|
237
|
+
when :discriminator
|
238
|
+
unless instance.is_a?(Hash)
|
239
|
+
state.push_schema_token('discriminator')
|
240
|
+
state.push_error
|
241
|
+
state.pop_schema_token
|
242
|
+
|
243
|
+
return
|
244
|
+
end
|
245
|
+
|
246
|
+
unless instance.key?(schema.discriminator)
|
247
|
+
state.push_schema_token('discriminator')
|
248
|
+
state.push_error
|
249
|
+
state.pop_schema_token
|
250
|
+
|
251
|
+
return
|
252
|
+
end
|
253
|
+
|
254
|
+
unless instance[schema.discriminator].is_a?(String)
|
255
|
+
state.push_schema_token('discriminator')
|
256
|
+
state.push_instance_token(schema.discriminator)
|
257
|
+
state.push_error
|
258
|
+
state.pop_instance_token
|
259
|
+
state.pop_schema_token
|
260
|
+
|
261
|
+
return
|
262
|
+
end
|
263
|
+
|
264
|
+
unless schema.mapping.key?(instance[schema.discriminator])
|
265
|
+
state.push_schema_token('mapping')
|
266
|
+
state.push_instance_token(schema.discriminator)
|
267
|
+
state.push_error
|
268
|
+
state.pop_instance_token
|
269
|
+
state.pop_schema_token
|
270
|
+
|
271
|
+
return
|
272
|
+
end
|
273
|
+
|
274
|
+
sub_schema = schema.mapping[instance[schema.discriminator]]
|
275
|
+
|
276
|
+
state.push_schema_token('mapping')
|
277
|
+
state.push_schema_token(instance[schema.discriminator])
|
278
|
+
validate_with_state(state, sub_schema, instance, schema.discriminator)
|
279
|
+
state.pop_schema_token
|
280
|
+
state.pop_schema_token
|
281
|
+
end
|
282
|
+
end
|
283
|
+
|
284
|
+
def self.validate_int(state, instance, min, max)
|
285
|
+
if instance.is_a?(Numeric)
|
286
|
+
if instance.modulo(1).nonzero? || instance < min || instance > max
|
287
|
+
state.push_error
|
288
|
+
end
|
289
|
+
else
|
290
|
+
state.push_error
|
291
|
+
end
|
292
|
+
end
|
293
|
+
end
|
data/lib/jtd/version.rb
CHANGED
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.
|
4
|
+
version: 0.1.2
|
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-
|
11
|
+
date: 2020-09-19 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:
|