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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 2ff0a320a2103cf8829980f9ef745f34ea00ebc6
4
- data.tar.gz: 4c04009e7c751282cd1f6b0d88d0e0e046486276
3
+ metadata.gz: fa027ee8b73560125224fdbc12c30bc9bfb2e04b
4
+ data.tar.gz: 191fa123ac163d68c095a7d790e07c2fbe63003b
5
5
  SHA512:
6
- metadata.gz: d74caa0e24ad0681fdf6133eecf290967059090c09892932c3450aa04f1c97da3e770bfa36eda60fc31a8d4b628f4131253fe4e3a38061086312ab03762e711f
7
- data.tar.gz: a381af6335e0eb034047bec3877434271be6dd1e6d32a19cb40974e0c37eabd723f8c7c6ec5c8b37549293afca5691adeef6d0f5580bda3e73ba5fcf3e897bd8
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
- Because "Commander Skywalker".
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 impossible to come up with a single-word name for a gem about commands
12
- that's at least marginally witty. If you can't achieve wit or cleverness, at
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 with
45
- the correct arguments will produce the expected result, rather than having to
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 sourcing,
64
- or to go a bit further and implement full CQRS. This is the architectural
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 a
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 suss out the fact that this sends an email.
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 instructions
160
- on how to proceed. What if `NotificationMailer` has a syntax error? The group is
161
- created, but the mail won't be sent. Now the user hasn't gotten a good error,
162
- and your database is potentially fouled up by half-performed requests. You can
163
- run this in a transaction, but that does not reduce the complexity contained
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
- You can of course set up a default for Group as with `#notifier` and pass in
230
- params only, but I find injecting a pre-constructed ActiveRecord object usually
231
- works well.
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&mdash;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 and
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 bubble
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 failure
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
- You'll also want to write some high-level integration tests to make sure that
326
- the command is implemented correctly inside your controller, and has the
327
- expected system-wide results. You shouldn't need to write integration specs to
328
- test every path -- it should suffice to test a successful path and a failing
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
- Your failures will initially look like this:
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. Commit your changes (`git commit -am 'Add some feature'`)
455
- 4. Push to the branch (`git push origin my-new-feature`)
456
- 5. Create a new Pull Request
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
@@ -120,7 +120,11 @@ module Skywalker
120
120
  # @since 2.2.0
121
121
  #
122
122
  private def run_failure_callbacks
123
- on_failure.call(self) unless on_failure.nil?
123
+ if on_failure
124
+ on_failure.call(self)
125
+ else
126
+ raise error
127
+ end
124
128
  end
125
129
 
126
130
  end
@@ -1,3 +1,3 @@
1
1
  module Skywalker
2
- VERSION = "2.2.0"
2
+ VERSION = "3.0.0"
3
3
  end
@@ -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(:nil_callback) { double("fakenil", nil?: true) }
150
- let(:instance) { klass.new(on_failure: nil_callback) }
145
+ let(:instance) { klass.new }
151
146
 
152
- it "does not call on_failure" do
153
- expect(nil_callback).not_to receive(:call)
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: 2.2.0
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: 2016-06-21 00:00:00.000000000 Z
11
+ date: 2017-06-29 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bundler