Separate input params and validation logic
solnic opened this issue · 3 comments
Many people have been confused about params and validations being mixed together. Right now when you include ROM::Model::Params
into a class you get attribute
DSL and ActiveModel::Validations
DSL too. One caveat here is that those validations don't support interaction with a database so for instance if you want to use uniqueness validation you need to override default ROM::Model::Validator
logic and do things manually there.
This does work (I use it in my app already) but is suboptimal and ugly and confusing. To improve that I believe it should be possible to have something like that:
class NewUserParams
include ROM::Model::Params
attribute :name, String
attribute :email, String
end
class NewUserValidator
include ROM::Model::Validator
validates :name, presence: true
validates :email, uniqueness: true
end
# "standalone" usage:
params = NewUserParams.new(name: 'Jane', email: 'jane@doe.org')
validator = NewUserValidator.new(params).call
# in command it stays the same, so
ROM.commands(:users) do
define(:create) do
input NewUserParams
validator NewUserValidator
result :one
end
end
There are a couple of thing to keep in mind:
ActiveModel::Validations
(that would be mixed intoROM::Model::Validator
) require access to attributes, which means we would need to forward:name
and:email
to injectedparams
instance (I hope it will work and we won't have to define instance variables copied fromparams
)- We would have to come up with a new object type that encapsulates both attributes and error messages object if we want to make it work with various form helpers that expect just one object (notice that an AM-compliant object has both attributes and errors)
- We would have to extend
ROM::Model::Validator
to be able to accept relations registry for validations that require database access
refs #14
The way I see it is validation on input params should be context free. The validation for uniqueness (and other contextual validations) should live in the command object/use case that implements this behaviour.
Uniqueness validations have a race condition between the validation code running and the insert statement (assuming you have a unique index), where another request could take the username after another request has already validated that it's available. With ActiveRecord you'd catch the record not unique exception and retry so that validations would catch it that time around and display the appropriate message to the user. How would ROM commands handle this scenario?
class SignUp
def perform
retry_once ActiveRecord::RecordNotUnique do
validate!
create_user
end
end
# ...
end
@stevehodgkiss yeah rom-sql command catches low-level constraint violations and return an error back for further handling in the application layer. See this spec
btw that's why I started with validations on input params and a separate validator object for use-case specific validations (aka contextual). But it turned out to be too messy (at least when using AM stuff :()