datacaster 2.0.2 → 3.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (45) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +604 -287
  3. data/config/locales/en.yml +25 -0
  4. data/datacaster.gemspec +3 -1
  5. data/lib/datacaster/absent.rb +4 -0
  6. data/lib/datacaster/and_node.rb +3 -5
  7. data/lib/datacaster/and_with_error_aggregation_node.rb +5 -6
  8. data/lib/datacaster/array_schema.rb +18 -16
  9. data/lib/datacaster/base.rb +33 -44
  10. data/lib/datacaster/caster.rb +4 -8
  11. data/lib/datacaster/checker.rb +8 -10
  12. data/lib/datacaster/comparator.rb +9 -9
  13. data/lib/datacaster/config.rb +28 -0
  14. data/lib/datacaster/context_node.rb +43 -0
  15. data/lib/datacaster/context_nodes/errors_caster.rb +21 -0
  16. data/lib/datacaster/context_nodes/i18n.rb +20 -0
  17. data/lib/datacaster/context_nodes/i18n_keys_mapper.rb +27 -0
  18. data/lib/datacaster/context_nodes/pass_if.rb +11 -0
  19. data/lib/datacaster/context_nodes/structure_cleaner.rb +103 -0
  20. data/lib/datacaster/context_nodes/user_context.rb +20 -0
  21. data/lib/datacaster/definition_dsl.rb +36 -0
  22. data/lib/datacaster/hash_mapper.rb +13 -16
  23. data/lib/datacaster/hash_schema.rb +14 -15
  24. data/lib/datacaster/i18n_values/base.rb +87 -0
  25. data/lib/datacaster/i18n_values/key.rb +34 -0
  26. data/lib/datacaster/i18n_values/scope.rb +28 -0
  27. data/lib/datacaster/message_keys_merger.rb +8 -15
  28. data/lib/datacaster/or_node.rb +3 -4
  29. data/lib/datacaster/predefined.rb +150 -65
  30. data/lib/datacaster/result.rb +39 -18
  31. data/lib/datacaster/runtimes/base.rb +47 -0
  32. data/lib/datacaster/runtimes/i18n.rb +20 -0
  33. data/lib/datacaster/runtimes/structure_cleaner.rb +47 -0
  34. data/lib/datacaster/runtimes/user_context.rb +39 -0
  35. data/lib/datacaster/substitute_i18n.rb +48 -0
  36. data/lib/datacaster/switch_node.rb +72 -0
  37. data/lib/datacaster/then_node.rb +7 -8
  38. data/lib/datacaster/transformer.rb +4 -8
  39. data/lib/datacaster/trier.rb +9 -11
  40. data/lib/datacaster/validator.rb +8 -9
  41. data/lib/datacaster/version.rb +1 -1
  42. data/lib/datacaster.rb +15 -35
  43. metadata +60 -10
  44. data/lib/datacaster/definition_context.rb +0 -20
  45. data/lib/datacaster/terminator.rb +0 -98
data/README.md CHANGED
@@ -1,9 +1,10 @@
1
1
  # Datacaster
2
2
 
3
- This gem provides run-time type checking and mapping of composite data structures (i.e. hashes/arrays of hashes/arrays of ... of literals).
3
+ This gem provides DSL for describing in a composable manner and performing run-time type checks and transformations of composite data structures (i.e. hashes/arrays of literals). Inspired by several concepts of functional programming such as monads.
4
4
 
5
- Its main use is in the validation and preliminary transformation of API params requests.
5
+ Detailed error-reporting (with full i18n support) is one of a distinct features. Let your API consumer know precisely which fields and in a what manner are wrong in a deeply nested structure!
6
6
 
7
+ It is currently used in production in several projects (mainly as request parameter validator).
7
8
 
8
9
  # Table of contents
9
10
 
