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,26 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'dry/effects/effect'
|
4
|
+
require 'dry/effects/constructors'
|
5
|
+
|
6
|
+
module Dry
|
7
|
+
module Effects
|
8
|
+
module Effects
|
9
|
+
class Resolve < ::Module
|
10
|
+
Resolve = Effect.new(type: :resolve)
|
11
|
+
|
12
|
+
def Constructors.Resolve(key)
|
13
|
+
Resolve.(key)
|
14
|
+
end
|
15
|
+
|
16
|
+
def initialize(*keys, **aliases)
|
17
|
+
module_eval do
|
18
|
+
(keys.zip(keys) + aliases.to_a).each do |name, key|
|
19
|
+
define_method(name) { |&block| ::Dry::Effects.yield(Resolve.(key), &block) }
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'dry/effects/effect'
|
4
|
+
|
5
|
+
module Dry
|
6
|
+
module Effects
|
7
|
+
module Effects
|
8
|
+
class Retry < ::Module
|
9
|
+
class RetryEffect < Effect
|
10
|
+
include ::Dry::Equalizer(:type, :name, :payload, :scope)
|
11
|
+
|
12
|
+
option :scope
|
13
|
+
end
|
14
|
+
|
15
|
+
def initialize
|
16
|
+
module_eval do
|
17
|
+
define_method(:repeat) do |scope|
|
18
|
+
effect = RetryEffect.new(type: :retry, name: :repeat, scope: scope)
|
19
|
+
::Dry::Effects.yield(effect)
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
@@ -0,0 +1,50 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'dry/effects/effect'
|
4
|
+
|
5
|
+
module Dry
|
6
|
+
module Effects
|
7
|
+
module Effects
|
8
|
+
class State < ::Module
|
9
|
+
class StateEffect < Effect
|
10
|
+
include ::Dry::Equalizer(:type, :name, :payload, :scope)
|
11
|
+
|
12
|
+
option :scope
|
13
|
+
end
|
14
|
+
|
15
|
+
def initialize(scope, default: Undefined, writer: true, as: scope)
|
16
|
+
read = StateEffect.new(type: :state, name: :read, scope: scope)
|
17
|
+
write = StateEffect.new(type: :state, name: :write, scope: scope)
|
18
|
+
|
19
|
+
module_eval do
|
20
|
+
if Undefined.equal?(default)
|
21
|
+
define_method(as) do |&block|
|
22
|
+
if block
|
23
|
+
Undefined.default(::Dry::Effects.yield(read) { Undefined }, &block)
|
24
|
+
else
|
25
|
+
value = ::Dry::Effects.yield(read) { raise Errors::MissingStateError, read }
|
26
|
+
|
27
|
+
Undefined.default(value) { raise Errors::UndefinedStateError, read }
|
28
|
+
end
|
29
|
+
end
|
30
|
+
else
|
31
|
+
define_method(as) do |&block|
|
32
|
+
if block
|
33
|
+
Undefined.default(::Dry::Effects.yield(read) { Undefined }, &block)
|
34
|
+
else
|
35
|
+
Undefined.default(::Dry::Effects.yield(read) { Undefined }, default)
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
if writer
|
41
|
+
define_method(:"#{as}=") do |value|
|
42
|
+
::Dry::Effects.yield(write.(value))
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
@@ -0,0 +1,68 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Dry
|
4
|
+
module Effects
|
5
|
+
module Errors
|
6
|
+
module Error
|
7
|
+
end
|
8
|
+
|
9
|
+
class UnhandledEffectError < RuntimeError
|
10
|
+
include Error
|
11
|
+
|
12
|
+
attr_reader :effect
|
13
|
+
|
14
|
+
def initialize(effect, message = Undefined)
|
15
|
+
@effect = effect
|
16
|
+
|
17
|
+
super(
|
18
|
+
Undefined.default(message) {
|
19
|
+
"Effect #{effect.inspect} not handled. "\
|
20
|
+
'Effects must be wrapped with corresponding handlers'
|
21
|
+
}
|
22
|
+
)
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
class MissingStateError < UnhandledEffectError
|
27
|
+
def initialize(effect)
|
28
|
+
message = "Value of +#{effect.scope}+ is not set, "\
|
29
|
+
'you need to provide value with an effect handler'
|
30
|
+
|
31
|
+
super(effect, message)
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
class UndefinedStateError < RuntimeError
|
36
|
+
include Error
|
37
|
+
|
38
|
+
def initialize(effect)
|
39
|
+
message = "+#{effect.scope}+ is not defined, you need to assign it first "\
|
40
|
+
'by using a writer, passing initial value to the handler, or '\
|
41
|
+
'providing a fallback value'
|
42
|
+
|
43
|
+
super(message)
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
class EffectRejectedError < RuntimeError
|
48
|
+
include Error
|
49
|
+
end
|
50
|
+
|
51
|
+
class ResolutionError < RuntimeError
|
52
|
+
include Error
|
53
|
+
|
54
|
+
def initialize(key)
|
55
|
+
super("Key +#{key.inspect}+ cannot be resolved")
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
class InvalidValueError < ArgumentError
|
60
|
+
include Error
|
61
|
+
|
62
|
+
def initialize(value, scope)
|
63
|
+
super("#{value.inspect} is invalid and cannot be assigned to #{scope}")
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
@@ -0,0 +1,13 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'dry/core/extensions'
|
4
|
+
|
5
|
+
Dry::Effects.extend(Dry::Core::Extensions)
|
6
|
+
|
7
|
+
Dry::Effects.register_extension(:auto_inject) do
|
8
|
+
require 'dry/effects/extensions/auto_inject'
|
9
|
+
end
|
10
|
+
|
11
|
+
Dry::Effects.register_extension(:system) do
|
12
|
+
require 'dry/effects/extensions/system'
|
13
|
+
end
|
@@ -0,0 +1,67 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'concurrent/map'
|
4
|
+
require 'dry/auto_inject/strategies/constructor'
|
5
|
+
require 'dry/effects/effects/resolve'
|
6
|
+
|
7
|
+
module Dry
|
8
|
+
module Effects
|
9
|
+
class DryAutoEffectsStrategies
|
10
|
+
extend Dry::Container::Mixin
|
11
|
+
|
12
|
+
class Base < AutoInject::Strategies::Constructor
|
13
|
+
private
|
14
|
+
|
15
|
+
def define_new
|
16
|
+
# nothing to do
|
17
|
+
end
|
18
|
+
|
19
|
+
def define_initialize(_)
|
20
|
+
# nothing to do
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
class Static < Base
|
25
|
+
private
|
26
|
+
|
27
|
+
def define_readers(dynamic = false)
|
28
|
+
map = dependency_map.to_h
|
29
|
+
cache = ::Concurrent::Map.new
|
30
|
+
instance_mod.class_exec do
|
31
|
+
map.each do |name, identifier|
|
32
|
+
resolve = ::Dry::Effects::Constructors::Resolve(identifier)
|
33
|
+
|
34
|
+
if dynamic
|
35
|
+
define_method(name) { ::Dry::Effects.yield(resolve) }
|
36
|
+
else
|
37
|
+
define_method(name) do
|
38
|
+
cache.fetch_or_store(name) do
|
39
|
+
::Dry::Effects.yield(resolve)
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
self
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
class Dynamic < Static
|
50
|
+
private
|
51
|
+
|
52
|
+
def define_readers(dynamic = true)
|
53
|
+
super
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
register :static, Static
|
58
|
+
register :dynamic, Dynamic
|
59
|
+
register :default, Static
|
60
|
+
end
|
61
|
+
|
62
|
+
def self.AutoInject(dynamic: false)
|
63
|
+
mod = Dry.AutoInject(EMPTY_HASH, strategies: DryAutoEffectsStrategies)
|
64
|
+
dynamic ? mod.dynamic : mod
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
@@ -0,0 +1,43 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'dry/system/container'
|
4
|
+
|
5
|
+
Dry::Effects.load_extensions(:auto_inject)
|
6
|
+
|
7
|
+
module Dry
|
8
|
+
module Effects
|
9
|
+
module System
|
10
|
+
class AutoRegistrar < ::Dry::System::AutoRegistrar
|
11
|
+
def call(dir)
|
12
|
+
super do |config|
|
13
|
+
config.memoize = true
|
14
|
+
config.instance { |c| c.instance.freeze }
|
15
|
+
yield(config) if block_given?
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
class Container < ::Dry::System::Container
|
21
|
+
setting :auto_registrar, AutoRegistrar
|
22
|
+
|
23
|
+
def self.injector(effects: true, **kwargs)
|
24
|
+
if effects
|
25
|
+
Dry::Effects.AutoInject(**kwargs)
|
26
|
+
else
|
27
|
+
super()
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
def self.finalize!
|
32
|
+
return self if finalized?
|
33
|
+
|
34
|
+
super
|
35
|
+
|
36
|
+
# Force all components to load
|
37
|
+
each_key { |key| resolve(key) }
|
38
|
+
self
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'concurrent/map'
|
4
|
+
require 'dry/core/class_attributes'
|
5
|
+
require 'dry/effects/inflector'
|
6
|
+
|
7
|
+
module Dry
|
8
|
+
module Effects
|
9
|
+
class Halt < StandardError
|
10
|
+
extend Core::ClassAttributes
|
11
|
+
|
12
|
+
@constants = ::Concurrent::Map.new
|
13
|
+
|
14
|
+
def self.[](key)
|
15
|
+
@constants.fetch_or_store(key) do
|
16
|
+
klass = ::Class.new(Halt)
|
17
|
+
const_set(Inflector.camelize(key), klass)
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
attr_reader :payload
|
22
|
+
|
23
|
+
def initialize(payload = Undefined)
|
24
|
+
super(EMPTY_STRING)
|
25
|
+
@payload = payload
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,58 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'fiber'
|
4
|
+
require 'dry/effects/initializer'
|
5
|
+
require 'dry/effects/effect'
|
6
|
+
require 'dry/effects/errors'
|
7
|
+
require 'dry/effects/stack'
|
8
|
+
require 'dry/effects/instructions/raise'
|
9
|
+
|
10
|
+
module Dry
|
11
|
+
module Effects
|
12
|
+
class Handler
|
13
|
+
class << self
|
14
|
+
def stack
|
15
|
+
::Thread.current[:dry_effects_stack] ||= Stack.new
|
16
|
+
end
|
17
|
+
|
18
|
+
def stack=(stack)
|
19
|
+
::Thread.current[:dry_effects_stack] = stack
|
20
|
+
end
|
21
|
+
|
22
|
+
def spawn_fiber(stack)
|
23
|
+
fiber = ::Fiber.new do
|
24
|
+
self.stack = stack
|
25
|
+
yield
|
26
|
+
end
|
27
|
+
result = fiber.resume
|
28
|
+
|
29
|
+
loop do
|
30
|
+
break result unless fiber.alive?
|
31
|
+
|
32
|
+
provided = stack.(result) do
|
33
|
+
::Dry::Effects.yield(result) do |_, error|
|
34
|
+
Instructions.Raise(error)
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
result = fiber.resume(provided)
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
extend Initializer
|
44
|
+
|
45
|
+
param :provider
|
46
|
+
|
47
|
+
def call(args = EMPTY_ARRAY, &block)
|
48
|
+
stack = Handler.stack
|
49
|
+
|
50
|
+
if stack.empty?
|
51
|
+
stack.push(provider.dup, args) { Handler.spawn_fiber(stack, &block) }
|
52
|
+
else
|
53
|
+
stack.push(provider.dup, args, &block)
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
@@ -0,0 +1,99 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'dry/initializer'
|
4
|
+
|
5
|
+
module Dry
|
6
|
+
module Effects
|
7
|
+
module Initializer
|
8
|
+
# @api private
|
9
|
+
module DefineWithHook
|
10
|
+
# @api private
|
11
|
+
def param(*)
|
12
|
+
super.tap do
|
13
|
+
@params_arity = nil
|
14
|
+
__define_with__
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
# @api private
|
19
|
+
def option(*)
|
20
|
+
super.tap do
|
21
|
+
__define_with__ unless method_defined?(:with)
|
22
|
+
@has_options = true
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
# @api private
|
27
|
+
def params_arity
|
28
|
+
@params_arity ||= begin
|
29
|
+
dry_initializer
|
30
|
+
.definitions
|
31
|
+
.reject { |_, d| d.option }
|
32
|
+
.size
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
# @api private
|
37
|
+
def options?
|
38
|
+
return @has_options if defined? @has_options
|
39
|
+
@has_options = false
|
40
|
+
end
|
41
|
+
|
42
|
+
# @api private
|
43
|
+
def __define_with__
|
44
|
+
seq_names = dry_initializer
|
45
|
+
.definitions
|
46
|
+
.reject { |_, d| d.option }
|
47
|
+
.keys
|
48
|
+
.join(', ')
|
49
|
+
|
50
|
+
seq_names << ', ' unless seq_names.empty?
|
51
|
+
|
52
|
+
undef_method(:with) if method_defined?(:with)
|
53
|
+
|
54
|
+
class_eval(<<-RUBY, __FILE__, __LINE__ + 1)
|
55
|
+
def with(new_options = EMPTY_HASH)
|
56
|
+
if new_options.empty?
|
57
|
+
self
|
58
|
+
else
|
59
|
+
self.class.new(#{seq_names}options.merge(new_options))
|
60
|
+
end
|
61
|
+
end
|
62
|
+
RUBY
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
# @api private
|
67
|
+
def self.extended(base)
|
68
|
+
base.extend(::Dry::Initializer)
|
69
|
+
base.extend(DefineWithHook)
|
70
|
+
base.include(InstanceMethods)
|
71
|
+
end
|
72
|
+
|
73
|
+
# @api private
|
74
|
+
module InstanceMethods
|
75
|
+
# Instance options
|
76
|
+
#
|
77
|
+
# @return [Hash]
|
78
|
+
#
|
79
|
+
# @api public
|
80
|
+
def options
|
81
|
+
@__options__ ||= self.class.dry_initializer.definitions.values.each_with_object({}) do |item, obj|
|
82
|
+
obj[item.target] = instance_variable_get(item.ivar)
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
define_method(:class, Kernel.instance_method(:class))
|
87
|
+
define_method(:instance_variable_get, Kernel.instance_method(:instance_variable_get))
|
88
|
+
|
89
|
+
# This makes sure we memoize options before an object becomes frozen
|
90
|
+
#
|
91
|
+
# @api public
|
92
|
+
def freeze
|
93
|
+
options
|
94
|
+
super
|
95
|
+
end
|
96
|
+
end
|
97
|
+
end
|
98
|
+
end
|
99
|
+
end
|