datacaster 2.0.2 → 3.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (43) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +518 -268
  3. data/config/locales/en.yml +24 -0
  4. data/datacaster.gemspec +2 -0
  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/structure_cleaner.rb +103 -0
  19. data/lib/datacaster/context_nodes/user_context.rb +20 -0
  20. data/lib/datacaster/definition_dsl.rb +37 -0
  21. data/lib/datacaster/hash_mapper.rb +13 -16
  22. data/lib/datacaster/hash_schema.rb +14 -15
  23. data/lib/datacaster/i18n_values/base.rb +87 -0
  24. data/lib/datacaster/i18n_values/key.rb +34 -0
  25. data/lib/datacaster/i18n_values/scope.rb +28 -0
  26. data/lib/datacaster/message_keys_merger.rb +8 -15
  27. data/lib/datacaster/or_node.rb +3 -4
  28. data/lib/datacaster/predefined.rb +119 -64
  29. data/lib/datacaster/result.rb +35 -14
  30. data/lib/datacaster/runtimes/base.rb +47 -0
  31. data/lib/datacaster/runtimes/i18n.rb +20 -0
  32. data/lib/datacaster/runtimes/structure_cleaner.rb +47 -0
  33. data/lib/datacaster/runtimes/user_context.rb +39 -0
  34. data/lib/datacaster/substitute_i18n.rb +48 -0
  35. data/lib/datacaster/then_node.rb +7 -8
  36. data/lib/datacaster/transformer.rb +4 -8
  37. data/lib/datacaster/trier.rb +9 -11
  38. data/lib/datacaster/validator.rb +8 -9
  39. data/lib/datacaster/version.rb +1 -1
  40. data/lib/datacaster.rb +15 -35
  41. metadata +57 -9
  42. data/lib/datacaster/definition_context.rb +0 -20
  43. 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
 
