How to create custom predicates in Trailblazer with form object?
I’d like to share how to avoid huge code when you have project with Trailblazer and many repeating validations for different models.
For example, you have validation for uniqueness for several models, in our case it could be Category and Book, this is how my code looks before refactoring:
I have Base class and Contract for Book and Category
# /app/concepts/lib/contract/base.rbmodule Lib
module Contract
class Base < Reform::Form
feature Reform::Form::Dry
end
end
end# /app/concepts/books/contract/update.rbclass Book::Contract::Update < Lib::Contract::Base property :title validation with: { form: true } do
configure do
option :form def case_insensitive_unique?(title)
!Book.where.not(id: form.model.id).exists?(['LOWER(title) = ?', title.downcase])
end
end
end required(:title).filled rule(title: [:title]) do |title|
title.filled? > title.case_insensitive_unique?
end
end# /app/concepts/categories/contract/update.rbclass Category::Contract::Update < Lib::Contract::Base property :name validation with: { form: true } do
configure do
option :form def case_insensitive_unique?(name)
!Category.where.not(id: form.model.id).exists?(['LOWER(name) = ?', name.downcase])
end
end
end required(:name).filled rule(name: [:name]) do |name|
name.filled? > name.case_insensitive_unique?
end
end
As we can see validations are almost the same, the only difference is on which model we check uniqueness. It’s obvious that we need to move this predicate method to some class and share it for all Contracts.
If we check dry-validation docs about custom predicates we can see this example
module MyPredicates
include Dry::Logic::Predicates
predicate(:email?) do |value|
!/magical-regex-that-matches-emails/.match(value).nil?
end
end
schema = Dry::Validation.Schema do
configure do
predicates(MyPredicates)
end
required(:email).filled(:str?, :email?)
endbut in our case for uniqueness validation we need form object to get the model id , I didn’t find how to get form object, so I refactored my code in such way:
Create module when we define our custom predicate method
# /app/concepts/lib/contract/custom_predicates.rbmodule Lib
module Contract
module CustomPredicates
include Dry::Logic::Predicates predicate(:case_insensitive_unique?) do |form, attribute, value|
!form.model.class.where.not(id: form.model.id).exists?(["LOWER(#{attribute}) = ?", value.downcase]) end
end
end
end
and rewrite our Contracts
# /app/concepts/books/contract/update.rbclass Books::Contract::Update < Lib::Contract::Base
property :title validation do
configure do
predicates(Lib::Contract::CustomPredicates)
end required(:title).filled rule(title: [:title]) do |title|
title.filled? > title.case_insensitive_unique?(form, :title)
end
end
end# /app/concepts/categories/contract/update.rbclass Categories::Contract::Update < Lib::Contract::Base
property :name validation do
configure do
predicates(Lib::Contract::CustomPredicates)
end required(:name).filled rule(name: [:name]) do |name|
name.filled? > name.case_insensitive_unique?(form, :name)
end
end
end
It seems like nothing complicated, but when I ran into such problem I didn’t find any examples or information how to paste form object to custom predicate method. So, the fact that formis available in rule was found by chance.
