datacaster 3.1.5 → 3.2.1

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: 29c3f6c825f2c2d9a664e370079cde58df4ce9685d34d8428fb29aa7b98c52f3
4
- data.tar.gz: 7d90ad442a4bd32ae4526d403629676c3aec9566973486bacd6565e8cb3d7bbd
3
+ metadata.gz: ed24c848eb7b909ee24655728013ccddad59fa7ad0c497326d767fcbe0ea369b
4
+ data.tar.gz: 6bc155f410e73bcd09a9941055e23c63aefc79caff1fcff47110d7d03b02b145
5
5
  SHA512:
6
- metadata.gz: acc11fefd17414ba1400bd98d2105b194301ac63f4d6b4204f001f918984326ab711c92d89133b475fd09d51b10c5d6427f6d6f516742a4ceb0a3eb2cf07f983
7
- data.tar.gz: 07f288ab4e44e0b7a931aa43b34bac1a98d34d68f9d72241fc34f1648aeedc0412b51bdbe1113d9c34674a283e5fc6c559d4d2c5c385f5ec55c65d3b8345fb73
6
+ metadata.gz: e59a0fc79dc9fbc350ec581d7741bc61d064439aea06f66cc814fba82e9678423f140ce0a5eeef17aa51a39c3f2457c5bc1b1909ab10db2b7e44ba1d214d2ac6
7
+ data.tar.gz: e53f422a189dcaee5f898f8f2b59fb9538fe26e9551b66f0bb3da467f4f1f91aad7480b2237df90b05892a4f72a2bdd56b446ad5691adca5f1d38b39ccfdb27a
data/README.md CHANGED
@@ -44,6 +44,7 @@ It is currently used in production in several projects (mainly as request parame
44
44
  - [`pick(*keys)`](#pickkeys)
45
45
  - [`remove`](#remove)
46
46
  - [`responds_to(method, error_key = nil)`](#responds_tomethod-error_key--nil)
47
+ - [`with(key, caster)`](#withkey-caster)
47
48
  - [`transform_to_value(value)`](#transform_to_valuevalue)
48
49
  - ["Web-form" types](#web-form-types)
49
50
  - [`iso8601(error_key = nil)`](#iso8601error_key--nil)
@@ -57,8 +58,9 @@ It is currently used in production in several projects (mainly as request parame
57
58
  - [`try(error_key = nil, catched_exception:) { |value| ... }`](#tryerror_key--nil-catched_exception--value--)
58
59
  - [`validate(active_model_validations, name = 'Anonymous')`](#validateactive_model_validations-name--anonymous)
59
60
  - [`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
+ - [`included_in(reference_values, error_key: nil)`](#included_inreference_values-error_key-nil)
61
62
  - [`relate(left, op, right, error_key: nil)`](#relateleft-op-right-error_key-nil)
63
+ - [`run { |value| ... }`](#run--value--)
62
64
  - [`transform { |value| ... }`](#transform--value--)
63
65
  - [`transform_if_present { |value| ... }`](#transform_if_present--value--)
64
66
  - [Array schemas](#array-schemas)
@@ -165,11 +167,15 @@ It is worth noting that in `a & b` validation composition as above, if `a` in so
165
167
 
166
168
  All datacaster validations, when called, return an instance of `Datacaster::Result` value, i.e. `Datacaster::ValidResult` or `Datacaster::ErrorResult`.
167
169
 
168
- You can call `#valid?`, `#value`, `#errors` methods directly, or, if preferred, call `#to_dry_result` method to convert `Datacaster::Result` to the corresponding `Dry::Monads::Result` (with all the included "batteries" of the latter, e.g. pattern matching, 'binding', etc.).
170
+ You can call `#valid?`, `#value`, `#errors` methods directly, or, if preferred, call `#to_dry_result` method to convert `Datacaster::Result` to the corresponding `Dry::Monads::Result`.
169
171
 
170
- `#value` and `#errors` would return `#nil` if the result is, correspondingly, `ErrorResult` and `ValidResult`. No methods would raise an error.
172
+ `#value` and `#errors` would return `#nil` if the result is, correspondingly, `ErrorResult` and `ValidResult`.
171
173
 
172
- Errors are returned as array or hash (or hash of arrays, or array of hashes, etc., for complex data structures). Errors support internationalization (i18n) natively. Each element of the returned array shows a separate error as a special i18n value object, and each key of the returned hash corresponds to the key of the validated hash.
174
+ `#value!` would return value for `ValidResult` and raise an error for `ErrorResult`.
175
+
176
+ `#value_or(another_value)` and `#value_or { |errors| another_value }` would return value for `ValidResult` and `another_value` for `ErrorResult`.
177
+
178
+ Errors are returned as array or hash (or hash of arrays, or array of hashes, etc., for complex data structures). Errors support internationalization (i18n) natively. Each element of the returned array shows a separate error as a special i18n value object, and each key of the returned hash corresponds to the key of the validated hash. When calling `#errors` those i18n value objects are converted to strings using the configured/detected I18n backend (Rails or `ruby-i18n`).
173
179
 
174
180
  In this README, instead of i18n values English strings are provided for brevity:
175
181
 
@@ -285,7 +291,25 @@ even_number.("test")
285
291
  # => Datacaster::ErrorResult(["is not an integer"])
286
292
  ```
287
293
 
288
- If left-hand validation of AND operator passes, *its result* (not the original value) is passed to the right-hand validation. See below in this file section on transformations where this might be relevant.
294
+ If left-hand validation of AND operator passes, *its result* (not the original value) is passed to the right-hand validation.
295
+
296
+ Alternatively, `steps` caster could be used, which accepts any number of "steps" as arguments and joins them with `&` logic:
297
+
298
+ ```ruby
299
+ even_number =
300
+ Datacaster.schema do
301
+ steps(
302
+ integer,
303
+ check { |x| x.even? },
304
+ transform { |x| x * 2 }
305
+ )
306
+ end
307
+
308
+ even_number.(6)
309
+ # => Datacaster::ValidResult(12)
310
+ ```
311
+
312
+ Naturally, if one of the "steps" returns an error, process short-circuits and this error is returned as a result.
289
313
 
290
314
  #### *OR operator*
291
315
 
@@ -764,6 +788,41 @@ Returns ValidResult if and only if the value `#responds_to?(method)`. Doesn't tr
764
788
 
765
789
  I18n keys: `error_key`, `'.responds_to'`, `'datacaster.errors.responds_to'`. Adds `reference` i18n variable, setting it to `method.to_s`.
766
790
 
791
+ #### `with(key, caster)`
792
+
793
+ Returns ValidResult if and only if value is enumerable and `caster` returns ValidResult. Transforms incoming hash, providing value described by `key` to `caster`, and putting its result back into the original hash.
794
+
795
+ ```ruby
796
+ upcase_name =
797
+ Datacaster.schema do
798
+ with(:name, transform(&:upcase))
799
+ end
800
+
801
+ upcase_name.(name: 'Josh')
802
+ # => Datacaster::ValidResult({:name=>"JOSH"})
803
+ ```
804
+
805
+ If an array is provided instead of string or Symbol for `key` argument, it is treated as array of key names for a deeply nested value:
806
+
807
+ ```ruby
808
+ upcase_person_name =
809
+ Datacaster.schema do
810
+ with([:person, :name], transform(&:upcase))
811
+ end
812
+
813
+ upcase_person_name.(person: {name: 'Josh'})
814
+ # => Datacaster::ValidResult({:person=>{:name=>"JOSH"}})
815
+
816
+ upcase_person_name.({})
817
+ # => Datacaster::ErrorResult({:person=>["is not Enumerable"]})
818
+ ```
819
+
820
+ Note that `Datacaster.absent` will be provided to `caster` if corresponding key is absent from the value.
821
+
822
+ I18n keys:
823
+
824
+ * is not enumerable – `'.must_be'`, `'datacaster.errors.must_be'`. Adds `reference` i18n variable, setting it to `"Enumerable"`.
825
+
767
826
  #### `transform_to_value(value)`
768
827
 
769
828
  Always returns ValidResult. The value is transformed to provided argument (disregarding the original value). See also [`default`](#defaultdefault_value-on-nil).
@@ -929,7 +988,7 @@ agreed_with_tos =
929
988
 
930
989
  I18n keys: `error_key`, `'.compare'`, `'datacaster.errors.compare'`. Adds `reference` i18n variable, setting it to `reference_value.to_s`.
931
990
 
932
- #### `included_in(*reference_values, error_key: nil)`
991
+ #### `included_in(reference_values, error_key: nil)`
933
992
 
934
993
  Returns ValidResult if and only if `reference_values.include?` the value.
935
994
 
@@ -973,6 +1032,12 @@ Formally, `relate(left, op, right, error_key: error_key)` will:
973
1032
  * call the `op` caster with the `[left_result, right_result]`, return the result unless it's valid
974
1033
  * return the original value as valid result
975
1034
 
1035
+ #### `run { |value| ... }`
1036
+
1037
+ Always returns ValidResult. Doesn't transform the value.
1038
+
1039
+ Useful to perform some side-effect such as raising an exception, making a log entry, etc.
1040
+
976
1041
  #### `transform { |value| ... }`
977
1042
 
978
1043
  Always returns ValidResult. Transforms the value: returns whatever the block has returned.
@@ -1,19 +1,21 @@
1
1
  module Datacaster
2
2
  class AndNode < Base
3
- def initialize(left, right)
4
- @left = left
5
- @right = right
3
+ def initialize(*casters)
4
+ @casters = casters
6
5
  end
7
6
 
8
7
  def cast(object, runtime:)
9
- left_result = @left.with_runtime(runtime).(object)
10
- return left_result unless left_result.valid?
11
-
12
- @right.with_runtime(runtime).(left_result.value)
8
+ Datacaster.ValidResult(
9
+ @casters.reduce(object) do |result, caster|
10
+ caster_result = caster.with_runtime(runtime).(result)
11
+ return caster_result unless caster_result.valid?
12
+ caster_result.value
13
+ end
14
+ )
13
15
  end
14
16
 
15
17
  def inspect
16
- "#<Datacaster::AndNode L: #{@left.inspect} R: #{@right.inspect}>"
18
+ "#<Datacaster::AndNode casters: #{@casters.inspect}>"
17
19
  end
18
20
  end
19
21
  end
@@ -0,0 +1,34 @@
1
+ module Datacaster
2
+ class AroundNode < Base
3
+ def initialize(around = nil, &block)
4
+ raise "Expected block" unless block_given?
5
+
6
+ @around = around
7
+ @run = block
8
+ end
9
+
10
+ def around(*casters)
11
+ if @around
12
+ raise ArgumentError, "only one call to .around(...) is expect, tried to call second time", caller
13
+ end
14
+
15
+ unless casters.all? { |x| Datacaster.instance?(x) }
16
+ raise ArgumentError, "provide datacaster instance to .around(...)", caller
17
+ end
18
+
19
+ caster = casters.length == 1 ? casters[0] : Datacaster::Predefined.steps(*casters)
20
+
21
+ self.class.new(caster, &@run)
22
+ end
23
+
24
+ def cast(object, runtime:)
25
+ unless @around
26
+ raise ArgumentError, "call .around(caster) beforehand", caller
27
+ end
28
+ end
29
+
30
+ def inspect
31
+ "#<#{self.class.name}>"
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,11 @@
1
+ module Datacaster
2
+ module AroundNodes
3
+ class Caster < Datacaster::AroundNode
4
+ def cast(object, runtime:)
5
+ super
6
+
7
+ Runtimes::Base.(runtime, @run, object, @around.with_runtime(runtime))
8
+ end
9
+ end
10
+ end
11
+ end
@@ -14,7 +14,7 @@ module Datacaster
14
14
  end
15
15
 
16
16
  raise TypeError.new("Either Datacaster::Result or Dry::Monads::Result " \
17
- "should be returned from cast block") unless result.is_a?(Datacaster::Result)
17
+ "should be returned from cast block, instead got #{result.inspect}") unless result.is_a?(Datacaster::Result)
18
18
 
19
19
  result
20
20
  end
@@ -0,0 +1,20 @@
1
+ module Datacaster
2
+ module ContextNodes
3
+ class ObjectContext < Datacaster::ContextNode
4
+ def initialize(base, object)
5
+ super(base)
6
+ @object = object
7
+ end
8
+
9
+ def inspect
10
+ "#<#{self.class.name}(#{@object.inspect}) base: #{@base.inspect}>"
11
+ end
12
+
13
+ private
14
+
15
+ def create_runtime(parent)
16
+ Runtimes::ObjectContext.new(parent, @object)
17
+ end
18
+ end
19
+ end
20
+ end
@@ -5,7 +5,7 @@ module Datacaster
5
5
  super(base)
6
6
 
7
7
  unless %i[fail remove pass].include?(strategy)
8
- raise ArgumentError.new("Strategy should be :fail (return error on extra keys), :remove (remove extra keys) or :pass (ignore presence of extra keys)")
8
+ raise ArgumentError.new("Strategy should be :fail (return error on extra keys), :remove (remove extra keys) or :pass (ignore presence of extra keys), instead got #{strategy.inspect}")
9
9
  end
10
10
 
11
11
  @strategy = strategy
@@ -23,7 +23,11 @@ module Datacaster
23
23
 
24
24
  def transform_result(result)
25
25
  return result unless result.valid?
26
- cast_success(result)
26
+ result = cast_success(result)
27
+ if @runtime.instance_variable_get(:@parent).respond_to?(:checked_schema=)
28
+ @runtime.instance_variable_get(:@parent).checked_schema = @runtime.checked_schema
29
+ end
30
+ result
27
31
  end
28
32
 
29
33
  def cast_success(result)
@@ -25,7 +25,7 @@ module Datacaster
25
25
  end
26
26
 
27
27
  def self.eval(&block)
28
- new.instance_exec(&block)
28
+ new.instance_eval(&block)
29
29
  end
30
30
 
31
31
  def method_missing(m, *args)
@@ -27,9 +27,9 @@ module Datacaster
27
27
  unwrapped = new_value.valid? ? new_value.value : new_value.raw_errors
28
28
 
29
29
  if key.length != unwrapped.length
30
- raise TypeError.new("When using transform_to_hash([:a, :b, :c] => validator), validator should return Array "\
30
+ raise TypeError, "When using transform_to_hash([:a, :b, :c] => validator), validator should return Array "\
31
31
  "with number of elements equal to the number of elements in left-hand-side array.\n" \
32
- "Got the following (values or errors) instead: #{keys.inspect} => #{values_or_errors.inspect}.")
32
+ "Got the following (values or errors) instead: #{key.inspect} => #{unwrapped.inspect}."
33
33
  end
34
34
  end
35
35
 
@@ -27,6 +27,10 @@ module Datacaster
27
27
  ContextNodes::UserContext.new(self, context)
28
28
  end
29
29
 
30
+ def with_object_context(object)
31
+ ContextNodes::ObjectContext.new(self, object)
32
+ end
33
+
30
34
  def call(object)
31
35
  call_with_runtime(object, Runtimes::Base.new)
32
36
  end
@@ -40,9 +44,22 @@ module Datacaster
40
44
  end
41
45
 
42
46
  def with_runtime(runtime)
43
- ->(object) do
44
- call_with_runtime(object, runtime)
47
+ result =
48
+ ->(object) do
49
+ call_with_runtime(object, runtime)
50
+ end
51
+
52
+ this = self
53
+
54
+ result.singleton_class.define_method(:with_runtime) do |new_runtime|
55
+ this.with_runtime(new_runtime)
45
56
  end
57
+
58
+ result.singleton_class.define_method(:without_runtime) do |new_runtime|
59
+ this
60
+ end
61
+
62
+ result
46
63
  end
47
64
 
48
65
  def i18n_key(*keys, **args)
@@ -16,6 +16,10 @@ module Datacaster
16
16
  Comparator.new(value, error_key)
17
17
  end
18
18
 
19
+ def run(&block)
20
+ Runner.new(&block)
21
+ end
22
+
19
23
  def transform(&block)
20
24
  Transformer.new(&block)
21
25
  end
@@ -45,6 +49,14 @@ module Datacaster
45
49
  )
46
50
  end
47
51
 
52
+ def strict_hash_schema(fields, error_key = nil)
53
+ schema(hash_schema(fields, error_key))
54
+ end
55
+
56
+ def choosy_hash_schema(fields, error_key = nil)
57
+ choosy_schema(hash_schema(fields, error_key))
58
+ end
59
+
48
60
  def transform_to_hash(fields)
49
61
  HashMapper.new(fields.transform_values { |x| DefinitionDSL.expand(x) })
50
62
  end
@@ -65,6 +77,12 @@ module Datacaster
65
77
  ContextNodes::StructureCleaner.new(base, :pass)
66
78
  end
67
79
 
80
+ # 'Around' types
81
+
82
+ def cast_around(&block)
83
+ AroundNodes::Caster.new(&block)
84
+ end
85
+
68
86
  # 'Meta' types
69
87
 
70
88
  def absent(error_key = nil, on: nil)
@@ -151,8 +169,12 @@ module Datacaster
151
169
  end
152
170
 
153
171
  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
172
+ if keys.empty?
173
+ raise RuntimeError, "pick(key, ...) accepts at least one argument", caller
174
+ end
175
+
176
+ if wrong = keys.find { |k| !Datacaster::Utils.pickable?(k) }
177
+ raise RuntimeError, "each argument should be String, Symbol, Integer or an array thereof, instead got #{wrong.inspect}", caller
156
178
  end
157
179
 
158
180
  retrieve_key = -> (from, key) do
@@ -242,6 +264,10 @@ module Datacaster
242
264
  check { |x| x.respond_to?(method) }.i18n_key(*error_keys, reference: method.to_s)
243
265
  end
244
266
 
267
+ def steps(*casters)
268
+ AndNode.new(*casters)
269
+ end
270
+
245
271
  def switch(base = nil, **on_clauses)
246
272
  switch = SwitchNode.new(base)
247
273
  on_clauses.reduce(switch) do |result, (k, v)|
@@ -253,6 +279,20 @@ module Datacaster
253
279
  transform { value }
254
280
  end
255
281
 
282
+ def with(keys, caster)
283
+ keys = Array(keys)
284
+
285
+ unless Datacaster::Utils.pickable?(keys)
286
+ raise RuntimeError, "provide String, Symbol, Integer or an array thereof instead of #{keys.inspect}", caller
287
+ end
288
+
289
+ if keys.length == 1
290
+ return transform_to_hash(keys[0] => pick(keys[0]) & caster)
291
+ end
292
+
293
+ with(keys[0], must_be(Enumerable) & with(keys[1..-1], caster))
294
+ end
295
+
256
296
  # Strict types
257
297
 
258
298
  def decimal(digits = 8, error_key = nil)
@@ -290,7 +330,7 @@ module Datacaster
290
330
  hash_value(error_key) & transform { |x| x.symbolize_keys }
291
331
  end
292
332
 
293
- def included_in(*values, error_key: nil)
333
+ def included_in(values, error_key: nil)
294
334
  error_keys = ['.included_in', 'datacaster.errors.included_in']
295
335
  error_keys.unshift(error_key) if error_key
296
336
  check { |x| values.include?(x) }.i18n_key(*error_keys, reference: values.map(&:to_s).join(', '))
@@ -9,6 +9,22 @@ module Datacaster
9
9
  @valid = !!valid
10
10
  end
11
11
 
12
+ def value_or(*values, &block)
13
+ if values.length > 1 || (values.length == 1 && block_given?)
14
+ raise RuntimeError, "provide either value or block: #or(value), #or { block }", caller
15
+ end
16
+
17
+ if valid?
18
+ value
19
+ else
20
+ if values.length == 1
21
+ values[0]
22
+ else
23
+ block.(errors)
24
+ end
25
+ end
26
+ end
27
+
12
28
  def valid?
13
29
  @valid
14
30
  end
@@ -0,0 +1,18 @@
1
+ module Datacaster
2
+ class Runner < Base
3
+ def initialize(&block)
4
+ raise "Expected block" unless block_given?
5
+
6
+ @run = block
7
+ end
8
+
9
+ def cast(object, runtime:)
10
+ Runtimes::Base.(runtime, @run, object)
11
+ Datacaster.ValidResult(object)
12
+ end
13
+
14
+ def inspect
15
+ "#<Datacaster::Runner>"
16
+ end
17
+ end
18
+ end
@@ -1,8 +1,15 @@
1
+ require 'set'
2
+
1
3
  module Datacaster
2
4
  module Runtimes
3
5
  class Base
6
+ attr_reader :reserved_instance_variables
7
+
4
8
  def self.call(r, proc, *args)
5
- r.instance_exec(*args, &proc)
9
+ r.before_call!(r)
10
+ result = r.instance_exec(*args, &proc)
11
+ r.after_call!(r)
12
+ result
6
13
  end
7
14
 
8
15
  def self.send_to_parent(r, m, *args, &block)
@@ -17,6 +24,10 @@ module Datacaster
17
24
 
18
25
  def initialize(parent = nil)
19
26
  @parent = parent
27
+
28
+ # We won't be setting any instance variables outside this
29
+ # constructor, so we can proxy all the rest to the @object
30
+ @reserved_instance_variables = Set.new(instance_variables + [:@reserved_instance_variables])
20
31
  end
21
32
 
22
33
  def method_missing(m, *args, &block)
@@ -27,6 +38,14 @@ module Datacaster
27
38
  !@parent.nil? && @parent.respond_to?(m, include_private)
28
39
  end
29
40
 
41
+ def after_call!(sender)
42
+ @parent.after_call!(sender) if @parent
43
+ end
44
+
45
+ def before_call!(sender)
46
+ @parent.before_call!(sender) if @parent
47
+ end
48
+
30
49
  def inspect
31
50
  "#<#{self.class.name} parent: #{@parent.inspect}>"
32
51
  end
@@ -6,6 +6,8 @@ module Datacaster
6
6
  def initialize(*)
7
7
  super
8
8
  @args = {}
9
+
10
+ @reserved_instance_variables += instance_variables
9
11
  end
10
12
 
11
13
  def i18n_var!(name, value)
@@ -0,0 +1,41 @@
1
+ require 'set'
2
+
3
+ module Datacaster
4
+ module Runtimes
5
+ class ObjectContext < Base
6
+ def initialize(parent, object)
7
+ super(parent)
8
+ @object = object
9
+
10
+ @reserved_instance_variables += instance_variables
11
+ end
12
+
13
+ def method_missing(m, *args, &block)
14
+ if @object.respond_to?(m)
15
+ @object.public_send(m, *args, &block)
16
+ else
17
+ super
18
+ end
19
+ end
20
+
21
+ def respond_to_missing?(m, include_private = false)
22
+ @object.respond_to?(m, include_private) || super
23
+ end
24
+
25
+ def after_call!(sender)
26
+ (sender.instance_variables.to_set - sender.reserved_instance_variables).each do |k|
27
+ @object.instance_variable_set(k, sender.instance_variable_get(k))
28
+ sender.remove_instance_variable(k)
29
+ end
30
+ super
31
+ end
32
+
33
+ def before_call!(sender)
34
+ super
35
+ (@object.instance_variables.to_set - sender.reserved_instance_variables).each do |k|
36
+ sender.instance_variable_set(k, @object.instance_variable_get(k))
37
+ end
38
+ end
39
+ end
40
+ end
41
+ end
@@ -1,13 +1,16 @@
1
1
  module Datacaster
2
2
  module Runtimes
3
3
  class StructureCleaner < Base
4
- attr_reader :checked_schema
4
+ attr_accessor :checked_schema
5
5
 
6
6
  def initialize(*)
7
7
  super
8
+ @ignore = false
8
9
  @checked_schema = {}
9
10
  @should_check_stack = [false]
10
11
  @pointer_stack = [@checked_schema]
12
+
13
+ @reserved_instance_variables += instance_variables
11
14
  end
12
15
 
13
16
  # Array checked schema are the same as hash one, where
@@ -29,6 +29,8 @@ module Datacaster
29
29
  def initialize(parent, user_context)
30
30
  super(parent)
31
31
  @context_struct = ContextStruct.new(user_context, self)
32
+
33
+ @reserved_instance_variables += instance_variables
32
34
  end
33
35
 
34
36
  def context
@@ -4,11 +4,11 @@ module Datacaster
4
4
  @base = base
5
5
 
6
6
  if Datacaster::Utils.pickable?(@base)
7
- @base = Datacaster::Predefined.pick(@base)
7
+ @base = Datacaster::Predefined.run { checked_key!(base) } &
8
+ Datacaster::Predefined.pick(base)
8
9
  end
9
10
 
10
11
  if !@base.nil? && !Datacaster.instance?(@base)
11
- puts @base.inspect
12
12
  raise RuntimeError, "provide a Datacaster::Base instance, a hash key, or an array of keys to switch(...) caster", caller
13
13
  end
14
14
 
@@ -0,0 +1,115 @@
1
+ module Datacaster
2
+ module Transaction
3
+ class StepErrorResult < RuntimeError
4
+ attr_accessor :result
5
+
6
+ def initialize(result)
7
+ super(result.inspect)
8
+ @result = result
9
+ end
10
+ end
11
+
12
+ module ClassMethods
13
+ def _caster
14
+ if @_block
15
+ @_caster = ContextNodes::StructureCleaner.new(@_block.(), @_strategy)
16
+ @_block = nil
17
+ @_strategy = nil
18
+ end
19
+ @_caster
20
+ end
21
+
22
+ def _perform(caster, strategy, &block)
23
+ if [caster, block].count(nil) != 1
24
+ raise RuntimeError, "provide either a caster as single argument, or just a block to `perform(...)` or `perform_*(...)` call", caller
25
+ end
26
+
27
+ if block
28
+ @_block = block
29
+ @_strategy = strategy
30
+ @_caster = nil
31
+ else
32
+ @_block = nil
33
+ @_strategy = nil
34
+ @_caster = ContextNodes::StructureCleaner.new(caster, strategy)
35
+ end
36
+ end
37
+
38
+ def perform(caster = nil, &block)
39
+ _perform(caster, :fail, &block)
40
+ end
41
+
42
+ def perform_partial(caster = nil, &block)
43
+ _perform(caster, :pass, &block)
44
+ end
45
+
46
+ def perform_choosy(caster = nil, &block)
47
+ _perform(caster, :remove, &block)
48
+ end
49
+
50
+ def define_steps(&block)
51
+ instance_eval(&block)
52
+ end
53
+
54
+ def method_missing(m, *args, **kwargs)
55
+ return super unless args.empty? && kwargs.empty?
56
+ return super unless method_defined?(m)
57
+ method = instance_method(m)
58
+ return super unless method.arity == 1
59
+
60
+ # convert immediate class method call to lazy instance method call
61
+ ->(*args, **kwargs) { send(m, *args, **kwargs) }
62
+ end
63
+
64
+ def call(*args, **kwargs)
65
+ new.call(*args, **kwargs)
66
+ end
67
+ end
68
+
69
+ def self.included(base)
70
+ base.extend Datacaster::Predefined
71
+ base.extend ClassMethods
72
+ base.include Datacaster::Mixin
73
+ end
74
+
75
+ def cast(object, runtime:)
76
+ if respond_to?(:perform)
77
+ @runtime = runtime
78
+ begin
79
+ result = perform(object)
80
+ rescue StepErrorResult => e
81
+ return e.result
82
+ end
83
+ @runtime = nil
84
+ return result
85
+ end
86
+
87
+ caster = self.class._caster
88
+ unless caster
89
+ raise RuntimeError, "define #perform (#perform_partial, #perform_choosy) method " \
90
+ "or call .perform(caster) or .perform { caster } beforehand", caller
91
+ end
92
+ caster.
93
+ with_object_context(self).
94
+ with_runtime(runtime).
95
+ (object)
96
+ end
97
+
98
+ def step(arg = nil, &block)
99
+ result = Datacaster::Predefined.cast(&block).
100
+ with_object_context(self).
101
+ with_runtime(@runtime).
102
+ (arg)
103
+ end
104
+
105
+ def step!(arg = nil, &block)
106
+ result = step(arg, &block)
107
+
108
+ if result.valid?
109
+ result.value
110
+ else
111
+ raise StepErrorResult.new(result)
112
+ end
113
+ end
114
+ end
115
+ end
@@ -1,3 +1,3 @@
1
1
  module Datacaster
2
- VERSION = "3.1.5"
2
+ VERSION = "3.2.1"
3
3
  end
data/transaction.md ADDED
@@ -0,0 +1,123 @@
1
+ # Datacaster transaction
2
+
3
+ As an experimental feature a "transaction" is available: datacaster defined as a class.
4
+
5
+ ## Example
6
+
7
+ ```ruby
8
+ require 'datacaster'
9
+
10
+ class UserRegistration
11
+ include Datacaster::Transaction
12
+
13
+ perform do
14
+ steps(
15
+ transform(&prepare),
16
+ typecast,
17
+ with(:email, transform(&send_email))
18
+ )
19
+ end
20
+
21
+ define_steps do
22
+ def typecast = hash_schema(name: string, email: string)
23
+ end
24
+
25
+ def initialize(user_id = 123)
26
+ @user_id = user_id
27
+ end
28
+
29
+ def prepare(x)
30
+ @user_id ||= 123
31
+ x.to_h
32
+ end
33
+
34
+ def send_email(email)
35
+ {address: email, sent: true, id: @user_id}
36
+ end
37
+ end
38
+
39
+ UserRegistration.(name: 'John', email: 'john@example.org')
40
+ # => Datacaster::ValidResult({:name=>"John", :email=>{:address=>"john@example.org", :result=>true, :id=>123}})
41
+ ```
42
+
43
+ ## Structure
44
+
45
+ Transaction is just a class which includes `Datacaster::Transaction`. Upon inclusion, all datacaster predefined methods are added as class methods.
46
+
47
+ Call `.perform(a_caster)` or `.perform { a_caster }` (where `caster` is a Datacaster instance) to define transaction steps.
48
+
49
+ Block form `perform { a_caster }` is used to defer definition of class methods (otherwise, they should've been written above `perform` in the code file). Block is eventually executed in a context of the class.
50
+
51
+ Transaction instance will behave as a normal datacaster (i.e. `a_caster` itself) with the following enhancements:
52
+
53
+ 1\. Transaction class has `.call` method which will initialize instance (available only if `#initialize` doesn't have required arguments) and pass arguments to the instance's `#call`.
54
+
55
+ 2\. Runtime-context for casters used in a transaction is the transaction instance itself. You can call transaction instance methods and get/set transaction instance variables inside blocks of `check { ... }`, `cast { ... }` and all the other predefined datacaster methods. That's why `@user_id` works in the example above.
56
+
57
+ 3\. Convenience method `define_steps` is added, which is just a better looking `class << self`.
58
+
59
+ 4\. If class method is not found, it is automatically converted (with class `method_missing`) to deferred instance method call. In the example above, `.prepare` class method is not defined. However, `perform` block executes in a class context and tries to look that method up. Instead, proc `->(value) { self.perfrom(value) }` is returned – a deferred instance method call (which is passed as block to standard `transform` datacaster).
60
+
61
+ Note that `steps` is a predefined Datacaster method (which works as `&`), and so is `transform` and `with`. They are not Transaction-specific enhancements.
62
+
63
+ ## Around steps with `cast_around`
64
+
65
+ An experimental addition to Datacaster convenient for the use in Transaction is `cast_around` – a way to wrap a number of steps inside some kind of setup/rollback block, e.g. a database transaction.
66
+
67
+ ```ruby
68
+ class UserRegistration
69
+ include Datacaster::Transaction
70
+
71
+ perform do
72
+ steps(
73
+ run { prepare },
74
+ inside_transaction.around(
75
+ run { register },
76
+ run { create_account }
77
+ ),
78
+ run { log }
79
+ )
80
+ end
81
+
82
+ define_steps do
83
+ def inside_transaction = cast_around do |value, inner|
84
+ puts "DB transaction started"
85
+ result = inner.(value)
86
+ puts "DB transaction ended"
87
+
88
+ result
89
+ end
90
+ end
91
+
92
+ def prepare
93
+ puts "Preparing"
94
+ end
95
+
96
+ def register
97
+ puts "User is registered"
98
+ end
99
+
100
+ def create_account
101
+ puts "Account has been created"
102
+ end
103
+
104
+ def log
105
+ puts "Creating log entry"
106
+ end
107
+ end
108
+
109
+ UserRegistration.('a user object')
110
+ # Preparing
111
+ # DB transaction started
112
+ # User is registered
113
+ # Account has been created
114
+ # DB transaction ended
115
+ # Creating log entry
116
+ # => #<Datacaster::ValidResult("a user object")>
117
+ ```
118
+
119
+ As shown in the example, `cast_around { |value, inner| ...}.around(*casters)` works in the following manner: it yields incoming value as the first argument (`value`) and casters specified in `.around(...)` part as the second argument (`inner`) to the block given. Casters are automatically joined with `steps` if there are more than one.
120
+
121
+ Block may call `steps.(value)` to execute casters in a regular manner. Block must return a kind of `Datacaster::Result`. `steps.(...)` will always return `Datacaster::Result`, so that result could be passed as a `cast_around` result, as shown in the example.
122
+
123
+ Note that `run` is a predefined Datacaster method, not specific to Transaction.
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.5
4
+ version: 3.2.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Eugene Zolotarev
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2023-10-07 00:00:00.000000000 Z
11
+ date: 2023-10-09 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activemodel
@@ -130,6 +130,8 @@ files:
130
130
  - lib/datacaster/absent.rb
131
131
  - lib/datacaster/and_node.rb
132
132
  - lib/datacaster/and_with_error_aggregation_node.rb
133
+ - lib/datacaster/around_node.rb
134
+ - lib/datacaster/around_nodes/caster.rb
133
135
  - lib/datacaster/array_schema.rb
134
136
  - lib/datacaster/base.rb
135
137
  - lib/datacaster/caster.rb
@@ -140,6 +142,7 @@ files:
140
142
  - lib/datacaster/context_nodes/errors_caster.rb
141
143
  - lib/datacaster/context_nodes/i18n.rb
142
144
  - lib/datacaster/context_nodes/i18n_keys_mapper.rb
145
+ - lib/datacaster/context_nodes/object_context.rb
143
146
  - lib/datacaster/context_nodes/pass_if.rb
144
147
  - lib/datacaster/context_nodes/structure_cleaner.rb
145
148
  - lib/datacaster/context_nodes/user_context.rb
@@ -154,18 +157,22 @@ files:
154
157
  - lib/datacaster/or_node.rb
155
158
  - lib/datacaster/predefined.rb
156
159
  - lib/datacaster/result.rb
160
+ - lib/datacaster/runner.rb
157
161
  - lib/datacaster/runtimes/base.rb
158
162
  - lib/datacaster/runtimes/i18n.rb
163
+ - lib/datacaster/runtimes/object_context.rb
159
164
  - lib/datacaster/runtimes/structure_cleaner.rb
160
165
  - lib/datacaster/runtimes/user_context.rb
161
166
  - lib/datacaster/substitute_i18n.rb
162
167
  - lib/datacaster/switch_node.rb
163
168
  - lib/datacaster/then_node.rb
169
+ - lib/datacaster/transaction.rb
164
170
  - lib/datacaster/transformer.rb
165
171
  - lib/datacaster/trier.rb
166
172
  - lib/datacaster/utils.rb
167
173
  - lib/datacaster/validator.rb
168
174
  - lib/datacaster/version.rb
175
+ - transaction.md
169
176
  homepage: https://github.com/EugZol/datacaster
170
177
  licenses:
171
178
  - MIT