skywalker 2.1.0 → 2.2.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: f34e5d3df438e1dded141d4b9e093785bb392cdd
4
- data.tar.gz: e108b0e461eddb8f298ca5c771a0baf988e6c86f
3
+ metadata.gz: 2ff0a320a2103cf8829980f9ef745f34ea00ebc6
4
+ data.tar.gz: 4c04009e7c751282cd1f6b0d88d0e0e046486276
5
5
  SHA512:
6
- metadata.gz: 2f0fe7a5edd3bc67f0478dc91369e10001ce9fb037a464cde6016241f202f1afc724d2b879e95e8198d1eda1663448984d9ec81e278df20687e65399f5f97f92
7
- data.tar.gz: 53d3eff371345c36dbe9ea155a29fbbad8806743ddac162abfda4e8c3c9557b6a9df224f56c6cfe63d72b8ac83f807eb9f01075d3149242c6da024c6b8b7c99d
6
+ metadata.gz: d74caa0e24ad0681fdf6133eecf290967059090c09892932c3450aa04f1c97da3e770bfa36eda60fc31a8d4b628f4131253fe4e3a38061086312ab03762e711f
7
+ data.tar.gz: a381af6335e0eb034047bec3877434271be6dd1e6d32a19cb40974e0c37eabd723f8c7c6ec5c8b37549293afca5691adeef6d0f5580bda3e73ba5fcf3e897bd8
data/CHANGELOG.md CHANGED
@@ -1,5 +1,21 @@
1
1
  All commits by Rob Yurkowski unless otherwise noted.
2
2
 
3
+ ## 2.2.0 (2016-06-21)
4
+
5
+ - Extracts command behaviour to `Skywalker::Callable` and
6
+ `Skywalker::Transactional`.
7
+
8
+ This simplifies the `Command` object, making it basically a combination of
9
+ `Callable` and `Transactional` mixins. It also allows for the reuse of the
10
+ constituent parts on a larger scale.
11
+
12
+ - Remove dependency on `ActiveRecord`.
13
+
14
+ We now do a check to see if ActiveRecord is defined. If not, we simply default
15
+ to calling the passed block. This allows us to avoid having to require
16
+ ActiveRecord, which lightens our dependencies and also makes us feel just a
17
+ tiny bit less dirty.
18
+
3
19
  ## 2.1.0 (2015-05-14)
4
20
 
5
21
  - Yields self to any block given to any object implementing `Skywalker::Acceptable`.
data/README.md CHANGED
@@ -1,11 +1,16 @@
1
1
  # Skywalker
2
2
 
