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