rschema 2.4.0 → 3.0.1.pre1
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/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
|