slayer 0.3.1 → 0.4.0.beta2

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: edff62ac6c3e45fdd46766caf1c9c49633413f48
4
- data.tar.gz: 433b5038d863ad187fbe4537fb535dbb31c428c4
3
+ metadata.gz: b6ddcd92133adc30cc39358c00cfd4cf8494d879
4
+ data.tar.gz: b427f2b04fe18881c4448de84e06e2593a1bcece
5
5
  SHA512:
6
- metadata.gz: 4c3a4d548e02caccc81c52d431146c345f3a4c5a8e74ce1e5141c971c8097cc9f4ee304e63aaadc4333cb4dfdc846ad2fccac1e20abe25890684b1122657d766
7
- data.tar.gz: ef8cc22437e472d3f59e5056441735550e24e5b6fb440b382df8c1cc0e9176451b40f80dae05cecb22f87d2ea84368124456f3e5113f318c9d545aadb6b3ede9
6
+ metadata.gz: 578355462582a7e68883d7aa7d73f9e52f4d8870d797c63f3f75061c9ccdd445342e9f51c0348583a022403e3a7569503808952764b07816536627eca2cf5a95
7
+ data.tar.gz: 6ce3eda0fce30e75602a50fefd714d3df12baf17a89f1fdd37914c6b47120fd1488830ac4e6a8a63fbae6b8c2410a51ddce178296b56788ebba1d9543a102865
data/.gitignore CHANGED
@@ -9,4 +9,6 @@
9
9
  /spec/reports/
10
10
  /tmp/
