skywalker 2.2.0 → 3.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.md +82 -42
- data/Rakefile +12 -0
- data/lib/skywalker/transactional.rb +5 -1
- data/lib/skywalker/version.rb +1 -1
- data/spec/lib/skywalker/transactional_spec.rb +3 -9
- metadata +2 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: fa027ee8b73560125224fdbc12c30bc9bfb2e04b
|
4
|
+
data.tar.gz: 191fa123ac163d68c095a7d790e07c2fbe63003b
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 39b68bf5a7ba707302388768bc2b8997ea0d20706befe4146435fd02641ed76e4384376a9bd04e5170453388dc7dc862ebf83550a314a19b7f317ec43683fda3
|
7
|
+
data.tar.gz: ba46cb5226cb3c855b731dc3564be5a155b64a569e79e3c255e831f5b6f941ffd9bcaa6e989e903b8109fbe84d6ad8fbf0e52c3ffbe144bbcb83d928dfdd7c4b
|
data/README.md
CHANGED
@@ -4,13 +4,16 @@ Skywalker is a gem that provides a simple command pattern for applications that
|
|
4
4
|
use transactions. (Or not! In later versions, Skywalker is much more modular and
|
5
5
|
can be [used for non-transactional purposes](#components), too.)
|
6
6
|
|
7
|
+
use transactions.
|
8
|
+
|
7
9
|
## Why Skywalker?
|
8
10
|
|
9
|
-
|
11
|
+
It's a reference to 'Commander Skywalker' from Star Wars, a rank the
|
12
|
+
main protagonist achieves and by which he is called.
|
10
13
|
|
11
|
-
It's
|
12
|
-
that's at least marginally witty. If you can't achieve wit or
|
13
|
-
least achieve topicality, right?
|
14
|
+
It's tricky to come up with a memorable single-word name for a gem about
|
15
|
+
commands that's at least marginally witty. If you can't achieve wit or
|
16
|
+
cleverness, at least achieve topicality, right?
|
14
17
|
|
15
18
|
## What is a command?
|
16
19
|
|
@@ -20,6 +23,28 @@ and considered a single unit. If one instruction fails, they should all fail.
|
|
20
23
|
That's a transaction, you say? You're correct! But there are some benefits of
|
21
24
|
considering transactional blocks as objects:
|
22
25
|
|
26
|
+
### Operations as Objects (And Interfaces as Collaborators)
|
27
|
+
|
28
|
+
One of the really cool things about Ruby is that it's trivial to pass a class
|
29
|
+
as an argument. This makes Dependency Injection (DI) dead simple. (If you've
|
30
|
+
never heard of DI, you should read my favourite architecture grump
|
31
|
+
[Piotr Solnic's take on it](http://solnic.eu/2013/12/17/the-world-needs-another-post-about-dependency-injection-in-ruby.html).)
|
32
|
+
|
33
|
+
This, of course, makes a lot of sense when you'd like to remove a reference to
|
34
|
+
a model class to test in isolation. But it also makes a lot of sense when you realize that portions
|
35
|
+
of your code are collaborators, too: anything that's orthogonal (or a side
|
36
|
+
effect) to what you're working on is something that you can test in isolation.
|
37
|
+
And if you've ever done this, you know that isolated tests are so much easier to
|
38
|
+
write, so much easier to maintain, and so much faster to run.
|
39
|
+
|
40
|
+
This isn't limited to a Command object. You can offload these chunks of code
|
41
|
+
into numerous other collaborators, some of which people call 'Service',
|
42
|
+
'Operation', or 'Policy' objects. But it is an essential property of these
|
43
|
+
types of objects and a huge benefit.
|
44
|
+
|
45
|
+
Skywalker also has an `Acceptable` module that can be used to create other such
|
46
|
+
objects that do not necessarily require a transaction.
|
47
|
+
|
23
48
|
### Testability
|
24
49
|
|
25
50
|
Skywalker places a strong emphasis on dependency injection.
|
@@ -41,9 +66,8 @@ and a single bad path through integration specs, and that will suffice.
|
|
41
66
|
|
42
67
|
The benefit of abstraction means that you can easily reason about a command
|
43
68
|
without having to know its internals. Standard caveats apply, but if you have a
|
44
|
-
`CreateGroup` command, you should be able to infer that calling the command
|
45
|
-
the correct arguments will produce the expected result
|
46
|
-
remember all the side effects of an operation.
|
69
|
+
`CreateGroup` command, you should be able to infer that calling the command
|
70
|
+
with the correct arguments will produce the expected result.
|
47
71
|
|
48
72
|
### Knowledge of Results Without Knowledge of Response
|
49
73
|
|
@@ -60,9 +84,9 @@ callback if `request.xhr?`? Simply override `run_success_callbacks` and
|
|
60
84
|
|
61
85
|
### A Gateway to Harder Architectures
|
62
86
|
|
63
|
-
It's not hard to create an `Event` class and step up toward full event
|
64
|
-
or to go a bit further and implement full CQRS. This is the
|
65
|
-
pattern your parents warned you about.
|
87
|
+
It's not hard to create an `Event` class and step up toward full event
|
88
|
+
sourcing, or to go a bit further and implement full CQRS. This is the
|
89
|
+
architectural pattern your parents warned you about.
|
66
90
|
|
67
91
|
## Installation
|
68
92
|
|
@@ -122,10 +146,10 @@ This might seem concise because it keeps the controller small. (Fat model, thin
|
|
122
146
|
controller has been a plank of Rails development for a while, but it's slowly
|
123
147
|
going away, thank heavens). But there are two problems here: first, it
|
124
148
|
introduces a point of coupling between the model and the mailer, which not only
|
125
|
-
makes testing slower, it means that these two objects are now entwined. Create
|
126
|
-
group through the Rails console? You're sending an email with no way to skip
|
149
|
+
makes testing slower, it means that these two objects are now entwined. Create
|
150
|
+
a group through the Rails console? You're sending an email with no way to skip
|
127
151
|
that. Secondly, it reduces the reasonability of the code. When you look at the
|
128
|
-
`GroupsController`, you can't
|
152
|
+
`GroupsController`, you can't immediately see that this sends an email.
|
129
153
|
|
130
154
|
**Moral #1: Orthogonal concerns should not be put into ActiveRecord callbacks.**
|
131
155
|
|
@@ -156,12 +180,12 @@ and longer. Maybe you're sending a tweet about the group, scheduling a
|
|
156
180
|
background job to update some thumbnails, or hitting a webhook URL. You're
|
157
181
|
losing the reasonability of the code because of the detail.
|
158
182
|
|
159
|
-
Moreover, imagine that the group email being sent contains critical
|
160
|
-
on how to proceed. What if `NotificationMailer` has a syntax
|
161
|
-
created, but the mail won't be sent. Now the user hasn't
|
162
|
-
and your database is potentially fouled up by
|
163
|
-
run this in a transaction, but that does not
|
164
|
-
within the controller.
|
183
|
+
Moreover, imagine that the group email being sent contains critical
|
184
|
+
instructions on how to proceed. What if `NotificationMailer` has a syntax
|
185
|
+
error? The group is created, but the mail won't be sent. Now the user hasn't
|
186
|
+
received a good error, and your database is potentially fouled up by
|
187
|
+
half-performed requests. You can run this in a transaction, but that does not
|
188
|
+
reduce the complexity contained within the controller.
|
165
189
|
|
166
190
|
**Moral #2: Rails controllers should dispatch to application logic, and receive
|
167
191
|
instructions on how to respond.**
|
@@ -226,9 +250,20 @@ class CreateGroupCommand < Skywalker::Command
|
|
226
250
|
end
|
227
251
|
```
|
228
252
|
|
229
|
-
|
230
|
-
|
231
|
-
|
253
|
+
Two notes on the above example:
|
254
|
+
|
255
|
+
First, you do not have to use `method`. You are free to do what you
|
256
|
+
wish—whether that is using a proc, or injecting the controller context as
|
257
|
+
an argument and then overwriting the callback methods to use that. But I (and
|
258
|
+
this library) take a principled stance that what occurs inside those callbacks
|
259
|
+
is usually the responsibility of the controller and should remain within it, so
|
260
|
+
this is made easy, and you are strongly encouraged to follow this pattern.
|
261
|
+
|
262
|
+
Secondly, it is ideologically 'purer' to pass in a method which would construct
|
263
|
+
a group, and the params separately, because it moves the instantiation of
|
264
|
+
domain concepts out of the controller. However, for the purpose of 'GSD', I
|
265
|
+
often wind up keeping the instantiation of simple AR objects inside of my
|
266
|
+
controller.
|
232
267
|
|
233
268
|
|
234
269
|
### Basic Composition Summary
|
@@ -261,12 +296,12 @@ commands themselves.
|
|
261
296
|
### What happens when callbacks fail?
|
262
297
|
|
263
298
|
Exceptions thrown inside the success callbacks (`on_success` or your own
|
264
|
-
callbacks defined in `run_success_callbacks`) will cause the command to fail
|
265
|
-
run the failure callbacks.
|
299
|
+
callbacks defined in `run_success_callbacks`) will cause the command to fail
|
300
|
+
and run the failure callbacks.
|
266
301
|
|
267
302
|
Exceptions thrown inside the failure callbacks (`on_failure` or your own
|
268
|
-
callbacks defined in `run_failure_callbacks`) will not be caught and will
|
269
|
-
out of the command.
|
303
|
+
callbacks defined in `run_failure_callbacks`) will not be caught and will
|
304
|
+
bubble out of the command.
|
270
305
|
|
271
306
|
### Overriding Methods
|
272
307
|
|
@@ -300,7 +335,7 @@ well-commented.
|
|
300
335
|
|
301
336
|
## Testing (and TDD)
|
302
337
|
|
303
|
-
Take a look at the `examples` directory, which uses example as above of a
|
338
|
+
Take a look at the `examples` directory, which uses the example as above of a
|
304
339
|
notifier, but makes it a bit more complicated: it assumes that we only send
|
305
340
|
emails if the user (which we'll pass in) has a preference set to receive email.
|
306
341
|
|
@@ -308,10 +343,10 @@ emails if the user (which we'll pass in) has a preference set to receive email.
|
|
308
343
|
|
309
344
|
Here's what you can assume in your tests:
|
310
345
|
|
311
|
-
1. Arguments that are present in the list of required_args will throw an error
|
346
|
+
1. Arguments that are present in the list of `required_args` will throw an error
|
312
347
|
before the command executes if they are not passed.
|
313
|
-
2. Operations that throw an error will abort the command and trigger its
|
314
|
-
state.
|
348
|
+
2. Operations that throw an error will abort the command and trigger its
|
349
|
+
failure state.
|
315
350
|
3. Calling `Command.new().call` is functionally equivalent to calling
|
316
351
|
`Command.call()`
|
317
352
|
|
@@ -320,14 +355,12 @@ Here's what you can assume in your tests:
|
|
320
355
|
There are two tests that you need to write. First, you'll want to write a
|
321
356
|
Command spec, which are very simplistic specs and should be used to verify the
|
322
357
|
validity of the command in isolation from the rest of the system. (This is what
|
323
|
-
the example shows.)
|
324
|
-
|
325
|
-
|
326
|
-
|
327
|
-
|
328
|
-
|
329
|
-
path, though your situation may vary depending on the detail of error handling
|
330
|
-
you perform.
|
358
|
+
the example shows.) You'll also want to write some high-level integration tests
|
359
|
+
to make sure that the command is implemented correctly inside your controller,
|
360
|
+
and has the expected system-wide results. You shouldn't need to write
|
361
|
+
integration specs to test every path -- it should suffice to test a successful
|
362
|
+
path and a failing path, though your situation may vary depending on the detail
|
363
|
+
of error handling you perform.
|
331
364
|
|
332
365
|
Here's one huge benefit: with a few small steps, you won't need to include
|
333
366
|
`rails_helper` to boot up the entire environment. That means blazingly fast
|
@@ -349,7 +382,7 @@ end
|
|
349
382
|
|
350
383
|
### Common Failures
|
351
384
|
|
352
|
-
|
385
|
+
Before version 3.0, you might have received an error like this:
|
353
386
|
|
354
387
|
```
|
355
388
|
undefined method `call' for nil:NilClass
|
@@ -362,6 +395,12 @@ the failure exception as `self.error`. You can also reraise the exception to
|
|
362
395
|
achieve a better result summary, but this is not done by default, as you may
|
363
396
|
also want to test error handling.
|
364
397
|
|
398
|
+
**Now**, in version 3.0 and later, any command that does not receive a callable
|
399
|
+
`on_failure` argument will raise any arguments it encounters during the process
|
400
|
+
of running `execute!`.
|
401
|
+
|
402
|
+
|
403
|
+
|
365
404
|
## Components
|
366
405
|
|
367
406
|
A `Skywalker::Command` is implemented through a series of modules that can be
|
@@ -451,6 +490,7 @@ For example, see `Skywalker::Command` documentation above.
|
|
451
490
|
|
452
491
|
1. Fork it ( https://github.com/robyurkowski/skywalker/fork )
|
453
492
|
2. Create your feature branch (`git checkout -b my-new-feature`)
|
454
|
-
3.
|
455
|
-
4.
|
456
|
-
5.
|
493
|
+
3. Ensure both sets of tests are green (`bundle exec rake`)
|
494
|
+
4. Commit your changes (`git commit -am 'Add some feature'`)
|
495
|
+
5. Push to the branch (`git push origin my-new-feature`)
|
496
|
+
6. Create a new Pull Request
|
data/Rakefile
CHANGED
@@ -1,2 +1,14 @@
|
|
1
1
|
require "bundler/gem_tasks"
|
2
2
|
|
3
|
+
begin
|
4
|
+
require 'rspec/core/rake_task'
|
5
|
+
|
6
|
+
RSpec::Core::RakeTask.new(:spec)
|
7
|
+
RSpec::Core::RakeTask.new(:example_spec) do |t|
|
8
|
+
t.ruby_opts = '-C examples'
|
9
|
+
end
|
10
|
+
|
11
|
+
desc "Runs both primary and example specs."
|
12
|
+
task default: [:spec, :example_spec]
|
13
|
+
rescue
|
14
|
+
end
|
data/lib/skywalker/version.rb
CHANGED
@@ -126,10 +126,6 @@ module Skywalker
|
|
126
126
|
end
|
127
127
|
|
128
128
|
describe "on_failure" do
|
129
|
-
before do
|
130
|
-
allow(instance).to receive(:error=)
|
131
|
-
end
|
132
|
-
|
133
129
|
context "when on_failure is not nil" do
|
134
130
|
it "calls the on_failure callback with itself" do
|
135
131
|
expect(on_failure).to receive(:call).with(instance)
|
@@ -146,12 +142,10 @@ module Skywalker
|
|
146
142
|
end
|
147
143
|
|
148
144
|
context "when on_failure is nil" do
|
149
|
-
let(:
|
150
|
-
let(:instance) { klass.new(on_failure: nil_callback) }
|
145
|
+
let(:instance) { klass.new }
|
151
146
|
|
152
|
-
it "
|
153
|
-
expect
|
154
|
-
instance.call
|
147
|
+
it "raises the error" do
|
148
|
+
expect { instance.call }.to raise_error ScriptError
|
155
149
|
end
|
156
150
|
end
|
157
151
|
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: skywalker
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version:
|
4
|
+
version: 3.0.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Rob Yurkowski
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date:
|
11
|
+
date: 2017-06-29 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: bundler
|