jtd 0.1.1 → 0.1.2
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 +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:
|