11
11
  /log*/*
12
- .byebug_history
12
+ .byebug_history
13
+
14
+ .DS_Store
data/README.md CHANGED
@@ -16,13 +16,15 @@ Slayer provides 3 base classes for organizing your business logic: `Forms`, `Com
16
16
 
17
17
  ### Commands
18
18
 
19
- `Slayer::Commands` are the bread and butter of your application's business logic. `Commands` are where you compose services, and perform one-off business logic tasks. In our applications, we usually create a single `Command` per `Controller` endpoint.
19
+ `Slayer::Commands` are the bread and butter of your application's business logic, and a specific implementation of the `Slayer::Service` object. `Commands` are where you compose services, and perform one-off business logic tasks. In our applications, we usually create a single `Command` per `Controller` endpoint.
20
20
 
21
- `Commands` should call `Services`, but `Services` should never call `Commands`.
21
+ `Slayer::Commands` must implement a `call` method, which always return a structured `Slayer::Result` object making operating on results straightforward. The `call` method can also take a block, which provides `Slayer::ResultMatcher` object, and enforces handling of both `pass` and `fail` conditions for that result.
22
+
23
+ This helps provide confidence that your core business logic is behaving in expected ways, and helps coerce you to develop in a clean and testable way.
22
24
 
23
25
  ### Services
24
26
 
25
- `Services` are the building blocks of `Commands`, and encapsulate re-usable chunks of application logic.
27
+ `Slayer::Service`s are the base class of `Slayer::Command`s, and encapsulate re-usable chunks of application logic. `Services` also return structured `Slayer::Result` objects.
26
28
 
27
29
  ## Installation
28
30
 
@@ -44,6 +46,224 @@ Or install it yourself as:
44
46
  $ gem install slayer
45
47
  ```
46
48
 
49
+ ## Usage
50
+
51
+ ### Commands
52
+
53
+ Slayer Commands should implement `call`, which will `pass` or `fail` the service based on input. Commands return a `Slayer::Result` which has a predictable interface for determining `success?` or `failure?`, a 'value' payload object, a 'status' value, and a user presentable `message`.
54
+
55
+ ```ruby
56
+ # A Command that passes when given the string "foo"
57
+ # and fails if given anything else.
58
+ class FooCommand < Slayer::Command
59
+ def call(foo:)
60
+ unless foo == "foo"
61
+ flunk! value: foo, message: "Argument must be foo!"
62
+ end
63
+
64
+ pass! value: foo
65
+ end
66
+ end
67
+ ```
68
+
69
+ Handling the results of a command can be done in two ways. The primary way is through a handler block. This block is passed a handler object, which is in turn given blocks to handle different result outcomes:
70
+
71
+ ```ruby
72
+ FooCommand.call(foo: "foo") do |m|
73
+ m.pass do |result|
74
+ puts "This code runs on success"
75
+ end
76
+
77
+ m.fail do |result|
78
+ puts "This code runs on failure. Message: #{result.message}"
79
+ end
80
+
81
+ m.all do
82
+ puts "This code runs on failure or success"
83
+ end
84
+
85
+ m.ensure do
86
+ puts "This code always runs after other handler blocks"
87
+ end
88
+ end
89
+ ```
90
+
91
+ The second is less comprehensive, but can be useful for very simple commands. The `call` method on a `Command` returns its result object, which has statuses set on itself:
92
+
93
+ ```ruby
94
+ result = FooCommand.call(foo: "foo")
95
+ puts result.success? # => true
96
+
97
+ result = FooCommand.call(foo: "bar")
98
+ puts result.success? # => false
99
+ ```
100
+
101
+ Here's a more complex example demonstrating how the command pattern can be used to encapuslate the logic for validating and creating a new user. This example is shown using a `rails` controller, but the same approach can be used regardless of the framework.
102
+
103
+ ```ruby
104
+ # commands/user_controller.rb
105
+ class CreateUserCommand < Slayer::Command
106
+ def call(create_user_form:)
107
+ unless arguments_valid?(create_user_form)
108
+ flunk! value: create_user_form, status: :arguments_invalid
109
+ end
110
+
111
+ user = nil
112
+ transaction do
113
+ user = User.create(create_user_form.attributes)
114
+ end
115
+
116
+ unless user.persisted?
117
+ flunk! message: I18n.t('user.create.error'), status: :unprocessible_entity
118
+ end
119
+
120
+ pass! value: user
121
+ end
122
+
123
+ def arguments_valid?(create_user_form)
124
+ create_user_form.kind_of?(CreateUserForm) &&
125
+ create_user_form.valid? &&
126
+ !User.exists?(email: create_user_form.email)
127
+ end
128
+ end
129
+
130
+ # controllers/user_controller.rb
131
+ class UsersController < ApplicationController
132
+ def create
133
+ @create_user_form = CreateUserForm.from_params(create_user_params)
134
+
135
+ CreateUserCommand.call(create_user_form: @create_user_form) do |m|
136
+ m.pass do |user|
137
+ auto_login(user)
138
+ redirect_to root_path, notice: t('user.create.success')
139
+ end
140
+
141
+ m.fail(:arguments_invalid) do |result|
142
+ flash[:error] = result.errors.full_messages.to_sentence
143
+ render :new, status: :unprocessible_entity
144
+ end
145
+
146
+ m.fail do |result|
147
+ flash[:error] = t('user.create.error')
148
+ render :new, status: :bad_request
149
+ end
150
+ end
151
+ end
152
+
153
+ private
154
+
155
+ def required_user_params
156
+ [:first_name, :last_name, :email, :password]
157
+ end
158
+
159
+ def create_user_params
160
+ permitted_params = required_user_params << :password_confirmation
161
+ params.require(:user).permit(permitted_params)
162
+ end
163
+ end
164
+ ```
165
+
166
+ ### Result Matcher
167
+
168
+ The result matcher is an object that is used to handle `Slayer::Result` objects based on their status.
169
+
170
+ #### Handlers: `pass`, `fail`, `all`, `ensure`
171
+
172
+ The result matcher block can take 4 types of handler blocks: `pass`, `fail`, `all`, and `ensure`. They operate as you would expect based on their names.
173
+
174
+ * The `pass` block runs if the command was successful.
175
+ * The `fail` block runs if the command was `flunked`.
176
+ * The `all` block runs on any type of result --- `pass` or `fail` --- unless the result has already been handled.
177
+ * The `ensure` block always runs after the result has been handled.
178
+
179
+ #### Handler Params
180
+
181
+ Every handler in the result matcher block is given three arguments: `value`, `result`, and `command`. These encapsulate the `value` provided in the `pass!` or `flunk!` call from the `Command`, the returned `Slayer::Result` object, and the `Slayer::Command` instance that was just run:
182
+
183
+ ```ruby
184
+ class NoArgCommand < Slayer::Command
185
+ def call
186
+ @instance_var = 'instance'
187
+ pass value: 'pass'
188
+ end
189
+ end
190
+
191
+
192
+ NoArgCommand.call do |m|
193
+ m.all do |value, result, command|
194
+ puts value # => 'pass'
195
+ puts result.success? # => true
196
+ puts command.instance_var # => 'instance'
197
+ end
198
+ endpoint
199
+ ```
200
+
201
+ #### Statuses
202
+
203
+ You can pass a `status` flag to both the `pass!` and `flunk!` methods that allows the result matcher to process different kinds of successes and failures differently:
204
+
205
+ ```ruby
206
+ class StatusCommand < Slayer::Command
207
+ def call
208
+ flunk! message: "Generic flunk"
209
+ flunk! message: "Specific flunk", status: :specific_flunk
210
+ flunk! message: "Extra specific flunk", status: :extra_specific_flunk
211
+
212
+ pass! message: "Generic pass"
213
+ pass! message: "Specific pass", status: :specific_pass
214
+ end
215
+ end
216
+
217
+ StatusCommand.call do |m|
218
+ m.fail { puts "generic fail" }
219
+ m.fail(:specific_flunk) { puts "specific flunk" }
220
+ m.fail(:extra_specific_flunk) { puts "extra specific flunk" }
221
+
222
+ m.pass { puts "generic pass" }
223
+ m.pass(:specific_pass) { puts "specific pass" }
224
+ end
225
+ ```
226
+
227
+ ### Forms
228
+
229
+ ### Services
230
+
231
+ ## RSpec & Minitest Integrations
232
+
233
+ `Slayer` provides assertions and matchers that make testing your `Commands` simpler.
234
+
235
+ ### RSpec
236
+
237
+ To use with RSpec, update your `spec_helper.rb` file to include:
238
+
239
+ `require 'slayer/rspec'`
240
+
241
+ This provides you with two new matchers: `be_successful_result` and `be_failed_result`, both of which can be chained with a `with_status` expectation:
242
+
243
+ ```ruby
244
+ RSpec.describe RSpecCommand do
245
+ describe '#call' do
246
+ context 'should pass' do
247
+ subject(:result) { RSpecCommand.call(should_pass: true) }
248
+
249
+ it { is_expected.to be_success_result }
250
+ it { is_expected.not_to be_failed_result }
251
+ it { is_expected.not_to be_successful_result.with_status(:no_status) }
252
+ end
253
+
254
+ context 'should fail' do
255
+ subject(:result) { RSpecCommand.call(should_pass: false) }
256
+
257
+ it { is_expected.to be_failed_result }
258
+ it { is_expected.not_to be_failed_result }
259
+ it { is_expected.not_to be_failed_result.with_status(:no_status) }
260
+ end
261
+ end
262
+ end
263
+ ```
264
+
265
+ ### Minitest
266
+
47
267
  ## Rails Integration
48
268
 
49
269
  While Slayer is independent of any framework, we do offer a first-class integration with Ruby on Rails. To install the Rails extensions, add this line to your application's Gemfile:
@@ -121,58 +341,6 @@ $ bin/rails g slayer:command foo_command
121
341
  $ bin/rails g slayer:service foo_service
122
342
  ```
123
343
 
124
- ## Usage
125
-
126
- ### Commands
127
-
128
- Slayer Commands should implement `call`, which will `pass` or `fail` the service based on input. Commands return a `Slayer::Result` which has a predictable interface for determining `success?` or `failure?`, a 'value' payload object, a 'status' value, and a user presentable `message`.
129
-
130
- ```ruby
131
- # A Command that passes when given the string "foo"
132
- # and fails if given anything else.
133
- class FooCommand < Slayer::Command
134
- def call(foo:)
135
- if foo == "foo"
136
- pass! value: foo, message: "Passing FooCommand"
137
- else
138
- fail! value: foo, message: "Failing FooCommand"
139
- end
140
- end
141
- end
142
-
143
- result = FooCommand.call(foo: "foo")
144
- result.success? # => true
145
-
146
- result = FooCommand.call(foo: "bar")
147
- result.success? # => false
148
- ```
149
-
150
- ### Forms
151
-
152
- ### Services
153
-
154
- Slayer Services are objects that should implement re-usable pieces of application logic or common tasks. To prevent circular dependencies Services are required to declare which other Service classes they depend on. If a circular dependency is detected an error is raised.
155
-
156
- In order to enforce the lack of circular dependencies, Service objects can only call other Services that are declared in their dependencies.
157
-
158
- ```ruby
159
- class NetworkService < Slayer::Service
160
- def self.post()
161
- ...
162
- end
163
- end
164
-
165
- class StripeService < Slayer::Service
166
- dependencies NetworkService
167
-
168
- def self.pay()
169
- ...
170
- NetworkService.post(url: "stripe.com", body: my_payload)
171
- ...
172
- end
173
- end
174
- ```
175
-
176
344
  ## Development
177
345
 
178
346
  After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake test` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
@@ -185,7 +353,6 @@ To generate documentation run `yard`. To view undocumented files run `yard stats
185
353
 
186
354
  Bug reports and pull requests are welcome on GitHub at https://github.com/apsislabs/slayer.
187
355
 
188
-
189
356
  ## License
190
357
 
191
358
  The gem is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT).
data/Rakefile CHANGED
@@ -7,4 +7,4 @@ Rake::TestTask.new(:test) do |t|
7
7
  t.test_files = FileList['test/**/*_test.rb']
