skywalker 2.2.0 → 3.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 +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
|