Slayer is intended to operate as a minimal service layer for your ruby application. To achieve this, Slayer provides base classes for business logic.
Slayer is still under development, and not yet ready for production use. We are targetting a stable API with the 0.4.0 launch, so expect breaking changes until then.
Slayer provides 2 base classes for organizing your business logic: Forms and Commands. These each have a distinct role in your application's structure.
Slayer::Forms are objects for wrapping a set of data, usually to be passed as a parameter to a Command or Service.
Slayer::Commands are the bread and butter of your application's business logic. Commands wrap logic into easily tested, isolated, composable classes. In our applications, we usually create a single Command per Controller endpoint.
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.
Add this line to your application's Gemfile:
gem'slayer'And then execute:
$ bundleOr install it yourself as:
$ gem install slayerSlayer 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 passed? or failed?, a 'value' payload object, a 'status' value, and a user presentable message.
# A Command that passes when given the string "foo"# and fails if given anything else.classFooCommand < Slayer::Commanddefcall(foo:)unlessfoo == "foo"returnerrvalue: foo,message: "Argument must be foo!"endokvalue: fooendendHandling 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:
FooCommand.call(foo: "foo")do |m|
m.okdo |value|
puts"This code runs on success"endm.errdo |_value,result|
puts"This code runs on failure. Message: #{result.message}"endm.alldoputs"This code runs on failure or success"endm.ensuredoputs"This code always runs after other handler blocks"endendThe 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:
result=FooCommand.call(foo: "foo")putsresult.ok?# => trueresult=FooCommand.call(foo: "bar")putsresult.ok?# => falseHere'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.
# commands/user_controller.rbclassCreateUserCommand < Slayer::Commanddefcall(create_user_form:)unlessarguments_valid?(create_user_form)returnerrvalue: create_user_form,status: :arguments_invalidenduser=niltransactiondouser=User.create(create_user_form.attributes)endunlessuser.persisted?returnerrmessage: I18n.t('user.create.error'),status: :unprocessible_entityendokvalue: userenddefarguments_valid?(create_user_form)create_user_form.kind_of?(CreateUserForm) &&
create_user_form.valid? &&
!User.exists?(email: create_user_form.email)endend# controllers/user_controller.rbclassUsersController < ApplicationControllerdefcreate@create_user_form=CreateUserForm.from_params(create_user_params)CreateUserCommand.call(create_user_form: @create_user_form)do |m|
m.okdo |user|
auto_login(user)redirect_toroot_path,notice: t('user.create.success')endm.err(:arguments_invalid)do |_user,result|
flash[:error]=result.errors.full_messages.to_sentencerender:new,status: :unprocessible_entityendm.errdo |_user,result|
flash[:error]=t('user.create.error')render:new,status: :bad_requestendendendprivatedefrequired_user_params[:first_name,:last_name,:email,:password]enddefcreate_user_paramspermitted_params=required_user_params << :password_confirmationparams.require(:user).permit(permitted_params)endendThe result matcher is an object that is used to handle Slayer::Result objects based on their status.
The result matcher block can take 4 types of handler blocks: ok, err, all, and ensure. They operate as you would expect based on their names.
- The
okblock runs if the command was successful. - The
errblock runs if the command waskoed. - The
allblock runs on any type of result ---okorerr--- unless the result has already been handled. - The
ensureblock always runs after the result has been handled.
Every handler in the result matcher block is given three arguments: value, result, and command. These encapsulate the value provided in the ok or return err call from the Command, the returned Slayer::Result object, and the Slayer::Command instance that was just run:
classNoArgCommand < Slayer::Commanddefcall@instance_var='instance'okvalue: 'pass'endendNoArgCommand.calldo |m|
m.alldo |value,result,command|
putsvalue# => 'pass'putsresult.ok?# => trueputscommand.instance_var# => 'instance'endendYou can pass a status flag to both the ok and return err methods that allows the result matcher to process different kinds of successes and failures differently:
classStatusCommand < Slayer::Commanddefcallreturnerrmessage: "Extra specific ko",status: :extra_specific_errifextra_specific_err?returnerrmessage: "Specific ko",status: :specific_errifspecific_err?returnerrmessage: "Generic ko"ifgeneric_err?returnokmessage: "Specific pass",status: :specific_passifspecific_pass?okmessage: "Generic pass"endendStatusCommand.calldo |m|
m.err{puts"generic err"}m.err(:specific_err){puts"specific err"}m.err(:extra_specific_err){puts"extra specific err"}m.ok{puts"generic pass"}m.ok(:specific_pass){puts"specific pass"}endSlayer provides assertions and matchers that make testing your Commands simpler.
To use with RSpec, update your spec_helper.rb file to include:
require 'slayer/rspec'
This provides you with two new matchers: be_successful_result and be_failed_result, both of which can be chained with a with_status, with_message, or with_value expectations:
RSpec.describeRSpecCommanddodescribe'#call'docontext'should pass'dosubject(:result){RSpecCommand.call(should_pass: true)}it{is_expected.tobe_success_result}it{is_expected.not_tobe_failed_result}it{is_expected.tobe_success_result.with_status(:no_status)}it{is_expected.tobe_success_result.with_message("message")}it{is_expected.tobe_success_result.with_value("value")}endcontext'should fail'dosubject(:result){RSpecCommand.call(should_pass: false)}it{is_expected.tobe_failed_result}it{is_expected.not_tobe_success_result}it{is_expected.tobe_failed_result.with_status(:no_status)}it{is_expected.tobe_failed_result.with_message("message")}it{is_expected.tobe_failed_result.with_value("value")}endendendThe RSpec helpers provide two utility functions for use in your tests which should simplify testing commands with stubbed results. This can be useful when you want test a Rails controller, and your command is already tested separately. In this case, you only really care about the logic in your matching blocks --- not in the command itself.
Put another way: this is useful when you want to test the success or failure conditions of your commands.
RSpec.describeFooController,type: :controllerdodocontext'successful command'dolet(:foo){create(:foo)}let(:fake_res){fake_result(ok: true,message: 'foo updated')}describe'#update'do# Foo will not be called, instead we will get back the stubbed response# from the let block above, allowing us to bypass the command logic and# test only the controller logicstub_command_response(UpdateFooCommand,fake_res)post:update,params: {id: foo.id}expect(response).tohave_http_status:okendendendThis method --- stub_command_response --- can take the return value as either a second argument, or as a block:
stub_command_response(UpdateFooCommand,fake_res)# => fake result as an argumentstub_command_response(UpdateFooCommand){fake_res}# => fake result as a blockTo use with Minitest, update your 'test_helper' file to include:
require slayer/minitest
This provides you with new assertions: assert_success and assert_failed:
require"minitest/autorun"classMinitestCommandTest < Minitest::Testdefsetup@success_result=MinitestCommand.call(should_pass: true)@failed_result=MinitestCommand.call(should_pass: false)enddeftest_is_okassert_success@success_result,status: :no_status,message: 'message',value: 'value'refute_failed@success_result,status: :no_status,message: 'message',value: 'value'enddeftest_is_errassert_failed@failed_result,status: :no_status,message: 'message',value: 'value'refute_success@failed_result,status: :no_status,message: 'message',value: 'value'endendNote: There is no current integration for Minitest::Spec.
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:
gem'slayer_rails'And then execute:
$ bundleAnd that's it. The integration provides a small handful of features that make your life easier when working with Ruby on Rails.
With slayer_rails, Slayer::Form objects are automatically extended with ActiveRecord validations. You can use the same validations you would on your ActiveRecord models, but directly on your forms.
With slayer_rails there are two new methods for instantiating Slayer::Form objects: from_params and from_model. These make it easier to populate forms with data while in your Rails controllers.
Take the following example for a FooController:
classFooController < ApplicationControllerdefnew@foo_form=FooForm.newenddefedit@foo=Foo.find(params[:id])@foo_form=FooForm.from_model(@foo)enddefcreate@foo_form=FooForm.from_params(foo_params)enddefupdate@foo_form=FooForm.from_params(foo_params)endprivatedeffoo_paramsparams.require(:foo).permit(:bar,:baz)endendSlayer::Command and Slayer::Service objects are extended with access to ActiveRecord transactions. Anywhere in your Command or Service objects, you can execute a transaction block, which will let you bundle database interactions.
classFooCommand < Slayer::Commanddefcalltransactiondo# => database interactionsendendendUse generators to make sure your Slayer objects are always in the right place. slayer_rails includes generators for Slayer::Form and Slayer::Command.
$ bin/rails g slayer:form foo_form
$ bin/rails g slayer:command foo_commandBackwards compatability with previous versions requires additional includes.
require'slayer/compat/compat_040'If you use test matchers, you will have to separately require the compatability layer for your test runner:
require'slayer/compat/minitest_compat_040'# ORrequire'slayer/compat/rspec_compat_040'After checking out the repo, run bin/setup to install dependencies. Then, run bundle exec rake to run the tests and lint checks. You can also run bin/console for an interactive prompt that will allow you to experiment.
To install this gem onto your local machine, run bundle exec rake install.
To generate documentation run yard. To view undocumented files run yard stats --list-undoc.
Create a release from main:
bin/release {major|minor|patch|pre}
git push --follow-tagsThe release script validates the repository, bumps the version, creates a git tag.
Publishing to RubyGems and creating a GitHub Release are handled automatically by GitHub Actions.
$ docker-compose up
$ bin/ssh_to_container
$ bin/console
Bug reports and pull requests are welcome on GitHub at https://github.com/apsislabs/slayer.
Any PRs should be accompanied with documentation in README.md, and changes documented in CHANGELOG.md.
The gem is available as open source under the terms of the MIT License.
slayer was built by Apsis Labs. We love sharing what we build! Check out our other libraries on Github, and if you like our work you can hire us to build your vision.

