rschema 3.0.1.pre5 → 3.0.2
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.md +188 -164
- data/lib/rschema.rb +35 -20
- data/lib/rschema/dsl.rb +89 -82
- data/lib/rschema/options.rb +4 -0
- data/lib/rschema/schemas/anything.rb +10 -0
- data/lib/rschema/schemas/boolean.rb +10 -0
- data/lib/rschema/schemas/coercer.rb +8 -0
- data/lib/rschema/schemas/convenience.rb +91 -6
- data/lib/rschema/schemas/enum.rb +10 -0
- data/lib/rschema/schemas/fixed_hash.rb +57 -10
- data/lib/rschema/schemas/fixed_length_array.rb +12 -0
- data/lib/rschema/schemas/maybe.rb +12 -1
- data/lib/rschema/schemas/pipeline.rb +15 -0
- data/lib/rschema/schemas/predicate.rb +11 -0
- data/lib/rschema/schemas/set.rb +10 -0
- data/lib/rschema/schemas/sum.rb +11 -0
- data/lib/rschema/schemas/type.rb +17 -0
- data/lib/rschema/schemas/variable_hash.rb +10 -0
- data/lib/rschema/schemas/variable_length_array.rb +10 -0
- data/lib/rschema/version.rb +1 -1
- metadata +18 -4
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: e971b0b2b3d6a20e7d3a4d30c0426ea3f58e02f7
|
4
|
+
data.tar.gz: a1cacce7540b7a25698c48e5aa1f57847dc55a85
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 05194c41bc61d36b6ebacc94583ab20fa693e8ad3d3cd69858fcdcd3ac110b73e5953180f8fbc9f5f18b3fa9099f210c925136feb08a90cf27efa4c28463e17f
|
7
|
+
data.tar.gz: 2f446b9473c06e7ad75ee426a872ec6843271a86b594d87dc07a282b50b51878ac8cbd9ec512cf1a010c55a0d36faf64c517506ea83ba21c7deb17edd9893fd9
|
data/README.md
CHANGED
@@ -22,7 +22,7 @@ First you create a schema:
|
|
22
22
|
|
23
23
|
blog_post_schema = RSchema.define_hash {{
|
24
24
|
title: _String,
|
25
|
-
tags:
|
25
|
+
tags: array(_Symbol),
|
26
26
|
body: _String,
|
27
27
|
}}
|
28
28
|
|
@@ -33,59 +33,58 @@ Then you can use the schema to validate data:
|
|
33
33
|
tags: [:trick, :developers, :unbeleivable],
|
34
34
|
body: '<p>blah blah</p>'
|
35
35
|
}
|
36
|
-
blog_post_schema.
|
36
|
+
blog_post_schema.valid?(input) #=> true
|
37
37
|
|
38
38
|
What Is A Schema?
|
39
39
|
-----------------
|
40
40
|
|
41
41
|
Schemas are objects that _describe and validate a value_.
|
42
42
|
|
43
|
-
The simplest schemas are `Type` schemas, which just
|
43
|
+
The simplest schemas are `Type` schemas, which just check the type of a value.
|
44
44
|
|
45
45
|
schema = RSchema.define { _Integer }
|
46
46
|
schema.class #=> RSchema::Schemas::Type
|
47
47
|
|
48
|
-
schema.
|
49
|
-
schema.
|
48
|
+
schema.valid?(1234) #=> true
|
49
|
+
schema.valid?('hi') #=> false
|
50
50
|
|
51
51
|
Then there are composite schemas, which are schemas composed of subschemas.
|
52
52
|
|
53
|
-
|
53
|
+
Array schemas are composite schemas:
|
54
54
|
|
55
|
-
schema = RSchema.define {
|
56
|
-
schema.
|
57
|
-
schema.
|
55
|
+
schema = RSchema.define { array(_Integer) }
|
56
|
+
schema.valid?([10, 11, 12]) #=> true
|
57
|
+
schema.valid?([10, 11, :hi]) #=> false
|
58
58
|
|
59
|
-
And so are
|
59
|
+
And so are hash schemas:
|
60
60
|
|
61
|
-
schema = RSchema.
|
62
|
-
|
63
|
-
|
61
|
+
schema = RSchema.define_hash {{
|
62
|
+
fname: _String,
|
63
|
+
age: _Integer,
|
64
|
+
}}
|
64
65
|
|
65
|
-
schema.
|
66
|
-
schema.
|
66
|
+
schema.valid?({ fname: 'Jane', age: 27 }) #=> true
|
67
|
+
schema.valid?({ fname: 'Johnny no age' }) #=> false
|
67
68
|
|
68
69
|
Schema objects are composable – they are designed to be combined.
|
69
70
|
This allows schemas to describe complex, nested data structures.
|
70
71
|
|
71
|
-
schema = RSchema.define_hash
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
}
|
77
|
-
end
|
72
|
+
schema = RSchema.define_hash {{
|
73
|
+
fname: maybe(_String),
|
74
|
+
favourite_foods: set(_Symbol),
|
75
|
+
children_by_age: variable_hash(_Integer => _String)
|
76
|
+
}}
|
78
77
|
|
79
78
|
input = {
|
80
|
-
fname:
|
81
|
-
favourite_foods: Set
|
79
|
+
fname: nil,
|
80
|
+
favourite_foods: Set[:bacon, :cheese, :onion],
|
82
81
|
children_by_age: {
|
83
82
|
7 => 'Jenny',
|
84
83
|
5 => 'Simon',
|
85
84
|
},
|
86
85
|
}
|
87
86
|
|
88
|
-
schema.
|
87
|
+
schema.valid?(input) #=> true
|
89
88
|
|
90
89
|
RSchema provides many different kinds of schema classes for common tasks, but
|
91
90
|
you can also write custom schema classes if you need to.
|
@@ -95,34 +94,35 @@ The DSL
|
|
95
94
|
-------
|
96
95
|
|
97
96
|
Schemas are usually created and composed via a DSL using `RSchema.define`.
|
98
|
-
|
97
|
+
Schemas can be created without the DSL, but the DSL is much more succinct.
|
99
98
|
|
100
99
|
For example, the following two schemas are identical. `schema1` is created via the
|
101
100
|
DSL, and `schema2` is created manually.
|
102
101
|
|
103
|
-
schema1 = RSchema.define {
|
102
|
+
schema1 = RSchema.define { array(_Symbol) }
|
104
103
|
|
105
|
-
schema2 = RSchema::Schemas::
|
106
|
-
RSchema::Schemas::
|
104
|
+
schema2 = RSchema::Schemas::Convenience.wrap(
|
105
|
+
RSchema::Schemas::VariableLengthArray.new(
|
106
|
+
RSchema::Schemas::Type.new(Symbol)
|
107
|
+
)
|
107
108
|
)
|
108
109
|
|
109
|
-
You will probably
|
110
|
-
something advanced
|
110
|
+
You will probably not need to create schemas manually unless you are doing
|
111
|
+
something advanced.
|
111
112
|
|
112
|
-
|
113
|
-
default DSL, or create a separate, custom DSL to suite your needs.
|
113
|
+
For a full list of DSL methods, see the API documentation for `RSchema::DSL`.
|
114
114
|
|
115
115
|
|
116
|
-
When Validation Fails
|
117
|
-
|
116
|
+
Errors (When Validation Fails)
|
117
|
+
------------------------------
|
118
118
|
|
119
|
-
When
|
119
|
+
When something fails validation, it is often important to know exactly _which
|
120
120
|
values_ were invalid, and _why_. RSchema provides details about every
|
121
121
|
failure within a result object.
|
122
122
|
|
123
123
|
schema = RSchema.define do
|
124
|
-
|
125
|
-
|
124
|
+
array(
|
125
|
+
fixed_hash(
|
126
126
|
name: _String,
|
127
127
|
hair: enum([:red, :brown, :blonde, :black])
|
128
128
|
)
|
@@ -136,13 +136,11 @@ failure within a result object.
|
|
136
136
|
{ name: 'Chris', hair: :red },
|
137
137
|
]
|
138
138
|
|
139
|
-
result = schema.
|
139
|
+
result = schema.validate(input)
|
140
140
|
|
141
|
-
result.class
|
141
|
+
result.class #=> RSchema::Result
|
142
142
|
result.valid? #=> false
|
143
|
-
result.error
|
144
|
-
result.error[2][:hair].to_s
|
145
|
-
#=> "Error RSchema::Schemas::Enum/not_a_member for value: :blond"
|
143
|
+
result.error #=> { 2 => { :hair => #<RSchema::Error> } }
|
146
144
|
|
147
145
|
The error above says that the value `:blond`, which exists at location
|
148
146
|
`input[2][:hair]`, is not a valid enum member. Looking back at the schema, we
|
@@ -156,13 +154,11 @@ error messages for developers or users.
|
|
156
154
|
|
157
155
|
error.value #=> :blond
|
158
156
|
error.symbolic_name #=> :not_a_member
|
159
|
-
error.schema #=> #<RSchema::Schemas::Enum
|
160
|
-
error.
|
161
|
-
error.to_s
|
162
|
-
|
163
|
-
|
164
|
-
# Value: :blond
|
165
|
-
# Vars: nil
|
157
|
+
error.schema #=> #<RSchema::Schemas::Enum ...>
|
158
|
+
error.schema.members #=> [:red, :brown, :blonde, :black]
|
159
|
+
error.to_s #=> "RSchema::Schemas::Enum/not_a_member"
|
160
|
+
error.inspect
|
161
|
+
#=> "<RSchema::Error RSchema::Schemas::Enum/not_a_member value=:blond>"
|
166
162
|
|
167
163
|
|
168
164
|
Type Schemas
|
@@ -172,8 +168,8 @@ The most basic kind of schema is a `Type` schema.
|
|
172
168
|
Type schemas validate the class of a value using `is_a?`.
|
173
169
|
|
174
170
|
schema = RSchema.define { type(String) }
|
175
|
-
schema.
|
176
|
-
schema.
|
171
|
+
schema.valid?('hi') #=> true
|
172
|
+
schema.valid?(1234) #=> false
|
177
173
|
|
178
174
|
Type schemas are so common that the RSchema DSL provides a shorthand way to
|
179
175
|
create them, using an underscore prefix:
|
@@ -186,22 +182,21 @@ Because type schemas use `is_a?`, they handle subclasses, and can also be used
|
|
186
182
|
to check for `include`d modules like `Enumerable`:
|
187
183
|
|
188
184
|
schema = RSchema.define { _Enumerable }
|
189
|
-
schema.
|
190
|
-
schema.
|
185
|
+
schema.valid?([1, 2, 3]) #=> true
|
186
|
+
schema.valid?({ a: 12 }) #=> true
|
191
187
|
|
192
188
|
Variable-length Array Schemas
|
193
189
|
-----------------------------
|
194
190
|
|
195
191
|
There are two types of array schemas.
|
196
|
-
The first type are `VariableLengthArray` schemas,
|
197
|
-
array
|
192
|
+
The first type are `VariableLengthArray` schemas, which check that all elements
|
193
|
+
in the array conform to a single subschema:
|
198
194
|
|
199
|
-
schema = RSchema.define {
|
200
|
-
schema.class #=> RSchema::Schemas::VariableLengthArray
|
195
|
+
schema = RSchema.define { array(_Symbol) }
|
201
196
|
|
202
|
-
schema.
|
203
|
-
schema.
|
204
|
-
schema.
|
197
|
+
schema.valid?([:a, :b, :c]) #=> true
|
198
|
+
schema.valid?([:a]) #=> true
|
199
|
+
schema.valid?([]) #=> true
|
205
200
|
|
206
201
|
Fixed-length Array Schemas
|
207
202
|
--------------------------
|
@@ -209,37 +204,38 @@ Fixed-length Array Schemas
|
|
209
204
|
There are also `FixedLengthArray` schemas, where the array must have a specific
|
210
205
|
length, and each element of the array has a separate subschema:
|
211
206
|
|
212
|
-
schema = RSchema.define{
|
213
|
-
schema.class #=> RSchema::Schemas::FixedLengthArray
|
207
|
+
schema = RSchema.define { array(_Integer, _String) }
|
214
208
|
|
215
|
-
schema.
|
216
|
-
schema.
|
217
|
-
schema.call(['heyoo', 33]).valid? #=> false
|
209
|
+
schema.valid?([10, 'hello']) #=> true
|
210
|
+
schema.valid?(['heyoo', 33]) #=> false
|
218
211
|
|
219
212
|
Fixed Hash Schemas
|
220
213
|
------------------
|
221
214
|
|
222
215
|
There are also two kinds of hash schemas.
|
223
216
|
|
224
|
-
`FixedHash` schemas
|
217
|
+
`FixedHash` schemas describe hashes where they keys are known constants:
|
225
218
|
|
226
219
|
schema = RSchema.define do
|
227
|
-
|
220
|
+
fixed_hash(
|
221
|
+
name: _String,
|
222
|
+
age: _Integer,
|
223
|
+
)
|
228
224
|
end
|
229
225
|
|
230
|
-
schema.
|
226
|
+
schema.valid?({ name: 'George', age: 2 }) #=> true
|
231
227
|
|
232
228
|
Elements can be optional:
|
233
229
|
|
234
230
|
schema = RSchema.define do
|
235
|
-
|
231
|
+
fixed_hash(
|
236
232
|
name: _String,
|
237
233
|
optional(:age) => _Integer,
|
238
234
|
)
|
239
235
|
end
|
240
236
|
|
241
|
-
schema.
|
242
|
-
schema.
|
237
|
+
schema.valid?({ name: 'Lucy', age: 21 }) #=> true
|
238
|
+
schema.valid?({ name: 'Ageless Tommy' }) #=> true
|
243
239
|
|
244
240
|
`FixedHash` schemas are common, so the `RSchema.define_hash` method exists
|
245
241
|
to make their creation more convenient:
|
@@ -252,13 +248,13 @@ to make their creation more convenient:
|
|
252
248
|
Variable Hash Schemas
|
253
249
|
---------------------
|
254
250
|
|
255
|
-
`VariableHash` schemas are for hashes where the keys are _not_ known
|
251
|
+
`VariableHash` schemas are for hashes where the keys are _not_ known ahead of time.
|
256
252
|
They contain one subschema for keys, and another subschema for values.
|
257
253
|
|
258
|
-
schema = RSchema.define {
|
259
|
-
schema.
|
260
|
-
schema.
|
261
|
-
schema.
|
254
|
+
schema = RSchema.define { variable_hash(_Symbol => _Integer) }
|
255
|
+
schema.valid?({}) #=> true
|
256
|
+
schema.valid?({ a: 1 }) #=> true
|
257
|
+
schema.valid?({ a: 1, b: 2 }) #=> true
|
262
258
|
|
263
259
|
Other Schema Types
|
264
260
|
------------------
|
@@ -266,40 +262,40 @@ Other Schema Types
|
|
266
262
|
RSchema provides a few other schema types through its DSL:
|
267
263
|
|
268
264
|
# boolean (only true or false)
|
269
|
-
boolean_schema = RSchema.define {
|
270
|
-
boolean_schema.
|
271
|
-
boolean_schema.
|
272
|
-
boolean_schema.
|
265
|
+
boolean_schema = RSchema.define { boolean }
|
266
|
+
boolean_schema.valid?(true) #=> true
|
267
|
+
boolean_schema.valid?(false) #=> true
|
268
|
+
boolean_schema.valid?(nil) #=> false
|
273
269
|
|
274
270
|
# anything (literally any value)
|
275
271
|
anything_schema = RSchema.define { anything }
|
276
|
-
anything_schema.
|
277
|
-
anything_schema.
|
278
|
-
anything_schema.
|
279
|
-
anything_schema.
|
272
|
+
anything_schema.valid?('Hi') #=> true
|
273
|
+
anything_schema.valid?(true) #=> true
|
274
|
+
anything_schema.valid?(1234) #=> true
|
275
|
+
anything_schema.valid?(nil) #=> true
|
280
276
|
|
281
277
|
# either (sum types)
|
282
278
|
either_schema = RSchema.define { either(_String, _Integer, _Float) }
|
283
|
-
either_schema.
|
284
|
-
either_schema.
|
285
|
-
either_schema.
|
279
|
+
either_schema.valid?('hi') #=> true
|
280
|
+
either_schema.valid?(5555) #=> true
|
281
|
+
either_schema.valid?(77.2) #=> true
|
286
282
|
|
287
283
|
# maybe (allows nil)
|
288
284
|
maybe_schema = RSchema.define { maybe(_Integer) }
|
289
|
-
maybe_schema.
|
290
|
-
maybe_schema.
|
285
|
+
maybe_schema.valid?(5) #=> true
|
286
|
+
maybe_schema.valid?(nil) #=> true
|
291
287
|
|
292
288
|
# enum (a set of valid values)
|
293
289
|
enum_schema = RSchema.define { enum([:a, :b, :c]) }
|
294
|
-
enum_schema.
|
295
|
-
enum_schema.
|
290
|
+
enum_schema.valid?(:a) #=> true
|
291
|
+
enum_schema.valid?(:z) #=> false
|
296
292
|
|
297
293
|
# predicate (block returns true for valid values)
|
298
294
|
predicate_schema = RSchema.define do
|
299
295
|
predicate { |x| x.even? }
|
300
296
|
end
|
301
|
-
predicate_schema.
|
302
|
-
predicate_schema.
|
297
|
+
predicate_schema.valid?(4) #=> true
|
298
|
+
predicate_schema.valid?(5) #=> false
|
303
299
|
|
304
300
|
# pipeline (apply multiple schemas to a single value, in order)
|
305
301
|
pipeline_schema = RSchema.define do
|
@@ -308,46 +304,12 @@ RSchema provides a few other schema types through its DSL:
|
|
308
304
|
predicate { |x| x.positive? },
|
309
305
|
)
|
310
306
|
end
|
311
|
-
pipeline_schema.
|
312
|
-
pipeline_schema.
|
313
|
-
pipeline_schema.
|
314
|
-
|
315
|
-
|
316
|
-
Coercion
|
317
|
-
--------
|
318
|
-
|
319
|
-
Coercers convert invalid data into valid data where possible, according to a
|
320
|
-
schema.
|
321
|
-
|
322
|
-
Take HTTP params as an example. Web forms often contain database IDs, which
|
323
|
-
are integers, but are submitted as strings by the browser. Param hash keys
|
324
|
-
are often expected to be `Symbol`s, but are also strings. The `HTTPCoercer`
|
325
|
-
can automatically convert these strings into the appropriate type, based on a
|
326
|
-
schema.
|
327
|
-
|
328
|
-
# Input keys and values are all strings.
|
329
|
-
input_params = {
|
330
|
-
'whatever_id' => '5',
|
331
|
-
'amount' => '123.45',
|
332
|
-
}
|
333
|
-
|
334
|
-
# The schema expects symbol keys, an integer value, and a float value.
|
335
|
-
param_schema = RSchema.define_hash {{
|
336
|
-
whatever_id: _Integer,
|
337
|
-
amount: _Float,
|
338
|
-
}}
|
339
|
-
|
340
|
-
# The schema is wrapped in a HTTPCoercer.
|
341
|
-
coercer = RSchema::HTTPCoercer.wrap(param_schema)
|
342
|
-
|
343
|
-
# Use the coercer like a normal schema object.
|
344
|
-
result = coercer.call(input_params)
|
345
|
-
|
346
|
-
# The result object contains the coerced value
|
347
|
-
result.valid? #=> true
|
348
|
-
result.value #=> { :whatever_id => 5, :amount => 123.45 }
|
307
|
+
pipeline_schema.valid?(123) #=> true
|
308
|
+
pipeline_schema.valid?(5.1) #=> true
|
309
|
+
pipeline_schema.valid?(-24) #=> false
|
349
310
|
|
350
|
-
|
311
|
+
For a full list of built-in schema types, see the API documentation for all
|
312
|
+
classes in the `RSchema::Schemas` module.
|
351
313
|
|
352
314
|
Extending The DSL
|
353
315
|
-----------------
|
@@ -371,26 +333,25 @@ And your methods will be available via `RSchema.define`:
|
|
371
333
|
|
372
334
|
schema = RSchema.define { palendrome }
|
373
335
|
|
374
|
-
schema.
|
375
|
-
schema.
|
376
|
-
|
377
|
-
This is the preferred way for other gems to extend RSchema with new kinds
|
378
|
-
of schema classes.
|
336
|
+
schema.valid?('racecar') #=> true
|
337
|
+
schema.valid?('ferrari') #=> false
|
379
338
|
|
339
|
+
This is the preferred way for you, and other gems, to extend RSchema with new
|
340
|
+
DSL methods.
|
380
341
|
|
381
342
|
Creating Your Own DSL
|
382
343
|
---------------------
|
383
344
|
|
384
|
-
The default DSL is designed to be extended (i.e. modified) by
|
385
|
-
If you want a DSL that isn't affected by external factors,
|
386
|
-
yourself.
|
345
|
+
The default DSL is designed to be extended (i.e. modified) by you, and
|
346
|
+
third-party gems. If you want a DSL that isn't affected by external factors,
|
347
|
+
you can create one yourself.
|
387
348
|
|
388
|
-
Create a new class, and include `RSchema::DSL`
|
389
|
-
methods that come built-in to RSchema. You can define your own
|
390
|
-
on this class.
|
349
|
+
Create a new class, and include `RSchema::DSL` if you want have all the
|
350
|
+
standard DSL methods that come built-in to RSchema. You can define your own
|
351
|
+
custom methods on this class.
|
391
352
|
|
392
353
|
class MyCustomDSL
|
393
|
-
include RSchema::DSL
|
354
|
+
include RSchema::DSL # this is optional
|
394
355
|
|
395
356
|
def palendrome
|
396
357
|
pipeline(
|
@@ -400,22 +361,76 @@ on this class.
|
|
400
361
|
end
|
401
362
|
end
|
402
363
|
|
403
|
-
Then
|
364
|
+
Then pass an instance of your DSL class into `RSchema.define`:
|
404
365
|
|
405
|
-
|
406
|
-
schema.
|
366
|
+
dsl = MyCustomDSL.new
|
367
|
+
schema = RSchema.define(dsl) { palendrome }
|
368
|
+
schema.valid?('racecar') #=> true
|
407
369
|
|
408
|
-
|
370
|
+
Coercion
|
371
|
+
--------
|
409
372
|
|
373
|
+
Coercers convert invalid data into valid data where possible, according to a
|
374
|
+
schema.
|
375
|
+
|
376
|
+
Take HTTP params as an example. Web forms often contain database IDs, which
|
377
|
+
are integers, but are submitted as strings by the browser. Param hash keys
|
378
|
+
are often expected to be `Symbol`s, but are also strings. RSchema can
|
379
|
+
automatically convert these strings into the appropriate type, based on a
|
380
|
+
schema.
|
410
381
|
|
411
|
-
|
412
|
-
|
382
|
+
# Input keys and values are all strings
|
383
|
+
input_params = {
|
384
|
+
'whatever_id' => '5',
|
385
|
+
'amount' => '123.45',
|
386
|
+
}
|
387
|
+
|
388
|
+
# The schema expects symbol keys, an integer value, and a float value
|
389
|
+
schema = RSchema.define_hash {{
|
390
|
+
whatever_id: _Integer,
|
391
|
+
amount: _Float,
|
392
|
+
}}
|
393
|
+
|
394
|
+
# A coercer is created by wrapping the schema
|
395
|
+
coercer = RSchema::CoercionWrapper::RACK_PARAMS.wrap(schema)
|
396
|
+
|
397
|
+
# Use the coercer like a normal schema object
|
398
|
+
result = coercer.validate(input_params)
|
399
|
+
|
400
|
+
# The result object contains the coerced value
|
401
|
+
result.valid? #=> true
|
402
|
+
result.value #=> { :whatever_id => 5, :amount => 123.45 }
|
403
|
+
|
404
|
+
Custom Coercion
|
405
|
+
---------------
|
406
|
+
|
407
|
+
Coercion is designed to be totally extensible. You'll have to take my word
|
408
|
+
for it, because there isn't much documentation at the moment.
|
409
|
+
|
410
|
+
See `lib/rschema/coercion_wrapper/rack_params.rb` for an example of how to
|
411
|
+
make a coercion wrapper.
|
412
|
+
|
413
|
+
See all the classes in `lib/rschema/coercers/` for examples of how to make
|
414
|
+
individual coercers.
|
415
|
+
|
416
|
+
If you have any problems or questions about implementing custom coercion, feel
|
417
|
+
free to contact me (Tom Dalling).
|
418
|
+
|
419
|
+
|
420
|
+
Implementing Your Own Schema Types
|
421
|
+
----------------------------------
|
413
422
|
|
414
423
|
Schemas are objects that conform to a certain interface (i.e. a duck type).
|
415
424
|
To create your own schema types, you just need to implement this interface.
|
416
425
|
|
426
|
+
The interface consists of two methods: `call` and `with_wrapped_subschemas`.
|
427
|
+
The `call` method is the interface for validating values.
|
428
|
+
The `with_wrapped_subschemas` is necessary for coercion to work.
|
429
|
+
If you have any problems or questions regarding this schema interface, feel
|
430
|
+
free to contact me (Tom Dalling).
|
431
|
+
|
417
432
|
Below is a custom schema for pairs – arrays with two elements of the same type.
|
418
|
-
This is already possible using existing schemas (e.g. `
|
433
|
+
This is already possible using existing schemas (e.g. `array(_String, _String)`),
|
419
434
|
and is only shown here for the purpose of demonstration.
|
420
435
|
|
421
436
|
class PairSchema
|
@@ -423,19 +438,31 @@ and is only shown here for the purpose of demonstration.
|
|
423
438
|
@subschema = subschema
|
424
439
|
end
|
425
440
|
|
426
|
-
|
441
|
+
#
|
442
|
+
# This method is mandatory.
|
443
|
+
#
|
444
|
+
# `pair` is the value to validate.
|
445
|
+
# `options` is an `RSchema::Options` object.
|
446
|
+
# This method must return a `RSchema::Result` object
|
447
|
+
#
|
448
|
+
def call(pair, options)
|
427
449
|
return not_an_array_failure(pair) unless pair.is_a?(Array)
|
428
450
|
return not_a_pair_failure(pair) unless pair.size == 2
|
429
451
|
|
452
|
+
# pass both array elements to `@subschema.call`
|
430
453
|
subresults = pair.map { |x| @subschema.call(x, options) }
|
431
454
|
|
455
|
+
# check if both elements are valid according to @subschema
|
432
456
|
if subresults.all?(&:valid?)
|
433
|
-
RSchema::Result.success(subresults.map(&:value)
|
457
|
+
RSchema::Result.success(subresults.map(&:value))
|
434
458
|
else
|
435
459
|
RSchema::Result.failure(subschema_error(subresults))
|
436
460
|
end
|
437
461
|
end
|
438
462
|
|
463
|
+
#
|
464
|
+
# This method is necessary for coercion to work
|
465
|
+
#
|
439
466
|
def with_wrapped_subschemas(wrapper)
|
440
467
|
PairSchema.new(wrapper.wrap(@subschema))
|
441
468
|
end
|
@@ -458,25 +485,22 @@ and is only shown here for the purpose of demonstration.
|
|
458
485
|
symbolic_name: :not_a_pair,
|
459
486
|
schema: self,
|
460
487
|
value: pair,
|
461
|
-
vars: {
|
462
|
-
expected_size: 2,
|
463
|
-
actual_size: pair.size,
|
464
|
-
}
|
465
488
|
)
|
466
489
|
)
|
467
490
|
end
|
468
491
|
|
492
|
+
#
|
493
|
+
# Returns a hash of index => error
|
494
|
+
#
|
469
495
|
def subschema_error(subresults)
|
470
496
|
subresults
|
471
497
|
.each_with_index
|
472
498
|
.select { |(result, idx)| result.invalid? }
|
473
|
-
.map(
|
499
|
+
.map { |(result, idx)| [idx, result.error] }
|
474
500
|
.to_h
|
475
501
|
end
|
476
502
|
end
|
477
503
|
|
478
|
-
TODO: need to explain how to implement `#call` and `#with_wrapped_subschemas`
|
479
|
-
|
480
504
|
Add your new schema class to the default DSL:
|
481
505
|
|
482
506
|
module PairSchemaDSL
|
@@ -490,13 +514,13 @@ Add your new schema class to the default DSL:
|
|
490
514
|
Then your schema is accessible from `RSchema.define`:
|
491
515
|
|
492
516
|
gps_coordinate_schema = RSchema.define { pair(_Float) }
|
493
|
-
gps_coordinate_schema.
|
517
|
+
gps_coordinate_schema.valid?([1.2, 3.4]) #=> true
|
494
518
|
|
495
519
|
Coercion should work, as long as `#with_wrapped_subschemas` was implemented
|
496
520
|
correctly.
|
497
521
|
|
498
|
-
coercer = RSchema::
|
499
|
-
result = coercer.
|
522
|
+
coercer = RSchema::CoercionWrapper::RACK_PARAMS.wrap(gps_coordinate_schema)
|
523
|
+
result = coercer.validate(["1", "2"])
|
500
524
|
result.valid? #=> true
|
501
525
|
result.value #=> [1.0, 2.0]
|
502
526
|
|