I’m attempting to move away from reform to dry-validation. One reform feature we were making use of was the ability to reuse our existing ActiveModel validations:
class Jobs::UpdateForm < Reform::Form
extend ActiveModel::ModelValidations
# various properties
copy_validations_from Project
end
This made it convenient to move validation from our models towards the edges of our application, but we also kept the validations on the models as a “last line of defense” and because not all paths went through the new validations.
I don’t see an obvious way to do the same with dry-validations.
My best idea so far is to define a base contract for a given model and have a bunch of rules as macros. Then the “pure model” contract would use all of those macros and I’d somehow shuttle the errors back to ActiveModel. For forms / JSON endpoints, I’d define the properties that are allowed on that endpoint and then use the appropriate macros.
I don’t see a way around duplicating the dry-logic bits (e.g. .value(:integer, gt?: -365, lt?: 365)).
Are there any great patterns that exist that I could crib from to get started?
I have an existing ActiveRecord/ActiveModel model with validations:
require 'active_model'
class MyValidations < ActiveModel::Validator
def validate(record)
if record.name.length > 24
record.errors.add :name, "This name is too long, try using the description instead"
end
end
end
class MyModel
include ActiveModel::Model
include ActiveModel::Validations
attr_accessor :id, :name, :description
validates_with MyValidations
end
m = MyModel.new(name: 'a' * 50)
puts m.validate
# => false
puts m.errors.messages
# => {:name=>["This name is too long, try using the description instead"]}
I wish to use dry-validations at the edges of my program, so I create a contract:
require 'dry-validation'
class MyContract < Dry::Validation::Contract
params do
optional(:name).value(:string)
end
rule(:name) do
if value.length > 24
key.failure "This name is too long, try using the description instead"
end
end
end
validation = MyContract.new.call(name: 'a' * 50)
puts validation.success?
# => false
puts validation.errors.to_h
# => {:name=>["This name is too long, try using the description instead"]}
In doing so, I have duplicated the logic of my validations out of MyValidations into MyContract. I do not want this logic to be duplicated.
class ContractValidator
def initialize(options)
@contract_class = options.fetch(:contract) { raise "The `contract:` option is required" }
end
def validate(record)
contract = @contract_class.new
validation = contract.call(record.attributes)
validation.errors.to_h.each { |k, v| v.each { |v| record.errors.add(k, v) } }
end
end
I can then use this in my model:
class MyModel
include ActiveModel::Model
include ActiveModel::Validations
attr_accessor :id, :name, :description
validates_with ContractValidator, contract: ::MyContract
def attributes
{ id: id, name: name, description: description }
end
end
In the real app, I create a base contract with all the rules as macros, then subcontracts for “model creation via HTTP”, “model editing via HTTP”, “model specific change via HTTP”, etc. I also create a subcontract for “model saving” which is what is actually used in the validates_with.
I don’t see a way to reuse the dry-logic predicates.