{"id":16482314,"url":"https://github.com/phortx/rails-use-case","last_synced_at":"2025-03-21T07:30:42.448Z","repository":{"id":40179073,"uuid":"239949632","full_name":"phortx/rails-use-case","owner":"phortx","description":"UseCase \u0026 Service classes for rails","archived":false,"fork":false,"pushed_at":"2024-02-23T16:53:31.000Z","size":67,"stargazers_count":3,"open_issues_count":7,"forks_count":2,"subscribers_count":1,"default_branch":"master","last_synced_at":"2024-04-24T10:34:50.063Z","etag":null,"topics":["architecture","hacktoberfest","hacktoberfest2022","rails","ruby"],"latest_commit_sha":null,"homepage":"","language":"Ruby","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/phortx.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2020-02-12T07:14:53.000Z","updated_at":"2024-08-05T18:22:28.269Z","dependencies_parsed_at":"2024-02-23T17:55:15.121Z","dependency_job_id":null,"html_url":"https://github.com/phortx/rails-use-case","commit_stats":{"total_commits":42,"total_committers":3,"mean_commits":14.0,"dds":"0.19047619047619047","last_synced_commit":"b7cf7bdc805f529b9da8d5abd34f79bda2355063"},"previous_names":[],"tags_count":9,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/phortx%2Frails-use-case","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/phortx%2Frails-use-case/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/phortx%2Frails-use-case/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/phortx%2Frails-use-case/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/phortx","download_url":"https://codeload.github.com/phortx/rails-use-case/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":244757234,"owners_count":20505355,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["architecture","hacktoberfest","hacktoberfest2022","rails","ruby"],"created_at":"2024-10-11T13:10:21.087Z","updated_at":"2025-03-21T07:30:42.103Z","avatar_url":"https://github.com/phortx.png","language":"Ruby","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Rails Use Case gem\n\nThis gem introduces UseCases and Services to Rails which allow you to keep your Models and Controllers slim.\n\nRead more: https://dev.to/phortx/pimp-your-rails-application-32d0\n\nClean Architecture suggests to put the business logic in UseCases which are classes, that contain reusable high\nlevel business logic which otherwise would normally be located in the controller. UseCases are easy to read,\nclean, reusable, testable and extendable.\nExamples are: Place an item in the cart, create a new user or delete a comment.\n\nThe purpose of a Service is to contain low level non-domain code like communication with a API,\ngenerating an export, upload via FTP or generating a PDF.\n\n\n## Setup\n\n```ruby\ngem 'rails_use_case'\n```\n\n\n## Use Case\n\nThe purpose of a UseCase is to contain reusable high level business logic which would normally be\nlocated in the controller. It defines a process via `step` definitions. A UseCase takes params\nand has a outcome, which is either successful or failed. It doesn't have a configuration file and doesn't\nlog anything. Examples are: Place an item in the cart, create a new user or delete a comment.\n\nThe params should always passed as hash and are automatically assigned to instance variables.\n\nUse 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`.\n\n\n### Steps\n\nSteps are executed in the defined order. Only when a step succeeds (returns true) the next step will\nbe executed. Steps can be skipped via `if` and `unless`.\n\nThe step either provides the name of a method within the use case or a block.\nWhen a block is given, it will be executed. Otherwise the framework will try to\ncall a method with the given name.\n\nYou can also have named inline steps: `step :foo, do: -\u003e { ... }` which is\nequivalent to `step { ... }` but with a given name. An existing method `foo`\nwill not be called in this case but rather the block be executed.\n\nThere are also two special steps: `success` and `failure`. Both will end the\nstep chain immediately. `success` will end the use case successfully (like there\nwould be no more steps). And `failure` respectively will end the use case with a\nerror. You should pass the error message and/or code via `message:` and/or\n`code:` options.\n\n\n### Failing\n\nA UseCase fails when a step returns a falsy value or raises an exception.\n\nFor even better error handling, you should let a UseCase fail via the shortcut\n`fail!()` which actually just raised an `UseCase::Error` but you can provide\nsome additional information. This way you can provide a human readable message\nwith error details and additionally you can pass an error code as symbol, which\nallows the calling code to do error handling:\n\n`fail!(message: 'You have called this wrong. Shame on you!', code: :missing_information)`.\n\nThe error_code can also passed as first argument to the `failure` step definition.\n\n\n### Record\n\nThe UseCase should assign the main record to `@record`. Calling `save!` without\nargument will try to save that record or raises an exception. Also the\n`@record` will automatically passed into the outcome.\n\nYou can either set the `@record` manually or via the `record` method. This comes\nin two flavors:\n\nEither passing the name of a param as symbol. Let's assume the UseCase\nhas a parameter called `user` (defined via `attr_accessor`), then you can assign\nthe user to `@record` by adding `record :user` to your use case.\n\nThe alternative way is to pass a block which returns the value for `@record`\nlike in the example UseCase below.\n\n\n### Example UseCase\n\n```ruby\nclass BlogPosts::Create \u003c Rails::UseCase\n  attr_accessor :title, :content, :author, :skip_notifications, :publish\n\n  validates :title, presence: true\n  validates :content, presence: true\n  validates :author, presence: true\n\n  record { BlogPost.new }\n\n  failure :access_denied, message: 'No permission', unless: -\u003e { author.can_publish_blog_posts? }\n  step    :assign_attributes\n  step    :save!\n  succeed unless: -\u003e { publish }\n  step    :publish, do: -\u003e { record.publish! }\n  step    :notify_subscribers, unless: -\u003e { skip_notifications }\n\n\n  private def assign_attributes\n    @record.assign_attributes(\n      title: title,\n      content: content,\n      created_by: author,\n      type: :default\n    )\n  end\n\n  private def notify_subscribers\n    # ... send some mails ...\n  end\nend\n```\n\nExample usage of that UseCase:\n\n```ruby\nresult = BlogPosts::Create.perform(\n  title: 'Super Awesome Stuff!',\n  content: 'Lorem Ipsum Dolor Sit Amet',\n  author: current_user,\n  skip_notifications: false\n)\n\nputs result.inspect\n=\u003e {\n  success: false,                        # Wether the UseCase ended successfully\n  record: BlogPost(...)                  # The value assigned to @record\n  errors: [],                            # List of validation errors\n  exception: Rails::UseCase::Error(...), # The exception raised by the UseCase\n  message: \"...\",                        # Error message\n  error_code: :save_failed               # Error Code\n}\n```\n\n- You can check whether a UseCase was successful via `result.success?`.\n- You can access the value of `@record` via `result.record`.\n- You can stop the UseCase process with a error message via throwing `Rails::UseCase::Error` exception.\n\n\n### Working with the result\n\nThe `perform` method of a UseCase returns an outcome object which contains a\n`code` field with the error code or `:success` otherwise. This comes handy when\nusing in controller actions for example and is a great way to delegate the\nbusiness logic part of a controller action to the respective UseCase.\nEverything the controller has to do, is to setup the params and dispatch the\nresult.\n\nGiven the Example above, here is the same call within a controller action with\nan case statement.\n\n```ruby\nclass BlogPostsController \u003c ApplicationController\n  # ...\n\n  def create\n    parameters = {\n      title: params[:post][:title],\n      content: params[:post][:content],\n      publish: params[:post][:publish],\n      author: current_user\n    }\n\n    outcome = BlogPosts::Create.perform(parameters).code\n\n    case outcome.code\n    when :success       then redirect_to(outcome.record)\n    when :access_denied then render(:new, flash: { error: \"Access Denied!\" })\n    when :foo           then redirect_to('/')\n    else                     render(:new, flash: { error: outcome.message })\n    end\n  end\n\n  # ...\nend\n```\n\nHowever this is not rails specific and can be used in any context.\n\n\n## Behavior\n\nA behavior is simply a module that contains methods to share logic between use cases and to keep them DRY.\n\nTo use a behavior in a use case, use the `with` directive, like `with BlogPosts`.\n\nBehaviors 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).\n\n\n### Example Behavior\n\nDefinition:\n\n```ruby\nmodule BlogPosts\n  def notify_subscribers\n    # ... send some mails ...\n  end\nend\n```\n\nUsage:\n\n```ruby\nclass CreateBlogPost \u003c Rails::UseCase\n  with BlogPosts\n\n  # ...\n\n  step :build_post\n  step :save!\n  step :notify_subscribers, unless: -\u003e { skip_notifications }\n\n  # ...\nend\n```\n\n\n\n## Service\n\nThe purpose of a Service is to contain low level non-domain code like communication with a API,\ngenerating an export, upload via FTP or generating a PDF. It takes params, has it's own configuration and writes a log file.\n\nIt comes with call style invocation: `PDFGenerationService.(some, params)`\n\nServices should be placed in the `app/services/` directory and the name should end with `Service` like `PDFGenerationService` or `ReportUploadService`.\n\n\n### Example Service\n\n```ruby\nclass PDFGenerationService \u003c Rails::Service\n  attr_reader :pdf_template, :values\n\n  # Constructor.\n  def initialize\n    super 'pdf_generation'\n    prepare\n    validate_libreoffice\n  end\n\n\n  # Entry point.\n  #\n  # @param [PdfTemplate] pdf_template PdfTemplate record.\n  # @param [Hash\u003cString, String\u003e] values Mapping of variables to their values.\n  #\n  # @returns [String] Path to PDF file.\n  def call(pdf_template, values = {})\n    @pdf_template = pdf_template\n    @values = prepare_variable_values(values)\n\n    write_odt_file\n    replace_variables\n    generate_pdf\n\n    @pdf_file_path\n  ensure\n    delete_tempfile\n  end\nend\n```\n\n### Configuration\n\nThe service tries to automatically load a configuration from `config/services/[service_name].yml`\nwhich is available via the `config` method.\n\n\n### Logging\n\nEach service automatically logs to a separate log file `log/services/[service_name].log`. You can write additional logs via `logger.info(msg)`.\n\nIt'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.\n\n\n## License\n\nMIT\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fphortx%2Frails-use-case","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fphortx%2Frails-use-case","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fphortx%2Frails-use-case/lists"}