datacaster 2.0.2 → 3.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.md +604 -287
- data/config/locales/en.yml +25 -0
- data/datacaster.gemspec +3 -1
- data/lib/datacaster/absent.rb +4 -0
- data/lib/datacaster/and_node.rb +3 -5
- data/lib/datacaster/and_with_error_aggregation_node.rb +5 -6
- data/lib/datacaster/array_schema.rb +18 -16
- data/lib/datacaster/base.rb +33 -44
- data/lib/datacaster/caster.rb +4 -8
- data/lib/datacaster/checker.rb +8 -10
- data/lib/datacaster/comparator.rb +9 -9
- data/lib/datacaster/config.rb +28 -0
- data/lib/datacaster/context_node.rb +43 -0
- data/lib/datacaster/context_nodes/errors_caster.rb +21 -0
- data/lib/datacaster/context_nodes/i18n.rb +20 -0
- data/lib/datacaster/context_nodes/i18n_keys_mapper.rb +27 -0
- data/lib/datacaster/context_nodes/pass_if.rb +11 -0
- data/lib/datacaster/context_nodes/structure_cleaner.rb +103 -0
- data/lib/datacaster/context_nodes/user_context.rb +20 -0
- data/lib/datacaster/definition_dsl.rb +36 -0
- data/lib/datacaster/hash_mapper.rb +13 -16
- data/lib/datacaster/hash_schema.rb +14 -15
- data/lib/datacaster/i18n_values/base.rb +87 -0
- data/lib/datacaster/i18n_values/key.rb +34 -0
- data/lib/datacaster/i18n_values/scope.rb +28 -0
- data/lib/datacaster/message_keys_merger.rb +8 -15
- data/lib/datacaster/or_node.rb +3 -4
- data/lib/datacaster/predefined.rb +150 -65
- data/lib/datacaster/result.rb +39 -18
- data/lib/datacaster/runtimes/base.rb +47 -0
- data/lib/datacaster/runtimes/i18n.rb +20 -0
- data/lib/datacaster/runtimes/structure_cleaner.rb +47 -0
- data/lib/datacaster/runtimes/user_context.rb +39 -0
- data/lib/datacaster/substitute_i18n.rb +48 -0
- data/lib/datacaster/switch_node.rb +72 -0
- data/lib/datacaster/then_node.rb +7 -8
- data/lib/datacaster/transformer.rb +4 -8
- data/lib/datacaster/trier.rb +9 -11
- data/lib/datacaster/validator.rb +8 -9
- data/lib/datacaster/version.rb +1 -1
- data/lib/datacaster.rb +15 -35
- metadata +60 -10
- data/lib/datacaster/definition_context.rb +0 -20
- 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
|
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
|
-
|
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
|
18
|
-
- [*OR operator
|
19
|
-
- [*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
|
-
- [`
|
23
|
-
- [`
|
24
|
-
- [`float`](#
|
25
|
-
- [`
|
26
|
-
- [`
|
27
|
-
- [`
|
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
|
-
- [`
|
30
|
-
- [`
|
31
|
-
- [`
|
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`](#
|
34
|
-
- [`any`](#
|
35
|
-
- [`
|
36
|
-
- [`
|
37
|
-
- [`
|
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
|
-
- [`
|
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
|
-
- [`
|
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
|
51
|
-
- [`check(
|
52
|
-
- [`try(
|
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,
|
55
|
-
- [`transform
|
56
|
-
- [`transform_if_present
|
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
|
-
- [
|
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(["
|
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 # ["
|
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 { ... }
|
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(["
|
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(["
|
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
|
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=>["
|
210
|
+
# => Datacaster::ErrorResult({:salary=>["is not an integer"]})
|
188
211
|
|
189
212
|
person_validator.("test")
|
190
|
-
# => Datacaster::ErrorResult(["
|
213
|
+
# => Datacaster::ErrorResult(["is not a hash"])
|
191
214
|
|
192
215
|
person_validator.(name: "John Smith", salary: "1000")
|
193
|
-
# => Datacaster::ErrorResult({:salary=>["
|
216
|
+
# => Datacaster::ErrorResult({:salary=>["is not an integer"]})
|
194
217
|
|
195
218
|
person_validator.(name: :john, salary: "1000")
|
196
|
-
# => Datacaster::ErrorResult({:name=>["
|
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=>["
|
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.
|
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(["
|
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(["
|
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
|
303
|
+
Let's support we want to run different validations depending on some value, e.g.:
|
281
304
|
|
282
|
-
|
283
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
411
|
+
Notice that shortcut definitions are available (illustrated in the example above) for the switch caster:
|
326
412
|
|
327
|
-
|
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
|
-
|
417
|
+
`switch()` without a `base` argument will pass the incoming value to the `.on(...)` casters.
|
330
418
|
|
331
|
-
|
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
|
-
`
|
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
|
-
|
427
|
+
I18n keys:
|
337
428
|
|
338
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
443
|
+
I18n keys: `error_key`, `'.array'`, `'datacaster.errors.array'`.
|
369
444
|
|
370
|
-
|
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
|
-
|
447
|
+
Returns ValidResult if and only if provided value is either a float, integer or string representing float/integer.
|
375
448
|
|
376
|
-
|
449
|
+
Transforms the value to the `BigDecimal` instance.
|
377
450
|
|
378
|
-
|
451
|
+
I18n keys: `error_key`, `'.decimal'`, `'datacaster.errors.decimal'`.
|
379
452
|
|
380
|
-
#### `
|
453
|
+
#### `float(error_key = nil)`
|
381
454
|
|
382
|
-
Returns ValidResult if and only if provided value is
|
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
|
-
|
457
|
+
I18n keys: `error_key`, `'.float'`, `'datacaster.errors.float'`.
|
385
458
|
|
386
|
-
|
459
|
+
#### `hash_value(error_key = nil)`
|
387
460
|
|
388
|
-
|
461
|
+
Returns ValidResult if and only if provided value is a `Hash`. Doesn't transform the value.
|
389
462
|
|
390
|
-
|
463
|
+
Note: this type is called `hash_value` instead of `hash`, because `hash` is a reserved method name in Ruby.
|
391
464
|
|
392
|
-
|
465
|
+
I18n keys: `error_key`, `'.hash_value'`, `'datacaster.errors.hash_value'`.
|
393
466
|
|
394
|
-
#### `
|
467
|
+
#### `integer(error_key = nil)`
|
395
468
|
|
396
|
-
Returns ValidResult if and only if provided value is
|
469
|
+
Returns ValidResult if and only if provided value is an integer. Doesn't transform the value.
|
397
470
|
|
398
|
-
|
471
|
+
I18n keys: `error_key`, `'.integer'`, `'datacaster.errors.integer'`.
|
399
472
|
|
400
|
-
|
473
|
+
#### `string(error_key = nil)`
|
401
474
|
|
402
|
-
|
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
|
-
|
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
|
-
|
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
|
-
#### `
|
481
|
+
#### `hash_with_symbolized_keys(error_key = nil)`
|
415
482
|
|
416
|
-
|
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
|
-
|
485
|
+
I18n keys: `error_key`, `'.hash_value'`, `'datacaster.errors.hash_value'`.
|
419
486
|
|
420
|
-
|
487
|
+
#### `integer32(error_key = nil)`
|
421
488
|
|
422
|
-
|
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
|
-
|
491
|
+
I18n keys:
|
425
492
|
|
426
|
-
|
493
|
+
* not an integer – `error_key`, `'.integer'`, `'datacaster.errors.integer'`
|
494
|
+
* too big – `error_key`, `'.integer32'`, `'datacaster.errors.integer32'`
|
427
495
|
|
428
|
-
|
496
|
+
#### `non_empty_string(error_key = nil)`
|
429
497
|
|
430
|
-
|
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
|
-
|
440
|
-
|
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
|
-
|
445
|
-
# => Datacaster::ErrorResult({:price=>["must be float"]})
|
446
|
-
```
|
503
|
+
### Special types
|
447
504
|
|
448
|
-
#### `
|
505
|
+
#### `absent(error_key = nil)`
|
449
506
|
|
450
|
-
Returns ValidResult if and only if value
|
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
|
-
|
509
|
+
I18n keys: `error_key`, `'.absent'`, `'datacaster.errors.absent'`.
|
453
510
|
|
454
|
-
|
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
|
-
|
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
|
-
|
462
|
-
pick_name.(last_name: "Johnson") # => Datacaster::ValidResult(#<Datacaster.absent>)
|
515
|
+
I18n keys: `error_key`, `'.any'`, `'datacaster.errors.any'`
|
463
516
|
|
464
|
-
|
465
|
-
```
|
517
|
+
#### `default(default_value, on: nil)`
|
466
518
|
|
467
|
-
|
519
|
+
Always returns ValidResult.
|
468
520
|
|
469
|
-
|
521
|
+
Returns `default_value` in the following cases:
|
470
522
|
|
471
|
-
|
472
|
-
|
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
|
-
|
475
|
-
pick_name_and_age.(last_name: "Johnson", age: 20) # => Datacaster::ValidResult([#<Datacaster.absent>, 20])
|
526
|
+
Returns the initial value otherwise.
|
476
527
|
|
477
|
-
|
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
|
-
|
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
|
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
|
-
|
625
|
+
See also `#cast_errors` for [error remapping](#error-remapping-cast_errors).
|
576
626
|
|
577
|
-
|
627
|
+
See also `#pick` for [simpler picking of hash values](#pickkey).
|
578
628
|
|
579
|
-
|
629
|
+
I18n keys:
|
580
630
|
|
581
|
-
|
631
|
+
* not a hash – `'.hash_value'`, `'datacaster.errors.hash_value'`
|
582
632
|
|
583
|
-
#### `
|
633
|
+
#### `must_be(klass, error_key = nil)`
|
584
634
|
|
585
|
-
Returns ValidResult if and only if value
|
635
|
+
Returns ValidResult if and only if the value `#is_a?(klass)`. Doesn't transform the value.
|
586
636
|
|
587
|
-
|
637
|
+
I18n keys: `error_key`, `'.must_be'`, `'datacaster.errors.must_be'`. Adds `reference` i18n variable, setting it to `klass.name`.
|
588
638
|
|
589
|
-
|
639
|
+
#### `optional(base)`
|
590
640
|
|
591
|
-
|
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
|
-
|
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
|
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
|
-
|
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
|
-
|
754
|
+
I18n keys: `error_key`, `'.to_boolean'`, `'datacaster.errors.to_boolean'`
|
615
755
|
|
616
|
-
|
756
|
+
#### `to_float(error_key = nil)`
|
617
757
|
|
618
|
-
|
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
|
-
|
770
|
+
These types are used to create 'hand-crafted' validators.
|
771
|
+
|
772
|
+
#### `cast { |value| ... }`
|
621
773
|
|
622
774
|
The most basic — "fully manual" — 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
|
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
|
781
|
+
# Actually, it's better to use 'check' here instead
|
630
782
|
user_id_exists =
|
631
783
|
Datacaster.schema do
|
632
|
-
cast
|
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).
|
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(
|
801
|
+
#### `check(error_key = nil) { |value| ... }`
|
650
802
|
|
651
|
-
Returns ValidResult if and only if provided block returns truthy value
|
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
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
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
|
903
|
+
#### `transform_if_present { |value| ... }`
|
746
904
|
|
747
|
-
Always returns ValidResult. If the value is `Datacaster.absent
|
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
|
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=>["
|
802
|
-
salaries.(:not_an_array) # Datacaster::ErrorResult(["
|
803
|
-
salaries.([]) # Datacaster::ErrorResult(["
|
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
|
-
|
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 => ["
|
830
|
-
# 1 => {:name => ["
|
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
|
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(["
|
843
|
-
If b) fails, `ErrorResult(["
|
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=>["
|
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(["
|
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 => ["
|
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
|
-
|
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
|
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=>["
|
1021
|
+
# => Datacaster::ErrorResult({:is_admin=>["should be absent"]})
|
904
1022
|
restricted_params.(username: "test", is_admin: nil)
|
905
|
-
# => Datacaster::ErrorResult({:is_admin=>["
|
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)`
|
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=>["
|
1045
|
+
# => Datacaster::ErrorResult({:dob=>["is not a string with ISO-8601 date and time"]})
|
928
1046
|
```
|
929
1047
|
|
930
|
-
Another use
|
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
|
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"
|
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=>["
|
1149
|
+
# => Datacaster::ErrorResult({:extra=>["should be absent"]})
|
1032
1150
|
```
|
1033
1151
|
|
1034
|
-
|
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=>["
|
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).
|
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=>["
|
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
|
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
|
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 `
|
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
|
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
|
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
|
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
|
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
|
-
##
|
1307
|
+
## Passing additional context to schemas
|
1188
1308
|
|
1189
|
-
|
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=>["
|
1201
|
-
# Instead of #<Datacaster::ErrorResult({:user_id=>["
|
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
|
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=>["
|
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
|
|