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