{"id":13878692,"url":"https://github.com/drexed/lite-command","last_synced_at":"2025-05-07T09:34:29.817Z","repository":{"id":35053612,"uuid":"201136103","full_name":"drexed/lite-command","owner":"drexed","description":"Ruby Command based framework (aka service objects)","archived":false,"fork":false,"pushed_at":"2025-05-02T16:51:02.000Z","size":384,"stargazers_count":5,"open_issues_count":4,"forks_count":0,"subscribers_count":2,"default_branch":"master","last_synced_at":"2025-05-02T17:05:02.259Z","etag":null,"topics":["command-pattern","ruby","service-object"],"latest_commit_sha":null,"homepage":"https://drexed.github.io/lite-command","language":"Ruby","has_issues":false,"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/drexed.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":null,"funding":null,"license":"LICENSE.txt","code_of_conduct":"CODE_OF_CONDUCT.md","threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2019-08-07T22:23:04.000Z","updated_at":"2025-05-02T16:50:45.000Z","dependencies_parsed_at":"2024-01-13T20:37:50.369Z","dependency_job_id":"b1cb0071-97b4-4f71-b5f9-9bd568671e00","html_url":"https://github.com/drexed/lite-command","commit_stats":{"total_commits":40,"total_committers":3,"mean_commits":"13.333333333333334","dds":"0.050000000000000044","last_synced_commit":"ae09fecc19103107ae9e3f78535acbcf74783e5f"},"previous_names":[],"tags_count":40,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/drexed%2Flite-command","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/drexed%2Flite-command/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/drexed%2Flite-command/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/drexed%2Flite-command/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/drexed","download_url":"https://codeload.github.com/drexed/lite-command/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":252851629,"owners_count":21814185,"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":["command-pattern","ruby","service-object"],"created_at":"2024-08-06T08:01:56.918Z","updated_at":"2025-05-07T09:34:29.796Z","avatar_url":"https://github.com/drexed.png","language":"Ruby","funding_links":[],"categories":["Ruby"],"sub_categories":[],"readme":"# Lite::Command\n\n[![Gem Version](https://badge.fury.io/rb/lite-command.svg)](http://badge.fury.io/rb/lite-command)\n\nLite::Command provides an API for building simple and complex command based service objects.\n\n## Installation\n\nAdd this line to your application's Gemfile:\n\n```ruby\ngem 'lite-command'\n```\n\nAnd then execute:\n\n    $ bundle\n\nOr install it yourself as:\n\n    $ gem install lite-command\n\n## Table of Contents\n\n* [Configuration](#configuration)\n* [Usage](#usage)\n* [Execution](#execution)\n  * [Dynamic Faults](#dynamic-faults)\n  * [Raising Faults](#raising-faults)\n* [Context](#context)\n  * [Attributes](#attributes)\n  * [Validations](#validations)\n* [States](#states)\n* [Statuses](#statuses)\n* [Hooks](#hooks)\n  * [Lifecycle Hooks](#lifecycle-hooks)\n  * [Status Hooks](#status-hooks)\n  * [State Hooks](#state-hooks)\n* [Children](#children)\n  * [Throwing Faults](#throwing-faults)\n* [Sequences](#sequences)\n* [Results](#results)\n* [Examples](#examples)\n  * [Disable Instance Calls](#disable-instance-calls)\n* [Generator](#generator)\n\n## Configuration\n\n`rails g lite:command:install` will generate the following file in your application root:\n`config/initalizers/lite_command.rb`\n\n```ruby\nLite::Command.configure do |config|\n  config.raise_dynamic_faults = true\nend\n```\n\n## Usage\n\nDefining a command is as simple as inheriting the base class and adding a `call` method\nto a command object (required).\n\n```ruby\nclass DecryptSecretMessage \u003c Lite::Command::Base\n\n  def call\n    if invalid_magic_numbers?\n      invalid!(\"Invalid crypto message\")\n    else\n      context.decrypted_message = SecretMessage.decrypt(context.encrypted_message)\n    end\n  end\n\n  private\n\n  def invalid_magic_numbers?\n    # Some logic...\n  end\n\nend\n```\n\n\u003e [!TIP]\n\u003e You should treat all command as emphemeral objects, so you should think about making\n\u003e all of your domain logic private and leaving the default command API is exposed.\n\n## Execution\n\nExecuting a command can be done as an instance or class call. It returns the command instance\nin a frozen state. These will never call will never raise an execption, but will be kept track\nof in its internal state.\n\n```ruby\nDecryptSecretMessage.call(...)\n# - or -\nDecryptSecretMessage.new(...).call\n\n# On success, fault or exception:\n#=\u003e \u003cDecryptSecretMessage ...\u003e\n```\n\n\u003e [!TIP]\n\u003e Class calls is the prefered format due to its readability. Read the [Disable Instance Calls](#disable-instance-calls)\n\u003e section on how to prevent instance style calls.\n\nCommands can be called with a `!` bang method to raise a `Lite::Command::Fault` or the\noriginal `StandardError` based exceptions.\n\n```ruby\nDecryptSecretMessage.call!(...)\n# - or -\nDecryptSecretMessage.new(...).call!\n\n# On success:\n#=\u003e \u003cDecryptSecretMessage ...\u003e\n\n# On fault:\n#=\u003e raises Lite::Command::Fault\n\n# On exception:\n#=\u003e raises StandardError\n```\n\n### Raising Faults\n\nSometimes its suitable to raise the offending soft call command fault later\nin a call stack. Use the `raise!` method to reraise the fault or original\nerror (if they differ). `original: false` is the default.\n\n```ruby\ncmd = DecryptSecretMessage.call(...)\nApm.track_stat(\"DecryptSecretMessage.called\")\n# other stuff...\n\n# On success:\ncmd.raise! #=\u003e nil\n\n# On fault:\ncmd.raise!(original: false) #=\u003e raises Lite::Command::Fault\ncmd.raise!(original: true)  #=\u003e raises Lite::Command::Fault\n\n# On exception:\ncmd.raise!(original: false) #=\u003e raises Lite::Command::Error\ncmd.raise!(original: true)  #=\u003e raises StandardError\n\n# Access the exception objects directly\ncmd.original_exception #=\u003e \u003cStandardError ...\u003e\ncmd.command_exception  #=\u003e \u003cLite::Command::Error ...\u003e\n```\n\n### Dynamic Faults\n\nDynamic faults are custom faults named after your command. This is especially\nhelpful for catching + running custom logic or filtering out specific\nexceptions from your APM service.\n\n```ruby\nclass DecryptSecretMessage \u003c Lite::Command::Base\n\n  def call\n    fail!(\"Some failure\")\n  end\n\n  private\n\n  # Disable raising dynamic faults on a per command basis.\n  # The `raise_dynamic_faults` configuration option must be\n  # enabled for this method to have any affect.\n  def raise_dynamic_faults?\n    false\n  end\n\nend\n\nDecryptSecretMessage.call!(...)\n#=\u003e raises DecryptSecretMessage::Failure\n```\n\n## Context\n\nAccessing the call arguments can be done through its internal context.\nIt can be used as internal storage to be accessed by it self and any\nof its children commands.\n\n\u003e [!NOTE]\n\u003e Attributes that do **NOT** exist on the context will return `nil`.\n\n```ruby\nclass DecryptSecretMessage \u003c Lite::Command::Base\n\n  def call\n    # `ctx` is an alias to `context`\n    context.decrypted_message = SecretMessage.decrypt(ctx.encrypted_message)\n  end\n\nend\n\ncmd = DecryptSecretMessage.call(encrypted_message: \"a22j3nkenjk2ne2\")\ncmd.context.decrypted_message #=\u003e \"Hello World\"\ncmd.ctx.fake_message          #=\u003e nil\n```\n\n### Attributes\n\nDelegate methods for a cleaner command setup by declaring `required` and\n`optional` arguments. `required` only verifies that argument was pass to the\ncontext or can be called via defined method or another delegated method.\nIs an `:if` or `:unless` callable option on a `required` delegation evaluates\nto false, it will be delegated as an `optional` attribute.\n\n```ruby\nclass DecryptSecretMessage \u003c Lite::Command::Base\n\n  required :user, :encrypted_message\n  required :secret_key, from: :user\n  required :algo, :algo_detector, if: :signed_in?\n  optional :version\n\n  def call\n    context.decrypted_message = SecretMessage.decrypt(\n      encrypted_message,\n      decryption_key: ENV[\"DECRYPT_KEY\"],\n      algo: algo,\n      version: version || 2\n    )\n  end\n\n  private\n\n  def algo_detector\n    @algo_detector ||= AlgoDetector.new(encrypted_message)\n  end\n\n  def signed_in?\n    ctx.user.signed_in?\n  end\n\nend\n\n# With valid options:\ncmd = DecryptSecretMessage.call(user: user, encrypted_message: \"ll23k2j3kcms\", version: 9)\ncmd.status                    #=\u003e \"success\"\ncmd.context.decrypted_message #=\u003e \"Hola Mundo\"\n\n# With invalid options:\ncmd = DecryptSecretMessage.call\ncmd.status   #=\u003e \"invalid\"\ncmd.reason   #=\u003e \"Encrypted message is a required argument. User is an undefined argument...\"\ncmd.metadata #=\u003e {\n             #=\u003e   user: [\"is a required argument\", \"is an undefined argument\"],\n             #=\u003e   encrypted_message: [\"is a required argument\"]\n             #=\u003e }\n```\n\n### Validations\n\nThe full power of active model valdations is available to validate\nany and all delegated arguments.\n\n```ruby\nclass DecryptSecretMessage \u003c Lite::Command::Base\n\n  required :encrypted_message\n  optional :version\n\n  validates :encrypted_message, length: 10..999\n  validates :version, inclusion: { in: %w[v1 v3 v8], allow_blank: true }\n  validate :validate_decrypt_magic_numbers\n\n  def call\n    context.decrypted_message = SecretMessage.decrypt(encrypted_message)\n  end\n\n  private\n\n  def validate_decrypt_magic_numbers\n    return if encrypted_message.starts_with?(\"~x01~\")\n\n    errors.add(:encrypted_message, :invalid, message: \"has invalid magic numbers\")\n  end\n\nend\n\n# With valid options:\ncmd = DecryptSecretMessage.call(encrypted_message: \"ll23k2j3kcms\", version: \"v1\")\ncmd.status                    #=\u003e \"success\"\ncmd.context.decrypted_message #=\u003e \"Hola Mundo\"\n\n# With invalid options:\ncmd = DecryptSecretMessage.call(encrypted_message: \"idk\", version: \"v23\")\ncmd.status   #=\u003e \"invalid\"\ncmd.reason   #=\u003e \"Encrypted message is too short (minimum is 10 character). Encrypted message has invalid magic numbers. Version is not included in list.\"\ncmd.metadata #=\u003e {\n             #=\u003e   user: [\"is not included in list\"],\n             #=\u003e   encrypted_message: [\"is too short (minimum is 10 character)\", \"has invalid magic numbers\"]\n             #=\u003e }\n```\n\n## States\n`state` represents the condition of all the code command should execute.\n\n| Status        | Description |\n| ------------- | ----------- |\n| `pending`     | Command objects that have been initialized. |\n| `executing`   | Command objects that are actively executing code. |\n| `complete`    | Command objects that executed to completion without fault/exception. |\n| `interrupted` | Command objects that could **NOT** be executed to completion due to a fault/exception. |\n\n\u003e [!CAUTION]\n\u003e States are automatically transitioned and should **NEVER** be altered manually.\n\n```ruby\ncmd = DecryptSecretMessage.call\ncmd.state        #=\u003e \"complete\"\n\ncmd.pending?     #=\u003e false\ncmd.executing?   #=\u003e false\ncmd.complete?    #=\u003e true\ncmd.interrupted? #=\u003e false\n\n# `complete` or `interrupted`\ncmd.executed?\n```\n\n## Statuses\n\n`status` represents the state of the domain logic executed via the `call` method.\nA status of `success` is returned even if the command has **NOT** been executed.\n\n| Status    | Description |\n| --------- | ----------- |\n| `success` | Call execution completed without fault/exception. |\n| `noop`    | **Fault** to skip completion of call execution early for an unsatisfied condition where proceeding is pointless. |\n| `invalid` | **Fault** to stop call execution due to missing, bad, or corrupt data. |\n| `failure` | **Fault** to stop call execution due to an unsatisfied condition where it blocks proceeding any further. |\n| `error`   | **Fault** to stop call execution due to a thrown `StandardError` based exception. |\n\n\u003e [!IMPORTANT]\n\u003e Each **fault** status has a setter method ending in `!` that invokes a matching fault procedure.\n\u003e Metadata may also be passed to enrich your fault response.\n\n```ruby\nclass DecryptSecretMessage \u003c Lite::Command::Base\n\n  def call\n    if context.encrypted_message.empty?\n      noop!(\"No message to decrypt\")\n    elsif context.encrypted_message.start_with?(\"== womp\")\n      invalid!(\"Invalid message start value\", metadata: { i18n: \"gb.invalid_start_value\" })\n    elsif context.encrypted_message.algo?(OldAlgo)\n      failure!(\"Unsafe encryption algo detected\")\n    else\n      context.decrypted_message = SecretMessage.decrypt(ctx.encrypted_message)\n    end\n  rescue CryptoError =\u003e e\n    Apm.report_error(e)\n    error!(\"Failed decryption due to: #{e}\", original_exception: e)\n  end\n\nend\n\ncmd = DecryptSecretMessage.call(encrypted_message: \"2jk3hjeh2hj2jh\")\ncmd.status   #=\u003e \"invalid\"\ncmd.reason   #=\u003e \"Invalid message start value\"\ncmd.metadata #=\u003e { i18n: \"gb.invalid_start_value\" }\n\ncmd.success? #=\u003e false\ncmd.noop?    #=\u003e false\ncmd.invalid? #=\u003e true\ncmd.invalid?(\"Other reason\") #=\u003e false\ncmd.failure? #=\u003e false\ncmd.error?   #=\u003e false\n\n# `success` or `noop`\ncmd.ok?      #=\u003e false\ncmd.ok?(\"Other reason\") #=\u003e false\n\n# NOT `success`\ncmd.fault?   #=\u003e true\ncmd.fault?(\"Other reason\") #=\u003e false\n\n# `invalid` or `failure` or `error`\ncmd.bad?     #=\u003e true\ncmd.bad?(\"Other reason\") #=\u003e false\n```\n\n## Hooks\n\nUse hooks to run arbituary code at transition points and on finalized internals.\nAll hooks are ran in the order they are defined. Hooks types can be defined\nmultiple times. Hooks are ran in the following order:\n\n```ruby\n1. after_initialize\n2. before_execution\n3. before_validation\n4. after_validation\n5. on_executing\n6. on_[success, noop, invalid, failure, error]\n7. on_[complete, interrupted]\n8. after_execution\n```\n\n### Lifecycle Hooks\n\nDefine before and after callbacks to call around execution.\n\n```ruby\nclass DecryptSecretMessage \u003c Lite::Command::Base\n\n  after_initialize  :some_method\n  before_validation :some_method\n  after_validation  :some_method\n  before_execution  :some_method\n  after_execution   :some_method\n\n  def call\n    # ...\n  end\n\nend\n```\n\n### Status Hooks\n\nDefine one or more callbacks that are called after execution for\nspecific statuses.\n\n```ruby\nclass DecryptSecretMessage \u003c Lite::Command::Base\n\n  on_success :some_method\n  on_noop    :some_method\n  on_invalid :some_method\n  on_failure :some_method\n  on_error   :some_method\n\n  def call\n    # ...\n  end\n\nend\n```\n\n### State Hooks\n\nDefine one or more callbacks that are called during transitions between states.\n\n```ruby\nclass DecryptSecretMessage \u003c Lite::Command::Base\n\n  on_pending     :some_method\n  on_executing   :some_method\n  on_complete    :some_method\n  on_interrupted :some_method\n\n  def call\n    # ...\n  end\n\nend\n```\n\n## Children\n\nWhen building complex commands, its best that you pass the parents context to the\nchild command (unless neccessary) so that it gains automated indexing and the\nparents `cmd_id`.\n\n```ruby\nclass DecryptSecretMessage \u003c Lite::Command::Base\n\n  def call\n    context.merge!(decryption_key: ENV[\"DECRYPT_KEY\"])\n    ValidateSecretMessage.call(context)\n  end\n\nend\n```\n\n### Throwing Faults\n\nThrowing faults allows you to bubble up child faults up to the parent. Use it to create\nbranches within your logic and create clean tracing of your command results. You can use\n`throw!` as a catch-all or any of the bang status method `failure!`. Any `reason` and\n`metadata` will be bubbled up from the original fault.\n\n```ruby\nclass DecryptSecretMessage \u003c Lite::Command::Base\n\n  def call\n    context.merge!(decryption_key: ENV[\"DECRYPT_KEY\"])\n    cmd = ValidateSecretMessage.call(context)\n\n    if cmd.invalid?(\"Invalid magic numbers\")\n      failure!(cmd) # Manually throw a specific fault\n    elsif command.fault?\n      throw!(cmd) # Automatically throws a matching fault\n    else\n      context.decrypted_message = SecretMessage.decrypt(ctx.encrypted_message)\n    end\n  end\n\nend\n```\n\n## Sequences\n\nA sequence is a command that calls commands in a linear fashion.\nThis is useful for composing multiple steps into one call.\n\n\u003e [!NOTE]\n\u003e Sequences only stop processing on `invalid`, `failure`, and `error`\n\u003e faults. This is due to the the idea the `noop` performs no work,\n\u003e so its no different than just passing the context forward. To change\n\u003e this behavior, just override the `ok?` method with you logic, eg: just `success`\n\n\u003e [!WARNING]\n\u003e Do **NOT** define a call method in this class. The sequence logic is\n\u003e automatically defined by the sequence class.\n\n```ruby\nclass ProcessCheckout \u003c Lite::Command::Sequence\n\n  required :user\n\n  step FinalizeInvoice\n  step ChargeCard, if: :card_available?\n  step SendConfirmationEmail, SendConfirmationText\n  step NotifyWarehouse, unless: proc { ctx.invoice.fullfilled_by_amazon? }\n\n  # Do NOT define a call method.\n\n  private\n\n  def card_available?\n    user.has_card?\n  end\n\nend\n\nseq = ProcessCheckout.call(...)\n# \u003cProcessCheckout ...\u003e\n```\n\n## Results\n\nDuring any point in the lifecyle of a command, `to_hash` can be called to dump out\nthe current values. The `index` value is auto-incremented and the `cmd_id` is static\nwhen its passed to child commands. This helps with debugging and logging.\n\n```ruby\ncommand = DecryptSecretMessage.call(...)\ncommand.to_hash #=\u003e {\n                #=\u003e   index: 1,\n                #=\u003e   cmd_id: \"018c2b95-b764-7615-a924-cc5b910ed1e5\",\n                #=\u003e   command: \"FailureCommand\",\n                #=\u003e   outcome: \"failure\",\n                #=\u003e   state: \"interrupted\",\n                #=\u003e   status: \"failure\",\n                #=\u003e   reason: \"Command stopped due to some failure\",\n                #=\u003e   metadata: {\n                #=\u003e     errors: { name: [\"is too short\"] },\n                #=\u003e     i18n_key: \"command.failure\"\n                #=\u003e   },\n                #=\u003e   caused_by: 3,\n                #=\u003e   caused_exception: \"[ChildCommand::Failure] something is wrong from within\",\n                #=\u003e   thrown_by: 2,\n                #=\u003e   thrown_exception: \"[FailureCommand::Failure] something is wrong from within\",\n                #=\u003e   runtime: 0.0123\n                #=\u003e }\n```\n\n## Examples\n\n### Disable Instance Calls\n\n```ruby\nclass DecryptSecretMessage \u003c Lite::Command::Base\n\n  private_class_method :new\n\n  def call\n    # ...\n  end\n\nend\n\nDecryptSecretMessage.new(...).call\n#=\u003e raise NoMethodError\n```\n\n## Generator\n\n`rails g command NAME` will generate the following file:\n\n```erb\napp/commands/[NAME]_command.rb\n```\n\nIf a `ApplicationCommand` file in the `app/commands` directory is available, the\ngenerator will create file that inherit from `ApplicationCommand` if not it will\nfallback to `Lite::Command::Base`.\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/[USERNAME]/lite-command. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [Contributor Covenant](http://contributor-covenant.org) code of conduct.\n\n## License\n\nThe gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).\n\n## Code of Conduct\n\nEveryone interacting in the Lite::Command project’s codebases, issue trackers, chat rooms and mailing lists is expected to follow the [code of conduct](https://github.com/[USERNAME]/lite-command/blob/master/CODE_OF_CONDUCT.md).\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fdrexed%2Flite-command","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fdrexed%2Flite-command","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fdrexed%2Flite-command/lists"}