outbacker 0.0.2
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 +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
|