dry-effects 0.1.0 → 0.1.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,109 @@
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
+ success, answer = catch_zero_division do
21
+ yield
22
+ end
23
+
24
+ if success
25
+ answer
26
+ else
27
+ :error
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
+ ```
@@ -0,0 +1,25 @@
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.
@@ -0,0 +1,126 @@
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.).
@@ -0,0 +1,188 @@
1
+ ---
2
+ title: Resolve (Dependency Injection)
3
+ layout: gem-single
4
+ name: dry-effects
5
+ ---
6
+
7
+ Resolve is an effect for injecting dependencies. A simple usage example:
8
+
9
+ ```ruby
10
+ require 'dry/effects'
11
+
12
+ class CreateUser
13
+ include Dry::Effects.Resolve(:user_repo)
14
+
15
+ def call(values)
16
+ name = values.values_at(:first_name, :last_name).join(' ')
17
+ user_repo.create(values.merge(name: name))
18
+ end
19
+ end
20
+ ```
21
+
22
+ Providing `user_repo` in tests:
23
+
24
+ ```ruby
25
+ RSpec.describe CreateUser do
26
+ # adds #provide
27
+ include Dry::Effects::Handler.Resolve
28
+
29
+ subject(:create_user) { described_class.new }
30
+
31
+ let(:user_repo) { double(:user_repo) }
32
+
33
+ it 'creates a user' do
34
+ expect(user_repo).to receive(:create).with(
35
+ first_name: 'John',
36
+ last_name: 'Doe',
37
+ name: 'John Doe'
38
+ )
39
+
40
+ provide(user_repo: user_repo) { create_user.(first_name: 'John', last_name: 'Doe') }
41
+ end
42
+ end
43
+ ```
44
+
45
+ Providing dependencies with middleware:
46
+
47
+ ```ruby
48
+ class ProviderMiddleware
49
+ include Dry::Effects::Handler.Resolve
50
+
51
+ def initialize(app, dependencies)
52
+ @app = app
53
+ @dependencies = dependencies
54
+ end
55
+
56
+ def call(env)
57
+ provide(@dependencies) { @app.(env) }
58
+ end
59
+ end
60
+ ```
61
+
62
+ Then in `config.ru`:
63
+
64
+ ```ruby
65
+ # ...some bootstrapping code ...
66
+
67
+ use ProviderMiddleware, user_repo: UserRepo.new
68
+ run Application.new
69
+ ```
70
+
71
+ ### Compatibility with `dry-container` and `dry-system`
72
+
73
+ Any object that responds to `.key?` and `.[]` can be used for providing dependencies. Thus, the default Resolve provider is compatible with `dry-container` and `dry-system` out of the box.
74
+
75
+ ```ruby
76
+ def call(env)
77
+ # Assuming App is a subclass of Dry::System::Container
78
+ provide(App) { @app.(env) }
79
+ end
80
+ ```
81
+
82
+ ### Providing static values
83
+
84
+ One can pass a container to the module builder:
85
+
86
+ ```ruby
87
+ class ProviderMiddleware
88
+ include Dry::Effects::Handler.Resolve(Application)
89
+
90
+ def initialize(app)
91
+ @app = app
92
+ end
93
+
94
+ def call(env)
95
+ # Here Application will be used for resolving dependencies
96
+ provide { @app.(env) }
97
+ end
98
+ end
99
+ ```
100
+
101
+ ### Injecting many keys and using aliases
102
+
103
+ ```ruby
104
+ require 'dry/effects'
105
+
106
+ class CreateUser
107
+ include Dry::Effects.Resolve(
108
+ # Injected as .schema
109
+ # but resolved with 'operations.create_user.schema'
110
+ 'operations.create_user.schema',
111
+ # Injected as .repo
112
+ # but resolved with 'repos.user_repo'
113
+ repo: 'repos.user_repo'
114
+ )
115
+
116
+ def call(values)
117
+ result = schema.(values)
118
+
119
+ if result.success?
120
+ user = repo.create(result.to_h)
121
+ [:ok, user]
122
+ else
123
+ [:err, result]
124
+ end
125
+ end
126
+ end
127
+ ```
128
+
129
+ ### Overriding dependencies in test environment
130
+
131
+ Sometimes you may want to push dependencies through an existing handler. This is normally needed for testing when you want to replace some dependencies in a test environment for an assembled app, like a Rack application. Passing `overridable: true` enables it:
132
+
133
+ ```ruby
134
+ require 'dry/effects'
135
+
136
+ class ProviderMiddleware
137
+ include Dry::Effects::Handler.Resolve
138
+
139
+ def initialize(app)
140
+ @app = app
141
+ end
142
+
143
+ def call(env)
144
+ provide(Application, overridable: overridable?) { @app.(env) }
145
+ end
146
+
147
+ def overridable?
148
+ ENV['RACK_ENV'].eql?('test')
149
+ end
150
+ end
151
+ ```
152
+
153
+ Now in tests, you can override some dependencies at will:
154
+
155
+ ```ruby
156
+ require 'dry/effects'
157
+ require 'rack/test'
158
+
159
+ RSpec.describe do
160
+ include Rack::Test::Methods
161
+ include Dry::Effects::Handler.Provider
162
+
163
+ let(:app) do
164
+ # building an assembled rack app
165
+ end
166
+
167
+ describe 'POST /users' do
168
+ let(:user_repo) { double(:user_repo) }
169
+
170
+ it 'creates a user' do
171
+ expect(user_repo).to receive(:create).with(
172
+ first_name: 'John', last_name: 'Doe'
173
+ ).and_return(1)
174
+
175
+ # Overriding one dependency
176
+ # It will only work if `overridable: true` is passed
177
+ # in the middleware
178
+ provide('repos.user_repo' => user_repo) do
179
+ post(
180
+ '/users',
181
+ JSON.dump(first_name: 'John', last_name: 'Doe'),
182
+ 'CONTENT_TYPE' => 'application/json'
183
+ )
184
+ end
185
+ end
186
+ end
187
+ end
188
+ ```