8
8
  end
9
9
 
10
- task :default => :test
10
+ task default: :test
@@ -2,6 +2,7 @@ require 'ext/string_ext' unless defined?(Rails)
2
2
 
3
3
  require 'slayer/version'
4
4
  require 'slayer/errors'
5
+ require 'slayer/hook'
5
6
  require 'slayer/result'
6
7
  require 'slayer/result_matcher'
7
8
  require 'slayer/service'
@@ -1,73 +1,18 @@
1
1
  module Slayer
2
- class Command
3
- attr_accessor :result
2
+ class Command < Service
3
+ singleton_skip_hook :call
4
4
 
5
- # Internal: Command Class Methods
6
5
  class << self
7
- def call(*args, &block)
8
- execute_call(block, *args) { |c, *a| c.run(*a) }
9
- end
10
-
11
- def call!(*args, &block)
12
- execute_call(block, *args) { |c, *a| c.run!(*a) }
6
+ def method_added(name)
7
+ return unless name == :call
8
+ super(name)
13
9
  end
14
10
 
15
- private
16
-
17
- def execute_call(command_block, *args)
18
- # Run the Command and capture the result
19
- command = self.new
20
- result = command.tap { yield(command, *args) }.result
21
-
22
- # Throw an exception if we don't return a result
23
- raise CommandNotImplementedError unless result.is_a? Result
24
-
25
- # Run the command block if one was provided
26
- unless command_block.nil?
27
- matcher = Slayer::ResultMatcher.new(result, command)
28
-
29
- command_block.call(matcher)
30
-
31
- # raise error if not all defaults were handled
32
- unless matcher.handled_defaults?
33
- raise(CommandResultNotHandledError, 'The pass or fail condition of a result was not handled')
34
- end
35
-
36
- begin
37
- matcher.execute_matching_block
38
- ensure
39
- matcher.execute_ensure_block
40
- end
41
- end
42
-
43
- return result
11
+ def call(*args, &block)
12
+ self.new.call(*args, &block)
44
13
  end
45
- end # << self
46
-
47
- def run(*args)
48
- call(*args)
49
- rescue CommandFailureError
50
- # Swallow the Command Failure
51
- end
52
-
53
- # Run the Command
54
- def run!(*args)
55
- call(*args)
56
- end
57
-
58
- # Fail the Command
59
-
60
- def fail!(value: nil, status: :default, message: nil)
61
- @result = Result.new(value, status, message)
62
- @result.fail!
63
- end
64
-
65
- # Pass the Command
66
- def pass!(value: nil, status: :default, message: nil)
67
- @result = Result.new(value, status, message)
68
14
  end
69
15
 
70
- # Call the command
71
16
  def call
72
17
  raise NotImplementedError, 'Commands must define method `#call`.'
73
18
  end
@@ -1,5 +1,5 @@
1
1
  module Slayer
2
- class CommandFailureError < StandardError
2
+ class ResultFailureError < StandardError
3
3
  attr_reader :result
4
4
 
5
5
  def initialize(result)
@@ -10,13 +10,12 @@ module Slayer
10
10
 
11
11
  class CommandNotImplementedError < StandardError
12
12
  def initialize(message = nil)
