Giter VIP home page Giter VIP logo

rails-use-case's Introduction

Rails Use Case gem

This gem introduces UseCases and Services to Rails which allow you to keep your Models and Controllers slim.

Read more: https://dev.to/phortx/pimp-your-rails-application-32d0

Clean Architecture suggests to put the business logic in UseCases which are classes, that contain reusable high level business logic which otherwise would normally be located in the controller. UseCases are easy to read, clean, reusable, testable and extendable. Examples are: Place an item in the cart, create a new user or delete a comment.

The purpose of a Service is to contain low level non-domain code like communication with a API, generating an export, upload via FTP or generating a PDF.

Setup

gem 'rails_use_case'

Use Case

The purpose of a UseCase is to contain reusable high level business logic which would normally be located in the controller. It defines a process via step definitions. A UseCase takes params and has a outcome, which is either successful or failed. It doesn't have a configuration file and doesn't log anything. Examples are: Place an item in the cart, create a new user or delete a comment.

The params should always passed as hash and are automatically assigned to instance variables.

Use Cases should be placed in the app/use_cases/ directory and the file and class name should start with a verb like create_blog_post.rb.

Steps

Steps are executed in the defined order. Only when a step succeeds (returns true) the next step will be executed. Steps can be skipped via if and unless.

The step either provides the name of a method within the use case or a block. When a block is given, it will be executed. Otherwise the framework will try to call a method with the given name.

You can also have named inline steps: step :foo, do: -> { ... } which is equivalent to step { ... } but with a given name. An existing method foo will not be called in this case but rather the block be executed.

There are also two special steps: success and failure. Both will end the step chain immediately. success will end the use case successfully (like there would be no more steps). And failure respectively will end the use case with a error. You should pass the error message and/or code via message: and/or code: options.

Failing

A UseCase fails when a step returns a falsy value or raises an exception.

For even better error handling, you should let a UseCase fail via the shortcut fail!() which actually just raised an UseCase::Error but you can provide some additional information. This way you can provide a human readable message with error details and additionally you can pass an error code as symbol, which allows the calling code to do error handling:

fail!(message: 'You have called this wrong. Shame on you!', code: :missing_information).

The error_code can also passed as first argument to the failure step definition.

Record

The UseCase should assign the main record to @record. Calling save! without argument will try to save that record or raises an exception. Also the @record will automatically passed into the outcome.

You can either set the @record manually or via the record method. This comes in two flavors:

Either passing the name of a param as symbol. Let's assume the UseCase has a parameter called user (defined via attr_accessor), then you can assign the user to @record by adding record :user to your use case.

The alternative way is to pass a block which returns the value for @record like in the example UseCase below.

Example UseCase

class BlogPosts::Create < Rails::UseCase
  attr_accessor :title, :content, :author, :skip_notifications, :publish

  validates :title, presence: true
  validates :content, presence: true
  validates :author, presence: true

  record { BlogPost.new }

  failure :access_denied, message: 'No permission', unless: -> { author.can_publish_blog_posts? }
  step    :assign_attributes
  step    :save!
  succeed unless: -> { publish }
  step    :publish, do: -> { record.publish! }
  step    :notify_subscribers, unless: -> { skip_notifications }


  private def assign_attributes
    @record.assign_attributes(
      title: title,
      content: content,
      created_by: author,
      type: :default
    )
  end

  private def notify_subscribers
    # ... send some mails ...
  end
end

Example usage of that UseCase:

result = BlogPosts::Create.perform(
  title: 'Super Awesome Stuff!',
  content: 'Lorem Ipsum Dolor Sit Amet',
  author: current_user,
  skip_notifications: false
)

puts result.inspect
=> {
  success: false,                        # Wether the UseCase ended successfully
  record: BlogPost(...)                  # The value assigned to @record
  errors: [],                            # List of validation errors
  exception: Rails::UseCase::Error(...), # The exception raised by the UseCase
  message: "...",                        # Error message
  error_code: :save_failed               # Error Code
}
  • You can check whether a UseCase was successful via result.success?.
  • You can access the value of @record via result.record.
  • You can stop the UseCase process with a error message via throwing Rails::UseCase::Error exception.