3
- Skywalker is a gem that provides a simple command pattern for applications that use transactions.
3
+ Skywalker is a gem that provides a simple command pattern for applications that
4
+ use transactions. (Or not! In later versions, Skywalker is much more modular and
5
+ can be [used for non-transactional purposes](#components), too.)
4
6
 
5
7
  ## Why Skywalker?
6
8
 
7
- It's impossible to come up with a single-word name for a gem about commands that's at least marginally
8
- witty. If you can't achieve wit or cleverness, at least achieve topicality, right?
9
+ Because "Commander Skywalker".
10
+
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?
9
14
 
10
15
  ## What is a command?
11
16
 
@@ -17,40 +22,47 @@ considering transactional blocks as objects:
17
22
 
18
23
  ### Testability
19
24
 
20
- With a command, you inject most any argument, which means that you can simulate
21
- the run of the command without providing real arguments. Best practice is to
22
- describe the operations in methods, which can then be stubbed out to test small
23
- portions in isolation.
25
+ Skywalker places a strong emphasis on dependency injection.
26
+
27
+ This means that you can unit test the command for correctness without having to
28
+ do a full integration test for every single path through the code. That makes
29
+ your test suite lean and mean, and encourages you to aim for weaker forms of
30
+ coupling (i.e. preferring connascence of name, rather than identity).
31
+
32
+ Best practice is to describe the operations in methods, which can then be
33
+ stubbed out to test small portions in isolation.
24
34
 
25
- This also allows you to make the reasonable inference that the command will abort
26
- properly if one step raises an error, and by convention, the same method (`on_failure`)
27
- will be called. In most cases, you can thereby verify happy path and a single bad path
28
- through integration specs, and that will suffice.
35
+ This also allows you to make the reasonable inference that the command will
36
+ abort properly if one step raises an error, and by convention, the same method
37
+ (`on_failure`) will be called. In most cases, you can thereby verify happy path
38
+ and a single bad path through integration specs, and that will suffice.
29
39
 
30
40
  ### Reasonability
31
41
 
32
- The benefit of abstraction means that you can easily reason about a command without
33
- having to know its internals. Standard caveats apply, but if you have a `CreateGroup`
34
- command, you should be able to infer that calling the command with the correct arguments
35
- will produce the expected result.
42
+ The benefit of abstraction means that you can easily reason about a command
43
+ 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.
36
47
 
37
48
  ### Knowledge of Results Without Knowledge of Response
38
49
 
39
- A command prescriptively takes callbacks or `#call`able objects, which can be called
40
- depending on the result of the command. By default, `Skywalker::Command` can handle
41
- an `on_success` and an `on_failure` callback, which are called after their respective
42
- results. You can define these in your controllers, which lets you run the same command
43
- but respond in unique ways, and keeps controller concerns inside the controller.
50
+ A command prescriptively takes callbacks or `#call`able objects, which can be
51
+ called depending on the result of the command. By default, `Skywalker::Command`
52
+ can handle an `on_success` and an `on_failure` callback, which are called after
53
+ their respective results. You can define these in your controllers, which lets
54
+ you run the same command but respond in unique ways, and keeps controller
55
+ concerns inside the controller.
44
56
 
45
- You can also easily override which callbacks are run. Need to run a different callback
46
- if `request.xhr?`? Simply override `run_success_callbacks` and `run_failure_callbacks`
47
- and call your own.
57
+ You can also easily override which callbacks are run. Need to run a different
58
+ callback if `request.xhr?`? Simply override `run_success_callbacks` and
59
+ `run_failure_callbacks` and call your own.
48
60
 
49
61
  ### A Gateway to Harder Architectures
50
62
 
51
- It's not hard to create an `Event` class and step up toward full event sourcing, or to
52
- go a bit further and implement full CQRS. This is the architectural pattern your parents
53
- warned you about.
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.
54
66
 
55
67
  ## Installation
56
68
 
@@ -70,11 +82,11 @@ Or install it yourself as:
70
82
 
71
83
  ## Usage
72
84
 
73
- Let's talk about a situation where you're creating a group and sending an email inside a
74
- Rails app.
85
+ Let's talk about a situation where you're creating a group and sending an email
86
+ inside a Rails app.
75
87
 
76
- Standard operating procedure usually falls into one of two patterns, both of which are
77
- mediocre. The first makes use of ActiveRecord callbacks:
88
+ Standard operating procedure usually falls into one of two patterns, both of
89
+ which are mediocre. The first makes use of ActiveRecord callbacks:
78
90
 
79
91
  ```ruby
80
92
  # app/controllers/groups_controller.rb
@@ -106,14 +118,14 @@ class Group < ActiveRecord::Base
106
118
  end
107
119
  ```
108
120
 
109
- This might seem concise because it keeps the controller small. (Fat model,
110
- thin controller has been a plank of Rails development for a while, but it's
111
- slowly going away, thank heavens). But there are two problems here:
112
- first, it introduces a point of coupling between the model and the mailer,
113
- which not only makes testing slower, it means that these two objects are
114
- now entwined. Create a group through the Rails console? You're sending an email with
115
- no way to skip that. Secondly, it reduces the reasonability of the code. When you
116
- look at the `GroupsController`, you can't suss out the fact that this sends an email.
121
+ This might seem concise because it keeps the controller small. (Fat model, thin
122
+ controller has been a plank of Rails development for a while, but it's slowly
123
+ going away, thank heavens). But there are two problems here: first, it
124
+ 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
127
+ 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.
117
129
 
118
130
  **Moral #1: Orthogonal concerns should not be put into ActiveRecord callbacks.**
119
131
 
@@ -138,21 +150,24 @@ class GroupsController < ApplicationController
138
150
  end
139
151
  ```
140
152
 
141
- This is more reasonable, but it's longer in the controller and at some point your eyes
142
- begin to glaze over. Imagine as these orthogonal concerns grow longer and longer. Maybe
143
- you're sending a tweet about the group, scheduling a background job to update some thumbnails,
144
- or hitting a webhook URL. You're losing the reasonability of the code because of the detail.
153
+ This is more reasonable, but it's longer in the controller and at some point
154
+ your eyes begin to glaze over. Imagine as these orthogonal concerns grow longer
155
+ and longer. Maybe you're sending a tweet about the group, scheduling a
156
+ background job to update some thumbnails, or hitting a webhook URL. You're
157
+ losing the reasonability of the code because of the detail.
145
158
 
146
- Moreover, imagine that the group email being sent contains critical instructions on how
147
- to proceed. What if `NotificationMailer` has a syntax error? The group is created, but the
148
- mail won't be sent. Now the user hasn't gotten a good error, and your database is potentially
149
- fouled up by half-performed requests. You can run this in a transaction, but that does not
150
- reduce the complexity contained within the controller.
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.
151
165
 
152
- **Moral #2: Rails controllers should dispatch to application logic, and receive instructions on how to respond.**
166
+ **Moral #2: Rails controllers should dispatch to application logic, and receive
167
+ instructions on how to respond.**
153
168
 
154
- The purpose of the command is to group orthogonal but interdependent results into logical operations. Here's how that
155
- looks with a `Skywalker::Command`:
169
+ The purpose of the command is to group orthogonal but interdependent results
170
+ into logical operations. Here's how that looks with a `Skywalker::Command`:
156
171
 
157
172
 
158
173
  ```ruby
@@ -239,13 +254,19 @@ command = AddGroupCommand.call(
239
254
 
240
255
  ```
241
256
 
242
- You can pass any object responding to `#call` to the `on_success` and `on_failure` handlers, including procs, lambdas, controller methods, or other commands themselves.
257
+ You can pass any object responding to `#call` to the `on_success` and
258
+ `on_failure` handlers, including procs, lambdas, controller methods, or other
259
+ commands themselves.
243
260
 
244
261
  ### What happens when callbacks fail?
245
262
 
246
- Exceptions thrown inside the success callbacks (`on_success` or your own callbacks defined in `run_success_callbacks`) will cause the command to fail and run the failure callbacks.
263
+ 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.
247
266
 
248
- Exceptions thrown inside the failure callbacks (`on_failure` or your own callbacks defined in `run_failure_callbacks`) will not be caught and will bubble out of the command.
267
+ 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.
249
270
 
250
271
  ### Overriding Methods
251
272
 
@@ -254,39 +275,63 @@ The following methods are overridable for easy customization:
254
275
  - `execute!`
255
276
  - Define your operations here.
256
277
  - `required_args`
257
- - An array of expected keys given to the command. Raises `ArgumentError` if keys are missing.
278
+ - An array of expected keys given to the command. Raises `ArgumentError` if
279
+ keys are missing.
258
280
  - `validate_arguments!`
259
- - Checks required args are present, but can be customized. All instance variables are set by this point.
281
+ - Checks required args are present, but can be customized. All instance
282
+ variables are set by this point.
260
283
  - `transaction(&block)`
261
- - Uses an `ActiveRecord::Base.transaction` by default, but can be customized. `execute!` runs inside of this.
284
+ - Uses an `ActiveRecord::Base.transaction` by default, but can be customized.
285
+ `execute!` runs inside of this.
262
286
  - `confirm_success`
263
287
  - Fires off callbacks on command success (i.e. non-error).
264
288
  - `run_success_callbacks`
265
- - Dictates which success callbacks are run. Defaults to `on_success` if defined.
289
+ - Dictates which success callbacks are run. Defaults to `on_success` if
290
+ defined.
266
291
  - `confirm_failure`
267
- - Fires off callbacks on command failure (i.e. erroneous state), and sets the exception as `command.error`.
292
+ - Fires off callbacks on command failure (i.e. erroneous state), and sets the
293
+ exception as `command.error`.
268
294
  - `run_failure_callbacks`
269
- - Dictates which failure callbacks are run. Defaults to `on_failure` if defined.
295
+ - Dictates which failure callbacks are run. Defaults to `on_failure` if
296
+ defined.
270
297
 
271
- For further reference, simply see the command file. It's less than 90 LOC and well-commented.
298
+ For further reference, simply see the command file. It's less than 90 LOC and
299
+ well-commented.
272
300
 
273
301
  ## Testing (and TDD)
274
302
 
275
- Take a look at the `examples` directory, which uses example as above of a notifier, but makes it a bit more complicated: it assumes that we only send emails if the user (which we'll pass in) has a preference set to receive email.
303
+ Take a look at the `examples` directory, which uses example as above of a
304
+ notifier, but makes it a bit more complicated: it assumes that we only send
305
+ emails if the user (which we'll pass in) has a preference set to receive email.
276
306
 
277
307
  ### Assumptions
278
308
 
279
309
  Here's what you can assume in your tests:
280
310
 
281
- 1. Arguments that are present in the list of required_args will throw an error before the command executes if they are not passed.
282
- 2. Operations that throw an error will abort the command and trigger its failure state.
283
- 3. Calling `Command.new().call` is functionally equivalent to calling `Command.call()`
311
+ 1. Arguments that are present in the list of required_args will throw an error
312
+ 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.
315
+ 3. Calling `Command.new().call` is functionally equivalent to calling
316
+ `Command.call()`
284
317
 
285
318
  ### Strategy
286
319
 
287
- There are two tests that you need to write. First, you'll want to write a Command spec, which are very simplistic specs and should be used to verify the validity of the command in isolation from the rest of the system. (This is what the example shows.) You'll also want to write some high-level integration tests to make sure that the command is implemented correctly inside your controller, and has the expected system-wide results. You shouldn't need to write integration specs to test every path -- it should suffice to test a successful path and a failing path, though your situation may vary depending on the detail of error handling you perform.
320
+ There are two tests that you need to write. First, you'll want to write a
321
+ Command spec, which are very simplistic specs and should be used to verify the
322
+ validity of the command in isolation from the rest of the system. (This is what
323
+ the example shows.)
288
324
 
289
- Here's one huge benefit: with a few small steps, you won't need to include `rails_helper` to boot up the entire environment. That means blazingly fast tests. All you need to do is stub `transaction` on your command, like so:
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.
331
+
332
+ Here's one huge benefit: with a few small steps, you won't need to include
333
+ `rails_helper` to boot up the entire environment. That means blazingly fast
334
+ tests. All you need to do is stub `transaction` on your command, like so:
290
335
 
291
336
  ```ruby
292
337
  RSpec.describe CreateGroupCommand do
@@ -311,8 +356,96 @@ undefined method `call' for nil:NilClass
311
356
  # .../lib/skywalker/command.rb:118:in `run_failure_callbacks'
312
357
  ```
313
358
 
314
- This means that the command failed and you didn't specify an `on_failure` callback. You can stick a debugger
315
- inside of `run_failure_callbacks`, and get the failure exception as `self.error`. You can also reraise the exceptiong to achieve a better result summary, but this is not done by default, as you may also want to test error handling.
359
+ This means that the command failed and you didn't specify an `on_failure`
360
+ callback. You can stick a debugger inside of `run_failure_callbacks`, and get
361
+ the failure exception as `self.error`. You can also reraise the exception to
362
+ achieve a better result summary, but this is not done by default, as you may
363
+ also want to test error handling.
364
+
365
+ ## Components
366
+
367
+ A `Skywalker::Command` is implemented through a series of modules that can be
368
+ used independently from each other and outside of the context of the `Command`
369
+ object.
370
+
371
+ ### `Acceptable`
372
+
373
+ `Skywalker::Acceptable` allows an object to receive a keyword list of arguments
374
+ upon instantiation. It creates a reader and writer for each keyword that doesn't
375
+ already have one, and it will raise an error for any keyword not given that is
376
+ present inside its `required_args` list.
377
+
378
+ Example:
379
+
380
+ ```ruby
381
+ require 'skywalker/acceptable'
382
+
383
+ class MyClass
384
+ include Skywalker::Acceptable
385
+
386
+ def required_args
387
+ %w(baz)
388
+ end
389
+
390
+ def bar
391
+ "definitely not #{@bar}"
392
+ end
393
+
394
+ def baz=(int)
395
+ @baz = int.to_s.reverse.to_i
396
+ end
397
+ end
398
+
399
+
400
+ MyClass.new(foo: "abc", bar: "xyz") # => ArgumentError, "baz required but not given"
401
+
402
+ instance = MyClass.new(foo: "abc", bar: "xyz", baz: 123) # => <MyClass#...>
403
+
404
+ instance.foo # => "abc"
405
+ instance.bar # => "definitely not xyz"
406
+ instance.baz # => 321
407
+ ```
408
+
409
+
410
+ ### `Callable`
411
+
412
+ A very simple module that allows a class to implement `self.call`, which
413
+ forwards any arguments to a new instance and then calls that instance.
414
+
415
+ Example:
416
+
417
+ ```ruby
418
+ require 'skywalker/callable'
419
+
420
+ class MyClass
421
+ include Skywalker::Callable
422
+
423
+ def initialize(message)
424
+ @message = message
425
+ end
426
+
427
+ def call
428
+ @message
429
+ end
430
+ end
431
+
432
+
433
+ MyClass.new("Hello World").call # => Hello World
434
+ MyClass.call("Hello World 2") # => Hello World 2
435
+ ```
436
+
437
+ Tiny but convenient.
438
+
439
+ ### `Transactional`
440
+
441
+ Will include `Acceptable`.
442
+
443
+ Implements the core transactional logic used by `Skywalker::Command`.
444
+
445
+ Makes `call` open a transaction, running `execute!` and calling
446
+ `confirm_success` or `confirm_failure` as appropriate.
447
+
448
+ For example, see `Skywalker::Command` documentation above.
316
449
 
317
450
  ## Contributing
318
451
 
@@ -1,33 +1,14 @@
1
1
  PATH
2
2
  remote: ../
3
3
  specs:
4
- skywalker (2.1.0)
5
- activerecord (>= 4.0)
4
+ skywalker (2.2.0)
6
5
 
7
6
  GEM
8
7
  remote: https://rubygems.org/
9
8
  specs:
10
- activemodel (4.2.1)
11
- activesupport (= 4.2.1)
12
- builder (~> 3.1)
13
- activerecord (4.2.1)
14
- activemodel (= 4.2.1)
15
- activesupport (= 4.2.1)
16
- arel (~> 6.0)
17
- activesupport (4.2.1)
18
- i18n (~> 0.7)
19
- json (~> 1.7, >= 1.7.7)
20
- minitest (~> 5.1)
21
- thread_safe (~> 0.3, >= 0.3.4)
22
- tzinfo (~> 1.1)
23
- arel (6.0.0)
24
- builder (3.2.2)
25
9
  coderay (1.1.0)
26
10
  diff-lcs (1.2.5)
27
- i18n (0.7.0)
28
- json (1.8.2)
29
11
  method_source (0.8.2)
30
- minitest (5.6.1)
31
12
  pry (0.10.1)
32
13
  coderay (~> 1.1.0)
33
14
  method_source (~> 0.8.1)
@@ -45,9 +26,6 @@ GEM
45
26
  rspec-support (~> 3.1.0)
46
27
  rspec-support (3.1.2)
47
28
  slop (3.6.0)
48
- thread_safe (0.3.5)
49
- tzinfo (1.2.2)
50
- thread_safe (~> 0.1)
51
29
 
52
30
  PLATFORMS
53
31
  ruby
@@ -56,3 +34,6 @@ DEPENDENCIES
56
34
  pry
57
35
  rspec
58
36
  skywalker!
37
+
38
+ BUNDLED WITH
39
+ 1.12.1
@@ -0,0 +1,30 @@
1
+ module Skywalker
2
+ module Callable
3
+
4
+ #
5
+ # Extend instead of include because we'd prefer to keep a uniform interface
6
+ # among Skywalker extensions, and we have in the past had additional
7
+ # instance methods defined herein.
8
+ #
9
+ # @since 2.2.0
10
+ #
11
+ def self.included(klass)
12
+ klass.extend ClassMethods
13
+ end
14
+
15
+
16
+ module ClassMethods
17
+
18
+ #
19
+ # Provides a convenient way to call a command without having to instantiate
20
+ # and call.
21
+ #
22
+ # @since 2.2.0
23
+ #
24
+ def call(*args)
25
+ new(*args).call
26
+ end
27
+ end
28
+ end
29
+ end
30
+
@@ -1,112 +1,9 @@
1
- require 'active_record'
2
- require 'skywalker/acceptable'
1
+ require 'skywalker/callable'
2
+ require 'skywalker/transactional'
3
3
 
4
4
  module Skywalker
5
5
  class Command
6
- include Acceptable
7
-
8
- ################################################################################
9
- # Class interface
10
- ################################################################################
11
-
12
- #
13
- # Provides a convenient way to call a command without having to instantiate
14
- # and call.
15
- #
16
- # @since 1.0.0
17
- #
18
- def self.call(*args)
19
- new(*args).call
20
- end
21
-
22
-
23
- attr_accessor :on_success,
24
- :on_failure,
25
- :error
26
-
27
-
28
- #
29
- # Call: runs the transaction and all operations.
30
- #
31
- # @since 1.0.0
32
- #
33
- def call
34
- transaction do
35
- execute!
36
-
37
- confirm_success
38
- end
39
-
40
- rescue Exception => error
41
- confirm_failure error
42
- end
43
-
44
-
45
- #
46
- # Procedural execution method. This should be implemented inside subclasses
47
- # to add operations.
48
- #
49
- # @since 1.0.0
50
- #
51
- protected def execute!
52
- end
53
-
54
-
55
- ################################################################################
56
- # Private interface
57
- ################################################################################
58
-
59
-
60
- #
61
- # Wraps the given block in transactional logic.
62
- #
63
- # @since 1.0.0
64
- #
65
- private def transaction(&block)
66
- ::ActiveRecord::Base.transaction(&block)
67
- end
68
-
69
- #
70
- # Triggers the given callback on success
71
- #
72
- # @since 1.0.0
73
- #
74
- private def confirm_success
75
- run_success_callbacks
76
- end
77
-
78
-
79
- #
80
- # Runs success callback if given. Override to specify additional callbacks
81
- # or to add branching logic here.
82
- #
83
- # @since 1.1.0
84
- #
85
- private def run_success_callbacks
86
- on_success.call(self) unless on_success.nil?
87
- end
88
-
89
-
90
- #
91
- # Triggered on failure of transaction. Sets `#error` so the exception can
92
- # be retrieved, and triggers the error callbacks.
93
- #
94
- # @since 1.0.0
95
- #
96
- private def confirm_failure(error)
97
- self.error = error
98
- run_failure_callbacks
99
- end
100
-
101
-
102
- #
103
- # Runs failure callback if given. Override to specify additional callbacks
104
- # or to add branching logic here.
105
- #
106
- # @since 1.1.0
107
- #
108
- private def run_failure_callbacks
109
- on_failure.call(self) unless on_failure.nil?
110
- end
6
+ include Callable
7
+ include Transactional
111
8
  end
112
9
  end
@@ -0,0 +1,127 @@
1
+ require 'skywalker/acceptable'
2
+
3
+ module Skywalker
4
+ module Transactional
5
+
6
+ #
7
+ # Requires Acceptable and add accessors for callbacks.
8
+ #
9
+ # @since 2.2.0
10
+ #
11
+ def self.included(klass)
12
+ klass.include Acceptable
13
+ klass.send(:attr_accessor, :on_success, :on_failure, :error)
14
+ end
15
+
16
+
17
+ #
18
+ # Runs the transaction and all operations.
19
+ #
20
+ # @since 2.2.0
21
+ #
22
+ def call
23
+ transaction do
24
+ execute!
25
+
26
+ confirm_success
27
+ end
28
+
29
+ rescue Exception => error
30
+ confirm_failure error
31
+ end
32
+
33
+
34
+ #
35
+ # Procedural execution method. This should be implemented inside subclasses
36
+ # to add operations.
37
+ #
38
+ # @since 2.2.0
39
+ #
40
+ protected def execute!
41
+ end
42
+
43
+
44
+ ################################################################################
45
+ # Private interface
46
+ ################################################################################
47
+
48
+
49
+ #
50
+ # Wraps the given block in transactional logic.
51
+ #
52
+ # @since 2.2.0
53
+ #
54
+ private def transaction(&block)
55
+ if active_record_defined?
56
+ active_record_transaction_method.call(&block)
57
+ else
58
+ block.call
59
+ end
60
+ end
61
+
62
+
63
+ #
64
+ # Allows us to artificially declare whether AR is loaded for specs.
65
+ #
66
+ # @since 2.2.0
67
+ #
68
+ private def active_record_defined?
69
+ defined?(ActiveRecord)
70
+ end
71
+
72
+ #
73
+ # Allows us to artificially choose which method to use as the AR
74
+ # transaction method.
75
+ #
76
+ # @since 2.2.0
77
+ #
78
+ private def active_record_transaction_method
79
+ ::ActiveRecord::Base.method(:transaction)
80
+ end
81
+
82
+
83
+ #
84
+ # Triggers the given callback on success
85
+ #
86
+ # @since 2.2.0
87
+ #
88
+ private def confirm_success
89
+ run_success_callbacks
90
+ end
91
+
92
+
93
+ #
94
+ # Runs success callback if given. Override to specify additional callbacks
95
+ # or to add branching logic here.
96
+ #
97
+ # @since 2.2.0
98
+ #
99
+ private def run_success_callbacks
100
+ on_success.call(self) unless on_success.nil?
101
+ end
102
+
103
+
104
+ #
105
+ # Triggered on failure of transaction. Sets `#error` so the exception can
106
+ # be retrieved, and triggers the error callbacks.
107
+ #
108
+ # @since 2.2.0
109
+ #
110
+ private def confirm_failure(error)
111
+ self.error = error
112
+ run_failure_callbacks
113
+ end
114
+
115
+
116
+ #
117
+ # Runs failure callback if given. Override to specify additional callbacks
118
+ # or to add branching logic here.
119
+ #
120
+ # @since 2.2.0
121
+ #
122
+ private def run_failure_callbacks
123
+ on_failure.call(self) unless on_failure.nil?
124
+ end
125
+
126
+ end
127
+ end
@@ -1,3 +1,3 @@
1
1
  module Skywalker
2
- VERSION = "2.1.0"
2
+ VERSION = "2.2.0"
3
3
  end
data/skywalker.gemspec CHANGED
@@ -20,8 +20,6 @@ Gem::Specification.new do |spec|
20
20
 
21
21
  spec.required_ruby_version = '>= 2.1.2'
22
22
 
23
- spec.add_dependency 'activerecord', '>= 4.0'
24
-
25
23
  spec.add_development_dependency "bundler", "~> 1.7"
26
24
  spec.add_development_dependency "rake", "~> 10.0"
27
25
  spec.add_development_dependency "rspec"
@@ -32,7 +32,7 @@ module Skywalker
32
32
 
33
33
  it "raises an error if an argument in its required_args is not present" do
34
34
  allow_any_instance_of(klass).to receive(:required_args).and_return([:required_arg])
35
- expect { klass.new }.to raise_error
35
+ expect { klass.new }.to raise_error ArgumentError
36
36
  end
37
37
 
38
38
  it "does not raise an error if an argument in its required_args is present" do
@@ -0,0 +1,23 @@
1
+ require 'spec_helper'
2
+ require 'skywalker/callable'
3
+
4
+ module Skywalker
5
+ RSpec.describe Callable do
6
+ let(:klass) { Class.new { include Callable } }
7
+
8
+ describe ".call" do
9
+ it "instantiates and calls" do
10
+ expect(klass).to receive_message_chain('new.call')
11
+ klass.call
12
+ end
13
+ end
14
+
15
+
16
+ describe "#call" do
17
+ it "raises an error because it is not defined" do
18
+ instance = klass.new
19
+ expect { instance.call }.to raise_error NoMethodError
20
+ end
21
+ end
22
+ end
23
+ end
@@ -3,135 +3,5 @@ require 'skywalker/command'
3
3
 
4
4
  module Skywalker
5
5
  RSpec.describe Command do
6
- describe "convenience" do
7
- it "provides a class call method that instantiates and calls" do
8
- expect(Command).to receive_message_chain('new.call')
9
- Command.call
10
- end
11
- end
12
-
13
-
14
- describe "validity control" do
15
- let(:command) { Command.new }
16
-
17
- it "executes in a transaction" do
18
- expect(command).to receive(:transaction)
19
- command.call
20
- end
21
- end
22
-
23
-
24
- describe "execution" do
25
- before do
26
- allow(command).to receive(:transaction).and_yield
27
- end
28
-
29
-
30
- describe "success handling" do
31
- let(:on_success) { double("on_success callback") }
32
- let(:command) { Command.new(on_success: on_success) }
33
-
34
- before do
35
- allow(command).to receive(:execute!).and_return(true)
36
- end
37
-
38
- it "triggers the confirm_success method" do
39
- expect(command).to receive(:confirm_success)
40
- command.call
41
- end
42
-
43
- it "runs the success callbacks" do
44
- expect(command).to receive(:run_success_callbacks)
45
- command.call
46
- end
47
-
48
- describe "on_success" do
49
- context "when on_success is not nil" do
50
- it "calls the on_success callback with itself" do
51
- expect(on_success).to receive(:call).with(command)
52
- command.call
53
- end
54
-
55
- context "when on_success is not callable" do
56
- let(:on_failure) { double("on_failure") }
57
- let(:command) { Command.new(on_success: "a string", on_failure: on_failure) }
58
-
59
- it "confirms failure if the on_success callback fails" do
60
- expect(on_failure).to receive(:call).with(command)
61
- command.call
62
- end
63
- end
64
- end
65
-
66
- context "when on_success is nil" do
67
- let(:nil_callback) { double("fakenil", nil?: true) }
68
- let(:command) { Command.new(on_success: nil_callback) }
69
-
70
- it "does not call on_success" do
71
- expect(nil_callback).not_to receive(:call)
72
- command.call
73
- end
74
- end
75
- end
76
- end
77
-
78
-
79
- describe "failure handling" do
80
- let(:on_failure) { double("on_failure callback") }
81
- let(:command) { Command.new(on_failure: on_failure) }
82
-
83
- before do
84
- allow(command).to receive(:execute!).and_raise(ScriptError)
85
- end
86
-
87
- it "triggers the confirm_failure method" do
88
- expect(command).to receive(:confirm_failure)
89
- command.call
90
- end
91
-
92
- it "sets the error on the command" do
93
- allow(on_failure).to receive(:call)
94
- expect(command).to receive(:error=)
95
- command.call
96
- end
97
-
98
- it "runs the failure callbacks" do
99
- allow(command).to receive(:error=)
100
- expect(command).to receive(:run_failure_callbacks)
101
- command.call
102
- end
103
-
104
- describe "on_failure" do
105
- before do
106
- allow(command).to receive(:error=)
107
- end
108
-
109
- context "when on_failure is not nil" do
110
- it "calls the on_failure callback with itself" do
111
- expect(on_failure).to receive(:call).with(command)
112
- command.call
113
- end
114
-
115
- context "when on_failure is not callable" do
116
- let(:command) { Command.new(on_failure: "a string") }
117
-
118
- it "raises an error" do
119
- expect { command.call }.to raise_error
120
- end
121
- end
122
- end
123
-
124
- context "when on_failure is nil" do
125
- let(:nil_callback) { double("fakenil", nil?: true) }
126
- let(:command) { Command.new(on_failure: nil_callback) }
127
-
128
- it "does not call on_failure" do
129
- expect(nil_callback).not_to receive(:call)
130
- command.call
131
- end
132
- end
133
- end
134
- end
135
- end
136
6
  end
137
7
  end
@@ -0,0 +1,163 @@
1
+ require 'spec_helper'
2
+ require 'skywalker/transactional'
3
+
4
+ module Skywalker
5
+ RSpec.describe Transactional do
6
+ let(:klass) { Class.new { include Transactional } }
7
+
8
+ describe "#transaction" do
9
+ let(:block) { Proc.new { "hey" } }
10
+ let(:instance) { klass.new }
11
+ let(:tx_method) { double("tx_method") }
12
+
13
+ context "when ActiveRecord is present" do
14
+ before do
15
+ allow(instance).to receive(:active_record_defined?).and_return true
16
+ allow(instance).to receive(:active_record_transaction_method).and_return tx_method
17
+ end
18
+
19
+ it "calls the transaction method with the block" do
20
+ allow(tx_method).to receive(:call) do |&blk|
21
+ blk.call(&block)
22
+ end
23
+
24
+ expect(instance.send(:transaction, &block)).to eq "hey"
25
+ end
26
+ end
27
+
28
+ context "when ActiveRecord is not present" do
29
+ before do
30
+ allow(instance).to receive(:active_record_defined?).and_return false
31
+ end
32
+
33
+ it "calls the block" do
34
+ expect(instance.send(:transaction, &block)).to eq "hey"
35
+ end
36
+ end
37
+ end
38
+
39
+ describe "validity control" do
40
+ let(:instance) { klass.new }
41
+
42
+ it "executes in a transaction" do
43
+ expect(instance).to receive(:transaction)
44
+ instance.call
45
+ end
46
+ end
47
+
48
+ describe "execution" do
49
+ before do
50
+ allow(instance).to receive(:transaction).and_yield
51
+ end
52
+
53
+
54
+ describe "success handling" do
55
+ let(:on_success) { double("on_success callback") }
56
+ let(:instance) { klass.new(on_success: on_success) }
57
+
58
+ before do
59
+ allow(instance).to receive(:execute!).and_return(true)
60
+ end
61
+
62
+ it "triggers the confirm_success method" do
63
+ expect(instance).to receive(:confirm_success)
64
+ instance.call
65
+ end
66
+
67
+ it "runs the success callbacks" do
68
+ expect(instance).to receive(:run_success_callbacks)
69
+ instance.call
70
+ end
71
+
72
+ describe "on_success" do
73
+ context "when on_success is not nil" do
74
+ it "calls the on_success callback with itself" do
75
+ expect(on_success).to receive(:call).with(instance)
76
+ instance.call
77
+ end
78
+
79
+ context "when on_success is not callable" do
80
+ let(:on_failure) { double("on_failure") }
81
+ let(:instance) { klass.new(on_success: "a string", on_failure: on_failure) }
82
+
83
+ it "confirms failure if the on_success callback fails" do
84
+ expect(on_failure).to receive(:call).with(instance)
85
+ instance.call
86
+ end
87
+ end
88
+ end
89
+
90
+ context "when on_success is nil" do
91
+ let(:nil_callback) { double("fakenil", nil?: true) }
92
+ let(:instance) { klass.new(on_success: nil_callback) }
93
+
94
+ it "does not call on_success" do
95
+ expect(nil_callback).not_to receive(:call)
96
+ instance.call
97
+ end
98
+ end
99
+ end
100
+ end
101
+
102
+
103
+ describe "failure handling" do
104
+ let(:on_failure) { double("on_failure callback") }
105
+ let(:instance) { klass.new(on_failure: on_failure) }
106
+
107
+ before do
108
+ allow(instance).to receive(:execute!).and_raise(ScriptError)
109
+ end
110
+
111
+ it "triggers the confirm_failure method" do
112
+ expect(instance).to receive(:confirm_failure)
113
+ instance.call
114
+ end
115
+
116
+ it "sets the error on the instance" do
117
+ allow(on_failure).to receive(:call)
118
+ expect(instance).to receive(:error=)
119
+ instance.call
120
+ end
121
+
122
+ it "runs the failure callbacks" do
123
+ allow(instance).to receive(:error=)
124
+ expect(instance).to receive(:run_failure_callbacks)
125
+ instance.call
126
+ end
127
+
128
+ describe "on_failure" do
129
+ before do
130
+ allow(instance).to receive(:error=)
131
+ end
132
+
133
+ context "when on_failure is not nil" do
134
+ it "calls the on_failure callback with itself" do
135
+ expect(on_failure).to receive(:call).with(instance)
136
+ instance.call
137
+ end
138
+
139
+ context "when on_failure is not callable" do
140
+ let(:instance) { klass.new(on_failure: "a string") }
141
+
142
+ it "raises an error" do
143
+ expect { instance.call }.to raise_error NoMethodError
144
+ end
145
+ end
146
+ end
147
+
148
+ context "when on_failure is nil" do
149
+ let(:nil_callback) { double("fakenil", nil?: true) }
150
+ let(:instance) { klass.new(on_failure: nil_callback) }
151
+
152
+ it "does not call on_failure" do
153
+ expect(nil_callback).not_to receive(:call)
154
+ instance.call
155
+ end
156
+ end
157
+ end
158
+ end
159
+ end
160
+
161
+ end
162
+ end
163
+
metadata CHANGED
@@ -1,29 +1,15 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: skywalker
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.1.0
4
+ version: 2.2.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: 2015-05-14 00:00:00.000000000 Z
11
+ date: 2016-06-21 00:00:00.000000000 Z
12
12
  dependencies:
13
- - !ruby/object:Gem::Dependency
14
- name: activerecord
15
- requirement: !ruby/object:Gem::Requirement
16
- requirements:
17
- - - ">="
18
- - !ruby/object:Gem::Version
19
- version: '4.0'
20
- type: :runtime
21
- prerelease: false
22
- version_requirements: !ruby/object:Gem::Requirement
23
- requirements:
24
- - - ">="
25
- - !ruby/object:Gem::Version
26
- version: '4.0'
27
13
  - !ruby/object:Gem::Dependency
28
14
  name: bundler
29
15
  requirement: !ruby/object:Gem::Requirement
@@ -116,11 +102,15 @@ files:
116
102
  - examples/spec/tiny_spec_helper.rb
117
103
  - lib/skywalker.rb
118
104
  - lib/skywalker/acceptable.rb
105
+ - lib/skywalker/callable.rb
119
106
  - lib/skywalker/command.rb
107
+ - lib/skywalker/transactional.rb
120
108
  - lib/skywalker/version.rb
121
109
  - skywalker.gemspec
122
110
  - spec/lib/skywalker/acceptable_spec.rb
111
+ - spec/lib/skywalker/callable_spec.rb
123
112
  - spec/lib/skywalker/command_spec.rb
113
+ - spec/lib/skywalker/transactional_spec.rb
124
114
  - spec/spec_helper.rb
125
115
  homepage: https://github.com/robyurkowski/skywalker
126
116
  licenses:
@@ -142,11 +132,13 @@ required_rubygems_version: !ruby/object:Gem::Requirement
142
132
  version: '0'
143
133
  requirements: []
144
134
  rubyforge_project:
145
- rubygems_version: 2.4.5
135
+ rubygems_version: 2.5.1
146
136
  signing_key:
147
137
  specification_version: 4
148
138
  summary: A simple command pattern implementation for transactional operations.
149
139
  test_files:
150
140
  - spec/lib/skywalker/acceptable_spec.rb
141
+ - spec/lib/skywalker/callable_spec.rb
151
142
  - spec/lib/skywalker/command_spec.rb
143
+ - spec/lib/skywalker/transactional_spec.rb
152
144
  - spec/spec_helper.rb