13
- message ||= 'Command implementation must call `fail!` or `pass!`, or '\
14
- 'return a <Slayer::Result> object'
13
+ message ||= 'Command implementation must return a <Slayer::Result> object'
15
14
  super message
16
15
  end
17
16
  end
18
17
 
19
- class CommandResultNotHandledError < StandardError; end
18
+ class ResultNotHandledError < StandardError; end
20
19
  class FormValidationError < StandardError; end
21
20
  class ServiceDependencyError < StandardError; end
22
21
  end
@@ -0,0 +1,154 @@
1
+ module Slayer
2
+ # Hook adds the ability to wrap all calls to a class in a wrapper method. The
3
+ # wrapper method is provided with a block that can be used to invoke the called
4
+ # method.
5
+ #
6
+ # The methods #skip_hook and #only_hook can be used to control which methods are
7
+ # and are not wrapped with the hook call.
8
+ #
9
+ # @example Including Hook on a class.
10
+ # class MyHookedClass
11
+ # include Hook
12
+ #
13
+ # hook :say_hello_and_goodbye
14
+ #
15
+ # def self.say_hello_and_goodbye
16
+ # puts "hello!"
17
+ #
18
+ # yield # calls hooked method
19
+ #
20
+ # puts "goodbye!"
21
+ # end
22
+ #
23
+ # def self.say_something
24
+ # puts "something"
25
+ # end
26
+ # end
27
+ #
28
+ # MyHookedClass.say_something
29
+ # # => "hello!"
30
+ # # "something"
31
+ # # "goodbye!"
32
+ #
33
+ # @example Skipping Hooks
34
+ #
35
+ # skip_hook :say_something, :do_something # the hook method will not be invoked for
36
+ # # these methods. They will be called directly.
37
+ #
38
+ # @example Only hooking
39
+ #
40
+ # only_hook :see_something, :hear_something # These are the only methods that will be
41
+ # # be hooked. All other methods will be
42
+ # # called directly.
43
+ module Hook
44
+ def self.included(klass)
45
+ klass.extend ClassMethods
46
+ end
47
+
48
+ # Everything in Hook::ClassMethods automatically get extended onto the
49
+ # class that includes Hook.
50
+ module ClassMethods
51
+ # Define the method that will be invoked whenever another method is invoked.
52
+ # This should be a class method.
53
+ def hook(hook_method)
54
+ @__hook = hook_method
55
+ end
56
+
57
+ # Define the set of methods that should always be called directly, and should
58
+ # never be hooked
59
+ def skip_hook(*hook_skips)
60
+ @__hook_skips = hook_skips
61
+ end
62
+
63
+ def singleton_skip_hook(*hook_skips)
64
+ @__singleton_hook_skips = hook_skips
65
+ end
66
+
67
+ # If only_hook is called then only the methods provided will be hooked. All
68
+ # other methods will be called directly.
69
+ def only_hook(*hook_only)
70
+ @__hook_only = hook_only
71
+ end
72
+
73
+ private
74
+
75
+ def __hook
76
+ @__hook ||= nil
77
+ end
78
+
79
+ def __hook_skips
80
+ @__hook_skips ||= []
81
+ end
82
+
83
+ def __singleton_hook_skips
84
+ @__singleton_hook_skips ||= []
85
+ end
86
+
87
+ def __hook_only
88
+ @__hook_only ||= nil
89
+ end
90
+
91
+ def __current_methods
92
+ @__current_methods ||= nil
93
+ end
94
+
95
+ def run_hook(name, instance, passed_block, &block)
96
+ if __hook
97
+ send(__hook, name, instance, passed_block, &block)
98
+ else
99
+ # rubocop:disable Performance/RedundantBlockCall
100
+ block.call
101
+ # rubocop:enable Performance/RedundantBlockCall
102
+ end
103
+ end
104
+
105
+ def singleton_method_added(name)
106
+ insert_hook_for(name,
107
+ define_method_fn: :define_singleton_method,
108
+ hook_target: self,
109
+ alias_target: singleton_class,
110
+ skip_methods: blacklist(__singleton_hook_skips))
111
+ end
112
+
113
+ def method_added(name)
114
+ insert_hook_for(name,
115
+ define_method_fn: :define_method,
116
+ hook_target: self,
117
+ alias_target: self,
118
+ skip_methods: blacklist(__hook_skips))
119
+ end
120
+
121
+ def insert_hook_for(name, define_method_fn:, hook_target:, alias_target:, skip_methods: [])
122
+ return if __current_methods && __current_methods.include?(name)
123
+
124
+ with_hooks = :"__#{name}_with_hooks"
125
+ without_hooks = :"__#{name}_without_hooks"
126
+
127
+ return if __hook_only && !__hook_only.include?(name.to_sym)
128
+ return if skip_methods.include? name.to_sym
129
+
130
+ @__current_methods = [name, with_hooks, without_hooks]
131
+ send(define_method_fn, with_hooks) do |*args, &block|
132
+ hook_target.send(:run_hook, name, self, block) do
133
+ send(without_hooks, *args)
134
+ end
135
+ end
136
+
137
+ alias_target.send(:alias_method, without_hooks, name)
138
+ alias_target.send(:alias_method, name, with_hooks)
139
+
140
+ @__current_methods = nil
141
+ end
142
+
143
+ def blacklist(additional = [])
144
+ list = Object.methods(false) + Object.private_methods(false)
145
+ list += Slayer::Hook::ClassMethods.private_instance_methods(false)
146
+ list += Slayer::Hook::ClassMethods.instance_methods(false)
147
+ list += additional
148
+ list << __hook if __hook
149
+
150
+ list
151
+ end
152
+ end
153
+ end
154
+ end
@@ -13,12 +13,12 @@ module Slayer
13
13
  end
