skywalker 1.0.0 → 1.1.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 189ad007dad735d22002e8e676be20d37202e427
4
- data.tar.gz: 49633c02c8e18e1572ed5d570b3ad49e1ca71f85
3
+ metadata.gz: 3f6ceac6b8264295e36a783bfb010c99b1caa549
4
+ data.tar.gz: 2705c927542812c9033a49957d37ed00a7a9cddf
5
5
  SHA512:
6
- metadata.gz: 32d48d08a9c6bd7a25af17485f2f0202fcee026b32133c3851093e2c530959148578c01dc29b012ea8f6ff08692c212b7eb8b912bdd695c1b4bc0c3bf3d77cc3
7
- data.tar.gz: 4103d5ba416a5e403d2c00390c92f62d8001e7712a56b9c692e186ddc6d78e8eba589bae6b201ae4521e252cac066a391739d4dc98255a03834beb16eed91208
6
+ metadata.gz: 93acfda6e3a9adf0516d804f206d234e5cf14efe338eb4abeef2bf810917e1bcdd9638c04545aad6823b4f5b275c48fafe40f93dea498c9161f800bbdc5fa6f7
7
+ data.tar.gz: 6b1a5df15debb17a660a2fa92eb95ce14b8b555a056768b905245ac1491f46d6ea398ef5ab50f9221deef18ff736b3c9ceb51a47bdc0ef7931994b6479b69c95
data/CHANGELOG.md ADDED
@@ -0,0 +1,13 @@
1
+ All commits by Rob Yurkowski unless otherwise noted.
2
+
3
+
4
+ ## 1.1.0 (2014-11-03)
5
+
6
+ - Expanded documentation.
7
+ - Allow `on_success` and `on_failure` callbacks to be optional.
8
+ - Allow passing arbitrary values to command instantiation.
9
+ - Adds this changelog.
10
+
11
+ ## 1.0.0 (2014-11-02)
12
+
13
+ - Initial creation.
data/README.md CHANGED
@@ -4,8 +4,8 @@ Skywalker is a gem that provides a simple command pattern for applications that
4
4
 
5
5
  ## Why Skywalker?
6
6
 
7
- It's impossible to come up with a single-word clever name for a gem about commands. If you can't
8
- achieve clever, achieve topicality.
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
9
 
10
10
  ## What is a command?
11
11
 
@@ -34,7 +34,19 @@ having to know its internals. Standard caveats apply, but if you have a `CreateG
34
34
  command, you should be able to infer that calling the command with the correct arguments
35
35
  will produce the expected result.
36
36
 
37
- ### A gateway to harder architectures
37
+ ### Knowledge of Results Without Knowledge of Response
38
+
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.
44
+
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.
48
+
49
+ ### A Gateway to Harder Architectures
38
50
 
39
51
  It's not hard to create an `Event` class and step up toward full event sourcing, or to
40
52
  go a bit further and implement full CQRS. This is the architectural pattern your parents
@@ -58,9 +70,153 @@ Or install it yourself as:
58
70
 
59
71
  ## Usage
60
72
 
73
+ Let's talk about a situation where you're creating a group and sending an email inside a
74
+ Rails app.
75
+
76
+ Standard operating procedure usually falls into one of two patterns, both of which are
77
+ mediocre. The first makes use of ActiveRecord callbacks:
78
+
79
+ ```ruby
80
+ # app/controllers/groups_controller.rb
81
+ class GroupsController < ApplicationController
82
+ # ...
83
+
84
+ def create
85
+ @group = Group.new(params.require(:group).permit(:name))
86
+
87
+ if @group.save
88
+ redirect_to @group, notice: "Created the group!"
89
+ else
90
+ flash[:alert] = "Oh no, something went wrong!"
91
+ render :new
92
+ end
93
+ end
94
+ end
95
+
96
+
97
+ # app/models/group.rb
98
+ class Group < ActiveRecord::Base
99
+ after_create :send_notification
100
+
101
+ private
102
+
103
+ def send_notification
104
+ NotificationMailer.group_created_notification(self).deliver
105
+ end
106
+ end
107
+ ```
108
+
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.
117
+
118
+ **Moral #1: Orthogonal concerns should not be put into ActiveRecord callbacks.**
119
+
120
+ The alternative is to keep this inside the controller:
121
+
122
+ ```ruby
123
+ # app/controllers/groups_controller.rb
124
+ class GroupsController < ApplicationController
125
+ # ...
126
+
127
+ def create
128
+ @group = Group.new(params.require(:group).permit(:name))
129
+
130
+ if @group.save
131
+ NotificationMailer.group_created_notification(@group).deliver
132
+ redirect_to @group, notice: "Created the group!"
133
+ else
134
+ flash[:alert] = "Oh no, something went wrong!"
135
+ render :new
136
+ end
137
+ end
138
+ end
139
+ ```
140
+
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.
145
+
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.
151
+
152
+ **Moral #2: Rails controllers should dispatch to application logic, and receive instructions on how to respond.**
153
+
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`:
156
+
157
+
158
+ ```ruby
159
+ # app/controllers/groups_controller.rb
160
+ class GroupsController < ApplicationController
161
+ # ...
162
+
163
+ def create
164
+ CreateGroupCommand.call(
165
+ group: Group.new(params.require(:group).permit(:name)),
166
+ on_success: method(:on_create_success),
167
+ on_failure: method(:on_create_failure)
168
+ )
169
+ end
170
+
171
+
172
+ def on_create_success(command)
173
+ redirect_to command.group, notice: "Created the group!"
174
+ end
175
+
176
+
177
+ def on_create_failure(command)
178
+ flash[:alert] = "Oh no, something went wrong!"
179
+ @group = command.group
180
+ render :new
181
+ end
182
+ end
183
+
184
+
185
+ # app/commands/create_group_command.rb
186
+ class CreateGroupCommand < Skywalker::Command
187
+ def execute!
188
+ save_group!
189
+ send_notification!
190
+ end
191
+
192
+
193
+ private def save_group!
194
+ group.save!
195
+ end
196
+
197
+
198
+ private def send_notifications!
199
+ notifier.call(group).deliver
200
+ end
201
+
202
+
203
+ private def notifier
204
+ @notifier ||= NotificationsMailer.method(:group_created_notification)
205
+ end
206
+ end
207
+ ```
208
+
209
+ You can of course set up a default for Group as with `#notifier` and pass in
210
+ params only, but I find injecting a pre-constructed ActiveRecord object usually
211
+ works well.
212
+
213
+
214
+ ### Basic Composition Summary
61
215
  Compose your commands:
