Light Service Extensions
Aims to enhance light-service to enhance this powerful and flexible service skeleton framework with an emphasis on simplicity
Console
run bin/console
for an interactive prompt.
Installation
Add this line to your application's Gemfile:
gem 'light-service-ext'
And then execute:
$ bundle install
Or install it yourself as:
$ gem install light-service-ext
ApplicationOrganizer
Adds the following support
Error Handling
Provided by
.with_error_handler
Records errors via
issue_error_report!
into context as exemplified below:{ errors: { base: "some-exception-message", internal_only: { type: 'ArgumentError', message: "`user_id` must be a number", exception: "ArgumentError : `user_id` must be a number", backtrace: [], # filtered backtrace via `[ActiveSupport::BacktraceCleaner](https://api.rubyonrails.org/classes/ActiveSupport/BacktraceCleaner.html)` error: original_captured_exception } } }
Captures
model validation
exceptions and record the messages to the organizer's:errors
context field- Supports the following exceptions by default
ActiveRecord::Errors
ActiveModel::Errors
Raises any non validation errors up the stack
API Responses
- records api responses set by an action's
:api_response
context field - Stored inside of the organizer's
:api_responses
field
Retrieve Record
Allows for a block to be defined on an organizer in order to retrieve the model record
Failing The Context
- Prevents further action's been executed in the following scenarios:
- All actions complete determined by organizer's
:outcome
context field set toLightServiceExt::Outcome::COMPLETE
- All actions complete determined by organizer's
Example
class TaxCalculator < LightServiceExt::ApplicationOrganizer
self.retrieve_record = -> (ctx:) { User.find_by(email: ctx.params[:email]) }
def self.call(input)
user = record(ctx: input) # `.record` method executes proc provided to `retrieve_record`
input = { user: user }.merge(input)
super(input)
end
def self.steps
[TaxValidator, CalcuateTaxAction]
end
end
ApplicationOrchestrator
Useful if you want the current
Organizer
to act as aOrchestrator
and call another organizer
- ONLY modifies the orchestrator context from executing
organizer_steps
if manually applied viaeach_organizer_result
Proc
method overrides
organizer_steps
~ must be a list of organizers to be called prior to orchestrator's actions
Example
class TaxCalculatorReport < LightServiceExt::ApplicationOrchestrator
self.retrieve_record = -> (ctx:) { User.find_by(email: ctx.params[:email]) }
def self.call(input)
user = record(ctx: input) # `.record` method executes proc provided to `retrieve_record`
input = { user: user }.merge(user: user)
reduce_with({ input: input }, steps)
super(input.merge({ user: user })) do |current_organizer_ctx, orchestrator_ctx:|
orchestrator_ctx.add_params(current_organizer_ctx.params.slice(:user_id)) # manually add params from executed organizer(s)
end
end
def organizer_steps
[TaxCalculator]
end
def steps
[TaxReportAction]
end
end
ApplicationAction
Useful methods
- TODO
Invoked Action
- NOTE Action's
executed
block gets called by the underlyingLightService::Action
- this means in order to call your action's methods you need to invoke it from
invoked_action:
instead ofself
- this means in order to call your action's methods you need to invoke it from
invoked_action:
added to current action's context before it gets executed- Consist of an instance of the current action that implements
LightServiceExt::ApplicationAction
- Consist of an instance of the current action that implements
ApplicationContract
- Enhances
Dry::Validation::Contract
with the following methods:#keys
~> returns names of params defined#t
~> returns translation messages in context with the current organizer- Arguments:
key
e.g. :not_foundbase_path:
e.g. :user**opts
options passed into underlying Rails i18n translate call
- E.g.
t(:not_found, base_path: 'business_create', scope: 'user')
would execute- =>
I18n.t('business_create.user.not_found', opts.except(:scope))
- =>
ApplicationValidatorAction
Responsible for mapping, filtering and validating the context
input:
field
executed
block does the following:- Appends
params:
field to the current context with the mapped and filtered values - Appends errors returned from a
ApplicationContract
dry-validation contract to the current context'serrors:
field - NOTE fails current context if
errors:
present
- Appends
Useful Accessors
.contract_class
~> sets the dry-validation contract to be applied by the current validator action.params_mapper_class
~> sets the mapper class that must implement.map_from(context)
and return mapped:input
values
ApplicationContext
Adds useful defaults to the organizer/orchestrator context
:input
~> values originally provided to organizer get moved here for better isolation:params
- stores values
filtered
andmapped
from originalinput
- outcomes/return values provided by any action that implements
LightServiceExt::ApplicationAction
:errors
- validation errors processed by
LightServiceExt::ApplicationValidatorAction
dry-validation contract- manually added by an action e.g.
{ errors: { email: 'not found' } }
:successful_actions
~> provides a list of actions processed mostly useful for debugging purposes e.g.['SomeActionClassName']
invoked_action
~> instance of action to being called.:current_api_response
~> action issued api response:api_responses
~> contains a list of external API interactions mostly for recording/debugging purposes (internal only):allow_raise_on_failure
~> determines whether or not to throw aRaiseOnContextError
error up the stack in the case of validation errors and/or captured exceptions:status
denotes the current status of the organizer with one of the following flags:
LightServiceExt::Status::COMPLETE
LightServiceExt::Status::INCOMPLETE
:last_failed_context
~ copy of context that failed e.g. witherrors
field presentinternal_only
~ includes the likes of raised error summary and should never be passed to endpoint responsesmeta
~ used to store any additional information that could be helpful especially for debugging purposes. Example
input = { order: order }
overrides = {} # optionally override `params`, `errors` and `allow_raise_on_failure`
= { current_user_id: 12345, request_id: some-unique-request-id, impersonator_id: 54321 }
LightServiceExt::ApplicationContext.make_with_defaults(input, overrides, meta: )
# => { input: { order: order },
# errors: { email: ['not found'] },
# params: { user_id: 1 },
# status: Status::INCOMPLETE,
# invoked_action: SomeActionInstance,
# successful_actions: ['SomeActionClassName'],
# current_api_response: { user_id: 1, status: 'ACTIVE' },
# api_responses: [ { user_id: 1, status: 'ACTIVE' } ],
# last_failed_context: {input: { order: order }, params: {}, ...},
# allow_raise_on_failure: true,
# internal_only: { error_info: ErrorInfoInstance },
# meta: { current_user_id: 12345, request_id: some-unique-request-id, impersonator_id: 54321 }
# }
Useful methods
.add_params(**params)
- Adds given args to context's
params
field - e.g.
add_params(user_id: 1) # => { params: { user_id: 1 } }
- Adds given args to context's
add_errors!
- Adds given args to to context's
errors
field - Fails and returns from current action/organizer's context
- e.g.
add_to_errors!(email: 'not found') # => { errors: { email: 'not found' } }
- Adds given args to to context's
.add_errors(**errors)
- Adds given args to to context's
errors
field - DOES NOT fails current context
- e.g.
add_to_errors(email: 'not found') # => { errors: { email: 'not found' } }
- Adds given args to to context's
.add_status(status)
- Should be one of Statuses e.g.
Status::COMPLETE
- e.g.
add_status(Status::COMPLETE) # => { status: Status::COMPLETE }
- Should be one of Statuses e.g.
.add_internal_only(attrs)
- e.g.
add_internal_only(request_id: 54) # => { internal_only: { error_info: nil, request_id: 54 } }
- e.g.
add_to_successful_actions(action_name_or_names)
~> adds action names successfully executed
ContextError
Provides all the information related to an exception/validation errors captured by the current organizer
Useful methods
#error_info
~>ErrorInfo
instance#context
~> state of context provided#error
~> original exception#message
~> summarizes which action failed etc.
ErrorInfo
- Summarize captured exception
Useful accessors
non_fatal_errors
~> takes a list of error class names considered to be non fatal exceptions
Useful methods
#error
~> captured exception#type
~> exception class name e.g.ArgumentError
#message
~> error messagetitle
~> combined error class name and error message e.g.ArgumentError : email must be present
#fatal_error?
#error_summary
~> summarizes exception with message and cleaned backtrace viaActiveSupport::BacktraceCleaner
Regex
Useful methods
.match?(type, value)
e.g.LightServiceExt::Regex.match?(email:, '[email protected]')
- supported
type
:
- supported
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.
Contributing
Bug reports and pull requests are welcome on GitHub at https://github.com/[USERNAME]/light-service-ext. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the code of conduct.
License
The gem is available as open source under the terms of the MIT License.
Code of Conduct
Everyone interacting in the LightServiceExt project's codebases, issue trackers, chat rooms and mailing lists is expected to follow the code of conduct.