• Jump To … +
    db_adapters.cr state_machine.cr value_object.cr visitor.cr db_adapters.rb log_decorator.rb validation.rb value_object.rb visitor.rb
  • validation.rb

  • ¶

    This is a simple validation engine. With a small amount of code, and using very few metaprogramming tricks, it is concise, extensible and unsurprising in it’s behavior.

  • ¶

    Here are the instance methods for our validable object

    module Validable
    
      def valid?
        failed_validations.empty?
      end
    
      def validation_errors
        failed_validations.map(&:error_message)
      end
    
      private
  • ¶

    Here is where all the magic happens. In order to see if any validation is failed, we get the validations from the class, for each we retrieve the field it’s validating against, and we use send to retrieve it’s value in order for the validation to check.

      def failed_validations
        self.class.validations.reject do |validation|
          field_value = send(validation.field_name)
          validation.valid?(field_value)
        end
      end
    
    end
  • ¶

    These are the class methods for the validables, we simply keep track of the validations for the class and add a couple of “pretty” methods to add them

    module Validations
    
      attr_reader :validations
    
      def validates_format(field_name, format)
        validation = FormatValidation.new(field_name, format)
        add_validation(validation)
      end
    
      def validates_type(field_name, type)
        validation = TypeValidation.new(field_name, type)
        add_validation(validation)
      end
    
      def add_validation(validation)
        @validations ||= []
    
        @validations << validation
      end
    end
  • ¶

    These are the validations, note that they are just plain ruby objects

    class FormatValidation
    
      attr_reader :field_name
    
      def initialize(field_name, regex)
        @field_name = field_name
        @regex = regex
      end
    
      def valid?(field_value)
        @regex =~ field_value
      end
    
      def error_message
        "#{field_name} doesn't match #{@regex}"
      end
    end
    
    class TypeValidation
    
      attr_reader :field_name
    
      def initialize(field_name, type)
        @field_name = field_name
        @type = type
      end
    
      def valid?(field_value)
        field_value.is_a?(@type)
      end
    
      def error_message
        "#{field_name} is not a #{@type}"
      end
    end
    
    class MaxValidation
    
      attr_reader :field_name
    
      def initialize(field_name, max_value)
        @field_name = field_name
        @max_value = max_value
      end
    
      def valid?(field_value)
        field_value <= @max_value
      end
    
      def error_message
        "#{field_name} is bigger than #{@max_value}"
      end
    end
  • ¶

    And here is all together.

    class Person
      include Validable
      extend Validations
    
      validates_type :name, String
      validates_type :phone, String
      validates_type :age, Integer
    
      validates_format :name, /^[A-Z]/
      validates_format :phone, /\d{3}-\d{3}-\d{3}/
  • ¶

    We can have ad-hoc validations even if we don’t have the nice validates_... syntax for them

      add_validation MaxValidation.new(:age, 140)
    
      attr_reader :name, :phone, :age
    
      def initialize(name, phone, age)
        @name = name
        @phone = phone
        @age = age
      end
    end
    
    p Person.new("Diego", "111-212-213", 89).valid? # true
    p Person.new("diego", "not a number", 150).valid? # false