Robert's Homepage

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

Drawbacks