├── .gitignore ├── Gemfile ├── LICENSE ├── Rakefile ├── Readme.md ├── lib ├── use_case.rb └── use_case │ ├── outcome.rb │ ├── validator.rb │ └── version.rb ├── test.rb ├── test ├── sample_use_case.rb ├── test_helper.rb ├── use_case │ ├── outcome_test.rb │ └── validator_test.rb └── use_case_test.rb └── use_case.gemspec /.gitignore: -------------------------------------------------------------------------------- 1 | /Gemfile.lock 2 | coverage 3 | test/reports -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source "http://rubygems.org" 2 | gemspec 3 | 4 | # Only used for tests 5 | gem "virtus" 6 | 7 | # Validations are optional, but required in order to test UseCase 8 | # itself 9 | gem "activemodel" 10 | 11 | gem "ci_reporter" 12 | gem "simplecov" 13 | gem "simplecov-rcov" 14 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2013 Gitorious AS 2 | 3 | Christian Johansen (christian@gitorious.com) 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 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require "rake/testtask" 2 | require "ci/reporter/rake/minitest" 3 | 4 | Rake::TestTask.new(:test) do |test| 5 | test.libs << "test" 6 | test.pattern = "test/**/*_test.rb" 7 | end 8 | 9 | task :default => :test 10 | -------------------------------------------------------------------------------- /Readme.md: -------------------------------------------------------------------------------- 1 | # Use Case 2 | 3 | Compose non-trivial business logic into use cases, that combine: 4 | 5 | * Input parameter abstractions; type safety and coercion, and white-listing of 6 | supported input for any given operation 7 | * Pre-conditions: System-level conditions that must be met, e.g. "a user must be 8 | logged in" etc. 9 | * Input parameter validation: ActiveRecord-like validations as composable 10 | objects. Combine specific sets of validation rules for the same input in 11 | different contexts etc. 12 | * Commands: Avoid defensive coding by performing the core actions in commands 13 | that receive type-converted input, and are only executed when pre-conditions 14 | are met and input is validated. 15 | 16 | ## Example 17 | 18 | `UseCase` is designed to break up and keep non-trivial workflows understandable 19 | and decoupled. As such, a trivial example would not illustrate what is good 20 | about it. The following example is simplified, yet still has enough aspects to 21 | show how `UseCase` helps you break things up. 22 | 23 | Pre-conditions are conditions not directly related to input parameters alone, 24 | and whose failure signifies other forms of errors than simple validation errors. 25 | If you have a Rails application that uses controller filters, then those are 26 | very likely good candidates for pre-conditions. 27 | 28 | The following example is a simplified use case from 29 | [Gitorious](http://gitorious.org) where we want to create a new repository. To 30 | do this, we need a user that can admin the project under which we want the new 31 | repository to live. 32 | 33 | _NB!_ This example illustrates how to solve common design challenges in Rails 34 | applications; that does not mean that `UseCase` is only useful to Rails 35 | applications. 36 | 37 | First, let's look at what your Rails controller will look like using a 38 | `UseCase`: 39 | 40 | ```rb 41 | class RepositoryController < ApplicationController 42 | include Gitorious::Authorization # Adds stuff like can_admin?(actor, thing) 43 | 44 | # ... 45 | 46 | def create 47 | outcome = CreateRepository.new(self, current_user).execute(params) 48 | 49 | outcome.pre_condition_failed do |f| 50 | f.when(:user_required) { redirect_to(login_path) } 51 | f.otherwise do 52 | flash[:error] = "You're not allowed to do that" 53 | redirect_to project_path 54 | end 55 | end 56 | 57 | outcome.failure do |model| 58 | # Render form with validation errors 59 | render :new, :locals => { :repository => model } 60 | end 61 | 62 | outcome.success do |repository| 63 | redirect_to(repository_path(repository)) 64 | end 65 | end 66 | end 67 | ``` 68 | 69 | Executing the use case in an `irb` session could look like this: 70 | 71 | ```rb 72 | include Gitorious::Authorization 73 | user = User.find_by_login("christian") 74 | project = Project.find_by_name("gitorious") 75 | outcome = CreateRepository.new(self, user).execute(:project => project, 76 | :name => "use_case") 77 | outcome.success? #=> true 78 | outcome.result.name #=> "use_case" 79 | ``` 80 | 81 | The code behind this use case follows: 82 | 83 | ```rb 84 | require "use_case" 85 | require "virtus" 86 | 87 | # Input parameters can be sanitized and pre-processed any way you like. One nice 88 | # way to go about it is to use Datamapper 2's Virtus gem to define a parameter 89 | # set. 90 | # 91 | # This class uses Project.find to look up a project by id if project_id is 92 | # provided and project is not. This is the only class that directly touches 93 | # classes from the Rails application. 94 | class NewRepositoryInput 95 | include Virtus.model 96 | attribute :name, String 97 | attribute :description, String 98 | attribute :project, Project 99 | attribute :project_id, Integer 100 | 101 | def project 102 | @project ||= Project.find(@project_id) 103 | end 104 | end 105 | 106 | # Validate new repositories. Extremely simplified example. 107 | NewRepositoryValidator = UseCase::Validator.define do 108 | validates_presence_of :name, :project 109 | end 110 | 111 | # This is often implemented as a controller filter in many Rails apps. 112 | # Unfortunately that means we have to duplicate the check when exposing the use 113 | # case in other contexts (e.g. a stand-alone API app, console API etc). 114 | class UserRequired 115 | # The constructor is only used by us and can look and do whever we want 116 | def initialize(user) 117 | @user = user 118 | end 119 | 120 | # A pre-condition must define this method 121 | # Params is an instance of NewRepositoryInput 122 | def satisfied?(params) 123 | !@user.nil? 124 | end 125 | end 126 | 127 | # Another pre-condition that uses app-wide state 128 | class ProjectAdminPrecondition 129 | def initialize(auth, user) 130 | @auth = auth 131 | @user = user 132 | end 133 | 134 | def satisfied?(params) 135 | @auth.can_admin?(@user, params.project) 136 | end 137 | end 138 | 139 | # The business logic. Here we can safely assume that all pre-conditions are 140 | # satisfied, and that input is valid and has the correct type. 141 | class CreateRepositoryCommand 142 | def initialize(user) 143 | @user = user 144 | end 145 | 146 | # Params is an instance of NewRepositoryInput 147 | def execute(params) 148 | params.project.repositories.create(:name => params.name, :user => @user) 149 | end 150 | end 151 | 152 | # The UseCase - this is just wiring together the various classes 153 | class CreateRepository 154 | include UseCase 155 | 156 | # There's no contract to satiesfy with the constructor - design it to receive 157 | # any dependencies you need. 158 | def initialize(auth, user) 159 | input_class(NewRepositoryInput) 160 | add_pre_condition(UserLoggedInPrecondition.new(user)) 161 | add_pre_condition(ProjectAdminPrecondition.new(auth, user)) 162 | # A step is comprised of a command with 0, 1 or many validators 163 | # (e.g. :validators => [...]) 164 | # The use case can span multiple steps (see below) 165 | step(CreateRepositoryCommand.new(user), :validator => NewRepositoryValidator) 166 | end 167 | end 168 | ``` 169 | 170 | ## The use case pipeline at a glance 171 | 172 | This is the high-level overview of how `UseCase` strings up a pipeline 173 | for you to plug in various kinds of business logic: 174 | 175 | ``` 176 | User input (-> input sanitation) (-> pre-conditions) -> steps 177 | ``` 178 | 179 | 1. Start with a hash of user input 180 | 2. Optionally wrap this in an object that performs type-coercion, 181 | enforces types etc. 182 | 3. Optionally run pre-conditions on the santized input 183 | 4. Execute steps. The initial step is fed the sanitized input, each 184 | following command is fed the result from the previous step. 185 | 186 | Each step is a pipeline in its own right: 187 | 188 | ``` 189 | Step: (-> builder) (-> validations) -> command 190 | ``` 191 | 192 | 1. Optionally refine input by running it through a pre-execution "builder" 193 | 2. Optionally run (refined) input through one or more validators 194 | 3. Execute command with (refined) input 195 | 196 | ## Input sanitation 197 | 198 | In your `UseCase` instance (typically in the constructor), you can call the 199 | `input_class` method to specify which class is used to santize inputs. If you do 200 | not use this, inputs are forwarded to pre-conditions and commands untouched 201 | (i.e. as a `Hash`). 202 | 203 | Datamapper 2's [Virtus](https://github.com/solnic/virtus) is a very promising 204 | solution for input sanitation and some level of type-safety. If you provide a 205 | `Virtus` backed class as `input_class` you will get an instance of that class as 206 | `params` in pre-conditions and commands. 207 | 208 | ## Pre-conditions 209 | 210 | A pre-condition is any object that responds to `satisfied?(params)` where params 211 | will either be a `Hash` or an instance of whatever you passed to `input_class`. 212 | The method should return `true/false`. If it raises, the outcome of the use case 213 | will call the `pre_condition_failed` block with the raised error. If it fails, 214 | the `pre_condition_failed` block will be called with a failure object wrapping 215 | the pre-condition instance that failed. 216 | 217 | The wrapper failure object provides three methods of interest: 218 | 219 | ### `when` 220 | 221 | The when method allows you to associate a block of code with a specific 222 | pre-condition. The block is called with the pre-condition instance if that 223 | pre-condition fails. Because the pre-condition class may not be explicitly 224 | available in contexts where you want to use `when`, a symbolic representation is 225 | used. 226 | 227 | If you have the following two pre-conditions: 228 | 229 | * `UserRequired` 230 | * `ProjectAdminRequired` 231 | 232 | Then you can use `when(:user_required) { |condition ... }` and 233 | `when(:project_admin_required) { |condition ... }`. If you want control over how 234 | a class name is symbolized, make the class implement `symbol`, i.e.: 235 | 236 | ```js 237 | class UserRequired 238 | def self.symbol; :user_plz; end 239 | def initialize(user); @user = user; end 240 | def satisfied?(params); !@user.nil?; end 241 | end 242 | 243 | # Then: 244 | 245 | outcome = use_case.execute(params) 246 | 247 | outcome.pre_condition_failed do |f| 248 | f.when(:user_plz) { |c| puts "Needs moar user" } 249 | # ... 250 | end 251 | ``` 252 | 253 | ### `otherwise` 254 | 255 | `otherwise` is a catch-all that is called if no calls to `when` mention the 256 | offending pre-condition: 257 | 258 | 259 | ```js 260 | class CreateProject 261 | include UseCase 262 | 263 | def initialize(user) 264 | add_pre_condition(UserRequired.new(user)) 265 | add_pre_condition(AdminRequired.new(user)) 266 | step(CreateProjectCommand.new(user)) 267 | end 268 | end 269 | 270 | # Then: 271 | 272 | outcome = CreateProject.new(current_user).execute(params) 273 | 274 | outcome.pre_condition_failed do |f| 275 | f.when(:user_required) { |c| puts "Needs moar user" } 276 | f.otherwise { |c| puts "#{c.name} pre-condition failed" } 277 | end 278 | ``` 279 | ### `pre_condition` 280 | 281 | If you want to roll your own flow control, simply get the offending 282 | pre-condition from this method. 283 | 284 | ## Validations 285 | 286 | The validator uses `ActiveModel::Validations`, so any Rails validation can go in 287 | here (except for `validates_uniqueness_of`, which apparently comes from 288 | elsewhere - see example below for how to work around this). The main difference 289 | is that the validator is created as a stand-alone object that can be used with 290 | any model instance. This design allows you to define multiple context-sensitive 291 | validations for a single object. 292 | 293 | You can of course provide your own validation if you want - any object that 294 | defines `call(object)` and returns something that responds to `valid?` is good. 295 | I am following the Datamapper2 project closely in this area. 296 | 297 | Because `UseCase::Validation` is not a required part of `UseCase`, and people 298 | may want to control their own dependencies, `activemodel` is _not_ a hard 299 | dependency. To use this feature, `gem install activemodel`. 300 | 301 | ## Builders 302 | 303 | When user input has passed input sanitation and pre-conditions have been 304 | satisfied, you can optionally pipe input through a "builder" before handing it 305 | over to validations and a command. 306 | 307 | The builder should be an object with a `build` or a `call` method (if it has 308 | both, `build` will be preferred). The method will be called with santized input. 309 | The return value will be passed on to validators and the commands. 310 | 311 | Builders can be useful if you want to run validations on a domain object rather 312 | than directly on "dumb" input. 313 | 314 | ### Example 315 | 316 | In a Rails application, the builder is useful to wrap user input in an unsaved 317 | `ActiveRecord` instance. The unsaved object will be run through the validators, 318 | and (if found valid), the command can save it and perform additional tasks that 319 | you possibly do with `ActiveRecord` observers now. 320 | 321 | This example also shows how to express uniqueness validators when you move 322 | validations out of your `ActiveRecord` models. 323 | 324 | ```rb 325 | require "activemodel" 326 | require "virtus" 327 | require "use_case" 328 | 329 | class User < ActiveRecord::Base 330 | def uniq? 331 | user = User.where("lower(name) = ?", name).first 332 | user.nil? || user == self 333 | end 334 | end 335 | 336 | UserValidator = UseCase::Validator.define do 337 | validates_presence_of :name 338 | validate :uniqueness 339 | 340 | def uniqueness 341 | errors.add(:name, "is taken") if !uniq? 342 | end 343 | end 344 | 345 | class NewUserInput 346 | include Virtus.model 347 | attribute :name, String 348 | end 349 | 350 | class NewUserCommand 351 | def execute(user) 352 | user.save! 353 | Mailer.user_signup(user).deliver 354 | user 355 | end 356 | 357 | def build(params) 358 | User.new(:name => params.name) 359 | end 360 | end 361 | 362 | class CreateUser 363 | include UseCase 364 | 365 | def initialize 366 | input_class(NewUserInput) 367 | cmd = NewUserCommand.new 368 | # Use the command as a builder too 369 | step(cmd, :builder => cmd, :validator => UserValidator) 370 | end 371 | end 372 | 373 | # Usage: 374 | outcome = CreateUser.new.execute(:name => "Chris") 375 | outcome.success? #=> true 376 | outcome.result #=> # 377 | ``` 378 | 379 | If the command fails to execute due to validation errors, using the builder 380 | allows us to access the partial object for re-rendering forms etc. Because this 381 | is such a common scenario, the command will automatically be used as the builder 382 | as well if there is no explicit `:builder` option, and the command responds to 383 | `build`. This means that the command in the previous example could be written as 384 | so: 385 | 386 | ```rb 387 | class CreateUser 388 | include UseCase 389 | 390 | def initialize 391 | input_class(NewUserInput) 392 | step(NewUserCommand.new, :validator => UserValidator) 393 | end 394 | end 395 | ``` 396 | 397 | When calling `execute` on this use case, we can observe the following flow: 398 | 399 | ```rb 400 | # This 401 | params = { :name => "Dude" } 402 | CreateUser.new.execute(params) 403 | 404 | # ...roughly expands to: 405 | # (command is the command instance wired in the use case constructor) 406 | input = NewUserInput.new(params) 407 | prepared = command.build(input) 408 | 409 | if UserValidator.call(prepared).valid? 410 | command.execute(prepared) 411 | end 412 | ``` 413 | 414 | ### Note 415 | 416 | I'm not thrilled by `builder` as a name/concept. Suggestions for a better name 417 | is welcome. 418 | 419 | ## Commands 420 | 421 | A command is any Ruby object that defines an `execute(params)` method. 422 | Alternately, it can be an object that responds to `call` (e.g. a lambda). Its 423 | return value will be passed to the outcome's `success` block. Any errors raised 424 | by this method is not rescued, so be sure to wrap `use_case.execute(params)` in 425 | a rescue block if you're worried that it raises. Better yet, detect known causes 426 | of exceptions in a pre-condition so you know that the command does not raise. 427 | 428 | If the command responds to the `build` message and there is no explicitly 429 | configured `:builder` for the current step, the command is also used as a 430 | builder (see example above, under "Builders"). 431 | 432 | ## Use cases 433 | 434 | A use case simply glues together all the components. Define a class, include 435 | `UseCase`, and configure the instance in the constructor. The constructor can 436 | take any arguments you like, making this solution suitable for DI (dependency 437 | injection) style designs. 438 | 439 | The use case can optionally call `input_class` once, `add_pre_condition` 440 | multiple times, and `step` multiple times. 441 | 442 | When using multiple steps, input sanitation with the `input_class` is 443 | performed once only. Pre-conditions are also only checked once - before any 444 | steps are executed. The use case will then execute the steps: 445 | 446 | ``` 447 | step_1: sanitizied_input -> (builder ->) (validators ->) command 448 | step_n: command_n-1 result -> (builder ->) (validators ->) command 449 | ``` 450 | 451 | In other words, all commands except the first one will be executed with the 452 | result of the previous command as input. 453 | 454 | ## Outcomes 455 | 456 | `UseCase#execute` returns an `Outcome`. You can use the outcome in primarily two 457 | ways. The primary approach is one that takes blocks for the three situations: 458 | `success(&block)`, `failure(&block)`, and `pre_condition_failed(&block)`. Only 459 | one of these will ever be called. This allows you to declaratively describe 460 | further flow in your program. 461 | 462 | For use on the console and other situations, this style is not the most 463 | convenient. For that reason each of the three methods above can also be called 464 | without a block, and they always return something: 465 | 466 | * `success` returns the command result 467 | * `failure` returns the validation object (e.g. `failure.errors.inspect`) 468 | * `pre_condition_failed` returns the pre-condition that failed, *or* an 469 | exception object, if a pre-condition raised an exception. 470 | 471 | In addition to these, the outcome object responds to `success?` and 472 | `pre_condition_failed?`. 473 | 474 | ## Inspiration and design considerations 475 | 476 | This small library is very much inspired by 477 | [Mutations](http://github.com/cypriss/mutations). Nice as it is, I found it to 478 | be a little limiting in terms of what kinds of commands it could comfortably 479 | encapsulate. Treating everything as a hash of inputs makes it hard to do things 480 | like "redirect if there's no user, render form if there are validation errors 481 | and redirect to new object if successful". 482 | 483 | As I started working on my own solution I quickly recognized the power in 484 | separating input parameter type constraints/coercions from validation rules. 485 | This is another area where UseCase differs from Mutations. UseCase is probably 486 | slightly more "enterprise" than Mutations, but fits the kinds of problems I 487 | intend to solve with it better than Mutations did. 488 | 489 | ## Testing 490 | 491 | Using UseCase will allow you to test almost all logic completely without loading 492 | Rails. In the example above, the input conversion is the only place that 493 | directly touches any classes from the Rails application. The rest of the classes 494 | work by the "data in, data out" principle, meaning you can easily test them with 495 | any kind of object (which spares you of loading heavy ActiveRecord-bound models, 496 | running opaque controller tets etc). 497 | 498 | ## Installation 499 | 500 | $ gem install use_case 501 | 502 | ## Developing 503 | 504 | $ bundle install 505 | $ rake 506 | 507 | ## Contributing 508 | 509 | * Clone repo 510 | * Make changes 511 | * Add test(s) 512 | * Run tests 513 | * If adding new abilities, add docs in Readme, or commit a working example 514 | * Send patch, [pull request](http://github.com/cjohansen/use_case) or [merge request](http://gitorious.org/gitorious/use_case) 515 | 516 | If you intend to add entirely new features, you might want to open an issue to 517 | discuss it with me first. 518 | 519 | ## License 520 | 521 | UseCase is free software licensed under the MIT license. 522 | 523 | ``` 524 | The MIT License (MIT) 525 | 526 | Copyright (C) 2013 Gitorious AS 527 | 528 | Permission is hereby granted, free of charge, to any person obtaining a copy 529 | of this software and associated documentation files (the "Software"), to deal 530 | in the Software without restriction, including without limitation the rights 531 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 532 | copies of the Software, and to permit persons to whom the Software is 533 | furnished to do so, subject to the following conditions: 534 | 535 | The above copyright notice and this permission notice shall be included in all 536 | copies or substantial portions of the Software. 537 | 538 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 539 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 540 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 541 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 542 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 543 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 544 | SOFTWARE. 545 | ``` 546 | -------------------------------------------------------------------------------- /lib/use_case.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | # -- 3 | # The MIT License (MIT) 4 | # 5 | # Copyright (C) 2013 Gitorious AS 6 | # 7 | # Permission is hereby granted, free of charge, to any person obtaining a copy 8 | # of this software and associated documentation files (the "Software"), to deal 9 | # in the Software without restriction, including without limitation the rights 10 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | # copies of the Software, and to permit persons to whom the Software is 12 | # furnished to do so, subject to the following conditions: 13 | # 14 | # The above copyright notice and this permission notice shall be included in all 15 | # copies or substantial portions of the Software. 16 | # 17 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 23 | # SOFTWARE. 24 | #++ 25 | require "use_case/outcome" 26 | require "use_case/validator" 27 | require "ostruct" 28 | 29 | module UseCase 30 | def input_class(input_class) 31 | @input_class = input_class 32 | end 33 | 34 | def add_pre_condition(pc) 35 | pre_conditions << pc 36 | end 37 | 38 | def step(command, options = {}) 39 | @steps ||= [] 40 | @steps << { 41 | :command => command, 42 | :builder => options[:builder], 43 | :validators => Array(options[:validators] || options[:validator]) 44 | } 45 | end 46 | 47 | def execute(params = {}) 48 | input = @input_class && @input_class.new(params) || params 49 | 50 | if outcome = verify_pre_conditions(input) 51 | return outcome 52 | end 53 | 54 | execute_steps(@steps, input) 55 | end 56 | 57 | private 58 | def execute_steps(steps, params) 59 | result = steps.inject(params) do |input, step| 60 | input = prepare_input(input, step) 61 | 62 | if outcome = validate_params(input, step[:validators]) 63 | return outcome 64 | end 65 | 66 | if step[:command].respond_to?(:execute) 67 | step[:command].execute(input) 68 | else 69 | step[:command].call(input) 70 | end 71 | end 72 | 73 | SuccessfulOutcome.new(result) 74 | end 75 | 76 | def verify_pre_conditions(input) 77 | pre_conditions.each do |pc| 78 | return PreConditionFailed.new(pc) if !pc.satisfied?(input) 79 | end 80 | nil 81 | end 82 | 83 | def prepare_input(input, step) 84 | builder = step[:builder] || step[:command] 85 | return builder.build(input) if builder.respond_to?(:build) 86 | return step[:builder].call(input) if step[:builder] 87 | input 88 | end 89 | 90 | def validate_params(input, validators) 91 | validators.each do |validator| 92 | result = validator.call(input) 93 | return FailedOutcome.new(result) if !result.valid? 94 | end 95 | nil 96 | end 97 | 98 | def pre_conditions; @pre_conditions ||= []; end 99 | end 100 | -------------------------------------------------------------------------------- /lib/use_case/outcome.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | # -- 3 | # The MIT License (MIT) 4 | # 5 | # Copyright (C) 2013 Gitorious AS 6 | # 7 | # Permission is hereby granted, free of charge, to any person obtaining a copy 8 | # of this software and associated documentation files (the "Software"), to deal 9 | # in the Software without restriction, including without limitation the rights 10 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | # copies of the Software, and to permit persons to whom the Software is 12 | # furnished to do so, subject to the following conditions: 13 | # 14 | # The above copyright notice and this permission notice shall be included in all 15 | # copies or substantial portions of the Software. 16 | # 17 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 23 | # SOFTWARE. 24 | #++ 25 | 26 | module UseCase 27 | class Outcome 28 | def pre_condition_failed?; false; end 29 | def success?; false; end 30 | def success; end 31 | def result; end 32 | def pre_condition_failed; end 33 | def failure; end 34 | end 35 | 36 | class SuccessfulOutcome < Outcome 37 | def initialize(result = nil) 38 | @result = result 39 | end 40 | 41 | def success?; true; end 42 | 43 | def success 44 | yield @result if block_given? 45 | @result 46 | end 47 | 48 | def result; @result; end 49 | 50 | def to_s 51 | "#" 52 | end 53 | end 54 | 55 | class PreConditionFailed < Outcome 56 | def initialize(pre_condition = nil) 57 | @pre_condition = pre_condition 58 | @failure = PreConditionFailure.new(@pre_condition) 59 | end 60 | 61 | def pre_condition_failed?; true; end 62 | 63 | def pre_condition_failed 64 | yield @failure if block_given? 65 | @failure 66 | end 67 | 68 | def to_s 69 | "#" 70 | end 71 | end 72 | 73 | class PreConditionFailure 74 | attr_reader :pre_condition 75 | def initialize(pre_condition); @pre_condition = pre_condition; end 76 | 77 | def when(symbol, &block) 78 | raise Exception.new("Cannot call when after otherwise") if @otherwise 79 | if symbol == self.symbol 80 | @called = true 81 | yield(@pre_condition) 82 | end 83 | end 84 | 85 | def otherwise(&block) 86 | @otherwise = true 87 | yield(@pre_condition) if !@called 88 | end 89 | 90 | def symbol 91 | return @pre_condition.symbol if @pre_condition.respond_to?(:symbol) 92 | klass = @pre_condition.class 93 | return klass.symbol if klass.respond_to?(:symbol) 94 | klass.name.gsub(/([^A-Z])([A-Z])/, '\1_\2').gsub(/[:_]+/, "_").downcase.to_sym 95 | end 96 | end 97 | 98 | class FailedOutcome < Outcome 99 | attr_reader :input 100 | 101 | def initialize(errors = nil) 102 | @errors = errors 103 | end 104 | 105 | def failure 106 | yield @errors if block_given? 107 | @errors 108 | end 109 | 110 | def to_s 111 | "#" 112 | end 113 | end 114 | end 115 | -------------------------------------------------------------------------------- /lib/use_case/validator.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | # -- 3 | # The MIT License (MIT) 4 | # 5 | # Copyright (C) 2013 Gitorious AS 6 | # 7 | # Permission is hereby granted, free of charge, to any person obtaining a copy 8 | # of this software and associated documentation files (the "Software"), to deal 9 | # in the Software without restriction, including without limitation the rights 10 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | # copies of the Software, and to permit persons to whom the Software is 12 | # furnished to do so, subject to the following conditions: 13 | # 14 | # The above copyright notice and this permission notice shall be included in all 15 | # copies or substantial portions of the Software. 16 | # 17 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 23 | # SOFTWARE. 24 | #++ 25 | require "active_model" 26 | 27 | module UseCase 28 | module Validator 29 | def self.define(&block) 30 | klass = Class.new do 31 | include ActiveModel::Validations 32 | 33 | def initialize(target) 34 | @target = target 35 | end 36 | 37 | def method_missing(name, *args, &block) 38 | @target.send(name, *args, &block) 39 | end 40 | 41 | def respond_to_missing?(name, include_all) 42 | @target.respond_to?(name) 43 | end 44 | 45 | def self.call(object) 46 | validator = new(object) 47 | validator.valid? 48 | validator 49 | end 50 | end 51 | 52 | klass.class_eval(&block) 53 | klass 54 | end 55 | end 56 | end 57 | -------------------------------------------------------------------------------- /lib/use_case/version.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | # -- 3 | # The MIT License (MIT) 4 | # 5 | # Copyright (C) 2013 Gitorious AS 6 | # 7 | # Permission is hereby granted, free of charge, to any person obtaining a copy 8 | # of this software and associated documentation files (the "Software"), to deal 9 | # in the Software without restriction, including without limitation the rights 10 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | # copies of the Software, and to permit persons to whom the Software is 12 | # furnished to do so, subject to the following conditions: 13 | # 14 | # The above copyright notice and this permission notice shall be included in all 15 | # copies or substantial portions of the Software. 16 | # 17 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 23 | # SOFTWARE. 24 | #++ 25 | 26 | module UseCase 27 | VERSION = "1.0.2" 28 | end 29 | -------------------------------------------------------------------------------- /test.rb: -------------------------------------------------------------------------------- 1 | require "virtus" 2 | require "use_case" 3 | 4 | class Project 5 | attr_reader :title 6 | def initialize(title); @title = title; end 7 | def self.find(id); new(" My name is #{id}"); end 8 | end 9 | 10 | class User 11 | attr_reader :name 12 | def initialize(name); @name = name; end 13 | def self.find(id); new(" My name is #{id}"); end 14 | end 15 | 16 | class NewRepositoryInput 17 | include Virtus.model 18 | 19 | attribute :name, String 20 | attribute :description, String 21 | attribute :merge_requests_enabled, Boolean, :default => true 22 | attribute :private_repository, Boolean, :default => true 23 | 24 | attribute :user, User 25 | attribute :user_id, Integer 26 | attribute :project, Project 27 | attribute :project_id, Integer 28 | 29 | def project; @project ||= Project.find(@project_id); end 30 | def user; @user ||= User.find(@user_id); end 31 | end 32 | 33 | class NewRepositoryValidator 34 | include UseCase::Validator 35 | validates_presence_of :name, :description, :merge_requests_enabled, :private_repository 36 | end 37 | 38 | class UserLoggedInPrecondition 39 | def initialize(user) 40 | @user = user 41 | end 42 | 43 | def satisfied?(params) 44 | @user[:id] == 42 45 | end 46 | end 47 | 48 | class ProjectAdminPrecondition 49 | def initialize(user) 50 | @user = user 51 | end 52 | 53 | def satisfied?(params) 54 | @user[:name] == params.name 55 | end 56 | end 57 | 58 | class CreateRepositoryCommand 59 | def initialize(user) 60 | @user = user 61 | end 62 | 63 | def execute(params) 64 | @user.merge(params) 65 | end 66 | end 67 | 68 | class CreateRepository 69 | include UseCase 70 | 71 | def initialize(user) 72 | input_class(NewRepositoryInput) 73 | pre_condition(UserLoggedInPrecondition.new(user)) 74 | pre_condition(ProjectAdminPrecondition.new(user)) 75 | validator(NewRepositoryValidator) 76 | command(CreateRepositoryCommand.new(user)) 77 | end 78 | end 79 | 80 | ### Example 81 | 82 | outcome = CreateRepository.new({ :id => 42, :name => "Boy" }).execute({ :name => "Boy" }) 83 | 84 | outcome.precondition_failed do |pc| 85 | puts "Pre-condition failed! #{pc}" 86 | end 87 | 88 | outcome.success do |result| 89 | puts "Your request was successful! #{result}" 90 | end 91 | 92 | outcome.failure do |errors| 93 | puts "There was a failure #{errors}" 94 | end 95 | 96 | puts outcome.to_s 97 | -------------------------------------------------------------------------------- /test/sample_use_case.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | # -- 3 | # The MIT License (MIT) 4 | # 5 | # Copyright (C) 2013 Gitorious AS 6 | # 7 | # Permission is hereby granted, free of charge, to any person obtaining a copy 8 | # of this software and associated documentation files (the "Software"), to deal 9 | # in the Software without restriction, including without limitation the rights 10 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | # copies of the Software, and to permit persons to whom the Software is 12 | # furnished to do so, subject to the following conditions: 13 | # 14 | # The above copyright notice and this permission notice shall be included in all 15 | # copies or substantial portions of the Software. 16 | # 17 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 23 | # SOFTWARE. 24 | #++ 25 | require "virtus" 26 | require "use_case" 27 | 28 | class Model 29 | attr_accessor :id, :name 30 | 31 | def initialize(id, name) 32 | @id = id 33 | @name = name 34 | end 35 | 36 | def to_s; "#<#{self.class.name}[id: #{id}, name: #{name}]>"; end 37 | def self.find(id); new(id, "From #{self.class.name}.find"); end 38 | end 39 | 40 | class Project < Model; end 41 | class Repository < Model; end 42 | 43 | class User < Model 44 | def can_admin?; @can_admin; end 45 | def can_admin=(ca); @can_admin = ca; end 46 | end 47 | 48 | class NewRepositoryInput 49 | include Virtus.model 50 | attribute :name, String 51 | end 52 | 53 | NewRepositoryValidator = UseCase::Validator.define do 54 | validates_presence_of :name 55 | end 56 | 57 | class UserLoggedInPrecondition 58 | def initialize(user); @user = user; end 59 | def satisfied?(params); @user && @user.id == 42; end 60 | end 61 | 62 | class ProjectAdminPrecondition 63 | def initialize(user); @user = user; end 64 | def satisfied?(params); @user.can_admin?; end 65 | end 66 | 67 | class CreateRepositoryCommand 68 | def initialize(user); @user = user; end 69 | def execute(params); Repository.new(1349, params.name); end 70 | end 71 | 72 | class CreateRepository 73 | include UseCase 74 | 75 | def initialize(user) 76 | input_class(NewRepositoryInput) 77 | add_pre_condition(UserLoggedInPrecondition.new(user)) 78 | add_pre_condition(ProjectAdminPrecondition.new(user)) 79 | step(CreateRepositoryCommand.new(user), :validators => NewRepositoryValidator) 80 | end 81 | end 82 | 83 | class ExplodingRepository 84 | include UseCase 85 | 86 | def initialize(user) 87 | cmd = CreateRepositoryCommand.new(user) 88 | def cmd.execute(params); raise "Crash!"; end 89 | step(cmd) 90 | end 91 | end 92 | 93 | class RepositoryBuilder 94 | attr_reader :name 95 | def initialize(name); @name = name; end 96 | def self.build(params) 97 | return new(nil) if params[:name] == "invalid" 98 | new(params[:name] + "!") 99 | end 100 | end 101 | 102 | class CreateRepositoryWithBuilder 103 | include UseCase 104 | 105 | def initialize(user) 106 | input_class(NewRepositoryInput) 107 | step(CreateRepositoryCommand.new(user), { 108 | :validators => NewRepositoryValidator, 109 | :builder => RepositoryBuilder 110 | }) 111 | end 112 | end 113 | 114 | class CreateRepositoryWithExplodingBuilder 115 | include UseCase 116 | 117 | def initialize(user) 118 | input_class(NewRepositoryInput) 119 | step(CreateRepositoryCommand.new(user), :builder => self) 120 | end 121 | 122 | def build(params); raise "Oops"; end 123 | end 124 | 125 | class PimpRepositoryCommand 126 | def execute(repository) 127 | repository.name += " (Pimped)" 128 | repository 129 | end 130 | end 131 | 132 | class PimpRepositoryCommandWithBuilder 133 | def build(repository) 134 | repository.id = 42 135 | repository 136 | end 137 | 138 | def execute(repository) 139 | repository.name += " (Pimped)" 140 | repository 141 | end 142 | end 143 | 144 | class CreatePimpedRepository 145 | include UseCase 146 | 147 | def initialize(user) 148 | input_class(NewRepositoryInput) 149 | step(CreateRepositoryCommand.new(user)) 150 | step(PimpRepositoryCommand.new) 151 | end 152 | end 153 | 154 | class CreatePimpedRepository2 155 | include UseCase 156 | 157 | def initialize(user) 158 | input_class(NewRepositoryInput) 159 | step(CreateRepositoryCommand.new(user), :builder => RepositoryBuilder) 160 | cmd = PimpRepositoryCommandWithBuilder.new 161 | step(cmd, :builder => cmd) 162 | end 163 | end 164 | 165 | PimpedRepositoryValidator = UseCase::Validator.define do 166 | validate :cannot_win 167 | def cannot_win; errors.add(:name, "You cannot win"); end 168 | end 169 | 170 | class CreatePimpedRepository3 171 | include UseCase 172 | 173 | def initialize(user) 174 | input_class(NewRepositoryInput) 175 | cmd = PimpRepositoryCommandWithBuilder.new 176 | step(CreateRepositoryCommand.new(user), :builder => RepositoryBuilder, :validator => NewRepositoryValidator) 177 | step(cmd, :builder => cmd, :validators => [NewRepositoryValidator, PimpedRepositoryValidator]) 178 | end 179 | end 180 | 181 | class InlineCommand 182 | include UseCase 183 | 184 | def initialize 185 | step(lambda { |params| params[:name] }) 186 | end 187 | end 188 | 189 | class ImplicitBuilder 190 | include UseCase 191 | 192 | def initialize(user) 193 | input_class(NewRepositoryInput) 194 | step(CreateRepositoryCommand.new(user), :builder => RepositoryBuilder) 195 | step(PimpRepositoryCommandWithBuilder.new) 196 | end 197 | end 198 | -------------------------------------------------------------------------------- /test/test_helper.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | # -- 3 | # The MIT License (MIT) 4 | # 5 | # Copyright (C) 2013 Gitorious AS 6 | # 7 | # Permission is hereby granted, free of charge, to any person obtaining a copy 8 | # of this software and associated documentation files (the "Software"), to deal 9 | # in the Software without restriction, including without limitation the rights 10 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | # copies of the Software, and to permit persons to whom the Software is 12 | # furnished to do so, subject to the following conditions: 13 | # 14 | # The above copyright notice and this permission notice shall be included in all 15 | # copies or substantial portions of the Software. 16 | # 17 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 23 | # SOFTWARE. 24 | #++ 25 | require "simplecov" 26 | require "simplecov-rcov" 27 | SimpleCov.formatter = SimpleCov::Formatter::RcovFormatter 28 | SimpleCov.start 29 | 30 | require "bundler/setup" 31 | require "minitest/autorun" 32 | 33 | Bundler.require(:default, :test) 34 | -------------------------------------------------------------------------------- /test/use_case/outcome_test.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | # -- 3 | # The MIT License (MIT) 4 | # 5 | # Copyright (C) 2013 Gitorious AS 6 | # 7 | # Permission is hereby granted, free of charge, to any person obtaining a copy 8 | # of this software and associated documentation files (the "Software"), to deal 9 | # in the Software without restriction, including without limitation the rights 10 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | # copies of the Software, and to permit persons to whom the Software is 12 | # furnished to do so, subject to the following conditions: 13 | # 14 | # The above copyright notice and this permission notice shall be included in all 15 | # copies or substantial portions of the Software. 16 | # 17 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 23 | # SOFTWARE. 24 | #++ 25 | require "test_helper" 26 | require "use_case/outcome" 27 | 28 | class MyPreCondition 29 | def self.symbol; :something; end 30 | end 31 | 32 | describe UseCase::Outcome do 33 | it "defaults to not failing and not being successful (noop)" do 34 | outcome = UseCase::Outcome.new 35 | outcome.success { fail "Shouldn't succeed" } 36 | outcome.pre_condition_failed { fail "Shouldn't have failed pre-conditions" } 37 | outcome.failure { fail "Shouldn't fail" } 38 | 39 | refute outcome.pre_condition_failed? 40 | refute outcome.success? 41 | end 42 | 43 | describe UseCase::SuccessfulOutcome do 44 | it "does not fail" do 45 | outcome = UseCase::SuccessfulOutcome.new 46 | outcome.pre_condition_failed { fail "Shouldn't have failed pre-conditions" } 47 | outcome.failure { fail "Shouldn't fail" } 48 | 49 | refute outcome.pre_condition_failed? 50 | assert outcome.success? 51 | end 52 | 53 | it "yields and returns result" do 54 | result = 42 55 | yielded_result = nil 56 | outcome = UseCase::SuccessfulOutcome.new(result) 57 | returned_result = outcome.success { |res| yielded_result = res } 58 | 59 | assert_equal result, yielded_result 60 | assert_equal result, returned_result 61 | end 62 | 63 | it "gets result without block" do 64 | outcome = UseCase::SuccessfulOutcome.new(42) 65 | assert_equal 42, outcome.success 66 | end 67 | end 68 | 69 | describe UseCase::PreConditionFailed do 70 | it "does not succeed or fail" do 71 | outcome = UseCase::PreConditionFailed.new 72 | outcome.success { fail "Shouldn't succeed" } 73 | outcome.failure { fail "Shouldn't fail" } 74 | 75 | assert outcome.pre_condition_failed? 76 | refute outcome.success? 77 | end 78 | 79 | it "returns failed pre-condition wrapped" do 80 | pre_condition = 42 81 | outcome = UseCase::PreConditionFailed.new(pre_condition) 82 | returned_pc = outcome.pre_condition_failed 83 | 84 | assert_equal pre_condition, returned_pc.pre_condition 85 | end 86 | 87 | describe "yielded wrapper" do 88 | it "has flow control API" do 89 | yielded = false 90 | pre_condition = Array.new 91 | outcome = UseCase::PreConditionFailed.new(pre_condition) 92 | 93 | returned_pc = outcome.pre_condition_failed do |f| 94 | f.when(:array) { |pc| yielded = pc } 95 | end 96 | 97 | assert_equal yielded, pre_condition 98 | end 99 | 100 | it "does not call non-matching block" do 101 | yielded = nil 102 | pre_condition = Array.new 103 | outcome = UseCase::PreConditionFailed.new(pre_condition) 104 | 105 | outcome.pre_condition_failed do |f| 106 | f.when(:something) { |pc| yielded = pc } 107 | end 108 | 109 | assert_nil yielded 110 | end 111 | 112 | it "matches by class symbol" do 113 | yielded = false 114 | pre_condition = MyPreCondition.new 115 | outcome = UseCase::PreConditionFailed.new(pre_condition) 116 | 117 | returned_pc = outcome.pre_condition_failed do |f| 118 | f.when(:something) { |pc| yielded = pc } 119 | end 120 | 121 | assert_equal yielded, pre_condition 122 | end 123 | 124 | it "yields to otherwise if no match" do 125 | yielded = false 126 | pre_condition = MyPreCondition.new 127 | outcome = UseCase::PreConditionFailed.new(pre_condition) 128 | 129 | returned_pc = outcome.pre_condition_failed do |f| 130 | f.when(:nothing) { |pc| yielded = 42 } 131 | f.otherwise { |pc| yielded = pc } 132 | end 133 | 134 | assert_equal yielded, pre_condition 135 | end 136 | 137 | it "raises if calling when after otherwise" do 138 | pre_condition = MyPreCondition.new 139 | outcome = UseCase::PreConditionFailed.new(pre_condition) 140 | 141 | assert_raises(Exception) do 142 | returned_pc = outcome.pre_condition_failed do |f| 143 | f.otherwise { |pc| yielded = pc } 144 | f.when(:nothing) { |pc| yielded = 42 } 145 | end 146 | end 147 | end 148 | 149 | it "accesses pre-condition symbol" do 150 | pre_condition = MyPreCondition.new 151 | outcome = UseCase::PreConditionFailed.new(pre_condition) 152 | failure = nil 153 | 154 | outcome.pre_condition_failed do |f| 155 | failure = f 156 | end 157 | 158 | assert_equal :something, failure.symbol 159 | end 160 | 161 | it "accesses pre-condition instance symbol" do 162 | pre_condition = MyPreCondition.new 163 | def pre_condition.symbol; :other; end 164 | outcome = UseCase::PreConditionFailed.new(pre_condition) 165 | failure = nil 166 | 167 | outcome.pre_condition_failed do |f| 168 | failure = f 169 | end 170 | 171 | assert_equal :other, failure.symbol 172 | end 173 | end 174 | end 175 | 176 | describe UseCase::FailedOutcome do 177 | it "does not succeed or fail pre-conditions" do 178 | outcome = UseCase::FailedOutcome.new 179 | outcome.success { fail "Shouldn't succeed" } 180 | outcome.pre_condition_failed { fail "Shouldn't fail pre-conditions" } 181 | 182 | refute outcome.pre_condition_failed? 183 | refute outcome.success? 184 | end 185 | 186 | it "yields and returns validation failure" do 187 | failure = 42 188 | yielded_result = nil 189 | outcome = UseCase::FailedOutcome.new(failure) 190 | returned_result = outcome.failure { |result| yielded_result = result } 191 | 192 | assert_equal failure, yielded_result 193 | assert_equal failure, returned_result 194 | end 195 | 196 | it "gets failure without block" do 197 | outcome = UseCase::FailedOutcome.new(42) 198 | assert_equal 42, outcome.failure 199 | end 200 | end 201 | end 202 | -------------------------------------------------------------------------------- /test/use_case/validator_test.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | # -- 3 | # The MIT License (MIT) 4 | # 5 | # Copyright (C) 2013 Gitorious AS 6 | # 7 | # Permission is hereby granted, free of charge, to any person obtaining a copy 8 | # of this software and associated documentation files (the "Software"), to deal 9 | # in the Software without restriction, including without limitation the rights 10 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | # copies of the Software, and to permit persons to whom the Software is 12 | # furnished to do so, subject to the following conditions: 13 | # 14 | # The above copyright notice and this permission notice shall be included in all 15 | # copies or substantial portions of the Software. 16 | # 17 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 23 | # SOFTWARE. 24 | #++ 25 | require "test_helper" 26 | require "use_case/validator" 27 | 28 | NewPersonValidator = UseCase::Validator.define do 29 | validates_presence_of :name 30 | end 31 | 32 | CustomValidator = UseCase::Validator.define do 33 | validate :validate_custom 34 | 35 | def validate_custom 36 | errors.add(:name, "is not Dude") if name != "Dude" 37 | end 38 | end 39 | 40 | class Person 41 | attr_accessor :name 42 | end 43 | 44 | describe UseCase::Validator do 45 | it "delegates all calls to underlying object" do 46 | person = Person.new 47 | person.name = "Christian" 48 | result = NewPersonValidator.call(person) 49 | 50 | assert result.respond_to?(:name) 51 | assert_equal "Christian", result.name 52 | end 53 | 54 | it "passes valid object" do 55 | person = Person.new 56 | person.name = "Christian" 57 | result = NewPersonValidator.call(person) 58 | 59 | assert result.valid? 60 | end 61 | 62 | it "fails invalid object" do 63 | result = NewPersonValidator.call(Person.new) 64 | 65 | refute result.valid? 66 | assert_equal 1, result.errors.count 67 | end 68 | 69 | it "supports custom validators" do 70 | result = CustomValidator.call(Person.new) 71 | 72 | refute result.valid? 73 | assert_equal 1, result.errors.count 74 | end 75 | 76 | it "passes custom validator" do 77 | person = Person.new 78 | person.name = "Dude" 79 | result = CustomValidator.call(person) 80 | 81 | assert result.valid? 82 | end 83 | end 84 | -------------------------------------------------------------------------------- /test/use_case_test.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | # -- 3 | # The MIT License (MIT) 4 | # 5 | # Copyright (C) 2013 Gitorious AS 6 | # 7 | # Permission is hereby granted, free of charge, to any person obtaining a copy 8 | # of this software and associated documentation files (the "Software"), to deal 9 | # in the Software without restriction, including without limitation the rights 10 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | # copies of the Software, and to permit persons to whom the Software is 12 | # furnished to do so, subject to the following conditions: 13 | # 14 | # The above copyright notice and this permission notice shall be included in all 15 | # copies or substantial portions of the Software. 16 | # 17 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 23 | # SOFTWARE. 24 | #++ 25 | require "test_helper" 26 | require "use_case" 27 | require "sample_use_case" 28 | 29 | describe UseCase do 30 | before do 31 | @logged_in_user = User.new(42, "Christian") 32 | @logged_in_user.can_admin = true 33 | end 34 | 35 | it "fails first pre-condition; no user logged in" do 36 | outcome = CreateRepository.new(nil).execute({}) 37 | 38 | outcome.pre_condition_failed do |f| 39 | assert_equal UserLoggedInPrecondition, f.pre_condition.class 40 | end 41 | end 42 | 43 | it "fails second pre-condition; user cannot admin" do 44 | @logged_in_user.can_admin = false 45 | outcome = CreateRepository.new(@logged_in_user).execute({}) 46 | 47 | outcome.pre_condition_failed do |f| 48 | assert_equal ProjectAdminPrecondition, f.pre_condition.class 49 | end 50 | end 51 | 52 | it "fails with error if pre-condition raises" do 53 | def @logged_in_user.id; raise "Oops!"; end 54 | 55 | assert_raises RuntimeError do 56 | CreateRepository.new(@logged_in_user).execute({}) 57 | end 58 | end 59 | 60 | it "fails on input validation" do 61 | outcome = CreateRepository.new(@logged_in_user).execute({}) 62 | 63 | validation = outcome.failure do |v| 64 | refute v.valid? 65 | assert_equal 1, v.errors.count 66 | assert v.errors[:name] 67 | end 68 | 69 | refute_nil validation 70 | end 71 | 72 | it "executes command" do 73 | outcome = CreateRepository.new(@logged_in_user).execute({ :name => "My repository" }) 74 | 75 | result = outcome.success do |res| 76 | assert_equal "My repository", res.name 77 | assert res.is_a?(Repository) 78 | end 79 | 80 | refute_nil result 81 | end 82 | 83 | it "raises if command raises" do 84 | use_case = ExplodingRepository.new(@logged_in_user) 85 | 86 | assert_raises RuntimeError do 87 | use_case.execute(nil) 88 | end 89 | end 90 | 91 | it "fails when builder processed inputs fail validation" do 92 | outcome = CreateRepositoryWithBuilder.new(@logged_in_user).execute({ :name => "invalid" }) 93 | 94 | validation = outcome.failure do |v| 95 | refute v.valid? 96 | assert_equal 1, v.errors.count 97 | assert v.errors[:name] 98 | end 99 | 100 | refute_nil validation 101 | end 102 | 103 | it "passes builder processed inputs to command" do 104 | outcome = CreateRepositoryWithBuilder.new(@logged_in_user).execute({ :name => "Dude" }) 105 | 106 | assert outcome.success?, outcome.failure && outcome.failure.full_messages.join("\n") 107 | assert_equal "Dude!", outcome.result.name 108 | end 109 | 110 | it "treats builder error as failed pre-condition" do 111 | assert_raises RuntimeError do 112 | CreateRepositoryWithExplodingBuilder.new(@logged_in_user).execute({ :name => "Dude" }) 113 | end 114 | end 115 | 116 | it "chains two commands" do 117 | outcome = CreatePimpedRepository.new(@logged_in_user).execute({ :name => "Mr" }) 118 | 119 | assert_equal 1349, outcome.result.id 120 | assert_equal "Mr (Pimped)", outcome.result.name 121 | end 122 | 123 | it "chains two commands with individual builders" do 124 | outcome = CreatePimpedRepository2.new(@logged_in_user).execute({ :name => "Mr" }) 125 | 126 | assert_equal 42, outcome.result.id 127 | assert_equal "Mr! (Pimped)", outcome.result.name 128 | end 129 | 130 | it "fails one of three validators" do 131 | outcome = CreatePimpedRepository3.new(@logged_in_user).execute({ :name => "Mr" }) 132 | 133 | refute outcome.success? 134 | assert_equal "You cannot win", outcome.failure.errors[:name].join 135 | end 136 | 137 | it "calls command lambda" do 138 | outcome = InlineCommand.new.execute({ :name => "Dissection" }) 139 | 140 | assert outcome.success? 141 | assert_equal "Dissection", outcome.result 142 | end 143 | 144 | it "implicitly uses command as builder" do 145 | outcome = ImplicitBuilder.new(@logged_in_user).execute({ :name => "Mr" }) 146 | 147 | assert_equal 42, outcome.result.id 148 | assert_equal "Mr! (Pimped)", outcome.result.name 149 | end 150 | end 151 | -------------------------------------------------------------------------------- /use_case.gemspec: -------------------------------------------------------------------------------- 1 | require "./lib/use_case/version" 2 | 3 | Gem::Specification.new do |s| 4 | s.name = "use_case" 5 | s.version = UseCase::VERSION 6 | s.author = "Christian Johansen" 7 | s.email = "christian@gitorious.com" 8 | s.homepage = "http://gitorious.org/gitorious/use_case" 9 | s.summary = s.description = "" 10 | 11 | s.files = `git ls-files`.split("\n") 12 | s.test_files = `git ls-files test`.split("\n") 13 | s.require_path = "lib" 14 | 15 | s.add_development_dependency "minitest", "~> 4" 16 | s.add_development_dependency "rake" 17 | end 18 | --------------------------------------------------------------------------------