14
14
 
15
15
  def failure?
16
- @failure || false
16
+ @failure ||= false
17
17
  end
18
18
 
19
- def fail!
20
- @failure = true
21
- raise CommandFailureError, self
19
+ def fail
20
+ @failure ||= true
21
+ self
22
22
  end
23
23
  end
24
24
  end
@@ -30,7 +30,7 @@ module Slayer
30
30
  #
31
31
  # If the block form of a {Command.call} is invoked, both the block must handle the default
32
32
  # status for both a {Result#success?} and a {Result#failure?}. If both are not handled,
33
- # the matching block will not be invoked and a {CommandResultNotHandledError} will be
33
+ # the matching block will not be invoked and a {ResultNotHandledError} will be
34
34
  # raised.
35
35
  #
36
36
  # @example Matcher invokes the matching pass block, with precedence given to {#pass} and {#fail}
@@ -74,7 +74,7 @@ module Slayer
74
74
  # m.pass(:ok) { puts "Pass, OK!"}
75
75
  # m.fail { puts "Fail!" }
76
76
  # end
77
- # # => raises CommandResultNotHandledError (because no default pass was provided)
77
+ # # => raises ResultNotHandledError (because no default pass was provided)
78
78
  #
79
79
  # # Call produces a successful Result with status :ok
80
80
  # SuccessCommand.call do |m|
@@ -94,7 +94,7 @@ module Slayer
94
94
  # SuccessCommand.call do |m|
95
95
  # m.pass(:ok, :default) { puts "Pass, OK!"}
96
96
  # end
97
- # # => raises CommandResultNotHandledError (because no default fail was provided)
97
+ # # => raises ResultNotHandledError (because no default fail was provided)
98
98
  class ResultMatcher
99
99
  attr_reader :result, :command
100
100
 
@@ -0,0 +1,42 @@
1
+ require 'rspec/expectations'
2
+
3
+ RSpec::Matchers.define :be_success_result do
4
+ match do |result|
5
+ result.success?
6
+ end
7
+
8
+ chain :with_status do |status|
9
+ @status = status
10
+ end
11
+
12
+ failure_message do |result|
13
+ return 'expected command to succeed' if @status.nil?
14
+ return "expected command to succeed with status :#{@status}, but got :#{result.status}"
15
+ end
16
+
17
+ failure_message_when_negated do |result|
18
+ return "expected command not to have status :#{@status}" if @status.present? && result.status == @status
19
+ return 'expected command to fail'
20
+ end
21
+ end
22
+
23
+ RSpec::Matchers.define :be_failed_result do
24
+ match do |result|
25
+ return result.failure? if @status.nil?
26
+ return result.failure? && (result.status == @status)
27
+ end
28
+
29
+ chain :with_status do |status|
30
+ @status = status
31
+ end
32
+
33
+ failure_message do |result|
34
+ return 'expected command to fail' if @status.nil?
35
+ return "expected command to fail with status :#{@status}, but got :#{result.status}"
36
+ end
37
+
38
+ failure_message_when_negated do |result|
39
+ return "expected command not to have status :#{@status}" if @status.present? && result.status == @status
40
+ return 'expected command to succeed'
41
+ end
42
+ end
@@ -1,182 +1,98 @@
1
1
  module Slayer
2
2
  # Slayer Services are objects that should implement re-usable pieces of
3
- # application logic or common tasks. To prevent circular dependencies Services
4
- # are required to declare which other Service classes they depend on. If a
5
- # circular dependency is detected an error is raised.
6
- #
7
- # In order to enforce the lack of circular dependencies, Service objects can
8
- # only call other Services that are declared in their dependencies.
3
+ # application logic or common tasks. All methods in a service are wrapped
4
+ # by default to enforce the return of a +Slayer::Result+ object.
9
5
  class Service
10
- # List the other Service class that this service class depends on. Only
11
- # dependencies that are included in this call my be invoked from class
12
- # or instances methods of this service class.
13
- #
14
- # If no dependencies are provided, no other Service classes may be used by
15
- # this Service class.
16
- #
17
- # @param deps [Array<Class>] An array of the other Slayer::Service classes that are used as dependencies
18
- #
19
- # @example Service calls with dependency declared
20
- # class StripeService < Slayer::Service
21
- # dependencies NetworkService
22
- #
23
- # def self.pay()
24
- # ...
25
- # NetworkService.post(url: "stripe.com", body: my_payload) # OK
26
- # ...
27
- # end
28
- # end
29
- #
30
- # @example Service calls without a dependency declared
31
- # class JiraApiService < Slayer::Service
32
- #
33
- # def self.create_issue()
34
- # ...
35
- # NetworkService.post(url: "stripe.com", body: my_payload) # Raises Slayer::ServiceDependencyError
36
- # ...
37
- # end
38
- # end
39
- #
40
- # @return [Array<Class>] The transitive closure of dependencies for this object.
41
- def self.dependencies(*deps)
42
- raise(ServiceDependencyError, "There were multiple dependencies calls of #{self}") if @deps
43
-
44
- deps.each do |dep|
45
- unless dep.is_a?(Class)
46
- raise(ServiceDependencyError, "The object #{dep} passed to dependencies service was not a class")
47
- end
48
-
49
- unless dep < Slayer::Service
50
- raise(ServiceDependencyError, "The object #{dep} passed to dependencies was not a subclass of #{self}")
51
- end
52
- end
6
+ include Hook
53
7
 
