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.
@@ -1,377 +1,36 @@
1
- require 'set'
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
- InvalidSchemaError = Class.new(StandardError)
5
- ValidationError = Class.new(StandardError)
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
- PredicateSchema = Struct.new(:name, :block) do
219
- def schema_walk(value, mapper)
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
- MaybeSchema = Struct.new(:subschema) do
233
- def schema_walk(value, mapper)
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
- EnumSchema = Struct.new(:value_set, :subschema) do
248
- def schema_walk(value, mapper)
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
-
@@ -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