https://github.com/gilbert/solid_use_case
A flexible use case pattern that works *with* your workflow, not against it.
https://github.com/gilbert/solid_use_case
Last synced: 12 months ago
JSON representation
A flexible use case pattern that works *with* your workflow, not against it.
- Host: GitHub
- URL: https://github.com/gilbert/solid_use_case
- Owner: gilbert
- License: mit
- Created: 2014-03-26T23:30:01.000Z (over 12 years ago)
- Default Branch: master
- Last Pushed: 2018-11-15T21:48:16.000Z (over 7 years ago)
- Last Synced: 2024-04-23T12:37:00.425Z (about 2 years ago)
- Language: Ruby
- Homepage:
- Size: 38.1 KB
- Stars: 107
- Watchers: 6
- Forks: 7
- Open Issues: 0
-
Metadata Files:
- Readme: README.md
- License: LICENSE.txt
Awesome Lists containing this project
README
# 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](http://library.makersquare.com/learn/fp-in-rails)
## 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](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.
The only thing required is using the `#steps` method:
### Rails Example
```ruby
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!):
```ruby
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:
```ruby
# 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.
```ruby
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.
```ruby
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:
```ruby
# 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:
```ruby
# 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:
```ruby
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