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.
@@ -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
@@ -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,3 @@
1
+ module Outbacker
2
+ VERSION = "0.0.2"
3
+ 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
@@ -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