datacaster 3.1.1 → 3.1.3

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: d563422d81d874ec9f74b4e95a3d24a7a59cf2c3f50ad709a226346616364882
4
- data.tar.gz: e33c3dffdbff37e5ce711f56c0196e8955379764c98c2429ec391fd33819797d
3
+ metadata.gz: d7cd7191206c373ab38c5516505bf38cbbdc76a7b7c433f976498accda34b054
4
+ data.tar.gz: 0443edf8c8f494caeacfb55ba902cce19210a53cf9f726e8306f7e487de58fd8
5
5
  SHA512:
6
- metadata.gz: d01eb486e4d11954f672e6958803b7ea4c60430c58850f2b4c8e8b16a500229c28a91ecadc45317775f7ff8098c4beddfb0373d415f72e5c917ccb1a93f589c8
7
- data.tar.gz: c9bdb6f156a7de6eb81fa9cb87dcbf050fd47292d417afd4d085cb64a9e5460c4022b6471de723a1e112cd730a5b12e14c595e9ce248e30ff18fd84625a2cc8a
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(key)`](#pickkey)
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
- #=> Datacaster::ErrorResult(['should be an array'])
181
+ # => Datacaster::ErrorResult(['should be an array'])
179
182
 
180
183
  # In reality
181
- #=> <Datacaster::ErrorResult([#<Datacaster::I18nValues::Key(.array, datacaster.errors.array) {:value=>nil}>])>
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](#pickkey).
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(key)`
710
+ #### `pick(*keys)`
688
711
 
689
712
  Returns ValidResult if and only if the value `#is_a?(Enumerable)`.
690
713
 
691
- Transforms the value to/returns:
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
- * `value[key]` if key is set in the value
694
- * `nil` if `value[key]` is set and is nil
695
- * `Datacaster.absent` if key is not set
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
- Alternative form could be used: `pick(*keys)`.
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`](#pickkey) and [`remove`](#remove)).
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 =
@@ -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(self.class.merge_errors(left_result.raw_errors, right_result.raw_errors))
20
+ Datacaster.ErrorResult(Utils.merge_errors(left_result.raw_errors, right_result.raw_errors))
21
21
  end
22
22
  end
23
23
  end
@@ -1,97 +1,5 @@
1
1
  module Datacaster
2
2
  class Base
3
- def self.merge_errors(left, right)
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
@@ -9,13 +9,13 @@ module Datacaster
9
9
  def cast(object, runtime:)
10
10
  result = Runtimes::Base.(runtime, @cast, object)
11
11
 
12
- raise TypeError.new("Either Datacaster::Result or Dry::Monads::Result " \
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 = self.class.merge_errors(errors, key.zip(new_value.raw_errors).to_h)
48
+ errors = Utils.merge_errors(errors, key.zip(new_value.raw_errors).to_h)
49
49
  else
50
- errors = self.class.merge_errors(errors, {key => new_value.raw_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 transform_to_value(value)
104
- transform { value }
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 pass
112
- transform(&:itself)
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 switch(*base, **on_clauses)
116
- switch =
117
- if base.length == 0
118
- SwitchNode.new
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
- SwitchNode.new(base)
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, strict: false)
132
- raise RuntimeError.new("provide keys to pick, e.g. pick(:key)") if keys.empty?
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
- must_be(Enumerable) & transform { |value|
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
- if value.respond_to?(:key?) && !value.key?(key)
138
- Datacaster.absent
139
- elsif value.respond_to?(:length) && key.is_a?(Integer) && key > 0 && key >= value.length
140
- Datacaster.absent
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 merge_message_keys(*keys)
151
- MessageKeysMerger.new(keys)
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 must_be(klass, error_key = nil)
161
- error_keys = ['.must_be', 'datacaster.errors.must_be']
162
- error_keys.unshift(error_key) if error_key
163
- check { |x| x.is_a?(klass) }.i18n_key(*error_keys, reference: klass.name)
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 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
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(error_key) { |x| x.is_a?(Hash) }
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[0] if base.is_a?(Array) && base.length == 1
4
+ @base = base
5
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
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
@@ -1,3 +1,3 @@
1
1
  module Datacaster
2
- VERSION = "3.1.1"
2
+ VERSION = "3.1.3"
3
3
  end
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 datacaster.is_a?(Base)
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.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-02 00:00:00.000000000 Z
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