{"id":13878709,"url":"https://github.com/sunny/actor","last_synced_at":"2025-04-10T13:55:26.657Z","repository":{"id":37999073,"uuid":"247253582","full_name":"sunny/actor","owner":"sunny","description":"Composable Ruby service objects","archived":false,"fork":false,"pushed_at":"2025-02-24T14:18:11.000Z","size":322,"stargazers_count":741,"open_issues_count":4,"forks_count":30,"subscribers_count":11,"default_branch":"main","last_synced_at":"2025-04-03T08:04:05.023Z","etag":null,"topics":["ruby-on-rails","service-objects"],"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/sunny.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":"CONTRIBUTING.md","funding":null,"license":"LICENSE.txt","code_of_conduct":"CODE_OF_CONDUCT.md","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-03-14T10:12:53.000Z","updated_at":"2025-03-31T14:11:21.000Z","dependencies_parsed_at":"2024-01-13T20:38:17.316Z","dependency_job_id":"b3e58a26-0da6-4baa-8d42-005179c81c45","html_url":"https://github.com/sunny/actor","commit_stats":{"total_commits":190,"total_committers":20,"mean_commits":9.5,"dds":0.2210526315789474,"last_synced_commit":"e0d249528053f39346521e0850163c9485fe4400"},"previous_names":[],"tags_count":23,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/sunny%2Factor","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/sunny%2Factor/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/sunny%2Factor/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/sunny%2Factor/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/sunny","download_url":"https://codeload.github.com/sunny/actor/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":248229607,"owners_count":21068943,"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":["ruby-on-rails","service-objects"],"created_at":"2024-08-06T08:01:57.365Z","updated_at":"2025-04-10T13:55:26.636Z","avatar_url":"https://github.com/sunny.png","language":"Ruby","funding_links":[],"categories":["Ruby"],"sub_categories":[],"readme":"# ServiceActor\n\nThis Ruby gem lets you move your application logic into small composable\nservice objects. It is a lightweight framework that helps you keep your models\nand controllers thin.\n\n![Photo of theater seats](https://user-images.githubusercontent.com/132/78340166-e7567000-7595-11ea-97c0-b3e5da2de7a1.png)\n\n## Contents\n\n- [Installation](#installation)\n- [Usage](#usage)\n  - [Inputs](#inputs)\n  - [Outputs](#outputs)\n  - [Fail](#fail)\n- [Play actors in a sequence](#play-actors-in-a-sequence)\n  - [Rollback](#rollback)\n  - [Inline actors](#inline-actors)\n  - [Play conditions](#play-conditions)\n  - [Input aliases](#input-aliases)\n- [Input options](#input-options)\n  - [Defaults](#defaults)\n  - [Allow nil](#allow-nil)\n  - [Conditions](#conditions)\n  - [Types](#types)\n  - [Custom input errors](#custom-input-errors)\n- [Testing](#testing)\n- [FAQ](#faq)\n- [Thanks](#thanks)\n- [Contributing](#contributing)\n- [License](#contributing)\n\n## Installation\n\nAdd the gem to your application’s Gemfile by executing:\n\n```sh\nbundle add service_actor\n```\n\n### Extensions\n\nFor **Rails generators**, you can use the\n[service_actor-rails](https://github.com/sunny/actor-rails) gem:\n\n```sh\nbundle add service_actor-rails\n```\n\nFor **TTY prompts**, you can use the\n[service_actor-promptable](https://github.com/pboling/service_actor-promptable) gem:\n\n```sh\nbundle add service_actor-promptable\n```\n\n## Usage\n\nActors are single-purpose actions in your application that represent your\nbusiness logic. They start with a verb, inherit from `Actor` and implement a\n`call` method.\n\n```rb\n# app/actors/send_notification.rb\nclass SendNotification \u003c Actor\n  def call\n    # …\n  end\nend\n```\n\nTrigger them in your application with `.call`:\n\n```rb\nSendNotification.call # =\u003e \u003cServiceActor::Result…\u003e\n```\n\nWhen called, an actor returns a result. Reading and writing to this result allows\nactors to accept and return multiple arguments. Let’s find out how to do that\nand then we’ll see how to\n[chain multiple actors together](#play-actors-in-a-sequence).\n\n### Inputs\n\nTo accept arguments, use `input` to create a method named after this input:\n\n```rb\nclass GreetUser \u003c Actor\n  input :user\n\n  def call\n    puts \"Hello #{user.name}!\"\n  end\nend\n```\n\nYou can now call your actor by providing the correct arguments:\n\n```rb\nGreetUser.call(user: User.first)\n```\n\n### Outputs\n\nAn actor can return multiple arguments. Declare them using `output`, which adds\na setter method to let you modify the result from your actor:\n\n```rb\nclass BuildGreeting \u003c Actor\n  output :greeting\n\n  def call\n    self.greeting = \"Have a wonderful day!\"\n  end\nend\n```\n\nThe result you get from calling an actor will include the outputs you set:\n\n```rb\nactor = BuildGreeting.call\nactor.greeting # =\u003e \"Have a wonderful day!\"\nactor.greeting? # =\u003e true\n```\n\nIf you only have one value you want from an actor, you can skip defining an\noutput by making it the return value of `.call()` and calling your actor with\n`.value()`:\n\n```rb\nclass BuildGreeting \u003c Actor\n  input :name\n\n  def call\n    \"Have a wonderful day, #{name}!\"\n  end\nend\n\nBuildGreeting.value(name: \"Fred\") # =\u003e \"Have a wonderful day, Fred!\"\n```\n\n### Fail\n\nTo stop the execution and mark an actor as having failed, use `fail!`:\n\n```rb\nclass UpdateUser \u003c Actor\n  input :user\n  input :attributes\n\n  def call\n    user.attributes = attributes\n\n    fail!(error: \"Invalid user\") unless user.valid?\n\n    # …\n  end\nend\n```\n\nThis will raise an error in your application with the given data added to the\nresult.\n\nTo test for the success of your actor instead of raising an exception, use\n`.result` instead of `.call`. You can then call `success?` or `failure?` on\nthe result.\n\nFor example in a Rails controller:\n\n```rb\n# app/controllers/users_controller.rb\nclass UsersController \u003c ApplicationController\n  def create\n    actor = UpdateUser.result(user: user, attributes: user_attributes)\n    if actor.success?\n      redirect_to actor.user\n    else\n      render :new, notice: actor.error\n    end\n  end\nend\n```\n\n## Play actors in a sequence\n\nTo help you create actors that are small, single-responsibility actions, an\nactor can use `play` to call other actors:\n\n```rb\nclass PlaceOrder \u003c Actor\n  play CreateOrder,\n       PayOrder,\n       SendOrderConfirmation,\n       NotifyAdmins\nend\n```\n\nCalling this actor will now call every actor along the way. Inputs and outputs\nwill go from one actor to the next, all sharing the same result set until it is\nfinally returned.\n\nIf you use `.value()` to call this actor, it will give the return value of\nthe final actor in the play chain.\n\n### Rollback\n\nWhen using `play`, if an actor calls `fail!`, the following actors will not be\ncalled.\n\nInstead, all the actors that succeeded will have their `rollback` method called\nin reverse order. This allows actors a chance to cleanup, for example:\n\n```rb\nclass CreateOrder \u003c Actor\n  output :order\n\n  def call\n    self.order = Order.create!(…)\n  end\n\n  def rollback\n    order.destroy\n  end\nend\n```\n\nRollback is only called on the _previous_ actors in `play` and is not called on\nthe failing actor itself. Actors should be kept to a single purpose and not have\nanything to clean up if they call `fail!`.\n\n### Inline actors\n\nFor small work or preparing the result set for the next actors, you can create\ninline actors by using lambdas. Each lambda has access to the shared result. For\nexample:\n\n```rb\nclass PayOrder \u003c Actor\n  input :order\n\n  play -\u003e actor { actor.order.currency ||= \"EUR\" },\n       CreatePayment,\n       UpdateOrderBalance,\n       -\u003e actor { Logger.info(\"Order #{actor.order.id} paid\") }\nend\n```\n\nYou can also call instance methods. For example:\n\n```rb\nclass PayOrder \u003c Actor\n  input :order\n\n  play :assign_default_currency,\n       CreatePayment,\n       UpdateOrderBalance,\n       :log_payment\n\n  private\n\n  def assign_default_currency\n    order.currency ||= \"EUR\"\n  end\n\n  def log_payment\n    Logger.info(\"Order #{order.id} paid\")\n  end\nend\n```\n\nIf you want to do work around the whole actor, you can also override the `call`\nmethod. For example:\n\n```rb\nclass PayOrder \u003c Actor\n  # …\n\n  def call\n    Time.with_timezone(\"Paris\") do\n      super\n    end\n  end\nend\n```\n\n### Play conditions\n\nActors in a play can be called conditionally:\n\n```rb\nclass PlaceOrder \u003c Actor\n  play CreateOrder,\n       Pay\n  play NotifyAdmins, if: -\u003e actor { actor.order.amount \u003e 42 }\n  play CreatePayment, unless: -\u003e actor { actor.order.currency == \"USD\" }\nend\n```\n\n### Input aliases\n\nYou can use `alias_input` to transform the output of an actor into the input of\nthe next actors.\n\n```rb\nclass PlaceComment \u003c Actor\n  play CreateComment,\n       NotifyCommentFollowers,\n       alias_input(commenter: :user),\n       UpdateUserStats\nend\n```\n\n## Input options\n\n### Defaults\n\nInputs can be optional by providing a `default` value in a lambda.\n\n```rb\nclass BuildGreeting \u003c Actor\n  input :name\n  input :adjective, default: -\u003e { \"wonderful\" }\n  input :length_of_time, default: -\u003e { [\"day\", \"week\", \"month\"].sample }\n  input :article,\n        default: -\u003e actor { actor.adjective.match?(/^[aeiou]/) ? \"an\" : \"a\" }\n\n  output :greeting\n\n  def call\n    self.greeting = \"Have #{article} #{adjective} #{length_of_time}, #{name}!\"\n  end\nend\n\nactor = BuildGreeting.call(name: \"Jim\")\nactor.greeting # =\u003e \"Have a wonderful week, Jim!\"\n\nactor = BuildGreeting.call(name: \"Siobhan\", adjective: \"elegant\")\nactor.greeting # =\u003e \"Have an elegant week, Siobhan!\"\n```\n\nWhile lambdas are the preferred way to specify defaults, you can also provide\na default value without using lambdas by using an immutable object.\n\n```rb\n# frozen_string_literal: true\n\nclass ExampleActor \u003c Actor\n  input :options, default: {\n    names: {male: \"Iaroslav\", female: \"Anna\"}.freeze,\n    country_codes: %w[gb ru].freeze\n  }.freeze\nend\n```\n\nNote that default values might be mutated if the values returned by the lambda\nare references to mutable objects, e.g.\n\n```rb\nclass ExampleActor \u003c Actor\n  input :options, default: -\u003e { Registry::DEFAULT_OPTIONS } # `Registry::DEFAULT_OPTIONS` is not frozen\n\n  def call\n    options[:names] = nil\n  end\nend\n```\n\n### Allow nil\n\nBy default inputs accept `nil` values. To raise an error instead:\n\n```rb\nclass UpdateUser \u003c Actor\n  input :user, allow_nil: false\n\n  # …\nend\n```\n\n### Conditions\n\nYou can ensure an input is included in a collection by using `inclusion`:\n\n```rb\nclass Pay \u003c Actor\n  input :currency, inclusion: %w[EUR USD]\n\n  # …\nend\n```\n\nThis raises an argument error if the input does not match one of the given\nvalues.\n\nDeclare custom conditions with the name of your choice by using `must`:\n\n```rb\nclass UpdateAdminUser \u003c Actor\n  input :user,\n        must: {\n          be_an_admin: -\u003e user { user.admin? }\n        }\n\n  # …\nend\n```\n\nThis will raise an argument error if any of the given lambdas returns a falsey\nvalue.\n\n### Types\n\nSometimes it can help to have a quick way of making sure we didn’t mess up our\ninputs.\n\nFor that you can use the `type` option and giving a class or an array\nof possible classes. If the input or output doesn’t match these types, an\nerror is raised.\n\n```rb\nclass UpdateUser \u003c Actor\n  input :user, type: User\n  input :age, type: [Integer, Float]\n\n  # …\nend\n```\n\nYou may also use strings instead of constants, such as `type: \"User\"`.\n\nWhen using a type condition, `allow_nil` defaults to `false`.\n\n### Custom input errors\n\nUse a `Hash` with `is:` and `message:` keys to prepare custom\nerror messages on inputs. For example:\n\n```rb\nclass UpdateAdminUser \u003c Actor\n  input :user,\n        must: {\n          be_an_admin: {\n            is: -\u003e user { user.admin? },\n            message: \"The user is not an administrator\"\n          }\n        }\n\n  # ...\nend\n```\n\nYou can also use incoming arguments when shaping your error text:\n\n```rb\nclass UpdateUser \u003c Actor\n  input :user,\n        allow_nil: {\n          is: false,\n          message: (lambda do |input_key:, **|\n            \"The value \\\"#{input_key}\\\" cannot be empty\"\n          end)\n        }\n\n  # ...\nend\n```\n\n\u003cdetails\u003e\n  \u003csummary\u003eSee examples of custom messages on all input arguments\u003c/summary\u003e\n\n  #### Inclusion\n\n  ```ruby\n  class Pay \u003c Actor\n    input :provider,\n          inclusion: {\n            in: [\"MANGOPAY\", \"PayPal\", \"Stripe\"],\n            message: (lambda do |value:, **|\n              \"Payment system \\\"#{value}\\\" is not supported\"\n            end)\n          }\n  end\n  ```\n\n  #### Must\n\n  ```ruby\n  class Pay \u003c Actor\n    input :provider,\n          must: {\n            exist: {\n              is: -\u003e provider { PROVIDERS.include?(provider) },\n              message: (lambda do |value:, **|\n                \"The specified provider \\\"#{value}\\\" was not found.\"\n              end)\n            }\n          }\n  end\n  ```\n\n  #### Type\n\n  ```ruby\n  class ReduceOrderAmount \u003c Actor\n    input :bonus_applied,\n          type: {\n            is: [TrueClass, FalseClass],\n            message: (lambda do |input_key:, expected_type:, given_type:, **|\n              \"Wrong type \\\"#{given_type}\\\" for \\\"#{input_key}\\\". \" \\\n                \"Expected: \\\"#{expected_type}\\\"\"\n            end)\n          }\n  end\n  ```\n\n  #### Allow nil\n\n  ```ruby\n  class CreateUser \u003c Actor\n    input :name,\n          allow_nil: {\n            is: false,\n            message: (lambda do |input_key:, **|\n              \"The value \\\"#{input_key}\\\" cannot be empty\"\n            end)\n          }\n  end\n  ```\n\n\u003c/details\u003e\n\n## Testing\n\nIn your application, add automated testing to your actors as you would do to any\nother part of your applications.\n\nYou will find that cutting your business logic into single purpose actors will\nmake it easier for you to test your application.\n\n## FAQ\n\nHowtos and frequently asked questions can be found on the\n[wiki](https://github.com/sunny/actor/wiki).\n\n## Thanks\n\nThis gem is influenced by (and compatible with)\n[Interactor](https://github.com/sunny/actor/wiki/Interactor).\n\nThank you to the wonderful\n[contributors](https://github.com/sunny/actor/graphs/contributors).\n\nThank you to @nicoolas25, @AnneSottise \u0026 @williampollet for the early thoughts\nand feedback on this gem.\n\nPhoto by [Lloyd Dirks](https://unsplash.com/photos/4SLz_RCk6kQ).\n\n## Contributing\n\nSee\n[CONTRIBUTING.md](https://github.com/sunny/actor/blob/main/CONTRIBUTING.md).\n\n## License\n\nThe gem is available as open source under the terms of the\n[MIT License](https://choosealicense.com/licenses/mit/).\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fsunny%2Factor","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fsunny%2Factor","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fsunny%2Factor/lists"}