rschema 3.0.1.pre5 → 3.0.2
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 +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
|
|