0.01
No release in over 3 years
Low commit activity in last 3 years
Interface and protocol for validating and validation discovery
2005
2006
2007
2008
2009
2010
2011
2012
2013
2014
2015
2016
2017
2018
2019
2020
2021
2022
2023
2024
 Dependencies

Development

Runtime

 Project Readme

Validate

Interface and protocol for validating and validation discovery

Rationale

It's just Ruby! No magic. No opaque DSLs. Just plain old Ruby programming that everyone can see and understand at a glance.

Synopsis

class Example
  attr_accessor :some_attr

  module Validate
    def self.call(example)
      !example.some_attr.nil?
    end
  end
end

e = Example.new # some_attr is nil

valid = Validate.(e)

test "Not valid" do
  refute(valid)
end

e.some_attr = 'something' # some_attr is no longer nil

valid = Validate.(e)

test "Is valid" do
  assert(valid)
end

In the example above, the Validate module within the Example class is discovered, and used as a validation script.

The internal Validate module is a Protocol. It requires a defined function of self.call(instance, optional_state)

Validation State

class Example
  attr_accessor :some_attr

  module Validate
    def self.call(example, state=[])
      state << 'All is well'
      true
    end
  end
end

e = Example.new

state = []
valid = Validate.(e, state)

test "Not valid" do
  refute(valid)
end

assert(state == ['All is well'])

Errors may be collected via the second optional state parameter on the Validate protocol. In the example above, state is an array, but it could just as easily be a hash, or whatever type of object you want.

Advanced Usage

Multiple Validation Scenarios

It can be useful to have specialized validation scenarios, in order to validate an object in different contexts. Scenarios should not be implemented unless there is a need for them, because they are not needed most of the time.

class Example
  attr_accessor :some_attr

  module Validate
    def self.some_particular_scenario
      SomeValidator
    end

    def self.some_other_scenario
      SomeOtherValidator
    end

    module SomeValidator
      def self.call(example)
        example.some_attr == 'something'
      end
    end

    module SomeOtherValidator
      def self.call(example)
        example.some_attr == 'something else'
      end
    end
  end
end

e = Example.new

e.some_attr = 'some invalid value'

valid = Validate.(e, scenario: :some_particular_scenario)

test "Not valid" do
  refute(valid)
end

e.some_attr = 'something' # some_attr is no longer nil

valid = Validate.(e, scenario: :some_particular_scenario)

test "Is valid" do
  assert(valid)
end

e.some_attr = 'some invalid value'

valid = Validate.(e, scenario: :some_other_scenario)

test "Not valid" do
  refute(valid)
end

e.some_attr = 'something else'

valid = Validate.(e, scenario: :some_other_scenario)

test "Is valid" do
  assert(valid)
end

Composing Validation Scenarios

Multiple scenarios may be invoked at once, if any in the sequence return false, the entire validation fails.

e = Example.new
e.some_attr = 'something else' # causes both scenarios to be valid

valid = Validate.(e, scenarios: [:some_particular_scenario, :some_other_scenario])

test "Is valid" do
  assert(valid)
end

e.some_attr = 'something and other thing' # causes only the first scenario to be valid

valid = Validate.(e, scenarios: [:some_particular_scenario, :some_other_scenario])

test "Not valid" do
  refute(valid)
end

e.some_attr = 'else other thing' # causes only the second scenario to be valid

valid = Validate.(e, scenarios: [:some_particular_scenario, :some_other_scenario])

test "Not valid" do
  refute(valid)
end

Multiple Scenarios and State

State will also be used to collect contextual information across all invoked scenarios.

class Example
  attr_accessor :some_attr

  module Validate
    def self.call(example, state=[])
      state << 'All is well'
      true
    end

    def self.some_scenario
      SomeScenario
    end

    def self.some_other_scenario
      SomeOtherScenario
    end

    module SomeScenario
      def self.call(example, state=[])
        state << 'Oh oh! SomeScenario went wrong'
        false
      end
    end

    module SomeOtherScenario
      def self.call(example, state=[])
        state << 'Oh oh! SomeOtherScenario went wrong'
        false
      end
    end
  end
end

e = Example.new

state = []
valid_1 = Validate.(e, state)
valid_2 = Validate.(e, state, scenario: [:some_scenario, :some_other_scenario])

valid = valid_1 && valid_2

test "Not valid" do
  refute(valid)
end

test "Validator state is collected" do
  assert(state == [
    'All is well',
    'Oh oh! SomeScenario went wrong',
    'Oh oh! SomeOtherScenario went wrong'
  ])
end

Comparison with Validation in Rails

What sets this library apart from other similar libraries in Ruby is the lack of a DSL, or a framework for generated error messaging. This is because validation in plain ruby is quite simple and high level already, and generated error messaging tends to result in poor error messages being presented to the user.

Some common examples:

# Simple example:

# Using Rails validations
validates :name, presence: true

# Using the Validate library
state << "a name is required" if example.name.nil?


# More complex example:

# Using Rails validations
validates :size, inclusion: { in: %w(small medium large) }

# Using the Validate library
unless %w(small medium large).include?(example.size)
  state << "size must be either small, medium, or large"
end


# Yet more complex example:

# Using Rails validations
validates :quantity, numericality: { is_integer: true, greater_than_or_equal_to: 0, less_than_or_equal_to: 10 }

# Using the Validate library
quantity = example.quantity
state << "quantity is not a number" unless quantity.to_i.to_s == quantity
state << "quantity cannot be less than 0" unless quantity >= 0
state << "quantity cannot be more than 10" unless quantity <= 10

License

The validate library is released under the MIT License.