rschema 3.0.1.pre3 → 3.0.1.pre4
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.md +278 -330
- data/lib/rschema.rb +104 -17
- data/lib/rschema/coercers.rb +3 -0
- data/lib/rschema/coercers/any.rb +40 -0
- data/lib/rschema/coercers/boolean.rb +30 -0
- data/lib/rschema/coercers/chain.rb +41 -0
- data/lib/rschema/coercers/date.rb +25 -0
- data/lib/rschema/coercers/fixed_hash/default_booleans_to_false.rb +62 -0
- data/lib/rschema/coercers/fixed_hash/remove_extraneous_attributes.rb +42 -0
- data/lib/rschema/coercers/fixed_hash/symbolize_keys.rb +62 -0
- data/lib/rschema/coercers/float.rb +18 -0
- data/lib/rschema/coercers/integer.rb +18 -0
- data/lib/rschema/coercers/symbol.rb +21 -0
- data/lib/rschema/coercers/time.rb +25 -0
- data/lib/rschema/coercion_wrapper.rb +46 -0
- data/lib/rschema/coercion_wrapper/rack_params.rb +21 -0
- data/lib/rschema/dsl.rb +271 -42
- data/lib/rschema/error.rb +12 -30
- data/lib/rschema/options.rb +2 -2
- data/lib/rschema/result.rb +18 -4
- data/lib/rschema/schemas.rb +3 -0
- data/lib/rschema/schemas/anything.rb +14 -12
- data/lib/rschema/schemas/boolean.rb +20 -21
- data/lib/rschema/schemas/coercer.rb +37 -0
- data/lib/rschema/schemas/convenience.rb +53 -0
- data/lib/rschema/schemas/enum.rb +25 -25
- data/lib/rschema/schemas/fixed_hash.rb +110 -91
- data/lib/rschema/schemas/fixed_length_array.rb +48 -48
- data/lib/rschema/schemas/maybe.rb +18 -17
- data/lib/rschema/schemas/pipeline.rb +20 -19
- data/lib/rschema/schemas/predicate.rb +24 -21
- data/lib/rschema/schemas/set.rb +40 -45
- data/lib/rschema/schemas/sum.rb +24 -28
- data/lib/rschema/schemas/type.rb +22 -21
- data/lib/rschema/schemas/variable_hash.rb +53 -52
- data/lib/rschema/schemas/variable_length_array.rb +39 -38
- data/lib/rschema/version.rb +1 -1
- metadata +49 -5
- data/lib/rschema/http_coercer.rb +0 -218
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 039f7db20e1b5e4106983ee4be9a6fa9ec5b3686
|
4
|
+
data.tar.gz: 63ab77eaa9563d038e241c93bfa3a19a038d61c1
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 94b58792f9148118644af0a2aaa18f641cbf33063a8ed9e167372741a6ccb62483d77c46e695c1121c6bfbd8b26e16800ec34650fcf6dac4fd90e9b6900e865f
|
7
|
+
data.tar.gz: 1f60e46c2d4fcecf208617fe651c5c278e4bb1f7449fcde7208a75df3e37c23d8443f0bc8ffc7f676dd3d0e063f3feb78842273762b95140f0666283ad0a8323
|
data/README.md
CHANGED
@@ -1,11 +1,17 @@
|
|
1
1
|
[![Build Status](https://travis-ci.org/tomdalling/rschema.svg?branch=master)](https://travis-ci.org/tomdalling/rschema)
|
2
2
|
[![Test Coverage](https://codeclimate.com/github/tomdalling/rschema/badges/coverage.svg)](https://codeclimate.com/github/tomdalling/rschema/coverage)
|
3
3
|
|
4
|
+
NOTE: These are the docs for the version 3 prerelease
|
5
|
+
=====================================================
|
6
|
+
|
7
|
+
For earlier versions, see the tags.
|
8
|
+
|
9
|
+
|
4
10
|
RSchema
|
5
11
|
=======
|
6
12
|
|
7
13
|
Schema-based validation and coercion for Ruby data structures. Heavily inspired
|
8
|
-
by [Prismatic/schema]
|
14
|
+
by [Prismatic/schema](https://github.com/Prismatic/schema).
|
9
15
|
|
10
16
|
Meet RSchema
|
11
17
|
------------
|
@@ -14,82 +20,72 @@ RSchema provides a way to describe, validate, and coerce the "shape" of data.
|
|
14
20
|
|
15
21
|
First you create a schema:
|
16
22
|
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
}}
|
23
|
-
```
|
23
|
+
blog_post_schema = RSchema.define_hash {{
|
24
|
+
title: _String,
|
25
|
+
tags: Array(_Symbol),
|
26
|
+
body: _String,
|
27
|
+
}}
|
24
28
|
|
25
29
|
Then you can use the schema to validate data:
|
26
30
|
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
blog_post_schema.call(input).valid? #=> true
|
34
|
-
```
|
31
|
+
input = {
|
32
|
+
title: "One Weird Trick Developers Don't Want You To Know!",
|
33
|
+
tags: [:trick, :developers, :unbeleivable],
|
34
|
+
body: '<p>blah blah</p>'
|
35
|
+
}
|
36
|
+
blog_post_schema.call(input).valid? #=> true
|
35
37
|
|
36
38
|
What Is A Schema?
|
37
39
|
-----------------
|
38
40
|
|
39
|
-
Schemas are objects that _describe and validate a
|
41
|
+
Schemas are objects that _describe and validate a value_.
|
40
42
|
|
41
43
|
The simplest schemas are `Type` schemas, which just validate the type of a value.
|
42
44
|
|
43
|
-
|
44
|
-
schema
|
45
|
-
schema.class #=> RSchema::Schemas::Type
|
45
|
+
schema = RSchema.define { _Integer }
|
46
|
+
schema.class #=> RSchema::Schemas::Type
|
46
47
|
|
47
|
-
schema.call(1234).valid? #=> true
|
48
|
-
schema.call('hi').valid? #=> false
|
49
|
-
```
|
48
|
+
schema.call(1234).valid? #=> true
|
49
|
+
schema.call('hi').valid? #=> false
|
50
50
|
|
51
51
|
Then there are composite schemas, which are schemas composed of subschemas.
|
52
52
|
|
53
53
|
Arrays are composite schemas:
|
54
54
|
|
55
|
-
|
56
|
-
schema
|
57
|
-
schema.call([10, 11,
|
58
|
-
schema.call([10, 11, :hi]).valid? #=> false
|
59
|
-
```
|
55
|
+
schema = RSchema.define { Array(_Integer) }
|
56
|
+
schema.call([10, 11, 12]).valid? #=> true
|
57
|
+
schema.call([10, 11, :hi]).valid? #=> false
|
60
58
|
|
61
59
|
And so are hashes:
|
62
60
|
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
end
|
61
|
+
schema = RSchema.define do
|
62
|
+
Hash(fname: _String, age: _Integer)
|
63
|
+
end
|
67
64
|
|
68
|
-
schema.call({ fname: 'Jane', age: 27 }).valid? #=> true
|
69
|
-
schema.call({ fname: 'Johnny' }).valid? #=> false
|
70
|
-
```
|
65
|
+
schema.call({ fname: 'Jane', age: 27 }).valid? #=> true
|
66
|
+
schema.call({ fname: 'Johnny' }).valid? #=> false
|
71
67
|
|
72
68
|
Schema objects are composable – they are designed to be combined.
|
73
69
|
This allows schemas to describe complex, nested data structures.
|
74
70
|
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
}
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
}
|
90
|
-
|
91
|
-
|
92
|
-
|
71
|
+
schema = RSchema.define_hash do
|
72
|
+
{
|
73
|
+
fname: predicate { |n| n.is_a?(String) && n.size > 0 },
|
74
|
+
favourite_foods: Set(_Symbol),
|
75
|
+
children_by_age: VariableHash(_Integer => _String)
|
76
|
+
}
|
77
|
+
end
|
78
|
+
|
79
|
+
input = {
|
80
|
+
fname: 'Johnny',
|
81
|
+
favourite_foods: Set.new([:bacon, :cheese, :onion]),
|
82
|
+
children_by_age: {
|
83
|
+
7 => 'Jenny',
|
84
|
+
5 => 'Simon',
|
85
|
+
},
|
86
|
+
}
|
87
|
+
|
88
|
+
schema.call(input).valid? #=> true
|
93
89
|
|
94
90
|
RSchema provides many different kinds of schema classes for common tasks, but
|
95
91
|
you can also write custom schema classes if you need to.
|
@@ -104,13 +100,11 @@ They can be created manually, although this is often too verbose.
|
|
104
100
|
For example, the following two schemas are identical. `schema1` is created via the
|
105
101
|
DSL, and `schema2` is created manually.
|
106
102
|
|
107
|
-
|
108
|
-
schema1 = RSchema.define { Array(_Symbol) }
|
103
|
+
schema1 = RSchema.define { Array(_Symbol) }
|
109
104
|
|
110
|
-
schema2 = RSchema::Schemas::VariableArray.new(
|
111
|
-
|
112
|
-
)
|
113
|
-
```
|
105
|
+
schema2 = RSchema::Schemas::VariableArray.new(
|
106
|
+
RSchema::Schemas::Type.new(Symbol)
|
107
|
+
)
|
114
108
|
|
115
109
|
You will probably never need to create schemas manually unless you are doing
|
116
110
|
something advanced, like writing your own DSL.
|
@@ -126,31 +120,29 @@ When data fails validation, it is often important to know exactly _which
|
|
126
120
|
values_ were invalid, and _why_. RSchema provides details about every
|
127
121
|
failure within a result object.
|
128
122
|
|
129
|
-
|
130
|
-
|
131
|
-
|
132
|
-
|
133
|
-
|
134
|
-
|
135
|
-
|
136
|
-
|
137
|
-
end
|
123
|
+
schema = RSchema.define do
|
124
|
+
Array(
|
125
|
+
Hash(
|
126
|
+
name: _String,
|
127
|
+
hair: enum([:red, :brown, :blonde, :black])
|
128
|
+
)
|
129
|
+
)
|
130
|
+
end
|
138
131
|
|
139
|
-
input = [
|
140
|
-
|
141
|
-
|
142
|
-
|
143
|
-
|
144
|
-
]
|
132
|
+
input = [
|
133
|
+
{ name: 'Dane', hair: :black },
|
134
|
+
{ name: 'Tom', hair: :brown },
|
135
|
+
{ name: 'Effie', hair: :blond },
|
136
|
+
{ name: 'Chris', hair: :red },
|
137
|
+
]
|
145
138
|
|
146
|
-
result = schema.call(input)
|
139
|
+
result = schema.call(input)
|
147
140
|
|
148
|
-
result.class #=> RSchema::Result
|
149
|
-
result.valid? #=> false
|
150
|
-
result.error #=> { 2 => { :hair => #<RSchema::Error> } }
|
151
|
-
result.error[2][:hair].to_s
|
152
|
-
|
153
|
-
```
|
141
|
+
result.class #=> RSchema::Result
|
142
|
+
result.valid? #=> false
|
143
|
+
result.error #=> { 2 => { :hair => #<RSchema::Error> } }
|
144
|
+
result.error[2][:hair].to_s
|
145
|
+
#=> "Error RSchema::Schemas::Enum/not_a_member for value: :blond"
|
154
146
|
|
155
147
|
The error above says that the value `:blond`, which exists at location
|
156
148
|
`input[2][:hair]`, is not a valid enum member. Looking back at the schema, we
|
@@ -159,20 +151,19 @@ see that there is a typo, and it should be `:blonde` instead of `:blond`.
|
|
159
151
|
Error objects contain a lot of information, which can be used to generate
|
160
152
|
error messages for developers or users.
|
161
153
|
|
162
|
-
|
163
|
-
error
|
164
|
-
|
165
|
-
|
166
|
-
error.
|
167
|
-
error.
|
168
|
-
error.
|
169
|
-
error.to_s #=>
|
170
|
-
|
171
|
-
|
172
|
-
|
173
|
-
|
174
|
-
|
175
|
-
```
|
154
|
+
error = result.error[2][:hair]
|
155
|
+
error.class #=> RSchema::Error
|
156
|
+
|
157
|
+
error.value #=> :blond
|
158
|
+
error.symbolic_name #=> :not_a_member
|
159
|
+
error.schema #=> #<RSchema::Schemas::Enum>
|
160
|
+
error.to_s #=> "Error RSchema::Schemas::Enum/not_a_member for value: :blond"
|
161
|
+
error.to_s(:detailed) #=>
|
162
|
+
# Error: not_a_member
|
163
|
+
# Schema: RSchema::Schemas::Enum
|
164
|
+
# Value: :blond
|
165
|
+
# Vars: nil
|
166
|
+
|
176
167
|
|
177
168
|
Type Schemas
|
178
169
|
------------
|
@@ -180,29 +171,23 @@ Type Schemas
|
|
180
171
|
The most basic kind of schema is a `Type` schema.
|
181
172
|
Type schemas validate the class of a value using `is_a?`.
|
182
173
|
|
183
|
-
|
184
|
-
schema
|
185
|
-
schema.call(
|
186
|
-
schema.call(1234).valid? #=> false
|
187
|
-
```
|
174
|
+
schema = RSchema.define { type(String) }
|
175
|
+
schema.call('hi').valid? #=> true
|
176
|
+
schema.call(1234).valid? #=> false
|
188
177
|
|
189
178
|
Type schemas are so common that the RSchema DSL provides a shorthand way to
|
190
179
|
create them, using an underscore prefix:
|
191
180
|
|
192
|
-
|
193
|
-
|
194
|
-
|
195
|
-
schema2 = RSchema.define { type(Integer) }
|
196
|
-
```
|
181
|
+
schema1 = RSchema.define { _Integer }
|
182
|
+
# is exactly the same as
|
183
|
+
schema2 = RSchema.define { type(Integer) }
|
197
184
|
|
198
185
|
Because type schemas use `is_a?`, they handle subclasses, and can also be used
|
199
186
|
to check for `include`d modules like `Enumerable`:
|
200
187
|
|
201
|
-
|
202
|
-
schema
|
203
|
-
schema.call(
|
204
|
-
schema.call({ a: 1, b: 2 }).valid? #=> true
|
205
|
-
```
|
188
|
+
schema = RSchema.define { _Enumerable }
|
189
|
+
schema.call([1, 2, 3]).valid? #=> true
|
190
|
+
schema.call({ a: 1, b: 2 }).valid? #=> true
|
206
191
|
|
207
192
|
Variable-length Array Schemas
|
208
193
|
-----------------------------
|
@@ -211,14 +196,12 @@ There are two types of array schemas.
|
|
211
196
|
The first type are `VariableLengthArray` schemas, where every element in the
|
212
197
|
array conforms to a single subschema:
|
213
198
|
|
214
|
-
|
215
|
-
schema
|
216
|
-
schema.class #=> RSchema::Schemas::VariableLengthArray
|
199
|
+
schema = RSchema.define { Array(_Symbol) }
|
200
|
+
schema.class #=> RSchema::Schemas::VariableLengthArray
|
217
201
|
|
218
|
-
schema.call([:a, :b, :c]).valid? #=> true
|
219
|
-
schema.call([:a]).valid? #=> true
|
220
|
-
schema.call([]).valid? #=> true
|
221
|
-
```
|
202
|
+
schema.call([:a, :b, :c]).valid? #=> true
|
203
|
+
schema.call([:a]).valid? #=> true
|
204
|
+
schema.call([]).valid? #=> true
|
222
205
|
|
223
206
|
Fixed-length Array Schemas
|
224
207
|
--------------------------
|
@@ -226,14 +209,12 @@ Fixed-length Array Schemas
|
|
226
209
|
There are also `FixedLengthArray` schemas, where the array must have a specific
|
227
210
|
length, and each element of the array has a separate subschema:
|
228
211
|
|
229
|
-
|
230
|
-
schema
|
231
|
-
schema.class #=> RSchema::Schemas::FixedLengthArray
|
212
|
+
schema = RSchema.define{ Array(_Integer, _String) }
|
213
|
+
schema.class #=> RSchema::Schemas::FixedLengthArray
|
232
214
|
|
233
|
-
schema.call([10, 'hello']).valid? #=> true
|
234
|
-
schema.call([22, 'world']).valid? #=> true
|
235
|
-
schema.call(['heyoo', 33]).valid? #=> false
|
236
|
-
```
|
215
|
+
schema.call([10, 'hello']).valid? #=> true
|
216
|
+
schema.call([22, 'world']).valid? #=> true
|
217
|
+
schema.call(['heyoo', 33]).valid? #=> false
|
237
218
|
|
238
219
|
Fixed Hash Schemas
|
239
220
|
------------------
|
@@ -242,37 +223,31 @@ There are also two kinds of hash schemas.
|
|
242
223
|
|
243
224
|
`FixedHash` schemas describes hashes where they keys are known constants:
|
244
225
|
|
245
|
-
|
246
|
-
|
247
|
-
|
248
|
-
end
|
226
|
+
schema = RSchema.define do
|
227
|
+
Hash(name: _String, age: _Integer)
|
228
|
+
end
|
249
229
|
|
250
|
-
schema.call({ name: 'George', age: 2 }).valid? #=> true
|
251
|
-
```
|
230
|
+
schema.call({ name: 'George', age: 2 }).valid? #=> true
|
252
231
|
|
253
232
|
Elements can be optional:
|
254
233
|
|
255
|
-
|
256
|
-
|
257
|
-
|
258
|
-
|
259
|
-
|
260
|
-
|
261
|
-
end
|
234
|
+
schema = RSchema.define do
|
235
|
+
Hash(
|
236
|
+
name: _String,
|
237
|
+
optional(:age) => _Integer,
|
238
|
+
)
|
239
|
+
end
|
262
240
|
|
263
|
-
schema.call({ name: 'Lucy', age: 21 }).valid? #=> true
|
264
|
-
schema.call({ name: 'Tom' }).valid? #=> true
|
265
|
-
```
|
241
|
+
schema.call({ name: 'Lucy', age: 21 }).valid? #=> true
|
242
|
+
schema.call({ name: 'Tom' }).valid? #=> true
|
266
243
|
|
267
244
|
`FixedHash` schemas are common, so the `RSchema.define_hash` method exists
|
268
245
|
to make their creation more convenient:
|
269
246
|
|
270
|
-
|
271
|
-
|
272
|
-
|
273
|
-
|
274
|
-
}}
|
275
|
-
```
|
247
|
+
schema = RSchema.define_hash {{
|
248
|
+
name: _String,
|
249
|
+
optional(:age) => _Integer,
|
250
|
+
}}
|
276
251
|
|
277
252
|
Variable Hash Schemas
|
278
253
|
---------------------
|
@@ -280,66 +255,62 @@ Variable Hash Schemas
|
|
280
255
|
`VariableHash` schemas are for hashes where the keys are _not_ known constants.
|
281
256
|
They contain one subschema for keys, and another subschema for values.
|
282
257
|
|
283
|
-
|
284
|
-
schema
|
285
|
-
schema.call({}).valid? #=> true
|
286
|
-
schema.call({ a: 1 }).valid? #=> true
|
287
|
-
schema.call({ a: 1, b: 2 }).valid? #=> true
|
288
|
-
```
|
258
|
+
schema = RSchema.define { VariableHash(_Symbol => _Integer) }
|
259
|
+
schema.call({}).valid? #=> true
|
260
|
+
schema.call({ a: 1 }).valid? #=> true
|
261
|
+
schema.call({ a: 1, b: 2 }).valid? #=> true
|
289
262
|
|
290
263
|
Other Schema Types
|
291
264
|
------------------
|
292
265
|
|
293
266
|
RSchema provides a few other schema types through its DSL:
|
294
267
|
|
295
|
-
|
296
|
-
|
297
|
-
boolean_schema
|
298
|
-
boolean_schema.call(
|
299
|
-
boolean_schema.call(
|
300
|
-
|
301
|
-
|
302
|
-
|
303
|
-
anything_schema
|
304
|
-
anything_schema.call(
|
305
|
-
anything_schema.call(
|
306
|
-
anything_schema.call(
|
307
|
-
|
308
|
-
|
309
|
-
|
310
|
-
either_schema
|
311
|
-
either_schema.call(
|
312
|
-
either_schema.call(
|
313
|
-
|
314
|
-
|
315
|
-
|
316
|
-
maybe_schema
|
317
|
-
maybe_schema.call(
|
318
|
-
|
319
|
-
|
320
|
-
|
321
|
-
enum_schema
|
322
|
-
enum_schema.call(:
|
323
|
-
|
324
|
-
|
325
|
-
|
326
|
-
|
327
|
-
|
328
|
-
|
329
|
-
predicate_schema.call(
|
330
|
-
|
331
|
-
|
332
|
-
|
333
|
-
|
334
|
-
|
335
|
-
|
336
|
-
|
337
|
-
|
338
|
-
|
339
|
-
pipeline_schema.call(
|
340
|
-
pipeline_schema.call(
|
341
|
-
pipeline_schema.call(-24).valid? #=> false
|
342
|
-
```
|
268
|
+
# boolean (only true or false)
|
269
|
+
boolean_schema = RSchema.define { Boolean() }
|
270
|
+
boolean_schema.call(true).valid? #=> true
|
271
|
+
boolean_schema.call(false).valid? #=> true
|
272
|
+
boolean_schema.call(nil).valid? #=> false
|
273
|
+
|
274
|
+
# anything (literally any value)
|
275
|
+
anything_schema = RSchema.define { anything }
|
276
|
+
anything_schema.call('Hi').valid? #=> true
|
277
|
+
anything_schema.call(true).valid? #=> true
|
278
|
+
anything_schema.call(1234).valid? #=> true
|
279
|
+
anything_schema.call(nil).valid? #=> true
|
280
|
+
|
281
|
+
# either (sum types)
|
282
|
+
either_schema = RSchema.define { either(_String, _Integer, _Float) }
|
283
|
+
either_schema.call('hi').valid? #=> true
|
284
|
+
either_schema.call(5555).valid? #=> true
|
285
|
+
either_schema.call(77.1).valid? #=> true
|
286
|
+
|
287
|
+
# maybe (allows nil)
|
288
|
+
maybe_schema = RSchema.define { maybe(_Integer) }
|
289
|
+
maybe_schema.call(5).valid? #=> true
|
290
|
+
maybe_schema.call(nil).valid? #=> true
|
291
|
+
|
292
|
+
# enum (a set of valid values)
|
293
|
+
enum_schema = RSchema.define { enum([:a, :b, :c]) }
|
294
|
+
enum_schema.call(:a).valid? #=> true
|
295
|
+
enum_schema.call(:z).valid? #=> false
|
296
|
+
|
297
|
+
# predicate (block returns true for valid values)
|
298
|
+
predicate_schema = RSchema.define do
|
299
|
+
predicate { |x| x.even? }
|
300
|
+
end
|
301
|
+
predicate_schema.call(4).valid? #=> true
|
302
|
+
predicate_schema.call(5).valid? #=> false
|
303
|
+
|
304
|
+
# pipeline (apply multiple schemas to a single value, in order)
|
305
|
+
pipeline_schema = RSchema.define do
|
306
|
+
pipeline(
|
307
|
+
either(_Integer, _Float),
|
308
|
+
predicate { |x| x.positive? },
|
309
|
+
)
|
310
|
+
end
|
311
|
+
pipeline_schema.call(123).valid? #=> true
|
312
|
+
pipeline_schema.call(5.1).valid? #=> true
|
313
|
+
pipeline_schema.call(-24).valid? #=> false
|
343
314
|
|
344
315
|
|
345
316
|
Coercion
|
@@ -354,29 +325,27 @@ are often expected to be `Symbol`s, but are also strings. The `HTTPCoercer`
|
|
354
325
|
can automatically convert these strings into the appropriate type, based on a
|
355
326
|
schema.
|
356
327
|
|
357
|
-
|
358
|
-
|
359
|
-
|
360
|
-
|
361
|
-
|
362
|
-
}
|
328
|
+
# Input keys and values are all strings.
|
329
|
+
input_params = {
|
330
|
+
'whatever_id' => '5',
|
331
|
+
'amount' => '123.45',
|
332
|
+
}
|
363
333
|
|
364
|
-
# The schema expects symbol keys, an integer value, and a float value.
|
365
|
-
param_schema = RSchema.define_hash {{
|
366
|
-
|
367
|
-
|
368
|
-
}}
|
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
|
+
}}
|
369
339
|
|
370
|
-
# The schema is wrapped in a HTTPCoercer.
|
371
|
-
coercer = RSchema::HTTPCoercer.wrap(param_schema)
|
340
|
+
# The schema is wrapped in a HTTPCoercer.
|
341
|
+
coercer = RSchema::HTTPCoercer.wrap(param_schema)
|
372
342
|
|
373
|
-
# Use the coercer like a normal schema object.
|
374
|
-
result = coercer.call(input_params)
|
343
|
+
# Use the coercer like a normal schema object.
|
344
|
+
result = coercer.call(input_params)
|
375
345
|
|
376
|
-
# The result object contains the coerced value
|
377
|
-
result.valid? #=> true
|
378
|
-
result.value #=> { :whatever_id => 5, :amount => 123.45 }
|
379
|
-
```
|
346
|
+
# The result object contains the coerced value
|
347
|
+
result.valid? #=> true
|
348
|
+
result.value #=> { :whatever_id => 5, :amount => 123.45 }
|
380
349
|
|
381
350
|
TODO: explain how to create custom coercers
|
382
351
|
|
@@ -385,31 +354,25 @@ Extending The DSL
|
|
385
354
|
|
386
355
|
To add methods to the default DSL, first create a module:
|
387
356
|
|
388
|
-
|
389
|
-
|
390
|
-
|
391
|
-
|
392
|
-
|
393
|
-
|
394
|
-
|
395
|
-
|
396
|
-
end
|
397
|
-
```
|
357
|
+
module MyCustomMethods
|
358
|
+
def palendrome
|
359
|
+
pipeline(
|
360
|
+
_String,
|
361
|
+
predicate { |s| s == s.reverse },
|
362
|
+
)
|
363
|
+
end
|
364
|
+
end
|
398
365
|
|
399
366
|
Then include your module into `RSchema::DefaultDSL`:
|
400
367
|
|
401
|
-
|
402
|
-
RSchema::DefaultDSL.include(MyCustomMethods)
|
403
|
-
```
|
368
|
+
RSchema::DefaultDSL.include(MyCustomMethods)
|
404
369
|
|
405
370
|
And your methods will be available via `RSchema.define`:
|
406
371
|
|
407
|
-
|
408
|
-
schema = RSchema.define { palendrome }
|
372
|
+
schema = RSchema.define { palendrome }
|
409
373
|
|
410
|
-
schema.call('racecar').valid? #=> true
|
411
|
-
schema.call('ferrari').valid? #=> false
|
412
|
-
```
|
374
|
+
schema.call('racecar').valid? #=> true
|
375
|
+
schema.call('ferrari').valid? #=> false
|
413
376
|
|
414
377
|
This is the preferred way for other gems to extend RSchema with new kinds
|
415
378
|
of schema classes.
|
@@ -426,25 +389,21 @@ Create a new class, and include `RSchema::DSL` to get all the standard DSL
|
|
426
389
|
methods that come built-in to RSchema. You can define your own custom methods
|
427
390
|
on this class.
|
428
391
|
|
429
|
-
|
430
|
-
|
431
|
-
include RSchema::DSL
|
392
|
+
class MyCustomDSL
|
393
|
+
include RSchema::DSL
|
432
394
|
|
433
|
-
|
434
|
-
|
435
|
-
|
436
|
-
|
437
|
-
|
438
|
-
|
439
|
-
end
|
440
|
-
```
|
395
|
+
def palendrome
|
396
|
+
pipeline(
|
397
|
+
_String,
|
398
|
+
predicate { |s| s == s.reverse },
|
399
|
+
)
|
400
|
+
end
|
401
|
+
end
|
441
402
|
|
442
403
|
Then simply use `instance_eval` to make use of your custom DSL.
|
443
404
|
|
444
|
-
|
445
|
-
schema
|
446
|
-
schema.call('racecar').valid? #=> true
|
447
|
-
```
|
405
|
+
schema = MyCustomDSL.new.instance_eval { palendrome }
|
406
|
+
schema.call('racecar').valid? #=> true
|
448
407
|
|
449
408
|
See the implementation of `RSchema.define` for reference.
|
450
409
|
|
@@ -459,96 +418,85 @@ Below is a custom schema for pairs – arrays with two elements of the same type
|
|
459
418
|
This is already possible using existing schemas (e.g. `Array(_String, _String)`),
|
460
419
|
and is only shown here for the purpose of demonstration.
|
461
420
|
|
462
|
-
|
463
|
-
|
464
|
-
|
465
|
-
|
466
|
-
|
467
|
-
|
468
|
-
|
469
|
-
|
470
|
-
|
471
|
-
|
472
|
-
|
473
|
-
|
474
|
-
|
475
|
-
|
476
|
-
|
477
|
-
|
421
|
+
class PairSchema
|
422
|
+
def initialize(subschema)
|
423
|
+
@subschema = subschema
|
424
|
+
end
|
425
|
+
|
426
|
+
def call(pair, options=RSchema::Options.default)
|
427
|
+
return not_an_array_failure(pair) unless pair.is_a?(Array)
|
428
|
+
return not_a_pair_failure(pair) unless pair.size == 2
|
429
|
+
|
430
|
+
subresults = pair.map { |x| @subschema.call(x, options) }
|
431
|
+
|
432
|
+
if subresults.all?(&:valid?)
|
433
|
+
RSchema::Result.success(subresults.map(&:value).to_a)
|
434
|
+
else
|
435
|
+
RSchema::Result.failure(subschema_error(subresults))
|
436
|
+
end
|
437
|
+
end
|
438
|
+
|
439
|
+
def with_wrapped_subschemas(wrapper)
|
440
|
+
PairSchema.new(wrapper.wrap(@subschema))
|
441
|
+
end
|
442
|
+
|
443
|
+
private
|
444
|
+
|
445
|
+
def not_an_array_failure(pair)
|
446
|
+
RSchema::Result.failure(
|
447
|
+
RSchema::Error.new(
|
448
|
+
symbolic_name: :not_an_array,
|
449
|
+
schema: self,
|
450
|
+
value: pair,
|
451
|
+
)
|
452
|
+
)
|
453
|
+
end
|
454
|
+
|
455
|
+
def not_a_pair_failure(pair)
|
456
|
+
RSchema::Result.failure(
|
457
|
+
RSchema::Error.new(
|
458
|
+
symbolic_name: :not_a_pair,
|
459
|
+
schema: self,
|
460
|
+
value: pair,
|
461
|
+
vars: {
|
462
|
+
expected_size: 2,
|
463
|
+
actual_size: pair.size,
|
464
|
+
}
|
465
|
+
)
|
466
|
+
)
|
467
|
+
end
|
468
|
+
|
469
|
+
def subschema_error(subresults)
|
470
|
+
subresults
|
471
|
+
.each_with_index
|
472
|
+
.select { |(result, idx)| result.invalid? }
|
473
|
+
.map(&:reverse)
|
474
|
+
.to_h
|
475
|
+
end
|
478
476
|
end
|
479
|
-
end
|
480
|
-
|
481
|
-
def with_wrapped_subschemas(wrapper)
|
482
|
-
PairSchema.new(wrapper.wrap(@subschema))
|
483
|
-
end
|
484
|
-
|
485
|
-
private
|
486
|
-
|
487
|
-
def not_an_array_failure(pair)
|
488
|
-
RSchema::Result.failure(
|
489
|
-
RSchema::Error.new(
|
490
|
-
symbolic_name: :not_an_array,
|
491
|
-
schema: self,
|
492
|
-
value: pair,
|
493
|
-
)
|
494
|
-
)
|
495
|
-
end
|
496
|
-
|
497
|
-
def not_a_pair_failure(pair)
|
498
|
-
RSchema::Result.failure(
|
499
|
-
RSchema::Error.new(
|
500
|
-
symbolic_name: :not_a_pair,
|
501
|
-
schema: self,
|
502
|
-
value: pair,
|
503
|
-
vars: {
|
504
|
-
expected_size: 2,
|
505
|
-
actual_size: pair.size,
|
506
|
-
}
|
507
|
-
)
|
508
|
-
)
|
509
|
-
end
|
510
|
-
|
511
|
-
def subschema_error(subresults)
|
512
|
-
subresults
|
513
|
-
.each_with_index
|
514
|
-
.select { |(result, idx)| result.invalid? }
|
515
|
-
.map(&:reverse)
|
516
|
-
.to_h
|
517
|
-
end
|
518
|
-
end
|
519
|
-
```
|
520
477
|
|
521
478
|
TODO: need to explain how to implement `#call` and `#with_wrapped_subschemas`
|
522
479
|
|
523
480
|
Add your new schema class to the default DSL:
|
524
481
|
|
525
|
-
|
526
|
-
|
527
|
-
|
528
|
-
|
529
|
-
|
530
|
-
end
|
482
|
+
module PairSchemaDSL
|
483
|
+
def pair(subschema)
|
484
|
+
PairSchema.new(subschema)
|
485
|
+
end
|
486
|
+
end
|
531
487
|
|
532
|
-
RSchema::DefaultDSL.include(PairSchemaDSL)
|
533
|
-
```
|
488
|
+
RSchema::DefaultDSL.include(PairSchemaDSL)
|
534
489
|
|
535
490
|
Then your schema is accessible from `RSchema.define`:
|
536
491
|
|
537
|
-
|
538
|
-
gps_coordinate_schema
|
539
|
-
gps_coordinate_schema.call([1.2, 3.4]).valid? #=> true
|
540
|
-
```
|
492
|
+
gps_coordinate_schema = RSchema.define { pair(_Float) }
|
493
|
+
gps_coordinate_schema.call([1.2, 3.4]).valid? #=> true
|
541
494
|
|
542
495
|
Coercion should work, as long as `#with_wrapped_subschemas` was implemented
|
543
496
|
correctly.
|
544
497
|
|
545
|
-
|
546
|
-
|
547
|
-
result
|
548
|
-
result.
|
549
|
-
result.value #=> [1.0, 2.0]
|
550
|
-
```
|
551
|
-
|
552
|
-
|
553
|
-
[Prismatic/schema]: https://github.com/Prismatic/schema
|
498
|
+
coercer = RSchema::HTTPCoercer.wrap(gps_coordinate_schema)
|
499
|
+
result = coercer.call(['1', '2'])
|
500
|
+
result.valid? #=> true
|
501
|
+
result.value #=> [1.0, 2.0]
|
554
502
|
|