{"id":21144334,"url":"https://github.com/gilbert/solid_use_case","last_synced_at":"2025-07-09T06:30:43.688Z","repository":{"id":15425419,"uuid":"18157832","full_name":"gilbert/solid_use_case","owner":"gilbert","description":"A flexible use case pattern that works *with* your workflow, not against it.","archived":false,"fork":false,"pushed_at":"2018-11-15T21:48:16.000Z","size":39,"stargazers_count":107,"open_issues_count":0,"forks_count":7,"subscribers_count":6,"default_branch":"master","last_synced_at":"2024-04-23T12:37:00.425Z","etag":null,"topics":[],"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/gilbert.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}},"created_at":"2014-03-26T23:30:01.000Z","updated_at":"2024-04-23T12:37:00.426Z","dependencies_parsed_at":"2022-08-26T05:11:30.551Z","dependency_job_id":null,"html_url":"https://github.com/gilbert/solid_use_case","commit_stats":null,"previous_names":["mindeavor/solid_use_case"],"tags_count":12,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/gilbert%2Fsolid_use_case","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/gilbert%2Fsolid_use_case/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/gilbert%2Fsolid_use_case/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/gilbert%2Fsolid_use_case/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/gilbert","download_url":"https://codeload.github.com/gilbert/solid_use_case/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":225489434,"owners_count":17482378,"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":[],"created_at":"2024-11-20T08:16:58.651Z","updated_at":"2024-11-20T08:16:59.179Z","avatar_url":"https://github.com/gilbert.png","language":"Ruby","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Solid Use Case\n\n**Solid Use Case** is a gem to help you implement well-tested and flexible use cases. Solid Use Case is not a framework - it's a **design pattern library**. This means it works *with* your app's workflow, not against it.\n\n[See the Austin on Rails presentation slides](http://library.makersquare.com/learn/fp-in-rails)\n\n## Installation\n\nAdd this line to your application's Gemfile:\n\n    gem 'solid_use_case', '~\u003e 2.2.0'\n\nAnd then execute:\n\n    $ bundle\n\nOr install it yourself as:\n\n    $ gem install solid_use_case\n\n## Usage\n\nAt its core, this library is a light wrapper around [Deterministic](https://github.com/pzol/deterministic), a practical abstraction over the Either monad. Don't let that scare you - you don't have to understand monad theory to reap its benefits.\n\nThe only thing required is using the `#steps` method:\n\n### Rails Example\n\n```ruby\nclass UserSignup\n  include SolidUseCase\n\n  steps :validate, :save_user, :email_user\n\n  def validate(params)\n    user = User.new(params[:user])\n    if !user.valid?\n      fail :invalid_user, :user =\u003e user\n    else\n      params[:user] = user\n      continue(params)\n    end\n  end\n\n  def save_user(params)\n    user = params[:user]\n    if !user.save\n      fail :user_save_failed, :user =\u003e user\n    else\n      continue(params)\n    end\n  end\n\n  def email_user(params)\n    UserMailer.async.deliver(:welcome, params[:user].id)\n    # Because this is the last step, we want to end with the created user\n    continue(params[:user])\n  end\nend\n```\n\nNow you can run your use case in your controller and easily respond to the different outcomes (with pattern matching!):\n\n```ruby\nclass UsersController \u003c ApplicationController\n  def create\n    UserSignup.run(params).match do\n      success do |user|\n        flash[:success] = \"Thanks for signing up!\"\n        redirect_to profile_path(user)\n      end\n\n      failure(:invalid_user) do |error_data|\n        render_form_errors(error_data, \"Oops, fix your mistakes and try again\")\n      end\n\n      failure(:user_save_failed) do |error_data|\n        render_form_errors(error_data, \"Sorry, something went wrong on our side.\")\n      end\n\n      failure do |exception|\n        flash[:error] = \"something went terribly wrong\"\n        render 'new'\n      end\n    end\n  end\n\n  private\n\n  def render_form_errors(user, error_message)\n    @user = user\n    @error_message = error_message\n    render 'new'\n  end\nend\n```\n\n## Control Flow Helpers\n\nBecause we're using consistent successes and failures, we can use different functions to gain some nice control flow while avoiding those pesky if-else statements :)\n\n### #check_exists\n\n`check_exists` (alias `maybe_continue`) allows you to implicitly return a failure when a value is nil:\n\n```ruby\n# NOTE: The following assumes that #post_comment returns a Success or Failure\nvideo = Video.find_by(id: params[:video_id])\ncheck_exists(video).and_then { post_comment(params) }\n\n# NOTE: The following assumes that #find_tag and #create_tag both return a Success or Failure\ncheck_exists(Tag.find_by(name: tag)).or_else { create_tag(tag) }.and_then { ... }\n\n# If you wanted, you could refactor the above to use a method:\ndef find_tag(name)\n  maybe_continue(Tag.find_by(name: name))\nend\n\n# Then, elsewhere...\nfind_tag(tag)\n.or_else { create_tag(tag) }\n.and_then do |active_record_tag|\n  # At this point you can safely assume you have a tag :)\nend\n```\n\n### #check_each\n\nIf you're iterating through an array where each item could fail, `#check_each` might come in handy. A key point is that `check_each` will only fail if you return a failure; You don't need to return a `continue()`.\n\nReturning a failure within a `#check_each` block will short-circuit the loop.\n\n```ruby\ndef validate_score(score)\n  fail :score_out_of_range unless score.between?(0,100)\nend\n\ninput = [10, 50, 104, 3]\n\ncheck_each(input) {|s| validate_score(s)}.and_then do |scores|\n  write_to_db_or_whatever(scores)\nend\n```\n\nIf you need to continue with a value that is different from the array, you can use `continue_with:`. This is useful when you want to check a subset of your overall data.\n\n```ruby\nparams = { game_id: 7, scores: [10,50] }\n\ncheck_each(params[:scores], continue_with: params) {|s|\n  validate_score(s)\n}.and_then {|foo|\n  # Here `foo` is the same value as `params` above\n}\n```\n\n### #attempt\n\n`attempt` allows you to catch an exception. It's useful when you want to attempt something that might fail, but don't want to write all that exception-handling boilerplate.\n\n`attempt` also **auto-wraps your values**; in other words, the inner code does **not** have to return a success or failure.\n\nFor example, a Stripe API call:\n\n```ruby\n# Goal: Only charge customer if he/she exists\nattempt {\n  Stripe::Customer.retrieve(some_id)\n}\n.and_then do |stripe_customer|\n  stripe_customer.charge(...)\nend\n```\n\n## RSpec Matchers\n\nIf you're using RSpec, Solid Use Case provides some helpful matchers for testing.\n\nFirst you mix them them into RSpec:\n\n```ruby\n# In your spec_helper.rb\nrequire 'solid_use_case'\nrequire 'solid_use_case/rspec_matchers'\n\nRSpec.configure do |config|\n  config.include(SolidUseCase::RSpecMatchers)\nend\n```\n\nAnd then you can use the matchers, with helpful error messages:\n\n```ruby\ndescribe MyApp::SignUp do\n  it \"runs successfully\" do\n    result = MyApp::SignUp.run(:username =\u003e 'alice', :password =\u003e '123123')\n    expect(result).to be_a_success\n  end\n\n  it \"fails when password is too short\" do\n    result = MyApp::SignUp.run(:username =\u003e 'alice', :password =\u003e '5')\n    expect(result).to fail_with(:invalid_password)\n\n    # The above `fail_with` line is equivalent to:\n    # expect(result.value).to be_a SolidUseCase::Either::ErrorStruct\n    # expect(result.value.type).to eq :invalid_password\n\n    # You still have access to your arbitrary error data\n    expect(result.value.something).to eq 'whatever'\n  end\nend\n```\n\n## Testing\n\n    $ bundle exec rspec\n\n## Contributing\n\n1. Fork it\n2. Create your feature branch (`git checkout -b my-new-feature`)\n3. Commit your changes (`git commit -am 'Add some feature'`)\n4. Push to the branch (`git push origin my-new-feature`)\n5. Create new Pull Request\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fgilbert%2Fsolid_use_case","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fgilbert%2Fsolid_use_case","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fgilbert%2Fsolid_use_case/lists"}