Repository: hackico-ai/ruby-hati-command Branch: main Commit: 5fcdbb040eca Files: 37 Total size: 66.7 KB Directory structure: gitextract_ucqxjbxu/ ├── .gitignore ├── .rubocop.yml ├── CODE_OF_CONDUCT.md ├── Gemfile ├── LICENSE ├── README.md ├── hati-command.gemspec ├── lib/ │ ├── hati/ │ │ └── command.rb │ ├── hati_command/ │ │ ├── befehl.rb │ │ ├── callee.rb │ │ ├── cmd.rb │ │ ├── errors/ │ │ │ ├── base_error.rb │ │ │ ├── configuration_error.rb │ │ │ ├── fail_fast_error.rb │ │ │ └── transaction_error.rb │ │ ├── failure.rb │ │ ├── result.rb │ │ ├── success.rb │ │ └── version.rb │ └── hati_command.rb └── spec/ ├── integration/ │ └── hati_command/ │ ├── befehl_ar_transaction_spec.rb │ ├── befehl_spec.rb │ ├── callee_spec.rb │ └── cmd_spec.rb ├── spec_helper.rb ├── support/ │ ├── active_record.rb │ └── dummy.rb └── unit/ └── hati_command/ ├── befehl_config_spec.rb ├── callee_spec.rb ├── cmd_spec.rb ├── errors/ │ ├── base_error_spec.rb │ ├── configuration_error_spec.rb │ ├── fail_fast_error_spec.rb │ └── transaction_error_spec.rb ├── failure_spec.rb ├── result_spec.rb └── success_spec.rb ================================================ FILE CONTENTS ================================================ ================================================ FILE: .gitignore ================================================ *.gem *.rbc /.config /coverage/ /InstalledFiles /pkg/ /spec/reports/ /spec/examples.txt /test/tmp/ /test/version_tmp/ /tmp/ # Used by dotenv library to load environment variables. # .env # Ignore Byebug command history file. .byebug_history ## Specific to RubyMotion: .dat* .repl_history build/ *.bridgesupport build-iPhoneOS/ build-iPhoneSimulator/ ## Specific to RubyMotion (use of CocoaPods): # # We recommend against adding the Pods directory to your .gitignore. However # you should judge for yourself, the pros and cons are mentioned at: # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control # # vendor/Pods/ ## Documentation cache and generated files: /.yardoc/ /_yardoc/ /doc/ /rdoc/ ## Environment normalization: /.bundle/ /vendor/bundle /lib/bundler/man/ # for a library or gem, you might want to ignore these files since the code is # intended to run in multiple environments; otherwise, check them in: # Gemfile.lock # .ruby-version # .ruby-gemset # unless supporting rvm < 1.11.0 or doing something fancy, ignore this: .rvmrc # Used by RuboCop. Remote config files pulled in from inherit_from directive. # .rubocop-https?--* # Specs .rspec_status # Dev env Gemfile.lock # macOS .DS_Store ================================================ FILE: .rubocop.yml ================================================ require: - rubocop-rspec - rubocop-rake AllCops: NewCops: enable TargetRubyVersion: 3.0 Style/StringLiteralsInInterpolation: EnforcedStyle: double_quotes ================================================ FILE: CODE_OF_CONDUCT.md ================================================ # [hackico-ai] Code of Conduct ## Our Pledge We, as members, contributors, and leaders of [Your Community Name], pledge to make participation in our community a harassment-free experience for everyone, regardless of age, body size, visible or invisible disability, ethnicity, sex characteristics, gender identity and expression, level of experience, education, socio-economic status, nationality, personal appearance, race, caste, color, religion, or sexual identity and orientation. We pledge to act and interact in ways that contribute to an open, welcoming, diverse, inclusive, and healthy community. ## Our Standards Examples of behavior that contributes to a positive environment for our community include: - Demonstrating empathy and kindness toward others. - Being respectful of differing opinions, viewpoints, and experiences. - Giving and gracefully accepting constructive feedback. - Accepting responsibility and apologizing to those affected by our mistakes, and learning from the experience. - Focusing on what is best not just for us as individuals, but for the overall community. Examples of unacceptable behavior include: - The use of sexualized language or imagery, and sexual attention or advances of any kind. - Trolling, insulting or derogatory comments, and personal or political attacks. - Public or private harassment. - Publishing others' private information, such as a physical or email address, without their explicit permission. - Other conduct which could reasonably be considered inappropriate in a professional setting. ## Enforcement Responsibilities Community leaders are responsible for clarifying and enforcing our standards of acceptable behavior and will take appropriate and fair corrective action in response to any behavior that they deem inappropriate, threatening, offensive, or harmful. Community leaders have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned with this Code of Conduct, and will communicate reasons for moderation decisions when appropriate. ## Scope This Code of Conduct applies within all community spaces and also applies when an individual is officially representing the community in public spaces. Examples of representing our community include using an official email address, posting via an official social media account, or acting as an appointed representative at an online or offline event. ## Enforcement Instances of abusive, harassing, or otherwise unacceptable behavior may be reported to the community leaders responsible for enforcement at [Yuri Gi](https://github.com/yurigitsu), [Marie Giy](https://github.com/yurigitsu). All complaints will be reviewed and investigated promptly and fairly. All community leaders are obligated to respect the privacy and security of the reporter of any incident. ## Enforcement Guidelines Community leaders will follow these Community Impact Guidelines in determining the consequences for any action they deem in violation of this Code of Conduct: ### 1. Correction **Community Impact**: Use of inappropriate language or other behavior deemed unprofessional or unwelcome in the community. **Consequence**: A private, written warning from community leaders, providing clarity around the nature of the violation and an explanation of why the behavior was inappropriate. A public apology may be requested. ### 2. Warning **Community Impact**: A violation through a single incident or series of actions. **Consequence**: A warning with consequences for continued behavior. No interaction with the people involved, including unsolicited interaction with those enforcing the Code of Conduct, for a specified period of time. This includes avoiding interactions in community spaces as well as external channels like social media. Violating these terms may lead to a temporary or permanent ban. ### 3. Temporary Ban **Community Impact**: A serious violation of community standards, including sustained inappropriate behavior. **Consequence**: A temporary ban from any sort of interaction or public communication with the community for a specified period of time. No public or private interaction with the people involved, including unsolicited interaction with those enforcing the Code of Conduct, is allowed during this period. Violating these terms may lead to a permanent ban. ### 4. Permanent Ban **Community Impact**: Demonstrating a pattern of violation of community standards, including sustained inappropriate behavior, harassment of an individual, or aggression toward or disparagement of classes of individuals. **Consequence**: A permanent ban from any sort of public interaction within the community. ## Attribution This Code of Conduct is adapted from the version 2.1, available at [https://www.contributor-covenant.org/version/2/1/code_of_conduct.html][v2.1]. For answers to common questions about this code of conduct, see the FAQ at [https://www.contributor-covenant.org/faq][FAQ]. Translations are available at [https://www.contributor-covenant.org/translations][translations]. [homepage]: https://www.contributor-covenant.org [v2.1]: https://www.contributor-covenant.org/version/2/1/code_of_conduct.html [FAQ]: https://www.contributor-covenant.org/faq [translations]: https://www.contributor-covenant.org/translations ================================================ FILE: Gemfile ================================================ # frozen_string_literal: true source 'https://rubygems.org' gemspec gem 'rake' # Spec gem 'activerecord' gem 'rspec', '~> 3.0' gem 'sqlite3' # Linter & Static gem 'fasterer', '~> 0.11.0' gem 'rubocop', '~> 1.21' gem 'rubocop-rake' gem 'rubocop-rspec', require: false ================================================ FILE: LICENSE ================================================ MIT License Copyright (c) 2025 hackico.ai Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: README.md ================================================ # ![Ruby](icon.svg) hati-command The `hati-command` gem provides a lightweight framework for structuring logic as discrete, callable actions — ideal for agentic AI systems that require modular execution and explicit outcome handling. ## What it provides: - **hati-command** lets you define commands as service objects or interactors, ready for orchestration by AI agents. - **hati-command** returns standardized `Success` and `Failure` results, making it easy to reason about next steps in autonomous workflows. - **hati-command** provides built-in error tracing and metadata propagation, enabling reliable debugging, observability, and auditability across execution chains. - **hati-command** supports integrated transaction handling, allowing commands to execute safely within database or domain-level transactional boundaries. ## Features - **Command Execution**: Encapsulate atomic operations with clear inputs and outputs. - **Structured Results**: Return `Result` objects with status, value, and metadata. - **Deterministic Execution**: Enforce explicit execution → outcome flow. - **Failure as Data**: Represent errors as explicit results. - **Framework-Agnostic Service Objects**: Works with plain `Ruby` or `Rails`. - **Execution Transparency**: Make decision points and failure paths visible. - **Automation & AI-Ready**: Suitable for orchestration and agent-driven w ## Table of Contents - [Introduction](#introduction) - [Features](#features) - [Installation](#installation) - [Basic Usage](#basic-usage) - [Handling Success](#handling-success) - [Handling Failure](#handling-failure) - [Transactional Behavior](#transactional-behavior-fail-fast-with-failure) - [Advanced Usage](#advanced-usage) - [Result Customization](#result-customization) - [meta](#meta) - [error](#error) - [trace](#trace) - [Native DB Transaction](#native-db-active-record-transaction) - [Command Configurations](#command-configurations) - [result_inference](#result_inference) - [call_as](#call_as) - [failure](#failure) - [fail_fast](#fail_fast) - [unexpected_err](#unexpected_err) - [ar_transaction](#ar_transaction) - [Development](#development) - [Contributing](#contributing) - [License](#license) - [Code of Conduct](#code-of-conduct) ## Installation Install the gem and add to the application's Gemfile by executing: ```bash bundle add hati-command ``` If bundler is not being used to manage dependencies, install the gem by executing: ```bash gem install hati-command ``` ## Basic Usage To use the `hati-command` gem, you can create a command class that includes the `HatiCommand::Cmd` module. Note: No need to nest object APIs under `private` as popular template for `Servie Object` designs only main caller method is public by design #### Example ```ruby require 'hati_command' class GreetingCommand include HatiCommand::Cmd def call(greeting = nil) message = build_greeting(greeting) return message if message.failure? process_message(message) end def build_greeting(greeting) greeting ? Success(greeting) : Failure("No greeting provided") end def process_message(message) message.success? ? Success(message.upcase) : Failure("No message provided") end end ``` ### Command `API` ```ruby result = GreetingCommand.call("Hello, World!") # Outputs: Result result = GreetingCommand.new # Outputs: private method `new' called ``` ### Handling `Success` ```ruby result = GreetingCommand.call("Hello, World!") puts result.success? # Outputs: true puts result.failure? # Outputs: false puts result.success # Outputs: "HELLO, WORLD!" puts result.failure # Outputs: nil puts result.value # Outputs: "HELLO, WORLD!" puts result.result # Outputs: HatiCommand::Success ``` ### Handling `Failure` ```ruby result = GreetingCommand.call puts result.failure? # Outputs: true puts result.success? # Outputs: false puts result.failure # Outputs: "No message provided" puts result.success # Outputs: nil puts result.value # Outputs: "No message provided" puts result.result # Outputs: HatiCommand::Failure ``` ### Transactional Behavior: Fail Fast with `Failure!` ```ruby class GreetingCommand include HatiCommand::Cmd # NOTE: Will catch unexpected and wrap to HatiCommand::Failure object # Requires true || ErrorObject command do unexpected_err true end def call(params) message = process_message(params[:message]) msg = normalize_message(message, params[:recipients]) Success(msg) end # NOTE: No message passed - auto break an execution def process_message(message) message ? message.upcase : Failure!("No message provided") end def normalize_message(message, recipients) Failure!("No recipients provided") if recipients.empty? recipients.map { |recipient| "#{recipient}: #{message}" } end end ``` ```ruby # NOTE: No message passed - command exited # Returns Result (Failure) object result = GreetingCommand.call puts result.failure? # Outputs: true puts result.failure # Outputs: "No message provided" puts result.value # Outputs: "No message provided" ``` ```ruby result = GreetingCommand.call(params.merge(message: "Hello!")) puts result.failure? # Outputs: true puts result.failure # Outputs: "No recipients provided" puts result.value # Outputs: "No recipients provided" ``` ```ruby result = GreetingCommand.call(params.merge(recipients: ["Alice", "Bob"])) puts result.failure? # Outputs: false puts result.success # Outputs: true puts result.value # Outputs: ["Alice: Hello!", "Bob: Hello!"] ``` ## Advanced Usage Configurations and customization allow users to tailor the command to meet their specific needs and preferences ### `Result` Customization Here are some advanced examples of result customization. Available options are - `meta` - Hash to attach custom metadata - `err` - Message or Error access via `error` method - `trace` - By design `Failure!` and `unexpected_err` error's stack top entry ### .meta ```ruby class GreetingCommand include HatiCommand::Cmd # ... def process_message(message) Success(message.upcase, meta: { lang: :eng, length: message.length }) end # ... end ``` ```ruby result = GreetingCommand.("Hello, Advanced World!") puts result.value # Outputs: "HELLO, ADVANCED WORLD!" puts result.meta[:lang] # Outputs: :eng puts result.meta[:length] # Outputs: 22 puts result.meta # Outputs: {:lang=>:eng, :length=>22} ``` ### .error ##### set via `err` access via `error` method. Availiable as param for `#Success` as well (ex. partial success) ```ruby class GreetingCommand include HatiCommand::Cmd # ... def process_message(message) Failure(message, err: "No message provided") end end ``` ```ruby result = GreetingCommand.call puts result.value # Outputs: nil puts result.error # Outputs: "No message provided" puts result.trace # Outputs: ``` ### .trace ##### Available as accessor on `Result` object ```ruby 1| class DoomedCommand 2| include HatiCommand::Cmd 3| 4| def call 5| Failure! 6| end 7| # ... 8| end ``` ```ruby result = GreetingCommand.call puts result.failure? # Outputs: true puts result.trace # Outputs: path/to/cmds/doomed_command.rb:5:in `call' ``` ### Command `Configurations` Provides options for default failure message or errors. Available configs are: - `result_inference`(Bool(true)) => implicit Result wrapper - `call_as`(Symbol[:call]) => Main call method name - `failure`(String | ErrorClass) => Message or Error - `fail_fast`(String || ErrorClass) => Message or Error - `unexpected_err`(Bool[true]) => Message or Error #### Native DB Active Record Transaction: - `ar_transaction`(Array[Symbol], returnable: Bool[true]) => methods to wrap in Transaction, requires 'activerecord' ```ruby class AppService include HatiCommand::Cmd command do result_inference true call_as :perform failure "Default Error" fail_fast "Default Fail Fast Error" unexpected_err BaseServiceError end # ... end class PaymentService < AppService command do ar_transaction :perform unexpected_err PaymentServiceTechnicalError end def perform(params) account = Account.lock.find(user_id) Failure("User account is inactive") unless user.active? CreditTransaction.create!(user_id: user.id, amount: amount) AuditLog.create!(action: 'add_funds', account: account) Success('Funds has been add to account') end # ... end ``` ### result_inference ```ruby class GreetingCommand include HatiCommand::Cmd command do result_inference true # Implicitly wraps non-Result as Success end def call 42 end # ... end ``` ```ruby result = GreetingCommand.call puts result.success # Outputs: 42 puts result.failure? # Outputs: false ``` ### call_as ```ruby class GreetingCommand include HatiCommand::Cmd command do call_as :execute # E.q. :perform, :run, etc. end def execute Success(42) end # ... end ``` ```ruby result = GreetingCommand.execute puts result.success # Outputs: 42 puts result.failure? # Outputs: false ``` ### failure ```ruby 1 | class DoomedCommand 2 | include HatiCommand::Cmd 3 | 4 | command do 5 | failure "Default Error" 6 | end 7 | 8 | def call(error = nil, fail_fast: false) 9 | Failure! if fail_fast 10| 11| return Failure("Foo") unless option 12| 13| Failure(error, err: "Insufficient funds") 14| end 15| # ... 16| end ``` NOTE: not configured fail fast uses default error ```ruby result = DoomedCommand.call(fail_fast: true) puts result.failure # Outputs: nil puts result.error # Outputs: "Default Error" puts result.trace # Outputs: path/to/cmds/doomed_command.rb:5:in `call' result = DoomedCommand.call puts result.failure # Outputs: "Foo" puts result.error # Outputs: "Default Error" result = DoomedCommand.call('Buzz') puts result.failure # Outputs: "Buzz" puts result.error # Outputs: "Insufficient funds" ``` ### fail_fast ```ruby 1 | class DoomedCommand 2 | include HatiCommand::Cmd 3 | 4 | command do 5 | fail_fast "Default Fail Fast Error" 6 | end 7 | 8 | def call 9 | Failure! 10| end 11| # ... 12| end ``` ```ruby result = DoomedCommand.call puts result.failure # Outputs: nil puts result.error # Outputs: "Default Fail Fast Error" puts result.trace # Outputs: path/to/cmds/doomed_command.rb:9:in `call' ``` ### unexpected_err ```ruby 1 | class GreetingCommand 2 | include HatiCommand::Cmd 3 | 4 | command do 5 | unexpected_err true 5 | end 6 | 7 | def call 8 | 1 + "2" 9 | end 10| # ... 11| end ``` ```ruby result = GreetingCommand.call puts result.failure # Outputs: nil puts result.error # Outputs: TypeError: no implicit conversion of Integer into String puts result.trace # Outputs: path/to/cmds/greeting_command.rb:9:in `call' ``` ### unexpected_err (wrapped) ```ruby 1 | class GreetingCommand 2 | include HatiCommand::Cmd 3 | 4 | class GreetingError < StandardError; end 5 | 6 | command do 7 | unexpected_err GreetingError 8 | end 9 | 10| def call 11| 1 + "2" 12| end 13| # ... 14| end ``` NOTE: Original error becomes value (failure) ```ruby result = GreetingCommand.call puts result.failure # Outputs: TypeError: no implicit conversion of Integer into String puts result.error # Outputs: GreetingError puts result.trace # Outputs: path/to/cmds/greeting_command.rb:12:in `call' ``` ### ar_transaction Wraps listed methods in Transaction with blocking non-Result returns. At this dev stage relies on 'activerecord' - NOTE: considering extensicve expirience of usage, we recomend to use some naming convention across codebase for such methods, to keep healthy Elegance-to-Explicitness ratio #### E.g. suffixes: \_flow, \_transaction, \_task, etc. - NOTE: `Failure()` works as transaction break, returns only from called method's as Result (Failure) object - NOTE: `Failure!()` works on Service level same fail_fast immediately halts execution, return from - NOTE: Unlike `ActiveRecord::Transaction` Implicit non-Result returns will trigger `TransactionError`, blocking partial commit state unless: ```ruby ar_transaction :transactional_method_name, returnable: false # Defaults to true ``` ### Pseudo-Example: ```ruby class PaymentService < AppService command do ar_transaction :add_funds_transaction unexpected_err PaymentServiceTechnicalError end def call(params) amount = currency_exchange(params[:amount]) debit_transaction = add_funds_transaction(amount) return debit_transaction if debit_transaction.success? Failure(debit_transaction, err: 'Unable to add funds') end def currency_exchange # ... end # Whole method evaluates in ActiveRecord::Transaction block def add_funds_transaction(amount) account = Account.lock.find(user_id) Failure("User account is inactive") unless user.active? # Fires TransactionError, unless :returnable configuration is disabled return 'I am an Error' user.balance += amount user.save Failure('Account debit issue') if user.errors CreditTransaction.create!(user_id: user.id, amount: amount) AuditLog.create!(action: 'add_funds', account: account) # NOTE: result inference won't work, use only Result objects Success('Great Succeess') end # ... end ``` ## Authors - [Marie Giy](https://github.com/mariegiy) ## Contributors - [yurigitsu](https://github.com/yurigitsu) ## Development After 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. To 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 the created tag, and push the `.gem` file to [rubygems.org](https://rubygems.org). ## Contributing Bug reports and pull requests are welcome on GitHub at https://github.com/hackico-ai/hati-command. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [code of conduct](https://github.com/hackico-ai/hati-command/blob/main/CODE_OF_CONDUCT.md). ## License The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT). ## Code of Conduct Everyone interacting in the HatCommand project's codebases, issue trackers, chat rooms and mailing lists is expected to follow the [code of conduct](https://github.com/hackico-ai/hati-command/blob/main/CODE_OF_CONDUCT.md). ================================================ FILE: hati-command.gemspec ================================================ # frozen_string_literal: true lib = File.expand_path('lib', __dir__) $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) require 'hati_command/version' Gem::Specification.new do |spec| spec.name = 'hati-command' spec.version = HatiCommand::VERSION spec.authors = ['Mariya Giy', 'Yuri Gi'] spec.email = %w[yurigi.pro@gmail.com giy.mariya@gmail.com] spec.license = 'MIT' spec.summary = 'A Ruby gem for creating modular, agent-ready service objects and command-pattern interactors, following Railway-oriented design principles with structured, explicit result handling.' spec.description = 'hati-command offers a clear, minimal abstraction for implementing composable service objects and command-pattern interactors. By enforcing explicit Success and Failure result pathways, it aligns well with autonomous system pipelines, decision-chain architectures, and AI-driven orchestration flows.' spec.homepage = "https://github.com/hackico-ai/#{spec.name}" spec.required_ruby_version = '>= 3.0.0' spec.files = Dir['CHANGELOG.md', 'LICENSE', 'README.md', 'hati-command.gemspec', 'lib/**/*'] spec.bindir = 'bin' spec.executables = [] spec.require_paths = ['lib'] spec.metadata['repo_homepage'] = spec.homepage spec.metadata['allowed_push_host'] = 'https://rubygems.org' spec.metadata['homepage_uri'] = spec.homepage spec.metadata['changelog_uri'] = "#{spec.homepage}/blob/main/CHANGELOG.md" spec.metadata['source_code_uri'] = spec.homepage spec.metadata['bug_tracker_uri'] = "#{spec.homepage}/issues" spec.metadata['rubygems_mfa_required'] = 'true' end ================================================ FILE: lib/hati/command.rb ================================================ # frozen_string_literal: true # Compatibility shim for Bundler auto-require require_relative '../hati_command' ================================================ FILE: lib/hati_command/befehl.rb ================================================ # frozen_string_literal: true # @module HatiCommand # Provides command handling functionalities for creating and managing commands. # This module serves as the main namespace for all command-related operations. module HatiCommand # @module Befehl # Core module for command handling that provides the base functionality for creating commands. # This module is designed to be extended by classes that need command handling capabilities. module Befehl # Extends the base class with command handling functionality # @param base [Class] the class that is extending this module # @return [void] def self.extended(base) base.extend(BefehlClassMethods) def base.inherited(subclass) super subclass.instance_variable_set(:@__command_config, @__command_config.dup) end end # @module BefehlClassMethods # Provides class methods for command configuration and execution. # These methods are automatically added to any class that extends Befehl. module BefehlClassMethods # @module namespace as alias ERR = HatiCommand::Errors # Configures a command block with specific settings # @yield [void] The configuration block # @return [Hash] The command configuration # @example # command do # failure :my_failure_handler # fail_fast true # end def command(&block) instance_eval(&block) if block_given? end # @return [Hash] The current command configuration settings def command_config __command_config end # Sets the result inference behavior for the command. # @param value [Boolean] Indicates whether to enable result inference. # @return [void] def result_inference(value) command_config[:result_inference] = value end # Sets the failure handler for the command # @param value [Symbol, Proc] The failure handler to be used # @return [void] def failure(value) command_config[:failure] = value end # Sets the fail-fast behavior for the command # @param value [Boolean] Whether to enable fail-fast behavior # @return [void] def fail_fast(value) command_config[:fail_fast] = value end # Sets the unexpected error handler for the command # @param value [Symbol, Proc, Boolean] The error handler to be used # @return [void] def unexpected_err(value) command_config[:unexpected_err] = value end # This method checks if a caller method has been set; if not, it defaults to `:call`. # @return [Symbol] The name of the method to call. def call_as(value = :call) command_config[:call_as] = value singleton_class.send(:alias_method, value, :call) end # WIP: experimental # TODO: set of methods def ar_transaction(*cmd_methods, returnable: true) has_ar_defined = defined?(ActiveRecord::Base) && ActiveRecord::Base.respond_to?(:transaction) raise ERR::ConfigurationError, 'No ActiveRecord defined' unless has_ar_defined has_valid_mthds = cmd_methods.any? { |value| value.is_a?(Symbol) } raise ERR::ConfigurationError, 'Invalid types. Accepts Array[Symbol]' unless has_valid_mthds command_config[:ar_transaction] = { methods: cmd_methods, returnable: returnable } dynamic_module = Module.new do cmd_methods.each do |method_name| define_method(method_name) do |*args, **kwargs, &block| rez = ActiveRecord::Base.transaction do result = super(*args, **kwargs, &block) # Rollbacks to prevent partial transaction state if returnable && !result.is_a?(HatiCommand::Result) raise ERR::ConfigurationError, 'This command configuration requires explicit Result-return from transaction' end # Allows explicit partial commit if result.failure? raise ERR::TransactionError.new('Transaction brake has been triggered', failure_obj: result.value) end result end rez rescue ERR::TransactionError => e # TODO: process trace corectly (line of code) HatiCommand::Failure.new(e.failure_obj, err: e.message, trace: e.backtrace&.first) # Every other error including FailFast goes to main caller method rescue ActiveRecord::ActiveRecordError => e # TODO: process trace HatiCommand::Failure.new(e, err: e.message, trace: e.backtrace&.first) end end end prepend dynamic_module end # Executes the command with the given arguments. # # This method creates a new instance of the command class, yields it to an optional block, # and then calls the instance method with the provided arguments. It handles the result # of the command execution, returning a success or failure result based on the outcome. # # @param args [Array] Arguments to be passed to the instance method. # @yield [Object] Optional block that yields the new instance for additional configuration. # @return [HatiCommand::Result, Object] The result of the command, wrapped in a Result object if applicable. # @raise [HatiCommand::Errors::FailFastError] If a fail-fast condition is triggered. # @raise [StandardError] If an unexpected error occurs and no handler is configured. def call(*args, __command_reciever: nil, **kwargs, &block) result = caller_result(*args, __command_reciever: __command_reciever, **kwargs, &block) return result unless command_config[:result_inference] return result if result.is_a?(HatiCommand::Result) HatiCommand::Success.new(result) rescue ERR::FailFastError => e handle_fail_fast_error(e) rescue StandardError => e handle_standard_error(e) end # TODO: think on opts to hide reciever def caller_result(*args, __command_reciever: nil, **kwargs, &block) # expecting pre-configured reciever if given if __command_reciever obj = __command_reciever else obj = new yield(obj) if !obj && block_given? end # TODO: add error if no instance method to call obj.send(command_config[:call_as] || :call, *args, **kwargs, &block) end module_function # @return [Hash] The current command configuration settings # @api private def __command_config @__command_config ||= {} end # Handles fail-fast errors during command execution # @param error [HatiCommand::Errors::FailFastError] The fail-fast error to handle # @return [HatiCommand::Failure] A failure object containing error details # @api private def handle_fail_fast_error(error) failure_obj = error.failure_obj return HatiCommand::Failure.new(error, trace: error.backtrace.first) unless failure_obj failure_obj.tap { |err| err.trace = error.backtrace[1] } end # Handles standard errors during command execution # @param error [StandardError] The error to handle # @return [HatiCommand::Failure] A failure object containing error details # @raise [StandardError] If no unexpected error handler is configured # @api private def handle_standard_error(error) internal_err = command_config[:unexpected_err] raise error unless internal_err err = internal_err.is_a?(TrueClass) ? error : internal_err HatiCommand::Failure.new(error, err: err, trace: error.backtrace.first) end def execute_with_transaction_handling? has_ar_defined = defined?(ActiveRecord::Base) && ActiveRecord::Base.respond_to?(:transaction) !!(command_config.dig(:ar_transaction, :methods) && has_ar_defined) end end end end ================================================ FILE: lib/hati_command/callee.rb ================================================ # frozen_string_literal: true # @module HatiCommand # Provides command handling functionalities and callable patterns. module HatiCommand # @module Callee # Module for adding callable functionality to a class. # This module implements the callable pattern, allowing classes to be called like functions # while maintaining object-oriented principles. # # @example # class MyCallable # include HatiCommand::Callee # # def call(input) # # Process input # input.upcase # end # end # # # Can be used as: # result = MyCallable.call("hello") # => "HELLO" # # # Or with a block: # MyCallable.call("hello") do |instance| # instance.configure(some: :option) # end module Callee # Extends the including class with callable functionality # @param base [Class] The class including this module # @return [void] # @api private def self.included(base) base.extend(CalleeClassMethods) end # Returns the identity of the module # @note This is a work in progress method # @return [String] The module's identity string # @api public def self.whoami 'My Name is Callee' end # @module CalleeClassMethods # Class methods that are extended to classes including Callee. # Provides the callable interface at the class level. module CalleeClassMethods # This method checks if a caller method has been set; if not, it defaults to `:call`. # # @return [Symbol] The name of the method to call. def __caller_method @__caller_method || :call end # Creates a new instance and calls its `call` method with the given arguments. # This method implements the callable pattern, allowing the class to be used # like a function while maintaining object-oriented principles. # # @param args [Array] Arguments to be passed to the instance's call method # @yield [Object] Optional block that yields the new instance before calling # @yieldparam instance [Object] The newly created instance # @return [Object] The result of the instance method call # # @example Without block # MyCallable.call(arg1, arg2) # @example With configuration block # MyCallable.call(input) do |instance| # instance.configure(option: value) # end def call(...) obj = new yield(obj) if block_given? obj.send(__caller_method, ...) end # This method allows you to configure command call method name such as: :execute, :perform, etc. # Note: method call_as and command main instance method should much # @param method_name [Symbol] The name of the alias to create for the `call` method. # @return [void] # # @example # class MyCallable # include HatiCommand::Callee # # call_as :execute # :run, :perform, etc. # # def execute(input) # :run, :perform, etc. # input.upcase # end # # end # MyCallable.execute("hello") # => "HELLO" # MyCallable.perform("hello") # => "HELLO" # MyCallable.run("hello") # => "HELLO" def call_as(method_name) @__caller_method = method_name singleton_class.send(:alias_method, method_name, :call) end end end end ================================================ FILE: lib/hati_command/cmd.rb ================================================ # frozen_string_literal: true # @module HatiCommand # Provides command handling functionalities with a focus on Railway-oriented programming. # This module implements the Railway pattern for better error handling and command flow control. module HatiCommand # @module Cmd # Dev-friendly extension of the Befehl core module with Railway track API. # This module provides a Railway-oriented programming interface for handling success and failure states # in a functional way, making it easier to chain operations and handle errors gracefully. # # @example # class MyCommand # include HatiCommand::Cmd # # def call(input) # if valid?(input) # Success(input) # else # Failure("Invalid input") # end # end # end module Cmd # Includes the module in the base class and sets up necessary configurations # @param base [Class] The class including this module # @return [void] def self.included(base) base.extend(HatiCommand::Befehl) base.private_class_method :new end # Returns the identity of the module # @note This is a work in progress method # @return [String] The module's identity string # @api public def self.whoami 'My Name is Cmd' end # Creates a Success monad representing a successful operation # @param value [Object, nil] The value to wrap in the Success monad # @param err [Object, nil] Optional error object # @param meta [Hash] Additional metadata for the success state # @return [HatiCommand::Success] A Success monad containing the result # @example # Success("Operation completed", meta: { time: Time.now }) def Success(value = nil, err: nil, meta: {}) # rubocop:disable Naming/MethodName HatiCommand::Success.new(value, err: err, meta: meta) end # Creates a Failure monad representing a failed operation # @param value [Object, nil] The value to wrap in the Failure monad # @param err [Object, nil] Optional error object (falls back to configured default) # @param meta [Hash] Additional metadata for the failure state # @return [HatiCommand::Failure] A Failure monad containing the error details # @example # Failure("Operation failed", err: StandardError.new, meta: { reason: "invalid_input" }) def Failure(value = nil, err: nil, meta: {}) # rubocop:disable Naming/MethodName default_err = self.class.command_config[:failure] HatiCommand::Failure.new(value, err: err || default_err, meta: meta) end # Creates a Failure monad and immediately raises a FailFastError # @param value [Object, nil] The value to wrap in the Failure monad # @param err [Object, nil] Optional error object (falls back to configured defaults) # @param meta [Hash] Additional metadata for the failure state # @param _opts [Hash] Additional options (currently unused) # @raise [HatiCommand::Errors::FailFastError] Always raises with the created Failure monad # @example # Failure!("Critical error", err: FatalError.new) def Failure!(value = nil, err: nil, meta: {}, **_opts) # rubocop:disable Naming/MethodName default_error = self.class.command_config[:fail_fast] || self.class.command_config[:failure] error = err || default_error failure_obj = HatiCommand::Failure.new(value, err: error, meta: meta) raise HatiCommand::Errors::FailFastError.new('Fail Fast Triggered', failure_obj: failure_obj) end end end ================================================ FILE: lib/hati_command/errors/base_error.rb ================================================ # frozen_string_literal: true module HatiCommand module Errors # Custom BaseError class for command issues scenarios in HatiCommand # # @example Raising a BaseError with a message # raise HatiCommand::Error::BaseError, "Operation failed" class BaseError < StandardError DEFAULT_MSG = 'Default message: Oooops! Something went wrong. Please check the logs.' attr_reader :failure_obj # @param message [String] The error message # @param failure_obj [Object] An optional Error || Failure DTO def initialize(message = nil, failure_obj: nil) msg = build_msg + (message || default_message) super(msg) @failure_obj = failure_obj end def error_klass self.class.name end def build_msg "[#{error_klass}] " end def default_message DEFAULT_MSG end end end end ================================================ FILE: lib/hati_command/errors/configuration_error.rb ================================================ # frozen_string_literal: true module HatiCommand module Errors # Custom error class for configuration issues scenarios in HatiCommand class ConfigurationError < BaseError def default_message 'Invalid configurations' end end end end ================================================ FILE: lib/hati_command/errors/fail_fast_error.rb ================================================ # frozen_string_literal: true module HatiCommand module Errors # Custom error class for FailFast scenario in HatiCommand class FailFastError < BaseError def default_message 'Halt Execution' end end end end ================================================ FILE: lib/hati_command/errors/transaction_error.rb ================================================ # frozen_string_literal: true module HatiCommand module Errors # Custom error class for Transaction issue scenarios in HatiCommand class TransactionError < BaseError def default_message 'Transaction Error has been triggerd' end end end end ================================================ FILE: lib/hati_command/failure.rb ================================================ # frozen_string_literal: true # @module HatiCommand # Provides command handling functionalities and result objects. module HatiCommand # @class Failure # Represents a failure result in the Result pattern. # This class is used to wrap failure values and provide a consistent interface # for handling both successful and failed operations. # # The Failure class is part of the Result pattern implementation, working alongside # the Success class to provide a type-safe way to handle operation outcomes. # # @example Basic usage # failure = HatiCommand::Failure.new("Operation failed") # failure.failure? # => true # failure.success? # => false # # @example With error and metadata # error = StandardError.new("Database connection failed") # failure = HatiCommand::Failure.new( # "Could not save record", # err: error, # meta: { attempted_at: Time.now } # ) # # @example Pattern matching # case result # when HatiCommand::Failure # handle_error(result.failure) # end # # @see HatiCommand::Success # @see HatiCommand::Result class Failure < Result # Returns the failure value wrapped by this Failure instance. # This method provides access to the actual error value or message # that describes why the operation failed. # # @return [Object] The wrapped failure value # @example # failure = Failure.new("Database error") # failure.failure # => "Database error" def failure value end # Indicates that this is a failure result. # This method is part of the Result pattern interface and always # returns true for Failure instances. # # @return [Boolean] Always returns true # @example # failure = Failure.new("Error") # failure.failure? # => true def failure? true end # Returns nil since a Failure has no success value. # This method is part of the Result pattern interface and always # returns nil for Failure instances. # # @return [nil] Always returns nil # @example # failure = Failure.new("Error") # failure.success # => nil def success nil end # Indicates that this is not a success result. # This method is part of the Result pattern interface and always # returns false for Failure instances. # # @return [Boolean] Always returns false # @example # failure = Failure.new("Error") # failure.success? # => false def success? false end # Returns the symbolic representation of this result type. # Useful for pattern matching and result type checking. # # @return [Symbol] Always returns :failure # @api public # @example # failure = Failure.new("Error") # failure.to_sym # => :failure def to_sym :failure end end end ================================================ FILE: lib/hati_command/result.rb ================================================ # frozen_string_literal: true # @module HatiCommand # Provides command handling functionalities and result objects. module HatiCommand # @class Result # Base class for the Result pattern implementation. # This class serves as the foundation for Success and Failure result types, # providing common functionality and a consistent interface for handling # operation outcomes. # # The Result pattern helps in handling operation outcomes in a type-safe way, # making it explicit whether an operation succeeded or failed, and carrying # additional context like error messages and metadata. # # @abstract Subclass and override {#to_sym} to implement a concrete result type # # @example Basic usage # result = HatiCommand::Result.new("Operation output") # result.value # => "Operation output" # # @example With error and metadata # result = HatiCommand::Result.new( # "Operation output", # err: "Warning: partial completion", # meta: { duration_ms: 150 } # ) # # @example Using trace information # result = HatiCommand::Result.new("Output", trace: caller(1..1)) # result.trace # => ["path/to/file.rb:42:in `method_name'"] # # @see HatiCommand::Success # @see HatiCommand::Failure # # @!attribute [r] value # @return [Object] The wrapped value representing the operation's output # # @!attribute [r] meta # @return [Hash] Additional metadata associated with the result # # @!attribute [rw] trace # @return [Array, nil] Execution trace information for debugging class Result attr_reader :value, :meta attr_accessor :trace, :err # Initializes a new Result instance with a value and optional context. # # @param value [Object] The value to be wrapped in the result # @param err [String, nil] Optional error message or error object # @param meta [Hash] Optional metadata for additional context # @param trace [Array, nil] Optional execution trace for debugging # # @example Basic initialization # result = Result.new("Success") # # @example With full context # result = Result.new( # "Partial success", # err: "Some records failed", # meta: { processed: 10, failed: 2 }, # trace: caller # ) def initialize(value, err: nil, meta: {}, trace: nil) @value = value @err = err @meta = meta @trace = trace end # Returns self to provide a consistent interface across result types. # This method ensures that all result objects can be treated uniformly # when chaining operations. # # @return [HatiCommand::Result] The result instance itself # @api public def result self end # Returns the error associated with this result. # This can be used to check for warnings or errors even in successful results. # # @return [String, nil] The error message or object, if any # @raise [StandardError] If accessing the error triggers an error condition # @api public # @example # result = Result.new("Value", err: "Warning message") # result.error # => "Warning message" def error @err end # Returns the symbolic representation of this result type. # This is an abstract method that should be overridden by concrete result types. # # @return [Symbol] Returns :undefined for the base class # @abstract Subclasses must override this method # @api public # @example # Result.new("value").to_sym # => :undefined def to_sym :undefined end end end ================================================ FILE: lib/hati_command/success.rb ================================================ # frozen_string_literal: true # @module HatiCommand # Provides command handling functionalities and result objects. module HatiCommand # @class Success # Represents a successful result in the Result pattern. # This class is used to wrap successful operation values and provide a consistent interface # for handling both successful and failed operations. # # The Success class is part of the Result pattern implementation, working alongside # the Failure class to provide a type-safe way to handle operation outcomes. # # @example Basic usage # success = HatiCommand::Success.new("Operation completed") # success.success? # => true # success.failure? # => false # # @example With metadata # success = HatiCommand::Success.new( # { id: 123, name: "Example" }, # meta: { duration_ms: 50 } # ) # success.success # => { id: 123, name: "Example" } # # @example Pattern matching # case result # when HatiCommand::Success # process_data(result.success) # end # # @see HatiCommand::Failure # @see HatiCommand::Result class Success < Result # Returns the success value wrapped by this Success instance. # This method provides access to the actual value or result # that was produced by the successful operation. # # @return [Object] The wrapped success value # @example # success = Success.new("Operation output") # success.success # => "Operation output" def success value end # Indicates that this is a success result. # This method is part of the Result pattern interface and always # returns true for Success instances. # # @return [Boolean] Always returns true # @example # success = Success.new("Result") # success.success? # => true def success? true end # Returns nil since a Success has no failure value. # This method is part of the Result pattern interface and always # returns nil for Success instances. # # @return [nil] Always returns nil # @example # success = Success.new("Result") # success.failure # => nil def failure nil end # Indicates that this is not a failure result. # This method is part of the Result pattern interface and always # returns false for Success instances. # # @return [Boolean] Always returns false # @example # success = Success.new("Result") # success.failure? # => false def failure? false end # Returns the symbolic representation of this result type. # Useful for pattern matching and result type checking. # # @return [Symbol] Always returns :success # @api public # @example # success = Success.new("Result") # success.to_sym # => :success def to_sym :success end end end ================================================ FILE: lib/hati_command/version.rb ================================================ # frozen_string_literal: true module HatiCommand VERSION = '0.1.2' end ================================================ FILE: lib/hati_command.rb ================================================ # frozen_string_literal: true require 'hati_command/version' # errors require 'hati_command/errors/base_error' require 'hati_command/errors/configuration_error' require 'hati_command/errors/fail_fast_error' require 'hati_command/errors/transaction_error' # result require 'hati_command/result' require 'hati_command/success' require 'hati_command/failure' # core require 'hati_command/callee' require 'hati_command/befehl' # cmd require 'hati_command/cmd' ================================================ FILE: spec/integration/hati_command/befehl_ar_transaction_spec.rb ================================================ # frozen_string_literal: true require 'spec_helper' require 'support/active_record' RSpec.describe HatiCommand::Befehl do let(:ar_model) { 'Widget' } let(:befehl_klass) { support_dummy_befehl('DummyBefehl') } let(:ar_command) { 'MyDummyExecBefehl' } describe 'ActiveRecord transaction wrapping' do before do stub_const(ar_model, Class.new(ActiveRecord::Base)) stub_const( ar_command, Class.new(befehl_klass) do command { ar_transaction :call } def call(message) Widget.create!(name: message) raise ActiveRecord::Rollback if message == :fail HatiCommand::Success.new(message) end end ) end it 'commits the transaction on success' do expect { MyDummyExecBefehl.call('Widget1') }.to change(Widget, :count).by(1) end it 'rolls back the transaction on failure' do expect do MyDummyExecBefehl.call(:fail) rescue StandardError nil end.not_to change(Widget, :count) end end end ================================================ FILE: spec/integration/hati_command/befehl_spec.rb ================================================ # frozen_string_literal: true require 'spec_helper' RSpec.describe HatiCommand::Befehl do let(:befehl_klass) { support_dummy_befehl('DummyBefehl') } before do stub_const( 'MyDummyBefehl', Class.new(befehl_klass) do command do fail_fast 'Default Fail Fast message provided' unexpected_err true result_inference true end def call(message, fail_fast: false, unexpected_err: false, result_inference: false) raise HatiCommand::Errors::FailFastError.new('Fail Fast Triggered') if fail_fast # rubocop:disable Style/RaiseArgs raise StandardError if unexpected_err result_inference ? message : HatiCommand::Success.new(message) end end ) end describe '.call' do let(:result) { MyDummyBefehl.call('Success!') } it 'returns success' do aggregate_failures 'result' do expect(result).to be_a(HatiCommand::Success) expect(result.value).to eq('Success!') expect(result.error).to be_nil end end context 'when fail_fast is true' do let(:result) { MyDummyBefehl.call('This is a fail fast message', fail_fast: true) } it 'returns failure' do aggregate_failures 'result' do expect(result).to be_a(HatiCommand::Failure) expect(result.value).to be_a(HatiCommand::Errors::FailFastError) end end end context 'when unexpected_err is true' do let(:result) { MyDummyBefehl.call('This is a unexpected error message', unexpected_err: true) } it 'returns failure' do aggregate_failures 'result' do expect(result).to be_a(HatiCommand::Failure) expect(result.error).to be_a(StandardError) end end end context 'when result_inference is true' do let(:result) { MyDummyBefehl.call('This is a result inference message', result_inference: true) } it 'returns success' do expect(result).to be_a(HatiCommand::Success) end end end describe 'call configuration' do before do stub_const( 'MyDummyExecBefehl', Class.new(befehl_klass) do command do call_as :execute end def execute(message) HatiCommand::Success.new(message) end end ) end describe '.execute' do let(:rez_msg) { 'This is a result inference message' } let(:result) { MyDummyExecBefehl.execute(rez_msg) } it 'returns success' do aggregate_failures do expect(result).to be_a(HatiCommand::Success) expect(result.value).to eq(rez_msg) end end end end end ================================================ FILE: spec/integration/hati_command/callee_spec.rb ================================================ # frozen_string_literal: true require 'spec_helper' RSpec.describe HatiCommand::Callee do context 'when run command' do before do stub_const('DummyCallee', Class.new do include HatiCommand::Callee def call(rez) HatiCommand::Success.new(rez) end end) end let(:callee_klass) { DummyCallee } describe '.call' do let(:result) { callee_klass.call('Success!') } it 'returns success' do expect(result.value).to eq('Success!') end end end context 'when call is configured' do before do stub_const('DummyExecutee', Class.new do include HatiCommand::Callee call_as :execute def execute(rez) HatiCommand::Success.new(rez) end end) end let(:callee_klass) { DummyExecutee } describe '.execute' do let(:result) { DummyExecutee.execute('Success!') } it 'returns success' do expect(result.value).to eq('Success!') end end end end ================================================ FILE: spec/integration/hati_command/cmd_spec.rb ================================================ # frozen_string_literal: true require 'spec_helper' RSpec.describe HatiCommand::Cmd do before do support_dummy_cmd('BaseDummyCommand') support_dummy_error('DummyFailFastError') support_dummy_error('DummyInternalError') stub_const( 'DummyCommand', Class.new(BaseDummyCommand) do command do fail_fast 'Default Fail Fast message provided' unexpected_err DummyInternalError end end ) stub_const( 'MyDummyCommand', Class.new(DummyCommand) do command do fail_fast DummyFailFastError end def call(greeting = nil, fail_fast: false, unexpected_err: false) raise 'Oooooooops' if unexpected_err salute = build_greeting(greeting) howdy = normalize_salute(salute, fail_fast) process_howdy(howdy.value) end def build_greeting(greeting) greeting ? Success(greeting) : Failure(greeting) end def normalize_salute(salute, fail_fast) Failure!(salute) if fail_fast salute.success ? Success("#{salute.success}!") : Failure(salute.failure) end def process_howdy(howdy) if howdy Success(howdy.upcase, meta: { lang: :eng, length: howdy.length }) else Failure(howdy, err: 'No message provided') end end end ) end let(:message) { 'Hello, World' } describe '.call' do context 'when pipeline success' do let(:raw_message) { 'Hello, World' } let(:message) { 'Hello, World!' } let(:result) { MyDummyCommand.call(raw_message) } it 'runs the success command' do aggregate_failures 'result' do expect(result.value).to eq(message.upcase) expect(result.success).to eq(message.upcase) end end it 'returns meta' do meta = { lang: :eng, length: message.length } expect(result.meta).to eq(meta) end end context 'when pipeline failure' do let(:result) { MyDummyCommand.call } it 'runs the failure command' do aggregate_failures 'result' do expect(result).to be_failure expect(result.value).to be_nil end end it 'returns message' do message = 'No message provided' expect(result.error).to eq(message) end end context 'when fail-fast' do let(:result) { MyDummyCommand.call(fail_fast: true) } it 'returns failure' do expect(result.error).to eq(DummyFailFastError) end end context 'when unexpected error' do let(:result) { MyDummyCommand.call(unexpected_err: true) } it 'returns failure' do expect(result.error).to eq(DummyInternalError) end end end end ================================================ FILE: spec/spec_helper.rb ================================================ # frozen_string_literal: true require 'bundler/setup' require 'hati_command' RSpec.configure do |config| config.example_status_persistence_file_path = '.rspec_status' config.disable_monkey_patching! config.expect_with :rspec do |c| c.syntax = :expect end # performance exclude_support_files = ['active_record'] Dir[File.join('./spec/support/**/*.rb')].each do |support_file| next if exclude_support_files.include?(support_file) require support_file end config.include Dummy end ================================================ FILE: spec/support/active_record.rb ================================================ # frozen_string_literal: true require 'active_record' ActiveRecord::Base.establish_connection( adapter: 'sqlite3', database: ':memory:' ) ActiveRecord::Schema.define do create_table :widgets, force: true do |t| t.string :name t.timestamps end end ================================================ FILE: spec/support/dummy.rb ================================================ # frozen_string_literal: true # NOTE: helper names follow convention 'support__' module Dummy def support_dummy_befehl(name) stub_const(name, Class.new do extend HatiCommand::Befehl end) end def support_dummy_cmd(name) stub_const(name, Class.new do include HatiCommand::Cmd command do fail_fast 'Base Fail Fast Message' end end) end def support_dummy_error(name) stub_const(name, Class.new(StandardError)) end end ================================================ FILE: spec/unit/hati_command/befehl_config_spec.rb ================================================ # frozen_string_literal: true require 'spec_helper' RSpec.describe HatiCommand::Befehl do describe 'command configurations' do let(:command_klass) { 'BefehlClass' } let(:befehl_klass) { command_klass.constantize } let(:command_klass_attr) { 'BefehlClassAttr' } let(:befehl_klass_attr) { command_klass_attr.constantize } before do stub_const(command_klass, Class.new do extend HatiCommand::Befehl::BefehlClassMethods command do call_as :execute fail_fast 'Befehl Fail Fast Message' failure 'Befehl Failure Message' unexpected_err 'Befehl Unexpected Error' result_inference true end end) # no block configs stub_const(command_klass_attr, Class.new do extend HatiCommand::Befehl::BefehlClassMethods call_as :execute fail_fast 'Befehl Fail Fast Message' failure 'Befehl Failure Message' unexpected_err 'Befehl Unexpected Error' result_inference true end) end describe '.fail_fast' do it 'sets the fail_fast config' do expect(befehl_klass.command_config[:fail_fast]).to eq('Befehl Fail Fast Message') end end describe '.unexpected_err' do it 'sets the unexpected_err config' do expect(befehl_klass.command_config[:unexpected_err]).to eq('Befehl Unexpected Error') end end describe '.failure' do it 'sets the failure config' do expect(befehl_klass.command_config[:failure]).to eq('Befehl Failure Message') end end describe '.result_inference' do it 'sets the result_inference config' do expect(befehl_klass.command_config[:result_inference]).to be(true) end end describe '.command_config' do let(:configs) { befehl_klass.command_config } let(:configs_attr) { befehl_klass_attr.command_config } it 'returns the configurations' do aggregate_failures 'of command options' do expect(configs[:fail_fast]).to eq('Befehl Fail Fast Message') expect(configs[:failure]).to eq('Befehl Failure Message') expect(configs[:unexpected_err]).to eq('Befehl Unexpected Error') expect(configs[:result_inference]).to be(true) expect(configs[:call_as]).to be(:execute) end end it 'has identical no-block and block configurations' do aggregate_failures 'of block and no-block command options' do expect(configs[:fail_fast]).to eq(configs_attr[:fail_fast]) expect(configs[:failure]).to eq(configs_attr[:failure]) expect(configs[:unexpected_err]).to eq(configs_attr[:unexpected_err]) expect(configs[:result_inference]).to be(configs_attr[:result_inference]) expect(configs[:call_as]).to be(configs_attr[:call_as]) end end end end end ================================================ FILE: spec/unit/hati_command/callee_spec.rb ================================================ # frozen_string_literal: true require 'spec_helper' # WIP: performance extensions RSpec.describe HatiCommand::Callee do subject(:callee_klass) { described_class } let(:whoami) { 'My Name is Callee' } describe '.whoami' do it 'returns the class name' do expect(callee_klass.whoami).to eq(whoami) end end end ================================================ FILE: spec/unit/hati_command/cmd_spec.rb ================================================ # frozen_string_literal: true require 'spec_helper' # WIP: performance extensions RSpec.describe HatiCommand::Cmd do subject(:cmd_klass) { described_class } let(:whoami) { 'My Name is Cmd' } describe '.whoami' do it 'returns the class name' do expect(cmd_klass.whoami).to eq(whoami) end end end ================================================ FILE: spec/unit/hati_command/errors/base_error_spec.rb ================================================ # frozen_string_literal: true require 'spec_helper' RSpec.describe HatiCommand::Errors::BaseError do subject(:error_klass) { described_class } describe '#initialize' do it 'initializes with a message' do error = error_klass.new('Operation failed') expect(error.message).to eq("[#{error_klass}] Operation failed") end it 'initializes with a failure object' do failure_obj = StandardError.new('Booom!') error = error_klass.new('Operation failed', failure_obj: failure_obj) expect(error.instance_variable_get(:@failure_obj)).to eq(failure_obj) end it 'uses the default message if no message is provided' do error = error_klass.new expect(error.message).to eq("[#{error_klass}] #{error_klass::DEFAULT_MSG}") end end describe '#error_klass' do it 'returns the class name' do error = error_klass.new expect(error.error_klass).to eq(error_klass.name) end end describe '#build_msg' do it 'builds the message correctly' do error = error_klass.new('Test message') expect(error.build_msg).to eq("[#{error_klass}] ") end end end ================================================ FILE: spec/unit/hati_command/errors/configuration_error_spec.rb ================================================ # frozen_string_literal: true require 'spec_helper' RSpec.describe HatiCommand::Errors::ConfigurationError do subject(:error_klass) { described_class } it 'inherits from BaseError' do expect(error_klass).to be < HatiCommand::Errors::BaseError end it 'has the correct default message' do error = error_klass.new expect(error.message).to eq("[#{error_klass}] #{error.default_message}") end end ================================================ FILE: spec/unit/hati_command/errors/fail_fast_error_spec.rb ================================================ # frozen_string_literal: true require 'spec_helper' RSpec.describe HatiCommand::Errors::FailFastError do subject(:error_klass) { described_class } it 'inherits from BaseError' do expect(error_klass).to be < HatiCommand::Errors::BaseError end it 'has the correct default message' do error = error_klass.new expect(error.message).to eq("[#{error_klass}] #{error.default_message}") end end ================================================ FILE: spec/unit/hati_command/errors/transaction_error_spec.rb ================================================ # frozen_string_literal: true require 'spec_helper' RSpec.describe HatiCommand::Errors::TransactionError do subject(:error_klass) { described_class } it 'inherits from BaseError' do expect(error_klass).to be < HatiCommand::Errors::BaseError end it 'has the correct default message' do error = error_klass.new expect(error.message).to eq("[#{error_klass}] #{error.default_message}") end end ================================================ FILE: spec/unit/hati_command/failure_spec.rb ================================================ # frozen_string_literal: true require 'spec_helper' RSpec.describe HatiCommand::Failure do subject(:failure_instance) { described_class.new(value) } let(:value) { 'error_value' } describe '#initialize' do it { is_expected.to have_attributes(value: value) } end describe '#failure' do it { expect(failure_instance.failure).to eq(value) } end describe '#failure?' do it { is_expected.to be_failure } end describe '#success' do it { expect(failure_instance.success).to be_nil } end describe '#success?' do it { expect(failure_instance.success?).to be false } end end ================================================ FILE: spec/unit/hati_command/result_spec.rb ================================================ # frozen_string_literal: true require 'rspec' RSpec.describe HatiCommand::Result do let(:value) { 'test_value' } describe '#initialize' do it { expect(described_class.new(value).value).to eq(value) } end describe '#result' do it 'returns result object' do result = described_class.new(value) expect(result.result).to eq(result) end end describe '#error' do it 'returns error' do result = described_class.new(value, err: 'test_message') expect(result.error).to eq('test_message') end end describe '#meta' do it 'returns meta' do result = described_class.new(value, meta: { 'test_meta' => 'test_value' }) expect(result.meta).to eq({ 'test_meta' => 'test_value' }) end end end ================================================ FILE: spec/unit/hati_command/success_spec.rb ================================================ # frozen_string_literal: true require 'spec_helper' RSpec.describe HatiCommand::Success do subject(:success_instance) { described_class.new(value) } let(:value) { 'success_value' } describe '#initialize' do it { is_expected.to have_attributes(value: value) } end describe '#result' do it { expect(success_instance.result).to eq(success_instance) } end describe '#success' do it { expect(success_instance.success).to eq(value) } end describe '#success?' do it { expect(success_instance.success?).to be true } end describe '#failure' do it { expect(success_instance.failure).to be_nil } end describe '#failure?' do it { expect(success_instance.failure?).to be false } end end