aye_commander 1.0.0
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.
- 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!
|