lib/validator.rb in kharon-0.2.0 vs lib/validator.rb in kharon-0.3.0

- old
+ new

@@ -1,10 +1,9 @@ module Kharon - # The validator uses aquarium as an AOP DSL to provide "before" and "after" joint point to its main methods. + # The validator is the main class of Kharon, it validates a hash given a structure. # @author Vincent Courtois <vincent.courtois@mycar-innovations.com> class Validator - include Aquarium::DSL # @!attribute [r] datas # @return The datas to filter, they shouldn't be modified to guarantee their integrity. attr_reader :datas @@ -12,167 +11,194 @@ # @return The filtered datas are the datas after they have been filtered (renamed keys for example) by the validator. attr_accessor :filtered # Constructor of the classe, receiving the datas to validate and filter. # @param [Hash] datas the datas to validate in the validator. + # @example create a new instance of validator. + # @validator = Kharon::Validator.new({key: "value"}) def initialize(datas) @datas = datas @filtered = Hash.new end - # @!group Public_interface - # Checks if the given key is an integer or not. # @param [Object] key the key about which verify the type. # @param [Hash] options a hash of options passed to this method (see documentation to know which options pass). + # @example Validates a key so it has to be an integer superior or equal to 2. + # @validator.integer(:an_integer_key, min: 2) def integer(key, options = {}) - match?(key, /\A\d+\Z/) ? store(key, ->(item){item.to_i}, options) : raise_type_error(key, "Integer") + before_all(key, options) + match?(key, /\A\d+\Z/) ? store_numeric(key, ->(item){item.to_i}, options) : raise_type_error(key, "Integer") end # Checks if the given key is a numeric or not. # @param [Object] key the key about which verify the type. # @param [Hash] options a hash of options passed to this method (see documentation to know which options pass). + # @example Validates a key so it has to be a numeric, is required and is between 2 and 5.5. + # @validator.numeric(:a_numeric_key, required: true, between: [2, 5.5]) def numeric(key, options = {}) - match?(key, /\A([+-]?\d+)([,.](\d+))?\Z/) ? store(key, ->(item){item.to_s.sub(/,/, ".").to_f}, options) : raise_type_error(key, "Numeric") + before_all(key, options) + match?(key, /\A([+-]?\d+)([,.](\d+))?\Z/) ? store_decimal(key, ->(item){item.to_s.sub(/,/, ".").to_f}, options) : raise_type_error(key, "Numeric") end # Checks if the given key is a not-empty string or not. # @param [Object] key the key about which verify the type. # @param [Hash] options a hash of options passed to this method (see documentation to know which options pass). + # @example Validates a key so it has to be a string, and seems like and email address (not sure of the regular expression though). + # @validator.text(:an_email, regex: "[a-zA-Z]+@[a-zA-Z]+\.[a-zA-Z]{2-4}") def text(key, options = {}) - is_typed?(key, String) ? store(key, ->(item){item.to_s}, options) : raise_type_error(key, "String") + before_all(key, options) + is_typed?(key, String) ? store_text(key, ->(item){item.to_s}, options) : raise_type_error(key, "String") end # Doesn't check the type of the key and let it pass without modification. # @param [Object] key the key about which verify the type. # @param [Hash] options a hash of options passed to this method (see documentation to know which options pass). + # @example Just checks if the key is in the hash. + # @validator.any(:a_key, required: true) def any(key, options = {}) + before_all(key, options) store(key, ->(item){item}, options) end # Checks if the given key is a datetime or not. # @param [Object] key the key about which verify the type. # @param [Hash] options a hash of options passed to this method (see documentation to know which options pass). + # @example Validates a key so it has to be a datetime, and depends on two other keys. + # @validator.datetime(:a_datetime, dependencies: [:another_key, :a_third_key]) def datetime(key, options = {}) + before_all(key, options) begin; store(key, ->(item){DateTime.parse(item.to_s)} , options); rescue; raise_type_error(key, "DateTime"); end end # Checks if the given key is a date or not. # @param [Object] key the key about which verify the type. # @param [Hash] options a hash of options passed to this method (see documentation to know which options pass). + # @example Validates a key so it has to be a date, and depends on another key. + # @validator.date(:a_date, dependency: :another_key) def date(key, options = {}) + before_all(key, options) begin; store(key, ->(item){Date.parse(item.to_s)}, options); rescue; raise_type_error(key, "Date"); end end # Checks if the given key is an array or not. # @param [Object] key the key about which verify the type. # @param [Hash] options a hash of options passed to this method (see documentation to know which options pass). + # @example Validates a key so it has to be an array, and checks if it has some values in it. + # @validator.date(:an_array, contains?: ["first", "second"]) def array(key, options = {}) - is_typed?(key, Array) ? store(key, ->(item){item.to_a}, options) : raise_type_error(key, "Array") + before_all(key, options) + is_typed?(key, Array) ? store_array(key, ->(item){item.to_a}, options) : raise_type_error(key, "Array") end # Checks if the given key is a hash or not. # @param [Object] key the key about which verify the type. # @param [Hash] options a hash of options passed to this method (see documentation to know which options pass). + # @example Validates a key so it has to be a hash, and checks if it has some keys. + # @validator.date(:a_hash, has_keys: [:first, :second]) def hash(key, options = {}) - is_typed?(key, Hash) ? store(key, ->(item){Hash.try_convert(item)}, options) : raise_type_error(key, "Hash") + before_all(key, options) + is_typed?(key, Hash) ? store_hash(key, ->(item){Hash.try_convert(item)}, options) : raise_type_error(key, "Hash") end # Checks if the given key is a boolean or not. # @param [Object] key the key about which verify the type. # @param [Hash] options a hash of options passed to this method (see documentation to know which options pass). + # @example Validates a key so it has to be a boolean. + # @validator.boolean(:a_boolean) def boolean(key, options = {}) + before_all(key, options) match?(key, /(true)|(false)/) ? store(key, ->(item){to_boolean(item)}, options) : raise_type_error(key, "Numeric") end # Checks if the given key is a SSID for a MongoDB object or not. # @param [Object] key the key about which verify the type. # @param [Hash] options a hash of options passed to this method (see documentation to know which options pass). + # @example Validates a key so it has to be a MongoDB SSID. + # @validator.ssid(:a_ssid) def ssid(key, options = {}) + before_all(key, options) match?(key, /^[0-9a-fA-F]{24}$/) ? store(key, ->(item){BSON::ObjectId.from_string(item.to_s)}, options) : raise_type_error(key, "Moped::BSON::ObjectId") end - # @!endgroup Public_interface + private - # @!group Advices - - # Before advice checking for "required", "dependency", and "dependencies" options. - before calls_to: self.instance_methods(false), exclude_methods: [:initialize, :new, :required, :dependency, :dependencies] do |joint_point, validator, *args| - unless !defined?(args[1]) or args[1].nil? or args[1].empty? - validator.required(args[0]) if (args[1].has_key?(:required) and args[1][:required] == true) - if args[1].has_key?(:dependencies) - validator.dependencies(args[0], args[1][:dependencies]) - elsif args[1].has_key?(:dependency) - validator.dependency(args[0], args[1][:dependency]) - end + # This method is executed before any call to a public method. + # @param [Object] key the key associated with the value currently filteres in the filtered datas. + # @param [Hash] options the options applied to the initial value. + def before_all(key, options) + required(key) if (options.has_key?(:required) and options[:required] == true) + if options.has_key?(:dependencies) + dependencies(key, options[:dependencies]) + elsif options.has_key?(:dependency) + dependency(key, options[:dependency]) end end - # After advice checking in numerics if limits are given, and if there are, if they are respected. - before calls_to: [:integer, :numeric] do |joint_point, validator, *args| - unless !defined?(args[1]) or args[1].nil? or args[1].empty? - if(args[1].has_key?(:between)) - validator.check_min_value(args[0], args[1][:between][0]) - validator.check_max_value(args[0], args[1][:between][1]) - else - validator.check_min_value(args[0], args[1][:min]) if(args[1].has_key?(:min)) - validator.check_max_value(args[0], args[1][:max]) if(args[1].has_key?(:max)) - end + # Stores a numeric number after checking its limits if given. + # @param [Object] key the key associated with the value to store in the filtered datas. + # @param [Proc] process a process (lambda) to execute on the initial value. Must contain strictly one argument. + # @param [Hash] options the options applied to the initial value. + def store_numeric(key, process, options) + if(options.has_key?(:between)) + check_min_value(key, options[:between][0]) + check_max_value(key, options[:between][1]) + else + check_min_value(key, options[:min]) if(options.has_key?(:min)) + check_max_value(key, options[:max]) if(options.has_key?(:max)) end + store(key, process, options) end - after calls_to: [:numeric] do |joint_point, validator, *args| - unless !defined?(args[1]) or args[1].nil? or args[1].empty? - if(args[1].has_key?(:round) and args[1][:round].kind_of?(Integer)) - validator.filtered[args[0]] = validator.filtered[args[0]].round(args[1][:round]) if validator.filtered.has_key?(args[0]) - elsif(args[1].has_key?(:floor) and args[1][:floor] == true) - validator.filtered[args[0]] = validator.filtered[args[0]].floor if validator.filtered.has_key?(args[0]) - elsif(args[1].has_key?(:ceil) and args[1][:ceil] == true) - validator.filtered[args[0]] = validator.filtered[args[0]].ceil if validator.filtered.has_key?(args[0]) - end + # Stores a decimal number, then apply the eventually passed round, ceil, or floor options. + # @param [Object] key the key associated with the value to store in the filtered datas. + # @param [Proc] process a process (lambda) to execute on the initial value. Must contain strictly one argument. + # @param [Hash] options the options applied to the initial value. + def store_decimal(key, process, options) + store_numeric(key, process, options) + if(options.has_key?(:round) and options[:round].kind_of?(Integer)) + filtered[key] = filtered[key].round(options[:round]) if filtered.has_key?(key) + elsif(options.has_key?(:floor) and options[:floor] == true) + filtered[key] = filtered[key].floor if filtered.has_key?(key) + elsif(options.has_key?(:ceil) and options[:ceil] == true) + filtered[key] = filtered[key].ceil if filtered.has_key?(key) end end - # After advcie for all methods, checking the "in" and "equals" options. - after calls_to: self.instance_methods(false), exclude_methods: [:initialize, :new, :required, :dependency, :dependencies] do |joint_point, validator, *args| - unless !defined?(args[1]) or args[1].nil? or args[1].empty? - if(args[1].has_key?(:in)) - validator.in_array?(args[0], args[1][:in]) - elsif(args[1].has_key?(:equals)) - validator.equals_to?(args[0], args[1][:equals]) - end - end + # Stores a hash after checking for the contains? and has_keys options. + # @param [Object] key the key associated with the value to store in the filtered datas. + # @param [Proc] process a process (lambda) to execute on the initial value. Must contain strictly one argument. + # @param [Hash] options the options applied to the initial value. + def store_hash(key, process, options) + has_keys?(key, options[:has_keys]) if(options.has_key?(:has_keys)) + contains?(filtered, datas[key].values, options[:contains]) if(options.has_key?(:contains)) + store(key, process, options) end - # After advice for hashes, checking the "has_keys" and "contains" options. - after calls_to: [:hash] do |joint_point, validator, *args| - unless !defined?(args[1]) or args[1].nil? or args[1].empty? - validator.has_keys?(args[0], args[1][:has_keys]) if(args[1].has_key?(:has_keys)) - validator.contains?(args[0], validator.datas[args[0]].values, args[1][:contains]) if(args[1].has_key?(:contains)) - end + # Stores an array after verifying that it contains the values given in the contains? option. + # @param [Object] key the key associated with the value to store in the filtered datas. + # @param [Proc] process a process (lambda) to execute on the initial value. Must contain strictly one argument. + # @param [Hash] options the options applied to the initial value. + def store_array(key, process, options) + contains?(key, datas[key], options[:contains]) if(options.has_key?(:contains)) + store(key, process, options) end - # After advice for arrays, checking the "contains" option. - after calls_to: [:array] do |joint_point, validator, *args| - unless !defined?(args[1]) or args[1].nil? or args[1].empty? - validator.contains?(args[0], validator.datas[args[0]], args[1][:contains]) if(args[1].has_key?(:contains)) - end + # Stores a string after verifying that it respects a regular expression given in parameter. + # @param [Object] key the key associated with the value to store in the filtered datas. + # @param [Proc] process a process (lambda) to execute on the initial value. Must contain strictly one argument. + # @param [Hash] options the options applied to the initial value. + def store_text(key, process, options) + match_regex?(key, datas[key], options[:regex]) if(options.has_key?(:regex)) + store(key, process, options) end - after calls_to: [:text] do |joint_point, validator, *args| - unless !defined?(args[1]) or args[1].nil? or args[1].empty? - validator.match_regex?(args[0], validator.datas[args[0]], args[1][:regex]) if(args[1].has_key?(:regex)) - end - end - - # @!endgroup Advices - # Checks if a required key is present in provided datas. # @param [Object] key the key of which check the presence. # @raise [ArgumentError] if the key is not present. def required(key) - raise ArgumentError.new("The key #{key} is required and not provided.") unless @datas.has_key?(key) + raise_error("The key #{key} is required and not provided.") unless @datas.has_key?(key) end # Syntaxic sugar used to chack several dependencies at once. # @param [Object] key the key needing another key to properly work. # @param [Object] dependency the key needed by another key for it to properly work. @@ -185,68 +211,66 @@ # Checks if a dependency is respected. A dependency is a key openly needed by another key. # @param [Object] key the key needing another key to properly work. # @param [Object] dependency the key needed by another key for it to properly work. # @raise [ArgumentError] if the required dependency is not present. def dependency(key, dependency) - raise ArgumentError.new("The key #{key} needs the key #{dependency} but it was not provided.") unless @datas.has_key?(dependency) + raise_error("The key #{key} needs the key #{dependency} but it was not provided.") unless @datas.has_key?(dependency) end # Checks if the value associated with the given key is greater than the given minimum value. # @param [Object] key the key associated with the value to compare. # @param [Numeric] min_value the required minimum value. # @raise [ArgumentError] if the initial value is strictly lesser than the minimum value. def check_min_value(key, min_value) - raise ArgumentError.new("The key #{key} was supposed to be greater or equal than #{min_value}, the value was #{datas[key]}") unless datas[key].to_i >= min_value.to_i + raise_error("The key #{key} was supposed to be greater or equal than #{min_value}, the value was #{datas[key]}") unless datas[key].to_i >= min_value.to_i end # Checks if the value associated with the given key is lesser than the given maximum value. # @param [Object] key the key associated with the value to compare. # @param [Numeric] max_value the required maximum value. # @raise [ArgumentError] if the initial value is strictly greater than the minimum value. def check_max_value(key, max_value) - raise ArgumentError.new("The key #{key} was supposed to be lesser or equal than #{max_value}, the value was #{datas[key]}") unless datas[key].to_i <= max_value.to_i + raise_error("The key #{key} was supposed to be lesser or equal than #{max_value}, the value was #{datas[key]}") unless datas[key].to_i <= max_value.to_i end # Checks if the value associated with the given key is included in the given array of values. # @param [Object] key the key associated with the value to check. # @param [Array] values the values in which the initial value should be contained. # @raise [ArgumentError] if the initial value is not included in the given possible values. def in_array?(key, values) - raise ArgumentError.new("The key #{key} was supposed to be in [#{values.join(", ")}], the value was #{datas[key]}") unless (values.empty? or values.include?(datas[key])) + raise_error("The key #{key} was supposed to be in [#{values.join(", ")}], the value was #{datas[key]}") unless (values.empty? or values.include?(datas[key])) end # Checks if the value associated with the given key is equal to the given value. # @param [Object] key the key associated with the value to check. # @param [Object] value the values with which the initial value should be compared. # @raise [ArgumentError] if the initial value is not equal to the given value. def equals_to?(key, value) - raise ArgumentError.new("The key #{key} was supposed to equal than #{value}, the value was #{datas[key]}") unless datas[key] == value + raise_error("The key #{key} was supposed to equal than #{value}, the value was #{datas[key]}") unless datas[key] == value end # Checks if the value associated with the given key has the given required keys. # @param [Object] key the key associated with the value to check. # @param [Array] required_keys the keys that the initial Hash typed value should contain. # @raise [ArgumentError] if the initial value has not each and every one of the given keys. def has_keys?(key, required_keys) - raise ArgumentError.new("The key #{key} was supposed to contains keys [#{required_keys.join(", ")}]") if (datas[key].keys & required_keys) != required_keys + raise_error("The key #{key} was supposed to contains keys [#{required_keys.join(", ")}]") if (datas[key].keys & required_keys) != required_keys end # Checks if the value associated with the given key has the given required values. # @param [Object] key the key associated with the value to check. # @param [Array] required_keys the values that the initial Enumerable typed value should contain. # @raise [ArgumentError] if the initial value has not each and every one of the given values. def contains?(key, values, required_values) - raise ArgumentError.new("The key #{key} was supposed to contains values [#{required_values.join(", ")}]") if (values & required_values) != required_values + raise_error("The key #{key} was supposed to contains values [#{required_values.join(", ")}]") if (values & required_values) != required_values end def match_regex?(key, value, regex) regex = Regexp.new(regex) if regex.kind_of?(String) - raise ArgumentError.new("The key #{key} was supposed to match the regex #{regex} but its value was #{value}") unless regex.match(value) + raise_error("The key #{key} was supposed to match the regex #{regex} but its value was #{value}") unless regex.match(value) end - private - # Check if the value associated with the given key matches the given regular expression. # @param [Object] key the key of the value to compare with the given regexp. # @param [Regexp] regex the regex with which match the initial value. # @return [Boolean] true if the initial value matches the regex, false if not. def match?(key, regex) @@ -259,35 +283,46 @@ # @return [Boolean] true if the initial value is from the right type, false if not. def is_typed?(key, type) return (!datas.has_key?(key) or datas[key].kind_of?(type)) end + # Transforms a given value in a boolean. + # @param [Object] value the value to transform into a boolean. + # @return [Boolean] true if the value was true, 1 or yes, false if not. + def to_boolean(value) + ["true", "1", "yes"].include?(value.to_s) ? true : false + end + # Tries to store the associated key in the filtered key, transforming it with the given process. # @param [Object] key the key associated with the value to store in the filtered datas. # @param [Proc] process a process (lambda) to execute on the initial value. Must contain strictly one argument. - # @param [Hash] options the options applied to the initial value. Only the option "rename" is checked and executed here. + # @param [Hash] options the options applied to the initial value. def store(key, process, options = {}) unless (options.has_key?(:extract) and options[:extract] == false) if datas.has_key?(key) value = ((options.has_key?(:cast) and options[:cast] == false) ? datas[key] : process.call(datas[key])) + if(options.has_key?(:in)) + in_array?(key, options[:in]) + elsif(options.has_key?(:equals)) + equals_to?(key, options[:equals]) + end options.has_key?(:rename) ? (@filtered[options[:rename]] = value) : (@filtered[key] = value) end end end # Raises a type error with a generic message. # @param [Object] key the key associated from the value triggering the error. # @param [Class] type the expected type, not respected by the initial value. # @raise [ArgumentError] the chosen type error. def raise_type_error(key, type) - raise ArgumentError.new("The key {key} was supposed to be an instance of #{type}, #{key.class} found.") + raise_error("The key {key} was supposed to be an instance of #{type}, #{key.class} found.") end - # Transforms a given value in a boolean. - # @param [Object] value the value to transform into a boolean. - # @return [Boolean] true if the value was true, 1 or yes, false if not. - def to_boolean(value) - ["true", "1", "yes"].include?(value.to_s) ? true : false + protected + + def raise_error(message) + raise ArgumentError.new(message) end end end \ No newline at end of file