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,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
|