📜 ⬆️ ⬇️

Triggerable - event-oriented logic for ActiveRecord models

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 ofFull formShort 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.

Source: https://habr.com/ru/post/247933/


All Articles