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