datacaster 3.1.1 → 3.1.3
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 +90 -15
- data/config/locales/en.yml +2 -0
- data/lib/datacaster/and_with_error_aggregation_node.rb +1 -1
- data/lib/datacaster/base.rb +1 -93
- data/lib/datacaster/caster.rb +4 -4
- data/lib/datacaster/hash_mapper.rb +2 -2
- data/lib/datacaster/mixin.rb +68 -0
- data/lib/datacaster/predefined.rb +126 -42
- data/lib/datacaster/switch_node.rb +7 -9
- data/lib/datacaster/utils.rb +40 -0
- data/lib/datacaster/version.rb +1 -1
- data/lib/datacaster.rb +5 -1
- metadata +4 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: d7cd7191206c373ab38c5516505bf38cbbdc76a7b7c433f976498accda34b054
|
|
4
|
+
data.tar.gz: 0443edf8c8f494caeacfb55ba902cce19210a53cf9f726e8306f7e487de58fd8
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 2f00d6cd8c2aae72cba3723dbea6b1c6cb9d760309e9fefa7897e5da691123459496348045849f47700f241df668e505120ba1d2e2f54c93a033e72b222dc3b9
|
|
7
|
+
data.tar.gz: 15c5d76c7cc7a16be05890bec3bcf549c0a5d103e9ba8bb42e2743c834e874bc1296fa5daa3af4feff93107d5acd8db2bd3b2da103f1c29aca1a169b86883c24
|
data/README.md
CHANGED
|
@@ -34,13 +34,14 @@ It is currently used in production in several projects (mainly as request parame
|
|
|
34
34
|
- [Special types](#special-types)
|
|
35
35
|
- [`absent(error_key = nil, on: nil)`](#absenterror_key--nil-on-nil)
|
|
36
36
|
- [`any(error_key = nil)`](#anyerror_key--nil)
|
|
37
|
+
- [`attribute(*keys)`](#attributekeys)
|
|
37
38
|
- [`default(default_value, on: nil)`](#defaultdefault_value-on-nil)
|
|
38
39
|
- [`merge_message_keys(*keys)`](#merge_message_keyskeys)
|
|
39
40
|
- [`must_be(klass, error_key = nil)`](#must_beklass-error_key--nil)
|
|
40
41
|
- [`optional(base, on: nil)`](#optionalbase-on-nil)
|
|
41
42
|
- [`pass`](#pass)
|
|
42
43
|
- [`pass_if(base)`](#pass_ifbase)
|
|
43
|
-
- [`pick(
|
|
44
|
+
- [`pick(*keys)`](#pickkeys)
|
|
44
45
|
- [`remove`](#remove)
|
|
45
46
|
- [`responds_to(method, error_key = nil)`](#responds_tomethod-error_key--nil)
|
|
46
47
|
- [`transform_to_value(value)`](#transform_to_valuevalue)
|
|
@@ -56,6 +57,8 @@ It is currently used in production in several projects (mainly as request parame
|
|
|
56
57
|
- [`try(error_key = nil, catched_exception:) { |value| ... }`](#tryerror_key--nil-catched_exception--value--)
|
|
57
58
|
- [`validate(active_model_validations, name = 'Anonymous')`](#validateactive_model_validations-name--anonymous)
|
|
58
59
|
- [`compare(reference_value, error_key = nil)`](#comparereference_value-error_key--nil)
|
|
60
|
+
- [`included_in(*reference_values, error_key: nil)`](#included_inreference_values-error_key-nil)
|
|
61
|
+
- [`relate(left, op, right, error_key: nil)`](#relateleft-op-right-error_key-nil)
|
|
59
62
|
- [`transform { |value| ... }`](#transform--value--)
|
|
60
63
|
- [`transform_if_present { |value| ... }`](#transform_if_present--value--)
|
|
61
64
|
- [Array schemas](#array-schemas)
|
|
@@ -175,10 +178,10 @@ array = Datacaster.schema { array }
|
|
|
175
178
|
array.(nil)
|
|
176
179
|
|
|
177
180
|
# In this README
|
|
178
|
-
|
|
181
|
+
# => Datacaster::ErrorResult(['should be an array'])
|
|
179
182
|
|
|
180
183
|
# In reality
|
|
181
|
-
|
|
184
|
+
# => <Datacaster::ErrorResult([#<Datacaster::I18nValues::Key(.array, datacaster.errors.array) {:value=>nil}>])>
|
|
182
185
|
```
|
|
183
186
|
|
|
184
187
|
See [section on i18n](#internationalization-i18n) for details.
|
|
@@ -521,6 +524,26 @@ Returns ValidResult if and only if provided value is not `Datacaster.absent` (th
|
|
|
521
524
|
|
|
522
525
|
I18n keys: `error_key`, `'.any'`, `'datacaster.errors.any'`
|
|
523
526
|
|
|
527
|
+
#### `attribute(*keys)`
|
|
528
|
+
|
|
529
|
+
Always returns ValidResult. Calls provided method(s) (recursively) on the value and returns their results. `*keys` should be specified in exactly the same manner as in [pick](#pickkeys).
|
|
530
|
+
|
|
531
|
+
```ruby
|
|
532
|
+
class User
|
|
533
|
+
def login
|
|
534
|
+
"Alex"
|
|
535
|
+
end
|
|
536
|
+
end
|
|
537
|
+
|
|
538
|
+
login = Datacaster.schema { attribute(:login) }
|
|
539
|
+
|
|
540
|
+
# => Datacaster::ValidResult("Alex")
|
|
541
|
+
login.(User.new)
|
|
542
|
+
|
|
543
|
+
# => Datacaster::ValidResult(#<Datacaster.absent>)
|
|
544
|
+
login.("test")
|
|
545
|
+
```
|
|
546
|
+
|
|
524
547
|
#### `default(default_value, on: nil)`
|
|
525
548
|
|
|
526
549
|
Always returns ValidResult.
|
|
@@ -631,7 +654,7 @@ mapping.(
|
|
|
631
654
|
|
|
632
655
|
See also `#cast_errors` for [error remapping](#error-remapping-cast_errors).
|
|
633
656
|
|
|
634
|
-
See also `#pick` for [simpler picking of hash values](#
|
|
657
|
+
See also `#pick` for [simpler picking of hash values](#pickkeys).
|
|
635
658
|
|
|
636
659
|
I18n keys:
|
|
637
660
|
|
|
@@ -684,15 +707,18 @@ Returns ValidResult if and only if base returns ValidResult. Returns base's erro
|
|
|
684
707
|
|
|
685
708
|
Doesn't transform the value: if base succeeds returns the original value (not the one that base returned).
|
|
686
709
|
|
|
687
|
-
#### `pick(
|
|
710
|
+
#### `pick(*keys)`
|
|
688
711
|
|
|
689
712
|
Returns ValidResult if and only if the value `#is_a?(Enumerable)`.
|
|
690
713
|
|
|
691
|
-
|
|
714
|
+
Each argument should be a string, a Symbol, an integer or an array thereof. Each argument plays the role of key(s) to fetch from the value:
|
|
692
715
|
|
|
693
|
-
*
|
|
694
|
-
*
|
|
695
|
-
|
|
716
|
+
* If the argument is an Array, value is extracted recursively
|
|
717
|
+
* Otherwise, `value[argument]` is fetched and added to the result (or `Datacaster.absent` if is is impossible to fetch)
|
|
718
|
+
|
|
719
|
+
If only one argument is provided to the `pick`, one fetched value is returned. If several arguments are provided, array is returned wherein each value corresponds to each argument.
|
|
720
|
+
|
|
721
|
+
Fetching single key:
|
|
696
722
|
|
|
697
723
|
```ruby
|
|
698
724
|
pick_name = Datacaster.schema { pick(:name) }
|
|
@@ -703,9 +729,7 @@ pick_name.(last_name: "Johnson") # => Datacaster::ValidResult(#<Datacaster.absen
|
|
|
703
729
|
pick_name.("test") # => Datacaster::ErrorResult(["is not Enumerable"])
|
|
704
730
|
```
|
|
705
731
|
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
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.
|
|
732
|
+
Fetching multiple keys:
|
|
709
733
|
|
|
710
734
|
```ruby
|
|
711
735
|
pick_name_and_age = Datacaster.schema { pick(:name, :age) }
|
|
@@ -716,6 +740,15 @@ pick_name_and_age.(last_name: "Johnson", age: 20) # => Datacaster::ValidResult([
|
|
|
716
740
|
pick_name_and_age.("test") # => Datacaster::ErrorResult(["is not Enumerable"])
|
|
717
741
|
```
|
|
718
742
|
|
|
743
|
+
Fetching deeply nested key:
|
|
744
|
+
|
|
745
|
+
```ruby
|
|
746
|
+
nested_hash_picker = Datacaster.schema { pick([:user, :age]) }
|
|
747
|
+
|
|
748
|
+
nested_hash_picker.(user: { age: 21 }) # => Datacaster::ValidResult(21)
|
|
749
|
+
nested_hash_picker.(user: { name: "Alex" }) # => Datacaster::ValidResult(#<Datacaster.absent>)
|
|
750
|
+
```
|
|
751
|
+
|
|
719
752
|
I18n keys:
|
|
720
753
|
|
|
721
754
|
* not a Enumerable – `'.must_be'`, `'datacaster.errors.must_be'`.
|
|
@@ -882,8 +915,6 @@ I18n is performed by ActiveModel gem.
|
|
|
882
915
|
|
|
883
916
|
#### `compare(reference_value, error_key = nil)`
|
|
884
917
|
|
|
885
|
-
This type is the way to ensure some value in your schema is some predefined "constant".
|
|
886
|
-
|
|
887
918
|
Returns ValidResult if and only if `reference_value` equals value.
|
|
888
919
|
|
|
889
920
|
```ruby
|
|
@@ -897,6 +928,50 @@ agreed_with_tos =
|
|
|
897
928
|
|
|
898
929
|
I18n keys: `error_key`, `'.compare'`, `'datacaster.errors.compare'`. Adds `reference` i18n variable, setting it to `reference_value.to_s`.
|
|
899
930
|
|
|
931
|
+
#### `included_in(*reference_values, error_key: nil)`
|
|
932
|
+
|
|
933
|
+
Returns ValidResult if and only if `reference_values.include?` the value.
|
|
934
|
+
|
|
935
|
+
I18n keys: `error_key`, `'.included_in'`, `'datacaster.errors.included_in'`. Adds `reference` i18n variable, setting it to `reference_values.map(&:to_s).join(', ')`.
|
|
936
|
+
|
|
937
|
+
#### `relate(left, op, right, error_key: nil)`
|
|
938
|
+
|
|
939
|
+
Returns ValidResult if and only if `left`, `right` and `op` returns valid result. Doesn't transform the value.
|
|
940
|
+
|
|
941
|
+
Use `relate` to check relations between object keys:
|
|
942
|
+
|
|
943
|
+
```ruby
|
|
944
|
+
ordered =
|
|
945
|
+
# Check that hash[:a] < hash[:b]
|
|
946
|
+
Datacaster.schema do
|
|
947
|
+
transform_to_hash(
|
|
948
|
+
a: relate(:a, :<, :b) & pick(:a),
|
|
949
|
+
b: pick(:b)
|
|
950
|
+
)
|
|
951
|
+
end
|
|
952
|
+
|
|
953
|
+
ordered.(a: 1, b: 2)
|
|
954
|
+
# => Datacaster::ValidResult({:a=>1, :b=>2})
|
|
955
|
+
|
|
956
|
+
ordered.(a: 2, b: 1)
|
|
957
|
+
# => Datacaster::ErrorResult({:a=>["a should be < b"]})
|
|
958
|
+
|
|
959
|
+
ordered.({})
|
|
960
|
+
# => Datacaster::ErrorResult({:a=>["a should be < b"]})
|
|
961
|
+
```
|
|
962
|
+
|
|
963
|
+
Notice that shortcut definitions are available (illustrated in the example above) for the `relate` caster:
|
|
964
|
+
|
|
965
|
+
* `:key` provided as 'left' or 'right' argument is exactly the same as `pick(:key)` (works for a string, a symbol or an integer)
|
|
966
|
+
* `:method` provided as 'op' argument is exactly the same as `check { |(l, r)| l.respond_to?(method) && l.public_send(method, r) }` (works for a string or a symbol)
|
|
967
|
+
|
|
968
|
+
Formally, `relate(left, op, right, error_key: error_key)` will:
|
|
969
|
+
|
|
970
|
+
* call the `left` caster with the original value, return the result unless it's valid
|
|
971
|
+
* call the `right` caster with the original value, return the result unless it's valid
|
|
972
|
+
* call the `op` caster with the `[left_result, right_result]`, return the result unless it's valid
|
|
973
|
+
* return the original value as valid result
|
|
974
|
+
|
|
900
975
|
#### `transform { |value| ... }`
|
|
901
976
|
|
|
902
977
|
Always returns ValidResult. Transforms the value: returns whatever the block has returned.
|
|
@@ -1278,7 +1353,7 @@ Note: in the "root" scope (immediately inside of `schema { ... }` block) the wor
|
|
|
1278
1353
|
|
|
1279
1354
|
### Mapping hashes: `transform_to_hash`
|
|
1280
1355
|
|
|
1281
|
-
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`](#
|
|
1356
|
+
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`](#pickkeys) and [`remove`](#remove)).
|
|
1282
1357
|
|
|
1283
1358
|
```ruby
|
|
1284
1359
|
city_with_distance =
|
data/config/locales/en.yml
CHANGED
|
@@ -11,10 +11,12 @@ en:
|
|
|
11
11
|
empty: should not be empty
|
|
12
12
|
float: is not a float
|
|
13
13
|
hash_value: is not a hash
|
|
14
|
+
included_in: is not one of %{reference}
|
|
14
15
|
integer: is not an integer
|
|
15
16
|
integer32: is not a 32-bit integer
|
|
16
17
|
iso8601: is not a string with ISO-8601 date and time
|
|
17
18
|
must_be: "is not %{reference}"
|
|
19
|
+
relate: "%{left} should be %{op} %{right}"
|
|
18
20
|
responds_to: "does not respond to %{reference}"
|
|
19
21
|
string: is not a string
|
|
20
22
|
to_boolean: does not look like a boolean
|
|
@@ -17,7 +17,7 @@ module Datacaster
|
|
|
17
17
|
if right_result.valid?
|
|
18
18
|
left_result
|
|
19
19
|
else
|
|
20
|
-
Datacaster.ErrorResult(
|
|
20
|
+
Datacaster.ErrorResult(Utils.merge_errors(left_result.raw_errors, right_result.raw_errors))
|
|
21
21
|
end
|
|
22
22
|
end
|
|
23
23
|
end
|
data/lib/datacaster/base.rb
CHANGED
|
@@ -1,97 +1,5 @@
|
|
|
1
1
|
module Datacaster
|
|
2
2
|
class Base
|
|
3
|
-
|
|
4
|
-
add_error_to_base = ->(hash, error) {
|
|
5
|
-
hash[:base] ||= []
|
|
6
|
-
hash[:base] = merge_errors(hash[:base], error)
|
|
7
|
-
hash
|
|
8
|
-
}
|
|
9
|
-
|
|
10
|
-
return [] if left.nil? && right.nil?
|
|
11
|
-
return right if left.nil?
|
|
12
|
-
return left if right.nil?
|
|
13
|
-
|
|
14
|
-
result = case [left.class, right.class]
|
|
15
|
-
when [Array, Array]
|
|
16
|
-
left | right
|
|
17
|
-
when [Array, Hash]
|
|
18
|
-
add_error_to_base.(right, left)
|
|
19
|
-
when [Hash, Hash]
|
|
20
|
-
(left.keys | right.keys).map do |k|
|
|
21
|
-
[k, merge_errors(left[k], right[k])]
|
|
22
|
-
end.to_h
|
|
23
|
-
when [Hash, Array]
|
|
24
|
-
add_error_to_base.(left, right)
|
|
25
|
-
else
|
|
26
|
-
raise ArgumentError.new("Expected failures to be Arrays or Hashes, left: #{left.inspect}, right: #{right.inspect}")
|
|
27
|
-
end
|
|
28
|
-
|
|
29
|
-
result
|
|
30
|
-
end
|
|
31
|
-
|
|
32
|
-
def &(other)
|
|
33
|
-
AndNode.new(self, other)
|
|
34
|
-
end
|
|
35
|
-
|
|
36
|
-
def |(other)
|
|
37
|
-
OrNode.new(self, other)
|
|
38
|
-
end
|
|
39
|
-
|
|
40
|
-
def *(other)
|
|
41
|
-
AndWithErrorAggregationNode.new(self, other)
|
|
42
|
-
end
|
|
43
|
-
|
|
44
|
-
def cast_errors(error_caster)
|
|
45
|
-
ContextNodes::ErrorsCaster.new(self, error_caster)
|
|
46
|
-
end
|
|
47
|
-
|
|
48
|
-
def then(other)
|
|
49
|
-
ThenNode.new(self, DefinitionDSL.expand(other))
|
|
50
|
-
end
|
|
51
|
-
|
|
52
|
-
def with_context(context)
|
|
53
|
-
unless context.is_a?(Hash)
|
|
54
|
-
raise "with_context expected Hash as argument, got #{context.inspect} instead"
|
|
55
|
-
end
|
|
56
|
-
ContextNodes::UserContext.new(self, context)
|
|
57
|
-
end
|
|
58
|
-
|
|
59
|
-
def call(object)
|
|
60
|
-
call_with_runtime(object, Runtimes::Base.new)
|
|
61
|
-
end
|
|
62
|
-
|
|
63
|
-
def call_with_runtime(object, runtime)
|
|
64
|
-
result = cast(object, runtime: runtime)
|
|
65
|
-
unless result.is_a?(Result)
|
|
66
|
-
raise RuntimeError.new("Caster should've returned Datacaster::Result, but returned #{result.inspect} instead")
|
|
67
|
-
end
|
|
68
|
-
result
|
|
69
|
-
end
|
|
70
|
-
|
|
71
|
-
def with_runtime(runtime)
|
|
72
|
-
->(object) do
|
|
73
|
-
call_with_runtime(object, runtime)
|
|
74
|
-
end
|
|
75
|
-
end
|
|
76
|
-
|
|
77
|
-
def i18n_key(*keys, **args)
|
|
78
|
-
ContextNodes::I18n.new(self, I18nValues::Key.new(keys, args))
|
|
79
|
-
end
|
|
80
|
-
|
|
81
|
-
def i18n_map_keys(mapping)
|
|
82
|
-
ContextNodes::I18nKeysMapper.new(self, mapping)
|
|
83
|
-
end
|
|
84
|
-
|
|
85
|
-
def i18n_scope(scope, **args)
|
|
86
|
-
ContextNodes::I18n.new(self, I18nValues::Scope.new(scope, args))
|
|
87
|
-
end
|
|
88
|
-
|
|
89
|
-
def i18n_vars(vars)
|
|
90
|
-
ContextNodes::I18n.new(self, I18nValues::Scope.new(nil, vars))
|
|
91
|
-
end
|
|
92
|
-
|
|
93
|
-
def inspect
|
|
94
|
-
"#<Datacaster::Base>"
|
|
95
|
-
end
|
|
3
|
+
include Mixin
|
|
96
4
|
end
|
|
97
5
|
end
|
data/lib/datacaster/caster.rb
CHANGED
|
@@ -9,13 +9,13 @@ module Datacaster
|
|
|
9
9
|
def cast(object, runtime:)
|
|
10
10
|
result = Runtimes::Base.(runtime, @cast, object)
|
|
11
11
|
|
|
12
|
-
|
|
13
|
-
"should be returned from cast block") unless [Datacaster::Result, Dry::Monads::Result].any? { |k| result.is_a?(k) }
|
|
14
|
-
|
|
15
|
-
if result.is_a?(Dry::Monads::Result)
|
|
12
|
+
if defined?(Dry::Monads::Result) && result.is_a?(Dry::Monads::Result)
|
|
16
13
|
result = result.success? ? Datacaster.ValidResult(result.value!) : Datacaster.ErrorResult(result.failure)
|
|
17
14
|
end
|
|
18
15
|
|
|
16
|
+
raise TypeError.new("Either Datacaster::Result or Dry::Monads::Result " \
|
|
17
|
+
"should be returned from cast block") unless result.is_a?(Datacaster::Result)
|
|
18
|
+
|
|
19
19
|
result
|
|
20
20
|
end
|
|
21
21
|
|
|
@@ -45,9 +45,9 @@ module Datacaster
|
|
|
45
45
|
end
|
|
46
46
|
else
|
|
47
47
|
if key.is_a?(Array)
|
|
48
|
-
errors =
|
|
48
|
+
errors = Utils.merge_errors(errors, key.zip(new_value.raw_errors).to_h)
|
|
49
49
|
else
|
|
50
|
-
errors =
|
|
50
|
+
errors = Utils.merge_errors(errors, {key => new_value.raw_errors})
|
|
51
51
|
end
|
|
52
52
|
end
|
|
53
53
|
end
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
module Datacaster
|
|
2
|
+
module Mixin
|
|
3
|
+
def &(other)
|
|
4
|
+
AndNode.new(self, other)
|
|
5
|
+
end
|
|
6
|
+
|
|
7
|
+
def |(other)
|
|
8
|
+
OrNode.new(self, other)
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def *(other)
|
|
12
|
+
AndWithErrorAggregationNode.new(self, other)
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def cast_errors(error_caster)
|
|
16
|
+
ContextNodes::ErrorsCaster.new(self, error_caster)
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def then(other)
|
|
20
|
+
ThenNode.new(self, DefinitionDSL.expand(other))
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def with_context(context)
|
|
24
|
+
unless context.is_a?(Hash)
|
|
25
|
+
raise "with_context expected Hash as argument, got #{context.inspect} instead"
|
|
26
|
+
end
|
|
27
|
+
ContextNodes::UserContext.new(self, context)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def call(object)
|
|
31
|
+
call_with_runtime(object, Runtimes::Base.new)
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def call_with_runtime(object, runtime)
|
|
35
|
+
result = cast(object, runtime: runtime)
|
|
36
|
+
unless result.is_a?(Result)
|
|
37
|
+
raise RuntimeError.new("Caster should've returned Datacaster::Result, but returned #{result.inspect} instead")
|
|
38
|
+
end
|
|
39
|
+
result
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def with_runtime(runtime)
|
|
43
|
+
->(object) do
|
|
44
|
+
call_with_runtime(object, runtime)
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def i18n_key(*keys, **args)
|
|
49
|
+
ContextNodes::I18n.new(self, I18nValues::Key.new(keys, args))
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def i18n_map_keys(mapping)
|
|
53
|
+
ContextNodes::I18nKeysMapper.new(self, mapping)
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def i18n_scope(scope, **args)
|
|
57
|
+
ContextNodes::I18n.new(self, I18nValues::Scope.new(scope, args))
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def i18n_vars(vars)
|
|
61
|
+
ContextNodes::I18n.new(self, I18nValues::Scope.new(nil, vars))
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def inspect
|
|
65
|
+
"#<Datacaster::Base>"
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
end
|
|
@@ -89,6 +89,26 @@ module Datacaster
|
|
|
89
89
|
check { |x| x != Datacaster.absent }.i18n_key(*error_keys)
|
|
90
90
|
end
|
|
91
91
|
|
|
92
|
+
def attribute(*keys)
|
|
93
|
+
if keys.empty? || keys.any? { |k| !Datacaster::Utils.pickable?(k) }
|
|
94
|
+
raise RuntimeError, "each argument should be String, Symbol, Integer or an array thereof", caller
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
transform do |input|
|
|
98
|
+
result =
|
|
99
|
+
keys.map do |key|
|
|
100
|
+
Array(key).reduce(input) do |result, k|
|
|
101
|
+
if result.respond_to?(k)
|
|
102
|
+
result.public_send(k)
|
|
103
|
+
else
|
|
104
|
+
break Datacaster.absent
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
keys.length == 1 ? result.first : result
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
|
|
92
112
|
def default(value, on: nil)
|
|
93
113
|
transform do |x|
|
|
94
114
|
if x == Datacaster.absent ||
|
|
@@ -100,55 +120,120 @@ module Datacaster
|
|
|
100
120
|
end
|
|
101
121
|
end
|
|
102
122
|
|
|
103
|
-
def
|
|
104
|
-
|
|
105
|
-
end
|
|
106
|
-
|
|
107
|
-
def remove
|
|
108
|
-
transform { Datacaster.absent }
|
|
123
|
+
def merge_message_keys(*keys)
|
|
124
|
+
MessageKeysMerger.new(keys)
|
|
109
125
|
end
|
|
110
126
|
|
|
111
|
-
def
|
|
112
|
-
|
|
127
|
+
def must_be(klass, error_key = nil)
|
|
128
|
+
error_keys = ['.must_be', 'datacaster.errors.must_be']
|
|
129
|
+
error_keys.unshift(error_key) if error_key
|
|
130
|
+
check { |x| x.is_a?(klass) }.i18n_key(*error_keys, reference: klass.name)
|
|
113
131
|
end
|
|
114
132
|
|
|
115
|
-
def
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
133
|
+
def optional(base, on: nil)
|
|
134
|
+
return absent | base if on == nil
|
|
135
|
+
cast do |x|
|
|
136
|
+
if x == Datacaster.absent ||
|
|
137
|
+
(!on.nil? && x.respond_to?(on) && x.public_send(on))
|
|
138
|
+
Datacaster.ValidResult(Datacaster.absent)
|
|
119
139
|
else
|
|
120
|
-
|
|
140
|
+
base.(x)
|
|
121
141
|
end
|
|
122
|
-
on_clauses.reduce(switch) do |result, (k, v)|
|
|
123
|
-
result.on(k, v)
|
|
124
142
|
end
|
|
125
143
|
end
|
|
126
144
|
|
|
145
|
+
def pass
|
|
146
|
+
transform(&:itself)
|
|
147
|
+
end
|
|
148
|
+
|
|
127
149
|
def pass_if(base)
|
|
128
150
|
ContextNodes::PassIf.new(base)
|
|
129
151
|
end
|
|
130
152
|
|
|
131
|
-
def pick(*keys
|
|
132
|
-
|
|
153
|
+
def pick(*keys)
|
|
154
|
+
if keys.empty? || keys.any? { |k| !Datacaster::Utils.pickable?(k) }
|
|
155
|
+
raise RuntimeError, "each argument should be String, Symbol, Integer or an array thereof", caller
|
|
156
|
+
end
|
|
133
157
|
|
|
134
|
-
|
|
158
|
+
retrieve_key = -> (from, key) do
|
|
159
|
+
if from.respond_to?(:key?) && !from.key?(key)
|
|
160
|
+
Datacaster.absent
|
|
161
|
+
elsif from.respond_to?(:length) && key.is_a?(Integer) && key > 0 && key >= from.length
|
|
162
|
+
Datacaster.absent
|
|
163
|
+
elsif !from.respond_to?(:[])
|
|
164
|
+
Datacaster.absent
|
|
165
|
+
else
|
|
166
|
+
from[key]
|
|
167
|
+
end
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
must_be(Enumerable) & transform { |input|
|
|
135
171
|
result =
|
|
136
172
|
keys.map do |key|
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
else
|
|
142
|
-
value[key]
|
|
173
|
+
Array(key).reduce(input) do |result, k|
|
|
174
|
+
result = retrieve_key.(result, k)
|
|
175
|
+
break result if result == Datacaster.absent
|
|
176
|
+
result
|
|
143
177
|
end
|
|
144
178
|
end
|
|
145
|
-
|
|
146
179
|
keys.length == 1 ? result.first : result
|
|
147
180
|
}
|
|
148
181
|
end
|
|
149
182
|
|
|
150
|
-
def
|
|
151
|
-
|
|
183
|
+
def relate(left, op, right, error_key: nil)
|
|
184
|
+
error_keys = ['.relate', 'datacaster.errors.relate']
|
|
185
|
+
additional_vars = {}
|
|
186
|
+
|
|
187
|
+
left_caster = left
|
|
188
|
+
if Datacaster::Utils.pickable?(left)
|
|
189
|
+
left_caster = pick(left)
|
|
190
|
+
elsif !Datacaster.instance?(left)
|
|
191
|
+
raise RuntimeError, "provide String, Symbol, Integer or array thereof instead of #{left.inspect}", caller
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
right_caster = right
|
|
195
|
+
if Datacaster::Utils.pickable?(right)
|
|
196
|
+
right_caster = pick(right)
|
|
197
|
+
elsif !Datacaster.instance?(right)
|
|
198
|
+
raise RuntimeError, "provide String, Symbol, Integer or array thereof instead of #{right.inspect}", caller
|
|
199
|
+
end
|
|
200
|
+
|
|
201
|
+
op_caster = op
|
|
202
|
+
if op.is_a?(String) || op.is_a?(Symbol)
|
|
203
|
+
op_caster = check { |(l, r)| l.respond_to?(op) && l.public_send(op, r) }
|
|
204
|
+
elsif !Datacaster.instance?(left)
|
|
205
|
+
raise RuntimeError, "provide String or Symbol instead of #{op.inspect}", caller
|
|
206
|
+
end
|
|
207
|
+
|
|
208
|
+
{left: left, op: op, right: right}.each do |k, v|
|
|
209
|
+
if [String, Symbol, Integer].any? { |c| v.is_a?(c) }
|
|
210
|
+
additional_vars[k] = v
|
|
211
|
+
end
|
|
212
|
+
end
|
|
213
|
+
|
|
214
|
+
if additional_vars.length == 3
|
|
215
|
+
error_keys.unshift(".#{left}.#{op}.#{right}")
|
|
216
|
+
end
|
|
217
|
+
error_keys.unshift(error_key) if error_key
|
|
218
|
+
|
|
219
|
+
cast do |value|
|
|
220
|
+
left_result = left_caster.(value)
|
|
221
|
+
next left_result unless left_result.valid?
|
|
222
|
+
i18n_var!(:left, left_result.value) unless additional_vars.key?(:left)
|
|
223
|
+
|
|
224
|
+
right_result = right_caster.(value)
|
|
225
|
+
next right_result unless right_result.valid?
|
|
226
|
+
i18n_var!(:right, right_result.value) unless additional_vars.key?(:right)
|
|
227
|
+
|
|
228
|
+
result = op_caster.([left_result.value, right_result.value])
|
|
229
|
+
next Datacaster.ErrorResult([I18nValues::Key.new(error_keys)]) unless result.valid?
|
|
230
|
+
|
|
231
|
+
Datacaster.ValidResult(value)
|
|
232
|
+
end.i18n_vars(additional_vars)
|
|
233
|
+
end
|
|
234
|
+
|
|
235
|
+
def remove
|
|
236
|
+
transform { Datacaster.absent }
|
|
152
237
|
end
|
|
153
238
|
|
|
154
239
|
def responds_to(method, error_key = nil)
|
|
@@ -157,22 +242,15 @@ module Datacaster
|
|
|
157
242
|
check { |x| x.respond_to?(method) }.i18n_key(*error_keys, reference: method.to_s)
|
|
158
243
|
end
|
|
159
244
|
|
|
160
|
-
def
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
245
|
+
def switch(base = nil, **on_clauses)
|
|
246
|
+
switch = SwitchNode.new(base)
|
|
247
|
+
on_clauses.reduce(switch) do |result, (k, v)|
|
|
248
|
+
result.on(k, v)
|
|
249
|
+
end
|
|
164
250
|
end
|
|
165
251
|
|
|
166
|
-
def
|
|
167
|
-
|
|
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
|
|
252
|
+
def transform_to_value(value)
|
|
253
|
+
transform { value }
|
|
176
254
|
end
|
|
177
255
|
|
|
178
256
|
# Strict types
|
|
@@ -205,13 +283,19 @@ module Datacaster
|
|
|
205
283
|
def hash_value(error_key = nil)
|
|
206
284
|
error_keys = ['.hash_value', 'datacaster.errors.hash_value']
|
|
207
285
|
error_keys.unshift(error_key) if error_key
|
|
208
|
-
check
|
|
286
|
+
check { |x| x.is_a?(Hash) }.i18n_key(*error_keys)
|
|
209
287
|
end
|
|
210
288
|
|
|
211
289
|
def hash_with_symbolized_keys(error_key = nil)
|
|
212
290
|
hash_value(error_key) & transform { |x| x.symbolize_keys }
|
|
213
291
|
end
|
|
214
292
|
|
|
293
|
+
def included_in(*values, error_key: nil)
|
|
294
|
+
error_keys = ['.included_in', 'datacaster.errors.included_in']
|
|
295
|
+
error_keys.unshift(error_key) if error_key
|
|
296
|
+
check { |x| values.include?(x) }.i18n_key(*error_keys, reference: values.map(&:to_s).join(', '))
|
|
297
|
+
end
|
|
298
|
+
|
|
215
299
|
def integer(error_key = nil)
|
|
216
300
|
error_keys = ['.integer', 'datacaster.errors.integer']
|
|
217
301
|
error_keys.unshift(error_key) if error_key
|
|
@@ -1,16 +1,14 @@
|
|
|
1
1
|
module Datacaster
|
|
2
2
|
class SwitchNode < Base
|
|
3
3
|
def initialize(base = nil, on_casters = [], else_caster = nil)
|
|
4
|
-
base = base
|
|
4
|
+
@base = base
|
|
5
5
|
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
@base = Datacaster::Predefined.pick(base)
|
|
13
|
-
else
|
|
6
|
+
if Datacaster::Utils.pickable?(@base)
|
|
7
|
+
@base = Datacaster::Predefined.pick(@base)
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
if !@base.nil? && !Datacaster.instance?(@base)
|
|
11
|
+
puts @base.inspect
|
|
14
12
|
raise RuntimeError, "provide a Datacaster::Base instance, a hash key, or an array of keys to switch(...) caster", caller
|
|
15
13
|
end
|
|
16
14
|
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
module Datacaster
|
|
2
|
+
module Utils
|
|
3
|
+
extend self
|
|
4
|
+
|
|
5
|
+
def merge_errors(left, right)
|
|
6
|
+
add_error_to_base = ->(hash, error) {
|
|
7
|
+
hash[:base] ||= []
|
|
8
|
+
hash[:base] = merge_errors(hash[:base], error)
|
|
9
|
+
hash
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
return [] if left.nil? && right.nil?
|
|
13
|
+
return right if left.nil?
|
|
14
|
+
return left if right.nil?
|
|
15
|
+
|
|
16
|
+
result = case [left.class, right.class]
|
|
17
|
+
when [Array, Array]
|
|
18
|
+
left | right
|
|
19
|
+
when [Array, Hash]
|
|
20
|
+
add_error_to_base.(right, left)
|
|
21
|
+
when [Hash, Hash]
|
|
22
|
+
(left.keys | right.keys).map do |k|
|
|
23
|
+
[k, merge_errors(left[k], right[k])]
|
|
24
|
+
end.to_h
|
|
25
|
+
when [Hash, Array]
|
|
26
|
+
add_error_to_base.(left, right)
|
|
27
|
+
else
|
|
28
|
+
raise ArgumentError.new("Expected failures to be Arrays or Hashes, left: #{left.inspect}, right: #{right.inspect}")
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
result
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def pickable?(value)
|
|
35
|
+
is_literal = ->(v) { [String, Symbol, Integer].any? { |c| v.is_a?(c) } }
|
|
36
|
+
is_literal.(value) ||
|
|
37
|
+
value.is_a?(Array) && !value.empty? && value.all? { |v| is_literal.(v) }
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
data/lib/datacaster/version.rb
CHANGED
data/lib/datacaster.rb
CHANGED
|
@@ -24,6 +24,10 @@ module Datacaster
|
|
|
24
24
|
Datacaster::Absent.instance
|
|
25
25
|
end
|
|
26
26
|
|
|
27
|
+
def instance?(object)
|
|
28
|
+
object.is_a?(Mixin)
|
|
29
|
+
end
|
|
30
|
+
|
|
27
31
|
private
|
|
28
32
|
|
|
29
33
|
def build_schema(i18n_scope: nil, &block)
|
|
@@ -31,7 +35,7 @@ module Datacaster
|
|
|
31
35
|
|
|
32
36
|
datacaster = DefinitionDSL.eval(&block)
|
|
33
37
|
|
|
34
|
-
unless
|
|
38
|
+
unless Datacaster.instance?(datacaster)
|
|
35
39
|
raise "Datacaster instance should be returned from a block (e.g. result of 'hash_schema(...)' call)"
|
|
36
40
|
end
|
|
37
41
|
|
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.1.
|
|
4
|
+
version: 3.1.3
|
|
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-10-
|
|
11
|
+
date: 2023-10-06 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: activemodel
|
|
@@ -150,6 +150,7 @@ files:
|
|
|
150
150
|
- lib/datacaster/i18n_values/key.rb
|
|
151
151
|
- lib/datacaster/i18n_values/scope.rb
|
|
152
152
|
- lib/datacaster/message_keys_merger.rb
|
|
153
|
+
- lib/datacaster/mixin.rb
|
|
153
154
|
- lib/datacaster/or_node.rb
|
|
154
155
|
- lib/datacaster/predefined.rb
|
|
155
156
|
- lib/datacaster/result.rb
|
|
@@ -162,6 +163,7 @@ files:
|
|
|
162
163
|
- lib/datacaster/then_node.rb
|
|
163
164
|
- lib/datacaster/transformer.rb
|
|
164
165
|
- lib/datacaster/trier.rb
|
|
166
|
+
- lib/datacaster/utils.rb
|
|
165
167
|
- lib/datacaster/validator.rb
|
|
166
168
|
- lib/datacaster/version.rb
|
|
167
169
|
homepage: https://github.com/EugZol/datacaster
|