├── .gitignore ├── .rspec ├── Gemfile ├── LICENSE.txt ├── README.md ├── Rakefile ├── lib ├── solid_use_case.rb └── solid_use_case │ ├── either.rb │ ├── either │ ├── class_methods.rb │ ├── error_struct.rb │ └── util.rb │ ├── rspec_matchers.rb │ └── version.rb ├── solid_use_case.gemspec └── spec ├── control_flow_spec.rb ├── either_spec.rb ├── rspec_matchers_spec.rb └── spec_helper.rb /.gitignore: -------------------------------------------------------------------------------- 1 | *.gem 2 | *.rbc 3 | .bundle 4 | .config 5 | .DS_Store 6 | .yardoc 7 | Gemfile.lock 8 | InstalledFiles 9 | _yardoc 10 | coverage 11 | doc/ 12 | lib/bundler/man 13 | pkg 14 | rdoc 15 | spec/reports 16 | test/tmp 17 | test/version_tmp 18 | tmp 19 | -------------------------------------------------------------------------------- /.rspec: -------------------------------------------------------------------------------- 1 | --color 2 | --format documentation 3 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | # Specify your gem's dependencies in solid_use_case.gemspec 4 | gemspec 5 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) 2014 Gilbert 2 | 3 | MIT License 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining 6 | a copy of this software and associated documentation files (the 7 | "Software"), to deal in the Software without restriction, including 8 | without limitation the rights to use, copy, modify, merge, publish, 9 | distribute, sublicense, and/or sell copies of the Software, and to 10 | permit persons to whom the Software is furnished to do so, subject to 11 | the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be 14 | included in all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 19 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 20 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 21 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 22 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Solid Use Case 2 | 3 | **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. 4 | 5 | [See the Austin on Rails presentation slides](http://library.makersquare.com/learn/fp-in-rails) 6 | 7 | ## Installation 8 | 9 | Add this line to your application's Gemfile: 10 | 11 | gem 'solid_use_case', '~> 2.2.0' 12 | 13 | And then execute: 14 | 15 | $ bundle 16 | 17 | Or install it yourself as: 18 | 19 | $ gem install solid_use_case 20 | 21 | ## Usage 22 | 23 | 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. 24 | 25 | The only thing required is using the `#steps` method: 26 | 27 | ### Rails Example 28 | 29 | ```ruby 30 | class UserSignup 31 | include SolidUseCase 32 | 33 | steps :validate, :save_user, :email_user 34 | 35 | def validate(params) 36 | user = User.new(params[:user]) 37 | if !user.valid? 38 | fail :invalid_user, :user => user 39 | else 40 | params[:user] = user 41 | continue(params) 42 | end 43 | end 44 | 45 | def save_user(params) 46 | user = params[:user] 47 | if !user.save 48 | fail :user_save_failed, :user => user 49 | else 50 | continue(params) 51 | end 52 | end 53 | 54 | def email_user(params) 55 | UserMailer.async.deliver(:welcome, params[:user].id) 56 | # Because this is the last step, we want to end with the created user 57 | continue(params[:user]) 58 | end 59 | end 60 | ``` 61 | 62 | Now you can run your use case in your controller and easily respond to the different outcomes (with pattern matching!): 63 | 64 | ```ruby 65 | class UsersController < ApplicationController 66 | def create 67 | UserSignup.run(params).match do 68 | success do |user| 69 | flash[:success] = "Thanks for signing up!" 70 | redirect_to profile_path(user) 71 | end 72 | 73 | failure(:invalid_user) do |error_data| 74 | render_form_errors(error_data, "Oops, fix your mistakes and try again") 75 | end 76 | 77 | failure(:user_save_failed) do |error_data| 78 | render_form_errors(error_data, "Sorry, something went wrong on our side.") 79 | end 80 | 81 | failure do |exception| 82 | flash[:error] = "something went terribly wrong" 83 | render 'new' 84 | end 85 | end 86 | end 87 | 88 | private 89 | 90 | def render_form_errors(user, error_message) 91 | @user = user 92 | @error_message = error_message 93 | render 'new' 94 | end 95 | end 96 | ``` 97 | 98 | ## Control Flow Helpers 99 | 100 | 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 :) 101 | 102 | ### #check_exists 103 | 104 | `check_exists` (alias `maybe_continue`) allows you to implicitly return a failure when a value is nil: 105 | 106 | ```ruby 107 | # NOTE: The following assumes that #post_comment returns a Success or Failure 108 | video = Video.find_by(id: params[:video_id]) 109 | check_exists(video).and_then { post_comment(params) } 110 | 111 | # NOTE: The following assumes that #find_tag and #create_tag both return a Success or Failure 112 | check_exists(Tag.find_by(name: tag)).or_else { create_tag(tag) }.and_then { ... } 113 | 114 | # If you wanted, you could refactor the above to use a method: 115 | def find_tag(name) 116 | maybe_continue(Tag.find_by(name: name)) 117 | end 118 | 119 | # Then, elsewhere... 120 | find_tag(tag) 121 | .or_else { create_tag(tag) } 122 | .and_then do |active_record_tag| 123 | # At this point you can safely assume you have a tag :) 124 | end 125 | ``` 126 | 127 | ### #check_each 128 | 129 | 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()`. 130 | 131 | Returning a failure within a `#check_each` block will short-circuit the loop. 132 | 133 | ```ruby 134 | def validate_score(score) 135 | fail :score_out_of_range unless score.between?(0,100) 136 | end 137 | 138 | input = [10, 50, 104, 3] 139 | 140 | check_each(input) {|s| validate_score(s)}.and_then do |scores| 141 | write_to_db_or_whatever(scores) 142 | end 143 | ``` 144 | 145 | 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. 146 | 147 | ```ruby 148 | params = { game_id: 7, scores: [10,50] } 149 | 150 | check_each(params[:scores], continue_with: params) {|s| 151 | validate_score(s) 152 | }.and_then {|foo| 153 | # Here `foo` is the same value as `params` above 154 | } 155 | ``` 156 | 157 | ### #attempt 158 | 159 | `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. 160 | 161 | `attempt` also **auto-wraps your values**; in other words, the inner code does **not** have to return a success or failure. 162 | 163 | For example, a Stripe API call: 164 | 165 | ```ruby 166 | # Goal: Only charge customer if he/she exists 167 | attempt { 168 | Stripe::Customer.retrieve(some_id) 169 | } 170 | .and_then do |stripe_customer| 171 | stripe_customer.charge(...) 172 | end 173 | ``` 174 | 175 | ## RSpec Matchers 176 | 177 | If you're using RSpec, Solid Use Case provides some helpful matchers for testing. 178 | 179 | First you mix them them into RSpec: 180 | 181 | ```ruby 182 | # In your spec_helper.rb 183 | require 'solid_use_case' 184 | require 'solid_use_case/rspec_matchers' 185 | 186 | RSpec.configure do |config| 187 | config.include(SolidUseCase::RSpecMatchers) 188 | end 189 | ``` 190 | 191 | And then you can use the matchers, with helpful error messages: 192 | 193 | ```ruby 194 | describe MyApp::SignUp do 195 | it "runs successfully" do 196 | result = MyApp::SignUp.run(:username => 'alice', :password => '123123') 197 | expect(result).to be_a_success 198 | end 199 | 200 | it "fails when password is too short" do 201 | result = MyApp::SignUp.run(:username => 'alice', :password => '5') 202 | expect(result).to fail_with(:invalid_password) 203 | 204 | # The above `fail_with` line is equivalent to: 205 | # expect(result.value).to be_a SolidUseCase::Either::ErrorStruct 206 | # expect(result.value.type).to eq :invalid_password 207 | 208 | # You still have access to your arbitrary error data 209 | expect(result.value.something).to eq 'whatever' 210 | end 211 | end 212 | ``` 213 | 214 | ## Testing 215 | 216 | $ bundle exec rspec 217 | 218 | ## Contributing 219 | 220 | 1. Fork it 221 | 2. Create your feature branch (`git checkout -b my-new-feature`) 222 | 3. Commit your changes (`git commit -am 'Add some feature'`) 223 | 4. Push to the branch (`git push origin my-new-feature`) 224 | 5. Create new Pull Request 225 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require "bundler/gem_tasks" 2 | -------------------------------------------------------------------------------- /lib/solid_use_case.rb: -------------------------------------------------------------------------------- 1 | require "deterministic" 2 | require "deterministic/core_ext/either" 3 | 4 | require "solid_use_case/version" 5 | require 'solid_use_case/either/class_methods.rb' 6 | require 'solid_use_case/either/error_struct.rb' 7 | require 'solid_use_case/either/util.rb' 8 | require 'solid_use_case/either.rb' 9 | 10 | module SolidUseCase 11 | def self.included(includer) 12 | includer.send :include, Deterministic::CoreExt::Either 13 | includer.send :include, Either 14 | includer.extend Either::ClassMethods 15 | end 16 | end 17 | 18 | class Deterministic::Either 19 | class AttemptAll 20 | alias :step :let 21 | end 22 | end 23 | -------------------------------------------------------------------------------- /lib/solid_use_case/either.rb: -------------------------------------------------------------------------------- 1 | module SolidUseCase 2 | module Either 3 | 4 | def run(inputs) 5 | steps = self.class.instance_variable_get("@__steps").clone 6 | current_result = Success(inputs) 7 | return current_result unless steps 8 | 9 | while steps.count > 0 10 | next_step = steps.shift 11 | 12 | current_result = current_result.and_then do 13 | if next_step.is_a?(Class) && (next_step.respond_to? :can_run_either?) && next_step.can_run_either? 14 | next_step.run(current_result.value) 15 | elsif next_step.is_a?(Symbol) 16 | self.send(next_step, current_result.value) 17 | else 18 | raise "Invalid step type: #{next_step.inspect}" 19 | end 20 | end 21 | end 22 | 23 | current_result 24 | end 25 | 26 | # # # # # # 27 | # Helpers # 28 | # # # # # # 29 | 30 | def check_exists(val, error=:not_found) 31 | if val.nil? 32 | fail(error) 33 | else 34 | continue(val) 35 | end 36 | end 37 | 38 | def check_each(array, continue_with: array) 39 | failure = nil 40 | array.find do |item| 41 | item_result = yield(item) 42 | if item_result.is_a?(Failure) 43 | failure = item_result 44 | true 45 | end 46 | end 47 | 48 | failure || continue(continue_with) 49 | end 50 | 51 | def attempt 52 | attempt_all do 53 | try { yield } 54 | end 55 | end 56 | 57 | def catch(required, *exceptions) 58 | exceptions << required 59 | result = attempt_all do 60 | try { yield } 61 | end 62 | if result.is_a?(Failure) && exceptions.any? 63 | raise result.value unless exceptions.include?(result.value) 64 | end 65 | end 66 | 67 | def fail(type, data={}) 68 | data[:type] = type 69 | Failure(ErrorStruct.new(data)) 70 | end 71 | 72 | alias :maybe_continue :check_exists 73 | alias :continue :Success 74 | 75 | def self.success(value) 76 | Success(value) 77 | end 78 | 79 | def self.failure(type, data={}) 80 | data[:type] = type 81 | Failure(ErrorStruct.new(data)) 82 | end 83 | end 84 | end 85 | -------------------------------------------------------------------------------- /lib/solid_use_case/either/class_methods.rb: -------------------------------------------------------------------------------- 1 | module SolidUseCase 2 | module Either 3 | module ClassMethods 4 | 5 | def run(input_hash={}) 6 | self.new.run(input_hash) 7 | end 8 | 9 | def steps(*args) 10 | @__steps ||= [] 11 | @__steps += args 12 | end 13 | 14 | def can_run_either? 15 | true 16 | end 17 | 18 | end 19 | end 20 | end -------------------------------------------------------------------------------- /lib/solid_use_case/either/error_struct.rb: -------------------------------------------------------------------------------- 1 | module SolidUseCase 2 | module Either 3 | class ErrorStruct < OpenStruct 4 | def ==(error_type_symbol) 5 | self[:type] == error_type_symbol 6 | end 7 | end 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /lib/solid_use_case/either/util.rb: -------------------------------------------------------------------------------- 1 | module SolidUseCase 2 | module Util 3 | 4 | def symbolize_names(object) 5 | case object 6 | when Hash 7 | new = {} 8 | object.each do |key, value| 9 | key = (key.to_sym rescue key) || key 10 | new[key] = symbolize_names(value) 11 | end 12 | new 13 | when Array 14 | object.map { |value| symbolize_names(value) } 15 | else 16 | object 17 | end 18 | end 19 | 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /lib/solid_use_case/rspec_matchers.rb: -------------------------------------------------------------------------------- 1 | module SolidUseCase 2 | module RSpecMatchers 3 | 4 | def be_a_success 5 | ValidateSuccess.new 6 | end 7 | 8 | def fail_with(error_name) 9 | MatchFailure.new(error_name) 10 | end 11 | 12 | class ValidateSuccess 13 | def matches?(result) 14 | @result = result 15 | @result.is_a? Deterministic::Success 16 | end 17 | 18 | def failure_message 19 | "expected result to be a success\n" + 20 | if @result.value.is_a? SolidUseCase::Either::ErrorStruct 21 | "Error & Data:\n #{@result.value.type} - #{@result.value.inspect}" 22 | elsif @result.value.is_a? Exception 23 | backtrace = @result.value.backtrace.reject do |file| 24 | file =~ %r{deterministic/either/attempt_all.rb|deterministic/core_ext/either.rb} 25 | end.take_while do |file| 26 | file.match(%r{rspec-core-[^/]+/lib/rspec/core/example\.rb}).nil? 27 | end 28 | "Raised Error:\n #{@result.value.message}\n\t#{backtrace.join "\n\t"}" 29 | else 30 | "Error: #{@result.value.inspect}" 31 | end 32 | end 33 | 34 | def failure_message_when_negated 35 | "expected result to not be a success" 36 | end 37 | end 38 | 39 | class MatchFailure 40 | 41 | def initialize(expected_error_name) 42 | @expected_error_name = expected_error_name 43 | end 44 | 45 | def matches?(result) 46 | @result = result 47 | @is_failure = @result.is_a?(Deterministic::Failure) 48 | @is_failure && @result.value.type == @expected_error_name 49 | end 50 | 51 | def failure_message 52 | if @is_failure 53 | "expected result to fail with :#{@expected_error_name} (failed with :#{@result.value.type} instead)" 54 | else 55 | "expected result to fail with :#{@expected_error_name} (result was successful instead)" 56 | end 57 | end 58 | 59 | def failure_message_when_negated 60 | if @is_failure 61 | "expected result to fail with an error not equal to :#{@expected_error_name}" 62 | else 63 | "expected result to fail with an error not equal to :#{@expected_error_name} (result was successful instead)" 64 | end 65 | end 66 | end 67 | 68 | end 69 | end 70 | -------------------------------------------------------------------------------- /lib/solid_use_case/version.rb: -------------------------------------------------------------------------------- 1 | module SolidUseCase 2 | VERSION = "2.2.0" 3 | end 4 | -------------------------------------------------------------------------------- /solid_use_case.gemspec: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | lib = File.expand_path('../lib', __FILE__) 3 | $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) 4 | require 'solid_use_case/version' 5 | 6 | Gem::Specification.new do |spec| 7 | spec.name = "solid_use_case" 8 | spec.version = SolidUseCase::VERSION 9 | spec.authors = ["Gilbert"] 10 | spec.email = ["gilbertbgarza@gmail.com"] 11 | spec.description = %q{Create use cases the way they were meant to be. Easily verify inputs at each step and seamlessly fail with custom error data and convenient pattern matching.} 12 | spec.summary = %q{A flexible UseCase pattern that works *with* your workflow, not against it.} 13 | spec.homepage = "https://github.com/mindeavor/solid_use_case" 14 | spec.license = "MIT" 15 | 16 | spec.files = `git ls-files`.split($/) 17 | spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) } 18 | spec.test_files = spec.files.grep(%r{^(test|spec|features)/}) 19 | spec.require_paths = ["lib"] 20 | 21 | spec.required_ruby_version = '~> 2.0' 22 | 23 | spec.add_dependency "deterministic", '~> 0.6.0' 24 | 25 | spec.add_development_dependency "bundler", "~> 1.3" 26 | spec.add_development_dependency "rake" 27 | spec.add_development_dependency "rspec", "~> 2.14.1" 28 | end 29 | -------------------------------------------------------------------------------- /spec/control_flow_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe "Control Flow Helpers" do 4 | 5 | describe '#check_exists' do 6 | class FloodGate 7 | include SolidUseCase 8 | 9 | def basic(input) 10 | check_exists(input).and_then {|x| Success(x * 2) } 11 | end 12 | 13 | def alias(input) 14 | maybe_continue(input) 15 | end 16 | 17 | def custom_error(input, err) 18 | check_exists(input, err) 19 | end 20 | end 21 | 22 | it "stops when the value is nil" do 23 | result = FloodGate.new.basic(nil) 24 | expect(result).to fail_with(:not_found) 25 | end 26 | 27 | it "continues when the value is not nil" do 28 | result = FloodGate.new.basic(17) 29 | expect(result).to be_a_success 30 | expect(result.value).to eq 34 31 | end 32 | 33 | it "has an alias" do 34 | result = FloodGate.new.basic(17) 35 | expect(result).to be_a_success 36 | expect(result.value).to eq 34 37 | end 38 | 39 | it "allows a custom error" do 40 | result = FloodGate.new.custom_error(nil, :my_error) 41 | expect(result).to fail_with(:my_error) 42 | end 43 | end 44 | 45 | describe '#attempt' do 46 | class Bubble 47 | include SolidUseCase 48 | 49 | def pop1 50 | attempt { "pop!" } 51 | end 52 | 53 | def pop2 54 | attempt { raise NoMethodError.new("oops") } 55 | end 56 | end 57 | 58 | it "succeeds when no exceptions happen" do 59 | expect(Bubble.new.pop1).to be_a_success 60 | end 61 | 62 | it "catches exceptions" do 63 | result = Bubble.new.pop2 64 | expect(result).to_not be_a_success 65 | expect(result.value).to be_a NoMethodError 66 | end 67 | end 68 | 69 | end 70 | -------------------------------------------------------------------------------- /spec/either_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe SolidUseCase::Either do 4 | 5 | describe 'Stepping' do 6 | class GiantSteps 7 | include SolidUseCase 8 | 9 | def run(inputs) 10 | attempt_all do 11 | step { step_1(inputs) } 12 | step {|inputs| step_2(inputs) } 13 | end 14 | end 15 | 16 | def step_1(inputs) 17 | inputs[:number] += 10 18 | continue(inputs) 19 | end 20 | 21 | def step_2(inputs) 22 | inputs[:number] *= 2 23 | continue(inputs) 24 | end 25 | end 26 | 27 | it "pipes one step result to the next step" do 28 | result = GiantSteps.run(:number => 10) 29 | expect(result).to be_a_success 30 | expect(result.value[:number]).to eq(40) 31 | end 32 | end 33 | 34 | 35 | describe 'Stepping DSL' do 36 | class GiantStepsDSL 37 | include SolidUseCase 38 | 39 | steps :step_1, :step_2 40 | 41 | def step_1(inputs) 42 | inputs[:number] += 10 43 | continue(inputs) 44 | end 45 | 46 | def step_2(inputs) 47 | inputs[:number] *= 2 48 | continue(inputs) 49 | end 50 | end 51 | 52 | it "pipes one step result to the next step" do 53 | result = GiantStepsDSL.run(:number => 10) 54 | expect(result).to be_a_success 55 | expect(result.value[:number]).to eq(40) 56 | end 57 | 58 | it "can run multiple times" do 59 | result = GiantStepsDSL.run(:number => 10) 60 | result = GiantStepsDSL.run(:number => 10) 61 | expect(result).to be_a_success 62 | expect(result.value[:number]).to eq(40) 63 | end 64 | 65 | class SubStep 66 | include SolidUseCase 67 | steps GiantStepsDSL, :last_step 68 | 69 | def last_step(inputs) 70 | inputs[:number] += 1 71 | continue(inputs[:number]) 72 | end 73 | end 74 | 75 | it "pipes one step result to the next step" do 76 | result = SubStep.run(:number => 10) 77 | expect(result).to be_a_success 78 | expect(result.value).to eq(41) 79 | end 80 | 81 | class ShortCircuit 82 | include SolidUseCase 83 | steps :first, :second 84 | 85 | def first(inputs) 86 | fail :jump_out_yo 87 | end 88 | 89 | def second(inputs) 90 | throw "Should not reach this point" 91 | end 92 | end 93 | 94 | it "doesn't run the next step if a failure occures" do 95 | expect { ShortCircuit.run }.to_not raise_error 96 | end 97 | end 98 | 99 | 100 | describe 'Failure Matching' do 101 | class FailureMatch 102 | include SolidUseCase 103 | 104 | def run(inputs) 105 | attempt_all do 106 | step { fail_it(inputs) } 107 | end 108 | end 109 | 110 | def fail_it(inputs) 111 | error_sym = inputs[:fail_with] 112 | fail(error_sym) 113 | end 114 | end 115 | 116 | it "pattern matches" do 117 | result = FailureMatch.run(:fail_with => :abc) 118 | # Custom RSpec matcher 119 | expect(result).to_not be_a_success 120 | 121 | expect(result.value).to be_a SolidUseCase::Either::ErrorStruct 122 | expect(result.value.type).to eq :abc 123 | 124 | matched = false 125 | result.match do 126 | success { raise StandardError.new "We shouldn't get here" } 127 | failure(:xyz) { raise StandardError.new "We shouldn't get here" } 128 | failure(:abc) { matched = true } 129 | failure { raise StandardError.new "We shouldn't get here" } 130 | end 131 | expect(matched).to eq true 132 | end 133 | end 134 | 135 | describe 'Helpers' do 136 | class CheckEachHelper 137 | include SolidUseCase 138 | 139 | def success_1 140 | vals = [:x, :y] 141 | check_each(vals) {|v| v} 142 | end 143 | 144 | def success_2 145 | vals = [:x, :y] 146 | check_each(vals, continue_with: 999) {|v| v} 147 | end 148 | 149 | def failure_1(goods) 150 | vals = [5, 10, 0, 15] 151 | check_each(vals) do |val| 152 | if val != 0 153 | goods.push(val) 154 | else 155 | fail :zero 156 | end 157 | end 158 | end 159 | end 160 | 161 | it "checks an array" do 162 | result = CheckEachHelper.new.success_1 163 | expect(result).to be_a_success 164 | expect(result.value).to eq([:x, :y]) 165 | end 166 | 167 | it "continues with a value" do 168 | result = CheckEachHelper.new.success_2 169 | expect(result).to be_a_success 170 | expect(result.value).to eq(999) 171 | end 172 | 173 | it "fails on first" do 174 | goods = [] 175 | result = CheckEachHelper.new.failure_1(goods) 176 | expect(result).to fail_with(:zero) 177 | expect(goods).to eq([5, 10]) 178 | end 179 | end 180 | 181 | describe 'Literals' do 182 | it "creates a success literal" do 183 | s = SolidUseCase::Either.success(10) 184 | expect(s).to be_a_success 185 | expect(s.value).to eq 10 186 | end 187 | 188 | it "creates a failure literal" do 189 | f = SolidUseCase::Either.failure(:mock, x: 20) 190 | expect(f).to_not be_a_success 191 | expect(f).to fail_with :mock 192 | expect(f.value[:x]).to eq 20 193 | end 194 | end 195 | end 196 | -------------------------------------------------------------------------------- /spec/rspec_matchers_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe 'Custom RSpec Matchers' do 4 | include SolidUseCase::RSpecMatchers 5 | 6 | class FailCase 7 | include SolidUseCase 8 | def run(error) 9 | fail(error) 10 | end 11 | end 12 | 13 | class SuccessCase 14 | include SolidUseCase 15 | def run(val) 16 | continue(val) 17 | end 18 | end 19 | 20 | class ExceptionCase 21 | include SolidUseCase 22 | def run(val) 23 | attempt_all do 24 | try { raise_exception } 25 | end 26 | end 27 | 28 | def raise_exception 29 | raise NoMethodError.new 'oops' 30 | end 31 | end 32 | 33 | describe '#fail_with' do 34 | 35 | it "matches error messages" do 36 | matcher = fail_with(:xyz) 37 | expect(matcher.matches? FailCase.run(:xyz)).to eq(true) 38 | expect(matcher.matches? FailCase.run(:abc)).to eq(false) 39 | end 40 | 41 | it "does not match successes" do 42 | matcher = fail_with(:hello) 43 | expect(matcher.matches? SuccessCase.run).to eq(false) 44 | end 45 | end 46 | 47 | describe 'exception handling' do 48 | it "provides a proper error message for exceptions" do 49 | matcher = be_a_success 50 | expect(matcher.matches? ExceptionCase.run).to eq(false) 51 | 52 | expect(matcher.failure_message).to include('oops') 53 | expect(matcher.failure_message).to_not include( 54 | 'deterministic/either/attempt_all.rb', 55 | 'deterministic/core_ext/either.rb', 56 | 'lib/rspec/core/example.rb' 57 | ) 58 | # Useful for seeing the backtrace output yourself 59 | # expect(ExceptionCase.run).to be_a_success 60 | end 61 | end 62 | end 63 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | require 'solid_use_case' 2 | require 'solid_use_case/rspec_matchers' 3 | 4 | RSpec.configure do |config| 5 | config.include(SolidUseCase::RSpecMatchers) 6 | end 7 | 8 | RSpec.configure do |config| 9 | config.treat_symbols_as_metadata_keys_with_true_values = true 10 | config.run_all_when_everything_filtered = true 11 | config.filter_run :focus 12 | 13 | # Run specs in random order to surface order dependencies. If you find an 14 | # order dependency and want to debug it, you can fix the order by providing 15 | # the seed, which is printed after each run. 16 | # --seed 1234 17 | config.order = 'random' 18 | end 19 | --------------------------------------------------------------------------------