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,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Dry
4
+ module Effects
5
+ class Instruction
6
+ end
7
+ end
8
+ end
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'dry/effects/instruction'
4
+
5
+ module Dry
6
+ module Effects
7
+ module Instructions
8
+ class Execute < Instruction
9
+ attr_reader :block
10
+
11
+ def initialize(block)
12
+ @block = block
13
+ end
14
+
15
+ def call
16
+ block.call
17
+ end
18
+ end
19
+
20
+ def self.Execute(&block)
21
+ Execute.new(block)
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'dry/effects/instruction'
4
+
5
+ module Dry
6
+ module Effects
7
+ module Instructions
8
+ class Raise < Instruction
9
+ attr_reader :error
10
+
11
+ def initialize(error)
12
+ @error = error
13
+ end
14
+
15
+ def call
16
+ raise error
17
+ end
18
+ end
19
+
20
+ def self.Raise(error)
21
+ Raise.new(error)
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'dry/effects/initializer'
4
+ require 'dry/effects/provider/class_interface'
5
+
6
+ module Dry
7
+ module Effects
8
+ class Provider
9
+ extend Initializer
10
+ extend ClassInterface
11
+
12
+ def call(_stack)
13
+ yield
14
+ end
15
+
16
+ def represent
17
+ type.to_s
18
+ end
19
+
20
+ def type
21
+ self.class.type
22
+ end
23
+
24
+ def provide?(effect)
25
+ type.equal?(effect.type)
26
+ end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,61 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'dry/core/class_attributes'
4
+
5
+ module Dry
6
+ module Effects
7
+ class Provider
8
+ module ClassInterface
9
+ def self.extended(base)
10
+ base.instance_exec do
11
+ defines :type
12
+
13
+ @mutex = ::Mutex.new
14
+ @effects = ::Hash.new do |es, type|
15
+ @mutex.synchronize do
16
+ es.fetch(type) do
17
+ es[type] = Class.new(Provider).tap do |provider|
18
+ provider.type type
19
+ end
20
+ end
21
+ end
22
+ end
23
+ end
24
+ end
25
+
26
+ include Core::ClassAttributes
27
+
28
+ attr_reader :effects
29
+
30
+ def [](type)
31
+ if self < Provider
32
+ Provider.effects.fetch(type) do
33
+ Provider.effects[type] = Class.new(self).tap do |subclass|
34
+ subclass.type type
35
+ end
36
+ end
37
+ else
38
+ @effects[type]
39
+ end
40
+ end
41
+
42
+ def mixin(*args, **kwargs)
43
+ handle_method = handle_method(*args, **kwargs)
44
+
45
+ provider = new(*args, **kwargs).freeze
46
+ handler = Handler.new(provider)
47
+
48
+ ::Module.new do
49
+ define_method(handle_method) do |*args, &block|
50
+ handler.(args, &block)
51
+ end
52
+ end
53
+ end
54
+
55
+ def handle_method(*, as: Undefined, **)
56
+ Undefined.default(as) { :"with_#{type}" }
57
+ end
58
+ end
59
+ end
60
+ end
61
+ end
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'dry/effects/provider'
4
+
5
+ module Dry
6
+ module Effects
7
+ module Providers
8
+ class Amb < Provider[:amb]
9
+ include Dry::Equalizer(:id, :value)
10
+
11
+ attr_reader :value
12
+
13
+ param :id
14
+
15
+ def get
16
+ value
17
+ end
18
+
19
+ def call(_)
20
+ @value = false
21
+ first = yield
22
+ @value = true
23
+ [first, yield]
24
+ end
25
+
26
+ def provide?(effect)
27
+ super && id.equal?(effect.id)
28
+ end
29
+
30
+ def represent
31
+ "amb[#{id}=#{@value}]"
32
+ end
33
+ end
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'dry/effects/provider'
4
+
5
+ module Dry
6
+ module Effects
7
+ module Providers
8
+ class Async < Provider[:async]
9
+ option :tasks, default: -> { ::Hash.new }
10
+
11
+ include Dry::Equalizer(:tasks)
12
+
13
+ attr_reader :stack
14
+
15
+ def async(block)
16
+ @tasks[block] = block
17
+ end
18
+
19
+ def await(task)
20
+ Handler.spawn_fiber(stack, &@tasks.delete(task))
21
+ end
22
+
23
+ def call(stack)
24
+ @stack = stack
25
+ super
26
+ nil
27
+ end
28
+ end
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,43 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'dry/effects/provider'
4
+ require 'dry/effects/instructions/execute'
5
+
6
+ module Dry
7
+ module Effects
8
+ module Providers
9
+ class Cache < Provider[:cache]
10
+ include Dry::Equalizer(:scope, :cache)
11
+
12
+ param :scope
13
+
14
+ attr_reader :cache
15
+
16
+ def fetch_or_store(key, block)
17
+ if cache.key?(key)
18
+ cache[key]
19
+ else
20
+ Instructions.Execute { cache[key] = block.call }
21
+ end
22
+ end
23
+
24
+ def call(stack, cache = EMPTY_HASH.dup)
25
+ @cache = cache
26
+ super(stack)
27
+ end
28
+
29
+ def provide?(effect)
30
+ super && scope.eql?(effect.scope)
31
+ end
32
+
33
+ def represent
34
+ if cache.empty?
35
+ "cache[#{scope} empty]"
36
+ else
37
+ "cache[#{scope} size=#{cache.size}]"
38
+ end
39
+ end
40
+ end
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,49 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'dry/effects/provider'
4
+
5
+ module Dry
6
+ module Effects
7
+ module Providers
8
+ class CurrentTime < Provider[:current_time]
9
+ include Dry::Equalizer(:fixed, :round)
10
+
11
+ option :fixed, default: -> { true }
12
+
13
+ option :round, default: -> { Undefined }
14
+
15
+ alias_method :fixed?, :fixed
16
+
17
+ attr_reader :time
18
+
19
+ def call(stack, time = Undefined)
20
+ if fixed?
21
+ @time = Undefined.default(time) { ::Time.now }
22
+ else
23
+ @time = time
24
+ end
25
+ super(stack)
26
+ end
27
+
28
+ def current_time(round_to: Undefined)
29
+ t = fixed? ? time : Undefined.default(time) { ::Time.now }
30
+ round = Undefined.default(round_to) { self.round }
31
+
32
+ if Undefined.equal?(round)
33
+ t
34
+ else
35
+ t.round(round)
36
+ end
37
+ end
38
+
39
+ def represent
40
+ if fixed?
41
+ "current_time[fixed=#{time.iso8601(6)}]"
42
+ else
43
+ 'current_time[fixed=false]'
44
+ end
45
+ end
46
+ end
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,84 @@
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 Defer < Provider[:defer]
10
+ include Dry::Equalizer(:executor)
11
+
12
+ option :executor, default: -> { :io }
13
+
14
+ attr_reader :later_calls
15
+
16
+ attr_reader :stack
17
+
18
+ def defer(block, executor)
19
+ stack = self.stack.dup
20
+ at = Undefined.default(executor, self.executor)
21
+ ::Concurrent::Promise.execute(executor: at) do
22
+ Handler.spawn_fiber(stack, &block)
23
+ end
24
+ end
25
+
26
+ def later(block, executor)
27
+ if @later_calls.frozen?
28
+ Instructions.Raise(Errors::EffectRejectedError.new(<<~MSG))
29
+ .later calls are not allowed, they would processed
30
+ by another stack. Add another defer handler to the current stack
31
+ MSG
32
+ else
33
+ at = Undefined.default(executor, self.executor)
34
+ stack = self.stack.dup
35
+ @later_calls << ::Concurrent::Promise.new(executor: at) do
36
+ Handler.spawn_fiber(stack, &block)
37
+ end
38
+ nil
39
+ end
40
+ end
41
+
42
+ def wait(promises)
43
+ if promises.is_a?(::Array)
44
+ ::Concurrent::Promise.zip(*promises).value!
45
+ else
46
+ promises.value!
47
+ end
48
+ end
49
+
50
+ def call(stack, executor: Undefined)
51
+ unless Undefined.equal?(executor)
52
+ @executor = executor
53
+ end
54
+
55
+ @stack = stack
56
+ @later_calls = []
57
+ super(stack)
58
+ ensure
59
+ later_calls.each(&:execute)
60
+ end
61
+
62
+ def dup
63
+ if defined? @later_calls
64
+ super.tap { |p| p.instance_exec { @later_calls = EMPTY_ARRAY } }
65
+ else
66
+ super
67
+ end
68
+ end
69
+
70
+ def represent
71
+ info = []
72
+ info << executor.to_s if executor.is_a?(::Symbol)
73
+ info << "call_later=#{later_calls.size}" if later_calls.any?
74
+
75
+ if info.empty?
76
+ 'defer'
77
+ else
78
+ "defer[#{info.join(' ')}]"
79
+ end
80
+ end
81
+ end
82
+ end
83
+ end
84
+ end
@@ -0,0 +1,65 @@
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 Env < Provider[:env]
10
+ include Dry::Equalizer(:values, :dynamic)
11
+
12
+ Locate = Effect.new(type: :env, name: :locate)
13
+
14
+ param :values, default: -> { EMPTY_HASH }
15
+
16
+ attr_reader :parent
17
+
18
+ def read(key)
19
+ parent.fetch(key) { fetch(key) }
20
+ end
21
+
22
+ def fetch(key)
23
+ values.fetch(key) do
24
+ if key.is_a?(::String) && ::ENV.key?(key)
25
+ ::ENV[key]
26
+ else
27
+ yield
28
+ end
29
+ end
30
+ end
31
+ protected :fetch
32
+
33
+ def locate
34
+ self
35
+ end
36
+
37
+ def call(stack, values = EMPTY_HASH, options = EMPTY_HASH)
38
+ unless values.empty?
39
+ @values = @values.merge(values)
40
+ end
41
+
42
+ if options.fetch(:overridable, false)
43
+ @parent = ::Dry::Effects.yield(Locate) { EMPTY_HASH }
44
+ else
45
+ @parent = EMPTY_HASH
46
+ end
47
+
48
+ super(stack)
49
+ end
50
+
51
+ def provide?(effect)
52
+ if super
53
+ !effect.name.equal?(:read) || key?(effect.payload[0])
54
+ else
55
+ false
56
+ end
57
+ end
58
+
59
+ def key?(key)
60
+ values.key?(key) || key.is_a?(::String) && ::ENV.key?(key) || parent.key?(key)
61
+ end
62
+ end
63
+ end
64
+ end
65
+ end