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