{"id":15029202,"url":"https://github.com/aaronlasseigne/active_interaction","last_synced_at":"2025-12-17T01:22:44.128Z","repository":{"id":9179223,"uuid":"10980701","full_name":"AaronLasseigne/active_interaction","owner":"AaronLasseigne","description":":briefcase: Manage application specific business logic.","archived":false,"fork":false,"pushed_at":"2025-02-02T23:36:24.000Z","size":1955,"stargazers_count":2107,"open_issues_count":19,"forks_count":142,"subscribers_count":24,"default_branch":"main","last_synced_at":"2025-05-11T11:06:15.420Z","etag":null,"topics":["activemodel","command-pattern","method-object","ruby","service-object"],"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/AaronLasseigne.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":"CONTRIBUTING.md","funding":null,"license":"LICENSE.md","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":"2013-06-26T22:19:51.000Z","updated_at":"2025-05-10T22:31:49.000Z","dependencies_parsed_at":"2023-10-16T10:30:13.198Z","dependency_job_id":"6c90df80-d4fb-4073-ae4d-e3b8da8bcd02","html_url":"https://github.com/AaronLasseigne/active_interaction","commit_stats":{"total_commits":1586,"total_committers":32,"mean_commits":49.5625,"dds":0.4047919293820933,"last_synced_commit":"5787f9bda7627b7f0322b295d3899004f0a8dd45"},"previous_names":["orgsync/active_interaction"],"tags_count":93,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/AaronLasseigne%2Factive_interaction","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/AaronLasseigne%2Factive_interaction/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/AaronLasseigne%2Factive_interaction/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/AaronLasseigne%2Factive_interaction/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/AaronLasseigne","download_url":"https://codeload.github.com/AaronLasseigne/active_interaction/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":253554082,"owners_count":21926612,"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":["activemodel","command-pattern","method-object","ruby","service-object"],"created_at":"2024-09-24T20:09:57.182Z","updated_at":"2025-12-17T01:22:44.042Z","avatar_url":"https://github.com/AaronLasseigne.png","language":"Ruby","readme":"# [ActiveInteraction][]\n\nActiveInteraction manages application-specific business logic.\nIt's an implementation of service objects designed to blend seamlessly into Rails.\nIt also helps you write safer code by validating that your inputs conform to your expectations.\nIf ActiveModel deals with your nouns, then ActiveInteraction handles your verbs.\n\n[![Version](https://img.shields.io/gem/v/active_interaction.svg?style=flat-square)](https://rubygems.org/gems/active_interaction)\n[![Test](https://img.shields.io/github/actions/workflow/status/AaronLasseigne/active_interaction/test.yml?label=Test\u0026style=flat-square\u0026branch=main)](https://github.com/AaronLasseigne/active_interaction/actions?query=workflow%3ATest)\n\n- [Installation](#installation)\n- [Basic usage](#basic-usage)\n  - [Validations](#validations)\n- [Filters](#filters)\n  - [Basic Filters](#basic-filters)\n    - [Array](#array)\n    - [Boolean](#boolean)\n    - [File](#file)\n    - [Hash](#hash)\n    - [String](#string)\n    - [Symbol](#symbol)\n    - [Dates and times](#dates-and-times)\n      - [Date](#date)\n      - [DateTime](#datetime)\n      - [Time](#time)\n    - [Numbers](#numbers)\n      - [Decimal](#decimal)\n      - [Float](#float)\n      - [Integer](#integer)\n  - [Advanced Filters](#advanced-filters)\n    - [Interface](#interface)\n    - [Object](#object)\n    - [Record](#record)\n- [Rails](#rails)\n  - [Setup](#setup)\n  - [Controller](#controller)\n    - [Index](#index)\n    - [Show](#show)\n    - [New](#new)\n    - [Create](#create)\n    - [Destroy](#destroy)\n    - [Edit](#edit)\n    - [Update](#update)\n- [Advanced usage](#advanced-usage)\n  - [Callbacks](#callbacks)\n  - [Composition](#composition)\n  - [Defaults](#defaults)\n  - [Descriptions](#descriptions)\n  - [Errors](#errors)\n  - [Forms](#forms)\n  - [Shared input options](#shared-input-options)\n  - [Optional inputs](#optional-inputs)\n  - [Translations](#translations)\n- [Credits](#credits)\n\n[API Documentation][]\n\n## Installation\n\nAdd it to your Gemfile:\n\n``` rb\ngem 'active_interaction', '~\u003e 5.5'\n```\n\nOr install it manually:\n\n``` sh\n$ gem install active_interaction --version '~\u003e 5.5'\n```\n\nThis project uses [Semantic Versioning][]. Check out [GitHub releases][] for a\ndetailed list of changes.\n\n## Basic usage\n\nTo define an interaction, create a subclass of `ActiveInteraction::Base`. Then\nyou need to do two things:\n\n1.  **Define your inputs.** Use class filter methods to define what you expect\n    your inputs to look like. For instance, if you need a boolean flag for\n    pepperoni, use `boolean :pepperoni`. Check out [the filters\n    section](#filters) for all the available options.\n\n2.  **Define your business logic.** Do this by implementing the `#execute`\n    method. Each input you defined will be available as the type you specified.\n    If any of the inputs are invalid, `#execute` won't be run. Filters are\n    responsible for checking your inputs. Check out [the validations\n    section](#validations) if you need more than that.\n\nThat covers the basics. Let's put it all together into a simple example that\nsquares a number.\n\n``` rb\nrequire 'active_interaction'\n\nclass Square \u003c ActiveInteraction::Base\n  float :x\n\n  def execute\n    x**2\n  end\nend\n```\n\nCall `.run` on your interaction to execute it. You must pass a single hash to\n`.run`. It will return an instance of your interaction. By convention, we call\nthis an outcome. You can use the `#valid?` method to ask the outcome if it's\nvalid. If it's invalid, take a look at its errors with `#errors`. In either\ncase, the value returned from `#execute` will be stored in `#result`.\n\n``` rb\noutcome = Square.run(x: 'two point one')\noutcome.valid?\n# =\u003e nil\noutcome.errors.messages\n# =\u003e {:x=\u003e[\"is not a valid float\"]}\n\noutcome = Square.run(x: 2.1)\noutcome.valid?\n# =\u003e true\noutcome.result\n# =\u003e 4.41\n```\n\nYou can also use `.run!` to execute interactions. It's like `.run` but more\ndangerous. It doesn't return an outcome. If the outcome would be invalid, it\nwill instead raise an error. But if the outcome would be valid, it simply\nreturns the result.\n\n``` rb\nSquare.run!(x: 'two point one')\n# ActiveInteraction::InvalidInteractionError: X is not a valid float\nSquare.run!(x: 2.1)\n# =\u003e 4.41\n```\n\n### Validations\n\nActiveInteraction checks your inputs. Often you'll want more than that.\nFor instance, you may want an input to be a string with at least one\nnon-whitespace character. Instead of writing your own validation for that, you\ncan use validations from ActiveModel.\n\nThese validations aren't provided by ActiveInteraction. They're from\nActiveModel. You can also use any custom validations you wrote yourself in your\ninteractions.\n\n``` rb\nclass SayHello \u003c ActiveInteraction::Base\n  string :name\n\n  validates :name,\n    presence: true\n\n  def execute\n    \"Hello, #{name}!\"\n  end\nend\n```\n\nWhen you run this interaction, two things will happen. **First\nActiveInteraction will check your inputs. Then ActiveModel will validate\nthem.** If both of those are happy, it will be executed.\n\n``` rb\nSayHello.run!(name: nil)\n# ActiveInteraction::InvalidInteractionError: Name is required\n\nSayHello.run!(name: '')\n# ActiveInteraction::InvalidInteractionError: Name can't be blank\n\nSayHello.run!(name: 'Taylor')\n# =\u003e \"Hello, Taylor!\"\n```\n\n## Filters\n\nYou can define filters inside an interaction using the appropriate class\nmethod. Each method has the same signature:\n\n- Some symbolic names. These are the attributes to create.\n\n- An optional hash of options. Each filter supports at least these two options:\n\n  - `default` is the fallback value to use if `nil` is given. To make a filter\n    optional, set `default: nil`.\n\n  - `desc` is a human-readable description of the input. This can be useful for\n    generating documentation. For more information about this, read [the\n    descriptions section](#descriptions).\n\n- An optional block of sub-filters. Only [array](#array) and [hash](#hash)\n  filters support this. Other filters will ignore blocks when given to them.\n\nLet's take a look at an example filter. It defines three inputs: `x`, `y`, and\n`z`. Those inputs are optional and they all share the same description (\"an\nexample filter\").\n\n``` rb\narray :x, :y, :z,\n  default: nil,\n  desc: 'an example filter' do\n    # Some filters support sub-filters here.\n  end\n```\n\nIn general, filters accept values of the type they correspond to, plus a few\nalternatives that can be reasonably coerced. Typically the coercions come from\nRails, so `\"1\"` can be interpreted as the boolean value `true`, the string\n`\"1\"`, or the number `1`.\n\n### Basic Filters\n\n#### Array\n\nIn addition to accepting arrays, array inputs will convert\n`ActiveRecord::Relation`s into arrays.\n\n``` rb\nclass ArrayInteraction \u003c ActiveInteraction::Base\n  array :toppings\n\n  def execute\n    toppings.size\n  end\nend\n\nArrayInteraction.run!(toppings: 'everything')\n# ActiveInteraction::InvalidInteractionError: Toppings is not a valid array\nArrayInteraction.run!(toppings: [:cheese, 'pepperoni'])\n# =\u003e 2\n```\n\nUse a block to constrain the types of elements an array can contain. Note that\nyou can only have one filter inside an array block, and it must not have a name.\n\n``` rb\narray :birthdays do\n  date\nend\n```\n\nFor `interface`, `object`, and `record` filters, the name of the array filter\nwill be singularized and used to determine the type of value passed. In the\nexample below, the objects passed would need to be of type `Cow`.\n\n``` rb\narray :cows do\n  object\nend\n```\n\nYou can override this by passing the necessary information to the inner filter.\n\n```ruby\narray :managers do\n  object class: People\nend\n```\n\nErrors that occur will be indexed based on the Rails configuration setting\n`index_nested_attribute_errors`. You can also manually override this setting\nwith the `:index_errors` option. In this state is is possible to get multiple\nerrors from a single filter.\n\n```ruby\nclass ArrayInteraction \u003c ActiveInteraction::Base\n  array :favorite_numbers, index_errors: true do\n    integer\n  end\n\n  def execute\n    favorite_numbers\n  end\nend\n\nArrayInteraction.run(favorite_numbers: [8, 'bazillion']).errors.details\n=\u003e {:\"favorite_numbers[1]\"=\u003e[{:error=\u003e:invalid_type, :type=\u003e\"array\"}]}\n```\n\nWith `:index_errors` set to `false` the error would have been:\n\n```ruby\n{:favorite_numbers=\u003e[{:error=\u003e:invalid_type, :type=\u003e\"array\"}]}\n```\n\n#### Boolean\n\nBoolean filters convert the strings `\"1\"`, `\"true\"`, and `\"on\"`\n(case-insensitive) into `true`. They also convert `\"0\"`, `\"false\"`, and `\"off\"`\ninto `false`. Blank strings will be treated as `nil`.\n\nBoolean values can be accessed using the filter name but can also be checked\nusing a predicate method of the same name.\n\n``` rb\nclass BooleanInteraction \u003c ActiveInteraction::Base\n  boolean :kool_aid\n\n  def execute\n    'Oh yeah!' if kool_aid? # could also use `kool_aid`\n  end\nend\n\nBooleanInteraction.run!(kool_aid: 1)\n# ActiveInteraction::InvalidInteractionError: Kool aid is not a valid boolean\nBooleanInteraction.run!(kool_aid: true)\n# =\u003e \"Oh yeah!\"\n```\n\n#### File\n\nFile filters also accept `TempFile`s and anything that responds to `#rewind`.\nThat means that you can pass the `params` from uploading files via forms in\nRails.\n\n``` rb\nclass FileInteraction \u003c ActiveInteraction::Base\n  file :readme\n\n  def execute\n    readme.size\n  end\nend\n\nFileInteraction.run!(readme: 'README.md')\n# ActiveInteraction::InvalidInteractionError: Readme is not a valid file\nFileInteraction.run!(readme: File.open('README.md'))\n# =\u003e 21563\n```\n\n#### Hash\n\nHash filters accept hashes. The expected value types are given by passing a\nblock and nesting other filters. You can have any number of filters inside a\nhash, including other hashes.\n\n``` rb\nclass HashInteraction \u003c ActiveInteraction::Base\n  hash :preferences do\n    boolean :newsletter\n    boolean :sweepstakes\n  end\n\n  def execute\n    puts 'Thanks for joining the newsletter!' if preferences[:newsletter]\n    puts 'Good luck in the sweepstakes!' if preferences[:sweepstakes]\n  end\nend\n\nHashInteraction.run!(preferences: 'yes, no')\n# ActiveInteraction::InvalidInteractionError: Preferences is not a valid hash\nHashInteraction.run!(preferences: { newsletter: true, 'sweepstakes' =\u003e false })\n# Thanks for joining the newsletter!\n# =\u003e nil\n```\n\nSetting default hash values can be tricky. The default value has to be either\n`nil` or `{}`. Use `nil` to make the hash optional. Use `{}` if you want to set\nsome defaults for values inside the hash. If any nested filter uses a\n[lazy default](#defaults) then the hash must also use a lazy default.\n\n``` rb\nhash :optional,\n  default: nil\n# =\u003e {:optional=\u003enil}\n\nhash :with_defaults,\n  default: {} do\n    boolean :likes_cookies,\n      default: true\n  end\n# =\u003e {:with_defaults=\u003e{:likes_cookies=\u003etrue}}\n```\n\nBy default, hashes remove any keys that aren't given as nested filters. To\nallow all hash keys, set `strip: false`. In general we don't recommend doing\nthis, but it's sometimes necessary.\n\n``` rb\nhash :stuff,\n  strip: false\n```\n\n#### String\n\nString filters define inputs that only accept strings.\n\n``` rb\nclass StringInteraction \u003c ActiveInteraction::Base\n  string :name\n\n  def execute\n    \"Hello, #{name}!\"\n  end\nend\n\nStringInteraction.run!(name: 0xDEADBEEF)\n# ActiveInteraction::InvalidInteractionError: Name is not a valid string\nStringInteraction.run!(name: 'Taylor')\n# =\u003e \"Hello, Taylor!\"\n```\n\nString filter strips leading and trailing whitespace by default. To disable it, set the\n`strip` option to `false`.\n\n``` rb\nstring :comment,\n  strip: false\n```\n\n#### Symbol\n\nSymbol filters define inputs that accept symbols. Strings will be converted\ninto symbols.\n\n``` rb\nclass SymbolInteraction \u003c ActiveInteraction::Base\n  symbol :method\n\n  def execute\n    method.to_proc\n  end\nend\n\nSymbolInteraction.run!(method: -\u003e {})\n# ActiveInteraction::InvalidInteractionError: Method is not a valid symbol\nSymbolInteraction.run!(method: :object_id)\n# =\u003e #\u003cProc:0x007fdc9ba94118\u003e\n```\n\n#### Dates and times\n\nFilters that work with dates and times behave similarly. By default, they all\nconvert strings into their expected data types using `.parse`. Blank strings\nwill be treated as `nil`. If you give the `format` option, they will instead\nconvert strings using `.strptime`. Note that formats won't work with `DateTime`\nand `Time` filters if a time zone is set.\n\n##### Date\n\n``` rb\nclass DateInteraction \u003c ActiveInteraction::Base\n  date :birthday\n\n  def execute\n    birthday + (18 * 365)\n  end\nend\n\nDateInteraction.run!(birthday: 'yesterday')\n# ActiveInteraction::InvalidInteractionError: Birthday is not a valid date\nDateInteraction.run!(birthday: Date.new(1989, 9, 1))\n# =\u003e #\u003cDate: 2007-08-28 ((2454341j,0s,0n),+0s,2299161j)\u003e\n```\n\n``` rb\ndate :birthday,\n  format: '%Y-%m-%d'\n```\n\n##### DateTime\n\n``` rb\nclass DateTimeInteraction \u003c ActiveInteraction::Base\n  date_time :now\n\n  def execute\n    now.iso8601\n  end\nend\n\nDateTimeInteraction.run!(now: 'now')\n# ActiveInteraction::InvalidInteractionError: Now is not a valid date time\nDateTimeInteraction.run!(now: DateTime.now)\n# =\u003e \"2015-03-11T11:04:40-05:00\"\n```\n\n``` rb\ndate_time :start,\n  format: '%Y-%m-%dT%H:%M:%S'\n```\n\n##### Time\n\nIn addition to converting strings with `.parse` (or `.strptime`), time filters\nconvert numbers with `.at`.\n\n``` rb\nclass TimeInteraction \u003c ActiveInteraction::Base\n  time :epoch\n\n  def execute\n    Time.now - epoch\n  end\nend\n\nTimeInteraction.run!(epoch: 'a long, long time ago')\n# ActiveInteraction::InvalidInteractionError: Epoch is not a valid time\nTimeInteraction.run!(epoch: Time.new(1970))\n# =\u003e 1426068362.5136619\n```\n\n``` rb\ntime :start,\n  format: '%Y-%m-%dT%H:%M:%S'\n```\n\n#### Numbers\n\nAll numeric filters accept numeric input. They will also convert strings using\nthe appropriate method from `Kernel` (like `.Float`). Blank strings will be\ntreated as `nil`.\n\n##### Decimal\n\n``` rb\nclass DecimalInteraction \u003c ActiveInteraction::Base\n  decimal :price\n\n  def execute\n    price * 1.0825\n  end\nend\n\nDecimalInteraction.run!(price: 'one ninety-nine')\n# ActiveInteraction::InvalidInteractionError: Price is not a valid decimal\nDecimalInteraction.run!(price: BigDecimal(1.99, 2))\n# =\u003e #\u003cBigDecimal:7fe792a42028,'0.2165E1',18(45)\u003e\n```\n\nTo specify the number of significant digits, use the `digits` option.\n\n``` rb\ndecimal :dollars,\n  digits: 2\n```\n\n##### Float\n\n``` rb\nclass FloatInteraction \u003c ActiveInteraction::Base\n  float :x\n\n  def execute\n    x**2\n  end\nend\n\nFloatInteraction.run!(x: 'two point one')\n# ActiveInteraction::InvalidInteractionError: X is not a valid float\nFloatInteraction.run!(x: 2.1)\n# =\u003e 4.41\n```\n\n##### Integer\n\n``` rb\nclass IntegerInteraction \u003c ActiveInteraction::Base\n  integer :limit\n\n  def execute\n    limit.downto(0).to_a\n  end\nend\n\nIntegerInteraction.run!(limit: 'ten')\n# ActiveInteraction::InvalidInteractionError: Limit is not a valid integer\nIntegerInteraction.run!(limit: 10)\n# =\u003e [10, 9, 8, 7, 6, 5, 4, 3, 2, 1, 0]\n```\n\nWhen a `String` is passed into an `integer` input, the value will be coerced.\nA default base of `10` is used though it may be overridden with the `base` option.\nIf a base of `0` is provided, the coercion will respect radix indicators present\nin the string.\n\n``` rb\nclass IntegerInteraction \u003c ActiveInteraction::Base\n  integer :limit1\n  integer :limit2, base: 8\n  integer :limit3, base: 0\n\n  def execute\n    [limit1, limit2, limit3]\n  end\nend\n\nIntegerInteraction.run!(limit1: 71, limit2: 71, limit3: 71)\n# =\u003e [71, 71, 71]\nIntegerInteraction.run!(limit1: \"071\", limit2: \"071\", limit3: \"0x71\")\n# =\u003e [71, 57, 113]\nIntegerInteraction.run!(limit1: \"08\", limit2: \"08\", limit3: \"08\")\nActiveInteraction::InvalidInteractionError: Limit2 is not a valid integer, Limit3 is not a valid integer\n```\n\n### Advanced Filters\n\n#### Interface\n\nInterface filters allow you to specify an interface that the passed value must\nmeet in order to pass. The name of the interface is used to look for a constant\ninside the ancestor listing for the passed value. This allows for a variety of\nchecks depending on what's passed. Class instances are checked for an included\nmodule or an inherited ancestor class. Classes are checked for an extended\nmodule or an inherited ancestor class. Modules are checked for an extended\nmodule.\n\n``` rb\nclass InterfaceInteraction \u003c ActiveInteraction::Base\n  interface :exception\n\n  def execute\n    exception\n  end\nend\n\nInterfaceInteraction.run!(exception: Exception)\n# ActiveInteraction::InvalidInteractionError: Exception is not a valid interface\nInterfaceInteraction.run!(exception: NameError) # a subclass of Exception\n# =\u003e NameError\n```\n\nYou can use `:from` to specify a class or module. This would be the equivalent\nof what's above.\n\n```rb\nclass InterfaceInteraction \u003c ActiveInteraction::Base\n  interface :error,\n    from: Exception\n\n  def execute\n    error\n  end\nend\n```\n\nYou can also create an anonymous interface on the fly by passing the `methods`\noption.\n\n``` rb\nclass InterfaceInteraction \u003c ActiveInteraction::Base\n  interface :serializer,\n    methods: %i[dump load]\n\n  def execute\n    input = '{ \"is_json\" : true }'\n    object = serializer.load(input)\n    output = serializer.dump(object)\n\n    output\n  end\nend\n\nrequire 'json'\n\nInterfaceInteraction.run!(serializer: Object.new)\n# ActiveInteraction::InvalidInteractionError: Serializer is not a valid interface\nInterfaceInteraction.run!(serializer: JSON)\n# =\u003e \"{\\\"is_json\\\":true}\"\n```\n\n#### Object\n\nObject filters allow you to require an instance of a particular class or one of\nits subclasses.\n\n``` rb\nclass Cow\n  def moo\n    'Moo!'\n  end\nend\n\nclass ObjectInteraction \u003c ActiveInteraction::Base\n  object :cow\n\n  def execute\n    cow.moo\n  end\nend\n\nObjectInteraction.run!(cow: Object.new)\n# ActiveInteraction::InvalidInteractionError: Cow is not a valid object\nObjectInteraction.run!(cow: Cow.new)\n# =\u003e \"Moo!\"\n```\n\nThe class name is automatically determined by the filter name. If your filter\nname is different than your class name, use the `class` option. It can be\neither the class, a string, or a symbol.\n\n``` rb\nobject :dolly1,\n  class: Sheep\nobject :dolly2,\n  class: 'Sheep'\nobject :dolly3,\n  class: :Sheep\n```\n\nIf you have value objects or you would like to build one object from another,\nyou can use the `converter` option. It is only called if the value provided is\nnot an instance of the class or one of its subclasses. The `converter` option\naccepts a symbol that specifies a class method on the object class or a proc.\nBoth will be passed the value and any errors thrown inside the converter will\ncause the value to be considered invalid. Any returned value that is not the\ncorrect class will also be treated as invalid. Any `default` that is not an\ninstance of the class or subclass and is not `nil` will also be converted.\n\n``` rb\nclass ObjectInteraction \u003c ActiveInteraction::Base\n  object :ip_address,\n    class: IPAddr,\n    converter: :new\n\n  def execute\n    ip_address\n  end\nend\n\nObjectInteraction.run!(ip_address: '192.168.1.1')\n# #\u003cIPAddr: IPv4:192.168.1.1/255.255.255.255\u003e\n\nObjectInteraction.run!(ip_address: 1)\n# ActiveInteraction::InvalidInteractionError: Ip address is not a valid object\n```\n\n#### Record\n\nRecord filters allow you to require an instance of a particular class (or one\nof its subclasses) or a value that can be used to locate an instance of the\nobject. If the value does not match, it will call `find` on the class of the\nrecord. This is particularly useful when working with ActiveRecord objects.\nLike an object filter, the class is derived from the name passed but can be\nspecified with the `class` option. Any `default` that is not an instance of the\nclass or subclass and is not `nil` will also be found. Blank strings passed in\nwill be treated as `nil`.\n\n``` rb\nclass RecordInteraction \u003c ActiveInteraction::Base\n  record :encoding\n\n  def execute\n    encoding\n  end\nend\n\n\u003e RecordInteraction.run!(encoding: Encoding::US_ASCII)\n=\u003e #\u003cEncoding:US-ASCII\u003e\n\n\u003e RecordInteraction.run!(encoding: 'ascii')\n=\u003e #\u003cEncoding:US-ASCII\u003e\n```\n\nA different method can be specified by providing a symbol to the `finder` option.\n\n## Rails\n\nActiveInteraction plays nicely with Rails. You can use interactions to handle\nyour business logic instead of models or controllers. To see how it all works,\nlet's take a look at a complete example of a controller with the typical\nresourceful actions.\n\n### Setup\n\nWe recommend putting your interactions in `app/interactions`. It's also very\nhelpful to group them by model. That way you can look in\n`app/interactions/accounts` for all the ways you can interact with accounts.\n\n```\n- app/\n  - controllers/\n    - accounts_controller.rb\n  - interactions/\n    - accounts/\n      - create_account.rb\n      - destroy_account.rb\n      - find_account.rb\n      - list_accounts.rb\n      - update_account.rb\n  - models/\n    - account.rb\n  - views/\n    - account/\n      - edit.html.erb\n      - index.html.erb\n      - new.html.erb\n      - show.html.erb\n```\n\n### Controller\n\n#### Index\n\n``` rb\n# GET /accounts\ndef index\n  @accounts = ListAccounts.run!\nend\n```\n\nSince we're not passing any inputs to `ListAccounts`, it makes sense to use\n`.run!` instead of `.run`. If it failed, that would mean we probably messed up\nwriting the interaction.\n\n``` rb\nclass ListAccounts \u003c ActiveInteraction::Base\n  def execute\n    Account.not_deleted.order(last_name: :asc, first_name: :asc)\n  end\nend\n```\n\n#### Show\n\nUp next is the show action. For this one we'll define a helper method to handle\nraising the correct errors. We have to do this because calling `.run!` would\nraise an `ActiveInteraction::InvalidInteractionError` instead of an\n`ActiveRecord::RecordNotFound`. That means Rails would render a 500 instead of\na 404.\n\n``` rb\n# GET /accounts/:id\ndef show\n  @account = find_account!\nend\n\nprivate\n\ndef find_account!\n  outcome = FindAccount.run(params)\n\n  if outcome.valid?\n    outcome.result\n  else\n    fail ActiveRecord::RecordNotFound, outcome.errors.full_messages.to_sentence\n  end\nend\n```\n\nThis probably looks a little different than you're used to. Rails commonly\nhandles this with a `before_filter` that sets the `@account` instance variable.\nWhy is all this interaction code better? Two reasons: One, you can reuse the\n`FindAccount` interaction in other places, like your API controller or a Resque\ntask. And two, if you want to change how accounts are found, you only have to\nchange one place.\n\nInside the interaction, we could use `#find` instead of `#find_by_id`. That way\nwe wouldn't need the `#find_account!` helper method in the controller because\nthe error would bubble all the way up. However, you should try to avoid raising\nerrors from interactions. If you do, you'll have to deal with raised exceptions\nas well as the validity of the outcome.\n\n``` rb\nclass FindAccount \u003c ActiveInteraction::Base\n  integer :id\n\n  def execute\n    account = Account.not_deleted.find_by_id(id)\n\n    if account\n      account\n    else\n      errors.add(:id, 'does not exist')\n    end\n  end\nend\n```\n\nNote that it's perfectly fine to add errors during execution. Not all errors\nhave to come from checking or validation.\n\n#### New\n\nThe new action will be a little different than the ones we've looked at so far.\nInstead of calling `.run` or `.run!`, it's going to initialize a new\ninteraction. This is possible because interactions behave like ActiveModels.\n\n``` rb\n# GET /accounts/new\ndef new\n  @account = CreateAccount.new\nend\n```\n\nSince interactions behave like ActiveModels, we can use ActiveModel validations\nwith them. We'll use validations here to make sure that the first and last\nnames are not blank. [The validations section](#validations) goes into more\ndetail about this.\n\n``` rb\nclass CreateAccount \u003c ActiveInteraction::Base\n  string :first_name, :last_name\n\n  validates :first_name, :last_name,\n    presence: true\n\n  def to_model\n    Account.new\n  end\n\n  def execute\n    account = Account.new(inputs)\n\n    unless account.save\n      errors.merge!(account.errors)\n    end\n\n    account\n  end\nend\n```\n\nWe used a couple of advanced features here. The `#to_model` method helps\ndetermine the correct form to use in the view. Check out [the section on\nforms](#forms) for more about that. Inside `#execute`, we merge errors. This is\na convenient way to move errors from one object to another. Read more about it\nin [the errors section](#errors).\n\n#### Create\n\nThe create action has a lot in common with the new action. Both of them use the\n`CreateAccount` interaction. And if creating the account fails, this action\nfalls back to rendering the new action.\n\n``` rb\n# POST /accounts\ndef create\n  outcome = CreateAccount.run(params.fetch(:account, {}))\n\n  if outcome.valid?\n    redirect_to(outcome.result)\n  else\n    @account = outcome\n    render(:new)\n  end\nend\n```\n\nNote that we have to pass a hash to `.run`. Passing `nil` is an error.\n\nSince we're using an interaction, we don't need strong parameters. The\ninteraction will ignore any inputs that weren't defined by filters. So you can\nforget about `params.require` and `params.permit` because interactions handle\nthat for you.\n\n#### Destroy\n\nThe destroy action will reuse the `#find_account!` helper method we wrote\nearlier.\n\n``` rb\n# DELETE /accounts/:id\ndef destroy\n  DestroyAccount.run!(account: find_account!)\n  redirect_to(accounts_url)\nend\n```\n\nIn this simple example, the destroy interaction doesn't do much. It's not clear\nthat you gain anything by putting it in an interaction. But in the future, when\nyou need to do more than `account.destroy`, you'll only have to update one\nspot.\n\n``` rb\nclass DestroyAccount \u003c ActiveInteraction::Base\n  object :account\n\n  def execute\n    account.destroy\n  end\nend\n```\n\n#### Edit\n\nJust like the destroy action, editing uses the `#find_account!` helper. Then it\ncreates a new interaction instance to use as a form object.\n\n``` rb\n# GET /accounts/:id/edit\ndef edit\n  account = find_account!\n  @account = UpdateAccount.new(\n    account: account,\n    first_name: account.first_name,\n    last_name: account.last_name)\nend\n```\n\nThe interaction that updates accounts is more complicated than the others. It\nrequires an account to update, but the other inputs are optional. If they're\nmissing, it'll ignore those attributes. If they're present, it'll update them.\n\n``` rb\nclass UpdateAccount \u003c ActiveInteraction::Base\n  object :account\n\n  string :first_name, :last_name,\n    default: nil\n\n  validates :first_name,\n    presence: true,\n    unless: -\u003e { first_name.nil? }\n  validates :last_name,\n    presence: true,\n    unless: -\u003e { last_name.nil? }\n\n  def execute\n    account.first_name = first_name if first_name.present?\n    account.last_name = last_name if last_name.present?\n\n    unless account.save\n      errors.merge!(account.errors)\n    end\n\n    account\n  end\nend\n```\n\n#### Update\n\nHopefully you've gotten the hang of this by now. We'll use `#find_account!` to\nget the account. Then we'll build up the inputs for `UpdateAccount`. Then we'll\nrun the interaction and either redirect to the updated account or back to the\nedit page.\n\n``` rb\n# PUT /accounts/:id\ndef update\n  inputs = { account: find_account! }.reverse_merge(params[:account])\n  outcome = UpdateAccount.run(inputs)\n\n  if outcome.valid?\n    redirect_to(outcome.result)\n  else\n    @account = outcome\n    render(:edit)\n  end\nend\n```\n\n## Advanced usage\n\n### Callbacks\n\n[ActiveSupport::Callbacks][] provides a powerful framework for defining callbacks.\nActiveInteraction uses that framework to allow hooking into various parts of an\ninteraction's lifecycle.\n\n``` rb\nclass Increment \u003c ActiveInteraction::Base\n  set_callback :filter, :before, -\u003e { puts 'before filter' }\n\n  integer :x\n\n  set_callback :validate, :after, -\u003e { puts 'after validate' }\n\n  validates :x,\n    numericality: { greater_than_or_equal_to: 0 }\n\n  set_callback :execute, :around, lambda { |_interaction, block|\n    puts '\u003e\u003e\u003e'\n    block.call\n    puts '\u003c\u003c\u003c'\n  }\n\n  def execute\n    puts 'executing'\n    x + 1\n  end\nend\n\nIncrement.run!(x: 1)\n# before filter\n# after validate\n# \u003e\u003e\u003e\n# executing\n# \u003c\u003c\u003c\n# =\u003e 2\n```\n\nIn order, the available callbacks are `filter`, `validate`, and `execute`.\nYou can set `before`, `after`, or `around` on any of them.\n\n### Composition\n\nYou can run interactions from within other interactions with `#compose`. If the\ninteraction is successful, it'll return the result (just like if you had called\nit with `.run!`). If something went wrong, execution will halt immediately and\nthe errors will be moved onto the caller.\n\n``` rb\nclass Add \u003c ActiveInteraction::Base\n  integer :x, :y\n\n  def execute\n    x + y\n  end\nend\n\nclass AddThree \u003c ActiveInteraction::Base\n  integer :x\n\n  def execute\n    compose(Add, x: x, y: 3)\n  end\nend\n\nAddThree.run!(x: 5)\n# =\u003e 8\n```\n\nTo bring in filters from another interaction, use `.import_filters`. Combined\nwith `inputs`, delegating to another interaction is a piece of cake.\n\n``` rb\nclass AddAndDouble \u003c ActiveInteraction::Base\n  import_filters Add\n\n  def execute\n    compose(Add, inputs) * 2\n  end\nend\n```\n\nNote that errors in composed interactions have a few tricky cases. See [the\nerrors section][] for more information about them.\n\n### Defaults\n\nThe default value for an input can take on many different forms. Setting the\ndefault to `nil` makes the input optional. Setting it to some value makes that\nthe default value for that input. Setting it to a lambda will lazily set the\ndefault value for that input. That means the value will be computed when the\ninteraction is run, as opposed to when it is defined.\n\nLambda defaults are evaluated in the context of the interaction, so you can use\nthe values of other inputs in them.\n\n``` rb\n# This input is optional.\ntime :a, default: nil\n# This input defaults to `Time.at(123)`.\ntime :b, default: Time.at(123)\n# This input lazily defaults to `Time.now`.\ntime :c, default: -\u003e { Time.now }\n# This input defaults to the value of `c` plus 10 seconds.\ntime :d, default: -\u003e { c + 10 }\n```\n\n### Descriptions\n\nUse the `desc` option to provide human-readable descriptions of filters. You\nshould prefer these to comments because they can be used to generate\ndocumentation. The interaction class has a `.filters` method that returns a\nhash of filters. Each filter has a `#desc` method that returns the description.\n\n``` rb\nclass Descriptive \u003c ActiveInteraction::Base\n  string :first_name,\n    desc: 'your first name'\n  string :last_name,\n    desc: 'your last name'\nend\n\nDescriptive.filters.each do |name, filter|\n  puts \"#{name}: #{filter.desc}\"\nend\n# first_name: your first name\n# last_name: your last name\n```\n\n### Errors\n\nActiveInteraction provides detailed errors for easier introspection and testing\nof errors. Detailed errors improve on regular errors by adding a symbol that\nrepresents the type of error that has occurred. Let's look at an example where\nan item is purchased using a credit card.\n\n``` rb\nclass BuyItem \u003c ActiveInteraction::Base\n  object :credit_card, :item\n  hash :options do\n    boolean :gift_wrapped\n  end\n\n  def execute\n    order = credit_card.purchase(item)\n    notify(credit_card.account)\n    order\n  end\n\n  private def notify(account)\n    # ...\n  end\nend\n```\n\nHaving missing or invalid inputs causes the interaction to fail and return\nerrors.\n\n``` rb\noutcome = BuyItem.run(item: 'Thing', options: { gift_wrapped: 'yes' })\noutcome.errors.messages\n# =\u003e {:credit_card=\u003e[\"is required\"], :item=\u003e[\"is not a valid object\"], :\"options.gift_wrapped\"=\u003e[\"is not a valid boolean\"]}\n```\n\nDetermining the type of error based on the string is difficult if not\nimpossible. Calling `#details` instead of `#messages` on `errors` gives you\nthe same list of errors with a testable label representing the error.\n\n``` rb\noutcome.errors.details\n# =\u003e {:credit_card=\u003e[{:error=\u003e:missing}], :item=\u003e[{:error=\u003e:invalid_type, :type=\u003e\"object\"}], :\"options.gift_wrapped\"=\u003e[{:error=\u003e:invalid_type, :type=\u003e\"boolean\"}]}\n```\n\nDetailed errors can also be manually added during the execute call by passing a\nsymbol to `#add` instead of a string.\n\n``` rb\ndef execute\n  errors.add(:monster, :no_passage)\nend\n```\n\nActiveInteraction also supports merging errors. This is useful if you want to\ndelegate validation to some other object. For example, if you have an\ninteraction that updates a record, you might want that record to validate\nitself. By using the `#merge!` helper on `errors`, you can do exactly that.\n\n``` rb\nclass UpdateThing \u003c ActiveInteraction::Base\n  object :thing\n\n  def execute\n    unless thing.save\n      errors.merge!(thing.errors)\n    end\n\n    thing\n  end\nend\n```\n\nWhen a composed interaction fails, its errors are merged onto the caller. This\ngenerally produces good error messages, but there are a few cases to look out\nfor.\n\n``` rb\nclass Inner \u003c ActiveInteraction::Base\n  boolean :x, :y\nend\n\nclass Outer \u003c ActiveInteraction::Base\n  string :x\n  boolean :z, default: nil\n\n  def execute\n    compose(Inner, x: x, y: z)\n  end\nend\n\noutcome = Outer.run(x: 'yes')\noutcome.errors.details\n# =\u003e { :x    =\u003e [{ :error =\u003e :invalid_type, :type =\u003e \"boolean\" }],\n#      :base =\u003e [{ :error =\u003e \"Y is required\" }] }\noutcome.errors.full_messages.join(' and ')\n# =\u003e \"X is not a valid boolean and Y is required\"\n```\n\nSince both interactions have an input called `x`, the inner error for that\ninput is moved to the `x` error on the outer interaction. This results in a\nmisleading error that claims the input `x` is not a valid boolean even though\nit's a string on the outer interaction.\n\nSince only the inner interaction has an input called `y`, the inner error for\nthat input is moved to the `base` error on the outer interaction. This results\nin a confusing error that claims the input `y` is required even though it's not\npresent on the outer interaction.\n\n### Forms\n\nThe outcome returned by `.run` can be used in forms as though it were an\nActiveModel object. You can also create a form object by calling `.new` on the\ninteraction.\n\nGiven an application with an `Account` model we'll create a new `Account` using\nthe `CreateAccount` interaction.\n\n```rb\n# GET /accounts/new\ndef new\n  @account = CreateAccount.new\nend\n\n# POST /accounts\ndef create\n  outcome = CreateAccount.run(params.fetch(:account, {}))\n\n  if outcome.valid?\n    redirect_to(outcome.result)\n  else\n    @account = outcome\n    render(:new)\n  end\nend\n```\n\nThe form used to create a new `Account` has slightly more information on the\n`form_for` call than you might expect.\n\n```erb\n\u003c%= form_for @account, as: :account, url: accounts_path do |f| %\u003e\n  \u003c%= f.text_field :first_name %\u003e\n  \u003c%= f.text_field :last_name %\u003e\n  \u003c%= f.submit 'Create' %\u003e\n\u003c% end %\u003e\n```\n\nThis is necessary because we want the form to act like it is creating a new\n`Account`. Defining `to_model` on the `CreateAccount` interaction tells the\nform to treat our interaction like an `Account`.\n\n```rb\nclass CreateAccount \u003c ActiveInteraction::Base\n  # ...\n\n  def to_model\n    Account.new\n  end\nend\n```\n\nNow our `form_for` call knows how to generate the correct URL and param name\n(i.e. `params[:account]`).\n\n```erb\n# app/views/accounts/new.html.erb\n\u003c%= form_for @account do |f| %\u003e\n  \u003c%# ... %\u003e\n\u003c% end %\u003e\n```\n\nIf you have an interaction that updates an `Account`, you can define `to_model`\nto return the object you're updating.\n\n```rb\nclass UpdateAccount \u003c ActiveInteraction::Base\n  # ...\n\n  object :account\n\n  def to_model\n    account\n  end\nend\n```\n\nActiveInteraction also supports [formtastic][] and [simple_form][]. The filters\nused to define the inputs on your interaction will relay type information to\nthese gems. As a result, form fields will automatically use the appropriate\ninput type.\n\n### Shared input options\n\nIt can be convenient to apply the same options to a bunch of inputs. One common\nuse case is making many inputs optional. Instead of setting `default: nil` on\neach one of them, you can use [`with_options`][] to reduce duplication.\n\n``` rb\nwith_options default: nil do\n  date :birthday\n  string :name\n  boolean :wants_cake\nend\n```\n\n### Optional inputs\n\nOptional inputs can be defined by using the `:default` option as described in\n[the filters section][]. Within the interaction, provided and default values\nare merged to create `inputs`. There are times where it is useful to know\nwhether a value was passed to `run` or the result of a filter default. In\nparticular, it is useful when `nil` is an acceptable value. For example, you\nmay optionally track your users' birthdays. You can use the `inputs.given?` predicate\nto see if an input was even passed to `run`. With `inputs.given?` you can also check\nthe input of a hash or array filter by passing a series of keys or indexes to\ncheck.\n\n``` rb\nclass UpdateUser \u003c ActiveInteraction::Base\n  object :user\n  date :birthday,\n    default: nil\n\n  def execute\n    user.birthday = birthday if inputs.given?(:birthday)\n    errors.merge!(user.errors) unless user.save\n    user\n  end\nend\n```\n\nNow you have a few options. If you don't want to update their birthday, leave\nit out of the hash. If you want to remove their birthday, set `birthday: nil`.\nAnd if you want to update it, pass in the new value as usual.\n\n``` rb\nuser = User.find(...)\n\n# Don't update their birthday.\nUpdateUser.run!(user: user)\n\n# Remove their birthday.\nUpdateUser.run!(user: user, birthday: nil)\n\n# Update their birthday.\nUpdateUser.run!(user: user, birthday: Date.new(2000, 1, 2))\n```\n\n### Translations\n\nActiveInteraction is i18n aware out of the box! All you have to do is add\ntranslations to your project. In Rails, these typically go into\n`config/locales`. For example, let's say that for some reason you want to print\neverything out backwards. Simply add translations for ActiveInteraction to your\n`hsilgne` locale.\n\n``` yml\n# config/locales/hsilgne.yml\nhsilgne:\n  active_interaction:\n    types:\n      array: yarra\n      boolean: naeloob\n      date: etad\n      date_time: emit etad\n      decimal: lamiced\n      file: elif\n      float: taolf\n      hash: hsah\n      integer: regetni\n      interface: ecafretni\n      object: tcejbo\n      string: gnirts\n      symbol: lobmys\n      time: emit\n    errors:\n      messages:\n        invalid: dilavni si\n        invalid_type: '%{type} dilav a ton si'\n        missing: deriuqer si\n```\n\nThen set your locale and run interactions like normal.\n\n``` rb\nclass I18nInteraction \u003c ActiveInteraction::Base\n  string :name\nend\n\nI18nInteraction.run(name: false).errors.messages[:name]\n# =\u003e [\"is not a valid string\"]\n\nI18n.locale = :hsilgne\nI18nInteraction.run(name: false).errors.messages[:name]\n# =\u003e [\"gnirts dilav a ton si\"]\n```\n\nEverything else works like an `activerecord` entry. For example, to rename an\nattribute you can use `attributes`.\n\nHere we'll rename the `num` attribute on an interaction named `product`:\n\n``` yml\nen:\n  active_interaction:\n    attributes:\n      product:\n        num: 'Number'\n```\n\n## Credits\n\nActiveInteraction is brought to you by [Aaron Lasseigne][].\nAlong with Aaron, [Taylor Fausak][] helped create and maintain ActiveInteraction but has since moved on.\n\nIf you want to contribute to ActiveInteraction, please read\n[our contribution guidelines][]. A [complete list of contributors][] is\navailable on GitHub.\n\nActiveInteraction is licensed under [the MIT License][].\n\n[activeinteraction]: https://github.com/AaronLasseigne/active_interaction\n[API Documentation]: http://rubydoc.info/github/AaronLasseigne/active_interaction\n[Semantic Versioning]: http://semver.org/spec/v2.0.0.html\n[GitHub releases]: https://github.com/AaronLasseigne/active_interaction/releases\n[aaron lasseigne]: https://github.com/AaronLasseigne\n[taylor fausak]: https://github.com/tfausak\n[our contribution guidelines]: CONTRIBUTING.md\n[complete list of contributors]: https://github.com/AaronLasseigne/active_interaction/graphs/contributors\n[the MIT License]: LICENSE.md\n[formtastic]: https://rubygems.org/gems/formtastic\n[simple_form]: https://rubygems.org/gems/simple_form\n[the filters section]: #filters\n[the errors section]: #errors\n[the optional inputs section]: #optional-inputs\n[`with_options`]: http://api.rubyonrails.org/classes/Object.html#method-i-with_options\n[ActiveSupport::Callbacks]: https://api.rubyonrails.org/classes/ActiveSupport/Callbacks.html\n","funding_links":[],"categories":[],"sub_categories":[],"project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Faaronlasseigne%2Factive_interaction","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Faaronlasseigne%2Factive_interaction","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Faaronlasseigne%2Factive_interaction/lists"}