Using Ruby Mutations in Practice
#Ruby #Ruby-on-Rails #Software Development
The following is an excerpt from my book Building and Deploying Crypto Trading Bots
Mutations is library and framework for building opinionated service objects in Ruby.
A Tour of Mutations
Within the framework all service objects have the same format. Every class inherits from the Mutations::Command
parent which provides a tiny API for children to declare required and optional inputs in the class definition.
class CreateInventoryOrder < Mutations::Command
required do ❶
integer :routing_number ❷
model :product, class: Product ❸
end
# Example of optional input:
# optional do
# integer :input_name
# end
end
Within the required
block ❶ the inputs that must be provided as arguments to the mutation are described along with their desired data types. Data types are described using the library’s “filter methods”. These methods include filters like integer,
string
, symbol
, model
, etc that assert certain properties (via
duck typing) about the input in order to roughly assert type. In our above mutation example the required
block specifies that a keyword argument routing_number
❷ must be provided and should be an integer
, while a second keyword argument product
must be provided and should be a model. If any required input is left out from the list of arguments or the wrong type of argument is given, the mutation will raise an error. To run a mutation the class method run
(or run!
) is invoked on the mutation class:
outcome = CreateInventoryOrder.run
outcome.errors.message_list
# => [Routing Number is required, Product is required]
outcome = CreateInventoryOrder.run(routing_number: 'not_an_int')
outcome.errors.message_list
# => [Routing Number isn't an integer]
This validation step is handy for ensuring correct input is provided. Once inputs have been defined for the mutation, children can override the validate
and execute
methods to include desired business logic.
Let’s take a look at an example below:
# Mutation
class CreateInventoryOrder < Mutations::Command
required do
integer :routing_number
model :product, class: Product
end
# Only if the inputs validate will the execute method run
def validate ❹
add_error(:product, :invalid, "Inventory is remaining for product") unless Product.find([product.id](http://product.id)).out_of_stock?
end
end
# running the mutation
outcome = CreateInventoryOrder.run(routing_number: 1, product: Product.first)
outcome.errors.message_list ❺ # => ['Inventory is remaining for product']
When a mutation executed the method run
calls the validate
method ❹ . The validate
method is a hook for the implementation to evaluate the state of provided inputs to determine whether or not to proceed with execution. The hook exposes an add_error
method that is used to append error messages to the mutation outcome. If the validation hook fails the mutation execution will halt before proceeding further. The error messages created in the validate
hook are then accessible through the errors
method of the return value ❺. If no errors are added in the validate method, the mutation proceeds to call the execute
hook.
class CreateInventoryOrder < Mutations::Command
required do
integer :routing_number
model :product, class: Product
end
# Only if the inputs validate will the execute method run
def validate
add_error(:product, :invalid, "Inventory is remaining for product") unless Product.find([product.id](http://product.id)).out_of_stock?
end
def execute ❻
pdf_order = PDF.new(routing_number, product)
payable = AccountsPayable.new(pdf_order)
SubscribersWebhookNotifier.perform_async
InventorOrder.new(pdf_order, payable)
end
end
# running the mutation
outcome = CreateInventoryOrder.run(routing_number: 1, product: Product.second)
outcome.success? # => true
outcome.result ❼ # => <InventoryOrder ..>
The execute
❻ method contains the business logic and data manipulation for a mutation. This is the implementation of the “strategy”. Once a mutation completes the returned value will be available in the result
❼ of the outcome. This return value is the value returned from the execute
method. The outcome has a success?
method to determine if the mutation ran as expected or not.
Benefits
- Decoupled business logic from core application routing (i.e. controllers).
- Uniform APIs for execution and testing
- Fits the “convention over configuration” philosophy bill
- Data type validations
- Single responsibility principle
- Acts as a stop gap toward the “Obese Model” Rails anti-pattern
- Highly testable classes
Drawbacks
- Moves away from conventional Rails model-view-controller pattern
- Opinionated and therefore not flexible on a platform level consideration (i.e. if your company has their own service object pattern)