54
- unless deps.uniq.length == deps.length
55
- raise(ServiceDependencyError, "There were duplicate dependencies in #{self}")
56
- end
57
-
58
- @deps = deps
8
+ skip_hook :pass, :flunk, :flunk!, :try!
9
+ singleton_skip_hook :pass, :flunk, :flunk!, :try!
59
10
 
60
- # Calculate the transitive dependencies and raise an error if there are circular dependencies
61
- transitive_dependencies
62
- end
11
+ attr_accessor :result
63
12
 
64
13
  class << self
14
+ # Create a passing Result
15
+ def pass(value: nil, status: :default, message: nil)
16
+ Result.new(value, status, message)
17
+ end
65
18
 
66
- attr_reader :deps
67
-
68
- def transitive_dependencies(dependency_hash = {}, visited = [])
69
- return @transitive_dependencies if @transitive_dependencies
70
-
71
- @deps ||= []
19
+ # Create a failing Result
20
+ def flunk(value: nil, status: :default, message: nil)
21
+ Result.new(value, status, message).fail
22
+ end
72
23
 
73
- # If we've already visited ourself, bail out. This is necessary to halt
74
- # execution for a circular chain of dependencies. #halting-problem-solved
75
- return dependency_hash[self] if visited.include?(self)
24
+ # Create a failing Result and halt execution of the Command
25
+ def flunk!(value: nil, status: :default, message: nil)
26
+ raise ResultFailureError, flunk(value: value, status: status, message: message)
27
+ end
76
28
 
77
- visited << self
78
- dependency_hash[self] ||= []
29
+ # If the block produces a successful result the value of the result will be
30
+ # returned. Otherwise, this will create a failing result and halt the execution
31
+ # of the Command.
32
+ def try!(value: nil, status: nil, message: nil)
33
+ r = yield
34
+ flunk!(value: value, status: status || :default, message: message) unless r.is_a?(Result)
35
+ return r.value if r.success?
36
+ flunk!(value: value || r.value, status: status || r.status, message: message || r.message)
37
+ end
38
+ end
79
39
 
80
- # Add each of our dependencies (and it's transitive dependency chain) to our
81
- # own dependencies.
40
+ def pass(*args)
41
+ self.class.pass(*args)
42
+ end
82
43
 
83
- @deps.each do |dep|
84
- dependency_hash[self] << dep
44
+ def flunk(*args)
45
+ self.class.flunk(*args)
46
+ end
85
47
 
86
- unless visited.include?(dep)
87
- child_transitive_dependencies = dep.transitive_dependencies(dependency_hash, visited)
88
- dependency_hash[self].concat(child_transitive_dependencies)
89
- end
48
+ def flunk!(*args)
49
+ self.class.flunk!(*args)
50
+ end
90
51
 
91
- dependency_hash[self].uniq
92
- end
52
+ def try!(*args, &block)
53
+ self.class.try!(*args, &block)
54
+ end
93
55
 
94
- # NO CIRCULAR DEPENDENCIES!
95
- if dependency_hash[self].include? self
96
- raise(ServiceDependencyError, "#{self} had a circular dependency")
97
- end
56
+ # Make sure child classes also hook correctly
57
+ def self.inherited(klass)
58
+ klass.include Hook
59
+ klass.hook :__service_hook
60
+ end
98
61
 
99
- # Store these now, so next time we can short-circuit.
100
- @transitive_dependencies = dependency_hash[self]
62
+ hook :__service_hook
101
63
 
102
- return @transitive_dependencies
64
+ # rubocop:disable Metrics/MethodLength
65
+ def self.__service_hook(_, instance, service_block)
66
+ begin
67
+ result = yield
68
+ rescue ResultFailureError => error
69
+ result = error.result
103
70
  end
104
71
 
105
- def before_each_method(*)
106
- @deps ||= []
107
- @@allowed_services ||= nil
72
+ raise CommandNotImplementedError unless result.is_a? Result
108
73
 
109
- # Confirm that this method call is allowed
110
- raise_if_not_allowed
74
+ unless service_block.nil?
75
+ matcher = Slayer::ResultMatcher.new(result, instance)
111
76
 
112
- @@allowed_services ||= []
113
- @@allowed_services << (@deps + [self])
114
- end
77
+ service_block.call(matcher)
115
78
 
116
- def raise_if_not_allowed
117
- if @@allowed_services
118
- allowed = @@allowed_services.last
119
- if !allowed || !allowed.include?(self)
120
- raise(ServiceDependencyError, "Attempted to call #{self} from another #{Slayer::Service}"\
121
- ' which did not declare it as a dependency')
122
- end
79
+ # raise error if not all defaults were handled
80
+ unless matcher.handled_defaults?
81
+ raise(ResultNotHandledError, 'The pass or fail condition of a result was not handled')
123
82
  end
124
- end
125
83
 
126
- def after_each_method(*)
127
- @@allowed_services.pop
128
- @@allowed_services = nil if @@allowed_services.empty?
129
- end
130
-
131
- def singleton_method_added(name)
132
- return if self == Slayer::Service
133
- return if @__last_methods_added && @__last_methods_added.include?(name)
134
-
135
- with = :"#{name}_with_before_each_method"
136
- without = :"#{name}_without_before_each_method"
137
-
138
- @__last_methods_added = [name, with, without]
139
- define_singleton_method with do |*args, &block|
140
- before_each_method name
141
- begin
142
- send without, *args, &block
143
- rescue
144
- raise
145
- ensure
146
- after_each_method name
147
- end
84
+ begin
85
+ matcher.execute_matching_block
86
+ ensure
87
+ matcher.execute_ensure_block
148
88
  end
149
-
150
- singleton_class.send(:alias_method, without, name)
151
- singleton_class.send(:alias_method, name, with)
152
-
153
- @__last_methods_added = nil
154
89
  end
