{"id":13878083,"url":"https://github.com/DmitryTsepelev/clean_actions","last_synced_at":"2025-07-16T14:31:21.884Z","repository":{"id":192913345,"uuid":"684690908","full_name":"DmitryTsepelev/clean_actions","owner":"DmitryTsepelev","description":"A modern modular service object toolkit for Rails, that respects database transactions and adds type checks to returned values.","archived":false,"fork":false,"pushed_at":"2024-08-26T15:09:25.000Z","size":49,"stargazers_count":79,"open_issues_count":0,"forks_count":1,"subscribers_count":3,"default_branch":"master","last_synced_at":"2025-06-24T06:19:44.764Z","etag":null,"topics":["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":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/DmitryTsepelev.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":null,"funding":null,"license":"LICENSE.txt","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":"2023-08-29T16:51:09.000Z","updated_at":"2025-04-15T12:02:16.000Z","dependencies_parsed_at":"2023-09-05T22:58:55.711Z","dependency_job_id":"2258afcf-a3c0-4258-9ff6-f9165bae4f4c","html_url":"https://github.com/DmitryTsepelev/clean_actions","commit_stats":null,"previous_names":["dmitrytsepelev/clean_actions"],"tags_count":1,"template":false,"template_full_name":null,"purl":"pkg:github/DmitryTsepelev/clean_actions","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/DmitryTsepelev%2Fclean_actions","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/DmitryTsepelev%2Fclean_actions/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/DmitryTsepelev%2Fclean_actions/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/DmitryTsepelev%2Fclean_actions/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/DmitryTsepelev","download_url":"https://codeload.github.com/DmitryTsepelev/clean_actions/tar.gz/refs/heads/master","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/DmitryTsepelev%2Fclean_actions/sbom","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":265518475,"owners_count":23780967,"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":["rails","ruby"],"created_at":"2024-08-06T08:01:39.387Z","updated_at":"2025-07-16T14:31:21.547Z","avatar_url":"https://github.com/DmitryTsepelev.png","language":"Ruby","readme":"# CleanActions\n\n[![Gem Version](https://badge.fury.io/rb/clean_actions.svg)](https://rubygems.org/gems/clean_actions)\n[![Tests status](https://github.com/DmitryTsepelev/clean_actions/actions/workflows/test.yml/badge.svg)](https://github.com/DmitryTsepelev/clean_actions/actions/workflows/test.yml)\n![](https://ruby-gem-downloads-badge.herokuapp.com/clean_actions?type=total)\n\nA modern modular service object toolkit for Rails, that respects database transactions and adds type checks to returned values.\n\n```ruby\nclass AddItemToCart \u003c CleanActions::Base\n  includes Dry::Initializer\n\n  option :user\n  option :item\n\n  # This will report an error if someone accidentally returns wrong instance from #perform_actions.\n  returns OrderItem\n\n  # Such checks are happening inside the transaction right before #perform_actions, so\n  # you can halt early.\n  fail_with(:banned_user) { @user.banned? }\n\n  # This method is executed inside the database transaction.\n  # If transaction was opened by another action, which called this one - savepoint won't be created.\n  # Last line will be used as a returned value.\n  def perform_actions\n    @order = CreateOrder.call(user: @user) # if CreateOrder fails - transaction will be rolled back\n    @order.order_items.create!(item: @item) # if something else fails here - transaction will be rolled back as well\n  end\n\n  # This method will be called for each action after whole transaction commits successfully.\n  def after_commit\n    ItemAddedSubscription.trigger(order: @order)\n  end\nend\n```\n\nYou can support my open–source work [here](https://boosty.to/dmitry_tsepelev).\n\n## Usage\n\nAdd this line to your application's Gemfile:\n\n```ruby\ngem 'clean_actions'\n```\n\n## Writing your actions\n\nInherit your actions from `CleanActions::Base`, which by defaut includes [typed returns](/README.md#Typed-Returns) and [fail_with](/README.md#Fail-With).\n\n\u003e If you want to exclude something — inherit from `CleanActions::Action` and configure all includes you need.\n\nYou should implement at least one of two methods—`#perform_actions` or `#after_commit`:\n\n```ruby\nclass AddItemToCart \u003c CleanActions::Base\n  def perform_actions\n    @order = CreateOrder.call(user: @user)\n    @order_item = @order.order_items\n      .create_with(quantity: 0)\n      .find_or_create_by!(item: @item)\n  end\n\n  def after_commit\n    NotifyUserAboutUpdatedOrderItemJob.perform_later(order_item: @order_item)\n  end\nend\n```\n\nWhen first action is called, it will be wrapped to the database transaction, and all actions called by it will be inside the same transaction. All `#perform_actions` will happen inside the transaction (and rolled back if needed). After that, in case of successful commit, all `#after_commit` actions will happen in order.\n\n## Error handling\n\nIf something goes wrong and transaction will raise an error—it will cause transaction to be rolled back. Errors should not be used as a way to manage a control flow, so all unhandled exceptions raised inside actions, will be reraised.\n\nHowever, if you do expect an error—it's better to represent it as a returned value. Use `#fail!(:reason)` for that:\n\n```ruby\nclass AddItemToCart \u003c CleanActions::Base\n  def perform_actions\n    fail!(:shop_is_closed)\n  end\nend\n\nAddItemToCart.call # =\u003e CleanActions::ActionFailure(reason: :shop_is_closed)\n```\n\n## Typed Returns\n\nHave you ever been in situation, when it's not clear, what will be returned by the class? Do you have some type system in your project? While you are setting it up—use typed returns:\n\n```ruby\nclass FetchOrder \u003c CleanActions::Base\n  returns Order\n\n  option :order_id\n\n  def perform_actions\n    User.find(order_id)\n  end\nend\n\nFetchOrder.call(42) # =\u003e \"expected FetchOrder to return Order, returned User\" is logged\n```\n\nThe last line of `#perform_actions` will be returned. Note that if you have this module on but configure nothing—action will return `nil`.\n\n## Isolation levels\n\nBy default transactions are executed in `READ COMMITTED` level. You can override it for a specific aciton:\n\n```ruby\nclass FetchOrder \u003c CleanActions::Base\n  with_isolation_level :repeatable_read\n\n  option :order_id\n\n  def perform_actions\n    # actions\n  end\nend\n\nFetchOrder.call(42) # =\u003e \"expected FetchOrder to return Order, returned User\" is logged\n```\n\nAlso, you can configure it for the whole project:\n\n```ruby\nCleanActions.config.isolation_level = :serializable\n```\n\n## Savepoints\n\nIf you want to run one action inside another but want a nested one be inside the ([SAVEPOINT](https://www.postgresql.org/docs/current/sql-savepoint.html))—use `with_savepoint`:\n\n```ruby\nclass AddItemToCart \u003c CleanActions::Base\n  def perform_actions\n    @order = CreateOrder.call(user: @user, with_savepoint: true)\n  end\nend\n```\n\nNote that `after_commit` still happens when the transaction from the root action is commited.\n\n## Error configuration\n\nWhen something weird happens during the action execution, the message is sent to the Rails log. Also, errors are _raised_ in development and test environments. To change that you can use `.config` object:\n\n```ruby\nCleanActions.config.raise_errors = true\n```\n\nHere is a list of errors affected by this config:\n\n- type mismatch from (Typed Returns)[/README.md#Typed-Returns];\n- action with (#before_transaction)[/README.md#before_transaction] is called inside the transaction;\n- invalid isolation levels;\n- action calls from unexpected places.\n\n## Advanced Lifecycle\n\nThis section contains some additional hooks to improve your actions.\n\n### before_transaction\n\nIf you want to do something outside the transaction (e.g., some IO operation)—use `before_transaction`:\n\n```ruby\nclass SyncData \u003c CleanActions::Base\n  def before_transaction\n    @response = ApiClient.fetch\n  end\n\n  def perform_actions\n    # use response\n  end\nend\n```\n\nPlease note, that error will be risen if this action will be called from another action (and transaction will be already in progress):\n\n```ruby\nclass OtherAction \u003c CleanActions::Base\n  def perform_actions\n    SyncData.call\n  end\nend\n\nOtherAction.call # =\u003e \"SyncData#before_transaction was called inside the transaction\"  is logged\n```\n\n⚠️ Do not call other actions from this method!\n\n### before_actions\n\nIf you want to do something before action — use `#before_action` callback, that is run inside the transaction but before `#perform_actions`:\n\n```ruby\nclass AddItemToCart \u003c CleanActions::Base\n  def before_actions\n    @order = Order.find(order_id)\n  end\n\n  def perform_actions\n    # use order\n  end\nend\n```\n\n⚠️ Do not call other actions from this method!\n\n### fail_with\n\nFail with is a syntax sugar over `#fail!` to decouple pre–checks from the execution logic. Take a look at the improved example from the [Error Handling](/README.md#Error-Handling) section:\n\n```ruby\nclass AddItemToCart \u003c CleanActions::Base\n  fail_with(:shop_is_closed) { Time.now.hour.in?(10..18) }\n\n  def perform_actions\n    # only when shop is open\n  end\nend\n```\n\nIf you want to check that action can be called successfully (at least, preconditions are met) — you can use `#dry_call`, which will run _all_ preconditions and return all failures:\n\n```ruby\nclass CheckNumber \u003c CleanActions::Base\n  fail_with(:fail1) { @value == 1 }\n  fail_with(:fail_odd) { @value.odd? }\n\n  def initialize(value:)\n    @value = value\n  end\nend\n\nCheckNumber.dry_call(value: 1) # =\u003e [CleanActions::ActionFailure.new(:fail_odd), CleanActions::ActionFailure.new(:fail1)]\n```\n\n⚠️ Do not call other actions from this method!\n\n### rollback\n\nActions rollback things inside `#perform_actions` in case of failure because of the database transactions. However, what if you want to rollback something non–transactional?\n\nWell, if you sent an email or enqueued background job—you cannot do much,. Just in case, you want do something—here is a `#rollback` method that happens only when action fails.\n\n```ruby\nclass DumbCounter \u003c CleanActions::Base\n  def perform_actions\n    Thread.current[:counter] ||= 0\n    Thread.current[:counter] += 1\n    fail!(:didnt_i_say_its_a_dumb_counter)\n  end\n\n  def rollback\n    Thread.current[:counter] ||= 0\n    Thread.current[:counter] -= 1\n  end\nend\n\nDumbCounter.call\nThread.current[:counter] # =\u003e 0\n```\n\n### ensure\n\nOpened file inside `#perform_actions` or want to do some other cleanup even when action fails? Use `#ensure`:\n\n```ruby\nclass UseFile \u003c CleanActions::Base\n  def perform_actions\n    @file = File.open # ...\n  end\n\n  def ensure\n    @file.close\n  end\nend\n```\n\n## Contributing\n\nBug reports and pull requests are welcome on GitHub at https://github.com/DmitryTsepelev/clean_actions.\n\n## License\n\nThe gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).\n","funding_links":[],"categories":["Ruby"],"sub_categories":[],"project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2FDmitryTsepelev%2Fclean_actions","html_url":"https://awesome.ecosyste.ms/projects/github.com%2FDmitryTsepelev%2Fclean_actions","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2FDmitryTsepelev%2Fclean_actions/lists"}