dry-effects 0.1.2 → 0.2.0

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 (92) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +57 -5
  3. data/LICENSE +1 -1
  4. data/README.md +19 -11
  5. data/dry-effects.gemspec +33 -41
  6. data/lib/dry/effects/all.rb +4 -4
  7. data/lib/dry/effects/container.rb +1 -1
  8. data/lib/dry/effects/effect.rb +2 -2
  9. data/lib/dry/effects/effects/async.rb +3 -1
  10. data/lib/dry/effects/effects/cache.rb +13 -9
  11. data/lib/dry/effects/effects/cmp.rb +3 -1
  12. data/lib/dry/effects/effects/current_time.rb +4 -2
  13. data/lib/dry/effects/effects/defer.rb +3 -1
  14. data/lib/dry/effects/effects/env.rb +4 -2
  15. data/lib/dry/effects/effects/fork.rb +3 -1
  16. data/lib/dry/effects/effects/implicit.rb +4 -2
  17. data/lib/dry/effects/effects/interrupt.rb +4 -2
  18. data/lib/dry/effects/effects/lock.rb +8 -6
  19. data/lib/dry/effects/effects/parallel.rb +4 -2
  20. data/lib/dry/effects/effects/random.rb +5 -3
  21. data/lib/dry/effects/effects/reader.rb +1 -1
  22. data/lib/dry/effects/effects/resolve.rb +23 -3
  23. data/lib/dry/effects/effects/retry.rb +4 -2
  24. data/lib/dry/effects/effects/state.rb +4 -2
  25. data/lib/dry/effects/effects/timeout.rb +3 -1
  26. data/lib/dry/effects/effects/timestamp.rb +3 -1
  27. data/lib/dry/effects/errors.rb +4 -4
  28. data/lib/dry/effects/extensions/active_support/tagged_logging.rb +13 -0
  29. data/lib/dry/effects/extensions/auto_inject.rb +5 -5
  30. data/lib/dry/effects/extensions/system.rb +8 -7
  31. data/lib/dry/effects/extensions.rb +12 -4
  32. data/lib/dry/effects/frame.rb +30 -9
  33. data/lib/dry/effects/halt.rb +3 -3
  34. data/lib/dry/effects/handler.rb +1 -1
  35. data/lib/dry/effects/inflector.rb +1 -1
  36. data/lib/dry/effects/initializer.rb +17 -16
  37. data/lib/dry/effects/instruction.rb +1 -1
  38. data/lib/dry/effects/instructions/execute.rb +2 -1
  39. data/lib/dry/effects/instructions/raise.rb +2 -1
  40. data/lib/dry/effects/provider/class_interface.rb +2 -2
  41. data/lib/dry/effects/provider.rb +2 -2
  42. data/lib/dry/effects/providers/async.rb +2 -2
  43. data/lib/dry/effects/providers/cache.rb +2 -2
  44. data/lib/dry/effects/providers/cmp.rb +1 -1
  45. data/lib/dry/effects/providers/current_time/time_generators.rb +1 -1
  46. data/lib/dry/effects/providers/current_time.rb +5 -5
  47. data/lib/dry/effects/providers/defer.rb +6 -6
  48. data/lib/dry/effects/providers/env.rb +2 -2
  49. data/lib/dry/effects/providers/fork.rb +2 -2
  50. data/lib/dry/effects/providers/implicit.rb +1 -1
  51. data/lib/dry/effects/providers/interrupt.rb +3 -3
  52. data/lib/dry/effects/providers/lock.rb +6 -8
  53. data/lib/dry/effects/providers/parallel.rb +3 -3
  54. data/lib/dry/effects/providers/random.rb +74 -2
  55. data/lib/dry/effects/providers/reader.rb +1 -1
  56. data/lib/dry/effects/providers/resolve.rb +8 -7
  57. data/lib/dry/effects/providers/retry.rb +5 -7
  58. data/lib/dry/effects/providers/state.rb +2 -2
  59. data/lib/dry/effects/providers/timeout.rb +2 -2
  60. data/lib/dry/effects/providers/timestamp.rb +2 -2
  61. data/lib/dry/effects/stack.rb +6 -6
  62. data/lib/dry/effects/version.rb +1 -1
  63. data/lib/dry/effects.rb +7 -7
  64. metadata +22 -69
  65. data/.codeclimate.yml +0 -12
  66. data/.github/ISSUE_TEMPLATE/----please-don-t-ask-for-support-via-issues.md +0 -10
  67. data/.github/ISSUE_TEMPLATE/---bug-report.md +0 -30
  68. data/.github/ISSUE_TEMPLATE/---feature-request.md +0 -18
  69. data/.github/workflows/ci.yml +0 -74
  70. data/.github/workflows/docsite.yml +0 -34
  71. data/.github/workflows/sync_configs.yml +0 -34
  72. data/.gitignore +0 -12
  73. data/.rspec +0 -4
  74. data/.rubocop.yml +0 -95
  75. data/CODE_OF_CONDUCT.md +0 -13
  76. data/CONTRIBUTING.md +0 -29
  77. data/Gemfile +0 -23
  78. data/Rakefile +0 -8
  79. data/docsite/source/effects/cache.html.md +0 -84
  80. data/docsite/source/effects/current_time.html.md +0 -186
  81. data/docsite/source/effects/defer.html.md +0 -130
  82. data/docsite/source/effects/env.html.md +0 -144
  83. data/docsite/source/effects/interrupt.html.md +0 -109
  84. data/docsite/source/effects/parallel.html.md +0 -25
  85. data/docsite/source/effects/reader.html.md +0 -126
  86. data/docsite/source/effects/resolve.html.md +0 -188
  87. data/docsite/source/effects/state.html.md +0 -178
  88. data/docsite/source/effects/timeout.html.md +0 -42
  89. data/docsite/source/effects.html.md +0 -29
  90. data/docsite/source/index.html.md +0 -212
  91. data/examples/cmp.rb +0 -51
  92. data/examples/state.rb +0 -29
