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.
@@ -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