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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +17 -9
- data/dry-effects.gemspec +27 -35
- data/lib/dry/effects/extensions.rb +8 -0
- data/lib/dry/effects/extensions/active_support/tagged_logging.rb +13 -0
- data/lib/dry/effects/version.rb +1 -1
- metadata +18 -47
- data/.codeclimate.yml +0 -12
- data/.github/ISSUE_TEMPLATE/----please-don-t-ask-for-support-via-issues.md +0 -10
- data/.github/ISSUE_TEMPLATE/---bug-report.md +0 -30
- data/.github/ISSUE_TEMPLATE/---feature-request.md +0 -18
- data/.github/workflows/ci.yml +0 -52
- data/.github/workflows/docsite.yml +0 -34
- data/.github/workflows/sync_configs.yml +0 -56
- data/.gitignore +0 -12
- data/.rspec +0 -4
- data/.rubocop.yml +0 -101
- data/CODE_OF_CONDUCT.md +0 -13
- data/CONTRIBUTING.md +0 -29
- data/Gemfile +0 -20
- data/Gemfile.devtools +0 -14
- data/Rakefile +0 -8
- data/docsite/source/effects.html.md +0 -29
- data/docsite/source/effects/cache.html.md +0 -84
- data/docsite/source/effects/current_time.html.md +0 -186
- data/docsite/source/effects/defer.html.md +0 -130
- data/docsite/source/effects/env.html.md +0 -144
- data/docsite/source/effects/interrupt.html.md +0 -109
- data/docsite/source/effects/parallel.html.md +0 -25
- data/docsite/source/effects/reader.html.md +0 -126
- data/docsite/source/effects/resolve.html.md +0 -188
- data/docsite/source/effects/state.html.md +0 -178
- data/docsite/source/effects/timeout.html.md +0 -44
- data/docsite/source/index.html.md +0 -212
- data/examples/cmp.rb +0 -51
- data/examples/state.rb +0 -29
- data/project.yml +0 -2
@@ -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.).
|