rom-rb/rom-rails

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 into ROM::Model::Validator) require access to attributes, which means we would need to forward :name and :email to injected params instance (I hope it will work and we won't have to define instance variables copied from params)
  • 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 :()