dry-effects 0.1.0 → 0.1.1

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.
@@ -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
+ ```