One of the main Rails trends at the moment is rethinking the role of ActiveRecord classes in the application: henceforth, models should become classes responsible for working with the database, not a hodgepodge of queries, associations, validations, domain methods and presentation methods. Despite the huge models, part of the domain logic still moves to other parts of the application, and this greatly complicates its understanding. In many applications, many actions are performed when events occur, while using
ActiveRecord :: Callbacks . This gem is an attempt to rethink the description of business rules for ActiveRecord models.
So,
triggerable is a gem for describing high-level event-oriented business rules. Rules can be declared both in the context of a class-model, and carried out from it into a separate file. At the moment, the library includes the implementation of two types of rules: triggers and automatics.
Triggers
A trigger is a rule that includes an event, an execution condition, and an action. For example, let us need to send new users an SMS after registration, but on condition that the user agreed to receive messages from us. Let's declare a simple trigger:
User.trigger name: 'SMS to user', on: :after_create, if: { receives_sms: true } do SmsGateway.send_welcome_sms(phone_number) end
How it works: a special callback is added to the model, which initiates the execution of all declared triggers when the condition is met. The action (
do block) will be executed in the context of the model. Since this rule is implemented based on ActiveRecord :: Callbacks, the same list of events is used (
before_save ,
after_create , etc.). An optional
name attribute is passed to the rule declaration; it can be used, for example, to log rule actions.
')
DSL restrictions
The condition can be defined in two ways, the first method is via the built-in DSL, in this case the
if value is a hash. To impose a restriction on a field, you must use its name as a key, and the value will be a hash with conditions. In the example above, the short form of comparison is used - in full form you can use the condition
{receives_sms: {is: true}} . The following simple conditions are currently available:
Type of | Full form | Short form |
---|
Value | {field: {is:: value}} | {field:: value} |
Affiliation | {status: {in: [: open,: accepted]}} | {status: [: open,: accepted]} |
Negation | {field: {is_not:: value} | |
More | {field: {greater_then:: value} | |
Less | {field: {less_then:: value} | |
Existence | {field: {exists: true} | |
In addition, a combination of conditions is available through
and and
or :
{ and: [{ field1: :value1 }, { field2: :value2 }] } { or: [{ field1: :value1 }, { field2: :value2 }] }
If you need to use association checking (currently not supported by DSL) or some other complicated case, you can use the second method - the lambda condition. In this case, the
if value is a block, while the model context will be saved inside the block, for example:
User.trigger on: :after_create, if: { receives_sms? && payments.count > 0 } do send_welcome_sms end
Actions
We have previously declared actions using the
do block, however, in cases where the same actions are performed by objects of different classes, duplication of code can be avoided using our own action class. To do this, you need to inherit from the
Triggerable :: Actions :: Action class and implement the only
def method
run_for! (Object, rule_name) , in which the first argument is the object on which the trigger is running, and the second is the name of the rule (passed in the
name attribute, see . above).
Let's go back to the example of sending SMS. Suppose customers can be registered in the system (
Customer class) who must also receive SMS after registration. Create a new action class and triggers:
class SendWelcomeSms < Triggerable::Actions::Action def run_for! object, trigger_name SmsGateway.send_welcome_sms(object.phone_number) end end User.trigger on: :after_create, if: { receives_sms: true }, do: :send_welcome_sms Customer.trigger on: :after_create, if: { and: [{ receives_sms: true }, { active: true}] }, do: :send_welcome_sms
Automations
Automation - performing a
deferred action when conditions are met. For example, suppose we need to send a message to users not immediately, but after 24 hours. The car will look like this:
User.automation name: 'SMS to user', if: { created_at: { after: 24.hours }, receives_sms: true } do: :send_welcome_sms
Differences from a trigger ad:
1. The event is not specified (
on )
2. The condition block indicates the execution time (
before or
after )
3. lambda-conditions are prohibited
How it works: for the work of the automation, you need to connect any engine for organizing scheduled tasks (for example,
whenever ) and ensure the launch of the automation engine:
Triggerable :: Engine.run_automations (interval) , where
interval is the time interval between task launches. At start-up, a query to the database based on the declared conditions will be executed for each automation (therefore, the lambda-conditions do not work), and an action will be taken for the selected models.
Announced actions will not be performed exactly after the specified time interval, but after the interval expires!Instead of conclusion
More information about connecting to the application and many other things can be found on the githab (as well as looking at the source code!). Waiting for questions, criticism and feedback in the comments.
UPD: One of the possible applications of this solution is to create an interface for declaring user rules (as in Zendesk), that is, possible actions are rigidly defined and these actions are allowed to run on models when the conditions selected by the user are met. The difference from the use of
ActiveRecord :: Observer is the declaration of automatic devices in a similar way with triggers.