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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 215d5577f60ce5957a0f8b6057a82b34bcebe79d
4
- data.tar.gz: a9de23baf8044b7762a8a9611929d67603409eb8
3
+ metadata.gz: 4bf71f16e83556e8bd0ac267c50c3e0bb2b02210
4
+ data.tar.gz: 44e531a24e345526b22def8a8df44c72f111b636
5
5
  SHA512:
6
- metadata.gz: 825c7dfc491f5c9c7eee23e5e437a10086bdc8ddf197ac32b103217d6019c00dca8da565b6e9cde61c7cb50bf23566d154be35354cfa55a6ad03a749e17e2155
7
- data.tar.gz: 90c662e2f3078521e795e772deff2e779d7dd8f1c52e86e038475b1d9357b8716f7ee4c31fb0b974679a7656f65377cd0b40b5f621ce9623eac3a7be6d808519
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
- # RSchema
4
+ RSchema
5
+ =======
5
6
 
6
7
  Schema-based validation and coercion for Ruby data structures. Heavily inspired
7
- by (read: stolen from) [Prismatic/schema][] for Clojure.
8
+ by [Prismatic/schema][].
8
9
 
9
10
  Meet RSchema
10
11
  ------------
11
12
 
12
- A "schema" is a data structure that describes the _shape_ of data.
13
- Schemas are generally just plain old hashes, arrays, and classes.
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
- post_schema = {
17
- title: String,
18
- tags: Array[Symbol],
19
- body: String
20
- }
18
+ blog_post_schema = RSchema.define_hash {{
19
+ title: _String,
20
+ tags: Array(_Symbol),
21
+ body: _String,
22
+ }}
21
23
  ```
22
24
 
23
- Schemas can be used to validate data. That is, they can check whether
24
- data is in the correct shape:
25
+ Then you can use the schema to validate data:
25
26
 
26
27
  ```ruby
27
- RSchema.validate(post_schema, {
28
- title: "You won't beleive how this developer foo'd her bar",
29
- tags: [:foos, :bars, :unbeleivable],
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
- }) #=> true
32
+ }
33
+ blog_post_schema.call(input).valid? #=> true
32
34
  ```
33
35
 
34
- What is a schema?
36
+ What Is A Schema?
35
37
  -----------------
36
38
 
37
- Schemas are Ruby data structures. The simplest type of schema is just a class:
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 = Integer
41
- RSchema.validate(schema, 5) #=> true
42
- RSchema.validate(schema, 'hello') #=> false
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[Integer]
50
- RSchema.validate(schema, [10, 11, 12]) #=> true
51
- RSchema.validate(schema, [10, 11, '12']) #=> false
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 = { fname: String, age: Integer }
58
- RSchema.validate(schema, { fname: 'Jane', age: 27 }) #=> true
59
- RSchema.validate(schema, { fname: 'Johnny' }) #=> false
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
- While schemas are just plain old Ruby data structures, RSchema also provides
63
- an extensible DSL for constructing more complicated schemas:
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.schema {{
76
+ schema = RSchema.define_hash {{
67
77
  fname: predicate { |n| n.is_a?(String) && n.size > 0 },
68
- favourite_foods: set_of(Symbol),
69
- children_by_age: hash_of(Integer => String)
78
+ favourite_foods: Set(_Symbol),
79
+ children_by_age: VariableHash(_Integer => _String)
70
80
  }}
71
81
 
72
- RSchema.validate(schema, {
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
- }) #=> true
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
- Using `RSchema.validate`, it is often difficult to tell exactly which values
86
- are failing validation.
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.schema do
90
- Array[{
91
- name: String,
92
- hair: enum([:red, :brown, :blonde, :black])
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
- value = [
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
- RSchema.validate(schema, value) #=> false
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
- To see exactly where validation fails, we can look at an
107
- `RSchema::ErrorDetails` object.
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
- The `validate!` method throws an exception when validation fails, and the
110
- exception contains the `RSchema::ErrorDetails` object.
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
- RSchema.validate!(schema, value) # throws exception:
114
- #=> RSchema::ValidationError: The value at [2, :hair] is not a valid enum member: :blond
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
- The error above says that the value `:blond`, which exists at location
118
- `value[2][:hair]`, is not a valid enum member. Looking back at the schema, we
119
- see that there is a typo, and it should be `:blonde` instead of `:blond`.
177
+ Type Schemas
178
+ ------------
120
179
 
121
- To get an `RSchema::ErrorDetails` object _without_ using exceptions, we can use
122
- the `RSchema.validation_error` method.
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
- error_details = RSchema.validation_error(schema, value)
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
- error_details.failing_value #=> :blond
128
- error_details.reason #=> "is not a valid enum member"
129
- error_details.key_path #=> [2, :hair]
130
- error_details.to_s #=> "The value at [2, :hair] is not a valid enum member: :blond"
192
+ ```ruby
193
+ schema1 = RSchema.define { _Integer }
194
+ # is exactly the same as
195
+ schema2 = RSchema.define { type(Integer) }
131
196
  ```
132
197
 
133
- Array Schemas
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
- There are two types of array schemas. When the array schema has a single
137
- element, it is a variable-length array schema:
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[Symbol]
141
- RSchema.validate(schema, [:a, :b, :c]) #=> true
142
- RSchema.validate(schema, [:a]) #=> true
143
- RSchema.validate(schema, []) #=> true
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
- Otherwise, it is a fixed-length array schema
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[Integer, String]
150
- RSchema.validate(schema, [10, 'hello']) #=> true
151
- RSchema.validate(schema, [10, 'hello', 'world']) #=> false
152
- RSchema.validate(schema, [10]) #=> false
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
- Hash schemas map constant keys to subschema values:
253
+ Elements can be optional:
159
254
 
160
255
  ```ruby
