dry-effects 0.1.4 → 0.1.5

Sign up to get free protection for your applications and to get access to all the features.
@@ -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.).