aye_commander 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/LICENSE +21 -0
- data/README.adoc +720 -0
- data/lib/aye_commander.rb +19 -0
- data/lib/aye_commander/abortable.rb +25 -0
- data/lib/aye_commander/callable.rb +41 -0
- data/lib/aye_commander/command.rb +30 -0
- data/lib/aye_commander/commander.rb +120 -0
- data/lib/aye_commander/errors.rb +24 -0
- data/lib/aye_commander/hookable.rb +82 -0
- data/lib/aye_commander/initializable.rb +18 -0
- data/lib/aye_commander/inspectable.rb +36 -0
- data/lib/aye_commander/ivar.rb +94 -0
- data/lib/aye_commander/limitable.rb +83 -0
- data/lib/aye_commander/resultable.rb +58 -0
- data/lib/aye_commander/shareable.rb +32 -0
- data/lib/aye_commander/status.rb +48 -0
- data/lib/aye_commander/version.rb +3 -0
- data/spec/aye_commander/abortable_spec.rb +24 -0
- data/spec/aye_commander/callable_spec.rb +47 -0
- data/spec/aye_commander/command_spec.rb +27 -0
- data/spec/aye_commander/commander_spec.rb +168 -0
- data/spec/aye_commander/errors_spec.rb +25 -0
- data/spec/aye_commander/hookable_spec.rb +105 -0
- data/spec/aye_commander/initializable_spec.rb +33 -0
- data/spec/aye_commander/inspectable_spec.rb +45 -0
- data/spec/aye_commander/ivar_spec.rb +130 -0
- data/spec/aye_commander/limitable_spec.rb +131 -0
- data/spec/aye_commander/resultable_spec.rb +64 -0
- data/spec/aye_commander/shareable_spec.rb +38 -0
- data/spec/aye_commander/status_spec.rb +97 -0
- data/spec/aye_commander_spec.rb +5 -0
- data/spec/spec_helper.rb +49 -0
- metadata +90 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: fa72a9da4e159367e9e08edd1a76360e61f105e7
|
4
|
+
data.tar.gz: 3c9cbe60d130049f9d6a16db3eadf6c8df609914
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 17422f08ebb5442a8f43f9a00cd48439957d4ba194bc39440ded506f28d44f547a37dba268b0f568063e4b2b6d7cbb61481da8a6f5d104755a03635726318aa7
|
7
|
+
data.tar.gz: df4f7f0e63fa53bbf4e99d388253f20089b3f70cd311620e700641baea51a92317038a50837be04d62011e77ce45b1790678d1787e4f1f56c5163d0c1d884cc3
|
data/LICENSE
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
The MIT License (MIT)
|
2
|
+
|
3
|
+
Copyright (c) 2016 Juan Ramón
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
7
|
+
in the Software without restriction, including without limitation the rights
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
10
|
+
furnished to do so, subject to the following conditions:
|
11
|
+
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
13
|
+
copies or substantial portions of the Software.
|
14
|
+
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
21
|
+
SOFTWARE.
|
data/README.adoc
ADDED
@@ -0,0 +1,720 @@
|
|
1
|
+
// Asciidoctor Source
|
2
|
+
// AyeCommander README
|
3
|
+
//
|
4
|
+
// Original author:
|
5
|
+
// - pyzlnar
|
6
|
+
//
|
7
|
+
// Notes:
|
8
|
+
// Compile with: $ asciidoctor README.adoc
|
9
|
+
|
10
|
+
= AyeCommander
|
11
|
+
[A small command gem]
|
12
|
+
:toc:
|
13
|
+
:showtitle:
|
14
|
+
:source-highlighter: coderay
|
15
|
+
|
16
|
+
image:https://travis-ci.org/pyzlnar/aye_commander.svg?branch=master["Build Status", link="https://travis-ci.org/pyzlnar/aye_commander"]
|
17
|
+
image:https://codeclimate.com/github/pyzlnar/aye_commander/badges/gpa.svg["Code Climate", link="https://codeclimate.com/github/pyzlnar/aye_commander"]
|
18
|
+
image:https://codeclimate.com/github/pyzlnar/aye_commander/badges/coverage.svg["Test Coverage", link="https://codeclimate.com/github/pyzlnar/aye_commander/coverage"]
|
19
|
+
|
20
|
+
== Requirements
|
21
|
+
|
22
|
+
- Ruby version >= 2.0
|
23
|
+
|
24
|
+
== Installation
|
25
|
+
|
26
|
+
To use AyeCommander add it to your Gemfile:
|
27
|
+
|
28
|
+
[source,ruby]
|
29
|
+
gem 'aye_commander'
|
30
|
+
|
31
|
+
And bundle!
|
32
|
+
|
33
|
+
[source,ruby]
|
34
|
+
bundle install
|
35
|
+
|
36
|
+
Or to use without bundler, install the gem:
|
37
|
+
|
38
|
+
[source,ruby]
|
39
|
+
gem install aye_commander
|
40
|
+
|
41
|
+
And then require it from your code
|
42
|
+
|
43
|
+
[source,ruby]
|
44
|
+
require 'aye_commander'
|
45
|
+
|
46
|
+
== Introduction
|
47
|
+
|
48
|
+
AyeCommander is a gem that helps to develop classes that follow the command pattern.
|
49
|
+
|
50
|
+
=== What is a Command?
|
51
|
+
|
52
|
+
[quote, Russ Oslen]
|
53
|
+
____
|
54
|
+
A command is an object that does nothing but wait to be executed and, when executed, goes out and
|
55
|
+
performs an application-specific task.
|
56
|
+
____
|
57
|
+
|
58
|
+
Simply put, a command is an object that does but one thing. So if only does one thing... why would
|
59
|
+
you need to use them?
|
60
|
+
|
61
|
+
=== When to use a Command
|
62
|
+
|
63
|
+
Let's imagine that we have to do a complicated operation in a web application, like charging money.
|
64
|
+
Just the charging alone might involved consuming one or more services to authorize and charge the
|
65
|
+
card, save several records with information about the payments and so on and so forth.
|
66
|
+
|
67
|
+
Writing all this code in a model is not exactly correct since it handles way more than just one
|
68
|
+
model and using a controller would not only make a fat controller, but also harder to read.
|
69
|
+
|
70
|
+
If we instead write all this logic in one (or more) commands, the code becomes not only easier to
|
71
|
+
read and understand, but also easier to reuse on a different context.
|
72
|
+
|
73
|
+
[source,ruby]
|
74
|
+
----
|
75
|
+
# Instead of letting the model handle more responsability than it should
|
76
|
+
class Order
|
77
|
+
def create_order
|
78
|
+
charge_card
|
79
|
+
save_payment
|
80
|
+
update_order
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
84
|
+
# Or polluting the controller with more than just, "How to respond to the end user?"
|
85
|
+
class OrdersController
|
86
|
+
def create
|
87
|
+
charge_card
|
88
|
+
save_payment
|
89
|
+
update_order
|
90
|
+
flash[:notice] = "Everything went well"
|
91
|
+
end
|
92
|
+
end
|
93
|
+
|
94
|
+
# Why not extract it all a command
|
95
|
+
class CheckoutOrderCommand
|
96
|
+
include AyeCommander::Command
|
97
|
+
def call
|
98
|
+
charge_card
|
99
|
+
save_payment
|
100
|
+
update_order
|
101
|
+
end
|
102
|
+
end
|
103
|
+
|
104
|
+
# Or maybe even several commands
|
105
|
+
class CheckoutOrderCommand
|
106
|
+
include AyeCommander::Commander
|
107
|
+
execute ChargeCardCommand, SavePaymentCommand, UpdateOrderCommand
|
108
|
+
end
|
109
|
+
----
|
110
|
+
|
111
|
+
=== When to NOT use a Command
|
112
|
+
|
113
|
+
As stated before a command is an object that does one thing. +
|
114
|
+
This simple definition may make it tempting to write commands left and right, but never forget that
|
115
|
+
you need to https://en.wikipedia.org/wiki/KISS_principle[KISS]. If what you're trying to do is
|
116
|
+
simple it doesn't really need be extracted into a command.
|
117
|
+
|
118
|
+
Ok, lets get cracking!
|
119
|
+
|
120
|
+
== The Command
|
121
|
+
|
122
|
+
Creating a command is really easy, you only need to do two things to get rocking:
|
123
|
+
|
124
|
+
- Include the `AyeCommander::Command` module
|
125
|
+
- Define a method named `call`
|
126
|
+
|
127
|
+
[source,ruby]
|
128
|
+
----
|
129
|
+
class ObtainRandomCommand
|
130
|
+
include AyeCommander::Command
|
131
|
+
|
132
|
+
def call
|
133
|
+
@random = array.sample
|
134
|
+
end
|
135
|
+
end
|
136
|
+
----
|
137
|
+
|
138
|
+
To use the command, you simply call it from somewhere else.
|
139
|
+
|
140
|
+
[source,ruby]
|
141
|
+
----
|
142
|
+
result = ObtainRandomCommand.call(array: [1, 2, 3])
|
143
|
+
=> #<ObtainRandomCommand::Result @status: success, @array: [1, 2, 3], @random: 3>
|
144
|
+
|
145
|
+
result.random
|
146
|
+
=> 3
|
147
|
+
----
|
148
|
+
|
149
|
+
It really doesn't get simpler than that, but there's actually more to a command than that, so lets
|
150
|
+
have a look at the more complicated parts.
|
151
|
+
|
152
|
+
== Limiting the arguments
|
153
|
+
|
154
|
+
As you keep working with commands, you may realize that's actually a bit complicated to know what a
|
155
|
+
command expects to receive as arguments, what's the minimum necessary it needs to work and which of
|
156
|
+
all the variables returned in the result are actually relevant to you.
|
157
|
+
|
158
|
+
=== Receiving Arguments
|
159
|
+
|
160
|
+
AyeCommander comes with two ways of limiting the arguments that your command needs to be able to
|
161
|
+
run: `requires` and `receives`.
|
162
|
+
|
163
|
+
A `requires` tells the command that it can't run properly without having said arguments so it will
|
164
|
+
in fact raise a `MissingRequiredArgumentError` if the command is called without said arguments.
|
165
|
+
|
166
|
+
A `receives` tells the command that it can *ONLY* run the command with that set of arguments, and
|
167
|
+
that receiving any extra is actually an error. In this case if a command receives any surplus, an
|
168
|
+
error is raised.
|
169
|
+
|
170
|
+
Arguments in `requires` are automatically added to `receives`, but no exception error is raised
|
171
|
+
unless you actually use a `receives`.
|
172
|
+
|
173
|
+
All validations can be skipped by sending the `:skip_validations` option when calling the command.
|
174
|
+
|
175
|
+
=== Returning Arguments
|
176
|
+
|
177
|
+
So now that your command ran, your result might end up with a bunch of variables that you may
|
178
|
+
actually not even need. If that's the case then you can use the `returns` method which as you might
|
179
|
+
imagine, cleans up the result by just returning the variables that you specified.
|
180
|
+
|
181
|
+
=== Limiters Examples
|
182
|
+
|
183
|
+
[source,ruby]
|
184
|
+
----
|
185
|
+
class SimpleCommand
|
186
|
+
include AyeCommander::Command
|
187
|
+
end
|
188
|
+
|
189
|
+
# At this point, our command will receive and return everything and anything.
|
190
|
+
SimpleCommand.call(something: :or, other: :var)
|
191
|
+
=> #<SimpleCommand::Result @status: success, @something: or, @other: var>
|
192
|
+
|
193
|
+
class SimpleCommand
|
194
|
+
requires :these, :two
|
195
|
+
end
|
196
|
+
|
197
|
+
# Now calling the command without :these and :two will raise an error
|
198
|
+
SimpleCommand.call
|
199
|
+
=> AyeCommander::MissingRequiredArgumentError: Missing required arguments: [:these, :two]
|
200
|
+
|
201
|
+
SimpleCommand.call(these: 1, two: 2)
|
202
|
+
=> #<SimpleCommand::Result @status: success, @these: 1, @two: 2>
|
203
|
+
|
204
|
+
# Adding any extras at this point is still ok!
|
205
|
+
SimpleCommand.call(these: 1, two: 2, three: 3)
|
206
|
+
=> #<SimpleCommand::Result @status: success, @these: 1, @two: 2, @three: 3>
|
207
|
+
|
208
|
+
class SimpleCommand
|
209
|
+
receives :four
|
210
|
+
end
|
211
|
+
|
212
|
+
# Now that a receives has been used, any extra arguments sent will raise an error
|
213
|
+
SimpleCommand.call(these: 1, two: 2, three: 3)
|
214
|
+
=> AyeCommander::UnexpectedReceivedArgumentError: Received unexpected arguments: [:three]
|
215
|
+
|
216
|
+
SimpleCommand.call(these: 1, two: 2, four: 4)
|
217
|
+
=> #<SimpleCommand::Result @status: success, @these: 1, @two: 2, @four: 4>
|
218
|
+
|
219
|
+
# Not sending something that is on the receives is ok as well!
|
220
|
+
SimpleCommand.call(these: 1, two: 2)
|
221
|
+
=> #<SimpleCommand::Result @status: success, @these: 1, @two: 2>
|
222
|
+
|
223
|
+
class SimpleCommand
|
224
|
+
returns :sum
|
225
|
+
|
226
|
+
def call
|
227
|
+
@sum = these + two
|
228
|
+
end
|
229
|
+
end
|
230
|
+
|
231
|
+
# Finally a returns will help clean up the result at the end!
|
232
|
+
SimpleCommand.call(these: 1, two: 2, four: 4)
|
233
|
+
=> #<SimpleCommand::Result @status: success, @sum: 3>
|
234
|
+
|
235
|
+
# At any point you can override the receives requires or returns.
|
236
|
+
|
237
|
+
# Skips receives and requires
|
238
|
+
SimpleCommand.call(skip_validations: true)
|
239
|
+
|
240
|
+
# Skips either
|
241
|
+
SimpleCommand.call(skip_validations: :receives)
|
242
|
+
SimpleCommand.call(skip_validations: :requires)
|
243
|
+
|
244
|
+
# Skips result cleanup
|
245
|
+
SimpleCommand.call(skip_cleanup: true)
|
246
|
+
----
|
247
|
+
|
248
|
+
== What's in a status?
|
249
|
+
|
250
|
+
As you may have noticed by now, every time a command is called a `status` is returned regardless
|
251
|
+
of whether or not we cleanup. So what exactly is a status?
|
252
|
+
|
253
|
+
Well, at its simplest form the status tells us the whether or not the command has succeeded. By
|
254
|
+
default a command will be successful, and will fail if you change the status to *ANYTHING* that's
|
255
|
+
not `:success`.
|
256
|
+
|
257
|
+
[source,ruby]
|
258
|
+
----
|
259
|
+
class ReactorStatusCommand
|
260
|
+
include AyeCommander::Command
|
261
|
+
|
262
|
+
def call?
|
263
|
+
success? # => true
|
264
|
+
@status = :meltdown
|
265
|
+
success? # => false
|
266
|
+
end
|
267
|
+
end
|
268
|
+
|
269
|
+
ReactorStatusCommand.call.failure?
|
270
|
+
=> true
|
271
|
+
----
|
272
|
+
|
273
|
+
As a side note you can use the `fail!` method to fail the command at any point.
|
274
|
+
[source,ruby]
|
275
|
+
----
|
276
|
+
def call
|
277
|
+
# These lines are functionally identical
|
278
|
+
@status = :failure
|
279
|
+
fail!
|
280
|
+
|
281
|
+
# So are these
|
282
|
+
@status = :meltdown
|
283
|
+
fail!(:meltdown)
|
284
|
+
end
|
285
|
+
----
|
286
|
+
|
287
|
+
NOTE: Failing a command *WILL NOT* stop the rest of the code from running. (More on that later)
|
288
|
+
|
289
|
+
=== Multiple succeeds
|
290
|
+
|
291
|
+
Up to this point the status may seem a bit bland... And you may be right!
|
292
|
+
|
293
|
+
A status can tell you more than just a simple suceed and fail! It can tell you how it succeeded or
|
294
|
+
how it failed. Doing this with failures is fairly easy, since anything that's not `:success` is
|
295
|
+
considered a failure, but how do you we add more statuses as successes?
|
296
|
+
|
297
|
+
[source,ruby]
|
298
|
+
----
|
299
|
+
class CreateUserTokenCommand
|
300
|
+
include AyeCommander::Command
|
301
|
+
succeeds_with :previously_created
|
302
|
+
|
303
|
+
def call
|
304
|
+
status # => :success
|
305
|
+
if user.token.present?
|
306
|
+
@status = :previously_created
|
307
|
+
success? # => true
|
308
|
+
else
|
309
|
+
user.create_random_token
|
310
|
+
fail!(:token_not_created) if user.token.blank?
|
311
|
+
end
|
312
|
+
end
|
313
|
+
end
|
314
|
+
----
|
315
|
+
|
316
|
+
This contrived example hopefully helps you understand when multiple success status can be useful.
|
317
|
+
In fact, you can actually even exclude success from the successful status. If you do, the status
|
318
|
+
will be initialized as the first in your successful statuses.
|
319
|
+
|
320
|
+
[source,ruby]
|
321
|
+
----
|
322
|
+
class ProcessCommand
|
323
|
+
include AyeCommander::Command
|
324
|
+
succeeds_with :started, :progress, :complete, exclude_success: true
|
325
|
+
|
326
|
+
def call
|
327
|
+
status # => :started
|
328
|
+
do_something
|
329
|
+
@status = :progress
|
330
|
+
do_something_else
|
331
|
+
@status = everything_ok? ? :complete : :failure
|
332
|
+
end
|
333
|
+
end
|
334
|
+
----
|
335
|
+
|
336
|
+
== Abort!
|
337
|
+
|
338
|
+
Now let's imagine that at point in time you want stop running the command. Not necessarily because
|
339
|
+
something went wrong, but you don't need to do anything more for the time being. What can you do?
|
340
|
+
|
341
|
+
Well the most obvious (and possibly more correct) answer is you can use `return` to exit out of the
|
342
|
+
flow. However at times you may define other methods in a command you kinda wish to exit from them,
|
343
|
+
something you can't do with a return.
|
344
|
+
|
345
|
+
[source,ruby]
|
346
|
+
----
|
347
|
+
def call
|
348
|
+
do_something
|
349
|
+
# A return may work here
|
350
|
+
return if status == :cant_do_next
|
351
|
+
end
|
352
|
+
|
353
|
+
private
|
354
|
+
|
355
|
+
def do_something
|
356
|
+
# But it doesn't work if you want to use it from here instead
|
357
|
+
return if status == :cant_do_next
|
358
|
+
end
|
359
|
+
----
|
360
|
+
|
361
|
+
To solve this problem, command has a method named `#abort!`.
|
362
|
+
Calling abort will stop the command on it's trails and will immediately return the result. It *WILL
|
363
|
+
NOT* change the status so if you need change or fail the status, do it before aborting.
|
364
|
+
|
365
|
+
[source,ruby]
|
366
|
+
----
|
367
|
+
class ProcessCommand
|
368
|
+
include AyeCommander::Command
|
369
|
+
succeeds_with :processed
|
370
|
+
|
371
|
+
def call
|
372
|
+
do_something
|
373
|
+
# These lines will never be called
|
374
|
+
do_something_else
|
375
|
+
end
|
376
|
+
|
377
|
+
private
|
378
|
+
|
379
|
+
def do_something
|
380
|
+
if true
|
381
|
+
@status = :processed
|
382
|
+
abort!
|
383
|
+
end
|
384
|
+
end
|
385
|
+
|
386
|
+
def do_something_else
|
387
|
+
@status = :something_else
|
388
|
+
end
|
389
|
+
end
|
390
|
+
|
391
|
+
ProcessCommand.call
|
392
|
+
=> #<SimpleCommand::Result @status: processed>
|
393
|
+
----
|
394
|
+
|
395
|
+
== Getting Hooked
|
396
|
+
|
397
|
+
A command also comes with your standard set of before, around and after hooks to tweak the command.
|
398
|
+
Additionaly commands come bundled with a fourth kind of hook, the aborted hook. The easiest way to
|
399
|
+
understand them, it to see the order of execution of a command.
|
400
|
+
|
401
|
+
[source,ruby]
|
402
|
+
----
|
403
|
+
# Rough representation of your typical call command
|
404
|
+
def call
|
405
|
+
initialize_command
|
406
|
+
validate_args
|
407
|
+
before_hooks
|
408
|
+
around_hooks { call_command }
|
409
|
+
after_hooks
|
410
|
+
aborted_hooks if aborted
|
411
|
+
return_result
|
412
|
+
end
|
413
|
+
----
|
414
|
+
|
415
|
+
Before going deeper into each kind of hook it's worth mentioning the behavior which all hooks share:
|
416
|
+
|
417
|
+
- All hooks can be declared either using a block, a symbol, a proc or a lambda.
|
418
|
+
- Multiple hooks of the same kind can be declared, they will always be run from the first one that
|
419
|
+
was declared to the last one.
|
420
|
+
- If you need a hook to be run before some that have already been declared, you can use the
|
421
|
+
`prepend: true` option.
|
422
|
+
- It might be obvious but worth noting that hooks are run in the command instance; as such you have
|
423
|
+
access to everything the command has.
|
424
|
+
|
425
|
+
[source,ruby]
|
426
|
+
----
|
427
|
+
# Basic hook order
|
428
|
+
before do
|
429
|
+
# I run first!
|
430
|
+
# If I wanted, I could abort the rest of the command from here!
|
431
|
+
end
|
432
|
+
|
433
|
+
before :my_hook
|
434
|
+
|
435
|
+
lambda_from_somewhere_else = -> { "I run third!" }
|
436
|
+
before lambda_from_somewhere_else
|
437
|
+
|
438
|
+
private
|
439
|
+
|
440
|
+
def my_hook
|
441
|
+
# I run second
|
442
|
+
end
|
443
|
+
----
|
444
|
+
|
445
|
+
[source,ruby]
|
446
|
+
----
|
447
|
+
# More complicated hook behavior
|
448
|
+
after :third do
|
449
|
+
# fourth
|
450
|
+
end
|
451
|
+
|
452
|
+
after :first, :second, prepend: true
|
453
|
+
----
|
454
|
+
|
455
|
+
IMPORTANT: Just because there's a lot of liberty with hook order it doesn't mean that its
|
456
|
+
recommended to abuse it. Always try to keep the order of your hooks clear, and use `prepend` only
|
457
|
+
if you *NEED* to.
|
458
|
+
|
459
|
+
=== Before Hooks
|
460
|
+
|
461
|
+
The most important thing to note of before hooks is that while indeed they're called before the
|
462
|
+
command, they're also called *AFTER* the validations have run. This is important because it does
|
463
|
+
mean that you if your command requires any arguments they can't be added through a before hook.
|
464
|
+
|
465
|
+
While it was possible to make the before hooks run before the validations this decision was taken
|
466
|
+
because `requires` and `receives` are meant to be *ARGUMENT* validators. This also means a couple of
|
467
|
+
things:
|
468
|
+
|
469
|
+
- Receives and requires become a way to tell the _users_ of your command how to use it properly
|
470
|
+
- When a validator error is raised you always know it's because of the arguments you sent
|
471
|
+
|
472
|
+
=== After Hooks
|
473
|
+
|
474
|
+
After hooks are the easiest to understand. They run after your command was called, but before the
|
475
|
+
result is created, so if you need to tweak your results a bit you can do it in here!
|
476
|
+
|
477
|
+
=== Aborted Hooks
|
478
|
+
|
479
|
+
As you might imagine, these hooks are only run if you abort the command. Why do we need them in the
|
480
|
+
first place? Well as you may remember, calling `abort!` will stop the command on its tracks and
|
481
|
+
return the result immediately. This means that if you call `abort!` during `call`, after_hooks
|
482
|
+
*WILL NOT* run. For these cases, you might want to use an abort hook instead.
|
483
|
+
|
484
|
+
=== Around Hooks
|
485
|
+
|
486
|
+
Oh man, around hooks. It seems that every time I see an implementation of around hooks they work in
|
487
|
+
a different way, so it's kinda hard to standarize them.
|
488
|
+
|
489
|
+
Around hooks in a command are sadly no different, as they just try to make sense.
|
490
|
+
|
491
|
+
First things first, when you use an around hook you must compromise to *ALWAYS* be able to receive
|
492
|
+
an object and call it at some point in your method/block. If you don't, your command will never be
|
493
|
+
called.
|
494
|
+
|
495
|
+
Now, when there are multiple around hooks the first one will call the second one and so forth until
|
496
|
+
the command is called. This means that before the `call` the code is run in the order the arounds
|
497
|
+
were, but after the `call` it is run in the *REVERSE* order.
|
498
|
+
|
499
|
+
Always keep this in mind.
|
500
|
+
|
501
|
+
[source,ruby]
|
502
|
+
----
|
503
|
+
around do |next_step|
|
504
|
+
puts "First before call"
|
505
|
+
next_step.call
|
506
|
+
puts "First after call"
|
507
|
+
end
|
508
|
+
|
509
|
+
around do |next_step|
|
510
|
+
puts "Second before call"
|
511
|
+
next_step.call
|
512
|
+
puts "Second after call"
|
513
|
+
end
|
514
|
+
|
515
|
+
def call
|
516
|
+
puts "Command called"
|
517
|
+
end
|
518
|
+
|
519
|
+
# Would output:
|
520
|
+
=> First before call
|
521
|
+
=> Second before call
|
522
|
+
=> Command called
|
523
|
+
=> Second after call
|
524
|
+
=> First after call
|
525
|
+
----
|
526
|
+
|
527
|
+
== Aye Aye Commander!
|
528
|
+
|
529
|
+
I've been waiting this whole README to write that.
|
530
|
+
|
531
|
+
A commander is actually a command which task is to run other commands. There are two ways to do this
|
532
|
+
so lets start with the simpler one.
|
533
|
+
|
534
|
+
=== Run and Done
|
535
|
+
|
536
|
+
Similarly to the command, on its simplest form you only need to do two things to use a commander.
|
537
|
+
|
538
|
+
- Include `AyeCommander::Commander`, not `AyeCommander::Command`
|
539
|
+
- Use `execute` with the `Command` s you want to be runned.
|
540
|
+
|
541
|
+
Calling the commander will run the commands one by one... and that's pretty much it.
|
542
|
+
|
543
|
+
[source,ruby]
|
544
|
+
----
|
545
|
+
class Palpatine
|
546
|
+
include AyeCommander::Commander
|
547
|
+
execute HelpRepublic, Order66, BuildEmpire
|
548
|
+
end
|
549
|
+
|
550
|
+
Palpatine.call
|
551
|
+
=> #<Palpatine::Result @status: success, @executed: [#<HelpRepublic @status: success>, #<Order66 @status: success>, #<BuildEmpire @status: success>]>
|
552
|
+
----
|
553
|
+
|
554
|
+
==== Commander Result
|
555
|
+
|
556
|
+
As you may have noticed, the commander result not only includes a status, but also an array with
|
557
|
+
the instances of the command that were run. Handy!
|
558
|
+
|
559
|
+
The commander result will not only contain this set of variables; at the end it will take all the
|
560
|
+
variables that were present on the last executed command. Which brings us to an important point:
|
561
|
+
commands run by the commander *ALWAYS* skip both cleanup and receives validations (requires are
|
562
|
+
still run).
|
563
|
+
|
564
|
+
This is done so that the complete set of variable is sent to the next command to be run. If you want
|
565
|
+
to cleanup the commander, you must declare its own set of returns.
|
566
|
+
|
567
|
+
[source,ruby]
|
568
|
+
----
|
569
|
+
class BadgerCommand
|
570
|
+
include AyeCommander::Command
|
571
|
+
returns :badger
|
572
|
+
end
|
573
|
+
|
574
|
+
class TheCommander
|
575
|
+
include AyeCommander::Commander
|
576
|
+
end
|
577
|
+
|
578
|
+
# Notice how the command returns is ignored
|
579
|
+
TheCommander.call(extra: :params)
|
580
|
+
=> #<TheCommander::Result @status: success, @executed: [...], @extra: params>
|
581
|
+
|
582
|
+
class TheCommander
|
583
|
+
returns :extra
|
584
|
+
end
|
585
|
+
|
586
|
+
# With returns defined, commander now cleans up the result
|
587
|
+
TheCommander.call(extra: :params)
|
588
|
+
=> #<TheCommander::Result @status: success, @extra: params>
|
589
|
+
----
|
590
|
+
|
591
|
+
==== Aborting and Failing
|
592
|
+
|
593
|
+
So what happens when the command we're running aborts? Absolutely Nothing! Remember that we can
|
594
|
+
abort! on success, so a commander doesn't really cares.
|
595
|
+
|
596
|
+
On the other hand if the command we're running *fails* the commander itself will fail and abort.
|
597
|
+
|
598
|
+
[source,ruby]
|
599
|
+
----
|
600
|
+
class Palpatine
|
601
|
+
include AyeCommander::Commander
|
602
|
+
execute HelpRepublic, Order66, BuildEmpire
|
603
|
+
end
|
604
|
+
|
605
|
+
# If Order66 were to fail
|
606
|
+
Palpatine.call
|
607
|
+
=> #<Palpatine::Result @status: failure, @executed: [#<HelpRepublic @status: success>, #<Order66 @status: jedi_escaped>]>
|
608
|
+
----
|
609
|
+
|
610
|
+
=== When we need more tweaking
|
611
|
+
|
612
|
+
Now, while executing several commands in a row is nice, sometimes you need a bit more of control on
|
613
|
+
when to run command A or B.
|
614
|
+
|
615
|
+
Don't worry, AyeCommander has you covered!
|
616
|
+
The only thing you need to do is define your own call method!
|
617
|
+
|
618
|
+
[source,ruby]
|
619
|
+
----
|
620
|
+
class PickyCommander
|
621
|
+
include AyeCommander::Commander
|
622
|
+
|
623
|
+
def call
|
624
|
+
execute FirstCommand
|
625
|
+
|
626
|
+
if command.failure?
|
627
|
+
execute ThisCommand, ThatCommand
|
628
|
+
else
|
629
|
+
execute AnotherCommand
|
630
|
+
end
|
631
|
+
end
|
632
|
+
end
|
633
|
+
----
|
634
|
+
|
635
|
+
There are a couple of things that we must notice here.
|
636
|
+
|
637
|
+
First of all, the `command` instance variable. This variable will always have the last command that
|
638
|
+
was executed. If no command has been run yet, it will have an anonymous command instance to which
|
639
|
+
you can add extras for the following commands to run.
|
640
|
+
|
641
|
+
[source,ruby]
|
642
|
+
----
|
643
|
+
before do
|
644
|
+
command.extra_arg = 'This extra arg'
|
645
|
+
end
|
646
|
+
|
647
|
+
after do
|
648
|
+
command.some_other = 'This' if command.that.blank?
|
649
|
+
end
|
650
|
+
|
651
|
+
def call
|
652
|
+
# Command instance will have extra_arg available
|
653
|
+
execute Command
|
654
|
+
# Commander Result will have some_other if that is blank after running Command
|
655
|
+
end
|
656
|
+
----
|
657
|
+
|
658
|
+
IMPORTANT: The `command` variable is available for *BOTH* kinds of commanders, so you can use it to
|
659
|
+
prepare and finalize your commander. This marks the biggest difference between a `Commander` and a
|
660
|
+
`Command`. While everything in a command operates on it's own instance, a commander operates over
|
661
|
+
the instance of the commands it executes.
|
662
|
+
|
663
|
+
The second thing to notice is that as opposed to their simple counterpart, the commander *DOES NOT*
|
664
|
+
abort nor fail when one of the commands you run fails. This is done so you can tweak the behavior
|
665
|
+
of the commander to your necessities, however recognizing that it is quite likely that you want
|
666
|
+
that behaviour for your commander there are ways to reenable it.
|
667
|
+
|
668
|
+
[source,ruby]
|
669
|
+
----
|
670
|
+
class UndecisiveCommander
|
671
|
+
include AyeCommander::Commander
|
672
|
+
|
673
|
+
# Using this will re-enable failing on all commands
|
674
|
+
abort_on_failure
|
675
|
+
|
676
|
+
def call
|
677
|
+
# But even with that option, you override it at an instance level
|
678
|
+
|
679
|
+
# Will always abort on failure
|
680
|
+
execute ThisCommand, abort_on_failure: true
|
681
|
+
|
682
|
+
# Will never abort on failure
|
683
|
+
execute ThatCommand, OtherCommand, abort_on_failure: false
|
684
|
+
end
|
685
|
+
end
|
686
|
+
----
|
687
|
+
|
688
|
+
== Top tips and tricks
|
689
|
+
|
690
|
+
- Never forget when and when not to use a command
|
691
|
+
|
692
|
+
- Have naming conventions +
|
693
|
+
I really suggest that for commands (and commanders), you finish their names with `Command`. This
|
694
|
+
clears up what they are and maybe what they do just by looking at the name.
|
695
|
+
|
696
|
+
- Use private methods to know what your command does at first glance +
|
697
|
+
|
698
|
+
[source,ruby]
|
699
|
+
----
|
700
|
+
class UpdateExchangeRatesCommand
|
701
|
+
include AyeCommander::Command
|
702
|
+
|
703
|
+
def call
|
704
|
+
fetch_todays_exchange_rates
|
705
|
+
save_exchange_rates
|
706
|
+
end
|
707
|
+
end
|
708
|
+
----
|
709
|
+
|
710
|
+
- But if the logic is too complicated, split it into more commands
|
711
|
+
|
712
|
+
[source,ruby]
|
713
|
+
----
|
714
|
+
class UpdateExchangeRatesCommand
|
715
|
+
include AyeCommander::Commander
|
716
|
+
execute FetchExchangeRatesCommand, SaveExchangeRates
|
717
|
+
end
|
718
|
+
----
|
719
|
+
|
720
|
+
- Write code, have fun!
|