datacaster 3.0.0 → 3.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.md +129 -48
- data/config/locales/en.yml +1 -0
- data/datacaster.gemspec +1 -1
- data/lib/datacaster/base.rb +1 -1
- data/lib/datacaster/comparator.rb +1 -1
- data/lib/datacaster/context_nodes/pass_if.rb +11 -0
- data/lib/datacaster/definition_dsl.rb +0 -1
- data/lib/datacaster/predefined.rb +55 -7
- data/lib/datacaster/result.rb +5 -5
- data/lib/datacaster/switch_node.rb +72 -0
- data/lib/datacaster/then_node.rb +1 -1
- data/lib/datacaster/version.rb +1 -1
- metadata +5 -3
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: d563422d81d874ec9f74b4e95a3d24a7a59cf2c3f50ad709a226346616364882
|
|
4
|
+
data.tar.gz: e33c3dffdbff37e5ce711f56c0196e8955379764c98c2429ec391fd33819797d
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: d01eb486e4d11954f672e6958803b7ea4c60430c58850f2b4c8e8b16a500229c28a91ecadc45317775f7ff8098c4beddfb0373d415f72e5c917ccb1a93f589c8
|
|
7
|
+
data.tar.gz: c9bdb6f156a7de6eb81fa9cb87dcbf050fd47292d417afd4d085cb64a9e5460c4022b6471de723a1e112cd730a5b12e14c595e9ce248e30ff18fd84625a2cc8a
|
data/README.md
CHANGED
|
@@ -15,9 +15,10 @@ It is currently used in production in several projects (mainly as request parame
|
|
|
15
15
|
- [Result value](#result-value)
|
|
16
16
|
- [Hash schema](#hash-schema)
|
|
17
17
|
- [Logical operators](#logical-operators)
|
|
18
|
-
- [*AND operator
|
|
19
|
-
- [*OR operator
|
|
20
|
-
- [*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)
|
|
21
22
|
- [Built-in types](#built-in-types)
|
|
22
23
|
- [Basic types](#basic-types)
|
|
23
24
|
- [`array(error_key = nil)`](#arrayerror_key--nil)
|
|
@@ -31,13 +32,14 @@ It is currently used in production in several projects (mainly as request parame
|
|
|
31
32
|
- [`integer32(error_key = nil)`](#integer32error_key--nil)
|
|
32
33
|
- [`non_empty_string(error_key = nil)`](#non_empty_stringerror_key--nil)
|
|
33
34
|
- [Special types](#special-types)
|
|
34
|
-
- [`absent(error_key = nil)`](#absenterror_key--nil)
|
|
35
|
+
- [`absent(error_key = nil, on: nil)`](#absenterror_key--nil-on-nil)
|
|
35
36
|
- [`any(error_key = nil)`](#anyerror_key--nil)
|
|
36
37
|
- [`default(default_value, on: nil)`](#defaultdefault_value-on-nil)
|
|
37
38
|
- [`merge_message_keys(*keys)`](#merge_message_keyskeys)
|
|
38
39
|
- [`must_be(klass, error_key = nil)`](#must_beklass-error_key--nil)
|
|
39
|
-
- [`optional(base)`](#optionalbase)
|
|
40
|
+
- [`optional(base, on: nil)`](#optionalbase-on-nil)
|
|
40
41
|
- [`pass`](#pass)
|
|
42
|
+
- [`pass_if(base)`](#pass_ifbase)
|
|
41
43
|
- [`pick(key)`](#pickkey)
|
|
42
44
|
- [`remove`](#remove)
|
|
43
45
|
- [`responds_to(method, error_key = nil)`](#responds_tomethod-error_key--nil)
|
|
@@ -266,7 +268,7 @@ And one special: AND with error aggregation (`*`).
|
|
|
266
268
|
|
|
267
269
|
The former 3 is described immediately below, and the latter is described in the section on hash schemas further in this file.
|
|
268
270
|
|
|
269
|
-
#### *AND operator
|
|
271
|
+
#### *AND operator*
|
|
270
272
|
|
|
271
273
|
```ruby
|
|
272
274
|
even_number = Datacaster.schema { integer & check { |x| x.even? } }
|
|
@@ -282,7 +284,7 @@ even_number.("test")
|
|
|
282
284
|
|
|
283
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.
|
|
284
286
|
|
|
285
|
-
#### *OR operator
|
|
287
|
+
#### *OR operator*
|
|
286
288
|
|
|
287
289
|
```ruby
|
|
288
290
|
# 'compare' custom type returns ValidResult if and only if validated value == compare's argument
|
|
@@ -296,12 +298,57 @@ person_or_entity.(:ngo) # => Datacaster::ErrorResult(["does not equal :entity
|
|
|
296
298
|
|
|
297
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.
|
|
298
300
|
|
|
299
|
-
#### *IF... THEN... ELSE operator
|
|
301
|
+
#### *IF... THEN... ELSE operator*
|
|
300
302
|
|
|
301
|
-
Let's
|
|
303
|
+
Let's support we want to run different validations depending on some value, e.g.:
|
|
302
304
|
|
|
303
|
-
|
|
304
|
-
|
|
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).
|
|
305
352
|
|
|
306
353
|
```ruby
|
|
307
354
|
person_or_entity =
|
|
@@ -317,7 +364,25 @@ person_or_entity =
|
|
|
317
364
|
# separate entity validator (excluding validation of 'kind' field)
|
|
318
365
|
entity = hash_schema(title: string, form: string, revenue: integer)
|
|
319
366
|
|
|
320
|
-
|
|
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:)
|
|
321
386
|
end
|
|
322
387
|
|
|
323
388
|
person_or_entity.(
|
|
@@ -341,22 +406,27 @@ person_or_entity.(
|
|
|
341
406
|
# => Datacaster::ErrorResult({:kind=>["is invalid"]})
|
|
342
407
|
```
|
|
343
408
|
|
|
344
|
-
|
|
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.
|
|
345
410
|
|
|
346
|
-
|
|
411
|
+
Notice that shortcut definitions are available (illustrated in the example above) for the switch caster:
|
|
347
412
|
|
|
348
|
-
|
|
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(...)`
|
|
349
416
|
|
|
350
|
-
|
|
417
|
+
`switch()` without a `base` argument will pass the incoming value to the `.on(...)` casters.
|
|
351
418
|
|
|
352
|
-
|
|
353
|
-
* 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)`:
|
|
354
420
|
|
|
355
|
-
`
|
|
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
|
|
356
426
|
|
|
357
|
-
|
|
427
|
+
I18n keys:
|
|
358
428
|
|
|
359
|
-
|
|
429
|
+
* all `.on` checks resulted in an error and there is no `.else`: `'.switch'`, `'datacaster.errors.switch'`
|
|
360
430
|
|
|
361
431
|
## Built-in types
|
|
362
432
|
|
|
@@ -432,9 +502,16 @@ Returns ValidResult if and only if provided value is a string and is not empty.
|
|
|
432
502
|
|
|
433
503
|
### Special types
|
|
434
504
|
|
|
435
|
-
#### `absent(error_key = nil)`
|
|
505
|
+
#### `absent(error_key = nil, on: nil)`
|
|
436
506
|
|
|
437
|
-
Returns ValidResult if and only if provided value is
|
|
507
|
+
Returns ValidResult if and only if provided value is absent. Relevant only for hash schemas (see below). Transforms the value to `Datacaster.absent`.
|
|
508
|
+
|
|
509
|
+
The value is considered absent:
|
|
510
|
+
|
|
511
|
+
* if the value is `Datacaster.absent` (`on` is disregarded in such case)
|
|
512
|
+
* if `on` is set to a method name to which the value responds and yields truthy
|
|
513
|
+
|
|
514
|
+
Set `on` to `:nil?`, `:empty?` or similar method names.
|
|
438
515
|
|
|
439
516
|
I18n keys: `error_key`, `'.absent'`, `'datacaster.errors.absent'`.
|
|
440
517
|
|
|
@@ -450,10 +527,10 @@ Always returns ValidResult.
|
|
|
450
527
|
|
|
451
528
|
Returns `default_value` in the following cases:
|
|
452
529
|
|
|
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
|
|
530
|
+
* if the value is `Datacaster.absent` (`on` is disregarded in such case)
|
|
531
|
+
* if `on` is set to a method name to which the value responds and yields truthy
|
|
455
532
|
|
|
456
|
-
Returns initial value otherwise.
|
|
533
|
+
Returns the initial value otherwise.
|
|
457
534
|
|
|
458
535
|
Set `on` to `:nil?`, `:empty?` or similar method names.
|
|
459
536
|
|
|
@@ -566,18 +643,25 @@ Returns ValidResult if and only if the value `#is_a?(klass)`. Doesn't transform
|
|
|
566
643
|
|
|
567
644
|
I18n keys: `error_key`, `'.must_be'`, `'datacaster.errors.must_be'`. Adds `reference` i18n variable, setting it to `klass.name`.
|
|
568
645
|
|
|
569
|
-
#### `optional(base)`
|
|
646
|
+
#### `optional(base, on: nil)`
|
|
570
647
|
|
|
571
|
-
Returns ValidResult if and only if the value is either
|
|
648
|
+
Returns ValidResult if and only if the value is either absent or passes `base` validation. In the value is absent, transforms it to the `Datacaster.absent`. Otherwise, returns `base` result.
|
|
649
|
+
|
|
650
|
+
Value is considered absent:
|
|
651
|
+
|
|
652
|
+
* if the value is `Datacaster.absent` (`on` is disregarded in such case)
|
|
653
|
+
* if `on` is set to a method name to which the value responds and yields truthy
|
|
654
|
+
|
|
655
|
+
Set `on` to `:nil?`, `:empty?` or similar method names.
|
|
572
656
|
|
|
573
657
|
```ruby
|
|
574
658
|
item_with_optional_price =
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
659
|
+
Datacaster.schema do
|
|
660
|
+
hash_schema(
|
|
661
|
+
name: string,
|
|
662
|
+
price: optional(float)
|
|
663
|
+
)
|
|
664
|
+
end
|
|
581
665
|
|
|
582
666
|
item_with_optional_price.(name: "Book", price: 1.23)
|
|
583
667
|
# => Datacaster::ValidResult({:name=>"Book", :price=>1.23})
|
|
@@ -594,6 +678,12 @@ Always returns ValidResult. Doesn't transform the value.
|
|
|
594
678
|
|
|
595
679
|
Useful to "mark" the value as validated (see section below on hash schemas, where this could be applied).
|
|
596
680
|
|
|
681
|
+
#### `pass_if(base)`
|
|
682
|
+
|
|
683
|
+
Returns ValidResult if and only if base returns ValidResult. Returns base's error result otherwise.
|
|
684
|
+
|
|
685
|
+
Doesn't transform the value: if base succeeds returns the original value (not the one that base returned).
|
|
686
|
+
|
|
597
687
|
#### `pick(key)`
|
|
598
688
|
|
|
599
689
|
Returns ValidResult if and only if the value `#is_a?(Enumerable)`.
|
|
@@ -642,15 +732,7 @@ I18n keys: `error_key`, `'.responds_to'`, `'datacaster.errors.responds_to'`. Add
|
|
|
642
732
|
|
|
643
733
|
#### `transform_to_value(value)`
|
|
644
734
|
|
|
645
|
-
Always returns ValidResult. The value is transformed to provided argument
|
|
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
|
-
```
|
|
735
|
+
Always returns ValidResult. The value is transformed to provided argument (disregarding the original value). See also [`default`](#defaultdefault_value-on-nil).
|
|
654
736
|
|
|
655
737
|
### "Web-form" types
|
|
656
738
|
|
|
@@ -957,7 +1039,7 @@ restricted_params.(username: "test", is_admin: nil)
|
|
|
957
1039
|
|
|
958
1040
|
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.
|
|
959
1041
|
|
|
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):
|
|
1042
|
+
Also, see documentation for [`optional(base)`](#optionalbase-on-nil) 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):
|
|
961
1043
|
|
|
962
1044
|
```ruby
|
|
963
1045
|
person =
|
|
@@ -1375,7 +1457,7 @@ en:
|
|
|
1375
1457
|
wrong_format: wrong format
|
|
1376
1458
|
```
|
|
1377
1459
|
|
|
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)
|
|
1460
|
+
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 `'.'`:
|
|
1379
1461
|
|
|
1380
1462
|
```ruby
|
|
1381
1463
|
schema =
|
|
@@ -1436,7 +1518,6 @@ schema =
|
|
|
1436
1518
|
Datacaster.schema(i18n_scope: 'user') do
|
|
1437
1519
|
check { |v| v[:id] == 1 } &
|
|
1438
1520
|
hash_schema(
|
|
1439
|
-
# '.wrong_format' inferred to be '.name.wrong_format'
|
|
1440
1521
|
name: check { false }
|
|
1441
1522
|
)
|
|
1442
1523
|
end
|
|
@@ -1470,9 +1551,9 @@ Every caster will automatically provide `value` variable for i18n interpolation.
|
|
|
1470
1551
|
|
|
1471
1552
|
All keyword arguments of `#i18n_key`, `#i18n_scope` and designed for that sole purpose `#i18n_vars` are provided as interpolation variables on i18n.
|
|
1472
1553
|
|
|
1473
|
-
It is possible to add i18n variables at the runtime (e.g. inside `check { ... }` block) by calling `i18n_vars!(variable: 'value')`
|
|
1554
|
+
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
1555
|
|
|
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!`
|
|
1556
|
+
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.
|
|
1476
1557
|
|
|
1477
1558
|
## Registering custom 'predefined' types
|
|
1478
1559
|
|
data/config/locales/en.yml
CHANGED
data/datacaster.gemspec
CHANGED
|
@@ -26,7 +26,7 @@ Gem::Specification.new do |spec|
|
|
|
26
26
|
spec.add_development_dependency 'rake', '~> 12.0'
|
|
27
27
|
spec.add_development_dependency 'rspec', '~> 3.0'
|
|
28
28
|
spec.add_development_dependency 'i18n', '~> 1.14'
|
|
29
|
+
spec.add_development_dependency 'dry-monads', '>= 1.3', '< 1.4'
|
|
29
30
|
|
|
30
|
-
spec.add_runtime_dependency 'dry-monads', '>= 1.3', '< 1.4'
|
|
31
31
|
spec.add_runtime_dependency 'zeitwerk', '>= 2', '< 3'
|
|
32
32
|
end
|
data/lib/datacaster/base.rb
CHANGED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
module Datacaster
|
|
2
|
+
module ContextNodes
|
|
3
|
+
class PassIf < Datacaster::ContextNode
|
|
4
|
+
def cast(object, runtime:)
|
|
5
|
+
@runtime = create_runtime(runtime)
|
|
6
|
+
result = @base.with_runtime(@runtime).call(object)
|
|
7
|
+
result.valid? ? Datacaster::ValidResult(object) : result
|
|
8
|
+
end
|
|
9
|
+
end
|
|
10
|
+
end
|
|
11
|
+
end
|
|
@@ -4,7 +4,6 @@ require 'date'
|
|
|
4
4
|
module Datacaster
|
|
5
5
|
class DefinitionDSL
|
|
6
6
|
include Datacaster::Predefined
|
|
7
|
-
include Dry::Monads[:result]
|
|
8
7
|
|
|
9
8
|
# Translates hashes like {a: <IntegerChecker>} to <HashSchema {a: <IntegerChecker>}>
|
|
10
9
|
# and arrays like [<IntegerChecker>] to <ArraySchema <IntegerChecker>>
|
|
@@ -53,12 +53,34 @@ module Datacaster
|
|
|
53
53
|
Validator.new(active_model_validations)
|
|
54
54
|
end
|
|
55
55
|
|
|
56
|
+
def schema(base)
|
|
57
|
+
ContextNodes::StructureCleaner.new(base, strategy: :fail)
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def choosy_schema(base)
|
|
61
|
+
ContextNodes::StructureCleaner.new(base, strategy: :remove)
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def partial_schema(base)
|
|
65
|
+
ContextNodes::StructureCleaner.new(base, strategy: :pass)
|
|
66
|
+
end
|
|
67
|
+
|
|
56
68
|
# 'Meta' types
|
|
57
69
|
|
|
58
|
-
def absent(error_key = nil)
|
|
70
|
+
def absent(error_key = nil, on: nil)
|
|
59
71
|
error_keys = ['.absent', 'datacaster.errors.absent']
|
|
60
72
|
error_keys.unshift(error_key) if error_key
|
|
61
|
-
|
|
73
|
+
|
|
74
|
+
cast do |x|
|
|
75
|
+
if x == Datacaster.absent ||
|
|
76
|
+
(!on.nil? && x.respond_to?(on) && x.public_send(on))
|
|
77
|
+
Datacaster.ValidResult(Datacaster.absent)
|
|
78
|
+
else
|
|
79
|
+
Datacaster.ErrorResult(
|
|
80
|
+
I18nValues::Key.new(error_keys, value: x)
|
|
81
|
+
)
|
|
82
|
+
end
|
|
83
|
+
end
|
|
62
84
|
end
|
|
63
85
|
|
|
64
86
|
def any(error_key = nil)
|
|
@@ -70,7 +92,7 @@ module Datacaster
|
|
|
70
92
|
def default(value, on: nil)
|
|
71
93
|
transform do |x|
|
|
72
94
|
if x == Datacaster.absent ||
|
|
73
|
-
(on && x.respond_to?(on) && x.public_send(on))
|
|
95
|
+
(!on.nil? && x.respond_to?(on) && x.public_send(on))
|
|
74
96
|
value
|
|
75
97
|
else
|
|
76
98
|
x
|
|
@@ -90,7 +112,25 @@ module Datacaster
|
|
|
90
112
|
transform(&:itself)
|
|
91
113
|
end
|
|
92
114
|
|
|
93
|
-
def
|
|
115
|
+
def switch(*base, **on_clauses)
|
|
116
|
+
switch =
|
|
117
|
+
if base.length == 0
|
|
118
|
+
SwitchNode.new
|
|
119
|
+
else
|
|
120
|
+
SwitchNode.new(base)
|
|
121
|
+
end
|
|
122
|
+
on_clauses.reduce(switch) do |result, (k, v)|
|
|
123
|
+
result.on(k, v)
|
|
124
|
+
end
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
def pass_if(base)
|
|
128
|
+
ContextNodes::PassIf.new(base)
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
def pick(*keys, strict: false)
|
|
132
|
+
raise RuntimeError.new("provide keys to pick, e.g. pick(:key)") if keys.empty?
|
|
133
|
+
|
|
94
134
|
must_be(Enumerable) & transform { |value|
|
|
95
135
|
result =
|
|
96
136
|
keys.map do |key|
|
|
@@ -123,8 +163,16 @@ module Datacaster
|
|
|
123
163
|
check { |x| x.is_a?(klass) }.i18n_key(*error_keys, reference: klass.name)
|
|
124
164
|
end
|
|
125
165
|
|
|
126
|
-
def optional(base)
|
|
127
|
-
absent | base
|
|
166
|
+
def optional(base, on: nil)
|
|
167
|
+
return absent | base if on == nil
|
|
168
|
+
cast do |x|
|
|
169
|
+
if x == Datacaster.absent ||
|
|
170
|
+
(!on.nil? && x.respond_to?(on) && x.public_send(on))
|
|
171
|
+
Datacaster.ValidResult(Datacaster.absent)
|
|
172
|
+
else
|
|
173
|
+
base.(x)
|
|
174
|
+
end
|
|
175
|
+
end
|
|
128
176
|
end
|
|
129
177
|
|
|
130
178
|
# Strict types
|
|
@@ -209,7 +257,7 @@ module Datacaster
|
|
|
209
257
|
elsif ['false', '0', false].include?(x)
|
|
210
258
|
Datacaster.ValidResult(false)
|
|
211
259
|
else
|
|
212
|
-
Datacaster.ErrorResult(
|
|
260
|
+
Datacaster.ErrorResult(I18nValues::Key.new(error_keys, value: x))
|
|
213
261
|
end
|
|
214
262
|
end
|
|
215
263
|
end
|
data/lib/datacaster/result.rb
CHANGED
|
@@ -1,9 +1,5 @@
|
|
|
1
|
-
require 'dry/monads'
|
|
2
|
-
|
|
3
1
|
module Datacaster
|
|
4
2
|
class Result
|
|
5
|
-
include Dry::Monads[:result]
|
|
6
|
-
|
|
7
3
|
def initialize(valid, value_or_errors)
|
|
8
4
|
@value_or_errors = value_or_errors
|
|
9
5
|
if !valid && !@value_or_errors.is_a?(Hash) && !@value_or_errors.is_a?(Array)
|
|
@@ -43,7 +39,11 @@ module Datacaster
|
|
|
43
39
|
end
|
|
44
40
|
|
|
45
41
|
def to_dry_result
|
|
46
|
-
@valid
|
|
42
|
+
if @valid
|
|
43
|
+
Dry::Monads::Result::Success.new(@value_or_errors)
|
|
44
|
+
else
|
|
45
|
+
Dry::Monads::Result::Failure.new(errors)
|
|
46
|
+
end
|
|
47
47
|
end
|
|
48
48
|
|
|
49
49
|
private
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
module Datacaster
|
|
2
|
+
class SwitchNode < Base
|
|
3
|
+
def initialize(base = nil, on_casters = [], else_caster = nil)
|
|
4
|
+
base = base[0] if base.is_a?(Array) && base.length == 1
|
|
5
|
+
|
|
6
|
+
case base
|
|
7
|
+
when nil
|
|
8
|
+
@base = nil
|
|
9
|
+
when Datacaster::Base
|
|
10
|
+
@base = base
|
|
11
|
+
when String, Symbol, Array
|
|
12
|
+
@base = Datacaster::Predefined.pick(base)
|
|
13
|
+
else
|
|
14
|
+
raise RuntimeError, "provide a Datacaster::Base instance, a hash key, or an array of keys to switch(...) caster", caller
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
@ons = on_casters
|
|
18
|
+
@else = else_caster
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def on(caster_or_value, clause)
|
|
22
|
+
caster =
|
|
23
|
+
case caster_or_value
|
|
24
|
+
when Datacaster::Base
|
|
25
|
+
caster_or_value
|
|
26
|
+
else
|
|
27
|
+
Datacaster::Predefined.compare(caster_or_value)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
clause = DefinitionDSL.expand(clause)
|
|
31
|
+
|
|
32
|
+
self.class.new(@base, @ons + [[caster, clause]], @else)
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def else(else_caster)
|
|
36
|
+
raise ArgumentError, "Datacaster: double else clause is not permitted", caller if @else
|
|
37
|
+
self.class.new(@base, @ons, else_caster)
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def cast(object, runtime:)
|
|
41
|
+
if @ons.empty?
|
|
42
|
+
raise RuntimeError, "switch caster requires at least one 'on' statement: switch(...).on(condition, cast)", caller
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
if @base.nil?
|
|
46
|
+
switch_result = object
|
|
47
|
+
else
|
|
48
|
+
switch_result = @base.with_runtime(runtime).(object)
|
|
49
|
+
return switch_result unless switch_result.valid?
|
|
50
|
+
switch_result = switch_result.value
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
@ons.each do |check, clause|
|
|
54
|
+
result = check.with_runtime(runtime).(switch_result)
|
|
55
|
+
next unless result.valid?
|
|
56
|
+
|
|
57
|
+
return clause.with_runtime(runtime).(object)
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# all 'on'-s have failed
|
|
61
|
+
return @else.with_runtime(runtime).(object) if @else
|
|
62
|
+
|
|
63
|
+
Datacaster.ErrorResult(
|
|
64
|
+
I18nValues::Key.new(['.switch', 'datacaster.errors.switch'], value: object)
|
|
65
|
+
)
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def inspect
|
|
69
|
+
"#<Datacaster::SwitchNode base: #{@base.inspect} on: #{@ons.inspect} else: #{@else.inspect}>"
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
end
|
data/lib/datacaster/then_node.rb
CHANGED
|
@@ -9,7 +9,7 @@ module Datacaster
|
|
|
9
9
|
def else(else_caster)
|
|
10
10
|
raise ArgumentError.new("Datacaster: double else clause is not permitted") if @else
|
|
11
11
|
|
|
12
|
-
self.class.new(@left, @then, else_caster)
|
|
12
|
+
self.class.new(@left, @then, DefinitionDSL.expand(else_caster))
|
|
13
13
|
end
|
|
14
14
|
|
|
15
15
|
def cast(object, runtime:)
|
data/lib/datacaster/version.rb
CHANGED
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: datacaster
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 3.
|
|
4
|
+
version: 3.1.1
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Eugene Zolotarev
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: exe
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2023-
|
|
11
|
+
date: 2023-10-02 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: activemodel
|
|
@@ -76,7 +76,7 @@ dependencies:
|
|
|
76
76
|
- - "<"
|
|
77
77
|
- !ruby/object:Gem::Version
|
|
78
78
|
version: '1.4'
|
|
79
|
-
type: :
|
|
79
|
+
type: :development
|
|
80
80
|
prerelease: false
|
|
81
81
|
version_requirements: !ruby/object:Gem::Requirement
|
|
82
82
|
requirements:
|
|
@@ -140,6 +140,7 @@ files:
|
|
|
140
140
|
- lib/datacaster/context_nodes/errors_caster.rb
|
|
141
141
|
- lib/datacaster/context_nodes/i18n.rb
|
|
142
142
|
- lib/datacaster/context_nodes/i18n_keys_mapper.rb
|
|
143
|
+
- lib/datacaster/context_nodes/pass_if.rb
|
|
143
144
|
- lib/datacaster/context_nodes/structure_cleaner.rb
|
|
144
145
|
- lib/datacaster/context_nodes/user_context.rb
|
|
145
146
|
- lib/datacaster/definition_dsl.rb
|
|
@@ -157,6 +158,7 @@ files:
|
|
|
157
158
|
- lib/datacaster/runtimes/structure_cleaner.rb
|
|
158
159
|
- lib/datacaster/runtimes/user_context.rb
|
|
159
160
|
- lib/datacaster/substitute_i18n.rb
|
|
161
|
+
- lib/datacaster/switch_node.rb
|
|
160
162
|
- lib/datacaster/then_node.rb
|
|
161
163
|
- lib/datacaster/transformer.rb
|
|
162
164
|
- lib/datacaster/trier.rb
|