dry-effects 0.1.0.alpha
Sign up to get free protection for your applications and to get access to all the features.
- 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
|