diff --git a/README.md b/README.md index edfca4a..d8340b8 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,6 @@ [![Build Status](https://travis-ci.org/drexed/lite-command.svg?branch=master)](https://travis-ci.org/drexed/lite-command) Lite::Command provides an API for building simple and complex command based service objects. -It provides extensions for handling errors and memoization to improve your object workflow productivity. ## Installation @@ -25,273 +24,128 @@ Or install it yourself as: ## Table of Contents * [Setup](#setup) -* [Simple](#simple) -* [Complex](#complex) -* [Procedure](#procedure) -* [Extensions](#extensions) +* [Execution](#execution) +* [Context](#context) +* [Internals](#Internals) +* [Generator](#generator) ## Setup -`rails g command NAME` will generate the following file: - -```erb -app/commands/[NAME]_command.rb -``` - -If a `ApplicationCommand` file in the `app/commands` directory is available, the -generator will create file that inherit from `ApplicationCommand` if not it will -fallback to `Lite::Command::Complex`. - -## Simple - -Simple commands are a traditional command service call objects. -It only exposes a `call` method that returns a value. - -**Setup** +Defining a command is as simple as adding a call method. ```ruby -class CalculatePower < Lite::Command::Simple +class CalculatePower < Lite::Command::Base - # NOTE: This `execute` class method is required to use with call - def self.execute(a, b) - a**b + def call + # TODO: implement calculator end end ``` -**Callers** - -```ruby -CalculatePower.execute(2, 2) #=> 4 - -# - or - - -CalculatePower.call(2, 3) #=> 8 -``` - -## Complex - -Complex commands are powerful command service call objects. -It can be extended to use error, memoization, and propagation mixins. - -**Setup** - -```ruby -class SearchMovies < Lite::Command::Complex - - attr_reader :name - - def initialize(name) - @name = name - end - - # NOTE: This `execute` instance method is required to use with call - def execute - { generate_fingerprint => movies_by_name } - end - - private - - def movies_by_name - HTTP.get("http://movies.com?title=#{name}") - end +## Execution - def generate_fingerprint - Digest::MD5.hexdigest(movies_by_name) - end - -end -``` +Executing a command can be done as an instance or class call. +It returns the command instance in a forzen state. +These will never call will never raise an execption, but will +be kept track of in its internal state. -**Caller** +**NOTE:** Class calls is the prefered format due to its readability. ```ruby -SearchMovies.execute('Toy Story') #=> { 'fingerprint_1' => [ 'Toy Story 1', ... ] } - -# - or - - -command = SearchMovies.new('Toy Story') -command.called? #=> false -command.call #=> { 'fingerprint_1' => [ 'Toy Story 1', ... ] } -command.called? #=> true +# Class call +CalculatePower.call(..args) -# - or - +# Instance call +caculator = CalculatePower.new(..args).call -command = SearchMovies.call('Toy Story') -command.called? #=> true -command.call #=> { 'fingerprint_1' => [ 'Toy Story 1', ... ] } +#=> ``` -**Result** +Commands can be called with a `!` bang method to raise a +`Lite::Command::Fault` based exception or the original +`StandardError` based exception. ```ruby -command = SearchMovies.new('Toy Story') -command.result #=> nil - -command.call #=> { 'fingerprint_1' => [ 'Toy Story 1', ... ] } -command.result #=> { 'fingerprint_1' => [ 'Toy Story 1', ... ] } - -command.recall! #=> Clears the `call`, `cache`, `errors` variables and then re-performs the call -command.result #=> { 'fingerprint_2' => [ 'Toy Story 2', ... ] } +CalculatePower.call!(..args) +#=> raises Lite::Command::Fault ``` -## Procedure - -Procedures are used to run a collection of commands. It uses the the complex procedure API -so it has access to all the methods. The `execute` method is already defined to -handle most common procedure steps. It can be use directly or subclassed. +## Context -**Setup** - -```ruby -class SearchChannels < Lite::Command::Procedure; end -``` +Accessing the call arguments can be done through its internal context. +It can be used as internal storage to be accessed by it self and any +of its children commands. ```ruby -commands = [DisneyChannel.new, EspnChannel.new(current_station), MtvChannel.new] - -procedure = SearchChannels.call(*commands) -procedure.result #=> ['disney: #3', 'espn: #59', 'mtv: #212'] -procedure.steps #=> [, , ] - -# - or - - -# If the errors extension is added you can stop the procedure at first failure. -procedure = SearchChannels.new(*commands) -procedure.exit_on_failure = true -procedure.call -procedure.result #=> ['disney: #3'] -procedure.failed_steps #=> [{ index: 1, step: 2, name: 'ErrorChannel', args: [current_station], errors: ['field error message'] }] -``` - -## Extensions +class CalculatePower < Lite::Command::Base -Extend complex (and procedures) base command with any of the following extensions: - -### Errors (optional) - -Learn more about using [Lite::Errors](https://github.com/drexed/lite-errors) - -**Setup** - -```ruby -class SearchMovies < Lite::Command::Complex - include Lite::Command::Extensions::Errors - - # ... ommited ... - - private - - # Add a explicit and/or exception errors to the error pool - def generate_fingerprint - if movies_by_name.nil? - errors.add(:fingerprint, 'invalid md5 request value') - else - Digest::MD5.hexdigest(movies_by_name) - end - rescue ArgumentError => exception - merge_exception!(exception, key: :custom_error_key) + def call + context.result = context.a ** context.b end end -``` - -**Instance Callers** - -```ruby -command = SearchMovies.call('Toy Story') -command.errors #=> Lite::Errors::Messages object - -command.validate! #=> Raises Lite::Command::ValidationError if it has any errors -command.valid? #=> Alias for validate! - -command.errored? #=> false -command.success? #=> true -command.failure? #=> Checks that it has been called and has errors -command.status #=> :failure - -command.result! #=> Raises Lite::Command::ValidationError if it has any errors, if not it returns the result - -# Use the following to merge errors from other commands or models -# with the default direction being `:from` -command.merge_errors!(command_2) -user_model.merge_errors!(command, direction: :to) -``` - -**Block Callers** - -```ruby -# Useful for controllers or actions that depend on states. -SearchMovies.perform('Toy Story') do |result, success, failure| - success.call { redirect_to(movie_path, notice: "Movie can be found at: #{result}") } - failure.call { redirect_to(root_path, notice: "Movie cannot be found at: #{result}") } -end -``` - -### Propagation (optional) - -Propagation methods help you perform an action on an object. If successful is -returns the result else it adds the object errors to the form object. Available -propagation methods are: - - `assign_and_return!(object, params)` - - `create_and_return!(klass, params)` - - `update_and_return!(object, params)` - - `destroy_and_return!(object)` - - `archive_and_return!(object)` (if using Lite::Archive) - - `save_and_return!(object)` - -**Setup** - -```ruby -class SearchMovies < Lite::Command::Complex - include Lite::Command::Extensions::Errors - include Lite::Command::Extensions::Propagation - - # ... ommited ... - - def execute - create_and_return!(User, name: 'John Doe') - end -end +command = CalculatePower.call(a: 2, b: 3) +command.context.result #=> 8 ``` -### Memoize (optional) - -Learn more about using [Lite::Memoize](https://github.com/drexed/lite-memoize) +## Internals + +#### States +State represents the state of the executable code. Once `execute` +is ran, it will always `complete` or `dnf` if a fault is thrown by a +child command. + +- `pending` + - Command objects that have been initialized. +- `executing` + - Command objects actively executing code. +- `complete` + - Command objects that executed to completion. +- `dnf` + - Command objects that could NOT be executed to completion. + This could be as a result of a fault/exception on the + object itself or one of its children. + +#### Statuses + +Status represents the state of the callable code. If no fault +is thrown then a status of `success` is returned even if `call` +has not been executed. The list of status include (by severity): + +- `success` + - No fault or exception +- `noop` + - Noop represents skipping completion of call execution early + an unsatisfied condition or logic check where there is no + point on proceeding. + - **eg:** account is sample: skip since its a non-alterable record +- `invalid` + - Invalid represents a stoppage of call execution due to + missing, bad, or corrupt data. + - **eg:** user not found: stop since rest of the call cant be executed +- `failure` + - Failure represents a stoppage of call execution due to + an unsatisfied condition or logic check where it blocks + proceeding any further. + - **eg:** record not found: stop since there is nothing todo +- `error` + - Error represents a caught exception for a call execution + that could not complete. + - **eg:** ApiServerError: stop since there was a 3rd party issue + +## Generator -**Setup** - -```ruby -class SearchMovies < Lite::Command::Complex - include Lite::Command::Extensions::Memoize - - # ... ommited ... - - private - - # Sets the value in the cache - # Subsequent method calls gets the cached value - # This saves you the extra external HTTP.get call - def movies_by_name - cache.memoize { HTTP.get("http://movies.com?title=#{name}") } - end - - # Gets the value in the cache - def generate_fingerprint - Digest::MD5.hexdigest(movies_by_name) - end +`rails g command NAME` will generate the following file: -end +```erb +app/commands/[NAME]_command.rb ``` -**Callers** - -```ruby -command = SearchMovies.call('Toy Story') -command.cache #=> Lite::Memoize::Instance object -``` +If a `ApplicationCommand` file in the `app/commands` directory is available, the +generator will create file that inherit from `ApplicationCommand` if not it will +fallback to `Lite::Command::Base`. ## Development