datacaster 3.1.5 → 3.2.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.md +69 -4
- data/lib/datacaster/and_node.rb +10 -8
- data/lib/datacaster/around_node.rb +34 -0
- data/lib/datacaster/around_nodes/caster.rb +11 -0
- data/lib/datacaster/caster.rb +1 -1
- data/lib/datacaster/context_nodes/object_context.rb +20 -0
- data/lib/datacaster/context_nodes/structure_cleaner.rb +6 -2
- data/lib/datacaster/definition_dsl.rb +1 -1
- data/lib/datacaster/hash_mapper.rb +2 -2
- data/lib/datacaster/mixin.rb +19 -2
- data/lib/datacaster/predefined.rb +42 -2
- data/lib/datacaster/result.rb +16 -0
- data/lib/datacaster/runner.rb +18 -0
- data/lib/datacaster/runtimes/base.rb +20 -1
- data/lib/datacaster/runtimes/i18n.rb +2 -0
- data/lib/datacaster/runtimes/object_context.rb +41 -0
- data/lib/datacaster/runtimes/structure_cleaner.rb +4 -1
- data/lib/datacaster/runtimes/user_context.rb +2 -0
- data/lib/datacaster/switch_node.rb +0 -1
- data/lib/datacaster/transaction.rb +115 -0
- data/lib/datacaster/version.rb +1 -1
- data/transaction.md +123 -0
- metadata +9 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: e213cf1bdf261dd4b20b32c5305ace7a0e6c8efac93108136b8ded182e7ff2c3
|
4
|
+
data.tar.gz: ee5776b8f888400a01c9463677690aa69e1c45ae497f3d7d6bae0c714e19a191
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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
|
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`.
|
172
|
+
`#value` and `#errors` would return `#nil` if the result is, correspondingly, `ErrorResult` and `ValidResult`.
|
171
173
|
|
172
|
-
|
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.
|
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).
|
@@ -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.
|
data/lib/datacaster/and_node.rb
CHANGED
@@ -1,19 +1,21 @@
|
|
1
1
|
module Datacaster
|
2
2
|
class AndNode < Base
|
3
|
-
def initialize(
|
4
|
-
@
|
5
|
-
@right = right
|
3
|
+
def initialize(*casters)
|
4
|
+
@casters = casters
|
6
5
|
end
|
7
6
|
|
8
7
|
def cast(object, runtime:)
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
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
|
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
|
data/lib/datacaster/caster.rb
CHANGED
@@ -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)
|
@@ -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
|
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: #{
|
32
|
+
"Got the following (values or errors) instead: #{key.inspect} => #{unwrapped.inspect}."
|
33
33
|
end
|
34
34
|
end
|
35
35
|
|
data/lib/datacaster/mixin.rb
CHANGED
@@ -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
|
-
|
44
|
-
|
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?
|
155
|
-
raise RuntimeError, "
|
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)
|
data/lib/datacaster/result.rb
CHANGED
@@ -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.
|
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
|
@@ -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
|
-
|
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
|
@@ -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
|
data/lib/datacaster/version.rb
CHANGED
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.
|
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-
|
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
|