161
- schema = { fname: String }
162
- RSchema.validate(schema, { fname: 'William' }) #=> true
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
- Keys can be optional:
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.schema {{
169
- :fname => String,
170
- optional(:age) => Integer
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
- There is also another type of hash schema that represents hashes with variable
177
- keys:
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.schema { hash_of(String => Integer) }
181
- RSchema.validate(schema, { 'hello' => 1, 'world' => 2 }) #=> true
182
- RSchema.validate(schema, { 'hello' => 1 }) #=> true
183
- RSchema.validate(schema, {}) #=> true
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.schema{ boolean }
194
- RSchema.validate(boolean_schema, false) #=> true
195
- RSchema.validate(boolean_schema, nil) #=> false
196
-
197
- # any
198
- any_schema = RSchema.schema{ any }
199
- RSchema.validate(any_schema, "Hi") #=> true
200
- RSchema.validate(any_schema, true) #=> true
201
- RSchema.validate(any_schema, false) #=> true
202
- RSchema.validate(any_schema, nil) #=> true
203
-
204
- # either
205
- either_schema = RSchema.schema{ either(String, Integer, Float) }
206
- RSchema.validate(either_schema, 'hi') #=> true
207
- RSchema.validate(either_schema, 5555) #=> true
208
- RSchema.validate(either_schema, 77.1) #=> true
209
- RSchema.validate(either_schema, nil) #=> false
210
-
211
- # maybe
212
- maybe_schema = RSchema.schema{ maybe(Integer) }
213
- RSchema.validate(maybe_schema, 5) #=> true
214
- RSchema.validate(maybe_schema, nil) #=> true
215
-
216
- # enum
217
- enum_schema = RSchema.schema{ enum([:a, :b, :c]) }
218
- RSchema.validate(enum_schema, :a) #=> true
219
- RSchema.validate(enum_schema, :z) #=> false
220
-
221
- # predicate
222
- predicate_schema = RSchema.schema do
223
- predicate('even') { |x| x.even? }
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
- RSchema.validate(predicate_schema, 4) #=> true
226
- RSchema.validate(predicate_schema, 5) #=> false
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
- RSchema is capable of coercing invalid values into valid ones, in some
233
- situations. Here are some examples:
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
- RSchema.coerce!(Symbol, "hello") #=> :hello
237
- RSchema.coerce!(String, :hello) #=> "hello"
238
- RSchema.coerce!(Integer, "5") #=> 5
239
- RSchema.coerce!(Integer, "cat") # !!! raises RSchema::ValidationError !!!
240
- RSchema.coerce!(Set, [1, 2, 3]) #=> <Set: {1, 2, 3}>
241
-
242
- schema = RSchema.schema {{
243
- fname: String,
244
- favourite_foods: set_of(Symbol)
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
- value = {
248
- fname: 'Peggy',
249
- favourite_foods: ['berries', 'cake']
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
- RSchema.coerce!(schema, value)
253
- #=> { fname: "Peggy", favourite_foods: <Set: #{:berries, :cake}> }
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
- Extending the DSL
381
+ TODO: explain how to create custom coercers
382
+
383
+ Extending The DSL
257
384
  -----------------
258
385
 
259
- You can create new, custom DSLs that extend the default DSL like so:
386
+ To add methods to the default DSL, first create a module:
260
387
 
261
388
  ```ruby
262
- module MyCustomDSL
263
- extend RSchema::DSL::Base
264
- def self.positive_and_even(type)
265
- predicate { |x| x > 0 && x.even? }
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
- Pass the custom DSL to `RSchema.schema` to use it:
399
+ Then include your module into `RSchema::DefaultDSL`:
271
400
 
272
401
  ```ruby
273
- schema = RSchema.schema(MyCustomDSL) { positive_and_even }
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
- Any Ruby object can be a schema, as long as it implements the `schema_walk`
282
- method. Here is a schema called `Coordinate`, which is an x/y pair of `Float`s
283
- in an array:
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
- # make the schema type class
287
- class CoordinateSchema
288
- def schema_walk(value, mapper)
289
- # validate `value`
290
- return RSchema::ErrorDetails.new(value, 'is not an Array') unless value.is_a?(Array)
291
- return RSchema::ErrorDetails.new(value, 'does not have two elements') unless value.size == 2
292
-
293
- # walk the subschemas/subvalues
294
- x, x_error = RSchema.walk(Float, value[0], mapper)
295
- y, y_error = RSchema.walk(Float, value[1], mapper)
296
-
297
- # look for subschema errors, and propagate them if found
298
- return x_error.extend_key_path(:x) if x_error
299
- return y_error.extend_key_path(:y) if y_error
300
-
301
- # return the valid value
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
- # add some DSL
307
- module RSchema::DSL
308
- def self.coordinate
309
- CoordinateSchema.new
525
+ ```ruby
526
+ module PairSchemaDSL
527
+ def pair(subschema)
528
+ PairSchema.new(subschema)
310
529
  end
311
530
  end
312
531
 
313
- # use the custom schema type (coercion works too)
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
- The `schema_walk` method receives two arguments:
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
- The `schema_walk` method has three responsibilities:
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
- 1. It must validate the given value. If the value is invalid, the method must
329
- return an `RSchema::ErrorDetails` object. If the value is valid, it must
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
- 2. For composite schemas, it must walk subvalues by calling `RSchema.walk`.
333
- The example above walks two subvalues (`value[0]` and `value[1]`) with the
334
- `Float` schema.
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