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,186 @@
1
+ ---
2
+ title: Current Time
3
+ layout: gem-single
4
+ name: dry-effects
5
+ ---
6
+
7
+ Obtaining the current time with `Time.now` is a classic example of a side effect. Code relying on accessing system time is harder to test. One possible solution is passing time around explicitly, but using effects can save you some typing depending on the case.
8
+
9
+ Providing and obtaining the current time is straightforward:
10
+
11
+ ```ruby
12
+ require 'dry/effects'
13
+
14
+ class CurrentTimeMiddleware
15
+ include Dry::Effects::Handler.CurrentTime
16
+
17
+ def initialize(app)
18
+ @app = app
19
+ end
20
+
21
+ def call(env)
22
+ # It will use Time.now internally once and set it fixed
23
+ with_current_time do
24
+ @app.(env)
25
+ end
26
+ end
27
+ end
28
+
29
+ ###
30
+
31
+ class CreateSubscription
32
+ include Dry::Efefcts.Resolve(:subscription_repo)
33
+ include Dry::Effects.CurrentTime
34
+
35
+ def call(values)
36
+ subscription_repo.create(
37
+ values.merge(start_at: current_time)
38
+ )
39
+ end
40
+ end
41
+ ```
42
+
43
+ ### Providing time in tests
44
+
45
+ A typical usage would be:
46
+
47
+ ```ruby
48
+ require 'dry/effects'
49
+
50
+ RSpec.configure do |config|
51
+ config.include Dry::Effects::Handler.CurrentTime
52
+ config.include Dry::Effects.CurrentTime
53
+
54
+ config.around { |ex| with_current_time(&ex) }
55
+ end
56
+ ```
57
+
58
+ Then anywhere in tests, you can use it:
59
+
60
+ ```ruby
61
+ it 'uses current time as a start' do
62
+ subscription = create_subscription(...)
63
+ expect(subscription.start_at).to eql(current_time)
64
+ end
65
+ ```
66
+
67
+ To change the time, call `with_current_time` with a proc:
68
+
69
+ ```ruby
70
+ it 'closes a subscription with current time' do
71
+ future = current_time + 86_400
72
+ closed_subscription = with_current_time(proc { future }) { close_subscription(subscription) }
73
+ expect(closed_subscription.closed_at).to eql(future)
74
+ end
75
+ ```
76
+
77
+ Wrapping time with a proc is required, read about generators below.
78
+
79
+ ### Time rounding
80
+
81
+ `current_time` accepts an argument for rounding time values. It can be passed statically to the module builder or dynamically to the effect constructor:
82
+
83
+ ```ruby
84
+ class CreateSubscription
85
+ include Dry::Effects.CurrentTime(round: 3)
86
+
87
+ def call(...)
88
+ # value will be rounded to milliseconds
89
+ current_time
90
+ # value will be rounded to microseconds
91
+ current_time(round: 6)
92
+ end
93
+ end
94
+ ```
95
+
96
+ ### Time is fixed
97
+
98
+ By default, calling `with_current_time` even without arguments will freeze the current time. This means `current_time` will return the same value during request processing etc.
99
+
100
+ You can "unfix" time with passing `fixed: false` to the handler builder:
101
+
102
+ ```ruby
103
+ include Dry::Effects::Handler.CurrentTime(fixed: false)
104
+ ```
105
+
106
+ However, this is not recommended because it will make the behavior of `current_time` different in tests (where you pass a fixed value) and in a production environment.
107
+
108
+ ### Using a custom generator
109
+
110
+ The default time provider accepts a custom generator which is a simple callable object. This way you can pass a proc with fixed time:
111
+
112
+ ```ruby
113
+ frozen = Time.now
114
+ with_fixed_time(proc { frozen }) do
115
+ # ...
116
+ end
117
+ ```
118
+
119
+ Or you can change time on every call:
120
+
121
+ ```ruby
122
+ start = Time.now
123
+ with_fixed_time(proc { start += 0.1 }) do
124
+ # ...
125
+ end
126
+ ```
127
+
128
+ ### Discrete time shifts
129
+
130
+ If you pass `step: x` to the handler, it will shift the current time on every access by `x`:
131
+
132
+ ```ruby
133
+ with_fixed_time(step: 0.1) do
134
+ current_time # => ... 18:00:00.000
135
+ current_time # => ... 18:00:00.100
136
+ current_time # => ... 18:00:00.200
137
+ end
138
+ ```
139
+
140
+ You can also pass initial time:
141
+
142
+ ```ruby
143
+ initial = Time.new(1970)
144
+ with_fixed_time(initial: initial, step: 60) do
145
+ current_time # => 1970-01-01 00:00:00 +0000
146
+ current_time # => 1970-01-01 00:01:00 +0000
147
+ current_time # => 1970-01-01 00:02:00 +0000
148
+ end
149
+ ```
150
+
151
+ ### Overriding handlers
152
+
153
+ Handlers of current time can be overridden by an outer handler if you pass `overridable: true`:
154
+
155
+ ```ruby
156
+ require 'dry/effects'
157
+
158
+ class CurrentTimeMiddleware
159
+ include Dry::Effects::Handler.CurrentTime
160
+
161
+ def initialize(app)
162
+ @app = app
163
+ end
164
+
165
+ def call(env)
166
+ with_current_time(overridable: ENV['RACK_ENV'].eql?('test')) do
167
+ @app.(env)
168
+ end
169
+ end
170
+ end
171
+ ```
172
+
173
+ It's usually done in tests:
174
+
175
+ ```ruby
176
+ # Using global time
177
+ frozen_time = Time.now
178
+
179
+ puts "Running with time #{frozen_time.iso8601}" if ENV['CI']
180
+
181
+ RSpec.configure do |config|
182
+ config.include Dry::Effects::Handler.CurrentTime
183
+ config.include(Module.new { define_method(:current_time) { frozen_time } })
184
+ config.around { |ex| with_current_time(proc { frozen_time }, &ex) }
185
+ end
186
+ ```
@@ -0,0 +1,130 @@
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.
@@ -0,0 +1,144 @@
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.