{"id":20292067,"url":"https://github.com/apsislabs/slayer","last_synced_at":"2025-06-30T06:35:02.419Z","repository":{"id":15089921,"uuid":"60317305","full_name":"apsislabs/slayer","owner":"apsislabs","description":"A Killer Service Layer","archived":false,"fork":false,"pushed_at":"2024-02-13T16:52:50.000Z","size":366,"stargazers_count":4,"open_issues_count":2,"forks_count":0,"subscribers_count":6,"default_branch":"main","last_synced_at":"2025-04-11T11:56:17.439Z","etag":null,"topics":["rails","ruby","service-object","slayer"],"latest_commit_sha":null,"homepage":"","language":"Ruby","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/apsislabs.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE.txt","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null}},"created_at":"2016-06-03T04:03:23.000Z","updated_at":"2023-04-03T12:50:05.000Z","dependencies_parsed_at":"2024-02-13T16:45:36.611Z","dependency_job_id":"0f679880-f623-4a88-9f95-eb092a99c435","html_url":"https://github.com/apsislabs/slayer","commit_stats":null,"previous_names":[],"tags_count":9,"template":false,"template_full_name":null,"purl":"pkg:github/apsislabs/slayer","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/apsislabs%2Fslayer","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/apsislabs%2Fslayer/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/apsislabs%2Fslayer/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/apsislabs%2Fslayer/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/apsislabs","download_url":"https://codeload.github.com/apsislabs/slayer/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/apsislabs%2Fslayer/sbom","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":262724500,"owners_count":23354260,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["rails","ruby","service-object","slayer"],"created_at":"2024-11-14T15:15:17.758Z","updated_at":"2025-06-30T06:35:02.391Z","avatar_url":"https://github.com/apsislabs.png","language":"Ruby","funding_links":[],"categories":[],"sub_categories":[],"readme":"![Slayer](https://raw.githubusercontent.com/apsislabs/slayer/master/slayer_logo.png)\n\n# Slayer: A Killer Service Layer\n\n[![Gem Version](https://badge.fury.io/rb/slayer.svg)](https://badge.fury.io/rb/slayer) [![Build Status](https://travis-ci.org/apsislabs/slayer.svg?branch=master)](https://travis-ci.org/apsislabs/slayer) [![Code Climate](https://codeclimate.com/github/apsislabs/slayer/badges/gpa.svg)](https://codeclimate.com/github/apsislabs/slayer) [![Coverage Status](https://coveralls.io/repos/github/apsislabs/slayer/badge.svg)](https://coveralls.io/github/apsislabs/slayer)\n\nSlayer is intended to operate as a minimal service layer for your ruby application. To achieve this, Slayer provides base classes for business logic.\n\n**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.**\n\n## Application Structure\n\nSlayer provides 2 base classes for organizing your business logic: `Forms` and `Commands`. These each have a distinct role in your application's structure.\n\n### Forms\n\n`Slayer::Forms` are objects for wrapping a set of data, usually to be passed as a parameter to a `Command` or `Service`.\n\n### Commands\n\n`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.\n\n`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.\n\nThis 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.\n\n## Installation\n\nAdd this line to your application's Gemfile:\n\n```ruby\ngem 'slayer'\n```\n\nAnd then execute:\n\n```sh\n$ bundle\n```\n\nOr install it yourself as:\n\n```sh\n$ gem install slayer\n```\n\n## Usage\n\n### Commands\n\nSlayer 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`.\n\n```ruby\n# A Command that passes when given the string \"foo\"\n# and fails if given anything else.\nclass FooCommand \u003c Slayer::Command\n  def call(foo:)\n    unless foo == \"foo\"\n      return err value: foo, message: \"Argument must be foo!\"\n    end\n\n    ok value: foo\n  end\nend\n```\n\nHandling 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:\n\n```ruby\nFooCommand.call(foo: \"foo\") do |m|\n  m.ok do |value|\n    puts \"This code runs on success\"\n  end\n\n  m.err do |_value, result|\n    puts \"This code runs on failure. Message: #{result.message}\"\n  end\n\n  m.all do\n    puts \"This code runs on failure or success\"\n  end\n\n  m.ensure do\n    puts \"This code always runs after other handler blocks\"\n  end\nend\n```\n\nThe 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:\n\n```ruby\nresult = FooCommand.call(foo: \"foo\")\nputs result.ok? # =\u003e true\n\nresult = FooCommand.call(foo: \"bar\")\nputs result.ok? # =\u003e false\n```\n\nHere'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.\n\n```ruby\n# commands/user_controller.rb\nclass CreateUserCommand \u003c Slayer::Command\n  def call(create_user_form:)\n    unless arguments_valid?(create_user_form)\n      return err value: create_user_form, status: :arguments_invalid\n    end\n\n    user = nil\n    transaction do\n      user = User.create(create_user_form.attributes)\n    end\n\n    unless user.persisted?\n      return err message: I18n.t('user.create.error'), status: :unprocessible_entity\n    end\n\n    ok value: user\n  end\n\n  def arguments_valid?(create_user_form)\n    create_user_form.kind_of?(CreateUserForm) \u0026\u0026\n      create_user_form.valid? \u0026\u0026\n      !User.exists?(email: create_user_form.email)\n  end\nend\n\n# controllers/user_controller.rb\nclass UsersController \u003c ApplicationController\n  def create\n    @create_user_form = CreateUserForm.from_params(create_user_params)\n\n    CreateUserCommand.call(create_user_form: @create_user_form) do |m|\n      m.ok do |user|\n        auto_login(user)\n        redirect_to root_path, notice: t('user.create.success')\n      end\n\n      m.err(:arguments_invalid) do |_user, result|\n        flash[:error] = result.errors.full_messages.to_sentence\n        render :new, status: :unprocessible_entity\n      end\n\n      m.err do |_user, result|\n        flash[:error] = t('user.create.error')\n        render :new, status: :bad_request\n      end\n    end\n  end\n\n  private\n\n    def required_user_params\n      [:first_name, :last_name, :email, :password]\n    end\n\n    def create_user_params\n      permitted_params = required_user_params \u003c\u003c :password_confirmation\n      params.require(:user).permit(permitted_params)\n    end\nend\n```\n\n### Result Matcher\n\nThe result matcher is an object that is used to handle `Slayer::Result` objects based on their status.\n\n#### Handlers: `ok`, `err`, `all`, `ensure`\n\nThe 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.\n\n* The `ok` block runs if the command was successful.\n* The `err` block runs if the command was `koed`.\n* The `all` block runs on any type of result --- `ok` or `err` --- unless the result has already been handled.\n* The `ensure` block always runs after the result has been handled.\n\n#### Handler Params\n\nEvery 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:\n\n```ruby\nclass NoArgCommand \u003c Slayer::Command\n  def call\n    @instance_var = 'instance'\n    ok value: 'pass'\n  end\nend\n\n\nNoArgCommand.call do |m|\n  m.all do |value, result, command|\n    puts value # =\u003e 'pass'\n    puts result.ok? # =\u003e true\n    puts command.instance_var # =\u003e 'instance'\n  end\nend\n```\n\n#### Statuses\n\nYou 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:\n\n```ruby\nclass StatusCommand \u003c Slayer::Command\n  def call\n    return err message: \"Extra specific ko\", status: :extra_specific_err if extra_specific_err?\n    return err message: \"Specific ko\", status: :specific_err if specific_err?\n    return err message: \"Generic ko\" if generic_err?\n\n    return ok message: \"Specific pass\", status: :specific_pass if specific_pass?\n\n    ok message: \"Generic pass\"\n  end\nend\n\nStatusCommand.call do |m|\n  m.err                         { puts \"generic err\" }\n  m.err(:specific_err)          { puts \"specific err\" }\n  m.err(:extra_specific_err)    { puts \"extra specific err\" }\n\n  m.ok                          { puts \"generic pass\" }\n  m.ok(:specific_pass)          { puts \"specific pass\" }\nend\n```\n\n## RSpec \u0026 Minitest Integrations\n\n`Slayer` provides assertions and matchers that make testing your `Commands` simpler.\n\n### RSpec\n\nTo use with RSpec, update your `spec_helper.rb` file to include:\n\n`require 'slayer/rspec'`\n\nThis 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:\n\n```ruby\nRSpec.describe RSpecCommand do\n  describe '#call' do\n    context 'should pass' do\n      subject(:result) { RSpecCommand.call(should_pass: true) }\n\n      it { is_expected.to be_success_result }\n      it { is_expected.not_to be_failed_result }\n      it { is_expected.to be_success_result.with_status(:no_status) }\n      it { is_expected.to be_success_result.with_message(\"message\") }\n      it { is_expected.to be_success_result.with_value(\"value\") }\n    end\n\n    context 'should fail' do\n      subject(:result) { RSpecCommand.call(should_pass: false) }\n\n      it { is_expected.to be_failed_result }\n      it { is_expected.not_to be_success_result }\n      it { is_expected.to be_failed_result.with_status(:no_status) }\n      it { is_expected.to be_failed_result.with_message(\"message\") }\n      it { is_expected.to be_failed_result.with_value(\"value\") }\n    end\n  end\nend\n```\n\n#### Stubbing Command Results\n\nThe 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\ntest 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.\n\nPut another way: this is useful when you want to test the success or failure conditions of your commands.\n\n```ruby\nRSpec.describe FooController, type: :controller do do\n  context 'successful command' do\n    let(:foo) { create(:foo) }\n    let(:fake_res) { fake_result(ok: true, message: 'foo updated') }\n\n    describe '#update' do\n      # Foo will not be called, instead we will get back the stubbed response\n      # from the let block above, allowing us to bypass the command logic and\n      # test only the controller logic\n      stub_command_response(UpdateFooCommand, fake_res)\n      post :update, params: { id: foo.id }\n      expect(response).to have_http_status :ok\n    end\n  end\nend\n\n```\n\nThis method --- `stub_command_response` --- can take the return value as either a second argument, or as a block:\n\n```ruby\nstub_command_response(UpdateFooCommand, fake_res)     # =\u003e fake result as an argument\nstub_command_response(UpdateFooCommand) { fake_res }  # =\u003e fake result as a block\n```\n\n### Minitest\n\nTo use with Minitest, update your 'test_helper' file to include:\n\n`require slayer/minitest`\n\nThis provides you with new assertions: `assert_success` and `assert_failed`:\n\n```ruby\nrequire \"minitest/autorun\"\n\nclass MinitestCommandTest \u003c Minitest::Test\n  def setup\n    @success_result = MinitestCommand.call(should_pass: true)\n    @failed_result = MinitestCommand.call(should_pass: false)\n  end\n\n  def test_is_ok\n    assert_success @success_result, status: :no_status, message: 'message', value: 'value'\n    refute_failed @success_result, status: :no_status, message: 'message', value: 'value'\n  end\n\n  def test_is_err\n    assert_failed @failed_result, status: :no_status, message: 'message', value: 'value'\n    refute_success @failed_result, status: :no_status, message: 'message', value: 'value'\n  end\nend\n```\n\n**Note:** There is no current integration for `Minitest::Spec`.\n\n## Rails Integration\n\nWhile 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:\n\n```ruby\ngem 'slayer_rails'\n```\n\nAnd then execute:\n\n```sh\n$ bundle\n```\n\nAnd that's it. The integration provides a small handful of features that make your life easier when working with Ruby on Rails.\n\n### Form Validations\n\nWith `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.\n\n### Form Creation\n\nWith `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.\n\nTake the following example for a `FooController`:\n\n```ruby\nclass FooController \u003c ApplicationController\n  def new\n    @foo_form = FooForm.new\n  end\n\n  def edit\n    @foo = Foo.find(params[:id])\n    @foo_form = FooForm.from_model(@foo)\n  end\n\n  def create\n    @foo_form = FooForm.from_params(foo_params)\n  end\n\n  def update\n    @foo_form = FooForm.from_params(foo_params)\n  end\n\n  private\n\n    def foo_params\n      params.require(:foo).permit(:bar, :baz)\n    end\nend\n```\n\n### Transactions\n\n`Slayer::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.\n\n```ruby\nclass FooCommand \u003c Slayer::Command\n  def call\n    transaction do\n      # =\u003e database interactions\n    end\n  end\nend\n```\n\n### Generators\n\nUse generators to make sure your `Slayer` objects are always in the right place. `slayer_rails` includes generators for `Slayer::Form` and `Slayer::Command`.\n\n```sh\n$ bin/rails g slayer:form foo_form\n$ bin/rails g slayer:command foo_command\n```\n\n## Compatability\n\nBackwards compatability with previous versions requires additional includes.\n\n```ruby\nrequire 'slayer/compat/compat_040'\n```\n\nIf you use test matchers, you will have to separately require the compatability layer for your test runner:\n\n```ruby\nrequire 'slayer/compat/minitest_compat_040'\n\n# OR\n\nrequire 'slayer/compat/rspec_compat_040'\n```\n\n## Development\n\nAfter 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.\n\nTo install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org).\n\nTo generate documentation run `yard`. To view undocumented files run `yard stats --list-undoc`.\n\n### Development w/ Docker\n\n    $ docker-compose up\n    $ bin/ssh_to_container\n    $ bin/console\n\n## Contributing\n\nBug reports and pull requests are welcome on GitHub at https://github.com/apsislabs/slayer.\n\nAny PRs should be accompanied with documentation in `README.md`, and changes documented in [`CHANGELOG.md`](https://keepachangelog.com/).\n\n## License\n\nThe gem is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT).\n\n---\n\n# Built by Apsis\n\n[![apsis](https://s3-us-west-2.amazonaws.com/apsiscdn/apsis.png)](https://www.apsis.io)\n\n`slayer` was built by Apsis Labs. We love sharing what we build! Check out our [other libraries on Github](https://github.com/apsislabs), and if you like our work you can [hire us](https://www.apsis.io/work-with-us/) to build your vision.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fapsislabs%2Fslayer","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fapsislabs%2Fslayer","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fapsislabs%2Fslayer/lists"}