@@ -14,55 +15,62 @@ Its main use is in the validation and preliminary transformation of API params r
14
15
  - [Result value](#result-value)
15
16
  - [Hash schema](#hash-schema)
16
17
  - [Logical operators](#logical-operators)
17
- - [*AND operator*:](#and-operator)
18
- - [*OR operator*:](#or-operator)
19
- - [*IF... THEN... ELSE operator*:](#if-then-else-operator)
18
+ - [*AND operator*](#and-operator)
19
+ - [*OR operator*](#or-operator)
20
+ - [*IF... THEN... ELSE operator*](#if-then-else-operator)
21
+ - [*SWITCH... ON... ELSE operator*](#switch-on-else-operator)
20
22
  - [Built-in types](#built-in-types)
21
23
  - [Basic types](#basic-types)
22
- - [`string`](#string)
23
- - [`integer`](#integer)
24
- - [`float`](#float)
25
- - [`decimal([digits = 8])`](#decimaldigits--8)
26
- - [`array`](#array)
27
- - [`hash_value`](#hash_value)
24
+ - [`array(error_key = nil)`](#arrayerror_key--nil)
25
+ - [`decimal(digits = 8, error_key = nil)`](#decimaldigits--8-error_key--nil)
26
+ - [`float(error_key = nil)`](#floaterror_key--nil)
27
+ - [`hash_value(error_key = nil)`](#hash_valueerror_key--nil)
28
+ - [`integer(error_key = nil)`](#integererror_key--nil)
29
+ - [`string(error_key = nil)`](#stringerror_key--nil)
28
30
  - [Convenience types](#convenience-types)
29
- - [`non_empty_string`](#non_empty_string)
30
- - [`hash_with_symbolized_keys`](#hash_with_symbolized_keys)
31
- - [`integer32`](#integer32)
31
+ - [`hash_with_symbolized_keys(error_key = nil)`](#hash_with_symbolized_keyserror_key--nil)
32
+ - [`integer32(error_key = nil)`](#integer32error_key--nil)
33
+ - [`non_empty_string(error_key = nil)`](#non_empty_stringerror_key--nil)
32
34
  - [Special types](#special-types)
33
- - [`absent`](#absent)
34
- - [`any`](#any)
35
- - [`transform_to_value(value)`](#transform_to_valuevalue)
36
- - [`remove`](#remove)
37
- - [`pass`](#pass)
38
- - [`responds_to(method)`](#responds_tomethod)
39
- - [`must_be(klass)`](#must_beklass)
35
+ - [`absent(error_key = nil)`](#absenterror_key--nil)
36
+ - [`any(error_key = nil)`](#anyerror_key--nil)
37
+ - [`default(default_value, on: nil)`](#defaultdefault_value-on-nil)
38
+ - [`merge_message_keys(*keys)`](#merge_message_keyskeys)
39
+ - [`must_be(klass, error_key = nil)`](#must_beklass-error_key--nil)
40
40
  - [`optional(base)`](#optionalbase)
41
+ - [`pass`](#pass)
42
+ - [`pass_if(base)`](#pass_ifbase)
41
43
  - [`pick(key)`](#pickkey)
42
- - [`merge_message_keys(*keys)`](#merge_message_keyskeys)
44
+ - [`remove`](#remove)
45
+ - [`responds_to(method, error_key = nil)`](#responds_tomethod-error_key--nil)
46
+ - [`transform_to_value(value)`](#transform_to_valuevalue)
43
47
  - ["Web-form" types](#web-form-types)
44
- - [`to_integer`](#to_integer)
45
- - [`to_float`](#to_float)
46
- - [`to_boolean`](#to_boolean)
47
- - [`iso8601`](#iso8601)
48
+ - [`iso8601(error_key = nil)`](#iso8601error_key--nil)
48
49
  - [`optional_param(base)`](#optional_parambase)
50
+ - [`to_boolean(error_key = nil)`](#to_booleanerror_key--nil)
51
+ - [`to_float(error_key = nil)`](#to_floaterror_key--nil)
52
+ - [`to_integer(error_key = nil)`](#to_integererror_key--nil)
49
53
  - [Custom and fundamental types](#custom-and-fundamental-types)
50
- - [`cast(name = 'Anonymous') { |value| ... }`](#castname--anonymous--value--)
51
- - [`check(name = 'Anonymous', error = 'is invalid') { |value| ... }`](#checkname--anonymous-error--is-invalid--value--)
52
- - [`try(name = 'Anonymous', error = 'is invalid', catched_exception:) { |value| ... }`](#tryname--anonymous-error--is-invalid-catched_exception--value--)
54
+ - [`cast { |value| ... }`](#cast--value--)
55
+ - [`check(error_key = nil) { |value| ... }`](#checkerror_key--nil--value--)
56
+ - [`try(error_key = nil, catched_exception:) { |value| ... }`](#tryerror_key--nil-catched_exception--value--)
53
57
  - [`validate(active_model_validations, name = 'Anonymous')`](#validateactive_model_validations-name--anonymous)
54
- - [`compare(reference_value, name = 'Anonymous', error = nil)`](#comparereference_value-name--anonymous-error--nil)
55
- - [`transform(name = 'Anonymous') { |value| ... }`](#transformname--anonymous--value--)
56
- - [`transform_if_present(name = 'Anonymous') { |value| ... }`](#transform_if_presentname--anonymous--value--)
57
- - [Passing additional context to schemas](#passing-additional-context-to-schemas)
58
+ - [`compare(reference_value, error_key = nil)`](#comparereference_value-error_key--nil)
59
+ - [`transform { |value| ... }`](#transform--value--)
60
+ - [`transform_if_present { |value| ... }`](#transform_if_present--value--)
58
61
  - [Array schemas](#array-schemas)
59
62
  - [Hash schemas](#hash-schemas)
60
63
  - [Absent is not nil](#absent-is-not-nil)
61
- - [Schema vs Partial schema](#schema-vs-partial-schema)
64
+ - [Schema vs Partial schema vs Choosy schema](#schema-vs-partial-schema-vs-choosy-schema)
62
65
  - [AND with error aggregation (`*`)](#and-with-error-aggregation-)
63
66
  - [Shortcut nested definitions](#shortcut-nested-definitions)
64
67
  - [Mapping hashes: `transform_to_hash`](#mapping-hashes-transform_to_hash)
65
- - [Error remapping](#error-remapping)
68
+ - [Passing additional context to schemas](#passing-additional-context-to-schemas)
69
+ - [Error remapping: `cast_errors`](#error-remapping-cast_errors)
70
+ - [Internationalization (i18n)](#internationalization-i18n)
71
+ - [Custom absolute keys](#custom-absolute-keys)
72
+ - [Custom relative keys and scopes](#custom-relative-keys-and-scopes)
73
+ - [Providing interpolation variables](#providing-interpolation-variables)
66
74
  - [Registering custom 'predefined' types](#registering-custom-predefined-types)
67
75
  - [Contributing](#contributing)
68
76
  - [Ideas/TODO](#ideastodo)
@@ -108,13 +116,13 @@ validator.("test").valid? # true
108
116
  validator.("test").value # "test"
109
117
  validator.("test").errors # nil
110
118
 
111
- validator.(1) # Datacaster::ErrorResult(["must be string"])
119
+ validator.(1) # Datacaster::ErrorResult(["is not a string"])
112
120
  validator.(1).valid? # false
113
121
  validator.(1).value # nil
114
- validator.(1).errors # ["must be string"]
122
+ validator.(1).errors # ["is not a string"]
115
123
  ```
116
124
 
117
- Datacaster instances are created with a call to `Datacaster.schema { ... }`, `Datacaster.partial_schema { ... }` or `Datacaster.choosy_schema { ... }` (described later in this file).
125
+ Datacaster instances are created with a call to `Datacaster.schema { ... }`, `Datacaster.partial_schema { ... }` or `Datacaster.choosy_schema { ... }`.
118
126
 
119
127
  Datacaster validators' results could be converted to [dry result monad](https://dry-rb.org/gems/dry-monads/1.0/result/):
120
128
 
@@ -124,7 +132,7 @@ require 'datacaster'
124
132
  validator = Datacaster.schema { string }
125
133
 
126
134
  validator.("test").to_dry_result # Success("test")
127
- validator.(1).to_dry_result # Failure(["must be string"])
135
+ validator.(1).to_dry_result # Failure(["is not a string"])
128
136
  ```
129
137
 
130
138
  `string` method call inside of the block in the examples above returns (with the help of some basic meta-programming magic) 'chainable' datacaster instance. To 'chain' datacaster instances 'logical AND' (`&`) operator is used:
@@ -135,7 +143,7 @@ require 'datacaster'
135
143
  validator = Datacaster.schema { string & check { |x| x.length > 5 } }
136
144
 
137
145
  validator.("test1") # Datacaster::ValidResult("test12")
138
- validator.(1) # Datacaster::ErrorResult(["must be string"])
146
+ validator.(1) # Datacaster::ErrorResult(["is not a string"])
139
147
  validator.("test") # Datacaster::ErrorResult(["is invalid"])
140
148
  ```
141
149
 
@@ -158,7 +166,22 @@ You can call `#valid?`, `#value`, `#errors` methods directly, or, if preferred,
158
166
 
159
167
  `#value` and `#errors` would return `#nil` if the result is, correspondingly, `ErrorResult` and `ValidResult`. No methods would raise an error.
160
168
 
161
- Errors are returned as array or hash (or hash of arrays, or array of hashes, etc., for complex data structures). Each element of the returned array shows a separate error (as a string), and each key of the returned hash corresponds to the key of the validated hash. More or less errors are similar to what you expect from `ActiveModel::Errors#to_hash`.
169
+ Errors are returned as array or hash (or hash of arrays, or array of hashes, etc., for complex data structures). Errors support internationalization (i18n) natively. Each element of the returned array shows a separate error as a special i18n value object, and each key of the returned hash corresponds to the key of the validated hash.
170
+
171
+ In this README, instead of i18n values English strings are provided for brevity:
172
+
173
+ ```ruby
174
+ array = Datacaster.schema { array }
175
+ array.(nil)
176
+
177
+ # In this README
178
+ #=> Datacaster::ErrorResult(['should be an array'])
179
+
180
+ # In reality
181
+ #=> <Datacaster::ErrorResult([#<Datacaster::I18nValues::Key(.array, datacaster.errors.array) {:value=>nil}>])>
182
+ ```
183
+
184
+ See [section on i18n](#internationalization-i18n) for details.
162
185
 
163
186
  ### Hash schema
164
187
 
@@ -184,22 +207,22 @@ person_validator.(name: "Jack Simon", salary: 50_000)
184
207
  # => Datacaster::ValidResult({:name=>"Jack Simon", :salary=>50000})
185
208
 
186
209
  person_validator.(name: "Jack Simon")
187
- # => Datacaster::ErrorResult({:salary=>["must be integer"]})
210
+ # => Datacaster::ErrorResult({:salary=>["is not an integer"]})
188
211
 
189
212
  person_validator.("test")
190
- # => Datacaster::ErrorResult(["must be hash"])
213
+ # => Datacaster::ErrorResult(["is not a hash"])
191
214
 
192
215
  person_validator.(name: "John Smith", salary: "1000")
193
- # => Datacaster::ErrorResult({:salary=>["must be integer"]})
216
+ # => Datacaster::ErrorResult({:salary=>["is not an integer"]})
194
217
 
195
218
  person_validator.(name: :john, salary: "1000")
196
- # => Datacaster::ErrorResult({:name=>["must be string"], :salary=>["must be integer"]})
219
+ # => Datacaster::ErrorResult({:name=>["is not a string"], :salary=>["is not an integer"]})
197
220
 
198
221
  person_validator.(name: "John Smith", salary: 100_000, title: "developer")
199
- # => Datacaster::ErrorResult({:title=>["must be absent"]})
222
+ # => Datacaster::ErrorResult({:title=>["should be absent"]})
200
223
  ```
201
224
 
202
- `Datacaster.schema` definitions don't permit, as you likely noticed from the example above, extra fields in the hash. In fact, `Datacaster.schema` automatically adds special built-in validator, called `Datacaster::Terminator::Raising`, at the end of your validation chain, which function is to ensure that all hash keys had been validated.
225
+ `Datacaster.schema` definitions don't permit, as you have likely noticed from the example above, extra fields in the hash.
203
226
 
204
227
  If you want to permit your hashes to contain extra fields, use `Datacaster.partial_schema` (it's the only difference between `.schema` and `.partial_schema`):
205
228
 
@@ -245,7 +268,7 @@ And one special: AND with error aggregation (`*`).
245
268
 
246
269
  The former 3 is described immediately below, and the latter is described in the section on hash schemas further in this file.
247
270
 
248
- #### *AND operator*:
271
+ #### *AND operator*
249
272
 
250
273
  ```ruby
251
274
  even_number = Datacaster.schema { integer & check { |x| x.even? } }
@@ -256,12 +279,12 @@ even_number.(2)
256
279
  even_number.(3)
257
280
  # => Datacaster::ErrorResult(["is invalid"])
258
281
  even_number.("test")
259
- # => Datacaster::ErrorResult(["must be integer"])
282
+ # => Datacaster::ErrorResult(["is not an integer"])
260
283
  ```
261
284
 
262
285
  If left-hand validation of AND operator passes, *its result* (not the original value) is passed to the right-hand validation. See below in this file section on transformations where this might be relevant.
263
286
 
264
- #### *OR operator*:
287
+ #### *OR operator*
265
288
 
266
289
  ```ruby
267
290
  # 'compare' custom type returns ValidResult if and only if validated value == compare's argument
@@ -270,17 +293,62 @@ person_or_entity = Datacaster.schema { compare(:person) | compare(:entity) }
270
293
  person_or_entity.(:person) # => Datacaster::ValidResult(:person)
271
294
  person_or_entity.(:entity) # => Datacaster::ValidResult(:entity)
272
295
 
273
- person_or_entity.(:ngo) # => Datacaster::ErrorResult(["must be equal to :entity"])
296
+ person_or_entity.(:ngo) # => Datacaster::ErrorResult(["does not equal :entity"])
274
297
  ```
275
298
 
276
299
  Notice that OR operator, if left-hand validation fails, passes the original value to the right-hand validation. As you see in the example above resultant error messages are not always convenient (i.e. to show something like "value must be :person or :entity" is preferable to showing somewhat misleading "must be equal to :entity"). See the next section on "IF... THEN... ELSE" for closer to the real world example.
277
300
 
278
- #### *IF... THEN... ELSE operator*:
301
+ #### *IF... THEN... ELSE operator*
279
302
 
280
- Let's suppose we want to validate that incoming hash is either 'person' or 'entity', where
303
+ Let's support we want to run different validations depending on some value, e.g.:
281
304
 
282
- - 'person' is a hash with 3 keys (kind: `:person`, name: string, salary: integer),
283
- - 'entity' is a hash with 4 keys (kind: `:entity`, title: string, form: string, revenue: integer).
305
+ * if 'salary' is more than 100_000, check for the additional key, 'passport'
306
+ * otherwise, ensure 'passport' key is absent
307
+ * in any case, check that 'name' key is present and is a string
308
+
309
+ ```ruby
310
+ applicant =
311
+ Datacaster.schema do
312
+ base = hash_schema(
313
+ name: string,
314
+ salary: integer
315
+ )
316
+
317
+ large_salary = check { |x| x[:salary] > 100_000 }
318
+
319
+ base &
320
+ large_salary.
321
+ then(passport: string).
322
+ else(passport: absent)
323
+ end
324
+
325
+ applicant.(name: 'John', salary: 50_000)
326
+ # => Datacaster::ValidResult({:name=>"John", :salary=>50000})
327
+
328
+ applicant.(name: 'Jane', salary: 101_000, passport: 'AB123CD')
329
+ # => Datacaster::ValidResult({:name=>"Jane", :salary=>101000, :passport=>"AB123CD"})
330
+
331
+ applicant.(name: 'George', salary: 101_000)
332
+ # => Datacaster::ErrorResult({:passport=>["is not a string"])
333
+ ```
334
+
335
+ Formally, with `a.then(b).else(c)`:
336
+
337
+ * if `a` returns `ValidResult`, then `b` is called *with the result of `a`* (not the original value) and whatever `b` returns is returned;
338
+ * otherwise, `c` is called with the original value, and whatever `c` returns is returned.
339
+
340
+ `else`-part is required and could not be omitted.
341
+
342
+ Note: this construct is *not* an equivalent of `a & b | c`.
343
+
344
+ With `a.then(b).else(c)` if `a` and `b` fails, then `b`'s error is returned. With `a & b | c`, instead, `c`'s result would be returned.
345
+
346
+ #### *SWITCH... ON... ELSE operator*
347
+
348
+ Let's suppose we want to validate that incoming hash is either 'person' or 'entity', where:
349
+
350
+ * 'person' is a hash with 3 keys (kind: `:person`, name: string, salary: integer),
351
+ * 'entity' is a hash with 4 keys (kind: `:entity`, title: string, form: string, revenue: integer).
284
352
 
285
353
  ```ruby
286
354
  person_or_entity =
@@ -296,7 +364,25 @@ person_or_entity =
296
364
  # separate entity validator (excluding validation of 'kind' field)
297
365
  entity = hash_schema(title: string, form: string, revenue: integer)
298
366
 
299
- kind_is_valid & hash_schema(kind: compare(:person)).then(person).else(entity)
367
+
368
+ # 1. First option, explicit definition
369
+
370
+ kind_is_valid &
371
+ switch(pick(:kind)).
372
+ on(compare(:person), person).
373
+ on(compare(:entity), entity)
374
+
375
+ # 2. Second option, shortcut definiton
376
+
377
+ kind_is_valid &
378
+ switch(:kind).
379
+ on(:person, person).
380
+ on(:entity, entity)
381
+
382
+ # 3. Third option, using keywords args and Ruby 3.1 value omission in hash literals
383
+
384
+ kind_is_valid &
385
+ switch(:kind, person:, entity:)
300
386
  end
301
387
 
302
388
  person_or_entity.(
@@ -320,168 +406,132 @@ person_or_entity.(
320
406
  # => Datacaster::ErrorResult({:kind=>["is invalid"]})
321
407
  ```
322
408
 
323
- See below documentation on 'check' custom type to know how to provide custom error message instead of 'is invalid'.
409
+ In our opinion the above example shows most laconic way to express underlying 'business-logic' (including elaborate error reporting on all kinds of failures) among all available competitor approaches/gems.
324
410
 
325
- Schema, defined above, behaves in all aspects (shown in the example and in other practical applications which might come to your mind) just as you might expect it to, after reading previous examples and the code above.
411
+ Notice that shortcut definitions are available (illustrated in the example above) for the switch caster:
326
412
 
327
- In our opinion the above example shows most laconic way to express underlying 'business-logic' (including elaborate error reporting on all kinds of failures) among all available competitor approaches/gems.
413
+ * `switch(:key)` is exactly the same as `switch(pick(:key))` (works for a string, a symbol, or an array thereof)
414
+ * `on(:key, ...)` is exactly the same as `on(compare(:key), ...)` (works for a string or a symbol)
415
+ * `switch([caster], on_check => on_caster, ...)` is exactly the same as `switch([caster]).on(on_check, on_caster).on(...)`
328
416
 
329
- Formally, with `a.then(b).else(c)`:
417
+ `switch()` without a `base` argument will pass the incoming value to the `.on(...)` casters.
330
418
 
331
- * if `a` returns `ValidResult`, then `b` is called *with the result of `a`* (not the original value) and whatever `b` returns is returned;
332
- * otherwise, `c` is called with the original value, and whatever `c` returns is returned.
419
+ Formally, with `switch(a).on(on_check, on_caster).else(c)`:
333
420
 
334
- `else`-part is required and could not be omitted.
421
+ * if `a` returns ErrorResult, it is the result of the switch
422
+ * otherwise, all `on_check` casters from the `.on` blocks are called with the result of `a`, until the first one which returns ValidResult is found – corresponding `on_caster` is called with the original value and its result is the result of the switch
423
+ * if all `on_check`-s returned ErrorResult
424
+ * and there is an `.else` block, `c` is called with the original value and its result is the result of the switch
425
+ * if there is no `.else` block, `ErrorResult(['is invalid'])` is returned from the switch
335
426
 
336
- Note: this construct is *not* an equivalent of `a & b | c`.
427
+ I18n keys:
337
428
 
338
- With `a.then(b).else(c)` if `a` and `b` fails, then `b`'s error is returned. With `a & b | c`, instead, `c`'s result would be returned.
429
+ * all `.on` checks resulted in an error and there is no `.else`: `'.switch'`, `'datacaster.errors.switch'`
339
430
 
340
431
  ## Built-in types
341
432
 
342
433
  Full description of all built-in types follows.
343
434
 
344
- ### Basic types
345
-
346
- #### `string`
435
+ Under "I18n keys" error keys (in the order of priority) which caster will use for translation of error messages are provided. Each caster provides `value` variable for i18n interpolation, setting it to `#to_s` of incoming value. Some casters provide additional variables, which is mentioned in the same section. See [Internationalization (i18n)](#internationalization-i18n) for the details.
347
436
 
348
- Returns ValidResult if and only if provided value is a string. Doesn't transform the value.
349
-
350
- #### `integer`
351
-
352
- Returns ValidResult if and only if provided value is an integer. Doesn't transform the value.
353
-
354
- #### `float`
355
-
356
- Returns ValidResult if and only if provided value is a float (checked with Ruby's `#is_a?(Float)`, i.e. integers are not considered valid floats). Doesn't transform the value.
357
-
358
- #### `decimal([digits = 8])`
359
-
360
- Returns ValidResult if and only if provided value is either a float, integer or string representing float/integer.
361
-
362
- Transforms the value to `BigDecimal` instance.
437
+ ### Basic types
363
438
 
364
- #### `array`
439
+ #### `array(error_key = nil)`
365
440
 
366
441
  Returns ValidResult if and only if provided value is an `Array`. Doesn't transform the value.
367
442
 
368
- #### `hash_value`
443
+ I18n keys: `error_key`, `'.array'`, `'datacaster.errors.array'`.
369
444
 
370
- Returns ValidResult if and only if provided value is a `Hash`. Doesn't transform the value.
371
-
372
- Note: this type is called `hash_value` instead of `hash`, because `hash` is reserved method name in Ruby.
445
+ #### `decimal(digits = 8, error_key = nil)`
373
446
 
374
- ### Convenience types
447
+ Returns ValidResult if and only if provided value is either a float, integer or string representing float/integer.
375
448
 
376
- #### `non_empty_string`
449
+ Transforms the value to the `BigDecimal` instance.
377
450
 
378
- Returns ValidResult if and only if provided value is a string and is not empty. Doesn't transform the value.
451
+ I18n keys: `error_key`, `'.decimal'`, `'datacaster.errors.decimal'`.
379
452
 
380
- #### `hash_with_symbolized_keys`
453
+ #### `float(error_key = nil)`
381
454
 
382
- Returns ValidResult if and only if provided value is an instance of `Hash`. Transforms the value to `#hash_with_symbolized_keys` (requires `ActiveSupport`).
455
+ Returns ValidResult if and only if provided value is a float (checked with Ruby's `#is_a?(Float)`, i.e. integers are not considered valid floats). Doesn't transform the value.
383
456
 
384
- #### `integer32`
457
+ I18n keys: `error_key`, `'.float'`, `'datacaster.errors.float'`.
385
458
 
386
- Returns ValidResult if and only if provided value is an integer and it's absolute value is <= 2_147_483_647. Doesn't transform the value.
459
+ #### `hash_value(error_key = nil)`
387
460
 
388
- ### Special types
461
+ Returns ValidResult if and only if provided value is a `Hash`. Doesn't transform the value.
389
462
 
390
- #### `absent`
463
+ Note: this type is called `hash_value` instead of `hash`, because `hash` is a reserved method name in Ruby.
391
464
 
392
- Returns ValidResult if and only if provided value is `Datacaster.absent` (this is singleton instance). Relevant only for hash schemas (see below). Doesn't transform the value.
465
+ I18n keys: `error_key`, `'.hash_value'`, `'datacaster.errors.hash_value'`.
393
466
 
394
- #### `any`
467
+ #### `integer(error_key = nil)`
395
468
 
396
- Returns ValidResult if and only if provided value is not `Datacaster.absent` (this is singleton instance). Relevant only for hash schemas (see below). Doesn't transform the value.
469
+ Returns ValidResult if and only if provided value is an integer. Doesn't transform the value.
397
470
 
398
- #### `transform_to_value(value)`
471
+ I18n keys: `error_key`, `'.integer'`, `'datacaster.errors.integer'`.
399
472
 
400
- Always returns ValidResult. The value is transformed to provided argument. Is used to provide default values, e.g.:
473
+ #### `string(error_key = nil)`
401
474
 
402
- ```ruby
403
- max_concurrent_connections = Datacaster.schema { compare(nil).then(transform_to_value(5)).else(integer) }
475
+ Returns ValidResult if and only if provided value is a string. Doesn't transform the value.
404
476
 
405
- max_concurrent_connections.(9) # => Datacaster::ValidResult(9)
406
- max_concurrent_connections.("9") # => Datacaster::ErrorResult(["must be integer"])
407
- max_concurrent_connections.(nil) # => Datacaster::ValidResult(5)
408
- ```
477
+ I18n keys: `error_key`, `'.string'`, `'datacaster.errors.string'`.
409
478
 
410
- #### `remove`
411
-
412
- Equivalent to `transform_to_value(Datacaster.absent)`. Always returns ValidResult. The value is transformed to `Datacaster.absent` (see section below on hash schemas, where this is useful).
479
+ ### Convenience types
413
480
 
414
- #### `pass`
481
+ #### `hash_with_symbolized_keys(error_key = nil)`
415
482
 
416
- Equivalent to `transform_to_value { |x| x }`. Always returns ValidResult. Doesn't transform the value. Useful to "mark" the value as validated (see section below on hash schemas, where this could be applied).
483
+ Returns ValidResult if and only if provided value is an instance of `Hash`. Transforms the value to `#hash_with_symbolized_keys` (requires `ActiveSupport`).
417
484
 
418
- #### `responds_to(method)`
485
+ I18n keys: `error_key`, `'.hash_value'`, `'datacaster.errors.hash_value'`.
419
486
 
420
- Returns ValidResult if and only if value `#responds_to?(method)`. Doesn't transform the value.
487
+ #### `integer32(error_key = nil)`
421
488
 
422
- #### `must_be(klass)`
489
+ Returns ValidResult if and only if provided value is an integer and it's absolute value is <= 2_147_483_647. Doesn't transform the value.
423
490
 
424
- Returns ValidResult if and only if value `#is_a?(klass)`. Doesn't transform the value.
491
+ I18n keys:
425
492
 
426
- #### `optional(base)`
493
+ * not an integer – `error_key`, `'.integer'`, `'datacaster.errors.integer'`
494
+ * too big – `error_key`, `'.integer32'`, `'datacaster.errors.integer32'`
427
495
 
428
- Returns ValidResult if and only if value is either `Datacaster.absent` (singleton instance) or passes `base` validation. See below documentation on hash schemas for details on `Datacaster.absent`.
496
+ #### `non_empty_string(error_key = nil)`
429
497
 
430
- ```ruby
431
- item_with_optional_price =
432
- Datacaster.schema do
433
- hash_schema(
434
- name: string,
435
- price: optional(float)
436
- )
437
- end
498
+ Returns ValidResult if and only if provided value is a string and is not empty. Doesn't transform the value.
438
499
 
439
- item_with_optional_price.(name: "Book", price: 1.23)
440
- # => Datacaster::ValidResult({:name=>"Book", :price=>1.23})
441
- item_with_optional_price.(name: "Book")
442
- # => Datacaster::ValidResult({:name=>"Book"})
500
+ * not a string – `error_key`, `'.string'`, `'datacaster.errors.string'`
501
+ * is empty – `error_key`, `'.non_empty_string'`, `'datacaster.errors.non_empty_string'`
443
502
 
444
- item_with_optional_price.(name: "Book", price: "wrong")
445
- # => Datacaster::ErrorResult({:price=>["must be float"]})
446
- ```
503
+ ### Special types
447
504
 
448
- #### `pick(key)`
505
+ #### `absent(error_key = nil)`
449
506
 
450
- Returns ValidResult if and only if value `#is_a?(Enumerable)`.
507
+ Returns ValidResult if and only if provided value is `Datacaster.absent` (this is singleton instance). Relevant only for hash schemas (see below). Doesn't transform the value.
451
508
 
452
- Transforms the value to/returns:
509
+ I18n keys: `error_key`, `'.absent'`, `'datacaster.errors.absent'`.
453
510
 
454
- * `value[key]` if key is set in the value
455
- * `nil` if `value[key]` is set and is nil
456
- * `Datacaster.absent` if key is not set
511
+ #### `any(error_key = nil)`
457
512
 
458
- ```ruby
459
- pick_name = Datacaster.schema { pick(:name) }
513
+ Returns ValidResult if and only if provided value is not `Datacaster.absent` (this is singleton instance). Relevant only for hash schemas (see below). Doesn't transform the value.
460
514
 
461
- pick_name.(name: "George") # => Datacaster::ValidResult("George")
462
- pick_name.(last_name: "Johnson") # => Datacaster::ValidResult(#<Datacaster.absent>)
515
+ I18n keys: `error_key`, `'.any'`, `'datacaster.errors.any'`
463
516
 
464
- pick_name.("test") # => Datacaster::ErrorResult(["must be Enumerable"])
465
- ```
517
+ #### `default(default_value, on: nil)`
466
518
 
467
- Alternative form could be used: `pick(*keys)`.
519
+ Always returns ValidResult.
468
520
 
469
- In this case, an array of results is returned, each element in which corresponds to the element in `keys` array (i.e. is an argument of the `pick`) and evaluated in accordance with the above rules.
521
+ Returns `default_value` in the following cases:
470
522
 
471
- ```ruby
472
- pick_name_and_age = Datacaster.schema { pick(:name, :age) }
523
+ * if the value is `Datacaster.absent` (`on` is disregarded in such case)
524
+ * if `on` is set to a method name to which the value responds and yields truthy
473
525
 
474
- pick_name_and_age.(name: "George", age: 20) # => Datacaster::ValidResult(["George", 20])
475
- pick_name_and_age.(last_name: "Johnson", age: 20) # => Datacaster::ValidResult([#<Datacaster.absent>, 20])
526
+ Returns the initial value otherwise.
476
527
 
477
- pick_name_and_age.("test") # => Datacaster::ErrorResult(["must be Enumerable"])
478
- ```
528
+ Set `on` to `:nil?`, `:empty?` or similar method names.
479
529
 
480
530
  #### `merge_message_keys(*keys)`
481
531
 
482
- Returns ValidResult only if value `#is_a?(Hash)`.
532
+ Returns ValidResult only if the value `#is_a?(Hash)`.
483
533
 
484
- Maps incoming hash to Datacaster styled messages.
534
+ Picks given keys of incoming hash and merges their values recursively.
485
535
 
486
536
  ```ruby
487
537
  mapper =
@@ -551,7 +601,7 @@ mapping.(
551
601
  # })
552
602
  ```
553
603
 
554
- Hash keys with `nil` and `[]` values are deeply ignored:
604
+ Hash keys with `nil` and `[]` values are removed recursively:
555
605
 
556
606
  ```ruby
557
607
  mapping = Datacaster.schema do
@@ -572,25 +622,111 @@ mapping.(
572
622
  # })
573
623
  ```
574
624
 
575
- ### "Web-form" types
625
+ See also `#cast_errors` for [error remapping](#error-remapping-cast_errors).
576
626
 
577
- These types are convenient to parse and validate POST forms and decode JSON requests.
627
+ See also `#pick` for [simpler picking of hash values](#pickkey).
578
628
 
579
- #### `to_integer`
629
+ I18n keys:
580
630
 
581
- Returns ValidResult if and only if value is an integer, float or string representing integer/float. Transforms value to integer.
631
+ * not a hash `'.hash_value'`, `'datacaster.errors.hash_value'`
582
632
 
583
- #### `to_float`
633
+ #### `must_be(klass, error_key = nil)`
584
634
 
585
- Returns ValidResult if and only if value is an integer, float or string representing integer/float. Transforms value to float.
635
+ Returns ValidResult if and only if the value `#is_a?(klass)`. Doesn't transform the value.
586
636
 
587
- #### `to_boolean`
637
+ I18n keys: `error_key`, `'.must_be'`, `'datacaster.errors.must_be'`. Adds `reference` i18n variable, setting it to `klass.name`.
588
638
 
589
- Returns ValidResult if and only if value is `true`, `1`, `'true'` or `false`, `0`, `'false'`. Transforms value to `true` or `false` (using apparent convention).
639
+ #### `optional(base)`
590
640
 
591
- #### `iso8601`
641
+ Returns ValidResult if and only if the value is either `Datacaster.absent` or passes `base` validation. See below documentation on hash schemas for details on `Datacaster.absent`.
592
642
 
593
- Returns ValidResult if and only if value is a string in [ISO8601](https://en.wikipedia.org/wiki/ISO_8601) date-time format.
643
+ ```ruby
644
+ item_with_optional_price =
645
+ Datacaster.schema do
646
+ hash_schema(
647
+ name: string,
648
+ price: optional(float)
649
+ )
650
+ end
651
+
652
+ item_with_optional_price.(name: "Book", price: 1.23)
653
+ # => Datacaster::ValidResult({:name=>"Book", :price=>1.23})
654
+ item_with_optional_price.(name: "Book")
655
+ # => Datacaster::ValidResult({:name=>"Book"})
656
+
657
+ item_with_optional_price.(name: "Book", price: "wrong")
658
+ # => Datacaster::ErrorResult({:price=>["is not a float"]})
659
+ ```
660
+
661
+ #### `pass`
662
+
663
+ Always returns ValidResult. Doesn't transform the value.
664
+
665
+ Useful to "mark" the value as validated (see section below on hash schemas, where this could be applied).
666
+
667
+ #### `pass_if(base)`
668
+
669
+ Returns ValidResult if and only if base returns ValidResult. Returns base's error result otherwise.
670
+
671
+ Doesn't transform the value: if base succeeds returns the original value (not the one that base returned).
672
+
673
+ #### `pick(key)`
674
+
675
+ Returns ValidResult if and only if the value `#is_a?(Enumerable)`.
676
+
677
+ Transforms the value to/returns:
678
+
679
+ * `value[key]` if key is set in the value
680
+ * `nil` if `value[key]` is set and is nil
681
+ * `Datacaster.absent` if key is not set
682
+
683
+ ```ruby
684
+ pick_name = Datacaster.schema { pick(:name) }
685
+
686
+ pick_name.(name: "George") # => Datacaster::ValidResult("George")
687
+ pick_name.(last_name: "Johnson") # => Datacaster::ValidResult(#<Datacaster.absent>)
688
+
689
+ pick_name.("test") # => Datacaster::ErrorResult(["is not Enumerable"])
690
+ ```
691
+
692
+ Alternative form could be used: `pick(*keys)`.
693
+
694
+ In this case, an array of results is returned, each element in which corresponds to the element in `keys` array (i.e. is an argument of the `pick`) and evaluated in accordance with the above rules.
695
+
696
+ ```ruby
697
+ pick_name_and_age = Datacaster.schema { pick(:name, :age) }
698
+
699
+ pick_name_and_age.(name: "George", age: 20) # => Datacaster::ValidResult(["George", 20])
700
+ pick_name_and_age.(last_name: "Johnson", age: 20) # => Datacaster::ValidResult([#<Datacaster.absent>, 20])
701
+
702
+ pick_name_and_age.("test") # => Datacaster::ErrorResult(["is not Enumerable"])
703
+ ```
704
+
705
+ I18n keys:
706
+
707
+ * not a Enumerable – `'.must_be'`, `'datacaster.errors.must_be'`.
708
+
709
+ #### `remove`
710
+
711
+ Always returns ValidResult. Always returns `Datacaster.absent`.
712
+
713
+ #### `responds_to(method, error_key = nil)`
714
+
715
+ Returns ValidResult if and only if the value `#responds_to?(method)`. Doesn't transform the value.
716
+
717
+ I18n keys: `error_key`, `'.responds_to'`, `'datacaster.errors.responds_to'`. Adds `reference` i18n variable, setting it to `method.to_s`.
718
+
719
+ #### `transform_to_value(value)`
720
+
721
+ Always returns ValidResult. The value is transformed to provided argument (disregarding the original value). See also [`default`](#defaultdefault_value-on-nil).
722
+
723
+ ### "Web-form" types
724
+
725
+ These types are convenient to parse and validate POST forms and decode JSON requests.
726
+
727
+ #### `iso8601(error_key = nil)`
728
+
729
+ Returns ValidResult if and only if the value is a string in [ISO8601](https://en.wikipedia.org/wiki/ISO_8601) date-time format.
594
730
 
595
731
  ```ruby
596
732
  dob = Datacaster.schema { iso8601 }
@@ -599,37 +735,53 @@ dob.("2011-02-03")
599
735
  # => Datacaster::ValidResult(#<DateTime: 2011-02-03T00:00:00+00:00 ...>)
600
736
  ```
601
737
 
602
- Transforms value to `DateTime` instance.
738
+ Transforms the value to the `DateTime` instance.
739
+
740
+ I18n keys: `error_key`, `'.iso8601'`, `'datacaster.errors.iso8601'`.
603
741
 
604
742
  #### `optional_param(base)`
605
743
 
606
- Returns ValidResult if and only if value is absent, empty string or passes `base` validation.
744
+ Returns ValidResult if and only if the value is absent, empty string or passes `base` validation.
607
745
 
608
- If the value is empty string (`""`), transforms it to `Datacaster.absent` instance. It makes sense to use this type only in conjunction with hash schema validations (see below), where `Datacaster.absent` keys are removed from the resultant hash.
746
+ If the value is empty string (`""`), transforms it to `Datacaster.absent` instance. It makes sense to use this type in conjunction with hash schema validations (see below), where `Datacaster.absent` keys are removed from the resultant hash.
609
747
 
610
748
  Otherwise, doesn't transform the value.
611
749
 
612
- ### Custom and fundamental types
750
+ #### `to_boolean(error_key = nil)`
751
+
752
+ Returns ValidResult if and only if the value is `true`, `1`, `'true'` or `false`, `0`, `'false'`. Transforms the value to `true` or `false` (using apparent convention).
613
753
 
614
- These custom types (or 'meta' types) are used to create 'hand-crafted' validators.
754
+ I18n keys: `error_key`, `'.to_boolean'`, `'datacaster.errors.to_boolean'`
615
755
 
616
- When `name` argument is available, that argument determines what would display with `#inspect` of that validator (and nothing else).
756
+ #### `to_float(error_key = nil)`
617
757
 
618
- When `error` argument is available, that argument determines what error text (should be string, but actual error will automatically be displayed as array of strings, see examples in the previous sections of this file) will be used if validation fails.
758
+ Returns ValidResult if and only if the value is an integer, float or string representing integer/float. Transforms value to float.
759
+
760
+ I18n keys: `error_key`, `'.to_float'`, `'datacaster.errors.to_float'`
761
+
762
+ #### `to_integer(error_key = nil)`
763
+
764
+ Returns ValidResult if and only if the value is an integer, float or string representing integer/float. Transforms the value to the integer.
765
+
766
+ I18n keys: `error_key`, `'.to_integer'`, `'datacaster.errors.to_integer'`.
767
+
768
+ ### Custom and fundamental types
619
769
 
620
- #### `cast(name = 'Anonymous') { |value| ... }`
770
+ These types are used to create 'hand-crafted' validators.
771
+
772
+ #### `cast { |value| ... }`
621
773
 
622
774
  The most basic &mdash; "fully manual" &mdash; validator.
623
775
 
624
- Calls block with the value. Returns whatever block returns.
776
+ Calls the block with the value. Returns whatever the block returns.
625
777
 
626
- Provided block must return either `Datacaster::Result` or `Dry::Result::Monad` (the latter will automatically be converted to the former), otherwise `cast` will raise runtime `TypeError`.
778
+ Provided block must return either a `Datacaster::Result` or a `Dry::Result::Monad` (the latter will automatically be converted to the former), otherwise `cast` will raise a runtime error.
627
779
 
628
780
  ```ruby
629
- # Actually, better use 'check' here instead (see below)
781
+ # Actually, it's better to use 'check' here instead
630
782
  user_id_exists =
631
783
  Datacaster.schema do
632
- cast('UserIdExists') do |user_id|
784
+ cast do |user_id|
633
785
  if User.exists?(user_id)
634
786
  Success(user_id) # or Datacaster::ValidResult(user_id)
635
787
  else
@@ -642,18 +794,18 @@ user_id_exists =
642
794
  end
643
795
  ```
644
796
 
645
- Notice, that for this example (as is written in the comment) `check` type is better option (see below). It's actually so hard to come up with an example where explicit `cast` is the best option that we didn't manage to do that. Refrain from using `cast` unless absolutely no other type could be used.
797
+ Notice, that for this example (as is written in the comment) `check` type is a better option (see below).
646
798
 
647
- `cast` will transform value, if such is the logic of provided block.
799
+ `cast` will transform the value, if such is the logic of the provided block.
648
800
 
649
- #### `check(name = 'Anonymous', error = 'is invalid') { |value| ... }`
801
+ #### `check(error_key = nil) { |value| ... }`
650
802
 
651
- Returns ValidResult if and only if provided block returns truthy value (i.e. anything except `false` and `nil`).
803
+ Returns ValidResult if and only if the provided block returns truthy value.
652
804
 
653
805
  ```ruby
654
806
  user_id_exists =
655
807
  Datacaster.schema do
656
- check('UserIdExists', 'user is not found') do |user_id|
808
+ check do |user_id|
657
809
  User.exists?(user_id)
658
810
  end
659
811
  end
@@ -661,9 +813,11 @@ user_id_exists =
661
813
 
662
814
  Doesn't transform the value.
663
815
 
664
- #### `try(name = 'Anonymous', error = 'is invalid', catched_exception:) { |value| ... }`
816
+ I18n keys: `error_key`, `'.check'`, `'datacaster.errors.check'`.
817
+
818
+ #### `try(error_key = nil, catched_exception:) { |value| ... }`
665
819
 
666
- Returns ValidResult if and only if block finishes without exceptions. If block raises an exception:
820
+ Returns ValidResult if and only if the block finishes without exceptions. If the block raises an exception:
667
821
 
668
822
  * if exception class equals to `catched_exception`, then ErrorResult is returned;
669
823
  * otherwise, exception is re-raised.
@@ -681,10 +835,10 @@ dangerous_validator =
681
835
  end
682
836
  ```
683
837
 
684
- As you see from the example, that's another 'meta type', which direct use is hard to justify. Consider using `check` instead (returning boolean value from the block instead of raising error).
685
-
686
838
  Doesn't transform the value.
687
839
 
840
+ I18n keys: `error_key`, `'.try'`, `'datacaster.errors.try'`
841
+
688
842
  #### `validate(active_model_validations, name = 'Anonymous')`
689
843
 
690
844
  Requires ActiveModel.
@@ -710,7 +864,9 @@ nickname.("user32") # Datacaster::ErrorResult(["only allows letters"])
710
864
 
711
865
  Doesn't transform the value.
712
866
 
713
- #### `compare(reference_value, name = 'Anonymous', error = nil)`
867
+ I18n is performed by ActiveModel gem.
868
+
869
+ #### `compare(reference_value, error_key = nil)`
714
870
 
715
871
  This type is the way to ensure some value in your schema is some predefined "constant".
716
872
 
@@ -725,9 +881,11 @@ agreed_with_tos =
725
881
  end
726
882
  ```
727
883
 
728
- #### `transform(name = 'Anonymous') { |value| ... }`
884
+ I18n keys: `error_key`, `'.compare'`, `'datacaster.errors.compare'`. Adds `reference` i18n variable, setting it to `reference_value.to_s`.
885
+
886
+ #### `transform { |value| ... }`
729
887
 
730
- Always returns ValidResult. Transforms the value: returns whatever block returned, automatically wrapping it into `ValidResult`.
888
+ Always returns ValidResult. Transforms the value: returns whatever the block has returned.
731
889
 
732
890
  ```ruby
733
891
  city =
@@ -742,81 +900,34 @@ city =
742
900
  city.(name: "Denver", distance: "2.5") # => Datacaster::ValidResult({:name=>"Denver", :distance=>4.02335})
743
901
  ```
744
902
 
745
- #### `transform_if_present(name = 'Anonymous') { |value| ... }`
903
+ #### `transform_if_present { |value| ... }`
746
904
 
747
- Always returns ValidResult. If the value is `Datacaster.absent` (singleton instance, see below section on hash schemas), then `Datacaster.absent` is returned (block isn't called). Otherwise, works like `transform`.
748
-
749
- ### Passing additional context to schemas
750
-
751
- You can pass `context` to schema using `.with_context` method
752
-
753
- ```ruby
754
- # class User < ApplicationRecord
755
- # ...
756
- # end
757
- #
758
- # class Post < ApplicationRecord
759
- # belongs_to :user
760
- # ...
761
- # end
762
-
763
- schema =
764
- Datacaster.schema do
765
- hash_schema(
766
- post_id: to_integer & check { |id| Post.where(id: id, user_id: context.current_user).exists? }
767
- )
768
- end
769
-
770
- current_user = ...
771
-
772
- schema.with_context(current_user: current_user).(post_id: 15)
773
- ```
774
-
775
- `context` is an [OpenStruct](https://ruby-doc.org/stdlib-3.1.0/libdoc/ostruct/rdoc/OpenStruct.html) instance which is initialized in `.with_context`
776
-
777
- **Note**
778
-
779
- `context` can be accesed only in types' blocks:
780
- ```ruby
781
- mail_transformer = Datacaster.schema { transform { |v| "#{v}#{context.postfix}" } }
782
-
783
- mail_transformer.with_context(postfix: "@domen.com").("admin")
784
- # => #<Datacaster::ValidResult("admin@domen.com")>
785
- ```
786
- It can't be used in schema definition block itself:
787
- ```ruby
788
- Datacaster.schema { context.error }
789
- # leads to `NoMethodError`
790
- ```
905
+ Always returns ValidResult. If the value is `Datacaster.absent`, then `Datacaster.absent` is returned (the block isn't called). Otherwise, works like [`transform`](#transform--value).
791
906
 
792
907
  ### Array schemas
793
908
 
794
- To define compound data type, array of 'something', use `array_schema(something)` (or, synonymically, `array_of(something)`). There is no way to define array wherein each element is of different type.
909
+ To define compound data type, array of 'something', use `array_schema(something)` (or the alias `array_of(something)`). There is no built-in way to define an array wherein each element is of a different type.
795
910
 
796
911
  ```ruby
797
912
  salaries = Datacaster.schema { array_of(integer) }
798
913
 
799
914
  salaries.([1000, 2000, 3000]) # Datacaster::ValidResult([1000, 2000, 3000])
800
915
 
801
- salaries.(["one thousand"]) # Datacaster::ErrorResult({0=>["must be integer"]})
802
- salaries.(:not_an_array) # Datacaster::ErrorResult(["must be array"])
803
- salaries.([]) # Datacaster::ErrorResult(["must not be empty"])
916
+ salaries.(["one thousand"]) # Datacaster::ErrorResult({0=>["is not an integer"]})
917
+ salaries.(:not_an_array) # Datacaster::ErrorResult(["should be an array"])
918
+ salaries.([]) # Datacaster::ErrorResult(["should not be empty"])
804
919
  ```
805
920
 
806
921
  To allow empty array use the following construct: `compare([]) | array_of(...)`.
807
922
 
808
- If you want to define array of hashes, shortcut definition could be used: instead of `array_of(hash_schema({...}))` use `array_of({...})`:
923
+ If you want to define an array of hashes, [shortcut definition](#shortcut-nested-definitions) could be used: instead of `array_of(hash_schema({...}))` use `array_of({...})`:
809
924
 
810
925
  ```ruby
811
926
  people =
812
927
  Datacaster.schema do
813
928
  array_of(
814
- # hash_schema(
815
- {
816
- name: string,
817
- salary: float
818
- }
819
- # )
929
+ name: string,
930
+ salary: float
820
931
  )
821
932
  end
822
933
 
@@ -826,25 +937,30 @@ people.([person1, person2]) # => Datacaster::ValidResult([{...}, {...}])
826
937
 
827
938
  people.([{salary: 250_000.0}, {salary: "50000"}])
828
939
  # => Datacaster::ErrorResult({
829
- # 0 => {:name => ["must be string"]},
830
- # 1 => {:name => ["must be string"], :salary => ["must be float"]}
940
+ # 0 => {:name => ["is not a string"]},
941
+ # 1 => {:name => ["is not a string"], :salary => ["is not a float"]}
831
942
  # })
832
943
  ```
833
944
 
834
- Notice, that extra keys of inner hashes could be validated only if each element is otherwise valid. In other words, if some of the elements have other validation errors, then "extra key must be absent" validation error won't appear on any element.
945
+ Notice that extra keys of inner hashes could be validated only if each element is otherwise valid. In other words, if some of the elements have other validation errors, then "extra key must be absent" validation error won't appear on any element. This could be avoided by using nested `Datacaster.schema` call to define element schema instead of shortcut definition or `hash_schema` call.
835
946
 
836
- Formally, `array_of(x)` will return ValidResult if and only if:
947
+ Formally, `array_of(x, error_keys = {})` will return ValidResult if and only if:
837
948
 
838
949
  a) provided value implements basic array methods (`#map`, `#zip`),
839
950
  b) provided value is not `#empty?`,
840
951
  c) each element of the provided value passes validation of `x`.
841
952
 
842
- If a) fails, `ErrorResult(["must be array"])` is returned.
843
- If b) fails, `ErrorResult(["must not be empty"])` is returned.
953
+ If a) fails, `ErrorResult(["should be an array"]) is returned.
954
+ If b) fails, `ErrorResult(["should not be empty"])` is returned.
844
955
  If c) fails, `ErrorResult({0 => ..., 1 => ...})` is returned. Wrapped hash contains keys which correspond to initial array's indices, and values correspond to failure returned from `x` validator, called for the corresponding element.
845
956
 
846
957
  Array schema transforms array if inner type (`x`) transforms element (in this case `array_schema` works more or less like `map` function). Otherwise, it doesn't transform.
847
958
 
959
+ I18n keys:
960
+
961
+ * not an array – `error_keys[:array]`, `'.array'`, `'datacaster.errors.array'`
962
+ * empty array – `error_keys[:empty]`, `'.empty'`, `'datacaster.errors.empty'`
963
+
848
964
  ### Hash schemas
849
965
 
850
966
  Hash schemas are "bread and butter" of Datacaster.
@@ -864,7 +980,7 @@ person.(name: "John Smith", salary: 100_000)
864
980
  # => Datacaster::ValidResult({:name=>"John Smith", :salary=>100000})
865
981
 
866
982
  person.(name: "John Smith", salary: "100_000")
867
- # => Datacaster::ErrorResult({:salary=>["must be integer"]})
983
+ # => Datacaster::ErrorResult({:salary=>["is not an integer"]})
868
984
  ```
869
985
 
870
986
  Formally, hash schema returns ValidResult if and only if:
@@ -873,17 +989,19 @@ a) provided value `is_a?(Hash)`,
873
989
  b) all values, fetched by keys mentioned in `hash_schema(...)` definition, pass corresponding validations,
874
990
  c) after all checks (including logical operators), there are no unchecked keys in the hash.
875
991
 
876
- If a) fails, `ErrorResult(["must be hash"])` is returned.
992
+ If a) fails, `ErrorResult(["is not a hash"])` is returned.
877
993
  if b) fails, `ErrorResult(key1 => [errors...], key2 => [errors...])` is returned. Each key of wrapped "error hash" corresponds to the key of validated hash, and each value of "error hash" contains array of errors, returned by the corresponding validator.
878
- If b) fulfilled, then and only then validated hash is checked for extra keys. If they are found, `ErrorResult(extra_key_1 => ["must be absent"], ...)` is returned.
994
+ If b) is fulfilled, then and only then validated hash is checked for extra keys. If they are found, `ErrorResult(extra_key_1 => ["should be absent"], ...)` is returned.
879
995
 
880
- Technically, last part is implemented with special singleton validator, called `#<Datacaster::Terminator::Raising>`, which is automatically added to the validation chain (with the use of `&` operator) by `Datacaster.schema` method. Don't be scared if you see it in the output of `#inspect` method of your validators (e.g. in `irb`).
996
+ I18n keys:
997
+
998
+ * not a hash – `error_key`, `'.hash_value'`, `'datacaster.errors.hash_value'`
881
999
 
882
1000
  #### Absent is not nil
883
1001
 
884
1002
  In practical tasks it's important to distinguish between absent (i.e. not set or deleted) and `nil` values of a hash.
885
1003
 
886
- To check some value for `nil`, use ordinary `compare(nil)` validator, mentioned above.
1004
+ To check some value for `nil`, use [`compare(nil)`](#comparereference_value-error_key--nil).
887
1005
 
888
1006
  To check some value for absence, use `absent` validator:
889
1007
 
@@ -900,14 +1018,14 @@ restricted_params.(username: "test")
900
1018
  # => Datacaster::ValidResult({:username=>"test"})
901
1019
 
902
1020
  restricted_params.(username: "test", is_admin: true)
903
- # => Datacaster::ErrorResult({:is_admin=>["must be absent"]})
1021
+ # => Datacaster::ErrorResult({:is_admin=>["should be absent"]})
904
1022
  restricted_params.(username: "test", is_admin: nil)
905
- # => Datacaster::ErrorResult({:is_admin=>["must be absent"]})
1023
+ # => Datacaster::ErrorResult({:is_admin=>["should be absent"]})
906
1024
  ```
907
1025
 
908
1026
  More practical case is to include `absent` validator in logical expressions, e.g. `something: absent | string`. If `something` is set to `nil`, this validation will fail, which could be the desired (and hardly achieved by any other validation framework) behavior.
909
1027
 
910
- Also, see documentation for `optional(base)` and `optional_param(base)` above. If some value becomes `Datacaster.absent` in its chain of validations-transformations, it is removed from the resultant hash (on the same stage where the lack of extra/unchecked keys in the hash is validated):
1028
+ Also, see documentation for [`optional(base)`](#optionalbase) and [`optional_param(base)`](#optional_parambase). If some value becomes `Datacaster.absent` in its chain of validations-transformations, it is removed from the resultant hash (on the same stage where the lack of extra/unchecked keys in the hash is validated):
911
1029
 
912
1030
  ```ruby
913
1031
  person =
@@ -924,10 +1042,10 @@ person.(name: "John Smith")
924
1042
  # => Datacaster::ValidResult({:name=>"John Smith"})
925
1043
 
926
1044
  person.(name: "John Smith", dob: "invalid date")
927
- # => Datacaster::ErrorResult({:dob=>["must be iso8601 string"]})
1045
+ # => Datacaster::ErrorResult({:dob=>["is not a string with ISO-8601 date and time"]})
928
1046
  ```
929
1047
 
930
- Another use-case for `Datacaster.absent` is to directly set some key to that value. In that case, it will be removed from the resultant hash. The most convenient way to do that is to use `remove` type (described above in this file):
1048
+ Another use case for `Datacaster.absent` is to directly set some key to that value. In that case, it will be removed from the resultant hash. The most convenient way to do that is to use the [`remove`](#remove) cast:
931
1049
 
932
1050
  ```ruby
933
1051
  anonimized_person =
@@ -942,9 +1060,9 @@ anonimized_person.(name: "John Johnson", dob: "1990-05-23")
942
1060
  # => Datacaster::ValidResult({:dob=>"1990-05-23"})
943
1061
  ```
944
1062
 
945
- Note: we need to `pass` `dob` field to "mark" it as validated, otherwise `Datacaster.schema` will return ErrorResult, notifying that unchecked extra field was in the initial hash.
1063
+ Note: we need to `pass` `dob` field to "mark" it as validated, otherwise `Datacaster.schema` will return `ErrorResult`, notifying that unchecked extra field was in the initial hash.
946
1064
 
947
- #### Schema vs Partial schema
1065
+ #### Schema vs Partial schema vs Choosy schema
948
1066
 
949
1067
  As written in the beginning of this section on `hash_schema`, at the last stage of validation it is ensured that hash contains no extra keys.
950
1068
 
@@ -1006,7 +1124,7 @@ RecordValidator =
1006
1124
  end
1007
1125
  ```
1008
1126
 
1009
- See "IF... THEN... ELSE" section above in this file for full description of how `a.then(b).else(c)` validator works.
1127
+ See also ["IF... THEN... ELSE"](#if-then-else-operator) section.
1010
1128
 
1011
1129
  Examples of how this validator would work:
1012
1130
 
@@ -1028,15 +1146,17 @@ RecordValidator.(
1028
1146
  description: 'CEO',
1029
1147
  extra: :key
1030
1148
  )
1031
- # => Datacaster::ErrorResult({:extra=>["must be absent"]})
1149
+ # => Datacaster::ErrorResult({:extra=>["should be absent"]})
1032
1150
  ```
1033
1151
 
1034
- Note that only the usage of `Datacaster.partial_schema` instead of `Datacaster.schema` allowed us to compose several `hash_schema`s from different files (from different calls to Datacaster API).
1152
+ Notice that only the usage of `Datacaster.partial_schema` instead of `Datacaster.schema` allowed us to compose several `hash_schema`s from different files (from different calls to Datacaster API).
1035
1153
 
1036
1154
  Had we used `schema` everywhere, `CommonFieldsValidator` would return failure for records which are supposed to be valid, because they would contain "extra" (i.e. not defined in `CommonFieldsValidator` itself) keys (e.g. `name` for person).
1037
1155
 
1038
1156
  As a rule of thumb, use `partial_schema` in any "intermediary" validators (extracted for the sake of clarity of code and reusability) and use `schema` in any "end" validators (ones which receive full record as input and use intermediary validators behind the scenes).
1039
1157
 
1158
+ Lastly, if you want to just delete extra unvalidated keys without returning a error, use `choosy_schema`.
1159
+
1040
1160
  #### AND with error aggregation (`*`)
1041
1161
 
1042
1162
  Often it is useful to run validator which are "further down the conveyor" (i.e. placed at the right-hand side of AND operator `&`) even if current (i.e. left-hand side) validator has failed.
@@ -1068,10 +1188,10 @@ This code will work as expected (i.e. `RecordValidator`, the "end" validator, wi
1068
1188
 
1069
1189
  ```ruby
1070
1190
  RecordValidator.(kind: 'person', name: 1)
1071
- # => Datacaster::ErrorResult({:description=>["must be string"]})
1191
+ # => Datacaster::ErrorResult({:description=>["is not a string"]})
1072
1192
  ```
1073
1193
 
1074
- It correctly returns `ErrorResult`, but it doesn't mention that in addition to `description` being wrongfully absent, `name` field is of wrong type (integer instead of string). That could be inconvenient where Datacaster is used, for example, as a params validator for an API service: end user of the API would need to repeatedly send requests, essentially "brute forcing" his way in through all the errors (fixing them one by one), instead of having the list of all errors in one iteration.
1194
+ It correctly returns `ErrorResult`, but it doesn't mention that in addition to `description` being wrongfully absent, `name` field is of the wrong type (integer instead of string). Such error reporting would be incomplete.
1075
1195
 
1076
1196
  Specifically to resolve this, "AND with error aggregation" (`*`) operator should be used in place of regular AND (`&`):
1077
1197
 
@@ -1082,12 +1202,12 @@ RecordValidator =
1082
1202
  end
1083
1203
 
1084
1204
  RecordValidator.(kind: 'person', name: 1)
1085
- # => Datacaster::ErrorResult({:description=>["must be string"], :name=>["must be string"]})
1205
+ # => Datacaster::ErrorResult({:description=>["is not a string"], :name=>["is not a string"]})
1086
1206
  ```
1087
1207
 
1088
1208
  Note: "star" (`*`) has been chosen arbitrarily among available Ruby operators. It shouldn't be read as multiplication (and, in fact, in Ruby it is used not only as multiplication sign).
1089
1209
 
1090
- Described in this example is the only case where `*` and `&` differ: in all other aspects they are full equivalents.
1210
+ Described in this example is the only case where `*` and `&` differ: in all other aspects they are fully equivalent.
1091
1211
 
1092
1212
  Formally, "AND with error aggregation" (`*`):
1093
1213
 
@@ -1096,9 +1216,9 @@ b) in all other cases behaves as regular "AND" (`&`).
1096
1216
 
1097
1217
  ### Shortcut nested definitions
1098
1218
 
1099
- Datacaster aimed at ease of use where multi-level embedded structures need to be validated, boilerplate reduced to inevitable minimum.
1219
+ Datacaster aimed at thr ease of use where multi-level embedded structures need to be validated, boilerplate reduced to inevitable minimum.
1100
1220
 
1101
- The words `hash_schema` and `array_schema`/`array_of` could be, therefore, omitted from the definition of nested structures (replaced with `{...}` and `[...]` correspondingly):
1221
+ The words `hash_schema` and `array_schema`/`array_of` could be omitted from the definition of nested structures (replaced with `{...}` and `[...]`):
1102
1222
 
1103
1223
  ```ruby
1104
1224
  # full definition
@@ -1140,11 +1260,11 @@ person =
1140
1260
  end
1141
1261
  ```
1142
1262
 
1143
- Note: in "root" scope (immediately inside of `schema { ... }` block) words `hash_schema` and `array_of` are still required. We consider that allowing to omit them as well would hurt readability of code.
1263
+ Note: in the "root" scope (immediately inside of `schema { ... }` block) the words `hash_schema` and `array_of` are still required. We consider that allowing to omit them as well would hurt readability of the code.
1144
1264
 
1145
1265
  ### Mapping hashes: `transform_to_hash`
1146
1266
 
1147
- One common task in processing compound data structures is to map one set of hash keys to another set. That's where `transform_to_hash` type comes to play (see also `pluck` and `remove` description above in this file).
1267
+ One common task in processing compound data structures is to map one set of hash keys to another set. That's where `transform_to_hash` type comes to play (see also [`pick`](#pickkey) and [`remove`](#remove)).
1148
1268
 
1149
1269
  ```ruby
1150
1270
  city_with_distance =
@@ -1160,7 +1280,7 @@ city_with_distance.(distance_in_meters: 1200.0)
1160
1280
  # => Datacaster::ValidResult({:distance_in_km=>1.2, :distance_in_miles=>1.9307999999999998})
1161
1281
  ```
1162
1282
 
1163
- Of course, order of keys in the definition hash doesn't change anything.
1283
+ Of course, order of keys in the definition hash doesn't change the result.
1164
1284
 
1165
1285
  Formally, `transform_to_hash`:
1166
1286
 
@@ -1168,7 +1288,7 @@ a) transforms (any) value to hash;
1168
1288
  b) this hash will contain keys listed in `transform_to_hash` definition;
1169
1289
  c) value of these keys will be: initial value (*not the corresponding key of it, the value altogether*) transformed with the corresponding validator/type;
1170
1290
  d) if any of the values from c) happen to be `Datacaster.absent`, this value *with its key* is removed from the resultant hash;
1171
- e) if the initial value happens to also be a hash, all its keys, except those which had been transformed, are merged to the resultant hash.
1291
+ e) if the initial value happens to also be a hash, all its unvalidated (unused) keys are merged to the resultant hash.
1172
1292
 
1173
1293
  `transform_to_hash` will return ValidResult if and only if all transformations return ValidResults.
1174
1294
 
@@ -1178,15 +1298,56 @@ Here is what is happening when `city_with_distance` (from the example above) is
1178
1298
 
1179
1299
  * Initial hash `{distance_in_meters: 1200}` is passed to `transform_to_hash`
1180
1300
  * `transform_to_hash` reads through its definition and creates resultant hash with the keys `distance_in_km`, `distance_in_miles`, `distance_in_meters`
1181
- * The key `distance_in_km` of the resultant hash in the transformation of the initial hash: firstly, hash is transformed to the value of its key with `pluck`, then that value is divided by 1000
1301
+ * The key `distance_in_km` of the resultant hash is the transformation of the initial hash: firstly, hash is transformed to the value of its key with `pick`, then that value is divided by 1000
1182
1302
  * Similarly, `distance_in_miles` value is built
1183
1303
  * `distance_in_meters` value is created by transforming initial value to `Datacaster.absent` (that is how `remove` works)
1184
1304
 
1185
- Note: because of point e) above we need to explicitly delete `distance_in_meters` key, because otherwise `transform_to_hash` will copy it to the resultant hash without validation. And all non-validated keys at the end of `Datacaster.schema` block (as explained above in section on partial schemas) result in error.
1305
+ Note: because of point e) above we need to explicitly delete `distance_in_meters` key, because otherwise `transform_to_hash` will copy it to the resultant hash without validation. And exitence of non-validated keys at the end of `Datacaster.schema` block results in an error result.
1186
1306
 
1187
- ## Error remapping
1307
+ ## Passing additional context to schemas
1188
1308
 
1189
- In some cases it might be useful to remap resulting `Datacaster::ErrorResult`:
1309
+ It is often useful to extract common data which is used in validations, but not a main subject of validations, to a separate context object.
1310
+
1311
+ This can be achived by using `#with_context`, which makes provided context available in the `context` structure:
1312
+
1313
+ ```ruby
1314
+ # class User < ApplicationRecord
1315
+ # ...
1316
+ # end
1317
+ #
1318
+ # class Post < ApplicationRecord
1319
+ # belongs_to :user
1320
+ # ...
1321
+ # end
1322
+
1323
+ schema =
1324
+ Datacaster.schema do
1325
+ hash_schema(
1326
+ post_id: to_integer & check { |id| Post.where(id: id, user_id: context.current_user).exists? }
1327
+ )
1328
+ end
1329
+
1330
+ current_user = ...
1331
+
1332
+ schema.with_context(current_user: current_user).(post_id: 15)
1333
+ ```
1334
+
1335
+ `context` is an [OpenStruct](https://ruby-doc.org/stdlib-3.1.0/libdoc/ostruct/rdoc/OpenStruct.html) instance.
1336
+
1337
+ **Note**
1338
+
1339
+ `context` can be accesed only in casters' blocks. It can't be used in schema definition itself:
1340
+
1341
+ ```ruby
1342
+ # will raise NoMethodError
1343
+ Datacaster.schema { context.error }
1344
+ ```
1345
+
1346
+ ## Error remapping: `cast_errors`
1347
+
1348
+ Validation often includes [remapping](#mapping-hashes-transform_to_hash) of hash keys. In such cases errors require remapping back to the original keys.
1349
+
1350
+ Let's see an example:
1190
1351
 
1191
1352
  ```ruby
1192
1353
  schema =
@@ -1197,11 +1358,11 @@ schema =
1197
1358
  )
1198
1359
  end
1199
1360
 
1200
- schema.(user_id: 'wrong') # => #<Datacaster::ErrorResult({:posts=>["must be integer"]})>
1201
- # Instead of #<Datacaster::ErrorResult({:user_id=>["must be integer"]})>
1361
+ schema.(user_id: 'wrong') # => #<Datacaster::ErrorResult({:posts=>["is not an integer"]})>
1362
+ # Instead of #<Datacaster::ErrorResult({:user_id=>["is not an integer"]})>
1202
1363
  ```
1203
1364
 
1204
- `.cast_errors` can be used in such case:
1365
+ `.cast_errors` can be used to remap errors back:
1205
1366
 
1206
1367
  ```ruby
1207
1368
  schema =
@@ -1219,10 +1380,166 @@ schema =
1219
1380
  )
1220
1381
  end
1221
1382
 
1222
- schema.(user_id: 'wrong') # => #<Datacaster::ErrorResult({:user_id=>["must be integer"]})>
1383
+ schema.(user_id: 'wrong') # => #<Datacaster::ErrorResult({:user_id=>["is not an integer"]})>
1384
+ ```
1385
+
1386
+ `.cast_errors` will extract errors from the `ErrorResult` and provide them as value for the provided caster. If that caster returns `ErrorResult`, runtime exception is raised. If that caster returns `ValidResult`, it is packed back into `ErrorResult` and returned.
1387
+
1388
+ Any instance of `Datacaster` supports `#cast_errors`.
1389
+
1390
+ See also [merge_message_keys](#merge_message_keyskeys) caster.
1391
+
1392
+ ## Internationalization (i18n)
1393
+
1394
+ Datacaster natively supports i18n. Default messages (their keys are listed under "I18n keys" in the caster descriptions) are packed with the gem: [`en.yml`](config/locales/en.yml).
1395
+
1396
+ There are several ways to customize messages, described in this section.
1397
+
1398
+ ### Custom absolute keys
1399
+
1400
+ There are two ways to set absolute error key (i.e. key with full path to an error inside of a yml i18n file).
1401
+
1402
+ Let's consider the following i18n file:
1403
+
1404
+ ```yml
1405
+ en:
1406
+ user:
1407
+ errors:
1408
+ not_found: User %{value} has not been found
1409
+ ```
1410
+
1411
+ Interpolated i18n variable `value` is added automatically for all built-in casters.
1412
+
1413
+ Firstly, you can set `error_key` of a caster:
1414
+
1415
+ ```ruby
1416
+ schema = Datacaster.schema { check('user.errors.not_found') { false } }
1417
+ schema.('john').errors # ['User john has not been found']
1418
+ ```
1419
+
1420
+ Secondly, you can call `#i18n_key` on a caster:
1421
+
1422
+ ```ruby
1423
+ schema =
1424
+ Datacaster.schema do
1425
+ check { false }.i18n_key('user.errors.not_found')
1426
+ end
1427
+
1428
+ schema.('john').errors # ['User john has not been found']
1429
+ ```
1430
+
1431
+ ### Custom relative keys and scopes
1432
+
1433
+ More often it is required to set specific i18n namespace for the whole validation schema. There is a manual way to do it with `#i18n_scope` and automatic scoping for hashes.
1434
+
1435
+ Let's consider the following i18n file:
1436
+
1437
+ ```yml
1438
+ en:
1439
+ user:
1440
+ errors:
1441
+ not_found: User has not been found
1442
+ name:
1443
+ wrong_format: wrong format
1444
+ ```
1445
+
1446
+ Let's gradually reduce the boilerplate, starting with the most explicit example. Notice that all relative keys (i.e. keys which will be scoped during the execution) start with `'.'`:
1447
+
1448
+ ```ruby
1449
+ schema =
1450
+ Datacaster.schema(i18n_scope: 'user') do
1451
+ check { |v| v[:id] == 1 }.i18n_key('.errors.not_found') &
1452
+ hash_schema(
1453
+ name: check { false }.i18n_key('.name.wrong_format')
1454
+ )
1455
+ end
1456
+
1457
+ schema.({id: 3}).errors # ['User has not been found']
1458
+ schema.({id: 1, name: 'wrong'}).errors # {name: ['wrong format']}
1459
+ ```
1460
+
1461
+ To reduce the boilerplate, Datacaster will infer scopes from hash key names:
1462
+
1463
+ ```ruby
1464
+ schema =
1465
+ Datacaster.schema(i18n_scope: 'user') do
1466
+ check { |v| v[:id] == 1 }.i18n_key('.errors.not_found') &
1467
+ hash_schema(
1468
+ # '.wrong_format' inferred to be '.name.wrong_format'
1469
+ name: check { false }.i18n_key('.wrong_format')
1470
+ )
1471
+ end
1472
+
1473
+ schema.({id: 1, name: 'wrong'}).errors # {name: ['wrong format']}
1474
+ ```
1475
+
1476
+ Relative keys can be set as `error_key` argument of casters:
1477
+
1478
+ ```ruby
1479
+ schema =
1480
+ Datacaster.schema(i18n_scope: 'user') do
1481
+ check('.errors.not_found') { |v| v[:id] == 1 } &
1482
+ hash_schema(
1483
+ # '.wrong_format' inferred to be '.name.wrong_format'
1484
+ name: check('.wrong_format') { false }
1485
+ )
1486
+ end
1487
+
1488
+ schema.({id: 1, name: 'wrong'}).errors # {name: ['wrong format']}
1489
+ ```
1490
+
1491
+ When feasible, format yaml file in accordance with the default casters' keys. However, with this approach often key names wouldn't make much sense in the application context:
1492
+
1493
+ ```yml
1494
+ en:
1495
+ user:
1496
+ check: User has not been found
1497
+ name:
1498
+ check: wrong format
1499
+ ```
1500
+
1501
+ ```ruby
1502
+ schema =
1503
+ # Only root scope is set, no other boilerplate
1504
+ Datacaster.schema(i18n_scope: 'user') do
1505
+ check { |v| v[:id] == 1 } &
1506
+ hash_schema(
1507
+ name: check { false }
1508
+ )
1509
+ end
1510
+
1511
+ schema.({id: 3}).errors # ['User has not been found']
1512
+ schema.({id: 1, name: 'wrong'}).errors # {name: ['wrong format']}
1513
+ ```
1514
+
1515
+ Use `#raw_errors` instead of `#errors` to get errors just before the I18n backend is called. This will allow to see all the i18n keys in the order of priority which will be used to produce final error messages.
1516
+
1517
+ Notice that the use of `.i18n_scope` prevents auto-scoping of hash key:
1518
+
1519
+ ```ruby
1520
+ schema =
1521
+ # Only root scope is set, no other boilerplate
1522
+ Datacaster.schema(i18n_scope: 'user') do
1523
+ hash_schema(
1524
+ name: check { false }.i18n_scope('.data')
1525
+ )
1526
+ end
1527
+
1528
+ # will search for the following keys:
1529
+ # - "user.data.check"
1530
+ # - "datacaster.errors.check"
1531
+ schema.(name: 'john').raw_errors
1223
1532
  ```
1224
- any instance of `Datacaster` can be passed to `.cast_errors`
1225
1533
 
1534
+ ### Providing interpolation variables
1535
+
1536
+ Every caster will automatically provide `value` variable for i18n interpolation.
1537
+
1538
+ All keyword arguments of `#i18n_key`, `#i18n_scope` and designed for that sole purpose `#i18n_vars` are provided as interpolation variables on i18n.
1539
+
1540
+ It is possible to add i18n variables at the runtime (e.g. inside `check { ... }` block) by calling `i18n_vars!(variable: 'value')` or `i18n_var!(:variable, 'value')`.
1541
+
1542
+ Outer calls of `#i18n_key` (`#i18n_scope`, `#i18n_vars`) have presedence before the inner if variable names collide. However, runtime calls of `#i18n_vars!` and `#i18n_var!` overwrite compile-time variables from the next nearest key, scope or vars on collision.
1226
1543
 
1227
1544
  ## Registering custom 'predefined' types
1228
1545