outbacker 0.0.2
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +14 -0
- data/.travis.yml +13 -0
- data/CHANGELOG.md +9 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +22 -0
- data/README.md +348 -0
- data/Rakefile +9 -0
- data/examples/app/controllers/appointments_controller.rb +77 -0
- data/examples/app/domain/appointment_calendar.rb +95 -0
- data/examples/config/initializers/outbacker.rb +27 -0
- data/examples/test/controllers/appointments_controller_test.rb +33 -0
- data/lib/outbacker.rb +130 -0
- data/lib/outbacker/version.rb +3 -0
- data/lib/test_support/outbacker_stub.rb +42 -0
- data/outbacker.gemspec +32 -0
- data/test/outbacker_stub_test.rb +97 -0
- data/test/outbacker_test.rb +315 -0
- data/test/test_helper.rb +64 -0
- metadata +167 -0
@@ -0,0 +1,95 @@
|
|
1
|
+
#
|
2
|
+
# Plain-old Ruby object that contains our business logic.
|
3
|
+
#
|
4
|
+
# Can be a domain object, a service object, a use-case object,
|
5
|
+
# a DCI context, or whatever.
|
6
|
+
#
|
7
|
+
class AppointmentCalendar
|
8
|
+
|
9
|
+
#
|
10
|
+
# Include the Outbacker module. See config/initializers/outbacker.rb
|
11
|
+
# for the restrictions on where you can include this.
|
12
|
+
#
|
13
|
+
include Outbacker
|
14
|
+
|
15
|
+
#
|
16
|
+
# An "outbacked" domain method, i.e., one that can
|
17
|
+
# process outcome callbacks passed into it—here via
|
18
|
+
# the &outcome_handlers parameter:
|
19
|
+
#
|
20
|
+
def book_appointment(params, &outcome_handlers)
|
21
|
+
|
22
|
+
#
|
23
|
+
# Immediately pass the outcome_handlers block to the
|
24
|
+
# with method (provided by Outbacker), which in turn
|
25
|
+
# takes a block that must wrap the body of your
|
26
|
+
# business-logic method:
|
27
|
+
#
|
28
|
+
with(outcome_handlers) do |outcomes|
|
29
|
+
if user_lacks_sufficient_credits?
|
30
|
+
#
|
31
|
+
# Trigger the insufficient_credits outcome and run the
|
32
|
+
# corresponding callback block (provided via the
|
33
|
+
# &outcome_handlers block):
|
34
|
+
#
|
35
|
+
outcomes.handle :insufficient_credits
|
36
|
+
return
|
37
|
+
end
|
38
|
+
|
39
|
+
appointment = Appointment.new(params)
|
40
|
+
if appointment.save
|
41
|
+
ledger.deduct_credits_for appointment
|
42
|
+
|
43
|
+
notify_user_about appointment
|
44
|
+
notify_office_about appointment
|
45
|
+
|
46
|
+
#
|
47
|
+
# Trigger the successful_booking outcome and run the
|
48
|
+
# corresponding callback block (provided via the
|
49
|
+
# &outcome_handlers block), and pass that block
|
50
|
+
# the newly-booked appointment:
|
51
|
+
#
|
52
|
+
outcomes.handle :successful_booking, appointment
|
53
|
+
else
|
54
|
+
#
|
55
|
+
# Trigger the failed_validation outcome and run the
|
56
|
+
# corresponding callback block (provided via the
|
57
|
+
# &outcome_handlers block), and pass that block
|
58
|
+
# the appointment that failed validation:
|
59
|
+
#
|
60
|
+
outcomes.handle :failed_validation, appointment
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
#
|
66
|
+
# Any other public business logic methods.
|
67
|
+
# ...
|
68
|
+
#
|
69
|
+
|
70
|
+
|
71
|
+
private
|
72
|
+
|
73
|
+
def user_lacks_sufficient_credits?
|
74
|
+
# Check current user's credit balance is >= cost of appointment.
|
75
|
+
end
|
76
|
+
|
77
|
+
def ledger
|
78
|
+
# Return Ledger object that manages credit balances and transactions.
|
79
|
+
end
|
80
|
+
|
81
|
+
def notify_user_about(appointment)
|
82
|
+
# Enqueue background jobs to send emails, SMS, phone push notifications, etc.
|
83
|
+
# This lets us avoids ActiveRecord callback spaghetti.
|
84
|
+
end
|
85
|
+
|
86
|
+
def notify_office_about(appointment)
|
87
|
+
# Post office dashboard notification and activity feed entry, enqueue
|
88
|
+
# background jobs to send emails, SMS, phone push notifications, etc.
|
89
|
+
# This also lets us avoids ActiveRecord callback spaghetti.
|
90
|
+
end
|
91
|
+
|
92
|
+
#
|
93
|
+
# Any other private internal helper methods.
|
94
|
+
#
|
95
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
|
2
|
+
#
|
3
|
+
# Specify where the Outbacker module cannot be included.
|
4
|
+
# If you try to include Outbacker within subclasses of
|
5
|
+
# ActiveRecord::Base, ActionController::Base, or
|
6
|
+
# MyBlacklistedClass, Outbacker will raise an exception.
|
7
|
+
#
|
8
|
+
# By default, ActiveRecord::Base and ActionController::Base
|
9
|
+
# are blacklisted. This is how Outbacker encourages skinny
|
10
|
+
# models, and discourages fat, obese models.
|
11
|
+
#
|
12
|
+
Outbacker.configure do |c|
|
13
|
+
c.blacklist = [ActiveRecord::Base, ActionController::Base, MyBlacklistedClass]
|
14
|
+
end
|
15
|
+
|
16
|
+
#
|
17
|
+
# Specify where the Outbacker module can be included.
|
18
|
+
# If you try to include Outbacker within subclasses of any
|
19
|
+
# classes other than UseCase, ServiceObject, or DomainObject,
|
20
|
+
# Outbacker will raise an exception.
|
21
|
+
#
|
22
|
+
# The default is an empty whitelist, but specifying a whitelist
|
23
|
+
# is recommended way to configure this policy.
|
24
|
+
#
|
25
|
+
Outbacker.configure do |c|
|
26
|
+
c.whitelist = [UseCase, ServiceObject, DomainObject]
|
27
|
+
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
|
2
|
+
#
|
3
|
+
# You'll probably want to move the following to your test_helper.rb
|
4
|
+
#
|
5
|
+
require 'outbacker'
|
6
|
+
require 'test_support/outbacker_stub'
|
7
|
+
|
8
|
+
|
9
|
+
class AppointmentsControllerTest < ActionController::TestCase
|
10
|
+
|
11
|
+
|
12
|
+
test "user is redirected to the credits purchase page when they lack sufficient credits" do
|
13
|
+
#
|
14
|
+
# Stub the AppointmntsController#book_appointment method, specifying that
|
15
|
+
# it will have an outcome of :insufficient_credits.
|
16
|
+
#
|
17
|
+
calendar_stub = Outbacker::OutbackerStub.new
|
18
|
+
calendar_stub.stub('book_appointment', :insufficient_credits, stubbed_appointment)
|
19
|
+
|
20
|
+
# This is a method we added to our controller to inject dependencies:
|
21
|
+
@controller.inject_calendar(calendar_stub)
|
22
|
+
|
23
|
+
post :create, appointment: {
|
24
|
+
starts_at: '201506051600-600',
|
25
|
+
ends_at: '201506051600-600',
|
26
|
+
etc: 'and so on'
|
27
|
+
}
|
28
|
+
|
29
|
+
assert_redirected_to new_credits_url
|
30
|
+
end
|
31
|
+
|
32
|
+
|
33
|
+
end
|
data/lib/outbacker.rb
ADDED
@@ -0,0 +1,130 @@
|
|
1
|
+
require "outbacker/version"
|
2
|
+
require "configurations"
|
3
|
+
|
4
|
+
module Outbacker
|
5
|
+
|
6
|
+
include Configurations
|
7
|
+
|
8
|
+
DEFAULT_BLACKLIST = [ActiveRecord::Base, ActionController::Base]
|
9
|
+
DEFAULT_WHITELIST = []
|
10
|
+
|
11
|
+
configuration_defaults do |c|
|
12
|
+
c.blacklist = DEFAULT_BLACKLIST
|
13
|
+
c.whitelist = DEFAULT_WHITELIST
|
14
|
+
end
|
15
|
+
|
16
|
+
|
17
|
+
#
|
18
|
+
# DSL-ish factory method to create an instance of OutcomeHandlerSet
|
19
|
+
# given a block of outcome handlers.
|
20
|
+
#
|
21
|
+
# To be used within your business-logic methods in combination with
|
22
|
+
# the OutcomeHandlerSet#handle method.
|
23
|
+
#
|
24
|
+
def with(outcome_handlers)
|
25
|
+
outcome_handler_set = OutcomeHandlerSet.new(outcome_handlers)
|
26
|
+
yield outcome_handler_set
|
27
|
+
|
28
|
+
if outcome_handlers.nil?
|
29
|
+
return outcome_handler_set.triggered_outcome, *outcome_handler_set.args
|
30
|
+
else
|
31
|
+
raise "No outcome selected" unless outcome_handler_set.outcome_handled?
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
#
|
36
|
+
# Class to encapsulate the processing of a block of outcome handlers.
|
37
|
+
#
|
38
|
+
OutcomeHandlerSet = Struct.new(:outcome_handlers,
|
39
|
+
:triggered_outcome,
|
40
|
+
:args,
|
41
|
+
:handled_outcome) do
|
42
|
+
|
43
|
+
#
|
44
|
+
# Process the outcome specified by the given outcome_key,
|
45
|
+
# using the outcome handlers set on this OutcomeHandlerSet
|
46
|
+
# instance. Any additiona arbitrary arguments can be passed
|
47
|
+
# through to the corresponding outcome handler callback.
|
48
|
+
#
|
49
|
+
def handle(outcome_key, *args)
|
50
|
+
self.triggered_outcome = outcome_key
|
51
|
+
self.args = args
|
52
|
+
|
53
|
+
if outcome_handlers
|
54
|
+
outcome_handlers.call(self)
|
55
|
+
raise "No outcome handler for outcome #{outcome_key}" unless outcome_handled?
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
#
|
60
|
+
# Internal method to indicate that the outcome has been
|
61
|
+
# handled by some han dler.
|
62
|
+
#
|
63
|
+
def outcome_handled?
|
64
|
+
!!self.handled_outcome
|
65
|
+
end
|
66
|
+
|
67
|
+
#
|
68
|
+
# Specify an outcome handler callback block for the specified
|
69
|
+
# outcome key.
|
70
|
+
#
|
71
|
+
def of(outcome_key, &outcome_block)
|
72
|
+
execute_outcome_block(outcome_key, &outcome_block)
|
73
|
+
end
|
74
|
+
|
75
|
+
#
|
76
|
+
# Provides an alternate way to specify a callback block using
|
77
|
+
# method names.
|
78
|
+
#
|
79
|
+
def method_missing(method_name, *args, &outcome_block)
|
80
|
+
super unless /^outcome_of_(?<suffix>.*)/ =~ method_name.to_s
|
81
|
+
outcome_key = suffix.to_sym
|
82
|
+
|
83
|
+
execute_outcome_block(outcome_key, &outcome_block)
|
84
|
+
end
|
85
|
+
|
86
|
+
|
87
|
+
private
|
88
|
+
|
89
|
+
#
|
90
|
+
# Internal helper method to execute the given outcome block
|
91
|
+
# if it matches the triggered outcome.
|
92
|
+
#
|
93
|
+
def execute_outcome_block(outcome_key, &outcome_block)
|
94
|
+
if !outcome_block
|
95
|
+
raise "No block provided for outcome #{outcome_key}"
|
96
|
+
end
|
97
|
+
|
98
|
+
if outcome_key == self.triggered_outcome
|
99
|
+
raise "Outcome #{outcome_key} already handled" if outcome_handled?
|
100
|
+
self.handled_outcome = outcome_key
|
101
|
+
outcome_block.call(*self.args)
|
102
|
+
end
|
103
|
+
end
|
104
|
+
|
105
|
+
end
|
106
|
+
|
107
|
+
#
|
108
|
+
# Restrict where Outbacker can be included.
|
109
|
+
#
|
110
|
+
def self.included(target_module)
|
111
|
+
apply_whitelist(target_module) if Outbacker.configuration.whitelist.any?
|
112
|
+
apply_blacklist(target_module) if Outbacker.configuration.blacklist.any?
|
113
|
+
end
|
114
|
+
|
115
|
+
def self.apply_whitelist(target_module)
|
116
|
+
Outbacker.configuration.whitelist.each do |whitelisted_classs|
|
117
|
+
return if target_module.ancestors.include?(whitelisted_classs)
|
118
|
+
end
|
119
|
+
fail "Can only include #{self.name} within a subclass of: #{Outbacker.configuration.whitelist.join(', ')}"
|
120
|
+
end
|
121
|
+
|
122
|
+
def self.apply_blacklist(target_module)
|
123
|
+
Outbacker.configuration.blacklist.each do |blacklisted_class|
|
124
|
+
if target_module.ancestors.include?(blacklisted_class)
|
125
|
+
fail "Cannot include #{self.name} within an #{blacklisted_class} class, a plain-old Ruby object is preferred."
|
126
|
+
end
|
127
|
+
end
|
128
|
+
end
|
129
|
+
|
130
|
+
end
|
@@ -0,0 +1,42 @@
|
|
1
|
+
#
|
2
|
+
# Provides a simple means to stub "Outbacked" methods. Specifically,
|
3
|
+
# it lets you specify what outcome callback you want invoked on
|
4
|
+
# your stubbed method.
|
5
|
+
#
|
6
|
+
# Usage:
|
7
|
+
# stubbed_object = OutbackerStub.new
|
8
|
+
# stubbed_object.stub('stubbed_method_name',
|
9
|
+
# :desired_outcome,
|
10
|
+
# block_arg1, block_arg2, ...)
|
11
|
+
#
|
12
|
+
# Alternatively, combine instantiation and stubbing:
|
13
|
+
# stubbed_object = OutbackerStub.new('stubbed_method_name',
|
14
|
+
# :desired_outcome,
|
15
|
+
# block_arg1, block_arg2, ...)
|
16
|
+
#
|
17
|
+
# Note that this only provides stubbing functionality, no mocking
|
18
|
+
# functionality (i.e., ability to verify that a method was invoked on
|
19
|
+
# a test double) is provided. This should be sufficient for your test
|
20
|
+
# needs, as you can/should write separate tests to verify that
|
21
|
+
# the expected methods were invoked on your double.
|
22
|
+
#
|
23
|
+
module Outbacker
|
24
|
+
class OutbackerStub
|
25
|
+
include Outbacker
|
26
|
+
|
27
|
+
def initialize(method_name=nil, outcome_key=nil, *block_args)
|
28
|
+
if method_name && outcome_key
|
29
|
+
stub(method_name, outcome_key, *block_args)
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
def stub(method_name, outcome_key, *block_args)
|
34
|
+
define_singleton_method(method_name, ->(*args, &outcome_handlers) {
|
35
|
+
with(outcome_handlers) do |outcomes|
|
36
|
+
outcomes.handle outcome_key, *block_args
|
37
|
+
end
|
38
|
+
})
|
39
|
+
end
|
40
|
+
|
41
|
+
end
|
42
|
+
end
|
data/outbacker.gemspec
ADDED
@@ -0,0 +1,32 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
lib = File.expand_path('../lib', __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require 'outbacker/version'
|
5
|
+
|
6
|
+
Gem::Specification.new do |spec|
|
7
|
+
spec.name = "outbacker"
|
8
|
+
spec.version = Outbacker::VERSION
|
9
|
+
spec.authors = ["Anthony Garcia"]
|
10
|
+
spec.email = ["polypressure@outlook.com"]
|
11
|
+
spec.summary = "Drive complexity out of your Rails controllers once and for all, while keeping your models fit and trim."
|
12
|
+
spec.description = <<-DESC
|
13
|
+
A micro library to keep conditional logic out of your Rails
|
14
|
+
controllers and help you write more intention-revealing
|
15
|
+
code with both skinny controllers and skinny models.
|
16
|
+
DESC
|
17
|
+
spec.homepage = "https://github.com/polypressure/outbacker"
|
18
|
+
spec.license = "MIT"
|
19
|
+
|
20
|
+
spec.files = `git ls-files -z`.split("\x0")
|
21
|
+
spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
|
22
|
+
spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
|
23
|
+
spec.require_paths = ["lib"]
|
24
|
+
|
25
|
+
spec.add_development_dependency "bundler", "~> 1.7"
|
26
|
+
spec.add_development_dependency "rake", "~> 10.0"
|
27
|
+
spec.add_development_dependency "minitest", "~> 5.5"
|
28
|
+
spec.add_development_dependency "m", "~> 1.3.1"
|
29
|
+
spec.add_development_dependency "simplecov"
|
30
|
+
spec.add_development_dependency 'configurations', '~> 2.2.0'
|
31
|
+
spec.add_development_dependency "codeclimate-test-reporter"
|
32
|
+
end
|
@@ -0,0 +1,97 @@
|
|
1
|
+
require 'test_helper'
|
2
|
+
|
3
|
+
class OutbackerStubTest < Minitest::Test
|
4
|
+
extend DefineTestNamesWithStrings
|
5
|
+
|
6
|
+
test "#stub defines the specified method on the stub instance" do
|
7
|
+
outbacker_stub = Outbacker::OutbackerStub.new
|
8
|
+
|
9
|
+
outbacker_stub.stub('register_user', :successful_registration, Object.new)
|
10
|
+
|
11
|
+
assert_respond_to outbacker_stub, 'register_user'
|
12
|
+
end
|
13
|
+
|
14
|
+
test "#stub invokes the outcome callback for the specified outcome key" do
|
15
|
+
outbacker_stub = Outbacker::OutbackerStub.new
|
16
|
+
correct_block_executed = false
|
17
|
+
|
18
|
+
outbacker_stub.stub('register_user', :successful_registration, Object.new)
|
19
|
+
|
20
|
+
outbacker_stub.register_user do |on_outcome|
|
21
|
+
on_outcome.of(:successful_registration) do |user|
|
22
|
+
correct_block_executed = true
|
23
|
+
end
|
24
|
+
|
25
|
+
on_outcome.of(:failed_validation) do |user|
|
26
|
+
correct_block_executed = false
|
27
|
+
end
|
28
|
+
|
29
|
+
on_outcome.of(:some_other_outcome) do |user|
|
30
|
+
correct_block_executed = false
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
assert correct_block_executed, "Outcome block not executed by stub."
|
35
|
+
end
|
36
|
+
|
37
|
+
test "#stub passes the specified block arguments to the outcome block" do
|
38
|
+
outbacker_stub = Outbacker::OutbackerStub.new
|
39
|
+
block_arg_1 = Object.new
|
40
|
+
block_arg_2 = Object.new
|
41
|
+
block_args_passed = []
|
42
|
+
|
43
|
+
outbacker_stub.stub('register_user', :successful_registration, block_arg_1, block_arg_2)
|
44
|
+
|
45
|
+
outbacker_stub.register_user do |on_outcome|
|
46
|
+
on_outcome.of(:successful_registration) do |arg1, arg2|
|
47
|
+
block_args_passed = [arg1, arg2]
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
assert_equal [block_arg_1, block_arg_2], block_args_passed
|
52
|
+
end
|
53
|
+
|
54
|
+
|
55
|
+
test "#new defines the specified method on the stub instance" do
|
56
|
+
outbacker_stub = Outbacker::OutbackerStub.new('register_user', :successful_registration, Object.new)
|
57
|
+
|
58
|
+
assert_respond_to outbacker_stub, 'register_user'
|
59
|
+
end
|
60
|
+
|
61
|
+
test "#new invokes the outcome callback for the specified outcome key" do
|
62
|
+
outbacker_stub = Outbacker::OutbackerStub.new('register_user', :successful_registration, Object.new)
|
63
|
+
correct_block_executed = false
|
64
|
+
|
65
|
+
outbacker_stub.register_user do |on_outcome|
|
66
|
+
on_outcome.of(:successful_registration) do |user|
|
67
|
+
correct_block_executed = true
|
68
|
+
end
|
69
|
+
|
70
|
+
on_outcome.of(:failed_validation) do |user|
|
71
|
+
correct_block_executed = false
|
72
|
+
end
|
73
|
+
|
74
|
+
on_outcome.of(:some_other_outcome) do |user|
|
75
|
+
correct_block_executed = false
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
assert correct_block_executed, "Outcome block not executed by stub."
|
80
|
+
end
|
81
|
+
|
82
|
+
test "#new passes the specified block arguments to the outcome block" do
|
83
|
+
block_arg_1 = Object.new
|
84
|
+
block_arg_2 = Object.new
|
85
|
+
outbacker_stub = Outbacker::OutbackerStub.new('register_user', :successful_registration, block_arg_1, block_arg_2)
|
86
|
+
block_args_passed = []
|
87
|
+
|
88
|
+
outbacker_stub.register_user do |on_outcome|
|
89
|
+
on_outcome.of(:successful_registration) do |arg1, arg2|
|
90
|
+
block_args_passed = [arg1, arg2]
|
91
|
+
end
|
92
|
+
end
|
93
|
+
|
94
|
+
assert_equal [block_arg_1, block_arg_2], block_args_passed
|
95
|
+
end
|
96
|
+
|
97
|
+
end
|