Working with the result

The perform method of a UseCase returns an outcome object which contains a code field with the error code or :success otherwise. This comes handy when using in controller actions for example and is a great way to delegate the business logic part of a controller action to the respective UseCase. Everything the controller has to do, is to setup the params and dispatch the result.

Given the Example above, here is the same call within a controller action with an case statement.

class BlogPostsController < ApplicationController
  # ...

  def create
    parameters = {
      title: params[:post][:title],
      content: params[:post][:content],
      publish: params[:post][:publish],
      author: current_user
    }

    outcome = BlogPosts::Create.perform(parameters).code

    case outcome.code
    when :success       then redirect_to(outcome.record)
    when :access_denied then render(:new, flash: { error: "Access Denied!" })
    when :foo           then redirect_to('/')
    else                     render(:new, flash: { error: outcome.message })
    end
  end

  # ...
end

However this is not rails specific and can be used in any context.

Behavior

A behavior is simply a module that contains methods to share logic between use cases and to keep them DRY.

To use a behavior in a use case, use the with directive, like with BlogPosts.

Behaviors should be placed in the app/behaviors/ directory and the file and module name should named in a way it can be prefixed with with, like blog_posts.rb (with blog posts).

Example Behavior

Definition:

module BlogPosts
  def notify_subscribers
    # ... send some mails ...
  end
end

Usage:

class CreateBlogPost < Rails::UseCase
  with BlogPosts

  # ...

  step :build_post
  step :save!
  step :notify_subscribers, unless: -> { skip_notifications }

  # ...
end

Service

The purpose of a Service is to contain low level non-domain code like communication with a API, generating an export, upload via FTP or generating a PDF. It takes params, has it's own configuration and writes a log file.

It comes with call style invocation: PDFGenerationService.(some, params)

Services should be placed in the app/services/ directory and the name should end with Service like PDFGenerationService or ReportUploadService.

Example Service

class PDFGenerationService < Rails::Service
  attr_reader :pdf_template, :values

  # Constructor.
  def initialize
    super 'pdf_generation'
    prepare
    validate_libreoffice
  end


  # Entry point.
  #
  # @param [PdfTemplate] pdf_template PdfTemplate record.
  # @param [Hash<String, String>] values Mapping of variables to their values.
  #
  # @returns [String] Path to PDF file.
  def call(pdf_template, values = {})
    @pdf_template = pdf_template
    @values = prepare_variable_values(values)

    write_odt_file
    replace_variables
    generate_pdf

    @pdf_file_path
  ensure
    delete_tempfile
  end
end

Configuration

The service tries to automatically load a configuration from config/services/[service_name].yml which is available via the config method.

Logging

Each service automatically logs to a separate log file log/services/[service_name].log. You can write additional logs via logger.info(msg).

It's possible to force the services to log to STDOUT by setting the environment variable SERVICE_LOGGER_STDOUT. This is useful for Heroku for example.

License

MIT

rails-use-case's People

Contributors

dependabot[bot] avatar dschmura avatar phortx avatar

Stargazers

 avatar  avatar  avatar

Watchers

 avatar

rails-use-case's Issues

`record` command

It would be nice to have a record DSL command to set the record:

class FooBar < Rails::UseCase
  attr_accessor :user

  record :user
end

or:

class FooBar < Rails::UseCase
  record { User.new }
end

Base Module for Behaviors

To allow the usage of rails included

Also explain how to move steps, params and validations to a behavior

Unified way to get error message(s) and outcomes

I often do something like

result.exception&.message || result.errors&.full_messages

To get the error messages. Would be nice when there is a simple way to get the errors as string. For example

result.error_message

Also there should be support for a error_code or something to tell the caller what happend as a Symbol.

Add succeed/fail commands

To optionally skip all steps below and stop the use case immediately. Fail also allows to add a message optionally.

step :a
step :b
succeed, if: -> { ... }
step :c
step :d
fail 'something is broken', if: -> { ... }
step :e
step :f

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.