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