Skip to content

Input Data Validation

Usually, input definitions are quite simple and raise an error if the provided data does not match. How to return a meaningful error result when input data does not conform to specifications.

This example uses dry-validation, but you are free on how to validate your input data.

require "dry/validation"

class User
  def initialize(name:, age:)
    @name, @age = name, age
  end
  attr_reader :name, :age

  class << self
    attr_accessor :has_db
  end

  def save
    !!User.has_db
  end

  def errors
    User.has_db ? nil : { database: ["not connected"] }
  end
end

Dry::Validation.load_extensions(:predicates_as_macros)
class CreateUserContract < Dry::Validation::Contract
  import_predicates_as_macros

  schema do
    required(:name).filled(:string)
    required(:age).value(:integer)
  end

  rule(:age).validate(gteq?: 18)
end

class CreateUser
  include Teckel::Operation
  result!

  input CreateUserContract.new

  input_constructor(->(input){
    result = self.class.input.call(input)
    if result.success?
      result.to_h
    else
      fail!(message: "Input data validation failed", errors: result.errors.to_h)
    end
  })

  output Types.Instance(User)
  error  Types::Hash.schema(
    message: Types::String,
    errors: Types::Hash.map(Types::Symbol, Types::Array.of(Types::String))
  )

  def call(input)
    user = User.new(**input)

    if user.save
      success! user
    else
      fail!(message: "Could not save User", errors: user.errors)
    end
  end

  finalize!
end
User.has_db = true
CreateUser.call(name: "Bob", age: 23).success
#=> #<User:<...> @age=23, @name="Bob">

Error from response from our validation in input_constructor:

CreateUser.call(name: "Bob", age: 10).failure
#=> {:errors=>{:age=>["must be greater than or equal to 18"]},
    :message=>"Input data validation failed"}

Error response from the our operation call:

User.has_db = false
CreateUser.call(name: "Bob", age: 23).failure
#=> {:errors=>{:database=>["not connected"]}, :message=>"Could not save User"}

Errors raised in input_constructor need to conform to the defined error:

>> class IncorrectFailure
..   include Teckel::Operation
.. 
..   result!
.. 
..   input(->(input) { input }) # pass
..   input_constructor(->(_input) {
..     fail!("Input data validation failed")
..   })
.. 
..   output none
..   error  Types::Hash.schema(message: Types::String)
.. 
..   def call(_); end
..
..   finalize!
.. end

>> IncorrectFailure.call rescue $ERROR_INFO
=> #<Dry::Types::ConstraintError:<...> "Input data validation failed" violates constraints (type?(Hash, "Input data validation failed") failed)>

Last update: 2021-09-28 15:32:53