rschema 2.4.0 → 3.0.1.pre1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.md +408 -197
- data/lib/rschema.rb +26 -367
- data/lib/rschema/dsl.rb +103 -0
- data/lib/rschema/error.rb +46 -0
- data/lib/rschema/http_coercer.rb +177 -0
- data/lib/rschema/options.rb +15 -0
- data/lib/rschema/result.rb +39 -0
- data/lib/rschema/schemas/anything.rb +17 -0
- data/lib/rschema/schemas/boolean.rb +27 -0
- data/lib/rschema/schemas/enum.rb +31 -0
- data/lib/rschema/schemas/fixed_hash.rb +118 -0
- data/lib/rschema/schemas/fixed_length_array.rb +60 -0
- data/lib/rschema/schemas/maybe.rb +23 -0
- data/lib/rschema/schemas/pipeline.rb +27 -0
- data/lib/rschema/schemas/predicate.rb +27 -0
- data/lib/rschema/schemas/set.rb +56 -0
- data/lib/rschema/schemas/sum.rb +36 -0
- data/lib/rschema/schemas/type.rb +27 -0
- data/lib/rschema/schemas/variable_hash.rb +67 -0
- data/lib/rschema/schemas/variable_length_array.rb +49 -0
- data/lib/rschema/version.rb +1 -1
- metadata +27 -10
- data/lib/rschema/rails_interop.rb +0 -19
data/lib/rschema.rb
CHANGED
@@ -1,377 +1,36 @@
|
|
1
|
-
require '
|
1
|
+
require 'rschema/options'
|
2
|
+
require 'rschema/error'
|
3
|
+
require 'rschema/result'
|
4
|
+
require 'rschema/schemas/type'
|
5
|
+
require 'rschema/schemas/maybe'
|
6
|
+
require 'rschema/schemas/enum'
|
7
|
+
require 'rschema/schemas/boolean'
|
8
|
+
require 'rschema/schemas/sum'
|
9
|
+
require 'rschema/schemas/pipeline'
|
10
|
+
require 'rschema/schemas/anything'
|
11
|
+
require 'rschema/schemas/predicate'
|
12
|
+
require 'rschema/schemas/set'
|
13
|
+
require 'rschema/schemas/variable_hash'
|
14
|
+
require 'rschema/schemas/fixed_hash'
|
15
|
+
require 'rschema/schemas/variable_length_array'
|
16
|
+
require 'rschema/schemas/fixed_length_array'
|
17
|
+
require 'rschema/dsl'
|
18
|
+
require 'rschema/http_coercer'
|
2
19
|
|
3
20
|
module RSchema
|
4
|
-
|
5
|
-
|
6
|
-
OptionalHashKey = Struct.new(:key)
|
7
|
-
ErrorDetails = Struct.new(:failing_value, :reason, :key_path) do
|
8
|
-
def initialize(failing_value, reason, key_path = [])
|
9
|
-
super(failing_value, reason, key_path)
|
10
|
-
end
|
11
|
-
|
12
|
-
def to_s
|
13
|
-
prefix = (key_path.empty? ? 'The root value' : "The value at #{key_path.inspect}")
|
14
|
-
"#{prefix} #{reason}: #{failing_value.inspect}"
|
15
|
-
end
|
16
|
-
|
17
|
-
def extend_key_path(key)
|
18
|
-
key_path.unshift(key)
|
19
|
-
self
|
20
|
-
end
|
21
|
-
end
|
22
|
-
|
23
|
-
def self.schema(dsl=RSchema::DSL, &block)
|
24
|
-
dsl.instance_exec(&block)
|
25
|
-
end
|
26
|
-
|
27
|
-
def self.validation_error(schema, value)
|
28
|
-
_, error = walk(schema, value)
|
29
|
-
error
|
30
|
-
end
|
31
|
-
|
32
|
-
def self.validate!(schema, value)
|
33
|
-
result, error = walk(schema, value)
|
34
|
-
if error.nil?
|
35
|
-
result
|
36
|
-
else
|
37
|
-
raise(ValidationError, error)
|
38
|
-
end
|
39
|
-
end
|
40
|
-
|
41
|
-
def self.validate(schema, value)
|
42
|
-
validation_error(schema, value).nil?
|
43
|
-
end
|
44
|
-
|
45
|
-
def self.coerce(schema, value)
|
46
|
-
walk(schema, value, CoercionMapper)
|
47
|
-
end
|
48
|
-
|
49
|
-
def self.coerce!(schema, value)
|
50
|
-
result, error = walk(schema, value, CoercionMapper)
|
51
|
-
if error.nil?
|
52
|
-
result
|
53
|
-
else
|
54
|
-
raise(ValidationError, error)
|
55
|
-
end
|
56
|
-
end
|
57
|
-
|
58
|
-
def self.walk(schema, value, mapper = nil)
|
59
|
-
raise(InvalidSchemaError, schema) unless schema.respond_to?(:schema_walk)
|
60
|
-
value = mapper.prewalk(schema, value) if mapper
|
61
|
-
value = schema.schema_walk(value, mapper)
|
62
|
-
value = mapper.postwalk(schema, value) if mapper
|
63
|
-
|
64
|
-
if value.is_a?(RSchema::ErrorDetails)
|
65
|
-
[nil, value]
|
66
|
-
else
|
67
|
-
[value, nil]
|
68
|
-
end
|
69
|
-
end
|
70
|
-
|
71
|
-
module DSL
|
72
|
-
module Base
|
73
|
-
def optional(key)
|
74
|
-
OptionalHashKey.new(key)
|
75
|
-
end
|
76
|
-
alias_method :_?, :optional
|
77
|
-
|
78
|
-
def hash_of(subschemas_hash)
|
79
|
-
raise InvalidSchemaError unless subschemas_hash.size == 1
|
80
|
-
GenericHashSchema.new(subschemas_hash.keys.first, subschemas_hash.values.first)
|
81
|
-
end
|
82
|
-
|
83
|
-
def set_of(subschema)
|
84
|
-
GenericSetSchema.new(subschema)
|
85
|
-
end
|
86
|
-
|
87
|
-
def predicate(name = nil, &block)
|
88
|
-
raise InvalidSchemaError unless block
|
89
|
-
PredicateSchema.new(name, block)
|
90
|
-
end
|
91
|
-
|
92
|
-
def maybe(subschema)
|
93
|
-
raise InvalidSchemaError unless subschema
|
94
|
-
MaybeSchema.new(subschema)
|
95
|
-
end
|
96
|
-
|
97
|
-
def enum(possible_values, subschema = nil)
|
98
|
-
raise InvalidSchemaError unless possible_values && possible_values.size > 0
|
99
|
-
EnumSchema.new(Set.new(possible_values), subschema)
|
100
|
-
end
|
101
|
-
|
102
|
-
def boolean
|
103
|
-
BooleanSchema
|
104
|
-
end
|
105
|
-
|
106
|
-
def any
|
107
|
-
AnySchema
|
108
|
-
end
|
109
|
-
|
110
|
-
def either(*subschemas)
|
111
|
-
unless subschemas.size > 1
|
112
|
-
raise InvalidSchemaError, 'EitherSchema requires two or more alternatives'
|
113
|
-
end
|
114
|
-
EitherSchema.new(subschemas)
|
115
|
-
end
|
116
|
-
end
|
117
|
-
extend Base
|
118
|
-
end
|
119
|
-
|
120
|
-
module CoercionMapper
|
121
|
-
def self.prewalk(schema, value)
|
122
|
-
if schema == Integer && value.is_a?(String)
|
123
|
-
try_convert(value) { Integer(value) }
|
124
|
-
elsif schema == Float && value.is_a?(String)
|
125
|
-
try_convert(value) { Float(value) }
|
126
|
-
elsif schema == Float && value.is_a?(Integer)
|
127
|
-
value.to_f
|
128
|
-
elsif schema == Symbol && value.is_a?(String)
|
129
|
-
value.to_sym
|
130
|
-
elsif schema == String && value.is_a?(Symbol)
|
131
|
-
value.to_s
|
132
|
-
elsif schema == Array && value.is_a?(Set)
|
133
|
-
value.to_a
|
134
|
-
elsif (schema == Set || schema.is_a?(GenericSetSchema)) && value.is_a?(Array)
|
135
|
-
Set.new(value)
|
136
|
-
elsif(schema.is_a?(Hash) && value.is_a?(Hash))
|
137
|
-
coerce_hash(schema, value)
|
138
|
-
elsif schema == BooleanSchema && value.is_a?(String) && value == 'true'
|
139
|
-
true
|
140
|
-
elsif schema == BooleanSchema && value.is_a?(String) && value == 'false'
|
141
|
-
false
|
142
|
-
else
|
143
|
-
value
|
144
|
-
end
|
145
|
-
end
|
146
|
-
|
147
|
-
def self.postwalk(schema, value)
|
148
|
-
value
|
149
|
-
end
|
150
|
-
|
151
|
-
def self.try_convert(x)
|
152
|
-
yield x
|
153
|
-
rescue
|
154
|
-
x
|
155
|
-
end
|
156
|
-
|
157
|
-
def self.coerce_hash(schema, value)
|
158
|
-
symbol_keys = Set.new(schema.keys.select{ |k| k.is_a?(Symbol) }.map(&:to_s))
|
159
|
-
value.reduce({}) do |accum, (k, v)|
|
160
|
-
# convert string keys to symbol keys, if needed
|
161
|
-
if k.is_a?(String) && symbol_keys.include?(k)
|
162
|
-
k = k.to_sym
|
163
|
-
end
|
164
|
-
|
165
|
-
# strip out keys that don't exist in the schema
|
166
|
-
if schema.has_key?(k) || schema.has_key?(OptionalHashKey.new(k))
|
167
|
-
accum[k] = v
|
168
|
-
end
|
169
|
-
|
170
|
-
accum
|
171
|
-
end
|
172
|
-
end
|
173
|
-
end
|
174
|
-
|
175
|
-
GenericHashSchema = Struct.new(:key_subschema, :value_subschema) do
|
176
|
-
def schema_walk(value, mapper)
|
177
|
-
if not value.is_a?(Hash)
|
178
|
-
return RSchema::ErrorDetails.new(value, 'is not a Hash')
|
179
|
-
end
|
180
|
-
|
181
|
-
value.reduce({}) do |accum, (k, v)|
|
182
|
-
# walk key
|
183
|
-
k_walked, error = RSchema.walk(key_subschema, k, mapper)
|
184
|
-
break error.extend_key_path('.keys') if error
|
185
|
-
|
186
|
-
# walk value
|
187
|
-
v_walked, error = RSchema.walk(value_subschema, v, mapper)
|
188
|
-
break error.extend_key_path(k) if error
|
189
|
-
|
190
|
-
accum[k_walked] = v_walked
|
191
|
-
accum
|
192
|
-
end
|
193
|
-
end
|
194
|
-
|
195
|
-
def inspect
|
196
|
-
"hash_of(#{key_subschema.inspect} => #{value_subschema.inspect})"
|
197
|
-
end
|
198
|
-
end
|
199
|
-
|
200
|
-
GenericSetSchema = Struct.new(:subschema) do
|
201
|
-
def schema_walk(value, mapper)
|
202
|
-
return RSchema::ErrorDetails.new(value, 'is not a Set') if not value.is_a?(Set)
|
203
|
-
|
204
|
-
value.reduce(Set.new) do |accum, subvalue|
|
205
|
-
subvalue_walked, error = RSchema.walk(subschema, subvalue, mapper)
|
206
|
-
break error.extend_key_path('.values') if error
|
207
|
-
|
208
|
-
accum << subvalue_walked
|
209
|
-
accum
|
210
|
-
end
|
211
|
-
end
|
212
|
-
|
213
|
-
def inspect
|
214
|
-
"set_of(#{subschema.inspect})"
|
215
|
-
end
|
21
|
+
def self.define(&block)
|
22
|
+
default_dsl.instance_eval(&block)
|
216
23
|
end
|
217
24
|
|
218
|
-
|
219
|
-
|
220
|
-
if block.call(value)
|
221
|
-
value
|
222
|
-
else
|
223
|
-
RSchema::ErrorDetails.new(value, 'fails predicate' + (name ? ": #{name}" : ''))
|
224
|
-
end
|
225
|
-
end
|
226
|
-
|
227
|
-
def inspect
|
228
|
-
'predicate' + (name ? "(#{name.inspect})" : '')
|
229
|
-
end
|
25
|
+
def self.define_hash(&block)
|
26
|
+
default_dsl.Hash(define(&block))
|
230
27
|
end
|
231
28
|
|
232
|
-
|
233
|
-
|
234
|
-
if value.nil?
|
235
|
-
value
|
236
|
-
else
|
237
|
-
subvalue_walked, error = RSchema.walk(subschema, value, mapper)
|
238
|
-
error || subvalue_walked
|
239
|
-
end
|
240
|
-
end
|
241
|
-
|
242
|
-
def inspect
|
243
|
-
"maybe(#{subschema.inspect})"
|
244
|
-
end
|
29
|
+
def self.default_dsl
|
30
|
+
@default_dsl ||= DefaultDSL.new
|
245
31
|
end
|
246
32
|
|
247
|
-
|
248
|
-
|
249
|
-
value_walked = if subschema
|
250
|
-
v, error = RSchema.walk(subschema, value, mapper)
|
251
|
-
return error if error
|
252
|
-
v
|
253
|
-
else
|
254
|
-
value
|
255
|
-
end
|
256
|
-
|
257
|
-
if value_set.include?(value_walked)
|
258
|
-
value_walked
|
259
|
-
else
|
260
|
-
RSchema::ErrorDetails.new(value_walked, "is not one of #{value_set.to_a}")
|
261
|
-
end
|
262
|
-
end
|
263
|
-
|
264
|
-
def inspect
|
265
|
-
"enum(#{value_set.inspect}" +
|
266
|
-
(subschema ? ", #{subschema.inspect}" : '') +
|
267
|
-
')'
|
268
|
-
end
|
269
|
-
end
|
270
|
-
|
271
|
-
EitherSchema = Struct.new(:alternatives) do
|
272
|
-
def schema_walk(value, mapper)
|
273
|
-
alternatives.each do |subschema|
|
274
|
-
v, error = RSchema.walk(subschema, value, mapper)
|
275
|
-
return v if error.nil?
|
276
|
-
end
|
277
|
-
|
278
|
-
RSchema::ErrorDetails.new(value, "matches none of #{alternatives.inspect}")
|
279
|
-
end
|
280
|
-
|
281
|
-
def inspect
|
282
|
-
"either(#{alternatives.map(&:inspect).join(', ')})"
|
283
|
-
end
|
284
|
-
end
|
285
|
-
|
286
|
-
module BooleanSchema
|
287
|
-
def self.schema_walk(value, mapper)
|
288
|
-
if value.is_a?(TrueClass) || value.is_a?(FalseClass)
|
289
|
-
value
|
290
|
-
else
|
291
|
-
RSchema::ErrorDetails.new(value, 'is not a boolean')
|
292
|
-
end
|
293
|
-
end
|
294
|
-
|
295
|
-
def self.inspect
|
296
|
-
'boolean'
|
297
|
-
end
|
298
|
-
end
|
299
|
-
|
300
|
-
module AnySchema
|
301
|
-
def self.schema_walk(value, mapper)
|
302
|
-
value
|
303
|
-
end
|
304
|
-
|
305
|
-
def self.inspect
|
306
|
-
'any'
|
307
|
-
end
|
308
|
-
end
|
309
|
-
end
|
310
|
-
|
311
|
-
class Class
|
312
|
-
def schema_walk(value, mapper)
|
313
|
-
if value.is_a?(self)
|
314
|
-
value
|
315
|
-
else
|
316
|
-
RSchema::ErrorDetails.new(value, "is not a #{self.name}, is a #{value.class.name}")
|
317
|
-
end
|
318
|
-
end
|
319
|
-
end
|
320
|
-
|
321
|
-
class Array
|
322
|
-
def schema_walk(value, mapper)
|
323
|
-
fixed_size = (size != 1)
|
324
|
-
|
325
|
-
if not value.is_a?(Array)
|
326
|
-
RSchema::ErrorDetails.new(value, 'is not an Array')
|
327
|
-
elsif fixed_size && value.size != size
|
328
|
-
RSchema::ErrorDetails.new(value, "does not have #{size} elements")
|
329
|
-
else
|
330
|
-
value.each.with_index.map do |subvalue, idx|
|
331
|
-
subschema = (fixed_size ? self[idx] : first)
|
332
|
-
subvalue_walked, error = RSchema.walk(subschema, subvalue, mapper)
|
333
|
-
break error.extend_key_path(idx) if error
|
334
|
-
subvalue_walked
|
335
|
-
end
|
336
|
-
end
|
33
|
+
class DefaultDSL
|
34
|
+
include RSchema::DSL
|
337
35
|
end
|
338
36
|
end
|
339
|
-
|
340
|
-
class Hash
|
341
|
-
def schema_walk(value, mapper)
|
342
|
-
return RSchema::ErrorDetails.new(value, 'is not a Hash') if not value.is_a?(Hash)
|
343
|
-
|
344
|
-
# extract details from the schema
|
345
|
-
required_keys = Set.new
|
346
|
-
all_subschemas = {}
|
347
|
-
each do |(k, subschema)|
|
348
|
-
if k.is_a?(RSchema::OptionalHashKey)
|
349
|
-
all_subschemas[k.key] = subschema
|
350
|
-
else
|
351
|
-
required_keys << k
|
352
|
-
all_subschemas[k] = subschema
|
353
|
-
end
|
354
|
-
end
|
355
|
-
|
356
|
-
# check for extra keys that shouldn't be there
|
357
|
-
extraneous = value.keys.reject{ |k| all_subschemas.has_key?(k) }
|
358
|
-
if extraneous.size > 0
|
359
|
-
return RSchema::ErrorDetails.new(value, "has extraneous keys: #{extraneous.inspect}")
|
360
|
-
end
|
361
|
-
|
362
|
-
# check for required keys that are missing
|
363
|
-
missing_requireds = required_keys.reject{ |k| value.has_key?(k) }
|
364
|
-
if missing_requireds.size > 0
|
365
|
-
return RSchema::ErrorDetails.new(value, "is missing required keys: #{missing_requireds.inspect}")
|
366
|
-
end
|
367
|
-
|
368
|
-
# walk the subvalues
|
369
|
-
value.reduce({}) do |accum, (k, subvalue)|
|
370
|
-
subvalue_walked, error = RSchema.walk(all_subschemas[k], subvalue, mapper)
|
371
|
-
break error.extend_key_path(k) if error
|
372
|
-
accum[k] = subvalue_walked
|
373
|
-
accum
|
374
|
-
end
|
375
|
-
end
|
376
|
-
end
|
377
|
-
|
data/lib/rschema/dsl.rb
ADDED
@@ -0,0 +1,103 @@
|
|
1
|
+
module RSchema
|
2
|
+
module DSL
|
3
|
+
def type(type)
|
4
|
+
Schemas::Type.new(type)
|
5
|
+
end
|
6
|
+
alias_method :_, :type
|
7
|
+
|
8
|
+
def Array(*subchemas)
|
9
|
+
if subchemas.count == 1
|
10
|
+
Schemas::VariableLengthArray.new(subchemas.first)
|
11
|
+
else
|
12
|
+
Schemas::FixedLengthArray.new(subchemas)
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
def Boolean
|
17
|
+
Schemas::Boolean.instance
|
18
|
+
end
|
19
|
+
|
20
|
+
def Hash(attribute_hash)
|
21
|
+
Schemas::FixedHash.new(__fixed_hash_attributes(attribute_hash))
|
22
|
+
end
|
23
|
+
|
24
|
+
def Hash_based_on(preexisting_hash_schema, new_attributes_hash)
|
25
|
+
old_attrs = preexisting_hash_schema.attributes
|
26
|
+
.map{ |attr| [attr.key, attr] }
|
27
|
+
.to_h
|
28
|
+
|
29
|
+
new_attrs = __fixed_hash_attributes(new_attributes_hash)
|
30
|
+
.map{ |attr| [attr.key, attr] }
|
31
|
+
.to_h
|
32
|
+
|
33
|
+
merged_attrs = old_attrs.merge(new_attrs)
|
34
|
+
.values
|
35
|
+
.reject{ |attr| attr.value_schema.nil? }
|
36
|
+
|
37
|
+
Schemas::FixedHash.new(merged_attrs)
|
38
|
+
end
|
39
|
+
|
40
|
+
def Set(subschema)
|
41
|
+
Schemas::Set.new(subschema)
|
42
|
+
end
|
43
|
+
|
44
|
+
def optional(key)
|
45
|
+
OptionalWrapper.new(key)
|
46
|
+
end
|
47
|
+
|
48
|
+
def VariableHash(subschemas)
|
49
|
+
unless subschemas.is_a?(Hash) && subschemas.size == 1
|
50
|
+
raise ArgumentError, 'argument must be a Hash of size 1'
|
51
|
+
end
|
52
|
+
|
53
|
+
key_schema, value_schema = subschemas.first
|
54
|
+
Schemas::VariableHash.new(key_schema, value_schema)
|
55
|
+
end
|
56
|
+
|
57
|
+
def maybe(subschema)
|
58
|
+
Schemas::Maybe.new(subschema)
|
59
|
+
end
|
60
|
+
|
61
|
+
def enum(valid_values, subschema=nil)
|
62
|
+
Schemas::Enum.new(valid_values, subschema || type(valid_values.first.class))
|
63
|
+
end
|
64
|
+
|
65
|
+
def either(*subschemas)
|
66
|
+
Schemas::Sum.new(subschemas)
|
67
|
+
end
|
68
|
+
|
69
|
+
def predicate(&block)
|
70
|
+
Schemas::Predicate.new(block)
|
71
|
+
end
|
72
|
+
|
73
|
+
def pipeline(*subschemas)
|
74
|
+
Schemas::Pipeline.new(subschemas)
|
75
|
+
end
|
76
|
+
|
77
|
+
def anything
|
78
|
+
Schemas::Anything.instance
|
79
|
+
end
|
80
|
+
|
81
|
+
def method_missing(sym, *args, &block)
|
82
|
+
type = sym.to_s
|
83
|
+
if type.start_with?('_') && args.empty? && block.nil?
|
84
|
+
constant = Object.const_get(type[1..-1])
|
85
|
+
type(constant)
|
86
|
+
else
|
87
|
+
super
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
OptionalWrapper = Struct.new(:key)
|
92
|
+
|
93
|
+
private
|
94
|
+
|
95
|
+
def __fixed_hash_attributes(attribute_hash)
|
96
|
+
attribute_hash.map do |dsl_key, value_schema|
|
97
|
+
optional = dsl_key.kind_of?(OptionalWrapper)
|
98
|
+
key = optional ? dsl_key.key : dsl_key
|
99
|
+
Schemas::FixedHash::Attribute.new(key, value_schema, optional)
|
100
|
+
end
|
101
|
+
end
|
102
|
+
end
|
103
|
+
end
|