Skip to content

Commit

Permalink
Merge ed37ced into ac8ae59
Browse files Browse the repository at this point in the history
  • Loading branch information
wkirby committed May 14, 2018
2 parents ac8ae59 + ed37ced commit 7978787
Show file tree
Hide file tree
Showing 30 changed files with 730 additions and 441 deletions.
4 changes: 3 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -9,4 +9,6 @@
/spec/reports/
/tmp/
/log*/*
.byebug_history
.byebug_history

.DS_Store
243 changes: 187 additions & 56 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,13 +18,15 @@ Slayer provides 3 base classes for organizing your business logic: `Forms`, `Com

### Commands

`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.
`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.

`Commands` should call `Services`, but `Services` should never call `Commands`.
`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.

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.

### Services

`Services` are the building blocks of `Commands`, and encapsulate re-usable chunks of application logic.
`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.

## Installation

Expand All @@ -46,6 +48,188 @@ Or install it yourself as:
$ gem install slayer
```

## Usage

### Commands

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`.

```ruby
# A Command that passes when given the string "foo"
# and fails if given anything else.
class FooCommand < Slayer::Command
def call(foo:)
unless foo == "foo"
flunk! value: foo, message: "Argument must be foo!"
end

pass! value: foo
end
end
```

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:

```ruby
FooCommand.call(foo: "foo") do |m|
m.pass do |result|
puts "This code runs on success"
end

m.fail do |result|
puts "This code runs on failure. Message: #{result.message}"
end

m.all do
puts "This code runs on failure or success"
end

m.ensure do
puts "This code always runs after other handler blocks"
end
end
```

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:

```ruby
result = FooCommand.call(foo: "foo")
puts result.success? # => true

result = FooCommand.call(foo: "bar")
puts result.success? # => false
```

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.

```ruby
# commands/user_controller.rb
class CreateUserCommand < Slayer::Command
def call(create_user_form:)
unless arguments_valid?(create_user_form)
flunk! value: create_user_form, status: :arguments_invalid
end

user = nil
transaction do
user = User.create(create_user_form.attributes)
end

unless user.persisted?
flunk! message: I18n.t('user.create.error'), status: :unprocessible_entity
end

pass! value: user
end

def arguments_valid?(create_user_form)
create_user_form.kind_of?(CreateUserForm) &&
create_user_form.valid? &&
!User.exists?(email: create_user_form.email)
end
end

# controllers/user_controller.rb
class UsersController < ApplicationController
def create
@create_user_form = CreateUserForm.from_params(create_user_params)

CreateUserCommand.call(create_user_form: @create_user_form) do |m|
m.pass do |user|
auto_login(user)
redirect_to root_path, notice: t('user.create.success')
end

m.fail(:arguments_invalid) do |result|
flash[:error] = result.errors.full_messages.to_sentence
render :new, status: :unprocessible_entity
end

m.fail do |result|
flash[:error] = t('user.create.error')
render :new, status: :bad_request
end
end
end

private

def required_user_params
[:first_name, :last_name, :email, :password]
end

def create_user_params
permitted_params = required_user_params << :password_confirmation
params.require(:user).permit(permitted_params)
end
end
```

### Result Matcher

The result matcher is an object that is used to handle `Slayer::Result` objects based on their status.

#### Handlers: `pass`, `fail`, `all`, `ensure`

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.

* The `pass` block runs if the command was successful.
* The `fail` block runs if the command was `flunked`.
* The `all` block runs on any type of result --- `pass` or `fail` --- unless the result has already been handled.
* The `ensure` block always runs after the result has been handled.

#### Handler Params

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:

```ruby
class NoArgCommand < Slayer::Command
def call
@instance_var = 'instance'
pass value: 'pass'
end
end


NoArgCommand.call do |m|
m.all do |value, result, command|
puts value # => 'pass'
puts result.success? # => true
puts command.instance_var # => 'instance'
end
endpoint
```

#### Statuses

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:

```ruby
class StatusCommand < Slayer::Command
def call
flunk! message: "Generic flunk"
flunk! message: "Specific flunk", status: :specific_flunk
flunk! message: "Extra specific flunk", status: :extra_specific_flunk

pass! message: "Generic pass"
pass! message: "Specific pass", status: :specific_pass
end
end

StatusCommand.call do |m|
m.fail { puts "generic fail" }
m.fail(:specific_flunk) { puts "specific flunk" }
m.fail(:extra_specific_flunk) { puts "extra specific flunk" }

m.pass { puts "generic pass" }
m.pass(:specific_pass) { puts "specific pass" }
end
```

### Forms

### Services

## Rails Integration

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:
Expand Down Expand Up @@ -123,58 +307,6 @@ $ bin/rails g slayer:command foo_command
$ bin/rails g slayer:service foo_service
```

## Usage

### Commands

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`.

```ruby
# A Command that passes when given the string "foo"
# and fails if given anything else.
class FooCommand < Slayer::Command
def call(foo:)
if foo == "foo"
pass! value: foo, message: "Passing FooCommand"
else
fail! value: foo, message: "Failing FooCommand"
end
end
end

result = FooCommand.call(foo: "foo")
result.success? # => true

result = FooCommand.call(foo: "bar")
result.success? # => false
```

### Forms

### Services

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.

In order to enforce the lack of circular dependencies, Service objects can only call other Services that are declared in their dependencies.

```ruby
class NetworkService < Slayer::Service
def self.post()
...
end
end

class StripeService < Slayer::Service
dependencies NetworkService

def self.pay()
...
NetworkService.post(url: "stripe.com", body: my_payload)
...
end
end
```

## Development

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.
Expand All @@ -187,7 +319,6 @@ To generate documentation run `yard`. To view undocumented files run `yard stats

Bug reports and pull requests are welcome on GitHub at https://github.com/apsislabs/slayer.


## License

The gem is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT).
2 changes: 1 addition & 1 deletion Rakefile
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,4 @@ Rake::TestTask.new(:test) do |t|
t.test_files = FileList['test/**/*_test.rb']
end