@@ -1,130 +0,0 @@
1
- ---
2
- title: Deferred execution
3
- layout: gem-single
4
- name: dry-effects
5
- ---
6
-
7
- `Defer` adds three methods for working with deferred code execution:
8
-
9
- - `defer` accepts a block and executes it (potentially) on a thread pool. It returns an object that can be awaited with `wait`. These objects are `Promise`s made by `concurrent-ruby`. You can use their API, but it's not fully supported and tested in conjunction with effects.
10
- - `wait` accepts a promise or an array of promises returned by `defer` and returns their values. The method blocks the current thread until all values are available.
11
- - `later` postpones block execution until the handler is finished (see examples below).
12
-
13
- ### Defer
14
-
15
- A simple example:
16
-
17
- ```ruby
18
- class CreateUser
19
- include Dry::Effects.Resolve(:user_repo, :send_invitation)
20
- include Dry::Effects.Defer
21
-
22
- def call(values)
23
- user = user_repo.create(values)
24
- defer { send_invitation.(user) }
25
- user
26
- end
27
- end
28
- ```
29
-
30
- In the code above, `send_invitation` is run on a thread pool. It's the simplest way to run code concurrently.
31
-
32
- Code within the `defer` block can use some effects but not all of them. For instance, `Interrupt` is not supported because you cannot return from one thread to another. This is a limitation of Ruby and threads in general.
33
-
34
- ### Handling
35
-
36
- The default handler uses `concurrent-ruby` to do the heavy lifting. As an option, it accepts the executor—usually a thread pool where the code will be run.
37
-
38
- > Three special values are also supported: `:io` returns the global pool for long, blocking (IO) tasks, `:fast` returns the global pool for short, fast operations, and `:immediate` returns the global ImmediateExecutor object.
39
-
40
- By default, `Dry::Effects::Handler.Defer` uses `:io`.
41
-
42
- ```ruby
43
- class HandleDefer
44
- include Dry::Effects::Handler.Defer(executor: :immediate)
45
-
46
- def initialize(app)
47
- @app = app
48
- end
49
-
50
- def call(env)
51
- # defer tasks in @app will be run on the same thread
52
- with_defer { @app.(env) }
53
- end
54
- end
55
- ```
56
-
57
- The executor can be passed directly to `with_defer`:
58
-
59
- ```ruby
60
- def call(env)
61
- with_defer(executor: :fast) { @app.(env) }
62
- end
63
- ```
64
-
65
- ### Using null executor
66
-
67
- For skipping deferred tasks, create a mocked executor
68
-
69
- ```ruby
70
- require 'concurrent/executor/executor_service'
71
-
72
- NullExecutor = Object.new.extend(Concurrent::ExecutorService).tap do |null|
73
- def null.post
74
- end
75
- end
76
- ```
77
-
78
- and provide it in middleware
79
-
80
- ```ruby
81
- class HandleDefer
82
- include Dry::Effects::Handler.Defer
83
- include Dry::Effects::Handler.Env(:environment)
84
-
85
- def initialize(app)
86
- @app = app
87
- end
88
-
89
- def call(env)
90
- with_defer(executor: executor) { @app.(env) }
91
- end
92
-
93
- def executor
94
- environment.equal?(:test) ? NullExecutor : :io
95
- end
96
- end
97
- ```
98
-
99
- ### Later
100
-
101
- `later` doesn't return a result that can be awaited. Instead, it starts deferred blocks on handler exit. Consider this example:
102
-
103
- ```ruby
104
- class CreateUser
105
- def call(values)
106
- user_repo.transaction do
107
- user = user_repo.create(values)
108
- defer { send_invitation.(user) }
109
- user_account = account_repo.create(user)
110
- user
111
- end
112
- end
113
- end
114
- ```
115
-
116
- There is no guarantee `send_invitation` will be run _after_ the transaction finishes. It may lead to race conditions or anomalies. If `account_repo.create` fails with an exception, the transaction will be rolled back yet the invitation will be sent!
117
-
118
- `later` captures the block but doesn't run it:
119
-
120
- ```ruby
121
- later { send_invitation.(user) }
122
- ```
123
-
124
- The invitaition will be sent when `with_defer` exits:
125
-
126
- ```ruby
127
- with_defer { @app.(env) }
128
- ```
129
-
130
- It usually happens outside of any transaction so that anomalies don't occur.
@@ -1,144 +0,0 @@
1
- ---
2
- title: Environment
3
- layout: gem-single
4
- name: dry-effects
5
- ---
6
-
7
- Configuring code via `ENV` can be handy, but testing it by mutating a global constant is usually not. Env is similar to Reader but exists for passing simple key-value pairs, precisely what `ENV` does.
8
-
9
- ### Providing environment
10
-
11
- ```ruby
12
- require 'dry/effects'
13
-
14
- class EnvironmentMiddleware
15
- include Dry::Effects::Handler.Env(environment: ENV['RACK_ENV'])
16
-
17
- def initialize(app)
18
- @app = app
19
- end
20
-
21
- def call(env)
22
- with_env { @app.(env) }
23
- end
24
- end
25
- ```
26
-
27
- ### Using environment
28
-
29
- By default, `Effects.Env` creates accessor to keys with the same name:
30
-
31
- ```ruby
32
- class CreateUser
33
- include Dry::Effects.Env(:environment)
34
-
35
- def call(...)
36
- #
37
- log unless environemnt.eql?('test')
38
- end
39
- end
40
- ```
41
-
42
- But you can pass a hash and use arbitrary method names:
43
-
44
- ```ruby
45
- class CreateUser
46
- include Dry::Effects.Env(env: :environment)
47
-
48
- def call(...)
49
- #
50
- log unless env.eql?('test')
51
- end
52
- end
53
- ```
54
-
55
- ### Interaction with `ENV`
56
-
57
- The Env handler will search for keys in `ENV` as a fallback:
58
-
59
- ```ruby
60
- class SendRequest
61
- include Dry::Effects.Env(endpoint: 'THIRD_PARTY')
62
-
63
- def call(...)
64
- # some code using `endpoint`
65
- end
66
- end
67
- ```
68
-
69
- In a production environment, it would be enough to pass `THIRD_PARTY` an environment variable and call `with_env` at the top of the application:
70
-
71
- ```ruby
72
- require 'dry/effects'
73
-
74
- class SidekiqEnvMiddleware
75
- include Dry::Effects::Handler.Env
76
-
77
- def call(_worker, _job, _queue, &block)
78
- with_env(&block)
79
- end
80
- end
81
-
82
- Sidekiq.configure_server do |config|
83
- config.server_middleware do |chain|
84
- chain.add SidekiqEnvMiddleware
85
- end
86
- end
87
- ```
88
-
89
- In tests, you can pass `THIRD_PARTY` without modifying `ENV`:
90
-
91
- ```ruby
92
- RSpec.describe SendRequest do
93
- include Dry::Effects::Handler.Env
94
-
95
- subject(:send_request) { described_class.new }
96
-
97
- let(:endpoint) { "fake server" }
98
-
99
- around { with_env('THIRD_PARTY' => endpoint, &ex) }
100
-
101
- it 'sends a request to a fake server' do
102
- send_request.(...)
103
- end
104
- end
105
- ```
106
-
107
- ### Overriding handlers
108
-
109
- By passing `overridable: true` you can override values provided by the nested handler:
110
-
111
- ```ruby
112
- class Application
113
- include Dry::Effects.Env(:foo)
114
-
115
- def call
116
- puts foo
117
- end
118
- end
119
-
120
- class ProvidingContext
121
- include Dry::Effects::Handler.Env
122
-
123
- def call
124
- with_env({ foo: 100 }, overridable: true) { yield }
125
- end
126
- end
127
-
128
- class OverridingContext
129
- include Dry::Effects::Handler.Env
130
-
131
- def call
132
- with_env(foo: 200) { yield }
133
- end
134
- end
135
-
136
- overriding = OverridingContext.new
137
- providing = ProvidingContext.new
138
- app = Application.new
139
-
140
- overriding.() { providing.() { app.() } }
141
- # prints 200, coming from overriding context
142
- ```
143
-
144
- Again, this is useful for testing, you pass `overridable: true` in the test environment and override environment values in specs.
@@ -1,109 +0,0 @@
1
- ---
2
- title: Interrupt
3
- layout: gem-single
4
- name: dry-effects
5
- ---
6
-
7
- Interrupt is an effect with the semantics of `raise`/`rescue` or `throw`/`catch`. It's added for consistency and compatibility with other effects. Underneath, it uses `raise` + `rescue` so that application code can detect the bubbling.
8
-
9
- ### Basic usage
10
-
11
- If you know what exceptions are, this should look familiar:
12
-
13
- ```ruby
14
- require 'dry/effects'
15
-
16
- class RunDivision
17
- include Dry::Effects::Handler.Interrupt(:division_by_zero, as: :catch_zero_division)
18
-
19
- def call
20
- error, answer = catch_zero_division do
21
- yield
22
- end
23
-
24
- if error
25
- :error
26
- else
27
- answer
28
- end
29
- end
30
- end
31
-
32
- class Divide
33
- include Dry::Effects.Interrupt(:division_by_zero)
34
-
35
- def call(dividend, divisor)
36
- if divisor.zero?
37
- division_by_zero
38
- else
39
- dividend / divisor
40
- end
41
- end
42
- end
43
-
44
- run = RunDivision.new
45
- divide = Divide.new
46
-
47
- app = -> a, b { run.() { divide.(a, b) } }
48
-
49
- app.(10, 2) # => 5
50
- app.(1, 0) # => :error
51
- ```
52
-
53
- The handler returns a flag indicating whether there was an interruption. `false` means the block was run without interruption, `true` stands for the code was interrupted at some point.
54
-
55
- ### Payload
56
-
57
- Interruption can have a payload:
58
-
59
- ```ruby
60
- class Callee
61
- include Dry::Effects.Interrupt(:halt)
62
-
63
- def call
64
- halt :foo
65
- end
66
- end
67
-
68
- class Caller
69
- include Dry::Effects::Handler.Interrupt(:halt, as: :catch_halt)
70
-
71
- def call
72
- _, result = catch_halt do
73
- yield
74
- :bar
75
- end
76
-
77
- result
78
- end
79
- end
80
-
81
- caller = Caller.new
82
- callee = Callee.new
83
-
84
- caller.() { callee.() } # => :foo
85
- caller.() { } # => :bar
86
- ```
87
-
88
- ### Composition
89
-
90
- Every Interrupt effect has to have an identifier so that they don't overlap. It's an equivalent of exception types. You can nest handlers with different identifiers; they will work just as you would expect:
91
-
92
- ```ruby
93
- class Catcher
94
- include Dry::Effects::Handler(:div_error, as: :catch_div)
95
- include Dry::Effects::Handler(:sqrt_error, as: :catch_sqrt)
96
-
97
- def call
98
- _, div_result = catch_div do
99
- _, sqrt_result = catch_sqrt do
100
- yield
101
- end
102
-
103
- sqrt_result
104
- end
105
-
106
- div_result
107
- end
108
- end
109
- ```
@@ -1,25 +0,0 @@
1
- ---
2
- title: Parallel execution
3
- layout: gem-single
4
- name: dry-effects
5
- ---
6
-
7
- There are two effects for using parallelism:
8
-
9
- - `par` creates a unit of parallel execution;
10
- - `join` combines an array of units to the array of their results.
11
-
12
- `par`/`join` is almost identical to `defer`/`wait` from [`Defer`](/gems/dry-effects/0.1/effects/defer), the difference is in the semantics. `defer` is not supposed to be always awaited, it's usually fired-and-forgotten. On the contrary, `par` is meant to be `join`ed at some point.
13
-
14
- ```ruby
15
- class MakeRequests
16
- include Dry::Effects.Resolve(:make_request)
17
-
18
- def call(urls)
19
- # Run every request in parallel and combine their results
20
- urls.map { |url| par { make_request.(url) } }.then { |pars| join(pars) }
21
- end
22
- end
23
- ```
24
-
25
- Just as [`Defer`](/gems/dry-effects/0.1/effects/defer), `Parallel` uses `concurrent-ruby` under the hood.
@@ -1,126 +0,0 @@
1
- ---
2
- title: Reader
3
- layout: gem-single
4
- name: dry-effects
5
- ---
6
-
7
- Reader is the simplest effect. It passes a value down to the stack.
8
-
9
- ```ruby
10
- require 'dry/effects'
11
-
12
- class SetLocaleMiddleware
13
- include Dry::Effects::Handler.Reader(:locale)
14
-
15
- def initialize(app)
16
- @app = app
17
- end
18
-
19
- def call(env)
20
- with_locale(detect_locale(env)) do
21
- @app.(env)
22
- end
23
- end
24
-
25
- def detect_locale(env)
26
- # arbitrary detection logic
27
- end
28
- end
29
-
30
- ### Anywhere in the app
31
-
32
- class GreetUser
33
- include Dry::Effects.Reader(:locale)
34
-
35
- def call(user)
36
- case locale
37
- when :en then "Hello #{user.name}"
38
- when :de then "Hallo #{user.name}"
39
- when :ru then "Привет, #{user.name}"
40
- when :it then "Ciao #{user.name}"
41
- end
42
- end
43
- end
44
- ```
45
-
46
- ### Testing with Reader
47
-
48
- If you run `GreetUser#call` without a Reader handler, it will raise an error. For unit tests you'll need some wrapping code:
49
-
50
- ```ruby
51
- RSpec.describe GreetUser do
52
- include Dry::Effects::Handler.Reader(:locale)
53
-
54
- subject(:greet) { described_class.new }
55
-
56
- let(:user) { double(:user, name: 'John') }
57
-
58
- it 'uses the current locale to greet the user' do
59
- examples = {
60
- en: 'Hello John',
61
- de: 'Hallo John',
62
- ru: 'Привет, John',
63
- it: 'Ciao John'
64
- }
65
-
66
- examples.each do |locale, expected_greeting|
67
- with_locale(locale) do
68
- expect(greet.(user)).to eql(expected_greeting)
69
- end
70
- end
71
- end
72
- end
73
- ```
74
-
75
- You can provide locale in an `around(:each)` hook:
76
-
77
- ```ruby
78
- require 'dry/effects'
79
-
80
- # Build a provider object with .call interface
81
- locale_provider = Object.new.extend(Dry::Effects::Handler.Reader(:locale, as: :call))
82
-
83
- RSpec.configure do |config|
84
- config.around(:each) do |ex|
85
- locale_provider.(:en, &ex)
86
- end
87
- end
88
- ```
89
-
90
- ### Nesting readers
91
-
92
- As a general rule, if there are two handlers in the stack, the nested takes precedence:
93
-
94
- ```ruby
95
- require 'dry/effects'
96
-
97
- extend Dry::Effects::Handler.Reader(:locale)
98
- extend Dry::Effects.Reader(:locale)
99
-
100
- with_locale(:en) { with_locale(:de) { locale } } # => :de
101
- ```
102
-
103
- ### Mixing readers
104
-
105
- Every Reader has an identifier. Handlers with different identifiers won't interfere:
106
-
107
- ```ruby
108
- require 'dry/effects'
109
-
110
- extend Dry::Effects::Handler.Reader(:locale)
111
- extend Dry::Effects::Handler.Reader(:context)
112
- extend Dry::Effects.Reader(:locale)
113
- extend Dry::Effects.Reader(:context)
114
-
115
- with_locale(:en) { with_context(:background) { [locale, context] } } # => [:en, :background]
116
- # Order doesn't matter:
117
- with_context(:background) { with_locale(:en) { [locale, context] } } # => [:en, :background]
118
- ```
119
-
120
- ### Relation to State
121
-
122
- Reader is part of the [State](/gems/dry-effects/0.1/effects/state) effect.
123
-
124
- ### Tradeoffs of implicit passing
125
-
126
- Passing values implicitly is not good or bad by itself; you should consider how it affects your code in every case. Providing the current locale is a good example where reader effect can be justified. On the other hand, passing optional values such as the IP-address of the current user should be done explicitly because they are not always present (consider background jobs, rake tasks, etc.).