lib/lotus/validations.rb in lotus-validations-0.0.0 vs lib/lotus/validations.rb in lotus-validations-0.1.0
- old
+ new
@@ -1,7 +1,398 @@
-require "lotus/validations/version"
+require 'lotus/validations/version'
+require 'lotus/validations/errors'
+require 'lotus/validations/attribute_validator'
module Lotus
+ # Lotus::Validations is a set of lightweight validations for Ruby objects.
+ #
+ # @since 0.1.0
module Validations
- # Your code goes here...
+
+ # Override Ruby's hook for modules.
+ #
+ # @param base [Class] the target action
+ #
+ # @since 0.1.0
+ # @api private
+ #
+ # @see http://www.ruby-doc.org/core/Module.html#method-i-included
+ def self.included(base)
+ base.extend ClassMethods
+ end
+
+ # Validations DSL
+ #
+ # @since 0.1.0
+ module ClassMethods
+ # Define an attribute
+ #
+ # @param name [#to_sym] the name of the attribute
+ # @param options [Hash] optional set of validations
+ # @option options [Class] :type the Ruby type used to coerce the value
+ # @option options [TrueClass,FalseClass] :acceptance requires Ruby
+ # thruthiness of the value
+ # @option options [TrueClass,FalseClass] :confirmation requires the value
+ # to be confirmed twice
+ # @option options [#include?] :exclusion requires the value NOT be
+ # included in the given collection
+ # @option options [Regexp] :format requires value to match the given
+ # Regexp
+ # @option options [#include?] :inclusion requires the value BE included in
+ # the given collection
+ # @option options [TrueClass,FalseClass] :presence requires the value be
+ # included in the given collection
+ # @option options [Numeric,Range] :size requires value's to be equal or
+ # included by the given validator
+ #
+ # @raise [ArgumentError] if an unknown or mispelled validation is given
+ #
+ # @example Attributes
+ # require 'lotus/validations'
+ #
+ # class Person
+ # include Lotus::Validations
+ #
+ # attribute :name
+ # end
+ #
+ # person = Person.new(name: 'Luca', age: 32)
+ # person.name # => "Luca"
+ # person.age # => raises NoMethodError because `:age` wasn't defined as attribute.
+ #
+ # @example Standard coercions
+ # require 'lotus/validations'
+ #
+ # class Person
+ # include Lotus::Validations
+ #
+ # attribute :fav_number, type: Integer
+ # end
+ #
+ # person = Person.new(fav_number: '23')
+ # person.valid?
+ #
+ # person.fav_number # => 23
+ #
+ # @example Custom coercions
+ # require 'lotus/validations'
+ #
+ # class FavNumber
+ # def initialize(number)
+ # @number = number
+ # end
+ # end
+ #
+ # class BirthDate
+ # end
+ #
+ # class Person
+ # include Lotus::Validations
+ #
+ # attribute :fav_number, type: FavNumber
+ # attribute :date, type: BirthDate
+ # end
+ #
+ # person = Person.new(fav_number: '23', date: 'Oct 23, 2014')
+ # person.valid?
+ #
+ # person.fav_number # => 23
+ # person.date # => this raises an error, because BirthDate#initialize doesn't accept any arg
+ #
+ # @example Acceptance
+ # require 'lotus/validations'
+ #
+ # class Signup
+ # include Lotus::Validations
+ #
+ # attribute :terms_of_service, acceptance: true
+ # end
+ #
+ # signup = Signup.new(terms_of_service: '1')
+ # signup.valid? # => true
+ #
+ # signup = Signup.new(terms_of_service: '')
+ # signup.valid? # => false
+ #
+ # @example Confirmation
+ # require 'lotus/validations'
+ #
+ # class Signup
+ # include Lotus::Validations
+ #
+ # attribute :password, confirmation: true
+ # end
+ #
+ # signup = Signup.new(password: 'secret', password_confirmation: 'secret')
+ # signup.valid? # => true
+ #
+ # signup = Signup.new(password: 'secret', password_confirmation: 'x')
+ # signup.valid? # => false
+ #
+ # @example Exclusion
+ # require 'lotus/validations'
+ #
+ # class Signup
+ # include Lotus::Validations
+ #
+ # attribute :music, exclusion: ['pop']
+ # end
+ #
+ # signup = Signup.new(music: 'rock')
+ # signup.valid? # => true
+ #
+ # signup = Signup.new(music: 'pop')
+ # signup.valid? # => false
+ #
+ # @example Format
+ # require 'lotus/validations'
+ #
+ # class Signup
+ # include Lotus::Validations
+ #
+ # attribute :name, format: /\A[a-zA-Z]+\z/
+ # end
+ #
+ # signup = Signup.new(name: 'Luca')
+ # signup.valid? # => true
+ #
+ # signup = Signup.new(name: '23')
+ # signup.valid? # => false
+ #
+ # @example Inclusion
+ # require 'lotus/validations'
+ #
+ # class Signup
+ # include Lotus::Validations
+ #
+ # attribute :age, inclusion: 18..99
+ # end
+ #
+ # signup = Signup.new(age: 32)
+ # signup.valid? # => true
+ #
+ # signup = Signup.new(age: 17)
+ # signup.valid? # => false
+ #
+ # @example Presence
+ # require 'lotus/validations'
+ #
+ # class Signup
+ # include Lotus::Validations
+ #
+ # attribute :name, presence: true
+ # end
+ #
+ # signup = Signup.new(name: 'Luca')
+ # signup.valid? # => true
+ #
+ # signup = Signup.new(name: nil)
+ # signup.valid? # => false
+ #
+ # @example Size
+ # require 'lotus/validations'
+ #
+ # class Signup
+ # MEGABYTE = 1024 ** 2
+ # include Lotus::Validations
+ #
+ # attribute :ssn, size: 11 # exact match
+ # attribute :password, size: 8..64 # range
+ # attribute :avatar, size 1..(5 * MEGABYTE)
+ # end
+ #
+ # signup = Signup.new(password: 'a-very-long-password')
+ # signup.valid? # => true
+ #
+ # signup = Signup.new(password: 'short')
+ # signup.valid? # => false
+ def attribute(name, options = {})
+ attributes[name.to_sym] = validate_options!(name, options)
+
+ class_eval %{
+ def #{ name }
+ @attributes[:#{ name }]
+ end
+ }
+ end
+
+ private
+ # Set of user defined attributes
+ #
+ # @return [Hash]
+ #
+ # @since 0.1.0
+ # @api private
+ def attributes
+ @attributes ||= Hash.new
+ end
+
+ # Checks at the loading time if the user defined validations are recognized
+ #
+ # @param name [Symbol] the attribute name
+ # @param options [Hash] the set of validations associated with the given attribute
+ #
+ # @raise [ArgumentError] if at least one of the validations are not
+ # recognized
+ #
+ # @since 0.1.0
+ # @api private
+ def validate_options!(name, options)
+ if (unknown = (options.keys - validations)) && unknown.any?
+ raise ArgumentError.new(%(Unknown validation(s): #{ unknown.join ', ' } for "#{ name }" attribute))
+ end
+
+ options
+ end
+
+ # Names of the implemented validations
+ #
+ # @return [Array]
+ #
+ # @since 0.1.0
+ # @api private
+ def validations
+ [:presence, :acceptance, :format, :inclusion, :exclusion, :confirmation, :size, :type]
+ end
+ end
+
+ # Validation errors
+ #
+ # @return [Lotus::Validations::Errors] the set of validation errors
+ #
+ # @since 0.1.0
+ #
+ # @see Lotus::Validations::Errors
+ #
+ # @example Valid attributes
+ # require 'lotus/validations'
+ #
+ # class Signup
+ # include Lotus::Validations
+ #
+ # attribute :email, presence: true, format: /\A(.*)@(.*)\.(.*)\z/
+ # end
+ #
+ # signup = Signup.new(email: 'user@example.org')
+ # signup.valid? # => true
+ #
+ # signup.errors
+ # # => #<Lotus::Validations::Errors:0x007fd594ba9228 @errors={}>
+ #
+ # @example Invalid attributes
+ # require 'lotus/validations'
+ #
+ # class Signup
+ # include Lotus::Validations
+ #
+ # attribute :email, presence: true, format: /\A(.*)@(.*)\.(.*)\z/
+ # attribute :age, size: 18..99
+ # end
+ #
+ # signup = Signup.new(email: '', age: 17)
+ # signup.valid? # => false
+ #
+ # signup.errors
+ # # => #<Lotus::Validations::Errors:0x007fe00ced9b78
+ # # @errors={
+ # # :email=>[
+ # # #<Lotus::Validations::Error:0x007fe00cee3290 @attribute=:email, @validation=:presence, @expected=true, @actual="">,
+ # # #<Lotus::Validations::Error:0x007fe00cee31f0 @attribute=:email, @validation=:format, @expected=/\A(.*)@(.*)\.(.*)\z/, @actual="">
+ # # ],
+ # # :age=>[
+ # # #<Lotus::Validations::Error:0x007fe00cee30d8 @attribute=:age, @validation=:size, @expected=18..99, @actual=17>
+ # # ]
+ # # }>
+ attr_reader :errors
+
+ # Create a new instance with the given attributes
+ #
+ # @param attributes [#to_h] an Hash like object which contains the
+ # attributes
+ #
+ # @since 0.1.0
+ #
+ # @example Initialize with Hash
+ # require 'lotus/validations'
+ #
+ # class Signup
+ # include Lotus::Validations
+ #
+ # attribute :name
+ # end
+ #
+ # signup = Signup.new(name: 'Luca')
+ #
+ # @example Initialize with Hash like
+ # require 'lotus/validations'
+ #
+ # class Params
+ # def initialize(attributes)
+ # @attributes = Hash[*attributes]
+ # end
+ #
+ # def to_h
+ # @attributes.to_h
+ # end
+ # end
+ #
+ # class Signup
+ # include Lotus::Validations
+ #
+ # attribute :name
+ # end
+ #
+ # params = Params.new([:name, 'Luca'])
+ # signup = Signup.new(params)
+ #
+ # signup.name # => "Luca"
+ def initialize(attributes)
+ @attributes = attributes.to_h
+ @errors = Errors.new
+ end
+
+ # Checks if the current data satisfies the defined validations
+ #
+ # @return [TrueClass,FalseClass] the result of the validations
+ #
+ # @since 0.1.0
+ def valid?
+ @errors.clear
+
+ _attributes.each do |name, options|
+ AttributeValidator.new(self, name, options).validate!
+ end
+
+ @errors.empty?
+ end
+
+ protected
+ # Returns the attributes passed at the initialize time
+ #
+ # @return [Hash] attributes
+ #
+ # @since 0.1.0
+ # @api private
+ #
+ # @example
+ # require 'lotus/validations'
+ #
+ # class Signup
+ # include Lotus::Validations
+ #
+ # attribute :email
+ # end
+ #
+ # signup = Signup.new(email: 'user@example.org')
+ # signup.attributes # => {:email=>"user@example.org"}
+ attr_reader :attributes
+
+ private
+ # @since 0.1.0
+ # @api private
+ #
+ # @see Lotus::Validations::ClassMethods#attributes
+ def _attributes
+ self.class.__send__(:attributes)
+ end
end
end