rschema 3.0.1.pre3 → 3.0.1.pre4
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 +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
|
[](https://travis-ci.org/tomdalling/rschema)
|
2
2
|
[](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
|
|