dry-effects 0.1.0.alpha

Sign up to get free protection for your applications and to get access to all the features.
Files changed (68) hide show
  1. checksums.yaml +7 -0
  2. data/.codeclimate.yml +15 -0
  3. data/.gitignore +12 -0
  4. data/.rspec +3 -0
  5. data/.rubocop.yml +73 -0
  6. data/.travis.yml +31 -0
  7. data/CHANGELOG.md +3 -0
  8. data/CONTRIBUTING.md +29 -0
  9. data/Gemfile +22 -0
  10. data/LICENSE +21 -0
  11. data/README.md +20 -0
  12. data/Rakefile +8 -0
  13. data/dry-effects.gemspec +48 -0
  14. data/examples/amb.rb +51 -0
  15. data/examples/state.rb +29 -0
  16. data/lib/dry/effects.rb +42 -0
  17. data/lib/dry/effects/all.rb +47 -0
  18. data/lib/dry/effects/constructors.rb +8 -0
  19. data/lib/dry/effects/container.rb +11 -0
  20. data/lib/dry/effects/effect.rb +29 -0
  21. data/lib/dry/effects/effects/amb.rb +23 -0
  22. data/lib/dry/effects/effects/async.rb +22 -0
  23. data/lib/dry/effects/effects/cache.rb +67 -0
  24. data/lib/dry/effects/effects/current_time.rb +27 -0
  25. data/lib/dry/effects/effects/defer.rb +31 -0
  26. data/lib/dry/effects/effects/env.rb +31 -0
  27. data/lib/dry/effects/effects/fork.rb +21 -0
  28. data/lib/dry/effects/effects/implicit.rb +25 -0
  29. data/lib/dry/effects/effects/interrupt.rb +29 -0
  30. data/lib/dry/effects/effects/lock.rb +45 -0
  31. data/lib/dry/effects/effects/parallel.rb +19 -0
  32. data/lib/dry/effects/effects/random.rb +19 -0
  33. data/lib/dry/effects/effects/reader.rb +15 -0
  34. data/lib/dry/effects/effects/resolve.rb +26 -0
  35. data/lib/dry/effects/effects/retry.rb +26 -0
  36. data/lib/dry/effects/effects/state.rb +50 -0
  37. data/lib/dry/effects/errors.rb +68 -0
  38. data/lib/dry/effects/extensions.rb +13 -0
  39. data/lib/dry/effects/extensions/auto_inject.rb +67 -0
  40. data/lib/dry/effects/extensions/system.rb +43 -0
  41. data/lib/dry/effects/halt.rb +29 -0
  42. data/lib/dry/effects/handler.rb +58 -0
  43. data/lib/dry/effects/inflector.rb +9 -0
  44. data/lib/dry/effects/initializer.rb +99 -0
  45. data/lib/dry/effects/instruction.rb +8 -0
  46. data/lib/dry/effects/instructions/execute.rb +25 -0
  47. data/lib/dry/effects/instructions/raise.rb +25 -0
  48. data/lib/dry/effects/provider.rb +29 -0
  49. data/lib/dry/effects/provider/class_interface.rb +61 -0
  50. data/lib/dry/effects/providers/amb.rb +36 -0
  51. data/lib/dry/effects/providers/async.rb +31 -0
  52. data/lib/dry/effects/providers/cache.rb +43 -0
  53. data/lib/dry/effects/providers/current_time.rb +49 -0
  54. data/lib/dry/effects/providers/defer.rb +84 -0
  55. data/lib/dry/effects/providers/env.rb +65 -0
  56. data/lib/dry/effects/providers/fork.rb +23 -0
  57. data/lib/dry/effects/providers/implicit.rb +39 -0
  58. data/lib/dry/effects/providers/interrupt.rb +37 -0
  59. data/lib/dry/effects/providers/lock.rb +125 -0
  60. data/lib/dry/effects/providers/parallel.rb +34 -0
  61. data/lib/dry/effects/providers/random.rb +13 -0
  62. data/lib/dry/effects/providers/reader.rb +61 -0
  63. data/lib/dry/effects/providers/resolve.rb +88 -0
  64. data/lib/dry/effects/providers/retry.rb +59 -0
  65. data/lib/dry/effects/providers/state.rb +30 -0
  66. data/lib/dry/effects/stack.rb +67 -0
  67. data/lib/dry/effects/version.rb +7 -0
  68. 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,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'dry/inflector'
4
+
5
+ module Dry
6
+ module Effects
7
+ Inflector = ::Dry::Inflector.new
8
+ end
9
+ 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