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 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