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,25 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'dry/effects/instruction'
|
4
|
+
|
5
|
+
module Dry
|
6
|
+
module Effects
|
7
|
+
module Instructions
|
8
|
+
class Execute < Instruction
|
9
|
+
attr_reader :block
|
10
|
+
|
11
|
+
def initialize(block)
|
12
|
+
@block = block
|
13
|
+
end
|
14
|
+
|
15
|
+
def call
|
16
|
+
block.call
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
def self.Execute(&block)
|
21
|
+
Execute.new(block)
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'dry/effects/instruction'
|
4
|
+
|
5
|
+
module Dry
|
6
|
+
module Effects
|
7
|
+
module Instructions
|
8
|
+
class Raise < Instruction
|
9
|
+
attr_reader :error
|
10
|
+
|
11
|
+
def initialize(error)
|
12
|
+
@error = error
|
13
|
+
end
|
14
|
+
|
15
|
+
def call
|
16
|
+
raise error
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
def self.Raise(error)
|
21
|
+
Raise.new(error)
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'dry/effects/initializer'
|
4
|
+
require 'dry/effects/provider/class_interface'
|
5
|
+
|
6
|
+
module Dry
|
7
|
+
module Effects
|
8
|
+
class Provider
|
9
|
+
extend Initializer
|
10
|
+
extend ClassInterface
|
11
|
+
|
12
|
+
def call(_stack)
|
13
|
+
yield
|
14
|
+
end
|
15
|
+
|
16
|
+
def represent
|
17
|
+
type.to_s
|
18
|
+
end
|
19
|
+
|
20
|
+
def type
|
21
|
+
self.class.type
|
22
|
+
end
|
23
|
+
|
24
|
+
def provide?(effect)
|
25
|
+
type.equal?(effect.type)
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,61 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'dry/core/class_attributes'
|
4
|
+
|
5
|
+
module Dry
|
6
|
+
module Effects
|
7
|
+
class Provider
|
8
|
+
module ClassInterface
|
9
|
+
def self.extended(base)
|
10
|
+
base.instance_exec do
|
11
|
+
defines :type
|
12
|
+
|
13
|
+
@mutex = ::Mutex.new
|
14
|
+
@effects = ::Hash.new do |es, type|
|
15
|
+
@mutex.synchronize do
|
16
|
+
es.fetch(type) do
|
17
|
+
es[type] = Class.new(Provider).tap do |provider|
|
18
|
+
provider.type type
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
include Core::ClassAttributes
|
27
|
+
|
28
|
+
attr_reader :effects
|
29
|
+
|
30
|
+
def [](type)
|
31
|
+
if self < Provider
|
32
|
+
Provider.effects.fetch(type) do
|
33
|
+
Provider.effects[type] = Class.new(self).tap do |subclass|
|
34
|
+
subclass.type type
|
35
|
+
end
|
36
|
+
end
|
37
|
+
else
|
38
|
+
@effects[type]
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
def mixin(*args, **kwargs)
|
43
|
+
handle_method = handle_method(*args, **kwargs)
|
44
|
+
|
45
|
+
provider = new(*args, **kwargs).freeze
|
46
|
+
handler = Handler.new(provider)
|
47
|
+
|
48
|
+
::Module.new do
|
49
|
+
define_method(handle_method) do |*args, &block|
|
50
|
+
handler.(args, &block)
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
def handle_method(*, as: Undefined, **)
|
56
|
+
Undefined.default(as) { :"with_#{type}" }
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'dry/effects/provider'
|
4
|
+
|
5
|
+
module Dry
|
6
|
+
module Effects
|
7
|
+
module Providers
|
8
|
+
class Amb < Provider[:amb]
|
9
|
+
include Dry::Equalizer(:id, :value)
|
10
|
+
|
11
|
+
attr_reader :value
|
12
|
+
|
13
|
+
param :id
|
14
|
+
|
15
|
+
def get
|
16
|
+
value
|
17
|
+
end
|
18
|
+
|
19
|
+
def call(_)
|
20
|
+
@value = false
|
21
|
+
first = yield
|
22
|
+
@value = true
|
23
|
+
[first, yield]
|
24
|
+
end
|
25
|
+
|
26
|
+
def provide?(effect)
|
27
|
+
super && id.equal?(effect.id)
|
28
|
+
end
|
29
|
+
|
30
|
+
def represent
|
31
|
+
"amb[#{id}=#{@value}]"
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'dry/effects/provider'
|
4
|
+
|
5
|
+
module Dry
|
6
|
+
module Effects
|
7
|
+
module Providers
|
8
|
+
class Async < Provider[:async]
|
9
|
+
option :tasks, default: -> { ::Hash.new }
|
10
|
+
|
11
|
+
include Dry::Equalizer(:tasks)
|
12
|
+
|
13
|
+
attr_reader :stack
|
14
|
+
|
15
|
+
def async(block)
|
16
|
+
@tasks[block] = block
|
17
|
+
end
|
18
|
+
|
19
|
+
def await(task)
|
20
|
+
Handler.spawn_fiber(stack, &@tasks.delete(task))
|
21
|
+
end
|
22
|
+
|
23
|
+
def call(stack)
|
24
|
+
@stack = stack
|
25
|
+
super
|
26
|
+
nil
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
@@ -0,0 +1,43 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'dry/effects/provider'
|
4
|
+
require 'dry/effects/instructions/execute'
|
5
|
+
|
6
|
+
module Dry
|
7
|
+
module Effects
|
8
|
+
module Providers
|
9
|
+
class Cache < Provider[:cache]
|
10
|
+
include Dry::Equalizer(:scope, :cache)
|
11
|
+
|
12
|
+
param :scope
|
13
|
+
|
14
|
+
attr_reader :cache
|
15
|
+
|
16
|
+
def fetch_or_store(key, block)
|
17
|
+
if cache.key?(key)
|
18
|
+
cache[key]
|
19
|
+
else
|
20
|
+
Instructions.Execute { cache[key] = block.call }
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
def call(stack, cache = EMPTY_HASH.dup)
|
25
|
+
@cache = cache
|
26
|
+
super(stack)
|
27
|
+
end
|
28
|
+
|
29
|
+
def provide?(effect)
|
30
|
+
super && scope.eql?(effect.scope)
|
31
|
+
end
|
32
|
+
|
33
|
+
def represent
|
34
|
+
if cache.empty?
|
35
|
+
"cache[#{scope} empty]"
|
36
|
+
else
|
37
|
+
"cache[#{scope} size=#{cache.size}]"
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
@@ -0,0 +1,49 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'dry/effects/provider'
|
4
|
+
|
5
|
+
module Dry
|
6
|
+
module Effects
|
7
|
+
module Providers
|
8
|
+
class CurrentTime < Provider[:current_time]
|
9
|
+
include Dry::Equalizer(:fixed, :round)
|
10
|
+
|
11
|
+
option :fixed, default: -> { true }
|
12
|
+
|
13
|
+
option :round, default: -> { Undefined }
|
14
|
+
|
15
|
+
alias_method :fixed?, :fixed
|
16
|
+
|
17
|
+
attr_reader :time
|
18
|
+
|
19
|
+
def call(stack, time = Undefined)
|
20
|
+
if fixed?
|
21
|
+
@time = Undefined.default(time) { ::Time.now }
|
22
|
+
else
|
23
|
+
@time = time
|
24
|
+
end
|
25
|
+
super(stack)
|
26
|
+
end
|
27
|
+
|
28
|
+
def current_time(round_to: Undefined)
|
29
|
+
t = fixed? ? time : Undefined.default(time) { ::Time.now }
|
30
|
+
round = Undefined.default(round_to) { self.round }
|
31
|
+
|
32
|
+
if Undefined.equal?(round)
|
33
|
+
t
|
34
|
+
else
|
35
|
+
t.round(round)
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
def represent
|
40
|
+
if fixed?
|
41
|
+
"current_time[fixed=#{time.iso8601(6)}]"
|
42
|
+
else
|
43
|
+
'current_time[fixed=false]'
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
@@ -0,0 +1,84 @@
|
|
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 Defer < Provider[:defer]
|
10
|
+
include Dry::Equalizer(:executor)
|
11
|
+
|
12
|
+
option :executor, default: -> { :io }
|
13
|
+
|
14
|
+
attr_reader :later_calls
|
15
|
+
|
16
|
+
attr_reader :stack
|
17
|
+
|
18
|
+
def defer(block, executor)
|
19
|
+
stack = self.stack.dup
|
20
|
+
at = Undefined.default(executor, self.executor)
|
21
|
+
::Concurrent::Promise.execute(executor: at) do
|
22
|
+
Handler.spawn_fiber(stack, &block)
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
def later(block, executor)
|
27
|
+
if @later_calls.frozen?
|
28
|
+
Instructions.Raise(Errors::EffectRejectedError.new(<<~MSG))
|
29
|
+
.later calls are not allowed, they would processed
|
30
|
+
by another stack. Add another defer handler to the current stack
|
31
|
+
MSG
|
32
|
+
else
|
33
|
+
at = Undefined.default(executor, self.executor)
|
34
|
+
stack = self.stack.dup
|
35
|
+
@later_calls << ::Concurrent::Promise.new(executor: at) do
|
36
|
+
Handler.spawn_fiber(stack, &block)
|
37
|
+
end
|
38
|
+
nil
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
def wait(promises)
|
43
|
+
if promises.is_a?(::Array)
|
44
|
+
::Concurrent::Promise.zip(*promises).value!
|
45
|
+
else
|
46
|
+
promises.value!
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
def call(stack, executor: Undefined)
|
51
|
+
unless Undefined.equal?(executor)
|
52
|
+
@executor = executor
|
53
|
+
end
|
54
|
+
|
55
|
+
@stack = stack
|
56
|
+
@later_calls = []
|
57
|
+
super(stack)
|
58
|
+
ensure
|
59
|
+
later_calls.each(&:execute)
|
60
|
+
end
|
61
|
+
|
62
|
+
def dup
|
63
|
+
if defined? @later_calls
|
64
|
+
super.tap { |p| p.instance_exec { @later_calls = EMPTY_ARRAY } }
|
65
|
+
else
|
66
|
+
super
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
def represent
|
71
|
+
info = []
|
72
|
+
info << executor.to_s if executor.is_a?(::Symbol)
|
73
|
+
info << "call_later=#{later_calls.size}" if later_calls.any?
|
74
|
+
|
75
|
+
if info.empty?
|
76
|
+
'defer'
|
77
|
+
else
|
78
|
+
"defer[#{info.join(' ')}]"
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|
@@ -0,0 +1,65 @@
|
|
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 Env < Provider[:env]
|
10
|
+
include Dry::Equalizer(:values, :dynamic)
|
11
|
+
|
12
|
+
Locate = Effect.new(type: :env, name: :locate)
|
13
|
+
|
14
|
+
param :values, default: -> { EMPTY_HASH }
|
15
|
+
|
16
|
+
attr_reader :parent
|
17
|
+
|
18
|
+
def read(key)
|
19
|
+
parent.fetch(key) { fetch(key) }
|
20
|
+
end
|
21
|
+
|
22
|
+
def fetch(key)
|
23
|
+
values.fetch(key) do
|
24
|
+
if key.is_a?(::String) && ::ENV.key?(key)
|
25
|
+
::ENV[key]
|
26
|
+
else
|
27
|
+
yield
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
protected :fetch
|
32
|
+
|
33
|
+
def locate
|
34
|
+
self
|
35
|
+
end
|
36
|
+
|
37
|
+
def call(stack, values = EMPTY_HASH, options = EMPTY_HASH)
|
38
|
+
unless values.empty?
|
39
|
+
@values = @values.merge(values)
|
40
|
+
end
|
41
|
+
|
42
|
+
if options.fetch(:overridable, false)
|
43
|
+
@parent = ::Dry::Effects.yield(Locate) { EMPTY_HASH }
|
44
|
+
else
|
45
|
+
@parent = EMPTY_HASH
|
46
|
+
end
|
47
|
+
|
48
|
+
super(stack)
|
49
|
+
end
|
50
|
+
|
51
|
+
def provide?(effect)
|
52
|
+
if super
|
53
|
+
!effect.name.equal?(:read) || key?(effect.payload[0])
|
54
|
+
else
|
55
|
+
false
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
def key?(key)
|
60
|
+
values.key?(key) || key.is_a?(::String) && ::ENV.key?(key) || parent.key?(key)
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|