90
+ return result
91
+ end
92
+ # rubocop:enable Metrics/MethodLength
155
93
 
156
- def method_added(name)
157
- return if self == Slayer::Service
158
- return if @__last_methods_added && @__last_methods_added.include?(name)
159
-
160
- with = :"#{name}_with_before_each_method"
161
- without = :"#{name}_without_before_each_method"
162
-
163
- @__last_methods_added = [name, with, without]
164
- define_method with do |*args, &block|
165
- self.class.before_each_method name
166
- begin
167
- send without, *args, &block
168
- rescue
169
- raise
170
- ensure
171
- self.class.after_each_method name
172
- end
173
- end
174
-
175
- alias_method without, name
176
- alias_method name, with
94
+ private_class_method :inherited
95
+ private_class_method :__service_hook
177
96
 
178
- @__last_methods_added = nil
179
- end
180
- end # << self
181
97
  end # class Service
182
98
  end # module Slayer
@@ -1,3 +1,3 @@
1
1
  module Slayer
2
- VERSION = '0.3.1'
2
+ VERSION = '0.4.0.beta2'
3
3
  end
@@ -1,4 +1,4 @@
1
- # coding: utf-8
1
+
2
2
  lib = File.expand_path('../lib', __FILE__)
3
3
  $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
4
  require 'slayer/version'
@@ -9,7 +9,7 @@ Gem::Specification.new do |spec|
9
9
  spec.authors = ['Wyatt Kirby', 'Noah Callaway']
10
10
  spec.email = ['wyatt@apsis.io', 'noah@apsis.io']
11
11
 
12
- spec.summary = %q{A killer service layer}
12
+ spec.summary = 'A killer service layer'
13
13
  spec.homepage = 'http://www.apsis.io'
14
14
  spec.license = 'MIT'
15
15
 
@@ -19,16 +19,15 @@ Gem::Specification.new do |spec|
19
19
  spec.require_paths = ['lib']
20
20
 
21
21
  spec.add_dependency 'virtus', '~> 1.0'
22
- spec.add_dependency 'dry-validation', '~> 0.10'
23
22
 
24
- spec.add_development_dependency 'coveralls'
25
- spec.add_development_dependency 'simplecov', '~> 0.13'
26
23
  spec.add_development_dependency 'bundler', '~> 1.12'
27
- spec.add_development_dependency 'rake', '~> 10.0'
24
+ spec.add_development_dependency 'byebug', '~> 9.0'
25
+ spec.add_development_dependency 'coveralls'
28
26
  spec.add_development_dependency 'minitest', '~> 5.0'
29
27
  spec.add_development_dependency 'minitest-reporters', '~> 1.1'
30
28
  spec.add_development_dependency 'mocha', '~> 1.2'
31
- spec.add_development_dependency 'byebug', '~> 9.0'
29
+ spec.add_development_dependency 'rubocop', '~> 0.48.1'
30
+ spec.add_development_dependency 'rake', '~> 10.0'
31
+ spec.add_development_dependency 'simplecov', '~> 0.13'
32
32
  spec.add_development_dependency 'yard', '~> 0.9'
33
- spec.add_development_dependency 'rubocop', '~> 0.47.1'
34
33
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: slayer
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.3.1
4
+ version: 0.4.0.beta2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Wyatt Kirby
@@ -9,7 +9,7 @@ authors:
9
9
  autorequire:
10
10
  bindir: exe
11
11
  cert_chain: []
12
- date: 2017-03-10 00:00:00.000000000 Z
12
+ date: 2018-05-14 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: virtus
@@ -26,131 +26,131 @@ dependencies:
26
26
  - !ruby/object:Gem::Version
27
27
  version: '1.0'
28
28
  - !ruby/object:Gem::Dependency
29
- name: dry-validation
29
+ name: bundler
30
30
  requirement: !ruby/object:Gem::Requirement
31
31
  requirements:
32
32
  - - "~>"
33
33
  - !ruby/object:Gem::Version
34
- version: '0.10'
35
- type: :runtime
34
+ version: '1.12'
35
+ type: :development
36
36
  prerelease: false
37
37
  version_requirements: !ruby/object:Gem::Requirement
38
38
  requirements:
39
39
  - - "~>"
40
40
  - !ruby/object:Gem::Version
41
- version: '0.10'
41
+ version: '1.12'
42
42
  - !ruby/object:Gem::Dependency
43
- name: coveralls
43
+ name: byebug
44
44
  requirement: !ruby/object:Gem::Requirement
45
45
  requirements:
46
- - - ">="
46
+ - - "~>"
47
47
  - !ruby/object:Gem::Version
48
- version: '0'
48
+ version: '9.0'
49
49
  type: :development
50
50
  prerelease: false
51
51
  version_requirements: !ruby/object:Gem::Requirement
52
52
  requirements:
53
- - - ">="
53
+ - - "~>"
54
54
  - !ruby/object:Gem::Version
55
- version: '0'
55
+ version: '9.0'
56
56
  - !ruby/object:Gem::Dependency
57
- name: simplecov
57
+ name: coveralls
58
58
  requirement: !ruby/object:Gem::Requirement
59
59
  requirements:
60
- - - "~>"
60
+ - - ">="
61
61
  - !ruby/object:Gem::Version
62
- version: '0.13'
62
+ version: '0'
63
63
  type: :development
64
64
  prerelease: false
65
65
  version_requirements: !ruby/object:Gem::Requirement
66
66
  requirements:
67
- - - "~>"
67
+ - - ">="
68
68
  - !ruby/object:Gem::Version
69
- version: '0.13'
69
+ version: '0'
70
70
  - !ruby/object:Gem::Dependency
