Closed ni3t closed 2 years ago
@ni3t - I like the idea behind this PR. 👍
However, I can't introduce another gem dependency to LightService. Take a look at this discussion we just had recently. The conclusion was to remove ActiveSupport if possible and your PR is going against this goal.
If you can make it work without ActiveModel
, I'll help you any way I can to merge this.
Add
validates
options toexpects
andpromises
Introduction
This PR introduces the concept of runtime validations of
expects
andpromises
keys to LightService.The functionality introduced in this PR is useful for elucidating hard-to-track-down errors, as well as additional validation when integrating different services. It exposes most of the functionality of
ActiveModel::Validations
on aLightService::Context
and merges it with theexpects
/promises
syntax.It also reconfigures the
default
option handler to allow more than one option to be passed toexpects
.I also added a couple of validators we use in our system:
:class
===
(case equality) check between the value's class and what is expectedNumeric === Float #=> true
:class_name
==
(standard equality) check between the value's class and what is expectedProblem Statement
Some errors can be very hard to track down in a complex system, because runtime errors, unless specifically rescued, rarely give good information on the location of the error. Modern error reporting tools are helpful with stack traces, but when developing locally it can be very hard to track it down.
At LoadUp, we commonly nest our services using a pattern we call
AsAction
- i.e. a service class has a subclass calledAsAction
, that passes values from the context into the service and is executed as a part of another service usingService::AsAction
. When nesting multiple services, it involves a lot of tracing. We have also built hundreds of actions that are reused in many places, and in a long, complicated service, it can be difficult to track what keys are on the context at any one point in time, especially if keys are being added during execution (i.e.Actions::Orders::FindBy::Id
, where an order is placed on the context based onorder_id
in the context.)Examples
For example, given the following action, it is impossible, without stack tracing, to determine what calls
fight!
and where it is called.However, with a simple presence validation, a much more helpful error is produced:
In practice, this error message can be made even more explicit:
Summary
These validations are completely optional, but for where it's needed, can really add value.
Caveats and Alternate Approaches
1. It includes a new dependency, ActiveModel.
I've tried to stick as close as possible to basic ActiveModel::Validation patterns, in an effort to minimize the impact of potential breaking changes in the future to ActiveModel. Also, I assume (and you know what assuming does...) that most of the user base for the gem uses rails, and will already have this gem installed. Lastly, ActiveSupport is already a dependency, so it's not too huge of a jump from there to ActiveModel.
An alternate approach could be to limit the vocabulary supported by this feature and hand-write the validators.
2. It only performs runtime validations
As much as I wish this could happen at compile time, it just isn't possible to validate these in any context but runtime. The way I think of LightService's
expects
andpromises
is somewhat akin to a runtime type checker, and this feature fits in a long those lines. However, because it raises errors, unless they are rescued the execution of the service completely halts and things like rollbacks and after_actions are not executed. (This is also a problem with the current implementation of the KeyVerifier.)An alternate approach could be to, instead of operating in the KeyVerifier, to operate in the
executed
at the very beginning, allowing the context to be failed and the resulting errors to be propagated in the result object. This might be a better approach, but I thought since this is operating outside theexecuted
block, it made more sense to run the validations at the same time as the key verifier.Side Note
I'd love to work on adding Ruby 3 types to the gem, and I believe adding these runtime validations can help the compiler produce more explicit types.