62
216
 
63
217
  ```ruby
218
+ require 'skywalker/command'
219
+
64
220
  class AddGroupCommand < Skywalker::Command
65
221
  def execute!
66
222
  # Your transactional operations go here. No need to open a transaction.
@@ -73,13 +229,36 @@ Then call your commands:
73
229
 
74
230
  ```ruby
75
231
  command = AddGroupCommand.call(
76
- on_success: method(:on_success),
77
- on_failure: method(:on_failure)
232
+ any_keyword_argument: "Is taken and has an attr_accessor defined for it."
78
233
  )
79
234
 
80
- 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.
81
235
  ```
82
236
 
237
+ 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.
238
+
239
+ ### Overriding Methods
240
+
241
+ The following methods are overridable for easy customization:
242
+
243
+ - `execute!`
244
+ - Define your operations here.
245
+ - `transaction(&block)`
246
+ - Uses an `ActiveRecord::Base.transaction` by default, but can be customized. `execute!` runs inside of this.
247
+ - `confirm_success`
248
+ - Fires off callbacks on command success (i.e. non-error).
249
+ - `run_success_callbacks`
250
+ - Dictates which success callbacks are run. Defaults to `on_success` if defined.
251
+ - `confirm_failure`
252
+ - Fires off callbacks on command failure (i.e. erroneous state), and sets the exception as `command.error`.
253
+ - `run_failure_callbacks`
254
+ - Dictates which failure callbacks are run. Defaults to `on_failure` if defined.
255
+
256
+ For further reference, simply see the command file. It's less than 90 LOC and well-commented.
257
+
258
+ ## Testing
259
+
260
+ To come.
261
+
83
262
  ## Contributing
84
263
 
85
264
  1. Fork it ( https://github.com/robyurkowski/skywalker/fork )
@@ -13,9 +13,14 @@ module Skywalker
13
13
  ################################################################################
14
14
  # Instantiates command, setting all arguments.
15
15
  ################################################################################
16
- def initialize(on_success: nil, on_failure: nil)
17
- self.on_success = on_success
18
- self.on_failure = on_failure
16
+ def initialize(**args)
17
+ args.each_pair do |k, v|
18
+ singleton_class.class_eval do
19
+ send(:attr_accessor, k) unless self.respond_to? k
20
+ end
21
+
22
+ self.send("#{k}=", v)
23
+ end
19
24
  end
20
25
 
21
26
 
@@ -57,7 +62,12 @@ module Skywalker
57
62
  # Trigger the given callback on success
58
63
  ################################################################################
59
64
  private def confirm_success
60
- on_success.call(self)
65
+ run_success_callbacks
66
+ end
67
+
68
+
69
+ private def run_success_callbacks
70
+ on_success.call(self) if self.respond_to?(:on_success)
61
71
  end
62
72
 
63
73
 
@@ -66,7 +76,12 @@ module Skywalker
66
76
  ################################################################################
67
77
  private def confirm_failure(error)
68
78
  self.error = error
69
- on_failure.call(self)
79
+ run_failure_callbacks
80
+ end
81
+
82
+
83
+ private def run_failure_callbacks
84
+ on_failure.call(self) if self.respond_to?(:on_failure)
70
85
  end
71
86
  end
72
87
  end
@@ -1,3 +1,3 @@
1
1
  module Skywalker
2
- VERSION = "1.0.0"
2
+ VERSION = "1.1.0"
3
3
  end
data/lib/skywalker.rb CHANGED
@@ -1,5 +1,4 @@
1
1
  require "skywalker/version"
2
2
 
3
3
  module Skywalker
4
- # Your code goes here...
5
4
  end
data/skywalker.gemspec CHANGED
@@ -18,6 +18,8 @@ Gem::Specification.new do |spec|
18
18
  spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
19
19
  spec.require_paths = ["lib"]
20
20
 
21
+ spec.required_ruby_version = '>= 2.1.2'
22
+
21
23
  spec.add_dependency 'activerecord', '~> 4.1'
22
24
 
23
25
  spec.add_development_dependency "bundler", "~> 1.7"
@@ -5,21 +5,20 @@ module Skywalker
5
5
  RSpec.describe Command do
6
6
  describe "convenience" do
7
7
  it "provides a class call method that instantiates and calls" do
8
- arg = 'blah'
9
-
10
8
  expect(Command).to receive_message_chain('new.call')
11
- Command.call(arg)
9
+ Command.call
12
10
  end
13
11
  end
14
12
 
15
13
 
16
14
  describe "instantiation" do
17
- it "accepts an on_success callback" do
18
- expect { Command.new(on_success: ->{ nil }) }.not_to raise_error
15
+ it "accepts a variable list of arguments" do
16
+ expect { Command.new(a_symbol: :my_symbol, a_string: "my string") }.not_to raise_error
19
17
  end
20
18
 
21
- it "accepts an on_failure callback" do
22
- expect { Command.new(on_failure: ->{ nil }) }.not_to raise_error
19
+ it "sets an instance variable for each argument" do
20
+ command = Command.new(a_symbol: :my_symbol)
21
+ expect(command.a_symbol).to eq(:my_symbol)
23
22
  end
24
23
  end
25
24
 
@@ -53,10 +52,27 @@ module Skywalker
53
52
  command.call
54
53
  end
55
54
 
56
- it "calls the on_success callback with itself" do
57
- expect(on_success).to receive(:call).with(command)
55
+ it "runs the success callbacks" do
56
+ expect(command).to receive(:run_success_callbacks)
58
57
  command.call
59
58
  end
59
+
60
+ describe "on_success" do
61
+ context "when on_success is defined" do
62
+ it "calls the on_success callback with itself" do
63
+ expect(on_success).to receive(:call).with(command)
64
+ command.call
65
+ end
66
+ end
67
+
68
+ context "when on_success is not defined" do
69
+ let(:command) { Command.new }
70
+
71
+ it "does not call on_success" do
72
+ expect(command).not_to receive(:on_success)
73
+ end
74
+ end
75
+ end
60
76
  end
61
77
 
62
78
 
@@ -79,11 +95,32 @@ module Skywalker
79
95
  command.call
80
96
  end
81
97
 
82
- it "calls the on_failure callback with itself" do
98
+ it "runs the failure callbacks" do
83
99
  allow(command).to receive(:error=)
84
- expect(on_failure).to receive(:call).with(command)
100
+ expect(command).to receive(:run_failure_callbacks)
85
101
  command.call
86
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 defined" 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
+ end
115
+
116
+ context "when on_failure is not defined" do
117
+ let(:command) { Command.new }
118
+
119
+ it "does not call on_failure" do
120
+ expect(command).not_to receive(:on_failure)
121
+ end
122
+ end
123
+ end
87
124
  end
88
125
  end
89
126
  end
data/spec/spec_helper.rb CHANGED
@@ -54,7 +54,7 @@ RSpec.configure do |config|
54
54
 
55
55
  # This setting enables warnings. It's recommended, but in some cases may
56
56
  # be too noisy due to issues in dependencies.
57
- config.warnings = true
57
+ # config.warnings = true
58
58
 
59
59
  # Many RSpec users commonly either run the entire suite or an individual
60
60
  # file, and it's useful to allow more verbose output when running an
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: skywalker
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.0.0
4
+ version: 1.1.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Rob Yurkowski
@@ -103,6 +103,7 @@ extra_rdoc_files: []
103
103
  files:
104
104
  - ".gitignore"
105
105
  - ".rspec"
106
+ - CHANGELOG.md
106
107
  - Gemfile
107
108
  - LICENSE.txt
108
109
  - README.md
@@ -125,7 +126,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
125
126
  requirements:
126
127
  - - ">="
127
128
  - !ruby/object:Gem::Version
128
- version: '0'
129
+ version: 2.1.2
129
130
  required_rubygems_version: !ruby/object:Gem::Requirement
130
131
  requirements:
131
132
  - - ">="