dry-effects 0.1.0.alpha
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/.codeclimate.yml +15 -0
- data/.gitignore +12 -0
- data/.rspec +3 -0
- data/.rubocop.yml +73 -0
- data/.travis.yml +31 -0
- data/CHANGELOG.md +3 -0
- data/CONTRIBUTING.md +29 -0
- data/Gemfile +22 -0
- data/LICENSE +21 -0
- data/README.md +20 -0
- data/Rakefile +8 -0
- data/dry-effects.gemspec +48 -0
- data/examples/amb.rb +51 -0
- data/examples/state.rb +29 -0
- data/lib/dry/effects.rb +42 -0
- data/lib/dry/effects/all.rb +47 -0
- data/lib/dry/effects/constructors.rb +8 -0
- data/lib/dry/effects/container.rb +11 -0
- data/lib/dry/effects/effect.rb +29 -0
- data/lib/dry/effects/effects/amb.rb +23 -0
- data/lib/dry/effects/effects/async.rb +22 -0
- data/lib/dry/effects/effects/cache.rb +67 -0
- data/lib/dry/effects/effects/current_time.rb +27 -0
- data/lib/dry/effects/effects/defer.rb +31 -0
- data/lib/dry/effects/effects/env.rb +31 -0
- data/lib/dry/effects/effects/fork.rb +21 -0
- data/lib/dry/effects/effects/implicit.rb +25 -0
- data/lib/dry/effects/effects/interrupt.rb +29 -0
- data/lib/dry/effects/effects/lock.rb +45 -0
- data/lib/dry/effects/effects/parallel.rb +19 -0
- data/lib/dry/effects/effects/random.rb +19 -0
- data/lib/dry/effects/effects/reader.rb +15 -0
- data/lib/dry/effects/effects/resolve.rb +26 -0
- data/lib/dry/effects/effects/retry.rb +26 -0
- data/lib/dry/effects/effects/state.rb +50 -0
- data/lib/dry/effects/errors.rb +68 -0
- data/lib/dry/effects/extensions.rb +13 -0
- data/lib/dry/effects/extensions/auto_inject.rb +67 -0
- data/lib/dry/effects/extensions/system.rb +43 -0
- data/lib/dry/effects/halt.rb +29 -0
- data/lib/dry/effects/handler.rb +58 -0
- data/lib/dry/effects/inflector.rb +9 -0
- data/lib/dry/effects/initializer.rb +99 -0
- data/lib/dry/effects/instruction.rb +8 -0
- data/lib/dry/effects/instructions/execute.rb +25 -0
- data/lib/dry/effects/instructions/raise.rb +25 -0
- data/lib/dry/effects/provider.rb +29 -0
- data/lib/dry/effects/provider/class_interface.rb +61 -0
- data/lib/dry/effects/providers/amb.rb +36 -0
- data/lib/dry/effects/providers/async.rb +31 -0
- data/lib/dry/effects/providers/cache.rb +43 -0
- data/lib/dry/effects/providers/current_time.rb +49 -0
- data/lib/dry/effects/providers/defer.rb +84 -0
- data/lib/dry/effects/providers/env.rb +65 -0
- data/lib/dry/effects/providers/fork.rb +23 -0
- data/lib/dry/effects/providers/implicit.rb +39 -0
- data/lib/dry/effects/providers/interrupt.rb +37 -0
- data/lib/dry/effects/providers/lock.rb +125 -0
- data/lib/dry/effects/providers/parallel.rb +34 -0
- data/lib/dry/effects/providers/random.rb +13 -0
- data/lib/dry/effects/providers/reader.rb +61 -0
- data/lib/dry/effects/providers/resolve.rb +88 -0
- data/lib/dry/effects/providers/retry.rb +59 -0
- data/lib/dry/effects/providers/state.rb +30 -0
- data/lib/dry/effects/stack.rb +67 -0
- data/lib/dry/effects/version.rb +7 -0
- metadata +263 -0
@@ -0,0 +1,23 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'dry/effects/provider'
|
4
|
+
|
5
|
+
module Dry
|
6
|
+
module Effects
|
7
|
+
module Providers
|
8
|
+
class Fork < Provider[:fork]
|
9
|
+
attr_reader :stack
|
10
|
+
|
11
|
+
def fork
|
12
|
+
stack = self.stack.dup
|
13
|
+
-> &cont { Handler.spawn_fiber(stack.dup, &cont) }
|
14
|
+
end
|
15
|
+
|
16
|
+
def call(stack)
|
17
|
+
@stack = stack
|
18
|
+
super
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
@@ -0,0 +1,39 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'dry/effects/provider'
|
4
|
+
|
5
|
+
module Dry
|
6
|
+
module Effects
|
7
|
+
module Providers
|
8
|
+
class Implicit < Provider[:implicit]
|
9
|
+
include Dry::Equalizer(:name, :static, :dictionary)
|
10
|
+
|
11
|
+
param :dependency
|
12
|
+
|
13
|
+
param :static, default: -> { EMPTY_HASH }
|
14
|
+
|
15
|
+
attr_reader :dictionary
|
16
|
+
|
17
|
+
def implicit(arg)
|
18
|
+
dictionary.fetch(arg.class)
|
19
|
+
end
|
20
|
+
|
21
|
+
def call(stack, dynamic = EMPTY_HASH)
|
22
|
+
if dynamic.empty?
|
23
|
+
@dictionary = static
|
24
|
+
else
|
25
|
+
@dictionary = static.merge(dynamic)
|
26
|
+
end
|
27
|
+
|
28
|
+
super(stack)
|
29
|
+
end
|
30
|
+
|
31
|
+
def provide?(effect)
|
32
|
+
super &&
|
33
|
+
dependency.equal?(effect.dependency) &&
|
34
|
+
dictionary.key?(effect.payload[0].class)
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
@@ -0,0 +1,37 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'dry/effects/provider'
|
4
|
+
require 'dry/effects/instructions/raise'
|
5
|
+
require 'dry/effects/halt'
|
6
|
+
|
7
|
+
module Dry
|
8
|
+
module Effects
|
9
|
+
module Providers
|
10
|
+
class Interrupt < Provider[:interrupt]
|
11
|
+
param :scope, default: -> { :default }
|
12
|
+
|
13
|
+
def interrupt(*payload)
|
14
|
+
Instructions.Raise(halt.new(payload))
|
15
|
+
end
|
16
|
+
|
17
|
+
def call(_stack)
|
18
|
+
yield
|
19
|
+
rescue halt => e
|
20
|
+
e.payload[0]
|
21
|
+
end
|
22
|
+
|
23
|
+
def halt
|
24
|
+
Halt[scope]
|
25
|
+
end
|
26
|
+
|
27
|
+
def represent
|
28
|
+
"interrupt[#{scope}]"
|
29
|
+
end
|
30
|
+
|
31
|
+
def provide?(effect)
|
32
|
+
super && scope.equal?(effect.scope)
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
@@ -0,0 +1,125 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'dry/equalizer'
|
4
|
+
require 'dry/effects/provider'
|
5
|
+
require 'dry/effects/initializer'
|
6
|
+
|
7
|
+
module Dry
|
8
|
+
module Effects
|
9
|
+
module Providers
|
10
|
+
class Lock < Provider[:lock]
|
11
|
+
class Handle
|
12
|
+
include ::Dry::Equalizer(:key)
|
13
|
+
|
14
|
+
extend Initializer
|
15
|
+
|
16
|
+
param :key
|
17
|
+
|
18
|
+
param :meta
|
19
|
+
end
|
20
|
+
|
21
|
+
class Backend
|
22
|
+
extend Initializer
|
23
|
+
|
24
|
+
param :locks, default: -> { ::Hash.new }
|
25
|
+
|
26
|
+
param :mutex, default: -> { ::Mutex.new }
|
27
|
+
|
28
|
+
def lock(key, meta)
|
29
|
+
mutex.synchronize do
|
30
|
+
if locked?(key)
|
31
|
+
nil
|
32
|
+
else
|
33
|
+
locks[key] = Handle.new(key, meta)
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
def locked?(key)
|
39
|
+
locks.key?(key)
|
40
|
+
end
|
41
|
+
|
42
|
+
def unlock(handle)
|
43
|
+
mutex.synchronize do
|
44
|
+
if locked?(handle.key)
|
45
|
+
locks.delete(handle.key)
|
46
|
+
true
|
47
|
+
else
|
48
|
+
false
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
def meta(key)
|
54
|
+
meta = Undefined.map(locks.fetch(key, Undefined), &:meta)
|
55
|
+
Undefined.default(meta, nil)
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
Locate = Effect.new(type: :lock, name: :locate)
|
60
|
+
|
61
|
+
option :backend, default: -> { Backend.new }
|
62
|
+
|
63
|
+
def lock(key, meta = Undefined)
|
64
|
+
locked = backend.lock(key, meta)
|
65
|
+
owned << locked if locked
|
66
|
+
locked
|
67
|
+
end
|
68
|
+
|
69
|
+
def locked?(key)
|
70
|
+
backend.locked?(key)
|
71
|
+
end
|
72
|
+
|
73
|
+
def unlock(handle)
|
74
|
+
backend.unlock(handle)
|
75
|
+
end
|
76
|
+
|
77
|
+
def meta(key)
|
78
|
+
backend.meta(key)
|
79
|
+
end
|
80
|
+
|
81
|
+
def locate
|
82
|
+
self
|
83
|
+
end
|
84
|
+
|
85
|
+
def call(stack, backend = Undefined)
|
86
|
+
backend_replace = Undefined.default(backend) do
|
87
|
+
parent = ::Dry::Effects.yield(Locate) { Undefined }
|
88
|
+
Undefined.map(parent, &:backend)
|
89
|
+
end
|
90
|
+
|
91
|
+
with_backend(backend_replace) do
|
92
|
+
super(stack)
|
93
|
+
ensure
|
94
|
+
owned.each { |handle| unlock(handle) }
|
95
|
+
end
|
96
|
+
end
|
97
|
+
|
98
|
+
def with_backend(backend)
|
99
|
+
if Undefined.equal?(backend)
|
100
|
+
yield
|
101
|
+
else
|
102
|
+
begin
|
103
|
+
before, @backend = @backend, backend
|
104
|
+
yield
|
105
|
+
ensure
|
106
|
+
@backend = before
|
107
|
+
end
|
108
|
+
end
|
109
|
+
end
|
110
|
+
|
111
|
+
def owned
|
112
|
+
@owned ||= []
|
113
|
+
end
|
114
|
+
|
115
|
+
def represent
|
116
|
+
if owned.empty?
|
117
|
+
super
|
118
|
+
else
|
119
|
+
"lock[owned=#{owned.size}]"
|
120
|
+
end
|
121
|
+
end
|
122
|
+
end
|
123
|
+
end
|
124
|
+
end
|
125
|
+
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'concurrent/promise'
|
4
|
+
require 'dry/effects/provider'
|
5
|
+
|
6
|
+
module Dry
|
7
|
+
module Effects
|
8
|
+
module Providers
|
9
|
+
class Parallel < Provider[:parallel]
|
10
|
+
option :executor, default: -> { :io }
|
11
|
+
|
12
|
+
attr_reader :stack
|
13
|
+
|
14
|
+
def par
|
15
|
+
stack = self.stack.dup
|
16
|
+
proc do |&block|
|
17
|
+
::Concurrent::Promise.execute(executor: executor) do
|
18
|
+
Handler.spawn_fiber(stack, &block)
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
def join(xs)
|
24
|
+
xs.map(&:value!)
|
25
|
+
end
|
26
|
+
|
27
|
+
def call(stack)
|
28
|
+
@stack = stack
|
29
|
+
super
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
@@ -0,0 +1,61 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'dry/effects/provider'
|
4
|
+
|
5
|
+
module Dry
|
6
|
+
module Effects
|
7
|
+
module Providers
|
8
|
+
class Reader < Provider[:reader]
|
9
|
+
def self.handle_method(scope, as: Undefined, **)
|
10
|
+
Undefined.default(as) { :"with_#{scope}" }
|
11
|
+
end
|
12
|
+
|
13
|
+
Any = Object.new.tap { |any|
|
14
|
+
def any.===(_)
|
15
|
+
true
|
16
|
+
end
|
17
|
+
}.freeze
|
18
|
+
|
19
|
+
include Dry::Equalizer(:scope, :state)
|
20
|
+
|
21
|
+
attr_reader :state
|
22
|
+
|
23
|
+
param :scope
|
24
|
+
|
25
|
+
option :type, as: :state_type, default: -> { Any }
|
26
|
+
|
27
|
+
def initialize(*)
|
28
|
+
super
|
29
|
+
|
30
|
+
@state = Undefined
|
31
|
+
end
|
32
|
+
|
33
|
+
def read
|
34
|
+
state
|
35
|
+
end
|
36
|
+
|
37
|
+
def call(stack, state)
|
38
|
+
case state
|
39
|
+
when state_type
|
40
|
+
@state = state
|
41
|
+
super(stack)
|
42
|
+
else
|
43
|
+
raise Errors::InvalidValueError.new(state, scope)
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
def represent
|
48
|
+
if Undefined.equal?(state)
|
49
|
+
"#{type}[#{scope} unset]"
|
50
|
+
else
|
51
|
+
"#{type}[#{scope} set]"
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
def provide?(effect)
|
56
|
+
effect.type.equal?(:state) && effect.name.equal?(:read) && scope.equal?(effect.scope)
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
@@ -0,0 +1,88 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'dry/effects/provider'
|
4
|
+
require 'dry/effects/instructions/raise'
|
5
|
+
|
6
|
+
module Dry
|
7
|
+
module Effects
|
8
|
+
module Providers
|
9
|
+
class Resolve < Provider[:resolve]
|
10
|
+
def self.handle_method(*, as: Undefined, **)
|
11
|
+
Undefined.default(as, :provide)
|
12
|
+
end
|
13
|
+
|
14
|
+
include Dry::Equalizer(:static, :parent, :dynamic)
|
15
|
+
|
16
|
+
Locate = Effect.new(type: :resolve, name: :locate)
|
17
|
+
|
18
|
+
param :static, default: -> { EMPTY_HASH }
|
19
|
+
|
20
|
+
attr_reader :parent
|
21
|
+
|
22
|
+
attr_reader :dynamic
|
23
|
+
|
24
|
+
def initialize(*)
|
25
|
+
super
|
26
|
+
@dynamic = EMPTY_HASH
|
27
|
+
end
|
28
|
+
|
29
|
+
def resolve(key)
|
30
|
+
if parent&.key?(key)
|
31
|
+
parent.resolve(key)
|
32
|
+
elsif dynamic.key?(key)
|
33
|
+
dynamic[key]
|
34
|
+
elsif static.key?(key)
|
35
|
+
static[key]
|
36
|
+
else
|
37
|
+
Instructions.Raise(Errors::ResolutionError.new(key))
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
def locate
|
42
|
+
self
|
43
|
+
end
|
44
|
+
|
45
|
+
def call(stack, dynamic = EMPTY_HASH, options = EMPTY_HASH)
|
46
|
+
@dynamic = dynamic
|
47
|
+
|
48
|
+
if options.fetch(:overridable, false)
|
49
|
+
@parent = ::Dry::Effects.yield(Locate) { nil }
|
50
|
+
else
|
51
|
+
@parent = nil
|
52
|
+
end
|
53
|
+
|
54
|
+
super(stack)
|
55
|
+
ensure
|
56
|
+
@dynamic = EMPTY_HASH
|
57
|
+
end
|
58
|
+
|
59
|
+
def provide?(effect)
|
60
|
+
if super
|
61
|
+
!effect.name.equal?(:resolve) || key?(effect.payload[0])
|
62
|
+
else
|
63
|
+
false
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
def key?(key)
|
68
|
+
static.key?(key) || dynamic.key?(key) || parent&.key?(key)
|
69
|
+
end
|
70
|
+
|
71
|
+
def represent
|
72
|
+
containers = [represent_container(static), represent_container(dynamic)].compact.join('+')
|
73
|
+
"resolve[#{containers.empty? ? 'empty' : containers}]"
|
74
|
+
end
|
75
|
+
|
76
|
+
def represent_container(container)
|
77
|
+
if container.is_a?(::Hash)
|
78
|
+
container.empty? ? nil : 'hash'
|
79
|
+
elsif container.is_a?(::Class)
|
80
|
+
container.name || container.to_s
|
81
|
+
else
|
82
|
+
container.to_s
|
83
|
+
end
|
84
|
+
end
|
85
|
+
end
|
86
|
+
end
|
87
|
+
end
|
88
|
+
end
|
@@ -0,0 +1,59 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'dry/effects/provider'
|
4
|
+
require 'dry/effects/halt'
|
5
|
+
|
6
|
+
module Dry
|
7
|
+
module Effects
|
8
|
+
module Providers
|
9
|
+
class Retry < Provider[:retry]
|
10
|
+
include Dry::Equalizer(:scope, :limit, :attempts)
|
11
|
+
|
12
|
+
param :scope
|
13
|
+
|
14
|
+
attr_reader :attempts
|
15
|
+
|
16
|
+
attr_reader :limit
|
17
|
+
|
18
|
+
def call(_, limit)
|
19
|
+
@limit = limit
|
20
|
+
@attempts = 0
|
21
|
+
|
22
|
+
loop do
|
23
|
+
return attempt { yield }
|
24
|
+
rescue halt
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
def repeat
|
29
|
+
Instructions.Raise(halt.new)
|
30
|
+
end
|
31
|
+
|
32
|
+
def attempt
|
33
|
+
if attempts_exhausted?
|
34
|
+
nil
|
35
|
+
else
|
36
|
+
@attempts += 1
|
37
|
+
yield
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
def attempts_exhausted?
|
42
|
+
attempts.equal?(limit)
|
43
|
+
end
|
44
|
+
|
45
|
+
def halt
|
46
|
+
Halt[scope]
|
47
|
+
end
|
48
|
+
|
49
|
+
def provide?(effect)
|
50
|
+
super && scope.equal?(effect.scope)
|
51
|
+
end
|
52
|
+
|
53
|
+
def represent
|
54
|
+
"retry[#{scope} #{attempts}/#{limit}]"
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|