{"id":13879384,"url":"https://github.com/pabloh/pathway","last_synced_at":"2025-07-16T15:32:12.190Z","repository":{"id":37269790,"uuid":"93070419","full_name":"pabloh/pathway","owner":"pabloh","description":"Define your business logic in simple steps","archived":false,"fork":false,"pushed_at":"2024-08-13T06:00:24.000Z","size":206,"stargazers_count":51,"open_issues_count":1,"forks_count":9,"subscribers_count":5,"default_branch":"master","last_synced_at":"2024-11-14T16:47:23.405Z","etag":null,"topics":["domain-driven-design","operations"],"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/pabloh.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":"2017-06-01T15:11:33.000Z","updated_at":"2024-08-13T05:58:51.000Z","dependencies_parsed_at":"2022-07-12T09:30:53.724Z","dependency_job_id":"e27c719c-3ad6-4e9f-a85c-99453caa7102","html_url":"https://github.com/pabloh/pathway","commit_stats":{"total_commits":156,"total_committers":5,"mean_commits":31.2,"dds":0.04487179487179482,"last_synced_commit":"5778585e065f65a92f3c6f1aaa95728abf178caf"},"previous_names":[],"tags_count":0,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/pabloh%2Fpathway","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/pabloh%2Fpathway/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/pabloh%2Fpathway/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/pabloh%2Fpathway/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/pabloh","download_url":"https://codeload.github.com/pabloh/pathway/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":226143895,"owners_count":17580245,"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":["domain-driven-design","operations"],"created_at":"2024-08-06T08:02:19.236Z","updated_at":"2025-07-16T15:32:12.176Z","avatar_url":"https://github.com/pabloh.png","language":"Ruby","funding_links":[],"categories":["Ruby"],"sub_categories":[],"readme":"# Pathway\n\n[![Gem Version](https://badge.fury.io/rb/pathway.svg)](https://badge.fury.io/rb/pathway)\n[![Tests](https://github.com/pabloh/pathway/workflows/Tests/badge.svg)](https://github.com/oabloh/pathway/actions?query=workflow%3ATests)\n[![Coverage Status](https://coveralls.io/repos/github/pabloh/pathway/badge.svg?branch=master)](https://coveralls.io/github/pabloh/pathway?branch=master)\n\nPathway encapsulates your business logic into simple operation objects (AKA application services on the [DDD](https://en.wikipedia.org/wiki/Domain-driven_design) lingo).\n\n## Installation\n\n    $ gem install pathway\n\n## Description\n\nPathway helps you separate your business logic from the rest of your application; regardless of is an HTTP backend, a background processing daemon, etc.\nThe main concept Pathway relies upon to build domain logic modules is the operation, this important concept will be explained in detail in the following sections.\n\nPathway also aims to be easy to use, stay lightweight and extensible (by the use of plugins), avoid unnecessary dependencies, keep the core classes clean from monkey patching and help yield an organized and uniform codebase.\n\n\u003c!--\n## Migrating to Ruby 3.x\n\nTODO: small comment and link to `auto_deconstruct_state` plugin\n--\u003e\n\n## Usage\n\n### Main concepts and API\n\nAs mentioned earlier the operation is an essential concept Pathway is built around. Operations not only structure your code (using steps as will be explained later) but also express meaningful business actions. Operations can be thought of as use cases too: they represent an activity -to be performed by an actor interacting with the system- which should be understandable by anyone familiar with the business regardless of their technical expertise.\n\nOperations shouldn't ideally contain any business rules but instead, orchestrate and delegate to other more specific subsystems and services. The only logic present then should be glue code or any data transformations required to make interactions with the inner system layers possible.\n\n#### Function object protocol (the `call` method)\n\nOperations work as function objects, they are callable and hold no state, as such, any object that responds to `call` and returns a result object can be a valid operation and that's the minimal protocol they need to follow.\nThe result object must follow its protocol as well (and a helper class is provided for that end) but we'll talk about that in a minute.\n\nLet's see an example:\n\n```ruby\nclass MyFirstOperation\n  def call(input)\n    result = Repository.create(input)\n\n    if result.valid?\n      Pathway::Result.success(result)\n    else\n      Pathway::Result.failure(:create_error)\n    end\n  end\nend\n\nresult = MyFirstOperation.new.call(foo: 'foobar')\nif result.success?\n  puts result.value.inspect\nelse\n  puts \"Error: #{result.error}\"\nend\n```\n\nNote first, we are not inheriting from any class nor including any module. This won't be the case in general as `pathway` provides classes to help build your operations, but it serves to illustrate how little is needed to implement one.\n\nAlso, let's ignore the specifics about `Repository.create(...)`, we just need to know that is some backend service from which a value is returned.\n\n\nWe then define a `call` method for the class. It only checks if the result is available and then wraps it into a successful `Result` object when is ok, or a failing one when is not.\nAnd basically, that's all is needed, you can then call the operation object, check whether it was completed correctly with `success?` and get the resulting value.\n\nBy following this protocol, you will be able to uniformly apply the same pattern on every HTTP endpoint (or whatever means your app has to communicate with the outside world). The upper layer of the application will offload all the domain logic to the operation and only will need to focus on the HTTP transmission details.\n\nMaintaining always the same operation protocol will also be very useful when composing them.\n\n#### Operation result\n\nAs should be evident by now an operation should always return either a successful or failed result. These concepts are represented by following a simple protocol, which `Pathway::Result` subclasses comply with.\n\nAs we've seen before, by querying `success?` on the result we can see if the operation we just ran went well, or call to `failure?` to see if it failed.\n\nThe actual result value produced by the operation is accessible at the `value` method and the error description (if there's any) at `error` when the operation fails.\nTo return wrapped values or errors from your operation you must call `Pathway::Result.success(value)` or `Pathway::Result.failure(error)`.\n\nIt is worth mentioning that when you inherit from `Pathway::Operation` you'll have helper methods at your disposal to create result objects easily. For instance, the previous section's example could be written as follows:\n\n```ruby\nclass MyFirstOperation \u003c Pathway::Operation\n  def call(input)\n    result = Repository.create(input)\n\n    result.valid? ? success(result) : failure(:create_error)\n  end\nend\n```\n\n#### Error objects\n\n`Pathway::Error` is a helper class to represent the error description from a failed operation execution (and also supports pattern matching as we'll see later).\nIts use is completely optional but provides you with a basic schema to communicate what went wrong. You can instantiate it by calling `new` on the class itself or using the helper method `error` provided by the operation class:\n\n```ruby\nclass CreateNugget \u003c Pathway::Operation\n  def call(input)\n    validation = Validator.call(input)\n\n    if validation.ok?\n      success(Nugget.create(validation.values))\n    else\n      error(:validation, message: 'Invalid input', details: validation.errors)\n    end\n  end\nend\n```\n\nAs you can see `error(...)` expects the `type` as the first parameter (and only the mandatory) then `message:` and `details` keyword arguments; these 2 last ones can be omitted and have default values. The type parameter must be a `Symbol`, `message:` a `String` and `details:` can be a `Hash` or any other structure you see fit.\n\nFinally, the `Error` object have three accessors available to get the values back:\n\n```ruby\nresult = CreateNugget.new.call(foo: 'foobar')\nif result.failure?\n  puts \"#{result.error.type} error: #{result.error.message}\"\n  puts \"Error details: #{result.error.details}\"\nend\n\n```\n\nMind you, `error(...)` creates an `Error` object wrapped into a `Pathway::Failure` so you don't have to do it yourself.\nIf you decide to use `Pathway::Error.new(...)` directly, you will have to pass all the arguments as keywords (including `type:`), and you will have to wrap the object before returning it.\n\n#### Initialization context\n\nIt was previously mentioned that operations should work like functions, that is, they don't hold state and you should be able to execute the same instance all the times you need, on the other hand, there will be some values that won't change during the operation lifetime and won't make sense to pass as `call` parameters, you can provide these values on initialization as context data.\n\nContext data can be thought of as 'request data' on an HTTP endpoint, values that aren't global but won't change during the execution of the request. Examples of this kind of data are the current user, the current device, a CSRF token, other configuration parameters, etc. You will want to pass these values on initialization, and probably pass them along to other operations down the line.\n\nYou must define your initializer to accept a `Hash` with these values, which is what every operation is expected to do, but as before, when inheriting from `Operation` you have the helper class method `context` handy to make it easier for you:\n\n```ruby\nclass CreateNugget \u003c Pathway::Operation\n  context :current_user, notify: false\n\n  def call(input)\n    validation = Validator.call(input)\n\n    if validation.valid?\n      nugget = Nugget.create(owner: current_user, **validation.values)\n\n      Notifier.notify(:new_nugget, nugget) if @notify\n      success(nugget)\n    else\n      error(:validation, message: 'Invalid input', details: validation.errors)\n    end\n  end\nend\n\n\nop = CreateNugget.new(current_user: user)\nop.call(foo: 'foobar')\n```\n\nIn the example above `context` is defining `:current_user` as a mandatory argument (it will raise an error if not provided) and `:notify` as an optional config argument, since it has a default value. Note that any extra non-defined value provided will be simply ignored.\n\nBoth of these parameters are available through accessors (and instance variables) inside the operation. Also, there is a `context` private method you use to get all the initialization values as a frozen hash, in order to pass them along easily.\n\n#### Alternative invocation syntax\n\nIf you don't care about keeping the operation instance around you can execute the operation directly on the class. To do so, use `call` with the initialization context first and then the remaining parameters:\n\n```ruby\nuser = User.first(session[:current_user_id])\ncontext = { current_user: user }\n\nCreateNugget.call(context, params[:nugget]) # Using 'call' on the class\n```\nAlso, you have Ruby's alternative syntax to invoke the `call` method: `CreateNugget.(context, params[:nugget])`. In both cases, you'll get the operation result like when invoking `call` on the operation's instance.\n\nMind you that a context must always be provided for this syntax, if you don't need any initialization use an empty hash.\n\nThere's also a third way to execute an operation, made available through a plugin, that will be explained later.\n\n#### Steps\n\nFinally, the steps are the heart of the `Operation` class and the main reason you will want to inherit your own classes from `Pathway::Operation`.\n\nSo far we know that every operation needs to implement a `call` method and return a valid result object, `pathway` provides another option: the `process` block DSL, this method will define `call` behind the scenes for us, while also providing a way to define a business-oriented set of steps to describe our operation's behavior.\n\nEvery step should be cohesive and focused on a single responsibility, ideally by offloading work to other subsystems. Designing steps this way is the developer's responsibility but is made much simpler by the use of custom steps provided by plugins as we'll see later.\n\n##### Process DSL\n\nLet's start by showing some actual code:\n\n```ruby\n# ...\n  # Inside an operation class body...\n  process do\n    step :authorize\n    step :validate\n    set  :create_nugget, to: :nugget\n    step :notify\n  end\n# ...\n```\n\nTo define your `call` method using the DSL just call to `process` and pass a block, inside it, the DSL will be available.\nEach `step` (or `set`) call is referring to a method inside the operation class, superclasses, or available through a plugin, these methods will be eventually invoked by `call`.\nAll of the steps constitute the operation use case and follow a series of conventions in order to carry the process state along the execution process.\n\nWhen you run the `call` method, the auto-generated code will save the provided argument at the `input` key within the execution state. Subsequent steps will receive this state and will be able to modify it, setting the result or auxiliary values, in order to communicate with the next steps on the execution path.\n\nEach step (as the operation as a whole) can succeed or fail, when the latter happens execution is halted, and the operation `call` method returns immediately.\nTo signal a failure you must return a `failure(...)` or `error(...)` in the same fashion as when defining `call` directly.\n\nIf you return a `success(...)` or anything that's not a failure the execution carries on but the value is ignored. If you want to save the result value, you must use `set` instead of `step` at the process block, which will save your wrapped value, into the key provided at `to:`.\nAlso, non-failure return values inside steps are automatically wrapped so you can use `success` for clarity's sake but it's optional.\nIf you omit the `to:` keyword argument when defining a `set` step, the result key will be used by default, but we'll explain more on that later.\n\n##### Operation execution state\n\nTo operate with the execution state, every step method receives a structure representing the current state. This structure is similar to a `Hash` and responds to its main methods (`:[]`, `:[]=`, `:fetch`, `:store`, `:include?` and `to_hash`).\n\nWhen an operation is executed, before running the first step, an initial state is created by copying all the values from the initialization context (and also including `input`).\nNote that these values can be replaced in later steps but it won't mutate the context object itself since is always frozen.\n\nA state object can be splatted on method definition in the same fashion as a `Hash`, thus, allowing us to cherry-pick the attributes we are interested in any given step:\n\n```ruby\n# ...\n  # This step only takes the values it needs and doesn't change the state.\n  def send_emails(state)\n    user, report = state[:user], state[:report]\n    ReportMailer.send_report(user.email, report)\n  end\n# ...\n```\n\u003c!--\nTODO: explain Ruby 2.7 and 3.0 state deconstruction alternatives\n\nNote the empty double splat at the end of the parameter list, this Ruby-ism means: grab the mentioned keys and ignore all the rest. If you omit the `**` when you have outstanding keys, Ruby's `Hash` destructing will fail.\n--\u003e\n\n##### Successful operation result\n\nOn each step, you can access or change the result the operation will produce on a successful execution.\nThe value will be stored at one of the attributes within the state.\nBy default, the state's key `:value` will hold the result, but if you prefer to use another name you can specify it through the `result_at` operation class method.\n\n##### Full example\n\nLet's now go through a fully defined operation using steps:\n\n```ruby\nclass CreateNugget \u003c Pathway::Operation\n  context :current_user\n\n  process do\n    step :authorize\n    step :validate\n    set  :create_nugget\n    step :notify\n  end\n\n  result_at :nugget\n\n  def authorize(*)\n    unless current_user.can? :create, Nugget\n      error(:forbidden)\n    end\n  end\n\n  def validate(state)\n    validation = NuggetForm.call(state[:input])\n\n    if validation.ok?\n      state[:params] = validation.values\n    else\n      error(:validation, details: validation.errors)\n    end\n  end\n\n  def create_nugget(state)\n    Nugget.create(owner: current_user, **state[:params])\n  end\n\n  def notify(state)\n    Notifier.notify(:new_nugget, state[:nugget])\n  end\nend\n```\n\nIn the example above the operation will produce a nugget (whatever that is...).\n\nAs you can see in the code, we are using the previously mentioned methods to indicate we need the current user to be present on initialization: `context: current_user`, a `call` method (defined by `process do ... end`), and the result value should be stored at the `:nugget` key (`result_at :nugget`).\n\nLet's delve into the `process` block: it defines three steps using the `step` method and `create_nugget` using `set`, as we said before, this last step will set the result key (`:nugget`) since the `to:` keyword argument is absent.\n\nNow, for each of the step methods:\n\n- `:authorize` doesn't need the state so just ignores it, then checks if the current user is allowed to run the operation and halts the execution by returning a `:forbidden` error type if is not, otherwise does nothing and the execution goes on.\n- `:validate` gets the state, checks the validity of the `:input` value which as we said is just the `call` method input, returns an `error(...)` when there's a problem, and if the validation is correct it updates the state but saving the sanitized values in `:params`. Note that on success the return value is `state[:params]`, but is ignored like on `:authorize`, since this method was also specified using `step`.\n- `:create_nugget` first takes the `:params` attribute from the state, and calls `create` on the `Nugget` model with the sanitized params and the current user. The return value is saved to the result key (`:nugget` in this case) as the step is defined using `step` without `to:`.\n- `:notify` grabs the `:nugget` from the state, and simply emits a notification with it, it has no meaningful return value, so is ignored.\n\nThe previous example goes through all the essential concepts needed for defining an operation class. If you can grasp it, you already have a good understanding on how to implement one. There are still some very important bits to cover (like testing), and we'll tackle them in the latter sections.\n\nOn a final note, you may be thinking the code could be a bit less verbose; also, shouldn't very common stuff like validation or authorization be simpler to use? and why always specify the result key name? maybe is possible to infer it from the surrounding code. We will address all those issues in the next section using plugins, `pathway`'s extension mechanism.\n\n### Plugins\n\nPathway operations can be extended with plugins. They are very similar to the ones found in [Roda](http://roda.jeremyevans.net/) or [Sequel](http://sequel.jeremyevans.net/). So if you are already familiar with any of those gems you shouldn't have any problem with `pathway`'s plugin system.\n\nTo activate a plugin just call the `plugin` method on the operation class:\n\n```ruby\nclass BaseOperation \u003c Pathway::Operation\n  plugin :foobar, qux: 'quz'\nend\n\nclass SomeOperation \u003c BaseOperation\n  # The :foobar plugin will also be activated here\nend\n```\n\nThe plugin name must be specified as a `Symbol` (or also as the `Module` where is implemented, but more on that later), and it can take parameters next to the plugin's name.\nWhen activated it will enrich your operations with new instance and class methods plus extra customs step for the `process` DSL.\n\nMind you, if you wish to activate a plugin for a number of operations you can activate it for all of them directly on the `Pathway::Operation` class, or you can create your own base operation and all its descendants will inherit the base class' plugins.\n\n#### `DryValidation` plugin\n\nThis plugin provides integration with the [dry-validation](http://dry-rb.org/gems/dry-validation/) gem. I won't explain in detail how to use this library since is already extensively documented on its official website, but instead, I'll assume certain knowledge of it, nonetheless, as you'll see in a moment, its API is pretty self-explanatory.\n\n`dry-validation` provides a very simple way to define contract objects (conceptually very similar to form objects) to process and validate input. The provided custom `:validate` step allows you to run your input through a contract to check if your data is valid before carrying on. When the input is invalid it will return an error object of type `:validation` and the reasons the validation failed will be available at the `details` attribute. Is usually the first step an operation runs.\n\nWhen using this plugin we can provide an already defined contract to the step to use or we can also define it within the operation.\nLet's see a few examples:\n\n```ruby\nclass NuggetContract \u003c Dry::Validation::Contract\n  params do\n    required(:owner).filled(:string)\n    required(:price).filled(:integer)\n  end\nend\n\nclass CreateNugget \u003c Pathway::Operation\n  plugin :dry_validation\n\n  contract NuggetContract\n\n  process do\n    step :validate\n    step :create_nugget\n  end\n\n  # ...\nend\n```\n\nAs is shown above, the contract is defined first, then configured to be used by the operation by calling `contract NuggetContract`, and validate the input at the process block by placing the step `step :validate` inside the `process` block.\n\n```ruby\nclass CreateNugget \u003c Pathway::Operation\n  plugin :dry_validation\n\n  contract do\n    params do\n      required(:owner).filled(:string)\n      required(:price).filled(:integer)\n    end\n  end\n\n  process do\n    step :validate\n    step :create_nugget\n  end\n\n  # ...\nend\n```\n\nNow, this second example is equivalent to the first one, but here we call `contract` with a block instead of an object parameter; this block will be used as the definition body for a contract class that will be stored internally. Thus keeping the contract and operation code in the same place, this is convenient when you have a rather simpler contract and don't need to reuse it.\n\nOne interesting nuance to keep in mind regarding the inline block contract is that, when doing operation inheritance, if the parent operation already has a contract, the child operation will define a new one inheriting from the parent's. This is very useful to share validation logic among related operations in the same class hierarchy.\n\nAs a side note, if your contract is simple enough and has parameters and not extra validations rules, you can call the `params` method directly instead, the following code is essentially equivalent to the previous example:\n\n```ruby\nclass CreateNugget \u003c Pathway::Operation\n  plugin :dry_validation\n\n  params do\n    required(:owner).filled(:string)\n    required(:price).filled(:integer)\n  end\n\n  process do\n    step :validate\n    step :create_nugget\n  end\n\n  # ...\nend\n```\n\n##### Contract options\n\nIf you are familiar with `dry-validation` you probably know it provides a way to [inject options](https://dry-rb.org/gems/dry-validation/1.4/external-dependencies/) before calling the contract.\n\nIn those scenarios, you must either set the `auto_wire: true` plugin argument or specify how to map options from the execution state to the contract when calling `step :validate`.\nLets see and example for the first case:\n\n```ruby\nclass CreateNugget \u003c Pathway::Operation\n  plugin :dry_validation, auto_wire: true\n\n  context :user_name\n\n  contract do\n    option :user_name\n\n    params do\n      required(:owner).filled(:string)\n      required(:price).filled(:integer)\n    end\n\n    rule(:owner) do\n      key.failure(\"invalid owner\") unless user_name == values[:owner]\n    end\n  end\n\n  process do\n    step :validate\n    step :create_nugget\n  end\n\n  # ...\nend\n```\n\nHere the defined contract needs a `:user_name` option, so we tell the operation to grab the attribute with the same name from the state by activating `:auto_wire`, afterwards, when the validation runs, the contract will already have the user name available.\n\nMind you, this option is `false` by default, so be sure to set it to `true` at `Pathway::Operation` if you'd rather have it enabled for all your operations.\n\nOn the other hand, if for some reason the name of the contract's option and state attribute don't match, we can just pass `with: {...}` when calling to `step :validate`, indicating how to wire the attributes, the following example illustrates just that:\n\n```ruby\nclass CreateNugget \u003c Pathway::Operation\n  plugin :dry_validation\n\n  context :current_user_name\n\n  contract do\n    option :user_name\n\n    params do\n      required(:owner).filled(:string)\n      required(:price).filled(:integer)\n    end\n\n    rule(:owner) do\n      key.failure(\"invalid owner\") unless user_name == values[:owner]\n    end\n  end\n\n  process do\n    step :validate, with: { user_name: :current_user_name } # Inject :user_name to the contract object with the state's :current_user_name\n    step :create_nugget\n  end\n\n  # ...\nend\n```\n\nThe `with:` parameter can always be specified for `step :validate`, and allows you to override the default mapping regardless if auto-wiring is active or not.\n\n#### `SimpleAuth` plugin\n\nThis very simple plugin adds a custom step called `:authorize`, that can be used to check for permissions and halt the operation with a `:forbidden` error when they aren't fulfilled.\n\nIn order to use it you must define a boolean predicate to check for permissions, by passing a block to the `authorization` method:\n\n```ruby\nclass MyOperation \u003c Pathway::Operation\n  plugin :simple_auth\n\n  context :current_user\n  authorization { current_user.is_admin? }\n\n  process do\n    step :authorize\n    step :perform_some_action\n  end\n\n  # ...\nend\n```\n\n#### `SequelModels` plugin\n\nThe `sequel_models` plugin helps integrate operations with the [Sequel](http://sequel.jeremyevans.net/) ORM, by adding a few custom steps.\n\nThis plugin expects you to be using `Sequel` model classes to access your DB. In order to exploit it, you need to indicate which model your operation is going to work with, hence you must specify said model when activating the plugin with the `model:` keyword argument, or later using the `model` class method.\nThis configuration will then be used on the operation class and all its descendants.\n\n```ruby\nclass MyOperation \u003c Pathway::Operation\n  plugin :sequel_models, model: Nugget, search_by: :name, set_result_key: false\nend\n\n# Or...\n\nclass MyOperation \u003c Pathway::Operation\n  plugin :sequel_models\n\n  # This is useful when using inheritance and you need different models per operation\n  model Nugget, search_by: :name, set_result_key: false\n\n  process do\n    step :authorize\n    step :perform_some_action\n  end\nend\n```\n\nAs you can see above you can also customize the search field (`:search_by`) and indicate if you want to override or not the result key (`:set_result_key`) when calling the `model` method.\nThese two options aren't mandatory, and by default, Pathway will set the search field to the class model primary key, and override the result key to a snake-cased version of the model name (ignoring namespaces if contained inside a class or module).\n\nLet's now take a look at the provided extensions:\n\n##### `:fetch_model` step\n\nThis step will fetch a model from the DB, by extracting the search field from the `call` method input parameter stored at `:input` in the execution state. If the model cannot be fetched from the DB it will halt the execution with a `:not_found` error, otherwise it will simply save the model into the result key (which will be `:nugget` for the example below).\nYou can later access the fetched model from that attribute and if the operation finishes successfully, it will be used as its result.\n\n```ruby\nclass UpdateNugget \u003c Pathway::Operation\n  plugin :sequel_models, model: Nugget\n\n  process do\n    step :validate\n    step :fetch_model\n    step :fetch_model, from: User, using: :user_id, search_by: :pk, to: :user # Even the default class can also be overrided with 'from:'\n    step :update_nugget\n  end\n\n  # ...\nend\n```\n\nAs a side note, and as shown in the 3rd step, `:fetch_model` allows you to override the search column (`search_by:`), the input parameter to extract from `input` (`using:`), the attribute to store the result (`to:`) and even the default search class (`from:`). If the current defaults don't fit your needs and you'll have these options available. This is commonly useful when you need some extra object, besides the main one, to execute your operation.\n\n##### `transaction` and `after_commit`\n\nThese two are a bit special since they aren't actually custom steps but just new methods that extend the process DSL itself.\nThese methods will take a block as an argument within which you can define inner steps.\nKeeping all that in mind the only thing `transaction` and `after_commit` really do is surround the inner steps with `SEQUEL_DB.transaction { ... }` and `SEQUEL_DB.after_commit { ... }` blocks, respectively.\n\n```ruby\nclass CreateNugget \u003c Pathway::Operation\n  plugin :sequel_models, model: Nugget\n\n  process do\n    step :validate\n    transaction do\n      step :create_nugget\n      step :attach_history_note\n      after_commit do\n        step :send_emails\n      end\n    end\n  end\n\n  # ...\nend\n```\n\nWhen won't get into the details for each step in the example above, but the important thing to take away is that `:create_nugget` and `:attach_history_note` will exists within a single transaction and `send_mails` (and any steps you add in the `after_commit` block) will only run after the transaction has finished successfully.\n\nAnother nuance to take into account is that calling `transaction` will start a new savepoint, since, in case you're already inside a transaction, it will be able to properly notify that the transaction failed by returning an error object when that happens.\n\n#### `Responder` plugin\n\nThis plugin extends the `call` class method on the operation to accept a block. You can then use this block for flow control on success, failure, and also different types of failures.\n\nThere are two ways to use this plugin: by discriminating between success and failure, and also discriminating according to the specific failure type.\n\nIn each case you must provide the action to execute for every outcome using blocks:\n\n```ruby\nMyOperation.plugin :responder # 'plugin' is actually a public method\n\nMyOperation.(context, params) do\n  success { |value| r.halt(200, value.to_json) } # BTW: 'r.halt' is a Roda request method used to exemplify\n  failure { |error| r.halt(403) }\nend\n```\n\u003c!--\n```ruby\ncase MyOperation.(context, params)\nin Success(value) then r.halt(200, value.to_json)\nin Failure(_)     then r.halt(403)\nend\n```\n--\u003e\n\nIn the example above we provide a block for both the success and the failure case. On each block, the result value or the error object error will be provided at the blocks' argument, and the result of the corresponding block will be the result of the whole expression.\n\nLets now show an example with the error type specified:\n\n```ruby\nMyOperation.plugin :responder\n\nMyOperation.(context, params) do\n  success              { |value| r.halt(200, value.to_json) }\n  failure(:forbidden)  { |error| r.halt(403) }\n  failure(:validation) { |error| r.halt(422, error.details.to_json) }\n  failure(:not_found)  { |error| r.halt(404) }\nend\n```\n\u003c!--\n```ruby\ncase MyOperation.(context, params)\nin Success(value)                       then r.halt(200, value.to_json)\nin Failure(type: :forbidden)            then r.halt(403)\nin failure(type: :validation, details:) then r.halt(422, details.to_json)\nin failure(type: :not_found)            then r.halt(404)\nend\n```\n--\u003e\n\nAs you can see is almost identical to the previous example only that this time you provide the error type on each `failure` call.\n\n\u003c!--\n#### `AutoDeconstructState` plugin\n\nTODO: Explain reason, how to migrate, how to activate\n--\u003e\n### Plugin architecture\n\nGoing a bit deeper now, we'll explain how to implement your own plugins. As was mentioned before `pathway` follows a very similar approach to the [Roda](http://roda.jeremyevans.net/) or [Sequel](http://sequel.jeremyevans.net/) plugin systems, which is reflected at its implementation.\n\nEach plugin must be defined in a file placed within the `pathway/plugins/` directory of your gem or application, so `pathway` can require the file; and must be implemented as a module inside the `Pathway::Plugins` namespace module. Inside your plugin module, three extra modules can be defined to extend the operation API `ClassMethods`, `InstanceMethods` and `DSLMethods`; plus a class method `apply` for plugin initialization when needed.\n\nIf you are familiar with the aforementioned plugin mechanism (or others as well), the function of each module is probably starting to feel evident: `ClassMethods` will be used to extend the operation class, so any class methods should be defined here; `InstanceMethods` will be included on the operation so all the instance methods you need to add to the operation should be here, this includes every custom step you need to add; and finally `DSLMethods` will be included on the `Operation::DSL` class, which holds all the DSL methods like `step` or `set`.\nThe `apply` method will simply be run whenever the plugin is included, taking the operation class on the first argument and all then arguments the call to `plugin` received (excluding the plugin name).\n\nLet's explain with more detail using a complete example:\n\n```ruby\n# lib/pathway/plugins/active_record.rb\n\nmodule Pathway\n  module Plugins\n    module ActiveRecord\n      module ClassMethods\n        attr_accessor :model, :pk\n\n        def inherited(subclass)\n          super\n          subclass.model = self.model\n          subclass.pk    = self.pk\n        end\n      end\n\n      module InstanceMethods\n        delegate :model, :pk, to: :class\n\n        # This method will conflict with :sequel_models so you mustn't load both plugins in the same operation\n        def fetch_model(state, column: pk)\n          current_pk = state[:input][column]\n          result     = model.first(column =\u003e current_pk)\n\n          if result\n            state.update(result_key =\u003e result)\n          else\n            error(:not_found)\n          end\n        end\n      end\n\n      module DSLMethods\n        # This method also conflicts with :sequel_models, so don't use them at once\n        def transaction(\u0026steps)\n          transactional_seq = -\u003e seq, _state do\n            ActiveRecord::Base.transaction do\n              raise ActiveRecord::Rollback if seq.call.failure?\n            end\n          end\n\n          around(transactional_seq, \u0026steps)\n        end\n      end\n\n      def self.apply(operation, model: nil, pk: nil)\n        operation.model = model\n        opertaion.pk    = pk || model\u0026.primary_key\n      end\n    end\n  end\nend\n```\n\nThe code above implements a plugin to provide basic interaction with the [ActiveRecord](http://guides.rubyonrails.org/active_record_basics.html) gem.\nEven though is a very simple plugin, it shows all the essentials to develop more complex ones.\n\nAs is pointed out in the code, some of the methods implemented here (`fetch_model` and `transmission`) collide with methods defined for `:sequel_models`, so as a consequence, these two plugins are not compatible with each other and cannot be activated for the same operation (although you can still do it for different operations within the same application).\nYou must be mindful about colliding method names when mixing plugins since `Pathway` can't bookkeep compatibility among every plugin that exists or will ever exist.\nIs a good practice to document known incompatibilities on the plugin definition itself when they are known.\n\nThe whole plugin is completely defined within the `ActiveRecord` module inside the `Pathway::Plugins` namespace, also the file is placed at the load path in `pathway/plugin/active_record.rb` (assuming `lib/` is listed in `$LOAD_PATH`). This will ensure when calling `plugin :active_record` inside an operation, the correct file will be loaded and the correct plugin module will be applied to the current operation.\n\nMoving on to the `ClassMethods` module, we can see the accessors `model` and `pk` are defined for the operation's class to allow configuration.\nAlso, the `inherited` hook is defined, this will simply be another class method at the operation and as such will be executed normally when the operation class is inherited. In our implementation, we just call to `super` (which is extremely important since other modules or parent classes could be using this hook) and then copy the `model` and `pk` options from the parent to the subclass in order to propagate the configuration downwards.\n\nAt the end of the `ActiveRecord` module definition, you can see the `apply` method. It will receive the operation class and the parameters passed when the `plugin` method is invoked. This method is usually used for loading dependencies or just setting up config parameters as we do in this particular example.\n\n`InstanceMethods` first defines a few delegator methods to the class itself for later use.\nThen the `fetch_model` step is defined (remember steps are but operation instance methods). Its first parameter is the state itself, as in the other steps we've seen before, and the remaining parameters are the options we can pass when calling `step :fetch_model` (mind you, this is also valid for steps defined in operations classes). Here we only take a single keyword argument: `column: pk`, with a default value; this will allow us to change the look-up column when using the step and is the only parameter we can use, passing other keyword arguments or extra positional parameters when invoking the step will raise errors.\n\nLet's now examine the `fetch_model` step body, it's not really that much different from other steps, here we extract the model primary key from `state[:input][column]` and use it to perform a search. If nothing is found an error is returned, otherwise the state is updated in the result key, to hold the model that was just fetched from the DB.\n\nWe finally see a `DSLMethods` module defined to extend the process DSL.\nFor this plugin, we'll define a way to group steps within an `ActiveRecord` transaction, much in the same way the `:sequel_models` plugin already does for `Sequel`.\nTo this end, we define a `transaction` method to expect a steps block and pass it down to the `around` helper below which expects a callable (like a `Proc`) and a step list block. As you can see the lambda we pass on the first parameter makes sure the steps are being run inside a transaction or aborts the transaction if the intermediate result is a failure.\n\nThe `around` method is a low-level tool available to help extend the process DSL and it may seem a bit daunting at first glance but its usage is quite simple, the block is just a step list like the ones we find inside the `process` call; and the parameter is a callable (usually a lambda), that will take 2 arguments, an object from which we can run the step list by invoking `call` (and is the only thing it can do), and the current state. From here we can examine the state and decide upon whether to run the steps, how many times (if any), or run some code before and/or after doing so, like what we need to do in our example to surround the steps within a DB transaction.\n\n### Testing tools\n\nAs of right now, only `rspec` is supported, that is, you can obviously test your operations with any framework you want, but all the provided matchers are designed for `rspec`.\n\n#### Rspec config\n\nIn order to load Pathway's operation matchers you must add the following line to your `spec_helper.rb` file, after loading `rspec`:\n\n```ruby\nrequire 'pathway/rspec'\n```\n\n#### Rspec matchers\n\nPathway provides a few matchers in order to test your operation easier.\nLet's go through a full example:\n\n```ruby\n# create_nugget.rb\n\nclass CreateNugget \u003c Pathway::Operation\n  plugin :dry_validation\n\n  params do\n    required(:owner).filled(:string)\n    required(:price).filled(:integer)\n    optional(:disabled).maybe(:bool)\n  end\n\n  process do\n    step :validate\n    set  :create_nugget\n  end\n\n  def create_nugget(state)\n    Nugget.create(state[:params])\n  end\nend\n\n\n# create_nugget_spec.rb\n\ndescribe CreateNugget do\n  describe '#call' do\n    subject(:operation) { CreateNugget.new }\n\n    context 'when the input is valid' do\n      let(:input) { owner: 'John Smith', value: '11230' }\n\n      it { is_expected.to succeed_on(input).returning(an_instace_of(Nugget)) }\n    end\n\n    context 'when the input is invalid' do\n      let(:input) { owner: '', value: '11230' }\n\n      it { is_expected.to fail_on(input).\n             with_type(:validation).\n             message('Is not valid').\n             and_details(owner: ['must be present']) }\n    end\n  end\n\n  describe '.contract' do\n    subject(:contract) { CreateNugget.build_contract }\n\n    it { is_expected.to require_fields(:owner, :price) }\n    it { is_expected.to accept_optional_field(:disabled) }\n  end\nend\n```\n\n##### `succeed_on` matcher\n\nThis first matcher works on the operation itself and that's why we could set `subject` with the operation instance and use `is_expected.to succeed_on(...)` on the example.\nThe assertion it performs is simply that the operation was successful, also you can optionally chain `returning(...)` if you want to test the returning value, this method allows nesting matchers as is the case in the example.\n\n##### `fail_on` matcher\n\nThis second matcher is analog to `succeed_on` but it asserts that operation execution was a failure instead. Also if you return an error object, and you need to, you can assert the error type using the `type` chain method (aliased as `and_type` and `with_type`); the error message (`and_message`, `with_message` or `message`); and the error details (`and_details`, `with_details` or `details`). Mind you, the chain methods for the message and details accept nested matchers while the `type` chain can only test by equality.\n\n##### contract/form matchers\n\nFinally, we can see that we are also testing the operation's contract (or form), implemented here with the `dry-validation` gem.\n\nTwo more matchers are provided: `require_fields` (aliased `require_field`) to test when a contract is expected to define a required set of fields, and `accept_optional_fields` (aliased `accept_optional_field`) to test when a contract must define a certain set of optional fields, both the contract class (at operation class method `contract_class`) or an instance (operation class method `build_contract`) can be provided.\n\nThese matchers are only useful when using `dry-validation` (on every version newer or equal to `0.11.0`) and will probably be extracted to their own gem in the future.\n\n## Development\n\nAfter checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.\n\nTo install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org).\n\n## Contributing\n\nBug reports and pull requests are welcome on GitHub at https://github.com/pabloh/pathway.\n\n## License\n\nThe gem is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT).\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fpabloh%2Fpathway","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fpabloh%2Fpathway","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fpabloh%2Fpathway/lists"}