datacaster 3.1.2 → 3.1.5

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: 22f1741420c172f91fba9bf20a341a785f609a7e0132e74bcd3a07e6069eaca4
4
- data.tar.gz: 641fb863b7a40931f481d066e09062cddff1a4e3ff642d5981dacf9bfaa036eb
3
+ metadata.gz: 29c3f6c825f2c2d9a664e370079cde58df4ce9685d34d8428fb29aa7b98c52f3
4
+ data.tar.gz: 7d90ad442a4bd32ae4526d403629676c3aec9566973486bacd6565e8cb3d7bbd
5
5
  SHA512:
6
- metadata.gz: 2a3573aac6a44ef0a5849da04325c9089119ad529cc6f6384d18bb6dda97f66358081c27d1fc260d95f7adaec5937bda7769cd9207ea9be6b01f2547f4425659
7
- data.tar.gz: '04765854fbe156b3418728d4d2392b78ce0837ead8e1e72b76a51d55e6227bb68eebf8a98e672c924205bbe731610f21c72a1be355c51c2f014886ab160efdb7'
6
+ metadata.gz: acc11fefd17414ba1400bd98d2105b194301ac63f4d6b4204f001f918984326ab711c92d89133b475fd09d51b10c5d6427f6d6f516742a4ceb0a3eb2cf07f983
7
+ data.tar.gz: 07f288ab4e44e0b7a931aa43b34bac1a98d34d68f9d72241fc34f1648aeedc0412b51bdbe1113d9c34674a283e5fc6c559d4d2c5c385f5ec55c65d3b8345fb73
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)
@@ -414,6 +415,7 @@ Notice that shortcut definitions are available (illustrated in the example above
414
415
 
415
416
  * `switch(:key)` is exactly the same as `switch(pick(:key))` (works for a string, a symbol, or an array thereof)
416
417
  * `on(:key, ...)` is exactly the same as `on(compare(:key), ...)` (works for a string or a symbol)
418
+ * `on(:key, ...)` will match on `:key` and `'key'` value, and the same is true for `on('key', ...)` (to disable that behavior provide `strict: true` keyword arg: `on('key', ..., strict: true)`)
417
419
  * `switch([caster], on_check => on_caster, ...)` is exactly the same as `switch([caster]).on(on_check, on_caster).on(...)`
418
420
 
419
421
  `switch()` without a `base` argument will pass the incoming value to the `.on(...)` casters.
@@ -523,6 +525,26 @@ Returns ValidResult if and only if provided value is not `Datacaster.absent` (th
523
525
 
524
526
  I18n keys: `error_key`, `'.any'`, `'datacaster.errors.any'`
525
527
 
528
+ #### `attribute(*keys)`
529
+
530
+ 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).
531
+
532
+ ```ruby
533
+ class User
534
+ def login
535
+ "Alex"
536
+ end
537
+ end
538
+
539
+ login = Datacaster.schema { attribute(:login) }
540
+
541
+ # => Datacaster::ValidResult("Alex")
542
+ login.(User.new)
543
+
544
+ # => Datacaster::ValidResult(#<Datacaster.absent>)
545
+ login.("test")
546
+ ```
547
+
526
548
  #### `default(default_value, on: nil)`
527
549
 
528
550
  Always returns ValidResult.
@@ -633,7 +655,7 @@ mapping.(
633
655
 
634
656
  See also `#cast_errors` for [error remapping](#error-remapping-cast_errors).
635
657
 
636
- See also `#pick` for [simpler picking of hash values](#pickkey).
658
+ See also `#pick` for [simpler picking of hash values](#pickkeys).
637
659
 
638
660
  I18n keys:
639
661
 
@@ -686,15 +708,18 @@ Returns ValidResult if and only if base returns ValidResult. Returns base's erro
686
708
 
687
709
  Doesn't transform the value: if base succeeds returns the original value (not the one that base returned).
688
710
 
689
- #### `pick(key)`
711
+ #### `pick(*keys)`
690
712
 
691
713
  Returns ValidResult if and only if the value `#is_a?(Enumerable)`.
692
714
 
693
- Transforms the value to/returns:
715
+ 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:
716
+
717
+ * If the argument is an Array, value is extracted recursively
718
+ * Otherwise, `value[argument]` is fetched and added to the result (or `Datacaster.absent` if is is impossible to fetch)
719
+
720
+ 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.
694
721
 
695
- * `value[key]` if key is set in the value
696
- * `nil` if `value[key]` is set and is nil
697
- * `Datacaster.absent` if key is not set
722
+ Fetching single key:
698
723
 
699
724
  ```ruby
700
725
  pick_name = Datacaster.schema { pick(:name) }
@@ -705,9 +730,7 @@ pick_name.(last_name: "Johnson") # => Datacaster::ValidResult(#<Datacaster.absen
705
730
  pick_name.("test") # => Datacaster::ErrorResult(["is not Enumerable"])
706
731
  ```
707
732
 
708
- Alternative form could be used: `pick(*keys)`.
709
-
710
- 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.
733
+ Fetching multiple keys:
711
734
 
712
735
  ```ruby
713
736
  pick_name_and_age = Datacaster.schema { pick(:name, :age) }
@@ -718,6 +741,15 @@ pick_name_and_age.(last_name: "Johnson", age: 20) # => Datacaster::ValidResult([
718
741
  pick_name_and_age.("test") # => Datacaster::ErrorResult(["is not Enumerable"])
719
742
  ```
720
743
 
744
+ Fetching deeply nested key:
745
+
746
+ ```ruby
747
+ nested_hash_picker = Datacaster.schema { pick([:user, :age]) }
748
+
749
+ nested_hash_picker.(user: { age: 21 }) # => Datacaster::ValidResult(21)
750
+ nested_hash_picker.(user: { name: "Alex" }) # => Datacaster::ValidResult(#<Datacaster.absent>)
751
+ ```
752
+
721
753
  I18n keys:
722
754
 
723
755
  * not a Enumerable – `'.must_be'`, `'datacaster.errors.must_be'`.
@@ -1322,7 +1354,7 @@ Note: in the "root" scope (immediately inside of `schema { ... }` block) the wor
1322
1354
 
1323
1355
  ### Mapping hashes: `transform_to_hash`
1324
1356
 
1325
- 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)).
1357
+ 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)).
1326
1358
 
1327
1359
  ```ruby
1328
1360
  city_with_distance =
@@ -7,17 +7,14 @@ module Datacaster
7
7
  attr_accessor :i18n_module
8
8
 
9
9
  def add_predefined_caster(name, definition)
10
- caster =
11
- case definition
12
- when Proc
13
- Datacaster.partial_schema(&definition)
14
- when Base
15
- definition
16
- else
17
- raise ArgumentError.new("Expected Datacaster defintion lambda or Datacaster instance")
18
- end
19
-
20
- Predefined.define_method(name.to_sym) { caster }
10
+ case definition
11
+ when Proc
12
+ Predefined.define_method(name.to_sym, &definition)
13
+ when Base
14
+ Predefined.define_method(name.to_sym) { definition }
15
+ else
16
+ raise ArgumentError.new("Expected Datacaster defintion lambda or Datacaster instance")
17
+ end
21
18
  end
22
19
 
23
20
  def i18n_t
@@ -54,15 +54,15 @@ module Datacaster
54
54
  end
55
55
 
56
56
  def schema(base)
57
- ContextNodes::StructureCleaner.new(base, strategy: :fail)
57
+ ContextNodes::StructureCleaner.new(base, :fail)
58
58
  end
59
59
 
60
60
  def choosy_schema(base)
61
- ContextNodes::StructureCleaner.new(base, strategy: :remove)
61
+ ContextNodes::StructureCleaner.new(base, :remove)
62
62
  end
63
63
 
64
64
  def partial_schema(base)
65
- ContextNodes::StructureCleaner.new(base, strategy: :pass)
65
+ ContextNodes::StructureCleaner.new(base, :pass)
66
66
  end
67
67
 
68
68
  # 'Meta' types
@@ -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,46 +120,108 @@ module Datacaster
100
120
  end
101
121
  end
102
122
 
103
- def transform_to_value(value)
104
- transform { value }
123
+ def merge_message_keys(*keys)
124
+ MessageKeysMerger.new(keys)
125
+ end
126
+
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)
131
+ end
132
+
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)
139
+ else
140
+ base.(x)
141
+ end
142
+ end
105
143
  end
106
144
 
107
- # min_amount: has_relation(:min_amount, :>, :max_amount)
145
+ def pass
146
+ transform(&:itself)
147
+ end
148
+
149
+ def pass_if(base)
150
+ ContextNodes::PassIf.new(base)
151
+ end
152
+
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
157
+
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|
171
+ result =
172
+ keys.map do |key|
173
+ Array(key).reduce(input) do |result, k|
174
+ result = retrieve_key.(result, k)
175
+ break result if result == Datacaster.absent
176
+ result
177
+ end
178
+ end
179
+ keys.length == 1 ? result.first : result
180
+ }
181
+ end
108
182
 
109
183
  def relate(left, op, right, error_key: nil)
110
184
  error_keys = ['.relate', 'datacaster.errors.relate']
111
185
  additional_vars = {}
112
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
+
113
208
  {left: left, op: op, right: right}.each do |k, v|
114
209
  if [String, Symbol, Integer].any? { |c| v.is_a?(c) }
115
210
  additional_vars[k] = v
116
- elsif !Datacaster.instance?(v)
117
- raise RuntimeError, "expected #{k} to be String, Symbol, Integer or Datacaster::Base, but got #{v.inspect}", caller
118
211
  end
119
212
  end
120
213
 
121
- if op.is_a?(Integer)
122
- raise RuntimeError, "expected op to be String, Symbol or Datacaster::Base, but got #{op.inspect}", caller
123
- end
124
-
125
- if [left, op, right].none? { |x| Datacaster.instance?(x) }
214
+ if additional_vars.length == 3
126
215
  error_keys.unshift(".#{left}.#{op}.#{right}")
127
216
  end
128
217
  error_keys.unshift(error_key) if error_key
129
218
 
130
- left = pick(left) unless Datacaster.instance?(left)
131
- right = pick(right) unless Datacaster.instance?(right)
132
- op_caster = op
133
- unless Datacaster.instance?(op_caster)
134
- op_caster = check { |(l, r)| l.respond_to?(op) && l.public_send(op, r) }
135
- end
136
-
137
219
  cast do |value|
138
- left_result = left.(value)
220
+ left_result = left_caster.(value)
139
221
  next left_result unless left_result.valid?
140
222
  i18n_var!(:left, left_result.value) unless additional_vars.key?(:left)
141
223
 
142
- right_result = right.(value)
224
+ right_result = right_caster.(value)
143
225
  next right_result unless right_result.valid?
144
226
  i18n_var!(:right, right_result.value) unless additional_vars.key?(:right)
145
227
 
@@ -154,71 +236,21 @@ module Datacaster
154
236
  transform { Datacaster.absent }
155
237
  end
156
238
 
157
- def pass
158
- transform(&:itself)
159
- end
160
-
161
- def switch(*base, **on_clauses)
162
- switch =
163
- if base.length == 0
164
- SwitchNode.new
165
- else
166
- SwitchNode.new(base)
167
- end
168
- on_clauses.reduce(switch) do |result, (k, v)|
169
- result.on(k, v)
170
- end
171
- end
172
-
173
- def pass_if(base)
174
- ContextNodes::PassIf.new(base)
175
- end
176
-
177
- def pick(*keys, strict: false)
178
- raise RuntimeError.new("provide keys to pick, e.g. pick(:key)") if keys.empty?
179
-
180
- must_be(Enumerable) & transform { |value|
181
- result =
182
- keys.map do |key|
183
- if value.respond_to?(:key?) && !value.key?(key)
184
- Datacaster.absent
185
- elsif value.respond_to?(:length) && key.is_a?(Integer) && key > 0 && key >= value.length
186
- Datacaster.absent
187
- else
188
- value[key]
189
- end
190
- end
191
-
192
- keys.length == 1 ? result.first : result
193
- }
194
- end
195
-
196
- def merge_message_keys(*keys)
197
- MessageKeysMerger.new(keys)
198
- end
199
-
200
239
  def responds_to(method, error_key = nil)
201
240
  error_keys = ['.responds_to', 'datacaster.errors.responds_to']
202
241
  error_keys.unshift(error_key) if error_key
203
242
  check { |x| x.respond_to?(method) }.i18n_key(*error_keys, reference: method.to_s)
204
243
  end
205
244
 
206
- def must_be(klass, error_key = nil)
207
- error_keys = ['.must_be', 'datacaster.errors.must_be']
208
- error_keys.unshift(error_key) if error_key
209
- 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
210
250
  end
211
251
 
212
- def optional(base, on: nil)
213
- return absent | base if on == nil
214
- cast do |x|
215
- if x == Datacaster.absent ||
216
- (!on.nil? && x.respond_to?(on) && x.public_send(on))
217
- Datacaster.ValidResult(Datacaster.absent)
218
- else
219
- base.(x)
220
- end
221
- end
252
+ def transform_to_value(value)
253
+ transform { value }
222
254
  end
223
255
 
224
256
  # Strict types
@@ -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
 
@@ -18,11 +16,18 @@ module Datacaster
18
16
  @else = else_caster
19
17
  end
20
18
 
21
- def on(caster_or_value, clause)
19
+ def on(caster_or_value, clause, strict: false)
22
20
  caster =
23
21
  case caster_or_value
24
22
  when Datacaster::Base
25
23
  caster_or_value
24
+ when String, Symbol
25
+ if strict
26
+ Datacaster::Predefined.compare(caster_or_value)
27
+ else
28
+ Datacaster::Predefined.compare(caster_or_value.to_s) |
29
+ Datacaster::Predefined.compare(caster_or_value.to_sym)
30
+ end
26
31
  else
27
32
  Datacaster::Predefined.compare(caster_or_value)
28
33
  end
@@ -30,5 +30,11 @@ module Datacaster
30
30
 
31
31
  result
32
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
33
39
  end
34
40
  end
@@ -1,3 +1,3 @@
1
1
  module Datacaster
2
- VERSION = "3.1.2"
2
+ VERSION = "3.1.5"
3
3
  end
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.2
4
+ version: 3.1.5
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-05 00:00:00.000000000 Z
11
+ date: 2023-10-07 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activemodel