71
- name: bundler
71
+ name: minitest
72
72
  requirement: !ruby/object:Gem::Requirement
73
73
  requirements:
74
74
  - - "~>"
75
75
  - !ruby/object:Gem::Version
76
- version: '1.12'
76
+ version: '5.0'
77
77
  type: :development
78
78
  prerelease: false
79
79
  version_requirements: !ruby/object:Gem::Requirement
80
80
  requirements:
81
81
  - - "~>"
82
82
  - !ruby/object:Gem::Version
83
- version: '1.12'
83
+ version: '5.0'
84
84
  - !ruby/object:Gem::Dependency
85
- name: rake
85
+ name: minitest-reporters
86
86
  requirement: !ruby/object:Gem::Requirement
87
87
  requirements:
88
88
  - - "~>"
89
89
  - !ruby/object:Gem::Version
90
- version: '10.0'
90
+ version: '1.1'
91
91
  type: :development
92
92
  prerelease: false
93
93
  version_requirements: !ruby/object:Gem::Requirement
94
94
  requirements:
95
95
  - - "~>"
96
96
  - !ruby/object:Gem::Version
97
- version: '10.0'
97
+ version: '1.1'
98
98
  - !ruby/object:Gem::Dependency
99
- name: minitest
99
+ name: mocha
100
100
  requirement: !ruby/object:Gem::Requirement
101
101
  requirements:
102
102
  - - "~>"
103
103
  - !ruby/object:Gem::Version
104
- version: '5.0'
104
+ version: '1.2'
105
105
  type: :development
106
106
  prerelease: false
107
107
  version_requirements: !ruby/object:Gem::Requirement
108
108
  requirements:
109
109
  - - "~>"
110
110
  - !ruby/object:Gem::Version
111
- version: '5.0'
111
+ version: '1.2'
112
112
  - !ruby/object:Gem::Dependency
113
- name: minitest-reporters
113
+ name: rubocop
114
114
  requirement: !ruby/object:Gem::Requirement
115
115
  requirements:
116
116
  - - "~>"
117
117
  - !ruby/object:Gem::Version
118
- version: '1.1'
118
+ version: 0.48.1
119
119
  type: :development
120
120
  prerelease: false
121
121
  version_requirements: !ruby/object:Gem::Requirement
122
122
  requirements:
123
123
  - - "~>"
124
124
  - !ruby/object:Gem::Version
125
- version: '1.1'
125
+ version: 0.48.1
126
126
  - !ruby/object:Gem::Dependency
127
- name: mocha
127
+ name: rake
128
128
  requirement: !ruby/object:Gem::Requirement
129
129
  requirements:
130
130
  - - "~>"
131
131
  - !ruby/object:Gem::Version
132
- version: '1.2'
132
+ version: '10.0'
133
133
  type: :development
134
134
  prerelease: false
135
135
  version_requirements: !ruby/object:Gem::Requirement
136
136
  requirements:
137
137
  - - "~>"
138
138
  - !ruby/object:Gem::Version
139
- version: '1.2'
139
+ version: '10.0'
140
140
  - !ruby/object:Gem::Dependency
141
- name: byebug
141
+ name: simplecov
142
142
  requirement: !ruby/object:Gem::Requirement
143
143
  requirements:
144
144
  - - "~>"
145
145
  - !ruby/object:Gem::Version
146
- version: '9.0'
146
+ version: '0.13'
147
147
  type: :development
148
148
  prerelease: false
149
149
  version_requirements: !ruby/object:Gem::Requirement
150
150
  requirements:
151
151
  - - "~>"
152
152
  - !ruby/object:Gem::Version
153
- version: '9.0'
153
+ version: '0.13'
154
154
  - !ruby/object:Gem::Dependency
155
155
  name: yard
156
156
  requirement: !ruby/object:Gem::Requirement
@@ -165,20 +165,6 @@ dependencies:
165
165
  - - "~>"
166
166
  - !ruby/object:Gem::Version
167
167
  version: '0.9'
168
- - !ruby/object:Gem::Dependency
169
- name: rubocop
170
- requirement: !ruby/object:Gem::Requirement
171
- requirements:
172
- - - "~>"
173
- - !ruby/object:Gem::Version
174
- version: 0.47.1
175
- type: :development
176
- prerelease: false
177
- version_requirements: !ruby/object:Gem::Requirement
178
- requirements:
179
- - - "~>"
180
- - !ruby/object:Gem::Version
181
- version: 0.47.1
182
168
  description:
183
169
  email:
184
170
  - wyatt@apsis.io
@@ -205,8 +191,10 @@ files:
205
191
  - lib/slayer/command.rb
206
192
  - lib/slayer/errors.rb
207
193
  - lib/slayer/form.rb
194
+ - lib/slayer/hook.rb
208
195
  - lib/slayer/result.rb
209
196
  - lib/slayer/result_matcher.rb
197
+ - lib/slayer/rspec.rb
210
198
  - lib/slayer/service.rb
211
199
  - lib/slayer/version.rb
212
200
  - slayer.gemspec
@@ -226,12 +214,12 @@ required_ruby_version: !ruby/object:Gem::Requirement
226
214
  version: '0'
227
215
  required_rubygems_version: !ruby/object:Gem::Requirement
228
216
  requirements:
229
- - - ">="
217
+ - - ">"
230
218
  - !ruby/object:Gem::Version
231
- version: '0'
219
+ version: 1.3.1
232
220
  requirements: []
233
221
  rubyforge_project:
234
- rubygems_version: 2.6.8
222
+ rubygems_version: 2.6.13
235
223
  signing_key:
236
224
  specification_version: 4
237
225
  summary: A killer service layer