task :default => :test
task default: :test
1 change: 1 addition & 0 deletions lib/slayer.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

require 'slayer/version'
require 'slayer/errors'
require 'slayer/hook'
require 'slayer/result'
require 'slayer/result_matcher'
require 'slayer/service'
Expand Down
69 changes: 7 additions & 62 deletions lib/slayer/command.rb
Original file line number Diff line number Diff line change
@@ -1,73 +1,18 @@
module Slayer
class Command
attr_accessor :result
class Command < Service
singleton_skip_hook :call

# Internal: Command Class Methods
class << self
def call(*args, &block)
execute_call(block, *args) { |c, *a| c.run(*a) }
end

def call!(*args, &block)
execute_call(block, *args) { |c, *a| c.run!(*a) }
def method_added(name)
return unless name == :call
super(name)
end

private

def execute_call(command_block, *args)
# Run the Command and capture the result
command = self.new
result = command.tap { yield(command, *args) }.result

# Throw an exception if we don't return a result
raise CommandNotImplementedError unless result.is_a? Result

# Run the command block if one was provided
unless command_block.nil?
matcher = Slayer::ResultMatcher.new(result, command)

command_block.call(matcher)

# raise error if not all defaults were handled
unless matcher.handled_defaults?
raise(CommandResultNotHandledError, 'The pass or fail condition of a result was not handled')
end

begin
matcher.execute_matching_block
ensure
matcher.execute_ensure_block
end
end

return result
def call(*args, &block)
self.new.call(*args, &block)
end
end # << self

def run(*args)
call(*args)
rescue CommandFailureError
# Swallow the Command Failure
end

# Run the Command
def run!(*args)
call(*args)
end

# Fail the Command

def fail!(value: nil, status: :default, message: nil)
@result = Result.new(value, status, message)
@result.fail!
end

# Pass the Command
def pass!(value: nil, status: :default, message: nil)
@result = Result.new(value, status, message)
end

# Call the command
def call
raise NotImplementedError, 'Commands must define method `#call`.'
end
Expand Down
Loading

0 comments on commit 7978787

Please sign in to comment.