datacaster 3.1.3 → 3.2.0

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: d7cd7191206c373ab38c5516505bf38cbbdc76a7b7c433f976498accda34b054
4
- data.tar.gz: 0443edf8c8f494caeacfb55ba902cce19210a53cf9f726e8306f7e487de58fd8
3
+ metadata.gz: e213cf1bdf261dd4b20b32c5305ace7a0e6c8efac93108136b8ded182e7ff2c3
4
+ data.tar.gz: ee5776b8f888400a01c9463677690aa69e1c45ae497f3d7d6bae0c714e19a191
5
5
  SHA512:
6
- metadata.gz: 2f00d6cd8c2aae72cba3723dbea6b1c6cb9d760309e9fefa7897e5da691123459496348045849f47700f241df668e505120ba1d2e2f54c93a033e72b222dc3b9
7
- data.tar.gz: 15c5d76c7cc7a16be05890bec3bcf549c0a5d103e9ba8bb42e2743c834e874bc1296fa5daa3af4feff93107d5acd8db2bd3b2da103f1c29aca1a169b86883c24
6
+ metadata.gz: 27b618c2662a249dbee5476a6888a581e5cef2a1de2b038357c99c552ca118667776bd69768607b3440237061b6d364bf07104ae697d943a22c3209192f83f69
7
+ data.tar.gz: 11895a503d2e7f9980e41067b2fd4d7d51ba3d2eaa2347143d25e44c6c2fc5601fd2d121cf17bb0f3104b0097dae695b0ce20084efb07c5b7a6e3e605b1000ec
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)
@@ -59,6 +60,7 @@ It is currently used in production in several projects (mainly as request parame
59
60
  - [`compare(reference_value, error_key = nil)`](#comparereference_value-error_key--nil)
60
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
 
@@ -415,6 +439,7 @@ Notice that shortcut definitions are available (illustrated in the example above
415
439
 
416
440
  * `switch(:key)` is exactly the same as `switch(pick(:key))` (works for a string, a symbol, or an array thereof)
417
441
  * `on(:key, ...)` is exactly the same as `on(compare(:key), ...)` (works for a string or a symbol)
442
+ * `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)`)
418
443
  * `switch([caster], on_check => on_caster, ...)` is exactly the same as `switch([caster]).on(on_check, on_caster).on(...)`
419
444
 
420
445
  `switch()` without a `base` argument will pass the incoming value to the `.on(...)` casters.
@@ -763,6 +788,41 @@ Returns ValidResult if and only if the value `#responds_to?(method)`. Doesn't tr
763
788
 
764
789
  I18n keys: `error_key`, `'.responds_to'`, `'datacaster.errors.responds_to'`. Adds `reference` i18n variable, setting it to `method.to_s`.
765
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
+
766
826
  #### `transform_to_value(value)`
767
827
 
768
828
  Always returns ValidResult. The value is transformed to provided argument (disregarding the original value). See also [`default`](#defaultdefault_value-on-nil).
@@ -972,6 +1032,12 @@ Formally, `relate(left, op, right, error_key: error_key)` will:
972
1032
  * call the `op` caster with the `[left_result, right_result]`, return the result unless it's valid
973
1033
  * return the original value as valid result
974
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
+
975
1041
  #### `transform { |value| ... }`
976
1042
 
977
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
@@ -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
@@ -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
@@ -54,15 +66,21 @@ module Datacaster
54
66
  end
55
67
 
56
68
  def schema(base)
57
- ContextNodes::StructureCleaner.new(base, strategy: :fail)
69
+ ContextNodes::StructureCleaner.new(base, :fail)
58
70
  end
59
71
 
60
72
  def choosy_schema(base)
61
- ContextNodes::StructureCleaner.new(base, strategy: :remove)
73
+ ContextNodes::StructureCleaner.new(base, :remove)
62
74
  end
63
75
 
64
76
  def partial_schema(base)
65
- ContextNodes::StructureCleaner.new(base, strategy: :pass)
77
+ ContextNodes::StructureCleaner.new(base, :pass)
78
+ end
79
+
80
+ # 'Around' types
81
+
82
+ def cast_around(&block)
83
+ AroundNodes::Caster.new(&block)
66
84
  end
67
85
 
68
86
  # 'Meta' types
@@ -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)
@@ -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
@@ -8,7 +8,6 @@ module Datacaster
8
8
  end
9
9
 
10
10
  if !@base.nil? && !Datacaster.instance?(@base)
11
- puts @base.inspect
12
11
  raise RuntimeError, "provide a Datacaster::Base instance, a hash key, or an array of keys to switch(...) caster", caller
13
12
  end
14
13
 
@@ -16,11 +15,18 @@ module Datacaster
16
15
  @else = else_caster
17
16
  end
18
17
 
19
- def on(caster_or_value, clause)
18
+ def on(caster_or_value, clause, strict: false)
20
19
  caster =
21
20
  case caster_or_value
22
21
  when Datacaster::Base
23
22
  caster_or_value
23
+ when String, Symbol
24
+ if strict
25
+ Datacaster::Predefined.compare(caster_or_value)
26
+ else
27
+ Datacaster::Predefined.compare(caster_or_value.to_s) |
28
+ Datacaster::Predefined.compare(caster_or_value.to_sym)
29
+ end
24
30
  else
25
31
  Datacaster::Predefined.compare(caster_or_value)
26
32
  end
@@ -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.3"
2
+ VERSION = "3.2.0"
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.3
4
+ version: 3.2.0
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-06 00:00:00.000000000 Z
11
+ date: 2023-10-08 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