Skip to content

goncalvesjoao/rest_my_case

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

96 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

RestMyCase Code Climate Build Status Test Coverage Gem Version

Light Ruby gem with everything you need in a "The Clean Architecture" use case scenario.

Many thanks to @tdantas and @junhanamaki and a shout-out to @joaquimadraz and his compel ruby validations gem.


1) Installation

Add this line to your application's Gemfile:

gem 'rest_my_case'

And then execute:

$ bundle

Or install it yourself as:

$ gem install rest_my_case

2) Ideology

Your business logic goes into separated use cases...

class FindPost < RestMyCase::Base

  def perform
    context.post = Post.find(context.id)
  end

end

class ArchivePost < RestMyCase::Base

  depends FindPost

  def perform
    context.post.status = 'archived'

    context.result = context.post.save
  end

end

web framework should only act as a bridge between the user and your business logic.

class PostsController < ApplicationController

  def archive
    @context = ArchivePost.perform id: params[:id]

    if @context.result
      redirect_to @context.post
    else
      render "archive" #view post.errors
    end
  end

end

Ideally your business logic should be a ruby gem that can be tested independently from your framework.

Checkout this step by step tutorial: (WIP) on how to isolate your code into a ruby gem and connect it to your rails or sinatra api.


3) Basic usage

class BuildPost < RestMyCase::Base
  def perform
    puts context.id
    puts context.post_attributes
  end
end
irb> params = { id: 1, post: { title: 'my first post' } }
irb> context = BuildPost.perform id: params[:id], post_attributes: params[:post]
1
{:title=>"my first post"}
irb> context.id
1

The Hash passed down to BuildPost.perform will be available through an instance method called #context that will return an OpenStruct object initialized with that Hash (see more in section 7).

Executing BuildPost.perform will instantiate your use case and all of its dependencies, build a context with the contents of params, run your use case (and its dependencies) with that context and return it at the end.

3.1) Normal usage

Organize your use cases by single responsibilities and establish your use case flow through "dependencies".

class FindPost < RestMyCase::Base
  def perform
    context.post = Post.find(context.id)
  end
end

class BuildPost < RestMyCase::Base
  depends FindPost

  def perform
    context.post.assign_attributes context.post_attributes
  end
end

The class method .depends will make BuildPost dependent of FindPost which means that calling BuildPost.perform will run FindPost#perform first and BuildPost#perform last. Both use cases will share the same context.

irb> params = { id: 1, post: { title: 'my first post' } }
irb> context = BuildPost.perform id: params[:id], post_attributes: params[:post]
irb> context.post.name
"my first post"

4) Lifecycle

4.1) Waiting to be implemented methods

Methods: #setup, #perform, #rollback and #final

class UseCase1 < RestMyCase::Base
  def setup
    puts 'UseCase1#setup'
  end
  def perform
    puts 'UseCase1#perform'
    error if context.should_fail
  end
  def rollback
    puts 'UseCase1#rollback'
  end
  def final
    puts 'UseCase1#final'
  end
end
irb> UseCase1.perform #will print
"UseCase1#setup"
"UseCase1#perform"
"UseCase1#final"

Method #rollback will be called after #perform and before #final if #error is invoked inside a #setup of #perform (see more in section 5).

irb> UseCase1.perform(should_fail: true) #will print
"UseCase1#setup"
"UseCase1#perform"
"UseCase1#rollback"
"UseCase1#final"

Method #final will run last and always, no matter how many times #error was called.


4.2) Dependencies

Default behaviour is to run your dependencies (#setup, #perform, #rollback and #final) methods, first.

class UseCase2 < RestMyCase::Base
  def perform
    puts 'UseCase2#perform'
  end
end

class UseCase3 < RestMyCase::Base
  def perform
    puts 'UseCase3#perform'
  end
end

class UseCase1 < RestMyCase::Base
  depends UseCase2,
          UseCase3

  def perform
    puts 'UseCase1#perform'
  end
end
irb> UseCase1.perform #will print
"UseCase2#perform"
"UseCase3#perform"
"UseCase1#perform"

See section 8 for more examples.


5) Flow control methods

