This code provides a robust foundation for building service objects in Rails, leveraging dry-validation for consistent input validation. It ensures cleaner code, automatic accessor creation, and structured error handling.
Welcome to the ServiceBase
and DryService
README! This guide will walk you through the usage and benefits of these modules in a Rails application and why we use it at Two Barrels!
The DryService
module and ServiceBase
class aim to streamline service object creation in Rails by integrating dry-validation
for input validation. This ensures consistency and predictability across different contexts (controllers, background jobs, tests, etc.) by validating inputs directly within the service classes.
Traditionally, input validation in Rails has relied on ActionController::Parameters
within controllers, often neglecting validation in other contexts like background jobs or inter-service communication. By incorporating validation directly into service objects, we achieve:
The ServiceBase
class includes the DryService
module, which provides validation capabilities using dry-validation
. Here’s a quick rundown:
contract
method to specify expected parameters and their validations.validate_params!
method ensures that parameters conform to the defined contract, raising a ValidationError
if they do not.setup
and execute
Methods: Child classes must define these methods to perform service-specific logic.Here’s an example service class:
module Checkout::Products
class DomainCheckoutService < ::ServiceBase
contract do
params do
required(:shopping_cart_item).value(type?: ShoppingCartItem)
required(:contact).filled(:hash)
end
end
def setup
fail!("FQDN is required") unless fqdn.present?
fail!("Service was not created") unless associated_service.present?
end
def execute
create_or_transfer_domain
verify_domain_transaction
update_service_with_domain_info
end
private
attr_reader :fqdn, :transfer_code, :associated_service, :domain_id
def create_or_transfer_domain
return initiate_transfer_domain if domain_transfer?
create_new_domain
end
def create_new_domain
domain_result = Domains::CreateDomainService.new(
account_id: Current.account_id,
fqdn: fqdn,
contact_id: contact.dig(:data, :id),
service_id: associated_service.id
).run!
@domain_id = domain_result&.dig("id")
end
def initiate_transfer_domain
fail!("Transfer code is required") unless transfer_code.present?
payload = {
"account_id": Current.account_id,
"domains": [
{
"fqdn": fqdn,
"transfer_code": transfer_code,
"service_id": associated_service.id,
"contact_id": contact.dig(:data, :id),
},
]
}
domain_result = Domains::InitiateTransferService.new(payload).run!
@domain_id = domain_result&. &.dig("id")
end
def verify_domain_transaction
fail!("Transfer failed for #{fqdn}") unless domain_id
end
def update_service_with_domain_info
associated_service.update!({
data: {
domain: {
fqdn: fqdn,
id: domain_id,
}
}
})
end
def domain_transfer?
@transfer ||= shopping_cart_item.data.dig("meta", "is_transfer") == true
end
def fqdn
@fqdn ||= shopping_cart_item.data.dig("meta", "domain")
end
def transfer_code
@transfer_code ||= shopping_cart_item.dig("meta", "transfer_code")
end
def associated_service
@service ||= shopping_cart_item.order_item.renewable_service
end
end
end
Add dry-validation to your Gemfile:
ruby
Copy code
gem ‘dry-validation’
Usage
Create your service classes under app/services/
and include ServiceBase
. Define your contract and implement the setup and execute methods.
class DocumentLockService < ServiceBase
class DocumentLockServiceError < ServiceError; end
class DocumentNotFoundError < DocumentLockServiceError; end
def setup
fail!("Document does not exist", DocumentNotFoundError) if @document.blank?
end
end
Clearly define the shape of your @result
in the setup method to improve readability and predictability:
def setup
@result = {
name: '',
description: '',
price: '',
data: {}
}
end
Execute services using:
result = YourService.new(params).run!
You can create standalone contracts and use them within your services:
module Schemas
module StorefrontVariant
Create = Dry::Schema.Params do
required(:storefront_id).filled(:string)
required(:storefront_variant).hash do
required(:vendor_product_id).filled(:string)
required(:name).filled(:string)
optional(:description).filled(:string)
end
end
end
end
contract do
params(Schemas::StorefrontVariant::Create)
end
By integrating dry-validation
within service objects, we achieve a robust and maintainable way to handle input validation in Rails applications. This approach ensures consistent validation, clear error handling, and a predictable service structure, making your codebase more resilient and easier to work with. Happy coding! 🚀