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,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'dry/effects/provider'
4
+
5
+ module Dry
6
+ module Effects
7
+ module Providers
8
+ class Fork < Provider[:fork]
9
+ attr_reader :stack
10
+
11
+ def fork
12
+ stack = self.stack.dup
13
+ -> &cont { Handler.spawn_fiber(stack.dup, &cont) }
14
+ end
15
+
16
+ def call(stack)
17
+ @stack = stack
18
+ super
19
+ end
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,39 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'dry/effects/provider'
4
+
5
+ module Dry
6
+ module Effects
7
+ module Providers
8
+ class Implicit < Provider[:implicit]
9
+ include Dry::Equalizer(:name, :static, :dictionary)
10
+
11
+ param :dependency
12
+
13
+ param :static, default: -> { EMPTY_HASH }
14
+
15
+ attr_reader :dictionary
16
+
17
+ def implicit(arg)
18
+ dictionary.fetch(arg.class)
19
+ end
20
+
21
+ def call(stack, dynamic = EMPTY_HASH)
22
+ if dynamic.empty?
23
+ @dictionary = static
24
+ else
25
+ @dictionary = static.merge(dynamic)
26
+ end
27
+
28
+ super(stack)
29
+ end
30
+
31
+ def provide?(effect)
32
+ super &&
33
+ dependency.equal?(effect.dependency) &&
34
+ dictionary.key?(effect.payload[0].class)
35
+ end
36
+ end
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'dry/effects/provider'
4
+ require 'dry/effects/instructions/raise'
5
+ require 'dry/effects/halt'
6
+
7
+ module Dry
8
+ module Effects
9
+ module Providers
10
+ class Interrupt < Provider[:interrupt]
11
+ param :scope, default: -> { :default }
12
+
13
+ def interrupt(*payload)
14
+ Instructions.Raise(halt.new(payload))
15
+ end
16
+
17
+ def call(_stack)
18
+ yield
19
+ rescue halt => e
20
+ e.payload[0]
21
+ end
22
+
23
+ def halt
24
+ Halt[scope]
25
+ end
26
+
27
+ def represent
28
+ "interrupt[#{scope}]"
29
+ end
30
+
31
+ def provide?(effect)
32
+ super && scope.equal?(effect.scope)
33
+ end
34
+ end
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,125 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'dry/equalizer'
4
+ require 'dry/effects/provider'
5
+ require 'dry/effects/initializer'
6
+
7
+ module Dry
8
+ module Effects
9
+ module Providers
10
+ class Lock < Provider[:lock]
11
+ class Handle
12
+ include ::Dry::Equalizer(:key)
13
+
14
+ extend Initializer
15
+
16
+ param :key
17
+
18
+ param :meta
19
+ end
20
+
21
+ class Backend
22
+ extend Initializer
23
+
24
+ param :locks, default: -> { ::Hash.new }
25
+
26
+ param :mutex, default: -> { ::Mutex.new }
27
+
28
+ def lock(key, meta)
29
+ mutex.synchronize do
30
+ if locked?(key)
31
+ nil
32
+ else
33
+ locks[key] = Handle.new(key, meta)
34
+ end
35
+ end
36
+ end
37
+
38
+ def locked?(key)
39
+ locks.key?(key)
40
+ end
41
+
42
+ def unlock(handle)
43
+ mutex.synchronize do
44
+ if locked?(handle.key)
45
+ locks.delete(handle.key)
46
+ true
47
+ else
48
+ false
49
+ end
50
+ end
51
+ end
52
+
53
+ def meta(key)
54
+ meta = Undefined.map(locks.fetch(key, Undefined), &:meta)
55
+ Undefined.default(meta, nil)
56
+ end
57
+ end
58
+
59
+ Locate = Effect.new(type: :lock, name: :locate)
60
+
61
+ option :backend, default: -> { Backend.new }
62
+
63
+ def lock(key, meta = Undefined)
64
+ locked = backend.lock(key, meta)
65
+ owned << locked if locked
66
+ locked
67
+ end
68
+
69
+ def locked?(key)
70
+ backend.locked?(key)
71
+ end
72
+
73
+ def unlock(handle)
74
+ backend.unlock(handle)
75
+ end
76
+
77
+ def meta(key)
78
+ backend.meta(key)
79
+ end
80
+
81
+ def locate
82
+ self
83
+ end
84
+
85
+ def call(stack, backend = Undefined)
86
+ backend_replace = Undefined.default(backend) do
87
+ parent = ::Dry::Effects.yield(Locate) { Undefined }
88
+ Undefined.map(parent, &:backend)
89
+ end
90
+
91
+ with_backend(backend_replace) do
92
+ super(stack)
93
+ ensure
94
+ owned.each { |handle| unlock(handle) }
95
+ end
96
+ end
97
+
98
+ def with_backend(backend)
99
+ if Undefined.equal?(backend)
100
+ yield
101
+ else
102
+ begin
103
+ before, @backend = @backend, backend
104
+ yield
105
+ ensure
106
+ @backend = before
107
+ end
108
+ end
109
+ end
110
+
111
+ def owned
112
+ @owned ||= []
113
+ end
114
+
115
+ def represent
116
+ if owned.empty?
117
+ super
118
+ else
119
+ "lock[owned=#{owned.size}]"
120
+ end
121
+ end
122
+ end
123
+ end
124
+ end
125
+ end
@@ -0,0 +1,34 @@
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 Parallel < Provider[:parallel]
10
+ option :executor, default: -> { :io }
11
+
12
+ attr_reader :stack
13
+
14
+ def par
15
+ stack = self.stack.dup
16
+ proc do |&block|
17
+ ::Concurrent::Promise.execute(executor: executor) do
18
+ Handler.spawn_fiber(stack, &block)
19
+ end
20
+ end
21
+ end
22
+
23
+ def join(xs)
24
+ xs.map(&:value!)
25
+ end
26
+
27
+ def call(stack)
28
+ @stack = stack
29
+ super
30
+ end
31
+ end
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'dry/effects/provider'
4
+
5
+ module Dry
6
+ module Effects
7
+ module Providers
8
+ class Random < Provider[:random]
9
+ public :rand
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,61 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'dry/effects/provider'
4
+
5
+ module Dry
6
+ module Effects
7
+ module Providers
8
+ class Reader < Provider[:reader]
9
+ def self.handle_method(scope, as: Undefined, **)
10
+ Undefined.default(as) { :"with_#{scope}" }
11
+ end
12
+
13
+ Any = Object.new.tap { |any|
14
+ def any.===(_)
15
+ true
16
+ end
17
+ }.freeze
18
+
19
+ include Dry::Equalizer(:scope, :state)
20
+
21
+ attr_reader :state
22
+
23
+ param :scope
24
+
25
+ option :type, as: :state_type, default: -> { Any }
26
+
27
+ def initialize(*)
28
+ super
29
+
30
+ @state = Undefined
31
+ end
32
+
33
+ def read
34
+ state
35
+ end
36
+
37
+ def call(stack, state)
38
+ case state
39
+ when state_type
40
+ @state = state
41
+ super(stack)
42
+ else
43
+ raise Errors::InvalidValueError.new(state, scope)
44
+ end
45
+ end
46
+
47
+ def represent
48
+ if Undefined.equal?(state)
49
+ "#{type}[#{scope} unset]"
50
+ else
51
+ "#{type}[#{scope} set]"
52
+ end
53
+ end
54
+
55
+ def provide?(effect)
56
+ effect.type.equal?(:state) && effect.name.equal?(:read) && scope.equal?(effect.scope)
57
+ end
58
+ end
59
+ end
60
+ end
61
+ end
@@ -0,0 +1,88 @@
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 Resolve < Provider[:resolve]
10
+ def self.handle_method(*, as: Undefined, **)
11
+ Undefined.default(as, :provide)
12
+ end
13
+
14
+ include Dry::Equalizer(:static, :parent, :dynamic)
15
+
16
+ Locate = Effect.new(type: :resolve, name: :locate)
17
+
18
+ param :static, default: -> { EMPTY_HASH }
19
+
20
+ attr_reader :parent
21
+
22
+ attr_reader :dynamic
23
+
24
+ def initialize(*)
25
+ super
26
+ @dynamic = EMPTY_HASH
27
+ end
28
+
29
+ def resolve(key)
30
+ if parent&.key?(key)
31
+ parent.resolve(key)
32
+ elsif dynamic.key?(key)
33
+ dynamic[key]
34
+ elsif static.key?(key)
35
+ static[key]
36
+ else
37
+ Instructions.Raise(Errors::ResolutionError.new(key))
38
+ end
39
+ end
40
+
41
+ def locate
42
+ self
43
+ end
44
+
45
+ def call(stack, dynamic = EMPTY_HASH, options = EMPTY_HASH)
46
+ @dynamic = dynamic
47
+
48
+ if options.fetch(:overridable, false)
49
+ @parent = ::Dry::Effects.yield(Locate) { nil }
50
+ else
51
+ @parent = nil
52
+ end
53
+
54
+ super(stack)
55
+ ensure
56
+ @dynamic = EMPTY_HASH
57
+ end
58
+
59
+ def provide?(effect)
60
+ if super
61
+ !effect.name.equal?(:resolve) || key?(effect.payload[0])
62
+ else
63
+ false
64
+ end
65
+ end
66
+
67
+ def key?(key)
68
+ static.key?(key) || dynamic.key?(key) || parent&.key?(key)
69
+ end
70
+
71
+ def represent
72
+ containers = [represent_container(static), represent_container(dynamic)].compact.join('+')
73
+ "resolve[#{containers.empty? ? 'empty' : containers}]"
74
+ end
75
+
76
+ def represent_container(container)
77
+ if container.is_a?(::Hash)
78
+ container.empty? ? nil : 'hash'
79
+ elsif container.is_a?(::Class)
80
+ container.name || container.to_s
81
+ else
82
+ container.to_s
83
+ end
84
+ end
85
+ end
86
+ end
87
+ end
88
+ end
@@ -0,0 +1,59 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'dry/effects/provider'
4
+ require 'dry/effects/halt'
5
+
6
+ module Dry
7
+ module Effects
8
+ module Providers
9
+ class Retry < Provider[:retry]
10
+ include Dry::Equalizer(:scope, :limit, :attempts)
11
+
12
+ param :scope
13
+
14
+ attr_reader :attempts
15
+
16
+ attr_reader :limit
17
+
18
+ def call(_, limit)
19
+ @limit = limit
20
+ @attempts = 0
21
+
22
+ loop do
23
+ return attempt { yield }
24
+ rescue halt
25
+ end
26
+ end
27
+
28
+ def repeat
29
+ Instructions.Raise(halt.new)
30
+ end
31
+
32
+ def attempt
33
+ if attempts_exhausted?
34
+ nil
35
+ else
36
+ @attempts += 1
37
+ yield
38
+ end
39
+ end
40
+
41
+ def attempts_exhausted?
42
+ attempts.equal?(limit)
43
+ end
44
+
45
+ def halt
46
+ Halt[scope]
47
+ end
48
+
49
+ def provide?(effect)
50
+ super && scope.equal?(effect.scope)
51
+ end
52
+
53
+ def represent
54
+ "retry[#{scope} #{attempts}/#{limit}]"
55
+ end
56
+ end
57
+ end
58
+ end
59
+ end