@@ -19,50 +20,55 @@ Its main use is in the validation and preliminary transformation of API params r
19
20
  - [*IF... THEN... ELSE operator*:](#if-then-else-operator)
20
21
  - [Built-in types](#built-in-types)
21
22
  - [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)
23
+ - [`array(error_key = nil)`](#arrayerror_key--nil)
24
+ - [`decimal(digits = 8, error_key = nil)`](#decimaldigits--8-error_key--nil)
25
+ - [`float(error_key = nil)`](#floaterror_key--nil)
26
+ - [`hash_value(error_key = nil)`](#hash_valueerror_key--nil)
27
+ - [`integer(error_key = nil)`](#integererror_key--nil)
28
+ - [`string(error_key = nil)`](#stringerror_key--nil)
28
29
  - [Convenience types](#convenience-types)
29
- - [`non_empty_string`](#non_empty_string)
30
- - [`hash_with_symbolized_keys`](#hash_with_symbolized_keys)
31
- - [`integer32`](#integer32)
30
+ - [`hash_with_symbolized_keys(error_key = nil)`](#hash_with_symbolized_keyserror_key--nil)
31
+ - [`integer32(error_key = nil)`](#integer32error_key--nil)
32
+ - [`non_empty_string(error_key = nil)`](#non_empty_stringerror_key--nil)
32
33
  - [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)
34
+ - [`absent(error_key = nil)`](#absenterror_key--nil)
35
+ - [`any(error_key = nil)`](#anyerror_key--nil)
36
+ - [`default(default_value, on: nil)`](#defaultdefault_value-on-nil)
37
+ - [`merge_message_keys(*keys)`](#merge_message_keyskeys)
38
+ - [`must_be(klass, error_key = nil)`](#must_beklass-error_key--nil)
40
39
  - [`optional(base)`](#optionalbase)
40
+ - [`pass`](#pass)
41
41
  - [`pick(key)`](#pickkey)
42
- - [`merge_message_keys(*keys)`](#merge_message_keyskeys)
42
+ - [`remove`](#remove)
43
+ - [`responds_to(method, error_key = nil)`](#responds_tomethod-error_key--nil)
44
+ - [`transform_to_value(value)`](#transform_to_valuevalue)
43
45
  - ["Web-form" types](#web-form-types)
44
- - [`to_integer`](#to_integer)
45
- - [`to_float`](#to_float)
46
- - [`to_boolean`](#to_boolean)
47
- - [`iso8601`](#iso8601)
46
+ - [`iso8601(error_key = nil)`](#iso8601error_key--nil)
48
47
  - [`optional_param(base)`](#optional_parambase)
48
+ - [`to_boolean(error_key = nil)`](#to_booleanerror_key--nil)
49
+ - [`to_float(error_key = nil)`](#to_floaterror_key--nil)
50
+ - [`to_integer(error_key = nil)`](#to_integererror_key--nil)
49
51
  - [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--)
52
+ - [`cast { |value| ... }`](#cast--value--)
53
+ - [`check(error_key = nil) { |value| ... }`](#checkerror_key--nil--value--)
54
+ - [`try(error_key = nil, catched_exception:) { |value| ... }`](#tryerror_key--nil-catched_exception--value--)
53
55
  - [`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)
56
+ - [`compare(reference_value, error_key = nil)`](#comparereference_value-error_key--nil)
57
+ - [`transform { |value| ... }`](#transform--value--)
58
+ - [`transform_if_present { |value| ... }`](#transform_if_present--value--)
58
59
  - [Array schemas](#array-schemas)
59
60
  - [Hash schemas](#hash-schemas)
60
61
  - [Absent is not nil](#absent-is-not-nil)
61
- - [Schema vs Partial schema](#schema-vs-partial-schema)
62
+ - [Schema vs Partial schema vs Choosy schema](#schema-vs-partial-schema-vs-choosy-schema)
62
63
  - [AND with error aggregation (`*`)](#and-with-error-aggregation-)
63
64
  - [Shortcut nested definitions](#shortcut-nested-definitions)
64
65
  - [Mapping hashes: `transform_to_hash`](#mapping-hashes-transform_to_hash)
65
- - [Error remapping](#error-remapping)
66
+ - [Passing additional context to schemas](#passing-additional-context-to-schemas)
67
+ - [Error remapping: `cast_errors`](#error-remapping-cast_errors)
68
+ - [Internationalization (i18n)](#internationalization-i18n)
69
+ - [Custom absolute keys](#custom-absolute-keys)
70
+ - [Custom relative keys and scopes](#custom-relative-keys-and-scopes)
71
+ - [Providing interpolation variables](#providing-interpolation-variables)
66
72
  - [Registering custom 'predefined' types](#registering-custom-predefined-types)
67
73
  - [Contributing](#contributing)
68
74
  - [Ideas/TODO](#ideastodo)
@@ -108,13 +114,13 @@ validator.("test").valid? # true
108
114
  validator.("test").value # "test"
109
115
  validator.("test").errors # nil
110
116
 
111
- validator.(1) # Datacaster::ErrorResult(["must be string"])
117
+ validator.(1) # Datacaster::ErrorResult(["is not a string"])
112
118
  validator.(1).valid? # false
113
119
  validator.(1).value # nil
114
- validator.(1).errors # ["must be string"]
120
+ validator.(1).errors # ["is not a string"]
115
121
  ```
116
122
 
117
- Datacaster instances are created with a call to `Datacaster.schema { ... }`, `Datacaster.partial_schema { ... }` or `Datacaster.choosy_schema { ... }` (described later in this file).
123
+ Datacaster instances are created with a call to `Datacaster.schema { ... }`, `Datacaster.partial_schema { ... }` or `Datacaster.choosy_schema { ... }`.
118
124
 
119
125
  Datacaster validators' results could be converted to [dry result monad](https://dry-rb.org/gems/dry-monads/1.0/result/):
120
126
 
@@ -124,7 +130,7 @@ require 'datacaster'
124
130
  validator = Datacaster.schema { string }
125
131
 
126
132
  validator.("test").to_dry_result # Success("test")
127
- validator.(1).to_dry_result # Failure(["must be string"])
133
+ validator.(1).to_dry_result # Failure(["is not a string"])
128
134
  ```
129
135
 
130
136
  `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 +141,7 @@ require 'datacaster'
135
141
  validator = Datacaster.schema { string & check { |x| x.length > 5 } }
136
142
 
137
143
  validator.("test1") # Datacaster::ValidResult("test12")
138
- validator.(1) # Datacaster::ErrorResult(["must be string"])
144
+ validator.(1) # Datacaster::ErrorResult(["is not a string"])
139
145
  validator.("test") # Datacaster::ErrorResult(["is invalid"])
140
146
  ```
141
147
 
@@ -158,7 +164,22 @@ You can call `#valid?`, `#value`, `#errors` methods directly, or, if preferred,
158
164
 
159
165
  `#value` and `#errors` would return `#nil` if the result is, correspondingly, `ErrorResult` and `ValidResult`. No methods would raise an error.
160
166
 
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`.
167
+ 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.
168
+
169
+ In this README, instead of i18n values English strings are provided for brevity:
170
+
171
+ ```ruby
172
+ array = Datacaster.schema { array }
173
+ array.(nil)
174
+
175
+ # In this README
176
+ #=> Datacaster::ErrorResult(['should be an array'])
177
+
178
+ # In reality
179
+ #=> <Datacaster::ErrorResult([#<Datacaster::I18nValues::Key(.array, datacaster.errors.array) {:value=>nil}>])>
180
+ ```
181
+
182
+ See [section on i18n](#internationalization-i18n) for details.
162
183
 
163
184
  ### Hash schema
164
185
 
@@ -184,22 +205,22 @@ person_validator.(name: "Jack Simon", salary: 50_000)
184
205
  # => Datacaster::ValidResult({:name=>"Jack Simon", :salary=>50000})
185
206
 
186
207
  person_validator.(name: "Jack Simon")
187
- # => Datacaster::ErrorResult({:salary=>["must be integer"]})
208
+ # => Datacaster::ErrorResult({:salary=>["is not an integer"]})
188
209
 
189
210
  person_validator.("test")
190
- # => Datacaster::ErrorResult(["must be hash"])
211
+ # => Datacaster::ErrorResult(["is not a hash"])
191
212
 
192
213
  person_validator.(name: "John Smith", salary: "1000")
193
- # => Datacaster::ErrorResult({:salary=>["must be integer"]})
214
+ # => Datacaster::ErrorResult({:salary=>["is not an integer"]})
194
215
 
195
216
  person_validator.(name: :john, salary: "1000")
196
- # => Datacaster::ErrorResult({:name=>["must be string"], :salary=>["must be integer"]})
217
+ # => Datacaster::ErrorResult({:name=>["is not a string"], :salary=>["is not an integer"]})
197
218
 
198
219
  person_validator.(name: "John Smith", salary: 100_000, title: "developer")
199
- # => Datacaster::ErrorResult({:title=>["must be absent"]})
220
+ # => Datacaster::ErrorResult({:title=>["should be absent"]})
200
221
  ```
201
222
 
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.
223
+ `Datacaster.schema` definitions don't permit, as you have likely noticed from the example above, extra fields in the hash.
203
224
 
204
225
  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
226
 
@@ -256,7 +277,7 @@ even_number.(2)
256
277
  even_number.(3)
257
278
  # => Datacaster::ErrorResult(["is invalid"])
258
279
  even_number.("test")
259
- # => Datacaster::ErrorResult(["must be integer"])
280
+ # => Datacaster::ErrorResult(["is not an integer"])
260
281
  ```
261
282
 
262
283
  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.
@@ -270,7 +291,7 @@ person_or_entity = Datacaster.schema { compare(:person) | compare(:entity) }
270
291
  person_or_entity.(:person) # => Datacaster::ValidResult(:person)
271
292
  person_or_entity.(:entity) # => Datacaster::ValidResult(:entity)
272
293
 
273
- person_or_entity.(:ngo) # => Datacaster::ErrorResult(["must be equal to :entity"])
294
+ person_or_entity.(:ngo) # => Datacaster::ErrorResult(["does not equal :entity"])
274
295
  ```
275
296
 
276
297
  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.
@@ -341,147 +362,106 @@ With `a.then(b).else(c)` if `a` and `b` fails, then `b`'s error is returned. Wit
341
362
 
342
363
  Full description of all built-in types follows.
343
364
 
344
- ### Basic types
345
-
346
- #### `string`
347
-
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.
365
+ 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.
353
366
 
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.
367
+ ### Basic types
363
368
 
364
- #### `array`
369
+ #### `array(error_key = nil)`
365
370
 
366
371
  Returns ValidResult if and only if provided value is an `Array`. Doesn't transform the value.
367
372
 
368
- #### `hash_value`
369
-
370
- Returns ValidResult if and only if provided value is a `Hash`. Doesn't transform the value.
373
+ I18n keys: `error_key`, `'.array'`, `'datacaster.errors.array'`.
371
374
 
372
- Note: this type is called `hash_value` instead of `hash`, because `hash` is reserved method name in Ruby.
373
-
374
- ### Convenience types
375
+ #### `decimal(digits = 8, error_key = nil)`
375
376
 
376
- #### `non_empty_string`
377
+ Returns ValidResult if and only if provided value is either a float, integer or string representing float/integer.
377
378
 
378
- Returns ValidResult if and only if provided value is a string and is not empty. Doesn't transform the value.
379
+ Transforms the value to the `BigDecimal` instance.
379
380
 
380
- #### `hash_with_symbolized_keys`
381
+ I18n keys: `error_key`, `'.decimal'`, `'datacaster.errors.decimal'`.
381
382
 
382
- Returns ValidResult if and only if provided value is an instance of `Hash`. Transforms the value to `#hash_with_symbolized_keys` (requires `ActiveSupport`).
383
+ #### `float(error_key = nil)`
383
384
 
384
- #### `integer32`
385
+ 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.
385
386
 
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.
387
+ I18n keys: `error_key`, `'.float'`, `'datacaster.errors.float'`.
387
388
 
388
- ### Special types
389
+ #### `hash_value(error_key = nil)`
389
390
 
390
- #### `absent`
391
+ Returns ValidResult if and only if provided value is a `Hash`. Doesn't transform the value.
391
392
 
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.
393
+ Note: this type is called `hash_value` instead of `hash`, because `hash` is a reserved method name in Ruby.
393
394
 
394
- #### `any`
395
+ I18n keys: `error_key`, `'.hash_value'`, `'datacaster.errors.hash_value'`.
395
396
 
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.
397
+ #### `integer(error_key = nil)`
397
398
 
398
- #### `transform_to_value(value)`
399
+ Returns ValidResult if and only if provided value is an integer. Doesn't transform the value.
399
400
 
400
- Always returns ValidResult. The value is transformed to provided argument. Is used to provide default values, e.g.:
401
+ I18n keys: `error_key`, `'.integer'`, `'datacaster.errors.integer'`.
401
402
 
402
- ```ruby
403
- max_concurrent_connections = Datacaster.schema { compare(nil).then(transform_to_value(5)).else(integer) }
403
+ #### `string(error_key = nil)`
404
404
 
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
- ```
405
+ Returns ValidResult if and only if provided value is a string. Doesn't transform the value.
409
406
 
410
- #### `remove`
407
+ I18n keys: `error_key`, `'.string'`, `'datacaster.errors.string'`.
411
408
 
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).
409
+ ### Convenience types
413
410
 
414
- #### `pass`
411
+ #### `hash_with_symbolized_keys(error_key = nil)`
415
412
 
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).
413
+ Returns ValidResult if and only if provided value is an instance of `Hash`. Transforms the value to `#hash_with_symbolized_keys` (requires `ActiveSupport`).
417
414
 
418
- #### `responds_to(method)`
415
+ I18n keys: `error_key`, `'.hash_value'`, `'datacaster.errors.hash_value'`.
419
416
 
420
- Returns ValidResult if and only if value `#responds_to?(method)`. Doesn't transform the value.
417
+ #### `integer32(error_key = nil)`
421
418
 
422
- #### `must_be(klass)`
419
+ 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
420
 
424
- Returns ValidResult if and only if value `#is_a?(klass)`. Doesn't transform the value.
421
+ I18n keys:
425
422
 
426
- #### `optional(base)`
423
+ * not an integer – `error_key`, `'.integer'`, `'datacaster.errors.integer'`
424
+ * too big – `error_key`, `'.integer32'`, `'datacaster.errors.integer32'`
427
425
 
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`.
426
+ #### `non_empty_string(error_key = nil)`
429
427
 
430
- ```ruby
431
- item_with_optional_price =
432
- Datacaster.schema do
433
- hash_schema(
434
- name: string,
435
- price: optional(float)
436
- )
437
- end
428
+ Returns ValidResult if and only if provided value is a string and is not empty. Doesn't transform the value.
438
429
 
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"})
430
+ * not a string – `error_key`, `'.string'`, `'datacaster.errors.string'`
431
+ * is empty – `error_key`, `'.non_empty_string'`, `'datacaster.errors.non_empty_string'`
443
432
 
444
- item_with_optional_price.(name: "Book", price: "wrong")
445
- # => Datacaster::ErrorResult({:price=>["must be float"]})
446
- ```
433
+ ### Special types
447
434
 
448
- #### `pick(key)`
435
+ #### `absent(error_key = nil)`
449
436
 
450
- Returns ValidResult if and only if value `#is_a?(Enumerable)`.
437
+ 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
438
 
452
- Transforms the value to/returns:
439
+ I18n keys: `error_key`, `'.absent'`, `'datacaster.errors.absent'`.
453
440
 
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
441
+ #### `any(error_key = nil)`
457
442
 
458
- ```ruby
459
- pick_name = Datacaster.schema { pick(:name) }
443
+ 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
444
 
461
- pick_name.(name: "George") # => Datacaster::ValidResult("George")
462
- pick_name.(last_name: "Johnson") # => Datacaster::ValidResult(#<Datacaster.absent>)
445
+ I18n keys: `error_key`, `'.any'`, `'datacaster.errors.any'`
463
446
 
464
- pick_name.("test") # => Datacaster::ErrorResult(["must be Enumerable"])
465
- ```
447
+ #### `default(default_value, on: nil)`
466
448
 
467
- Alternative form could be used: `pick(*keys)`.
449
+ Always returns ValidResult.
468
450
 
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.
451
+ Returns `default_value` in the following cases:
470
452
 
471
- ```ruby
472
- pick_name_and_age = Datacaster.schema { pick(:name, :age) }
453
+ * if value is `Datacaster.absent` (`on` is disregarded in such case)
454
+ * if `on` is set to method name to which the value responds and yields truthy
473
455
 
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])
456
+ Returns initial value otherwise.
476
457
 
477
- pick_name_and_age.("test") # => Datacaster::ErrorResult(["must be Enumerable"])
478
- ```
458
+ Set `on` to `:nil?`, `:empty?` or similar method names.
479
459
 
480
460
  #### `merge_message_keys(*keys)`
481
461
 
482
- Returns ValidResult only if value `#is_a?(Hash)`.
462
+ Returns ValidResult only if the value `#is_a?(Hash)`.
483
463
 
484
- Maps incoming hash to Datacaster styled messages.
464
+ Picks given keys of incoming hash and merges their values recursively.
485
465
 
486
466
  ```ruby
487
467
  mapper =
@@ -551,7 +531,7 @@ mapping.(
551
531
  # })
552
532
  ```
553
533
 
554
- Hash keys with `nil` and `[]` values are deeply ignored:
534
+ Hash keys with `nil` and `[]` values are removed recursively:
555
535
 
556
536
  ```ruby
557
537
  mapping = Datacaster.schema do
@@ -572,25 +552,113 @@ mapping.(
572
552
  # })
573
553
  ```
574
554
 
575
- ### "Web-form" types
555
+ See also `#cast_errors` for [error remapping](#error-remapping-cast_errors).
576
556
 
577
- These types are convenient to parse and validate POST forms and decode JSON requests.
557
+ See also `#pick` for [simpler picking of hash values](#pickkey).
578
558
 
579
- #### `to_integer`
559
+ I18n keys:
580
560
 
581
- Returns ValidResult if and only if value is an integer, float or string representing integer/float. Transforms value to integer.
561
+ * not a hash `'.hash_value'`, `'datacaster.errors.hash_value'`
582
562
 
583
- #### `to_float`
563
+ #### `must_be(klass, error_key = nil)`
584
564
 
585
- Returns ValidResult if and only if value is an integer, float or string representing integer/float. Transforms value to float.
565
+ Returns ValidResult if and only if the value `#is_a?(klass)`. Doesn't transform the value.
586
566
 
587
- #### `to_boolean`
567
+ I18n keys: `error_key`, `'.must_be'`, `'datacaster.errors.must_be'`. Adds `reference` i18n variable, setting it to `klass.name`.
588
568
 
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).
569
+ #### `optional(base)`
590
570
 
591
- #### `iso8601`
571
+ 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
572
 
593
- Returns ValidResult if and only if value is a string in [ISO8601](https://en.wikipedia.org/wiki/ISO_8601) date-time format.
573
+ ```ruby
574
+ item_with_optional_price =
575
+ Datacaster.schema do
576
+ hash_schema(
577
+ name: string,
578
+ price: optional(float)
579
+ )
580
+ end
581
+
582
+ item_with_optional_price.(name: "Book", price: 1.23)
583
+ # => Datacaster::ValidResult({:name=>"Book", :price=>1.23})
584
+ item_with_optional_price.(name: "Book")
585
+ # => Datacaster::ValidResult({:name=>"Book"})
586
+
587
+ item_with_optional_price.(name: "Book", price: "wrong")
588
+ # => Datacaster::ErrorResult({:price=>["is not a float"]})
589
+ ```
590
+
591
+ #### `pass`
592
+
593
+ Always returns ValidResult. Doesn't transform the value.
594
+
595
+ Useful to "mark" the value as validated (see section below on hash schemas, where this could be applied).
596
+
597
+ #### `pick(key)`
598
+
599
+ Returns ValidResult if and only if the value `#is_a?(Enumerable)`.
600
+
601
+ Transforms the value to/returns:
602
+
603
+ * `value[key]` if key is set in the value
604
+ * `nil` if `value[key]` is set and is nil
605
+ * `Datacaster.absent` if key is not set
606
+
607
+ ```ruby
608
+ pick_name = Datacaster.schema { pick(:name) }
609
+
610
+ pick_name.(name: "George") # => Datacaster::ValidResult("George")
611
+ pick_name.(last_name: "Johnson") # => Datacaster::ValidResult(#<Datacaster.absent>)
612
+
613
+ pick_name.("test") # => Datacaster::ErrorResult(["is not Enumerable"])
614
+ ```
615
+
616
+ Alternative form could be used: `pick(*keys)`.
617
+
618
+ 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.
619
+
620
+ ```ruby
621
+ pick_name_and_age = Datacaster.schema { pick(:name, :age) }
622
+
623
+ pick_name_and_age.(name: "George", age: 20) # => Datacaster::ValidResult(["George", 20])
624
+ pick_name_and_age.(last_name: "Johnson", age: 20) # => Datacaster::ValidResult([#<Datacaster.absent>, 20])
625
+
626
+ pick_name_and_age.("test") # => Datacaster::ErrorResult(["is not Enumerable"])
627
+ ```
628
+
629
+ I18n keys:
630
+
631
+ * not a Enumerable – `'.must_be'`, `'datacaster.errors.must_be'`.
632
+
633
+ #### `remove`
634
+
635
+ Always returns ValidResult. Always returns `Datacaster.absent`.
636
+
637
+ #### `responds_to(method, error_key = nil)`
638
+
639
+ Returns ValidResult if and only if the value `#responds_to?(method)`. Doesn't transform the value.
640
+
641
+ I18n keys: `error_key`, `'.responds_to'`, `'datacaster.errors.responds_to'`. Adds `reference` i18n variable, setting it to `method.to_s`.
642
+
643
+ #### `transform_to_value(value)`
644
+
645
+ Always returns ValidResult. The value is transformed to provided argument. Is used to provide default values, e.g.:
646
+
647
+ ```ruby
648
+ max_concurrent_connections = Datacaster.schema { compare(nil).then(transform_to_value(5)).else(integer) }
649
+
650
+ max_concurrent_connections.(9) # => Datacaster::ValidResult(9)
651
+ max_concurrent_connections.("9") # => Datacaster::ErrorResult(["is not an integer"])
652
+ max_concurrent_connections.(nil) # => Datacaster::ValidResult(5)
653
+ ```
654
+
655
+ ### "Web-form" types
656
+
657
+ These types are convenient to parse and validate POST forms and decode JSON requests.
658
+
659
+ #### `iso8601(error_key = nil)`
660
+
661
+ Returns ValidResult if and only if the value is a string in [ISO8601](https://en.wikipedia.org/wiki/ISO_8601) date-time format.
594
662
 
595
663
  ```ruby
596
664
  dob = Datacaster.schema { iso8601 }
@@ -599,37 +667,53 @@ dob.("2011-02-03")
599
667
  # => Datacaster::ValidResult(#<DateTime: 2011-02-03T00:00:00+00:00 ...>)
600
668
  ```
601
669
 
602
- Transforms value to `DateTime` instance.
670
+ Transforms the value to the `DateTime` instance.
671
+
672
+ I18n keys: `error_key`, `'.iso8601'`, `'datacaster.errors.iso8601'`.
603
673
 
604
674
  #### `optional_param(base)`
605
675
 
606
- Returns ValidResult if and only if value is absent, empty string or passes `base` validation.
676
+ Returns ValidResult if and only if the value is absent, empty string or passes `base` validation.
607
677
 
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.
678
+ 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
679
 
610
680
  Otherwise, doesn't transform the value.
611
681
 
612
- ### Custom and fundamental types
682
+ #### `to_boolean(error_key = nil)`
683
+
684
+ 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).
685
+
686
+ I18n keys: `error_key`, `'.to_boolean'`, `'datacaster.errors.to_boolean'`
613
687
 
614
- These custom types (or 'meta' types) are used to create 'hand-crafted' validators.
688
+ #### `to_float(error_key = nil)`
615
689
 
616
- When `name` argument is available, that argument determines what would display with `#inspect` of that validator (and nothing else).
690
+ Returns ValidResult if and only if the value is an integer, float or string representing integer/float. Transforms value to float.
617
691
 
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.
692
+ I18n keys: `error_key`, `'.to_float'`, `'datacaster.errors.to_float'`
619
693
 
620
- #### `cast(name = 'Anonymous') { |value| ... }`
694
+ #### `to_integer(error_key = nil)`
695
+
696
+ Returns ValidResult if and only if the value is an integer, float or string representing integer/float. Transforms the value to the integer.
697
+
698
+ I18n keys: `error_key`, `'.to_integer'`, `'datacaster.errors.to_integer'`.
699
+
700
+ ### Custom and fundamental types
701
+
702
+ These types are used to create 'hand-crafted' validators.
703
+
704
+ #### `cast { |value| ... }`
621
705
 
622
706
  The most basic &mdash; "fully manual" &mdash; validator.
623
707
 
624
- Calls block with the value. Returns whatever block returns.
708
+ Calls the block with the value. Returns whatever the block returns.
625
709
 
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`.
710
+ 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
711
 
628
712
  ```ruby
629
- # Actually, better use 'check' here instead (see below)
713
+ # Actually, it's better to use 'check' here instead
630
714
  user_id_exists =
631
715
  Datacaster.schema do
632
- cast('UserIdExists') do |user_id|
716
+ cast do |user_id|
633
717
  if User.exists?(user_id)
634
718
  Success(user_id) # or Datacaster::ValidResult(user_id)
635
719
  else
@@ -642,18 +726,18 @@ user_id_exists =
642
726
  end
643
727
  ```
644
728
 
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.
729
+ Notice, that for this example (as is written in the comment) `check` type is a better option (see below).
646
730
 
647
- `cast` will transform value, if such is the logic of provided block.
731
+ `cast` will transform the value, if such is the logic of the provided block.
648
732
 
649
- #### `check(name = 'Anonymous', error = 'is invalid') { |value| ... }`
733
+ #### `check(error_key = nil) { |value| ... }`
650
734
 
651
- Returns ValidResult if and only if provided block returns truthy value (i.e. anything except `false` and `nil`).
735
+ Returns ValidResult if and only if the provided block returns truthy value.
652
736
 
653
737
  ```ruby
654
738
  user_id_exists =
655
739
  Datacaster.schema do
656
- check('UserIdExists', 'user is not found') do |user_id|
740
+ check do |user_id|
657
741
  User.exists?(user_id)
658
742
  end
659
743
  end
@@ -661,9 +745,11 @@ user_id_exists =
661
745
 
662
746
  Doesn't transform the value.
663
747
 
664
- #### `try(name = 'Anonymous', error = 'is invalid', catched_exception:) { |value| ... }`
748
+ I18n keys: `error_key`, `'.check'`, `'datacaster.errors.check'`.
665
749
 
666
- Returns ValidResult if and only if block finishes without exceptions. If block raises an exception:
750
+ #### `try(error_key = nil, catched_exception:) { |value| ... }`
751
+
752
+ Returns ValidResult if and only if the block finishes without exceptions. If the block raises an exception:
667
753
 
668
754
  * if exception class equals to `catched_exception`, then ErrorResult is returned;
669
755
  * otherwise, exception is re-raised.
@@ -681,10 +767,10 @@ dangerous_validator =
681
767
  end
682
768
  ```
683
769
 
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
770
  Doesn't transform the value.
687
771
 
772
+ I18n keys: `error_key`, `'.try'`, `'datacaster.errors.try'`
773
+
688
774
  #### `validate(active_model_validations, name = 'Anonymous')`
689
775
 
690
776
  Requires ActiveModel.
@@ -710,7 +796,9 @@ nickname.("user32") # Datacaster::ErrorResult(["only allows letters"])
710
796
 
711
797
  Doesn't transform the value.
712
798
 
713
- #### `compare(reference_value, name = 'Anonymous', error = nil)`
799
+ I18n is performed by ActiveModel gem.
800
+
801
+ #### `compare(reference_value, error_key = nil)`
714
802
 
715
803
  This type is the way to ensure some value in your schema is some predefined "constant".
716
804
 
@@ -725,9 +813,11 @@ agreed_with_tos =
725
813
  end
726
814
  ```
727
815
 
728
- #### `transform(name = 'Anonymous') { |value| ... }`
816
+ I18n keys: `error_key`, `'.compare'`, `'datacaster.errors.compare'`. Adds `reference` i18n variable, setting it to `reference_value.to_s`.
817
+
818
+ #### `transform { |value| ... }`
729
819
 
730
- Always returns ValidResult. Transforms the value: returns whatever block returned, automatically wrapping it into `ValidResult`.
820
+ Always returns ValidResult. Transforms the value: returns whatever the block has returned.
731
821
 
732
822
  ```ruby
733
823
  city =
@@ -742,81 +832,34 @@ city =
742
832
  city.(name: "Denver", distance: "2.5") # => Datacaster::ValidResult({:name=>"Denver", :distance=>4.02335})
743
833
  ```
744
834
 
745
- #### `transform_if_present(name = 'Anonymous') { |value| ... }`
746
-
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
835
+ #### `transform_if_present { |value| ... }`
752
836
 
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
- ```
837
+ 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
838
 
792
839
  ### Array schemas
793
840
 
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.
841
+ 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
842
 
796
843
  ```ruby
797
844
  salaries = Datacaster.schema { array_of(integer) }
798
845
 
799
846
  salaries.([1000, 2000, 3000]) # Datacaster::ValidResult([1000, 2000, 3000])
800
847
 
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"])
848
+ salaries.(["one thousand"]) # Datacaster::ErrorResult({0=>["is not an integer"]})
849
+ salaries.(:not_an_array) # Datacaster::ErrorResult(["should be an array"])
850
+ salaries.([]) # Datacaster::ErrorResult(["should not be empty"])
804
851
  ```
805
852
 
806
853
  To allow empty array use the following construct: `compare([]) | array_of(...)`.
807
854
 
808
- If you want to define array of hashes, shortcut definition could be used: instead of `array_of(hash_schema({...}))` use `array_of({...})`:
855
+ 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
856
 
810
857
  ```ruby
811
858
  people =
812
859
  Datacaster.schema do
813
860
  array_of(
814
- # hash_schema(
815
- {
816
- name: string,
817
- salary: float
818
- }
819
- # )
861
+ name: string,
862
+ salary: float
820
863
  )
821
864
  end
822
865
 
@@ -826,25 +869,30 @@ people.([person1, person2]) # => Datacaster::ValidResult([{...}, {...}])
826
869
 
827
870
  people.([{salary: 250_000.0}, {salary: "50000"}])
828
871
  # => Datacaster::ErrorResult({
829
- # 0 => {:name => ["must be string"]},
830
- # 1 => {:name => ["must be string"], :salary => ["must be float"]}
872
+ # 0 => {:name => ["is not a string"]},
873
+ # 1 => {:name => ["is not a string"], :salary => ["is not a float"]}
831
874
  # })
832
875
  ```
833
876
 
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.
877
+ 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
878
 
836
- Formally, `array_of(x)` will return ValidResult if and only if:
879
+ Formally, `array_of(x, error_keys = {})` will return ValidResult if and only if:
837
880
 
838
881
  a) provided value implements basic array methods (`#map`, `#zip`),
839
882
  b) provided value is not `#empty?`,
840
883
  c) each element of the provided value passes validation of `x`.
841
884
 
842
- If a) fails, `ErrorResult(["must be array"])` is returned.
843
- If b) fails, `ErrorResult(["must not be empty"])` is returned.
885
+ If a) fails, `ErrorResult(["should be an array"]) is returned.
886
+ If b) fails, `ErrorResult(["should not be empty"])` is returned.
844
887
  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
888
 
846
889
  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
890
 
891
+ I18n keys:
892
+
893
+ * not an array – `error_keys[:array]`, `'.array'`, `'datacaster.errors.array'`
894
+ * empty array – `error_keys[:empty]`, `'.empty'`, `'datacaster.errors.empty'`
895
+
848
896
  ### Hash schemas
849
897
 
850
898
  Hash schemas are "bread and butter" of Datacaster.
@@ -864,7 +912,7 @@ person.(name: "John Smith", salary: 100_000)
864
912
  # => Datacaster::ValidResult({:name=>"John Smith", :salary=>100000})
865
913
 
866
914
  person.(name: "John Smith", salary: "100_000")
867
- # => Datacaster::ErrorResult({:salary=>["must be integer"]})
915
+ # => Datacaster::ErrorResult({:salary=>["is not an integer"]})
868
916
  ```
869
917
 
870
918
  Formally, hash schema returns ValidResult if and only if:
@@ -873,17 +921,19 @@ a) provided value `is_a?(Hash)`,
873
921
  b) all values, fetched by keys mentioned in `hash_schema(...)` definition, pass corresponding validations,
874
922
  c) after all checks (including logical operators), there are no unchecked keys in the hash.
875
923
 
876
- If a) fails, `ErrorResult(["must be hash"])` is returned.
924
+ If a) fails, `ErrorResult(["is not a hash"])` is returned.
877
925
  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.
926
+ 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
927
 
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`).
928
+ I18n keys:
929
+
930
+ * not a hash – `error_key`, `'.hash_value'`, `'datacaster.errors.hash_value'`
881
931
 
882
932
  #### Absent is not nil
883
933
 
884
934
  In practical tasks it's important to distinguish between absent (i.e. not set or deleted) and `nil` values of a hash.
885
935
 
886
- To check some value for `nil`, use ordinary `compare(nil)` validator, mentioned above.
936
+ To check some value for `nil`, use [`compare(nil)`](#comparereference_value-error_key--nil).
887
937
 
888
938
  To check some value for absence, use `absent` validator:
889
939
 
@@ -900,14 +950,14 @@ restricted_params.(username: "test")
900
950
  # => Datacaster::ValidResult({:username=>"test"})
901
951
 
902
952
  restricted_params.(username: "test", is_admin: true)
903
- # => Datacaster::ErrorResult({:is_admin=>["must be absent"]})
953
+ # => Datacaster::ErrorResult({:is_admin=>["should be absent"]})
904
954
  restricted_params.(username: "test", is_admin: nil)
905
- # => Datacaster::ErrorResult({:is_admin=>["must be absent"]})
955
+ # => Datacaster::ErrorResult({:is_admin=>["should be absent"]})
906
956
  ```
907
957
 
908
958
  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
959
 
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):
960
+ 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
961
 
912
962
  ```ruby
913
963
  person =
@@ -924,10 +974,10 @@ person.(name: "John Smith")
924
974
  # => Datacaster::ValidResult({:name=>"John Smith"})
925
975
 
926
976
  person.(name: "John Smith", dob: "invalid date")
927
- # => Datacaster::ErrorResult({:dob=>["must be iso8601 string"]})
977
+ # => Datacaster::ErrorResult({:dob=>["is not a string with ISO-8601 date and time"]})
928
978
  ```
929
979
 
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):
980
+ 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
981
 
932
982
  ```ruby
933
983
  anonimized_person =
@@ -942,9 +992,9 @@ anonimized_person.(name: "John Johnson", dob: "1990-05-23")
942
992
  # => Datacaster::ValidResult({:dob=>"1990-05-23"})
943
993
  ```
944
994
 
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.
995
+ 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
996
 
947
- #### Schema vs Partial schema
997
+ #### Schema vs Partial schema vs Choosy schema
948
998
 
949
999
  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
1000
 
@@ -1006,7 +1056,7 @@ RecordValidator =
1006
1056
  end
1007
1057
  ```
1008
1058
 
1009
- See "IF... THEN... ELSE" section above in this file for full description of how `a.then(b).else(c)` validator works.
1059
+ See also ["IF... THEN... ELSE"](#if-then-else-operator) section.
1010
1060
 
1011
1061
  Examples of how this validator would work:
1012
1062
 
@@ -1028,15 +1078,17 @@ RecordValidator.(
1028
1078
  description: 'CEO',
1029
1079
  extra: :key
1030
1080
  )
1031
- # => Datacaster::ErrorResult({:extra=>["must be absent"]})
1081
+ # => Datacaster::ErrorResult({:extra=>["should be absent"]})
1032
1082
  ```
1033
1083
 
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).
1084
+ 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
1085
 
1036
1086
  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
1087
 
1038
1088
  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
1089
 
1090
+ Lastly, if you want to just delete extra unvalidated keys without returning a error, use `choosy_schema`.
1091
+
1040
1092
  #### AND with error aggregation (`*`)
1041
1093
 
1042
1094
  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 +1120,10 @@ This code will work as expected (i.e. `RecordValidator`, the "end" validator, wi
1068
1120
 
1069
1121
  ```ruby
1070
1122
  RecordValidator.(kind: 'person', name: 1)
1071
- # => Datacaster::ErrorResult({:description=>["must be string"]})
1123
+ # => Datacaster::ErrorResult({:description=>["is not a string"]})
1072
1124
  ```
1073
1125
 
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.
1126
+ 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
1127
 
1076
1128
  Specifically to resolve this, "AND with error aggregation" (`*`) operator should be used in place of regular AND (`&`):
1077
1129
 
@@ -1082,12 +1134,12 @@ RecordValidator =
1082
1134
  end
1083
1135
 
1084
1136
  RecordValidator.(kind: 'person', name: 1)
1085
- # => Datacaster::ErrorResult({:description=>["must be string"], :name=>["must be string"]})
1137
+ # => Datacaster::ErrorResult({:description=>["is not a string"], :name=>["is not a string"]})
1086
1138
  ```
1087
1139
 
1088
1140
  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
1141
 
1090
- Described in this example is the only case where `*` and `&` differ: in all other aspects they are full equivalents.
1142
+ Described in this example is the only case where `*` and `&` differ: in all other aspects they are fully equivalent.
1091
1143
 
1092
1144
  Formally, "AND with error aggregation" (`*`):
1093
1145
 
@@ -1096,9 +1148,9 @@ b) in all other cases behaves as regular "AND" (`&`).
1096
1148
 
1097
1149
  ### Shortcut nested definitions
1098
1150
 
1099
- Datacaster aimed at ease of use where multi-level embedded structures need to be validated, boilerplate reduced to inevitable minimum.
1151
+ Datacaster aimed at thr ease of use where multi-level embedded structures need to be validated, boilerplate reduced to inevitable minimum.
1100
1152
 
1101
- The words `hash_schema` and `array_schema`/`array_of` could be, therefore, omitted from the definition of nested structures (replaced with `{...}` and `[...]` correspondingly):
1153
+ The words `hash_schema` and `array_schema`/`array_of` could be omitted from the definition of nested structures (replaced with `{...}` and `[...]`):
1102
1154
 
1103
1155
  ```ruby
1104
1156
  # full definition
@@ -1140,11 +1192,11 @@ person =
1140
1192
  end
1141
1193
  ```
1142
1194
 
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.
1195
+ 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
1196
 
1145
1197
  ### Mapping hashes: `transform_to_hash`
1146
1198
 
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).
1199
+ 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
1200
 
1149
1201
  ```ruby
1150
1202
  city_with_distance =
@@ -1160,7 +1212,7 @@ city_with_distance.(distance_in_meters: 1200.0)
1160
1212
  # => Datacaster::ValidResult({:distance_in_km=>1.2, :distance_in_miles=>1.9307999999999998})
1161
1213
  ```
1162
1214
 
1163
- Of course, order of keys in the definition hash doesn't change anything.
1215
+ Of course, order of keys in the definition hash doesn't change the result.
1164
1216
 
1165
1217
  Formally, `transform_to_hash`:
1166
1218
 
@@ -1168,7 +1220,7 @@ a) transforms (any) value to hash;
1168
1220
  b) this hash will contain keys listed in `transform_to_hash` definition;
1169
1221
  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
1222
  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.
1223
+ e) if the initial value happens to also be a hash, all its unvalidated (unused) keys are merged to the resultant hash.
1172
1224
 
1173
1225
  `transform_to_hash` will return ValidResult if and only if all transformations return ValidResults.
1174
1226
 
@@ -1178,15 +1230,56 @@ Here is what is happening when `city_with_distance` (from the example above) is
1178
1230
 
1179
1231
  * Initial hash `{distance_in_meters: 1200}` is passed to `transform_to_hash`
1180
1232
  * `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
1233
+ * 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
1234
  * Similarly, `distance_in_miles` value is built
1183
1235
  * `distance_in_meters` value is created by transforming initial value to `Datacaster.absent` (that is how `remove` works)
1184
1236
 
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.
1237
+ 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
1238
 
1187
- ## Error remapping
1239
+ ## Passing additional context to schemas
1188
1240
 
1189
- In some cases it might be useful to remap resulting `Datacaster::ErrorResult`:
1241
+ 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.
1242
+
1243
+ This can be achived by using `#with_context`, which makes provided context available in the `context` structure:
1244
+
1245
+ ```ruby
1246
+ # class User < ApplicationRecord
1247
+ # ...
1248
+ # end
1249
+ #
1250
+ # class Post < ApplicationRecord
1251
+ # belongs_to :user
1252
+ # ...
1253
+ # end
1254
+
1255
+ schema =
1256
+ Datacaster.schema do
1257
+ hash_schema(
1258
+ post_id: to_integer & check { |id| Post.where(id: id, user_id: context.current_user).exists? }
1259
+ )
1260
+ end
1261
+
1262
+ current_user = ...
1263
+
1264
+ schema.with_context(current_user: current_user).(post_id: 15)
1265
+ ```
1266
+
1267
+ `context` is an [OpenStruct](https://ruby-doc.org/stdlib-3.1.0/libdoc/ostruct/rdoc/OpenStruct.html) instance.
1268
+
1269
+ **Note**
1270
+
1271
+ `context` can be accesed only in casters' blocks. It can't be used in schema definition itself:
1272
+
1273
+ ```ruby
1274
+ # will raise NoMethodError
1275
+ Datacaster.schema { context.error }
1276
+ ```
1277
+
1278
+ ## Error remapping: `cast_errors`
1279
+
1280
+ Validation often includes [remapping](#mapping-hashes-transform_to_hash) of hash keys. In such cases errors require remapping back to the original keys.
1281
+
1282
+ Let's see an example:
1190
1283
 
1191
1284
  ```ruby
1192
1285
  schema =
@@ -1197,11 +1290,11 @@ schema =
1197
1290
  )
1198
1291
  end
1199
1292
 
1200
- schema.(user_id: 'wrong') # => #<Datacaster::ErrorResult({:posts=>["must be integer"]})>
1201
- # Instead of #<Datacaster::ErrorResult({:user_id=>["must be integer"]})>
1293
+ schema.(user_id: 'wrong') # => #<Datacaster::ErrorResult({:posts=>["is not an integer"]})>
1294
+ # Instead of #<Datacaster::ErrorResult({:user_id=>["is not an integer"]})>
1202
1295
  ```
1203
1296
 
1204
- `.cast_errors` can be used in such case:
1297
+ `.cast_errors` can be used to remap errors back:
1205
1298
 
1206
1299
  ```ruby
1207
1300
  schema =
@@ -1219,10 +1312,167 @@ schema =
1219
1312
  )
1220
1313
  end
1221
1314
 
1222
- schema.(user_id: 'wrong') # => #<Datacaster::ErrorResult({:user_id=>["must be integer"]})>
1315
+ schema.(user_id: 'wrong') # => #<Datacaster::ErrorResult({:user_id=>["is not an integer"]})>
1316
+ ```
1317
+
1318
+ `.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.
1319
+
1320
+ Any instance of `Datacaster` supports `#cast_errors`.
1321
+
1322
+ See also [merge_message_keys](#merge_message_keyskeys) caster.
1323
+
1324
+ ## Internationalization (i18n)
1325
+
1326
+ 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).
1327
+
1328
+ There are several ways to customize messages, described in this section.
1329
+
1330
+ ### Custom absolute keys
1331
+
1332
+ There are two ways to set absolute error key (i.e. key with full path to an error inside of a yml i18n file).
1333
+
1334
+ Let's consider the following i18n file:
1335
+
1336
+ ```yml
1337
+ en:
1338
+ user:
1339
+ errors:
1340
+ not_found: User %{value} has not been found
1341
+ ```
1342
+
1343
+ Interpolated i18n variable `value` is added automatically for all built-in casters.
1344
+
1345
+ Firstly, you can set `error_key` of a caster:
1346
+
1347
+ ```ruby
1348
+ schema = Datacaster.schema { check('user.errors.not_found') { false } }
1349
+ schema.('john').errors # ['User john has not been found']
1350
+ ```
1351
+
1352
+ Secondly, you can call `#i18n_key` on a caster:
1353
+
1354
+ ```ruby
1355
+ schema =
1356
+ Datacaster.schema do
1357
+ check { false }.i18n_key('user.errors.not_found')
1358
+ end
1359
+
1360
+ schema.('john').errors # ['User john has not been found']
1361
+ ```
1362
+
1363
+ ### Custom relative keys and scopes
1364
+
1365
+ 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.
1366
+
1367
+ Let's consider the following i18n file:
1368
+
1369
+ ```yml
1370
+ en:
1371
+ user:
1372
+ errors:
1373
+ not_found: User has not been found
1374
+ name:
1375
+ wrong_format: wrong format
1376
+ ```
1377
+
1378
+ 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) starts with `'.'`:
1379
+
1380
+ ```ruby
1381
+ schema =
1382
+ Datacaster.schema(i18n_scope: 'user') do
1383
+ check { |v| v[:id] == 1 }.i18n_key('.errors.not_found') &
1384
+ hash_schema(
1385
+ name: check { false }.i18n_key('.name.wrong_format')
1386
+ )
1387
+ end
1388
+
1389
+ schema.({id: 3}).errors # ['User has not been found']
1390
+ schema.({id: 1, name: 'wrong'}).errors # {name: ['wrong format']}
1391
+ ```
1392
+
1393
+ To reduce the boilerplate, Datacaster will infer scopes from hash key names:
1394
+
1395
+ ```ruby
1396
+ schema =
1397
+ Datacaster.schema(i18n_scope: 'user') do
1398
+ check { |v| v[:id] == 1 }.i18n_key('.errors.not_found') &
1399
+ hash_schema(
1400
+ # '.wrong_format' inferred to be '.name.wrong_format'
1401
+ name: check { false }.i18n_key('.wrong_format')
1402
+ )
1403
+ end
1404
+
1405
+ schema.({id: 1, name: 'wrong'}).errors # {name: ['wrong format']}
1406
+ ```
1407
+
1408
+ Relative keys can be set as `error_key` argument of casters:
1409
+
1410
+ ```ruby
1411
+ schema =
1412
+ Datacaster.schema(i18n_scope: 'user') do
1413
+ check('.errors.not_found') { |v| v[:id] == 1 } &
1414
+ hash_schema(
1415
+ # '.wrong_format' inferred to be '.name.wrong_format'
1416
+ name: check('.wrong_format') { false }
1417
+ )
1418
+ end
1419
+
1420
+ schema.({id: 1, name: 'wrong'}).errors # {name: ['wrong format']}
1421
+ ```
1422
+
1423
+ 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:
1424
+
1425
+ ```yml
1426
+ en:
1427
+ user:
1428
+ check: User has not been found
1429
+ name:
1430
+ check: wrong format
1431
+ ```
1432
+
1433
+ ```ruby
1434
+ schema =
1435
+ # Only root scope is set, no other boilerplate
1436
+ Datacaster.schema(i18n_scope: 'user') do
1437
+ check { |v| v[:id] == 1 } &
1438
+ hash_schema(
1439
+ # '.wrong_format' inferred to be '.name.wrong_format'
1440
+ name: check { false }
1441
+ )
1442
+ end
1443
+
1444
+ schema.({id: 3}).errors # ['User has not been found']
1445
+ schema.({id: 1, name: 'wrong'}).errors # {name: ['wrong format']}
1446
+ ```
1447
+
1448
+ 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.
1449
+
1450
+ Notice that the use of `.i18n_scope` prevents auto-scoping of hash key:
1451
+
1452
+ ```ruby
1453
+ schema =
1454
+ # Only root scope is set, no other boilerplate
1455
+ Datacaster.schema(i18n_scope: 'user') do
1456
+ hash_schema(
1457
+ name: check { false }.i18n_scope('.data')
1458
+ )
1459
+ end
1460
+
1461
+ # will search for the following keys:
1462
+ # - "user.data.check"
1463
+ # - "datacaster.errors.check"
1464
+ schema.(name: 'john').raw_errors
1223
1465
  ```
1224
- any instance of `Datacaster` can be passed to `.cast_errors`
1225
1466
 
1467
+ ### Providing interpolation variables
1468
+
1469
+ Every caster will automatically provide `value` variable for i18n interpolation.
1470
+
1471
+ All keyword arguments of `#i18n_key`, `#i18n_scope` and designed for that sole purpose `#i18n_vars` are provided as interpolation variables on i18n.
1472
+
1473
+ 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')`.
1474
+
1475
+ 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!` overwrites compile-time variables from the next nearest key, scope or vars on collision.
1226
1476
 
1227
1477
  ## Registering custom 'predefined' types
1228
1478