Methods Behaviour
#abort Stops other remaining use cases from running and triggers #rollback on already executed use cases (in reverse order).
#skip Will prevent #perform (of the use case that called #skip) from running and will not stop other use cases from running nor trigger a #rollback (only works by being used inside a #setup method).
#error(error_message = '') Will do the same as #abort but will also push error_message to #context.errors array so you can track down what happen in what use case (see more in section 7).
*#invoke(use_case_classes) Does the same as the class method .depends but executes the use cases on demand. Shares the context to them and if they call #abort on their side, the use case that invoked will also abort.

#skip, #abort, #error and #invoke have a "bang!" version that will raise a controlled exception, preventing the remaining lines of code from running.

class UseCase1 < RestMyCase::Base
  def perform
    puts 'before #error!'
    error!
    puts 'after #error!'
  end
end
irb> UseCase1.perform #will print only
"before #error!"

6) Configuration methods

Methods Behaviour
*.depends(use_case_classes) Adds the use_case_classes array to the use case's dependencies list, that will be executed by order before the actual use case (see more in section 4).
*.required_context(attributes) WIP.
*.context_reader(methods) Defines getter methods that return context.send method, to help reduce the context.method boilerplate.
*.context_writer(methods) Defines setter methods that set context.send "#{method}=", value, to help reduce the context.method = value boilerplate.
*.context_accessor(methods) Calls both .context_reader and .context_writer methods.
.silence_dependencies_abort= If false once a dependency calls #abort(!) the next in line dependencies will not be called (and #rollback will be called in reverse order) but if true all dependencies will run no matter how many times #abort(!) was called (usefull when you want to run multiple validations (see more in section 9).

7) #context methods

The returning object is an instance of RestMyCase::Context::Base class that inherits from OpenStruct and implements the following methods:

Methods Behaviour
#attributes Alias to #marshal_dump, returns all of the context's stored data.
#to_hash Serializes and unserializes #attributes turning any existing ruby objects into serialized strings.
*#values_at(keys) Short for #attributes.values_at(*keys), returns an array with correspondent values for each key.
#valid? Checks if #errors is empty
#ok? Alias to #valid?
#success? Alias to #ok?
#errors Array that gets 'pushed' with { message: error_message, class_name: UseCase.class.name } (or error_message itself if error_message already a Hash) every time UseCase#error(error_message) is called.

If defined?(ActiveModel) is true, ActiveModel::Serialization will be included and in turn methods like #to_json(options = {}) and #serializable_hash(options = nil) will become available.


8) Examples

If UseCase1 depends on UseCase2 and UseCase3 in that respective order.

Running UseCase1.perform will pass down the context to each use case in the following manner:

8.1) Given that no use case called the method(s) #error(!)

UseCase2#setup -> UseCase3#setup -> UseCase1#setup ->
UseCase2#perform -> UseCase3#perform -> UseCase1#perform ->
UseCase2#final -> UseCase3#final -> UseCase1#final

8.2) Given that UseCase3#setup calls #skip(!)

UseCase2#setup -> UseCase3#setup -> UseCase1#setup ->
UseCase2#perform -> UseCase1#perform ->
UseCase2#final -> UseCase3#final -> UseCase1#final

8.3) Given that UseCase3#setup calls #error(!)

UseCase2#setup -> UseCase3#setup ->
UseCase3#rollback -> UseCase2#rollback ->
UseCase2#final -> UseCase3#final -> UseCase1#final

8.4) Given that UseCase3#perform calls #error(!)

UseCase2#setup -> UseCase3#setup -> UseCase1#setup ->
UseCase2#perform -> UseCase3#perform ->
UseCase3#rollback -> UseCase2#rollback ->
UseCase2#final -> UseCase3#final -> UseCase1#final

9) RestMyCase::Validator class

WIP


10) RestMyCase::Status module

class UseCase1 < RestMyCase::Base
  include RestMyCase::Status
end

Adds following methods:

Methods Behaviour
#context Returns an instance of RestMyCase::Context::Status
#status Returns context.status (see more in section 10.1)
#failure(status, error_message = nil) WIP

#failure! is also present and does the same as the other flow control "bang!" methods (see section 5).

10.1) RestMyCase::Context::Status

WIP


11) RestMyCase::HttpStatus module (for seamless API integration)

class UseCase1 < RestMyCase::Base
  include RestMyCase::HttpStatus
end

Includes the module RestMyCase::Status and #context becomes an instance of RestMyCase::Context::HttpStatus.

11.1) RestMyCase::Context::HttpStatus

WIP

About

Light "The Clean Architecture" use case implementation

Resources

License

Stars

Watchers

Forks

Packages

No packages published

Languages