Plans for dry-validation + dry-schema (a new gem!)


#1

Hey folks,

I’d like to share with you my plans for dry-validation hoping to get some early feedback. So, dry-validation has become too complex, and I don’t like complexity, esp. if it can’t be justified in any reasonable way. As it’s usually the case, complexity is caused by mixing too many concerns in a single place. In this case it’s schema validation with “domain” validation. Because of this, I want to extract dry-schema from dry-validation, and turn dry-validation into a general purpose validation library with features built specifically for defining more complex validation rules, that often need way more info than a single value. This means, practically, that dry-validation will have the same API as it has now for defining schemas, but it will be clearly separated from a new API that I’ll introduce for defining complex rules (that we now call “high level rules”), I don’t know exactly how this API will look like, but it needs to provide a simple infrastructure that at the same time is more flexible than strict predicate composition. We’ll have “validation objects” rather than plain schemas, these objects will have schemas that they can apply to input, and they will apply their own rules to input already processed by their schemas. These objects will be able to access external systems, like a db, or literally anything that they may need in order to apply their rules, they will be able to provide detailed info about failed rules, so that we can generate all kinds of error messages in a simpler way and so on. I basically want to address all known limitations and bugs that we are aware of with this new API. Existing high-level rule API will remain the same inside schemas, so that we can easily transition to the new API w/o being forced to do it immediately when updating to the next version of dry-validation.

I think a nice bonus of this change is that dry-schema will be a simpler gem that people can use for defining validation schemas, whereas dry-validation will be way more powerful and built on top of dry-schema and dry-logic.

Here’s more-or-less what I’m thinking about:

class NewUserValidation < Dry::Validation::SomeGoodClassName # ¯\_(⊙_ʖ⊙)_/¯
  # same API for DI
  option :users

  # same schema API as we currently have
  schema do
    required(:name).filled
    required(:age).maybe(:int?)
    required(:email).filled(:email?)
    optional(:username).maybe(:str?)
    optional(:password).maybe(:str?)
  end

  # similar API for high-level rules with plain ruby inside the blocks
  validate(unique_email: :email) do |email|
    # can work as a predicate - so returns true/false and error message will be inferred from the config
    users.where(email: email).count.zero?
  end
  
  # but you can have more control over error messages for custom rules
  # and return an error object with a message
  validate(min_age: :age) do |age|
    if age <= 18
      error(:age, “you gotta be over 18yo”)
    end
  end

  # we could have an api based on simple pattern-matching too, ie:
  validate(:age, lteq?: 18) { error(“you gotta be over 18yo”) }

  # we can have rules that depend on other rules:
  on(:min_age) do |age|
    # more checks, this is only executed when `:min_age` passed
  end

  # “global” failure messages will be supported (an equivalent of ":base" errors in AM::V)
  validate(:username, :password) do |username, password|
    if !username.empty? && password.empty?
      error.global(“oops we need a password too”)
    end
  end
end

# validators are objects, so you just instantiate them, DI is supported OOTB
user_validator = NewUserValidator.new(users: users_relation)

 # same API as we have now
result = user_validator.(name: "Jane", age: 17, email: "jane@doe.org")

 # simple predicates for checking if it’s all good or not
result.success?
result.failured?

 # dumps to a validated hash
Hash(result)

 # gives you errors
result.errors

# gives you message objects for more control
result.messages

This change means strong separation between pure schema validations and more domain-specific validations. You will still have access to all predicates in schema definitions, and it’s up to you what kind of checks you want to have in schemas and “domain validations”. ie checking if :age is a number is a low-level check, but making sure it’s greater than 18 is a domain-specific check.

This separation will not only make dry-v much simpler internally, but also allow us to have schemas for processing/validating input at the HTTP boundary, and then having domain validators that can be called in the application layer. Schemas and validators can be composed, it means that you’ll be able to specify schemas and reuse them in validators. This way your application’s domain validation will live in the app layer, and hairy HTTP processing/validation will be in the HTTP layer (ie controllers, roda routes, etc.) and this will be possible with 0 code duplication (ie you won’t have to define same attributes in two places).

I’d like to make this change in dry-validation 0.11.0 and it will depend on dry-schema (the new gem) 0.11.0. This will not break your current schemas so don’t worry. Prior 1.0.0 we can easily deprecate various APIs (ie Dry::Validation.Schema { … } will probably become Dry::Schema.define { … } and I do want to deprecate high-level rules too, but give enough time so that we can easily upgrade to the new API, so again, don’t worry).

I believe this is the right thing to do, but as always I’m open to your feedback. If you have any thoughts about this please share them here and we can discuss more details (like how the API should look like etc.). I’m also happy to answer any questions, leave them here as well :slight_smile:

We’ve got a long backlog in the issue tracker and I’d like to address all of those issues ASAP and make the codebase simpler AND add features that are even more powerful than what we have already. The final goal is to turn this into 1.0.0 in a couple months from now.

Cheers!
s.


#2

+1
This is what I do already with dry validation.


#3

This looks fantastic. The above API would solve all my pain points with the current dry-validation. Look forward to it!


#4

not sure this is part of this refactoring but currently I struggle to use dry-validation to validate completeness of our models - we can store incomplete models and have high-level rules which define the completeness. but to validate the models and ‘reuse’ schema with nested object fails as the schema(AddressSchema) expect a Hash on this node but sees an Address instance. beside this check on Hash the only requirement on the validation input is the [] method and the key? method.

beside this the proposed separation is totally in line with what we doing regarding the HTTP param validation and the high-level model validation.