{"id":13878348,"url":"https://github.com/bkuhlmann/transactable","last_synced_at":"2025-04-14T06:22:05.251Z","repository":{"id":59008850,"uuid":"535006528","full_name":"bkuhlmann/transactable","owner":"bkuhlmann","description":"A domain specific language for functionally composable transactional workflows.","archived":false,"fork":false,"pushed_at":"2024-03-09T17:41:07.000Z","size":217,"stargazers_count":25,"open_issues_count":0,"forks_count":0,"subscribers_count":2,"default_branch":"main","last_synced_at":"2024-10-07T08:18:44.487Z","etag":null,"topics":["function-composition","transaction"],"latest_commit_sha":null,"homepage":"https://alchemists.io/projects/transactable","language":"Ruby","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"other","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/bkuhlmann.png","metadata":{"files":{"readme":"README.adoc","changelog":null,"contributing":null,"funding":".github/FUNDING.yml","license":"LICENSE.adoc","code_of_conduct":null,"threat_model":null,"audit":null,"citation":"CITATION.cff","codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null},"funding":{"github":["bkuhlmann"]}},"created_at":"2022-09-10T13:28:41.000Z","updated_at":"2024-05-29T04:11:41.000Z","dependencies_parsed_at":"2023-02-12T17:30:47.140Z","dependency_job_id":"d30895db-6823-4869-aa79-7b8c746fa0a2","html_url":"https://github.com/bkuhlmann/transactable","commit_stats":{"total_commits":60,"total_committers":1,"mean_commits":60.0,"dds":0.0,"last_synced_commit":"b947fff0595e24fc9fe0ffa926be9f93a372fb32"},"previous_names":[],"tags_count":16,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/bkuhlmann%2Ftransactable","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/bkuhlmann%2Ftransactable/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/bkuhlmann%2Ftransactable/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/bkuhlmann%2Ftransactable/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/bkuhlmann","download_url":"https://codeload.github.com/bkuhlmann/transactable/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":223622421,"owners_count":17174865,"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":["function-composition","transaction"],"created_at":"2024-08-06T08:01:46.938Z","updated_at":"2024-11-08T02:33:54.110Z","avatar_url":"https://github.com/bkuhlmann.png","language":"Ruby","funding_links":["https://github.com/sponsors/bkuhlmann"],"categories":["Ruby"],"sub_categories":[],"readme":":toc: macro\n:toclevels: 5\n:figure-caption!:\n\n:command_pattern_link: link:https://alchemists.io/articles/command_pattern[Command Pattern]\n:debug_link: link:https://github.com/ruby/debug[Debug]\n:dry_container_link: link:https://dry-rb.org/gems/dry-container[Dry Container]\n:dry_events_link: link:https://dry-rb.org/gems/dry-events[Dry Events]\n:dry_monads_link: link:https://dry-rb.org/gems/dry-monads[Dry Monads]\n:dry_schema_link: link:https://dry-rb.org/gems/dry-schema[Dry Schema]\n:dry_validation_link: link:https://dry-rb.org/gems/dry-validation[Dry Validation]\n:function_composition_link: link:https://alchemists.io/articles/ruby_function_composition[Function Composition]\n:infusible_link: link:https://alchemists.io/projects/infusible[Infusible]\n:railway_pattern_link: link:https://fsharpforfunandprofit.com/posts/recipe-part2[Railway Pattern]\n\n= Transactable\n\n⚠️ *This gem is deprecated and will be fully destroyed on 2025-03-05. Please use the link:https://alchemists.io/projects/pipeable[Pipeable] gem instead.* ⚠️\n\nA DSL for transactional workflows built atop native {function_composition_link} which leverages the {railway_pattern_link}. This allows you to write a sequence of _steps_ that cleanly read from left-to-right or top-to-bottom which results in a success or a failure without having to rely on exceptions which are expensive.\n\ntoc::[]\n\n== Features\n\n* Built atop of native {function_composition_link}.\n* Adheres to the {railway_pattern_link}.\n* Provides built-in and customizable domain-specific steps.\n* Provides chainable _pipes_ which can be used to build more complex workflows.\n* Supports instrumentation for tracking metrics, logging usage, and much more.\n* Compatible with {dry_monads_link}.\n* Compatible with {infusible_link}.\n\n== Requirements\n\n. link:https://www.ruby-lang.org[Ruby].\n. A strong understanding of {function_composition_link}.\n\n== Setup\n\nTo install _with_ security, run:\n\n[source,bash]\n----\n# 💡 Skip this line if you already have the public certificate installed.\ngem cert --add \u003c(curl --compressed --location https://alchemists.io/gems.pem)\ngem install transactable --trust-policy HighSecurity\n----\n\nTo install _without_ security, run:\n\n[source,bash]\n----\ngem install transactable\n----\n\nYou can also add the gem directly to your project:\n\n[source,bash]\n----\nbundle add transactable\n----\n\nOnce the gem is installed, you only need to require it:\n\n[source,ruby]\n----\nrequire \"transactable\"\n----\n\n== Usage\n\nYou can turn any object into a _transaction_ by requiring and including this gem as follows:\n\n[source,ruby]\n----\nrequire \"csv\"\nrequire \"transactable\"\n\nclass Demo\n  include Transactable\n\n  def initialize client: CSV\n    @client = client\n  end\n\n  def call data\n    pipe data,\n         check(/Book.+Price/, :match?),\n         :parse,\n         map { |item| \"#{item[:book]}: #{item[:price]}\" }\n  end\n\n  private\n\n  attr_reader :client\n\n  def parse result\n    result.fmap do |data|\n      client.instance(data, headers: true, header_converters: proc { |key| key.downcase.to_sym })\n            .to_a\n            .map(\u0026:to_h)\n    end\n  end\nend\n----\n\nThe above allows `Demo#call` to be a _transactional_ sequence steps which may pass or fail due to all step being {dry_monads_link}. This is the essence of the {railway_pattern_link}.\n\nTo execute the above example, you'd only need to pass CSV content to it:\n\n[source,ruby]\n----\nDemo.new.call \u003c\u003c~CSV\n  Book,Author,Price,At\n  Mystics,urGoh,10.50,2022-01-01\n  Skeksis,skekSil,20.75,2022-02-13\nCSV\n----\n\nThe computed result is a success with each book listing a price:\n\n....\nSuccess [\"Mystics: 10.50\", \"Skeksis: 20.75\"]\n....\n\n=== Pipe\n\nOnce you've included the `Transactable` module within your class, the `#pipe` method is available to you and is how you build a sequence of steps for processing. The method signature is:\n\n[source,ruby]\n----\npipe(input, *steps)\n----\n\nThe first argument is your input which can be a Ruby primitive or a monad. Regardless, the input will be automatically wrapped as a `Success` -- but only if not a `Result` to begin with -- before passing to the first step. From there, all steps are _required_ to answer a monad in order to adhere to the {railway_pattern_link}.\n\nBehind the scenes, the `#pipe` method is syntactic sugar on top of {function_composition_link} which means if this code were to be rewritten:\n\n[source,ruby]\n----\npipe csv,\n     check(/Book.+Price/, :match?),\n     :parse,\n     map { |item| \"#{item[:book]}: #{item[:price]}\" }\n----\n\nThen the above would look like this using native Ruby:\n\n[source,ruby]\n----\n(\n  check(/Book.+Price/, :match?) \u003e\u003e\n  method(:parse) \u003e\u003e\n  map { |item| \"#{item[:book]}: #{item[:price]}\" }\n).call Success(csv)\n----\n\nThe problem with native function composition is that it reads backwards by passing your input at the end of all sequential steps. With the `#pipe` method, you have the benefit of allowing your eye to read the code from top to bottom in addition to not having to type multiple _forward composition_ operators.\n\n=== Steps\n\nThere are several ways to compose steps for your transactional pipe. As long as all steps succeed, you'll get a successful response. Otherwise, the first step to fail will pass the failure down by skipping all subsequent steps (unless you dynamically attempt to turn the failure into a success). The following sections detail how to mix and match steps for building a robust implementation.\n\n==== Basic\n\nThe following are the basic (default) steps for building for more advanced functionality.\n\n===== As\n\nAllows you to message the input as different output. Example:\n\n[source,ruby]\n----\npipe :a, as(:inspect)                  # Success \":a\"\npipe %i[a b c], as(:dig, 1)            # Success :b\npipe Failure(\"Danger!\"), as(:inspect)  # Failure \"Danger!\"\n----\n\n===== Bind\n\nAllows you to perform operations on a successful result only. You are then responsible for answering a success or failure accordingly. This is a convenience wrapper to native {dry_monads_link} `#bind` functionality. Example:\n\n[source,ruby]\n----\npipe %i[a b c], bind { |input| Success input.join(\"-\") }           # Success \"a-b-c\"\npipe %i[a b c], bind { |input| Failure input }                     # Failure [:a, :b, :c]\npipe Failure(\"Danger!\"), bind { |input| Success input.join(\"-\") }  # Failure \"Danger!\"\n----\n\n===== Check\n\nAllows you to check if the input and messaged object evaluate to `true` or `Success`. When successful, input is passed through as a `Success`. When false, input is passed through as a `Failure`. Example:\n\n[source,ruby]\n----\npipe :a, check(%i[a b], :include?)                  # Success :a\npipe :a, check(%i[b c], :include?)                  # Failure :a\npipe Failure(\"Danger!\"), check(%i[a b], :include?)  # Failure \"Danger!\"\n----\n\n===== Fmap\n\nAllows you to unwrap a successful operation, make a modification, and rewrap the modification as a new success. This is a convenience wrapper to native {dry_monads_link} `#fmap` functionality. Example:\n\n[source,ruby]\n----\npipe %i[a b c], fmap { |input| input.join \"-\" }           # Success \"a-b-c\"\npipe Failure(\"Danger!\"), fmap { |input| input.join \"-\" }  # Failure \"Danger!\"\n----\n\n===== Insert\n\nAllows you to insert an element after the input (default behavior) and wraps native link:https://rubyapi.org/o/array#method-i-insert[Array#insert] functionality. If the input is not an array, it will be cast as one. You can use the `:at` key to specify where you want insertion to happen. This step is most useful when needing to assemble arguments for passing to a subsequent step. Example:\n\n[source,ruby]\n----\npipe :a, insert(:b)                  # Success [:a, :b]\npipe :a, insert(:b, at: 0)           # Success [:b, :a]\npipe %i[a c], insert(:b, at: 1)      # Success [:a, :b, :c]\npipe Failure(\"Danger!\"), insert(:b)  # Failure \"Danger!\"\n----\n\n===== Map\n\nAllows you to map over an enumerable and wraps native link:https://rubyapi.org/o/enumerable#method-i-map[Enumerable#map] functionality.\n\n[source,ruby]\n----\npipe %i[a b c], map(\u0026:inspect)           # Success [\":a\", \":b\", \":c\"]\npipe Failure(\"Danger!\"), map(\u0026:inspect)  # Failure \"Danger!\"\n----\n\n===== Merge\n\nAllows you to merge the input with additional attributes as a single hash. If the input is not a hash, then the input will be merged with the attributes using `step` as the key. The default `step` key can be renamed to a different key by using the `:as` key. Like the _Insert_ step, this is most useful when needing to assemble arguments and/or data for consumption by subsequent steps. Example:\n\n[source,ruby]\n----\npipe({a: 1}, merge(b: 2))             # Success {a: 1, b: 2}\npipe \"test\", merge(b: 2)              # Success {step: \"test\", b: 2}\npipe \"test\", merge(as: :a, b: 2)      # Success {a: \"test\", b: 2}\npipe Failure(\"Danger!\"), merge(b: 2)  # Failure \"Danger!\"\n----\n\n===== Orr\n\nAllows you to operate on a failure and produce either a success or another failure. This is a convenience wrapper to native {dry_monads_link} `#or` functionality.\n\nℹ️ Syntactically, `or` can't be used for this step since `or` is a native Ruby keyword so `orr` is used instead.\n\nExample:\n\n[source,ruby]\n----\npipe %i[a b c], orr { |input| Success input.join(\"-\") }          # Success [:a, :b, :c]\npipe Failure(\"Danger!\"), orr { Success \"Resolved\" }              # Success \"Resolved\"\npipe Failure(\"Danger!\"), orr { |input| Failure \"Big #{input}\" }  # Failure \"Big Danger!\"\n----\n\n===== Tee\n\nAllows you to run an operation and ignore the response while input is passed through as output. This behavior is similar in nature to the link:https://www.gnu.org/savannah-checkouts/gnu/gawk/manual/html_node/Tee-Program.html[tee] program in Bash. Example:\n\n[source,ruby]\n----\npipe \"test\", tee(Kernel, :puts, \"Example.\")\n\n# Example.\n# Success \"test\"\n\npipe Failure(\"Danger!\"), tee(Kernel, :puts, \"Example.\")\n\n# Example.\n# Failure \"Danger!\"\n----\n\n===== To\n\nAllows you to delegate to an object -- which doesn't have a callable interface and may or may not answer a result -- for processing of input. If the response is not a monad, it'll be automatically wrapped as a `Success`. Example:\n\n[source,ruby]\n----\nModel = Struct.new :label, keyword_init: true do\n  include Dry::Monads[:result]\n\n  def self.for(...) = Success new(...)\nend\n\npipe({label: \"Test\"}, to(Model, :for))    # Success #\u003cstruct Model label=\"Test\"\u003e\npipe Failure(\"Danger!\"), to(Model, :for)  # Failure \"Danger!\"\n----\n\n===== Try\n\nAllows you to try an operation which may fail while catching the exception as a failure for further processing. Example:\n\n[source,ruby]\n----\npipe \"test\", try(:to_json, catch: JSON::ParserError)     # Success \"\\\"test\\\"\"\npipe \"test\", try(:invalid, catch: NoMethodError)         # Failure \"undefined method...\"\npipe Failure(\"Danger!\"), try(:to_json, catch: JSON::ParserError)  # Failure \"Danger!\"\n----\n\n===== Use\n\nAllows you to use another transaction which might have multiple steps of it's own, use an object that adheres to the {command_pattern_link}, or any function which answers a {dry_monads_link} `Result` object. In other words, you can use _use_ any object which responds to `#call` and answers a {dry_monads_link} `Result` object. This is great for chaining multiple transactions together.\n\n[source,ruby]\n----\nfunction = -\u003e input { Success input * 3 }\n\npipe 3, use(function)                   # Success 9\npipe Failure(\"Danger!\"), use(function)  # Failure \"Danger!\"\n----\n\n===== Validate\n\nAllows you to use an operation that will validate the input. This is especially useful when using {dry_schema_link}, {dry_validation_link}, or any operation that can respond to `#call` while answering a result that can be converted into a hash.\n\nBy default, the `:as` key uses `:to_h` as it's value so you get automatic casting to a `Hash`. Use `nil`, as the value, to disable this behavior. You can also pass in any value to the `:as` key which is a valid method that the result will respond to.\n\n[source,ruby]\n----\nschema = Dry::Schema.Params { required(:label).filled :string }\n\npipe({label: \"Test\"}, validate(schema))           # Success label: \"Test\"\npipe({label: \"Test\"}, validate(schema, as: nil))  # Success #\u003cDry::Schema::Result{:label=\u003e\"Test\"} errors={} path=[]\u003e\npipe Failure(\"Danger!\"), validate(schema)         # Failure \"Danger!\"\n----\n\n==== Advanced\n\nSeveral options are available should you need to advance beyond the basic steps. Each is described in detail below.\n\n===== Procs\n\nYou can always use a `Proc` as a custom step. Example:\n\n[source,ruby]\n----\ninclude Transactable\ninclude Dry::Monads[:result]\n\npipe :a,\n     insert(:b),\n     proc { Success \"input_ignored\" },\n     as(:to_sym)\n\n# Yields: Success :input_ignored\n----\n\nℹ️ While procs are effective, you are limited in what you can do with them in terms of additional behavior and instrumentation support.\n\n===== Lambdas\n\nIn addition to procs, lambdas can be used too. Example:\n\n[source,ruby]\n----\ninclude Transactable\n\npipe :a,\n     insert(:b),\n     -\u003e result { result.fmap { |input| input.join \"_\" } },\n     as(:to_sym)\n\n# Yields: Success :a_b\n----\n\nℹ️ Lambdas are a step up from procs but, like procs, you are limited in what you can do with them in terms of additional behavior and instrumentation support.\n\n===== Methods\n\nMethods -- in addition to procs and lambdas -- are the _preferred_ way to add custom steps due to the concise syntax. Example:\n\n[source,ruby]\n----\nclass Demo\n  include Transactable\n\n  def call input\n    pipe :a,\n         insert(:b),\n         :join,\n         as(:to_sym)\n  end\n\n  private\n\n  def join(result) = result.fmap { |input| input.join \"_\" }\nend\n\nDemo.new.call :a  # Yields: Success :a_b\n----\n\nAll methods can be referenced by symbol as shown via `:join` above. Using a symbol is syntactic sugar for link:https://rubyapi.org/o/object#method-i-method[Object#method] so the use of the `:join` symbol is the same as using `method(:join)`. Both work but the former requires less typing than the latter.\n\nℹ️ You won't be able to instrument these method calls (unless you inject instrumentation) but are great when needing additional behavior between the default steps.\n\n===== Custom\n\nIf you'd like to define permanent and reusable steps, you can register a custom step which requires you to:\n\n. Define a custom step as a new class.\n. Register your custom step along side the existing default steps.\n\nHere's what this would look like:\n\n[source,ruby]\n----\nmodule MySteps\n  class Join \u003c Transactable::Steps::Abstract\n    def initialize(delimiter = \"_\", **)\n      super(**)\n      @delimiter = delimiter\n    end\n\n    def call(result) = result.fmap { |input| input.join delimiter }\n\n    private\n\n    attr_reader :delimiter\n  end\nend\n\nTransactable::Steps::Container.register(:join) { MySteps::Join }\n\ninclude Transactable\n\npipe :a, insert(:b), join, as(:to_sym)\n# Yields: Success :a_b\n\npipe :a, insert(:b), join(\"\"), as(:to_sym)\n# Yields: Success :ab\n----\n\n=== Containers\n\nShould you not want the basic steps, need custom steps, or a hybrid of basic and custom steps, you can define your own container and provide it as an argument to `.with` when including transactable behavior. Example:\n\n[source,ruby]\n----\nrequire \"dry/container\"\n\nmodule MyContainer\n  extend Dry::Container::Mixin\n\n  register :echo, -\u003e result { result }\n  register(:insert) { Transactable::Steps::Insert }\nend\n\ninclude Transactable.with(MyContainer)\n\npipe :a, echo, insert(:b)\n\n# Yields: Success [:a, :b]\n----\n\nThe above is a hybrid example where the `MyContainer` registers a custom `echo` step along with the default `insert` step to make a new container. This is included when passed in as an argument via `.with` (i.e. `include Transactable.with(MyContainer)`).\n\nWhether you use default, custom, or hybrid steps, you have maximum flexibility using this approach.\n\n=== Composition\n\nShould you ever need to make a plain old Ruby object functionally composable, then you can _include_ the `Transactable::Composable` module which will give you the necessary `\\#\u003e\u003e`, `#\u003c\u003c`, and `#call` methods where you only need to implement the `#call` method.\n\n=== Instrumentation\n\nEach transaction includes instrumentation using {dry_events_link} which you can subscribe to or ignore entirely. The following events are supported:\n\n* `step`: Published for each step regardless of success or failure.\n* `step.success`: Published for success steps only.\n* `step.failure`: Published for failure steps only.\n\nUsing the example code at the start of this _Usage_ section, here's how you can subscribe to events emitted by the transaction:\n\n[source,ruby]\n----\nTransactable::Instrument::EVENTS.each do |name|\n  Transactable::Container[:instrument].subscribe name do |event|\n    puts \"#{event.id}: #{event.payload}\"\n  end\nend\n----\n\nNow, as before, you can call the transaction with subscribers enabled:\n\n[source,ruby]\n----\ndemo.call csv\n----\n\nThe above will then yield the following results in your console:\n\n....\nstep: {:name=\u003e\"Transactable::Steps::Check\", :arguments=\u003e[[], {}, nil]}\nstep.success: {:name=\u003e\"Transactable::Steps::Check\", :value=\u003e\"Book,Author,Price,At\\nMystics,urGoh,10.50,2022-01-01\\nSkeksis,skekSil,20.75,2022-02-13\\n\", :arguments=\u003e[[], {}, nil]}\nstep: {:name=\u003e\"Transactable::Steps::Map\", :arguments=\u003e[[], {}, #\u003cProc:0x0000000106405900 (irb):15\u003e]}\nstep.success: {:name=\u003e\"Transactable::Steps::Map\", :value=\u003e[\"Mystics: 10.50\", \"Skeksis: 20.75\"], :arguments=\u003e[[], {}, #\u003cProc:0x0000000106405900 (irb):15\u003e]}\n....\n\nFinally, the `Transactable::Instrumentable` module is available should you need to _prepend_ instrumentation to any of your class' `#call` methods.\n\nThere is a lot you can do with instrumentation so check out the {dry_events_link} documentation for further details.\n\n== Development\n\nTo contribute, run:\n\n[source,bash]\n----\ngit clone https://github.com/bkuhlmann/transactable\ncd transactable\nbin/setup\n----\n\nYou can also use the IRB console for direct access to all objects:\n\n[source,bash]\n----\nbin/console\n----\n\n=== Architecture\n\nThe architecture of this gem is built on top of the following concepts and gems:\n\n* {function_composition_link}: Made possible through the use of the `\\#\u003e\u003e` and `#\u003c\u003c` methods on the link:https://rubyapi.org/3.1/o/method[Method] and link:https://rubyapi.org/3.1/o/proc[Proc] objects.\n* {dry_container_link}: Allows related dependencies to be grouped together for injection as desired.\n* {dry_events_link}: Allows all steps to be observable so you can subscribe to any/all events for metric, logging, and other capabilities.\n* {dry_monads_link}: Critical to ensuring the entire pipeline of steps adhere to the {railway_pattern_link} and leans heavily on the `Result` object.\n* link:https://dry-rb.org/gems/dry-transaction[Dry Transaction]: Specifically the concept of a _step_ where each step can have an _operation_ and/or _input_ to be processed. Instrumentation is used as well so you can have rich metrics, logging, or any other kind of observer wired up as desired.\n* link:https://alchemists.io/projects/infusible[Infusible]: Coupled with {dry_container_link}, allows dependencies to be automatically injected.\n* link:https://alchemists.io/projects/marameters[Marameters]: Through the use of the `.categorize` method, dynamic message passing is possible by inspecting the operation method's parameters.\n\n=== Style Guide\n\n* *Transactions*\n** Use a single method (i.e. `#call`) which is public and adheres to the {command_pattern_link} so transactions can be piped together if desired.\n* *Steps*\n** Inherit from the `Abstract` class in order to gain monad, composition, and dependency behavior. Any dependencies injected are automatically filtered out so all subclasses have direct and clean access to the base positional, keyword, and block arguments. These variables are prefixed with `base_*` in order to not conflict with subclasses which might only want to use non-prefixed variables for convenience.\n** All filtered arguments -- in other words, the unused arguments -- need to be passed up to the superclass from the subclass (i.e. `super(*positionals, **keywords, \u0026block)`). Doing so allows the superclass (i.e. `Abstract`) to provide access to `base_positionals`, `base_keywords`, and `base_block` for use if desired by the subclass.\n** The `#call` method must define a single positional `result` parameter since a monad will be passed as an argument. Example: `def call(result) = # Implementation`.\n** Each block within the `#call` method should use the `input` parameter to be consistent. More specific parameters like `argument` or `operation` should be used to improve readability when possible. Example: `def call(result) = result.bind { |input| # Implementation }`.\n** Use implicit blocks sparingly. Most of the default steps shy away from using blocks because it can make the code more complex. Use private methods, custom steps, and/or separate transactions if the code becomes too complex because you might have a smaller object which needs extraction.\n\n=== Debugging\n\nIf you need to debug (i.e. {debug_link}) your pipe, use a lambda. Example:\n\n[source,ruby]\n----\npipe data,\n     check(/Book.+Price/, :match?),\n     -\u003e result { binding.break },    # Breakpoint\n     :parse\n----\n\nThe above breakpoint will allow you inspect the result of the `#check` step and/or build a modified result for passing to the subsequent `#method` step.\n\n=== Troubleshooting\n\nThe following might be of aid to as you implement your own transactions.\n\n==== Type Errors\n\nIf you get a `TypeError: Step must be functionally composable and answer a monad`, it means:\n\n. The step must be a `Proc`, `Method`, or some object which responds to `\\#\u003e\u003e`, `#\u003c\u003c`, and `#call`.\n. The step doesn't answer a result monad (i.e. `Success some_value` or `Failure some_value`).\n\n==== No Method Errors\n\nIf you get a `NoMethodError: undefined method `success?` exception, it might mean that you forgot to add a comma after one of your steps. Example:\n\n[source,ruby]\n----\n# Valid\npipe \"https://www.wikipedia.org\",\n     to(client, :get),\n     try(:parse, catch: HTTP::Error)\n\n# Invalid\npipe \"https://www.wikipedia.org\",\n     to(client, :get)  # \u003c= Comma is missing on this line.\n     try(:parse, catch: HTTP::Error)\n----\n\n== Tests\n\nTo test, run:\n\n[source,bash]\n----\nbin/rake\n----\n\n== Benchmarks\n\nTo view/compare performance, run:\n\n[source,bash]\n----\nbin/benchmark\n----\n\n💡 You can view current benchmarks at the end of the above file if you don't want to manually run them.\n\n== link:https://alchemists.io/policies/license[License]\n\n== link:https://alchemists.io/policies/security[Security]\n\n== link:https://alchemists.io/policies/code_of_conduct[Code of Conduct]\n\n== link:https://alchemists.io/policies/contributions[Contributions]\n\n== link:https://alchemists.io/projects/transactable/versions[Versions]\n\n== link:https://alchemists.io/community[Community]\n\n== Credits\n\n* Built with link:https://alchemists.io/projects/gemsmith[Gemsmith].\n* Engineered by link:https://alchemists.io/team/brooke_kuhlmann[Brooke Kuhlmann].\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fbkuhlmann%2Ftransactable","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fbkuhlmann%2Ftransactable","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fbkuhlmann%2Ftransactable/lists"}