• Stars
    star
    109
  • Rank 317,163 (Top 7 %)
  • Language
    Ruby
  • License
    MIT License
  • Created over 10 years ago
  • Updated almost 6 years ago

Reviews

There are no reviews yet. Be the first to send feedback to the community and the maintainers!

Repository Details

A flexible use case pattern that works *with* your workflow, not against it.

Solid Use Case

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.

See the Austin on Rails presentation slides

Installation

Add this line to your application's Gemfile:

gem 'solid_use_case', '~> 2.2.0'

And then execute:

$ bundle

Or install it yourself as:

$ gem install solid_use_case

Usage

At its core, this library is a light wrapper around 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.

The only thing required is using the #steps method:

Rails Example

class UserSignup
  include SolidUseCase

  steps :validate, :save_user, :email_user

  def validate(params)
    user = User.new(params[:user])
    if !user.valid?
      fail :invalid_user, :user => user
    else
      params[:user] = user
      continue(params)
    end
  end

  def save_user(params)
    user = params[:user]
    if !user.save
      fail :user_save_failed, :user => user
    else
      continue(params)
    end
  end

  def email_user(params)
    UserMailer.async.deliver(:welcome, params[:user].id)
    # Because this is the last step, we want to end with the created user
    continue(params[:user])
  end
end

Now you can run your use case in your controller and easily respond to the different outcomes (with pattern matching!):

class UsersController < ApplicationController
  def create
    UserSignup.run(params).match do
      success do |user|
        flash[:success] = "Thanks for signing up!"
        redirect_to profile_path(user)
      end

      failure(:invalid_user) do |error_data|
        render_form_errors(error_data, "Oops, fix your mistakes and try again")
      end

      failure(:user_save_failed) do |error_data|
        render_form_errors(error_data, "Sorry, something went wrong on our side.")
      end

      failure do |exception|
        flash[:error] = "something went terribly wrong"
        render 'new'
      end
    end
  end

  private

  def render_form_errors(user, error_message)
    @user = user
    @error_message = error_message
    render 'new'
  end
end

Control Flow Helpers

Because 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 :)

#check_exists

check_exists (alias maybe_continue) allows you to implicitly return a failure when a value is nil:

# NOTE: The following assumes that #post_comment returns a Success or Failure
video = Video.find_by(id: params[:video_id])
check_exists(video).and_then { post_comment(params) }

# NOTE: The following assumes that #find_tag and #create_tag both return a Success or Failure
check_exists(Tag.find_by(name: tag)).or_else { create_tag(tag) }.and_then { ... }

# If you wanted, you could refactor the above to use a method:
def find_tag(name)
  maybe_continue(Tag.find_by(name: name))
end

# Then, elsewhere...
find_tag(tag)
.or_else { create_tag(tag) }
.and_then do |active_record_tag|
  # At this point you can safely assume you have a tag :)
end

#check_each

If 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().

Returning a failure within a #check_each block will short-circuit the loop.

def validate_score(score)
  fail :score_out_of_range unless score.between?(0,100)
end

input = [10, 50, 104, 3]

check_each(input) {|s| validate_score(s)}.and_then do |scores|
  write_to_db_or_whatever(scores)
end

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

params = { game_id: 7, scores: [10,50] }

check_each(params[:scores], continue_with: params) {|s|
  validate_score(s)
}.and_then {|foo|
  # Here `foo` is the same value as `params` above
}

#attempt

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.

attempt also auto-wraps your values; in other words, the inner code does not have to return a success or failure.

For example, a Stripe API call:

# Goal: Only charge customer if he/she exists
attempt {
  Stripe::Customer.retrieve(some_id)
}
.and_then do |stripe_customer|
  stripe_customer.charge(...)
end

RSpec Matchers

If you're using RSpec, Solid Use Case provides some helpful matchers for testing.

First you mix them them into RSpec:

# In your spec_helper.rb
require 'solid_use_case'
require 'solid_use_case/rspec_matchers'

RSpec.configure do |config|
  config.include(SolidUseCase::RSpecMatchers)
end

And then you can use the matchers, with helpful error messages:

describe MyApp::SignUp do
  it "runs successfully" do
    result = MyApp::SignUp.run(:username => 'alice', :password => '123123')
    expect(result).to be_a_success
  end

  it "fails when password is too short" do
    result = MyApp::SignUp.run(:username => 'alice', :password => '5')
    expect(result).to fail_with(:invalid_password)

    # The above `fail_with` line is equivalent to:
    # expect(result.value).to be_a SolidUseCase::Either::ErrorStruct
    # expect(result.value.type).to eq :invalid_password

    # You still have access to your arbitrary error data
    expect(result.value.something).to eq 'whatever'
  end
end

Testing

$ bundle exec rspec

Contributing

  1. Fork it
  2. Create your feature branch (git checkout -b my-new-feature)
  3. Commit your changes (git commit -am 'Add some feature')
  4. Push to the branch (git push origin my-new-feature)
  5. Create new Pull Request

More Repositories

1

es-papp

A proposal for adding partial application support to JavaScript.
JavaScript
359
star
2

zaml

The Final Form of configuration files
TypeScript
46
star
3

jsdn

JavaScript Diagram Notation
JavaScript
29
star
4

es-explicit-this

Explicit this naming in ECMAScript functions
18
star
5

mithril-cc

An opinionated library for writing Mithril components
TypeScript
13
star
6

rollup-endpoint

The easiest way to serve a rollup bundle.
JavaScript
13
star
7

blog-post-examples

Examples (and more!) from my blog.
JavaScript
11
star
8

query-loader

Easy-to-use loading bar that loads static assets before displaying the page.
JavaScript
10
star
9

curated-js

A harshly curated list of minimal and pragmatic JavaScript libraries
7
star
10

proposal-temporary-bindings

6
star
11

staticmatic-js-app-starter

Quick starter for backbone.js, underscore.js, and jquery
JavaScript
6
star
12

shout

A nice-looking url shortener (with admin panel) that uses random dictionary words
HTML
4
star
13

lomic

Programming language for the game Nomic
Ruby
3
star
14

solarjs

A Radically Simple Node.js Server Framework
TypeScript
2
star
15

keydash

Improve your keyboard navigation skills. For free!
TypeScript
2
star
16

node-sass-endpoint

Easily server a SASS file from express.
JavaScript
2
star
17

powermotivate.me

A javascript implementation of the Seinfeld calendar
JavaScript
2
star
18

Fx.js

A lightweight, self-contained javascript animation library
JavaScript
2
star
19

Sublime-Text-2-Icon

2
star
20

staticmatic-basic

Default skeleton for StaticMatic
Ruby
2
star
21

pomic

A programming version of the game Nomic
Ruby
1
star
22

cheese_bot

A Planet Wars bot for Google's AI Contest http://ai-contest.com/
Java
1
star
23

repo-map

JavaScript
1
star
24

snake-mmo

Basically a test project to help develop SocketStream.
CoffeeScript
1
star
25

Modernize-Web-Solutions

The source code to our portfolio site!
JavaScript
1
star
26

fortune-lang-prototype

The fortune lab
OCaml
1
star
27

lomicbox

Manage multiple instances of lomic with ease
1
star
28

solbrain

Prolog
1
star
29

js-zero

Backup of an old type checker
JavaScript
1
star
30

staticmatic-iphone

A basic skeleton with some useful iPhone boilerplate
Ruby
1
star
31

soupmobile-webpop

The source to soupmobile.com's new design
CSS
1
star
32

temp52

Shell
1
star
33

test17

Shell
1
star
34

temp53

Shell
1
star
35

totg

flash game
ActionScript
1
star