├── .rspec ├── lib ├── compel │ ├── version.rb │ ├── exceptions │ │ ├── type_error.rb │ │ └── invalid_object_error.rb │ ├── coercion │ │ ├── types │ │ │ ├── any.rb │ │ │ ├── float.rb │ │ │ ├── integer.rb │ │ │ ├── json.rb │ │ │ ├── array.rb │ │ │ ├── regexp.rb │ │ │ ├── string.rb │ │ │ ├── time.rb │ │ │ ├── date.rb │ │ │ ├── datetime.rb │ │ │ ├── boolean.rb │ │ │ ├── hash.rb │ │ │ ├── type.rb │ │ │ └── date_type.rb │ │ ├── nil_result.rb │ │ ├── result.rb │ │ └── coercion.rb │ ├── builder │ │ ├── any.rb │ │ ├── json.rb │ │ ├── float.rb │ │ ├── integer.rb │ │ ├── boolean.rb │ │ ├── time.rb │ │ ├── date.rb │ │ ├── datetime.rb │ │ ├── array.rb │ │ ├── hash.rb │ │ ├── schema.rb │ │ ├── string.rb │ │ ├── methods.rb │ │ ├── common.rb │ │ └── common_value.rb │ ├── validation │ │ ├── conditions │ │ │ ├── min.rb │ │ │ ├── max.rb │ │ │ ├── format.rb │ │ │ ├── in.rb │ │ │ ├── length.rb │ │ │ ├── is.rb │ │ │ ├── min_length.rb │ │ │ ├── max_length.rb │ │ │ ├── if.rb │ │ │ └── condition.rb │ │ ├── result.rb │ │ └── validation.rb │ ├── result.rb │ ├── contract.rb │ ├── validators │ │ ├── base.rb │ │ ├── type_validator.rb │ │ ├── array_validator.rb │ │ └── hash_validator.rb │ └── errors.rb └── compel.rb ├── .travis.yml ├── Rakefile ├── Gemfile ├── .gitignore ├── spec ├── spec_helper.rb ├── support │ └── sinatra_app.rb └── compel │ ├── errors_spec.rb │ ├── sinatra_integration_spec.rb │ ├── validation_spec.rb │ ├── coercion_spec.rb │ ├── compel_spec.rb │ └── builder_spec.rb ├── compel.gemspec ├── LICENSE └── README.md /.rspec: -------------------------------------------------------------------------------- 1 | --color 2 | --format documentation 3 | --require spec_helper 4 | -------------------------------------------------------------------------------- /lib/compel/version.rb: -------------------------------------------------------------------------------- 1 | module Compel 2 | VERSION = '0.5.1' 3 | end 4 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: ruby 2 | rvm: 3 | - 2.2.0 4 | - 2.2.3 5 | - 2.3.0 6 | -------------------------------------------------------------------------------- /lib/compel/exceptions/type_error.rb: -------------------------------------------------------------------------------- 1 | module Compel 2 | 3 | class TypeError < StandardError 4 | 5 | end 6 | 7 | end 8 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require 'bundler/gem_tasks' 2 | require 'rspec/core/rake_task' 3 | 4 | task :default => :test 5 | RSpec::Core::RakeTask.new(:test) 6 | -------------------------------------------------------------------------------- /lib/compel/exceptions/invalid_object_error.rb: -------------------------------------------------------------------------------- 1 | module Compel 2 | 3 | class InvalidObjectError < StandardError 4 | 5 | attr_accessor :object 6 | 7 | end 8 | 9 | end 10 | -------------------------------------------------------------------------------- /lib/compel/coercion/types/any.rb: -------------------------------------------------------------------------------- 1 | module Compel 2 | module Coercion 3 | 4 | class Any < Type 5 | 6 | def coerce_value 7 | value 8 | end 9 | 10 | end 11 | 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /lib/compel/builder/any.rb: -------------------------------------------------------------------------------- 1 | module Compel 2 | module Builder 3 | 4 | class Any < Schema 5 | 6 | def initialize 7 | super(Coercion::Any) 8 | end 9 | 10 | end 11 | 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /lib/compel/builder/json.rb: -------------------------------------------------------------------------------- 1 | module Compel 2 | module Builder 3 | 4 | class JSON < Schema 5 | 6 | def initialize 7 | super(Coercion::JSON) 8 | end 9 | 10 | end 11 | 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | gemspec 4 | 5 | group :test do 6 | gem 'codeclimate-test-reporter' 7 | gem 'simplecov', require: false 8 | gem 'rack-test' 9 | gem 'sinatra', require: false 10 | end 11 | -------------------------------------------------------------------------------- /lib/compel/coercion/nil_result.rb: -------------------------------------------------------------------------------- 1 | module Compel 2 | module Coercion 3 | 4 | class NilResult < Result 5 | 6 | def initialize 7 | super(nil, nil, nil) 8 | end 9 | 10 | end 11 | 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /lib/compel/coercion/types/float.rb: -------------------------------------------------------------------------------- 1 | module Compel 2 | module Coercion 3 | 4 | class Float < Type 5 | 6 | def coerce_value 7 | Float(value) rescue nil 8 | end 9 | 10 | end 11 | 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /lib/compel/coercion/types/integer.rb: -------------------------------------------------------------------------------- 1 | module Compel 2 | module Coercion 3 | 4 | class Integer < Type 5 | 6 | def coerce_value 7 | Integer(value) rescue nil 8 | end 9 | 10 | end 11 | 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /lib/compel/builder/float.rb: -------------------------------------------------------------------------------- 1 | module Compel 2 | module Builder 3 | 4 | class Float < Schema 5 | 6 | include CommonValue 7 | 8 | def initialize 9 | super(Coercion::Float) 10 | end 11 | 12 | end 13 | 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /lib/compel/coercion/types/json.rb: -------------------------------------------------------------------------------- 1 | require 'json' 2 | 3 | module Compel 4 | module Coercion 5 | 6 | class JSON < Type 7 | 8 | def coerce_value 9 | ::JSON.parse(value) rescue nil 10 | end 11 | 12 | end 13 | 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /lib/compel/builder/integer.rb: -------------------------------------------------------------------------------- 1 | module Compel 2 | module Builder 3 | 4 | class Integer < Schema 5 | 6 | include CommonValue 7 | 8 | def initialize 9 | super(Coercion::Integer) 10 | end 11 | 12 | end 13 | 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /lib/compel/coercion/types/array.rb: -------------------------------------------------------------------------------- 1 | module Compel 2 | module Coercion 3 | 4 | class Array < Type 5 | 6 | def coerce_value 7 | if value.is_a?(::Array) 8 | value 9 | end 10 | end 11 | 12 | end 13 | 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /lib/compel/coercion/types/regexp.rb: -------------------------------------------------------------------------------- 1 | module Compel 2 | module Coercion 3 | 4 | class Regexp < Type 5 | 6 | def coerce_value 7 | if value.is_a?(::Regexp) 8 | value 9 | end 10 | end 11 | 12 | end 13 | 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /lib/compel/coercion/types/string.rb: -------------------------------------------------------------------------------- 1 | module Compel 2 | module Coercion 3 | 4 | class String < Type 5 | 6 | def coerce_value 7 | if value.is_a?(::String) 8 | value 9 | end 10 | end 11 | 12 | end 13 | 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /lib/compel/coercion/types/time.rb: -------------------------------------------------------------------------------- 1 | module Compel 2 | module Coercion 3 | 4 | class Time < DateType 5 | 6 | def klass 7 | ::Time 8 | end 9 | 10 | def default_format 11 | '%FT%T' 12 | end 13 | 14 | end 15 | 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /lib/compel/coercion/types/date.rb: -------------------------------------------------------------------------------- 1 | module Compel 2 | module Coercion 3 | 4 | class Date < DateType 5 | 6 | def klass 7 | ::Date 8 | end 9 | 10 | def default_format 11 | '%Y-%m-%d' 12 | end 13 | 14 | end 15 | 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.gem 2 | *.rbc 3 | .bundle 4 | .config 5 | .yardoc 6 | .rvmrc 7 | .ruby-version 8 | .ruby-gemset 9 | Gemfile.lock 10 | InstalledFiles 11 | _yardoc 12 | coverage 13 | doc/ 14 | lib/bundler/man 15 | pkg 16 | rdoc 17 | spec/reports 18 | test/tmp 19 | test/version_tmp 20 | tmp 21 | -------------------------------------------------------------------------------- /lib/compel/coercion/types/datetime.rb: -------------------------------------------------------------------------------- 1 | module Compel 2 | module Coercion 3 | 4 | class DateTime < DateType 5 | 6 | def klass 7 | ::DateTime 8 | end 9 | 10 | def default_format 11 | '%FT%T' 12 | end 13 | 14 | end 15 | 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /lib/compel/builder/boolean.rb: -------------------------------------------------------------------------------- 1 | module Compel 2 | module Builder 3 | 4 | class Boolean < Schema 5 | 6 | def initialize 7 | super(Coercion::Boolean) 8 | end 9 | 10 | def is(value, options = {}) 11 | Coercion.coerce!(value, Coercion::Boolean) 12 | 13 | build_option :is, value, options 14 | end 15 | 16 | end 17 | 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /lib/compel/coercion/types/boolean.rb: -------------------------------------------------------------------------------- 1 | module Compel 2 | module Coercion 3 | 4 | class Boolean < Type 5 | 6 | def coerce_value 7 | if /(false|f|no|n|0)$/i === "#{value}" 8 | return false 9 | end 10 | 11 | if /(true|t|yes|y|1)$/i === "#{value}" 12 | return true 13 | end 14 | end 15 | 16 | end 17 | 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /lib/compel/validation/conditions/min.rb: -------------------------------------------------------------------------------- 1 | module Compel 2 | module Validation 3 | 4 | class Min < Condition 5 | 6 | def validate_value 7 | unless valid? 8 | "cannot be less than #{option_value}" 9 | end 10 | end 11 | 12 | private 13 | 14 | def valid? 15 | !value.nil? && value >= option_value 16 | end 17 | 18 | end 19 | 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /lib/compel/validation/conditions/max.rb: -------------------------------------------------------------------------------- 1 | module Compel 2 | module Validation 3 | 4 | class Max < Condition 5 | 6 | def validate_value 7 | unless valid? 8 | "cannot be greater than #{option_value}" 9 | end 10 | end 11 | 12 | private 13 | 14 | def valid? 15 | !value.nil? && value <= option_value 16 | end 17 | 18 | end 19 | 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /lib/compel/validation/conditions/format.rb: -------------------------------------------------------------------------------- 1 | module Compel 2 | module Validation 3 | 4 | class Format < Condition 5 | 6 | def validate_value 7 | if value_type == Coercion::String && !valid? 8 | "must match format #{option_value.source}" 9 | end 10 | end 11 | 12 | def valid? 13 | !value.nil? && value =~ option_value 14 | end 15 | 16 | end 17 | 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /lib/compel/validation/conditions/in.rb: -------------------------------------------------------------------------------- 1 | module Compel 2 | module Validation 3 | 4 | class In < Condition 5 | 6 | def validate_value 7 | unless valid? 8 | "must be within #{option_value}" 9 | end 10 | end 11 | 12 | private 13 | 14 | def valid? 15 | option_value.include?(value) 16 | end 17 | 18 | end 19 | 20 | class Range < In; end 21 | 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /lib/compel/validation/conditions/length.rb: -------------------------------------------------------------------------------- 1 | module Compel 2 | module Validation 3 | 4 | class Length < Condition 5 | 6 | def validate_value 7 | unless valid? 8 | "cannot have length different than #{option_value}" 9 | end 10 | end 11 | 12 | private 13 | 14 | def valid? 15 | !value.nil? && option_value == "#{value}".length 16 | end 17 | 18 | end 19 | 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | require 'simplecov' 2 | require 'codeclimate-test-reporter' 3 | 4 | SimpleCov.start do 5 | formatter SimpleCov::Formatter::MultiFormatter.new [ 6 | SimpleCov::Formatter::HTMLFormatter, 7 | CodeClimate::TestReporter::Formatter 8 | ] 9 | end 10 | 11 | require 'compel' 12 | 13 | require 'rack/test' 14 | require 'support/sinatra_app' 15 | 16 | def app 17 | SinatraApp 18 | end 19 | 20 | include Rack::Test::Methods 21 | -------------------------------------------------------------------------------- /lib/compel/builder/time.rb: -------------------------------------------------------------------------------- 1 | module Compel 2 | module Builder 3 | 4 | class Time < Schema 5 | 6 | include CommonValue 7 | 8 | def initialize 9 | super(Coercion::Time) 10 | end 11 | 12 | def format(value, options = {}) 13 | build_option :format, value, options 14 | end 15 | 16 | def iso8601(options = {}) 17 | build_option :format, '%FT%T', options 18 | end 19 | 20 | end 21 | 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /lib/compel/builder/date.rb: -------------------------------------------------------------------------------- 1 | module Compel 2 | module Builder 3 | 4 | class Date < Schema 5 | 6 | include CommonValue 7 | 8 | def initialize 9 | super(Coercion::Date) 10 | end 11 | 12 | def format(value, options = {}) 13 | build_option :format, value, options 14 | end 15 | 16 | def iso8601(options = {}) 17 | build_option :format, '%Y-%m-%d', options 18 | end 19 | 20 | end 21 | 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /lib/compel/builder/datetime.rb: -------------------------------------------------------------------------------- 1 | module Compel 2 | module Builder 3 | 4 | class DateTime < Schema 5 | 6 | include CommonValue 7 | 8 | def initialize 9 | super(Coercion::DateTime) 10 | end 11 | 12 | def format(value, options = {}) 13 | build_option :format, value, options 14 | end 15 | 16 | def iso8601(options = {}) 17 | build_option :format, '%FT%T', options 18 | end 19 | 20 | end 21 | 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /lib/compel/validation/result.rb: -------------------------------------------------------------------------------- 1 | module Compel 2 | module Validation 3 | 4 | class Result 5 | 6 | attr_reader :value, 7 | :klass, 8 | :error_message 9 | 10 | def initialize(value, klass, error_message = nil) 11 | @value = value 12 | @klass = klass 13 | @error_message = error_message 14 | end 15 | 16 | def valid? 17 | @error_message.nil? 18 | end 19 | 20 | end 21 | 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /lib/compel/coercion/types/hash.rb: -------------------------------------------------------------------------------- 1 | module Compel 2 | module Coercion 3 | 4 | class Hash < Type 5 | 6 | def coerce_value 7 | if ::Hash.try_convert(value) 8 | symbolyze_keys(value) 9 | end 10 | end 11 | 12 | private 13 | 14 | def symbolyze_keys(hash) 15 | {}.tap do |symbolyzed_hash| 16 | hash.each do |key, value| 17 | symbolyzed_hash[key.to_sym] = value 18 | end 19 | end 20 | end 21 | end 22 | 23 | end 24 | end 25 | -------------------------------------------------------------------------------- /lib/compel/validation/conditions/is.rb: -------------------------------------------------------------------------------- 1 | module Compel 2 | module Validation 3 | 4 | class Is < Condition 5 | 6 | def option_value 7 | if value_type == Coercion::Hash 8 | return @option_value.to_hash 9 | end 10 | 11 | @option_value 12 | end 13 | 14 | def validate_value 15 | unless valid? 16 | "must be #{option_value}" 17 | end 18 | end 19 | 20 | private 21 | 22 | def valid? 23 | value == option_value 24 | end 25 | 26 | end 27 | 28 | end 29 | end 30 | -------------------------------------------------------------------------------- /lib/compel/builder/array.rb: -------------------------------------------------------------------------------- 1 | module Compel 2 | module Builder 3 | 4 | class Array < Schema 5 | 6 | def initialize 7 | super(Coercion::Array) 8 | end 9 | 10 | def items(schema, options = {}) 11 | if !schema.is_a?(Schema) 12 | raise Compel::TypeError, '#items must be a valid Schema' 13 | end 14 | 15 | build_option :items, schema, options 16 | end 17 | 18 | def is(value) 19 | build_option :is, Coercion.coerce!(value, Coercion::Array) 20 | end 21 | 22 | end 23 | 24 | end 25 | end 26 | -------------------------------------------------------------------------------- /lib/compel/result.rb: -------------------------------------------------------------------------------- 1 | module Compel 2 | 3 | class Result 4 | 5 | attr_reader :value, :errors 6 | 7 | def initialize(validator) 8 | @valid = validator.valid? 9 | @value = validator.serialize 10 | @errors = validator.serialize_errors 11 | end 12 | 13 | def valid? 14 | @valid 15 | end 16 | 17 | def raise? 18 | if !valid? 19 | exception = InvalidObjectError.new 20 | exception.object = value 21 | 22 | raise exception, 'object has errors' 23 | end 24 | 25 | value 26 | end 27 | 28 | end 29 | 30 | end 31 | -------------------------------------------------------------------------------- /lib/compel/validation/conditions/min_length.rb: -------------------------------------------------------------------------------- 1 | module Compel 2 | module Validation 3 | 4 | class MinLength < Condition 5 | 6 | def validate_value 7 | unless valid? 8 | "cannot have length less than #{option_value}" 9 | end 10 | end 11 | 12 | private 13 | 14 | def valid? 15 | unless value.nil? 16 | _value = value.is_a?(Array) || value.is_a?(Hash) ? value.dup : "#{value}" 17 | 18 | return option_value <= _value.length 19 | end 20 | 21 | true 22 | end 23 | 24 | end 25 | 26 | end 27 | end 28 | -------------------------------------------------------------------------------- /lib/compel/validation/conditions/max_length.rb: -------------------------------------------------------------------------------- 1 | module Compel 2 | module Validation 3 | 4 | class MaxLength < Condition 5 | 6 | def validate_value 7 | unless valid? 8 | "cannot have length greater than #{option_value}" 9 | end 10 | end 11 | 12 | private 13 | 14 | def valid? 15 | unless value.nil? 16 | _value = value.is_a?(Array) || value.is_a?(Hash) ? value.dup : "#{value}" 17 | 18 | return _value.length <= option_value 19 | end 20 | 21 | true 22 | end 23 | 24 | end 25 | 26 | end 27 | end 28 | -------------------------------------------------------------------------------- /lib/compel/contract.rb: -------------------------------------------------------------------------------- 1 | module Compel 2 | 3 | class Contract 4 | 5 | attr_reader :object, :schema 6 | 7 | def initialize(object, schema) 8 | @object = object 9 | @schema = schema 10 | end 11 | 12 | def validate 13 | Result.new(setup!.validate) 14 | end 15 | 16 | private 17 | 18 | def setup! 19 | validator_klass.new(object, schema) 20 | end 21 | 22 | def validator_klass 23 | if schema.type == Coercion::Hash 24 | Validators::HashValidator 25 | elsif schema.type == Coercion::Array 26 | Validators::ArrayValidator 27 | else 28 | Validators::TypeValidator 29 | end 30 | end 31 | 32 | end 33 | 34 | end 35 | -------------------------------------------------------------------------------- /lib/compel.rb: -------------------------------------------------------------------------------- 1 | require 'time' 2 | 3 | require 'compel/exceptions/type_error' 4 | require 'compel/exceptions/invalid_object_error' 5 | 6 | require 'compel/validators/base' 7 | require 'compel/builder/methods' 8 | require 'compel/coercion/coercion' 9 | require 'compel/validation/validation' 10 | 11 | require 'compel/result' 12 | require 'compel/errors' 13 | require 'compel/contract' 14 | 15 | module Compel 16 | 17 | extend Builder::Methods 18 | 19 | def self.run!(params, schema) 20 | Contract.new(params, schema).validate.raise? 21 | end 22 | 23 | def self.run?(params, schema) 24 | Contract.new(params, schema).validate.valid? 25 | end 26 | 27 | def self.run(params, schema) 28 | Contract.new(params, schema).validate 29 | end 30 | 31 | end 32 | -------------------------------------------------------------------------------- /lib/compel/validators/base.rb: -------------------------------------------------------------------------------- 1 | module Compel 2 | module Validators 3 | 4 | class Base 5 | 6 | attr_reader :input, 7 | :output, 8 | :errors, 9 | :schema 10 | 11 | def initialize(input, schema) 12 | @input = input.nil? ? schema.default_value : input 13 | @schema = schema 14 | @output = nil 15 | @errors = [] 16 | end 17 | 18 | def valid? 19 | @errors.empty? 20 | end 21 | 22 | def self.validate(input, schema) 23 | new(input, schema).validate 24 | end 25 | 26 | end 27 | 28 | end 29 | end 30 | 31 | require 'compel/validators/type_validator' 32 | require 'compel/validators/hash_validator' 33 | require 'compel/validators/array_validator' 34 | -------------------------------------------------------------------------------- /lib/compel/coercion/result.rb: -------------------------------------------------------------------------------- 1 | module Compel 2 | module Coercion 3 | 4 | class Result 5 | 6 | attr_reader :coerced, 7 | :value, 8 | :klass, 9 | :error 10 | 11 | def initialize(coerced, value, klass, coercion_error = nil) 12 | @coerced = coerced 13 | @value = value 14 | @klass = klass 15 | @error = coercion_error.nil? ? standard_error : coercion_error 16 | end 17 | 18 | def valid? 19 | @error.nil? 20 | end 21 | 22 | private 23 | 24 | def standard_error 25 | if !klass.nil? && coerced.nil? 26 | "'#{value}' is not a valid #{klass_final_type}" 27 | end 28 | end 29 | 30 | def klass_final_type 31 | "#{klass}".split('::')[-1] 32 | end 33 | 34 | end 35 | 36 | end 37 | end 38 | -------------------------------------------------------------------------------- /lib/compel/builder/hash.rb: -------------------------------------------------------------------------------- 1 | module Compel 2 | module Builder 3 | 4 | class Hash < Schema 5 | 6 | def initialize 7 | super(Coercion::Hash) 8 | 9 | options[:keys] = { value: {} } 10 | end 11 | 12 | def keys(object, options = {}) 13 | build_option :keys, coerce_keys_schemas(object), options 14 | end 15 | 16 | private 17 | 18 | def coerce_keys_schemas(object) 19 | begin 20 | fail if object.nil? 21 | 22 | Coercion.coerce!(object, Coercion::Hash) 23 | rescue 24 | raise TypeError, 'Builder::Hash keys must be an Hash' 25 | end 26 | 27 | unless object.values.all?{|value| value.is_a?(Builder::Schema) } 28 | raise TypeError, 'All Builder::Hash keys must be a valid Schema' 29 | end 30 | 31 | object 32 | end 33 | 34 | end 35 | 36 | end 37 | end 38 | -------------------------------------------------------------------------------- /lib/compel/coercion/types/type.rb: -------------------------------------------------------------------------------- 1 | module Compel 2 | module Coercion 3 | 4 | class Type 5 | 6 | attr_accessor :value, 7 | :options 8 | 9 | def self.coerce(value, options = {}) 10 | new(value, options).coerce 11 | end 12 | 13 | def initialize(value, options = {}) 14 | @value = value 15 | @options = options 16 | end 17 | 18 | def coerce 19 | result = coerce_value 20 | 21 | # There are some special cases that 22 | # we need to build a custom error 23 | if result.is_a?(Result) 24 | return result 25 | end 26 | 27 | Coercion::Result.new(result, value, self.class) 28 | end 29 | 30 | class << self 31 | 32 | def human_name 33 | "#{self.name.split('::')[-1]}" 34 | end 35 | 36 | end 37 | 38 | end 39 | 40 | end 41 | end 42 | -------------------------------------------------------------------------------- /lib/compel/errors.rb: -------------------------------------------------------------------------------- 1 | module Compel 2 | 3 | class Errors 4 | 5 | def initialize 6 | @errors = {} 7 | end 8 | 9 | def add(key, error) 10 | if error.nil? || error.empty? 11 | return 12 | end 13 | 14 | if error.is_a?(Compel::Errors) || error.is_a?(Hash) 15 | if @errors[key].nil? 16 | @errors[key] = {} 17 | end 18 | 19 | @errors[key].merge!(error.to_hash) 20 | else 21 | if @errors[key].nil? 22 | @errors[key] = [] 23 | end 24 | 25 | if !error.is_a?(Array) 26 | error = [error] 27 | end 28 | 29 | @errors[key].concat(error) 30 | end 31 | 32 | @errors 33 | end 34 | 35 | def length 36 | @errors.keys.length 37 | end 38 | 39 | def empty? 40 | length == 0 41 | end 42 | 43 | def to_hash 44 | @errors 45 | end 46 | 47 | end 48 | 49 | end 50 | -------------------------------------------------------------------------------- /lib/compel/coercion/types/date_type.rb: -------------------------------------------------------------------------------- 1 | module Compel 2 | module Coercion 3 | 4 | class DateType < Type 5 | 6 | attr_reader :format 7 | 8 | def coerce_value 9 | @format = default_format 10 | 11 | if options[:format] 12 | @format = options[:format][:value] 13 | end 14 | 15 | if value.is_a?(klass) 16 | @value = value.strftime(format) 17 | end 18 | 19 | coerced = klass.strptime(value, format) 20 | 21 | if coerced.strftime(format) == value 22 | return coerced 23 | end 24 | 25 | build_error_result 26 | 27 | rescue 28 | build_error_result 29 | end 30 | 31 | def build_error_result 32 | custom_error = "'#{value}' is not a parsable #{klass.to_s.downcase} with format: #{format}" 33 | 34 | Result.new(nil, value, self.class, custom_error) 35 | end 36 | 37 | end 38 | 39 | end 40 | end 41 | -------------------------------------------------------------------------------- /spec/support/sinatra_app.rb: -------------------------------------------------------------------------------- 1 | require 'sinatra/base' 2 | require 'compel' 3 | 4 | class SinatraApp < Sinatra::Base 5 | 6 | set :show_exceptions, false 7 | set :raise_errors, true 8 | 9 | before do 10 | content_type :json 11 | end 12 | 13 | helpers do 14 | 15 | def compel(schema) 16 | params.merge! Compel.run!(params, Compel.hash.keys(schema)) 17 | end 18 | 19 | end 20 | 21 | error Compel::InvalidObjectError do |exception| 22 | status 400 23 | { errors: exception.object[:errors] }.to_json 24 | end 25 | 26 | configure :development do 27 | set :show_exceptions, false 28 | set :raise_errors, true 29 | end 30 | 31 | post '/api/posts' do 32 | compel({ 33 | post: Compel.hash.keys({ 34 | title: Compel.string.required, 35 | body: Compel.string, 36 | published: Compel.boolean.default(false) 37 | }).required 38 | }) 39 | 40 | params.to_json 41 | end 42 | 43 | end 44 | -------------------------------------------------------------------------------- /compel.gemspec: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | 3 | lib = File.expand_path('../lib', __FILE__) 4 | $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) 5 | require 'compel/version' 6 | 7 | Gem::Specification.new do |gem| 8 | gem.name = 'compel' 9 | gem.version = Compel::VERSION 10 | gem.authors = ['Joaquim Adráz'] 11 | gem.email = ['joaquim.adraz@gmail.com'] 12 | gem.description = %q{Compel} 13 | gem.summary = %q{Ruby Object Coercion and Validation} 14 | gem.homepage = 'https://github.com/joaquimadraz/compel' 15 | 16 | gem.files = `git ls-files`.split($/) 17 | gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) } 18 | gem.test_files = gem.files.grep(%r{^(test|spec|features)/}) 19 | gem.require_paths = ['lib'] 20 | 21 | gem.add_development_dependency 'rspec', '~> 3.2' 22 | gem.add_development_dependency 'rake', '~> 0' 23 | gem.add_development_dependency 'pry', '~> 0' 24 | end 25 | -------------------------------------------------------------------------------- /lib/compel/builder/schema.rb: -------------------------------------------------------------------------------- 1 | module Compel 2 | module Builder 3 | 4 | class Schema 5 | 6 | include Builder::Common 7 | 8 | attr_reader :type, 9 | :options 10 | 11 | def self.human_name 12 | "#{self.name.split('::')[1..-1].join('::')}" 13 | end 14 | 15 | def initialize(type) 16 | @type = type 17 | @options = default_options 18 | end 19 | 20 | def required? 21 | options[:required][:value] 22 | end 23 | 24 | def default_value 25 | options[:default][:value] if options[:default] 26 | end 27 | 28 | def validate(object) 29 | Contract.new(object, self).validate 30 | end 31 | 32 | def build_option(name, value, extra_options = {}) 33 | options[name] = { value: value }.merge(extra_options) 34 | 35 | self 36 | end 37 | 38 | def default_options 39 | { required: { value: false } } 40 | end 41 | 42 | end 43 | 44 | end 45 | end 46 | -------------------------------------------------------------------------------- /lib/compel/builder/string.rb: -------------------------------------------------------------------------------- 1 | module Compel 2 | module Builder 3 | 4 | class String < Schema 5 | 6 | # Taken from ruby_regex gem by @eparreno 7 | # https://github.com/eparreno/ruby_regex 8 | URL_REGEX = /(\A\z)|(\A(http|https):\/\/[a-z0-9]+([\-\.]{1}[a-z0-9]+)*\.[a-z]{2,5}(([0-9]{1,5})?\/.*)?\z)/ix 9 | 10 | # Taken from Michael Hartl's 'The Ruby on Rails Tutorial' 11 | # https://www.railstutorial.org/book 12 | EMAIL_REGEX = /\A[\w+\-.]+@[a-z\d\-]+(\.[a-z]+)*\.[a-z]+\z/i 13 | 14 | include CommonValue 15 | 16 | def initialize 17 | super(Coercion::String) 18 | end 19 | 20 | def format(regex, options = {}) 21 | build_option :format, Coercion.coerce!(regex, Coercion::Regexp), options 22 | end 23 | 24 | def url(options = {}) 25 | build_option :format, URL_REGEX, options 26 | end 27 | 28 | def email(options = {}) 29 | build_option :format, EMAIL_REGEX, options 30 | end 31 | 32 | end 33 | 34 | end 35 | end 36 | -------------------------------------------------------------------------------- /lib/compel/validation/conditions/if.rb: -------------------------------------------------------------------------------- 1 | # encoding: UTF-8 2 | 3 | module Compel 4 | module Validation 5 | 6 | class If < Condition 7 | 8 | def validate_value 9 | unless valid? 10 | "is invalid" 11 | end 12 | end 13 | 14 | private 15 | 16 | def valid? 17 | CustomValidator.new(option_value).valid?(value) 18 | end 19 | 20 | end 21 | 22 | class CustomValidator 23 | 24 | attr_reader :caller, 25 | :method 26 | 27 | def initialize(caller) 28 | ❨╯°□°❩╯︵┻━┻? caller 29 | end 30 | 31 | def valid?(value) 32 | caller.send(method, value) 33 | end 34 | 35 | private 36 | 37 | def ❨╯°□°❩╯︵┻━┻? caller 38 | fail unless caller.is_a?(Proc) || caller.arity > 1 39 | 40 | if caller.arity == 1 41 | @caller = caller 42 | @method = 'call' 43 | else 44 | @caller = caller.binding.receiver 45 | @method = caller.call 46 | end 47 | end 48 | 49 | end 50 | 51 | end 52 | end 53 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Joaquim Adráz 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /lib/compel/validation/conditions/condition.rb: -------------------------------------------------------------------------------- 1 | module Compel 2 | module Validation 3 | 4 | class Condition 5 | 6 | attr_reader :value, 7 | :options, 8 | :value_type, 9 | :option_value 10 | 11 | def self.validate(value, option_value, options = {}) 12 | new(value, option_value, options).validate 13 | end 14 | 15 | def initialize(value, option_value, options = {}) 16 | @value = value 17 | @value_type = options.delete(:type) || Coercion::Types::Any 18 | @options = options 19 | @option_value = option_value 20 | end 21 | 22 | def validate 23 | Validation::Result.new \ 24 | value, value_type, validate_value_with_error_message 25 | end 26 | 27 | private 28 | 29 | def validate_value_with_error_message 30 | error_message = validate_value 31 | 32 | if error_message 33 | error_message_with_value(options[:message] || error_message) 34 | end 35 | end 36 | 37 | def error_message_with_value(message) 38 | message.gsub(/\{\{value\}\}/, "#{value}") 39 | end 40 | 41 | end 42 | 43 | end 44 | end 45 | -------------------------------------------------------------------------------- /lib/compel/coercion/coercion.rb: -------------------------------------------------------------------------------- 1 | require 'compel/coercion/types/type' 2 | require 'compel/coercion/types/date_type' 3 | require 'compel/coercion/types/integer' 4 | require 'compel/coercion/types/float' 5 | require 'compel/coercion/types/string' 6 | require 'compel/coercion/types/date' 7 | require 'compel/coercion/types/time' 8 | require 'compel/coercion/types/datetime' 9 | require 'compel/coercion/types/hash' 10 | require 'compel/coercion/types/json' 11 | require 'compel/coercion/types/boolean' 12 | require 'compel/coercion/types/regexp' 13 | require 'compel/coercion/types/array' 14 | require 'compel/coercion/types/any' 15 | 16 | require 'compel/coercion/result' 17 | require 'compel/coercion/nil_result' 18 | 19 | module Compel 20 | 21 | module Coercion 22 | 23 | def coerce!(value, type, options = {}) 24 | result = coerce(value, type, options) 25 | 26 | unless result.valid? 27 | raise Compel::TypeError, result.error 28 | end 29 | 30 | result.coerced 31 | end 32 | 33 | def coerce(value, type, options = {}) 34 | return Coercion::NilResult.new if value.nil? 35 | 36 | type.coerce(value, options) 37 | end 38 | 39 | extend self 40 | 41 | end 42 | 43 | end 44 | -------------------------------------------------------------------------------- /lib/compel/builder/methods.rb: -------------------------------------------------------------------------------- 1 | require 'compel/builder/common' 2 | require 'compel/builder/common_value' 3 | require 'compel/builder/schema' 4 | require 'compel/builder/hash' 5 | require 'compel/builder/json' 6 | require 'compel/builder/string' 7 | require 'compel/builder/integer' 8 | require 'compel/builder/float' 9 | require 'compel/builder/datetime' 10 | require 'compel/builder/time' 11 | require 'compel/builder/date' 12 | require 'compel/builder/boolean' 13 | require 'compel/builder/array' 14 | require 'compel/builder/any' 15 | 16 | module Compel 17 | module Builder 18 | 19 | module Methods 20 | 21 | def hash 22 | Builder::Hash.new 23 | end 24 | 25 | def json 26 | Builder::JSON.new 27 | end 28 | 29 | def string 30 | Builder::String.new 31 | end 32 | 33 | def integer 34 | Builder::Integer.new 35 | end 36 | 37 | def float 38 | Builder::Float.new 39 | end 40 | 41 | def datetime 42 | Builder::DateTime.new 43 | end 44 | 45 | def time 46 | Builder::Time.new 47 | end 48 | 49 | def date 50 | Builder::Date.new 51 | end 52 | 53 | def boolean 54 | Builder::Boolean.new 55 | end 56 | 57 | def array 58 | Builder::Array.new 59 | end 60 | 61 | def any 62 | Builder::Any.new 63 | end 64 | 65 | extend self 66 | 67 | end 68 | 69 | end 70 | end 71 | -------------------------------------------------------------------------------- /lib/compel/builder/common.rb: -------------------------------------------------------------------------------- 1 | module Compel 2 | module Builder 3 | 4 | module Common 5 | 6 | def is(value, options = {}) 7 | build_option :is, Coercion.coerce!(value, self.type), options 8 | end 9 | 10 | def required(options = {}) 11 | build_option :required, true, options 12 | end 13 | 14 | def default(value, options = {}) 15 | build_option :default, Coercion.coerce!(value, self.type), options 16 | end 17 | 18 | def length(value, options = {}) 19 | build_option :length, Coercion.coerce!(value, Coercion::Integer), options 20 | end 21 | 22 | def min_length(value, options = {}) 23 | build_option :min_length, Coercion.coerce!(value, Coercion::Integer), options 24 | end 25 | 26 | def max_length(value, options = {}) 27 | build_option :max_length, Coercion.coerce!(value, Coercion::Integer), options 28 | end 29 | 30 | def if(lambda = nil, options = {}, &block) 31 | build_option :if, coerce_if_proc(lambda || block), options 32 | 33 | rescue 34 | raise Compel::TypeError, 'invalid proc for if' 35 | end 36 | 37 | # this is lovely, refactor later 38 | def coerce_if_proc(proc) 39 | if proc && proc.is_a?(Proc) && 40 | (proc.arity == 1 || proc.arity == 0 && 41 | (proc.call.is_a?(::Symbol) || proc.call.is_a?(::String))) 42 | proc 43 | else 44 | fail 45 | end 46 | end 47 | 48 | end 49 | 50 | end 51 | end 52 | -------------------------------------------------------------------------------- /spec/compel/errors_spec.rb: -------------------------------------------------------------------------------- 1 | describe Compel::Errors do 2 | 3 | it 'should add error' do 4 | errors = Compel::Errors.new 5 | errors.add(:first_name, 'is required') 6 | 7 | expect(errors.to_hash[:first_name]).to include('is required') 8 | end 9 | 10 | it 'should add multiple errors' do 11 | errors = Compel::Errors.new 12 | errors.add(:first_name, 'is required') 13 | errors.add(:first_name, 'is invalid') 14 | 15 | expect(errors.to_hash[:first_name]).to include('is required') 16 | expect(errors.to_hash[:first_name]).to include('is invalid') 17 | end 18 | 19 | it 'should add Compel::Errors' do 20 | address_errors = Compel::Errors.new 21 | address_errors.add(:line_one, 'is required') 22 | address_errors.add(:post_code, 'must be an Hash') 23 | 24 | errors = Compel::Errors.new 25 | errors.add(:address, address_errors) 26 | 27 | expect(errors.to_hash[:address][:line_one]).to include('is required') 28 | expect(errors.to_hash[:address][:post_code]).to include('must be an Hash') 29 | end 30 | 31 | it 'should add nested Compel::Errors' do 32 | post_code_errors = Compel::Errors.new 33 | post_code_errors.add(:prefix, 'is invalid') 34 | post_code_errors.add(:suffix, 'is required') 35 | 36 | address_errors = Compel::Errors.new 37 | address_errors.add(:line_one, 'is required') 38 | address_errors.add(:post_code, post_code_errors) 39 | 40 | errors = Compel::Errors.new 41 | errors.add(:address, address_errors) 42 | 43 | expect(errors.to_hash[:address][:line_one]).to include('is required') 44 | expect(errors.to_hash[:address][:post_code][:prefix]).to include('is invalid') 45 | expect(errors.to_hash[:address][:post_code][:suffix]).to include('is required') 46 | end 47 | 48 | end 49 | -------------------------------------------------------------------------------- /lib/compel/builder/common_value.rb: -------------------------------------------------------------------------------- 1 | module Compel 2 | module Builder 3 | 4 | module CommonValue 5 | 6 | def in(value, options = {}) 7 | build_option :in, coerce_values_ary!(value, :in), options 8 | end 9 | 10 | def range(value, options = {}) 11 | build_option :range, coerce_values_ary!(value, :range), options 12 | end 13 | 14 | def min(value, options = {}) 15 | build_option :min, coerce_value!(value, :min), options 16 | end 17 | 18 | def max(value, options = {}) 19 | build_option :max, coerce_value!(value, :max), options 20 | end 21 | 22 | def coerce_values_ary!(values, method) 23 | begin 24 | fail if values.nil? 25 | 26 | Coercion.coerce!(values, Coercion::Array) 27 | rescue 28 | raise_array_error(method) 29 | end 30 | 31 | values.map{ |value| Coercion.coerce!(value, self.type) } 32 | 33 | rescue 34 | raise_array_values_error(method) 35 | end 36 | 37 | def coerce_value!(value, method) 38 | begin 39 | fail if value.nil? 40 | 41 | Coercion.coerce!(value, self.type) 42 | rescue 43 | raise_value_error(method) 44 | end 45 | end 46 | 47 | def raise_array_error(method) 48 | raise TypeError, "#{self.class.human_name} ##{method} " \ 49 | "value must an Array" 50 | end 51 | 52 | def raise_array_values_error(method) 53 | raise TypeError, "All #{self.class.human_name} ##{method} values " \ 54 | "must be a valid #{self.type.human_name}" 55 | end 56 | 57 | def raise_value_error(method) 58 | raise TypeError, "#{self.class.human_name} ##{method} value " \ 59 | "must be a valid #{self.type.human_name}" 60 | end 61 | 62 | end 63 | 64 | end 65 | end 66 | -------------------------------------------------------------------------------- /lib/compel/validators/type_validator.rb: -------------------------------------------------------------------------------- 1 | # Validates a type, given an input, type and options 2 | # output is a coerced value 3 | # error is an array of strings 4 | module Compel 5 | module Validators 6 | 7 | class TypeValidator < Base 8 | 9 | def validate 10 | if !schema.required? && input.nil? 11 | return self 12 | end 13 | 14 | # coerce 15 | coercion_result = Coercion.coerce(input, schema.type, schema.options) 16 | 17 | unless coercion_result.valid? 18 | @errors = [coercion_result.error] 19 | return self 20 | end 21 | 22 | @output = coercion_result.coerced 23 | 24 | # validate 25 | @errors = Validation.validate(@output, schema.type, schema.options) 26 | 27 | # validate array values 28 | if schema.type == Coercion::Array && errors.empty? 29 | validate_array_values(input) 30 | end 31 | 32 | self 33 | end 34 | 35 | def validate_array_values(values) 36 | result = Result.new \ 37 | ArrayValidator.validate(values, schema) 38 | 39 | @output = result.value 40 | 41 | if !result.valid? 42 | # TODO: ArrayValidator should do this for me: 43 | # remove invalid coerced index, 44 | # and set the original value. 45 | # If it's an Hash, keep errors key 46 | result.errors.keys.each do |index| 47 | if @output[index.to_i].is_a?(Hash) 48 | # Keep errors key on hash if exists 49 | @output[index.to_i].merge!(values[index.to_i]) 50 | else 51 | # Array, Integer, String, Float, Dates.. etc 52 | @output[index.to_i] = values[index.to_i] 53 | end 54 | end 55 | 56 | @errors = result.errors 57 | end 58 | end 59 | 60 | alias_method :serialize, :output 61 | alias_method :serialize_errors, :errors 62 | 63 | end 64 | 65 | end 66 | end 67 | -------------------------------------------------------------------------------- /spec/compel/sinatra_integration_spec.rb: -------------------------------------------------------------------------------- 1 | describe Compel do 2 | 3 | context 'Sinatra Integration' do 4 | 5 | it 'should return 400 for missing params' do 6 | post('/api/posts') do |response| 7 | response_json = JSON.parse(response.body) 8 | 9 | expect(response.status).to eq(400) 10 | expect(response_json['errors']['post']).to \ 11 | include('is required') 12 | end 13 | end 14 | 15 | it 'should return 400 for missing title' do 16 | params = { 17 | post: { 18 | body: 'Body', 19 | published: 0 20 | } 21 | } 22 | 23 | post('/api/posts', params) do |response| 24 | response_json = JSON.parse(response.body) 25 | 26 | expect(response.status).to eq(400) 27 | expect(response_json['errors']['post']['title']).to \ 28 | include('is required') 29 | end 30 | end 31 | 32 | it 'should return 400 for invalid boolean' do 33 | params = { 34 | post: { 35 | title: 'Title', 36 | published: 'falss' 37 | } 38 | } 39 | 40 | post('/api/posts', params) do |response| 41 | response_json = JSON.parse(response.body) 42 | 43 | expect(response.status).to eq(400) 44 | expect(response_json['errors']['post']['published']).to \ 45 | include("'falss' is not a valid Boolean") 46 | end 47 | end 48 | 49 | it 'should return 200' do 50 | params = { 51 | post: { 52 | title: 'Title', 53 | body: 'Body', 54 | published: false 55 | } 56 | } 57 | 58 | post('/api/posts', params) do |response| 59 | response_json = JSON.parse(response.body) 60 | 61 | expect(response.status).to eq(200) 62 | expect(response_json['post']).to \ 63 | eq({ 64 | 'title' => 'Title', 65 | 'body' => 'Body', 66 | 'published' => false 67 | }) 68 | end 69 | end 70 | 71 | end 72 | 73 | end 74 | -------------------------------------------------------------------------------- /lib/compel/validation/validation.rb: -------------------------------------------------------------------------------- 1 | require 'compel/validation/conditions/condition' 2 | require 'compel/validation/conditions/is' 3 | require 'compel/validation/conditions/in' 4 | require 'compel/validation/conditions/min' 5 | require 'compel/validation/conditions/max' 6 | require 'compel/validation/conditions/format' 7 | require 'compel/validation/conditions/length' 8 | require 'compel/validation/conditions/min_length' 9 | require 'compel/validation/conditions/max_length' 10 | require 'compel/validation/conditions/if' 11 | 12 | require 'compel/validation/result' 13 | 14 | module Compel 15 | 16 | module Validation 17 | 18 | CONDITIONS = { 19 | is: Validation::Is, 20 | in: Validation::In, 21 | min: Validation::Min, 22 | max: Validation::Max, 23 | range: Validation::Range, 24 | format: Validation::Format, 25 | length: Validation::Length, 26 | min_length: Validation::MinLength, 27 | max_length: Validation::MaxLength, 28 | if: Validation::If, 29 | } 30 | 31 | def validate(value, type, options) 32 | if value.nil? && !!(options[:required] && options[:required][:value]) 33 | return [options[:required][:message] || 'is required'] 34 | end 35 | 36 | errors = Errors.new 37 | 38 | options.each do |name, option_values| 39 | next unless condition_exists?(name) 40 | 41 | cloned_options = option_values.dup 42 | 43 | option_value = cloned_options.delete(:value) 44 | 45 | result = condition_klass(name).validate \ 46 | value, option_value, cloned_options.merge(type: type) 47 | 48 | unless result.valid? 49 | errors.add :base, result.error_message 50 | end 51 | end 52 | 53 | errors.to_hash[:base] || [] 54 | end 55 | 56 | def condition_exists?(option_name) 57 | CONDITIONS.keys.include?(option_name.to_sym) 58 | end 59 | 60 | def condition_klass(option_name) 61 | CONDITIONS[option_name.to_sym] 62 | end 63 | 64 | extend self 65 | 66 | end 67 | 68 | end 69 | -------------------------------------------------------------------------------- /lib/compel/validators/array_validator.rb: -------------------------------------------------------------------------------- 1 | module Compel 2 | module Validators 3 | 4 | class ArrayValidator < Base 5 | 6 | attr_reader :items_schema 7 | 8 | def initialize(input, schema) 9 | super 10 | 11 | @errors = Errors.new 12 | @output = [] 13 | @items_schema = schema.options[:items][:value] if schema.options[:items] 14 | end 15 | 16 | def validate 17 | unless array_valid? 18 | return self 19 | end 20 | 21 | if items_schema.nil? 22 | @output = input 23 | return self 24 | end 25 | 26 | items_validator = \ 27 | ArrayItemsValidator.validate(input, items_schema) 28 | 29 | @output = items_validator.output 30 | 31 | unless items_validator.valid? 32 | @errors = items_validator.errors 33 | end 34 | 35 | self 36 | end 37 | 38 | def serialize_errors 39 | @errors.to_hash 40 | end 41 | 42 | alias_method :serialize, :output 43 | 44 | private 45 | 46 | def array_valid? 47 | if !schema.required? && input.nil? 48 | return false 49 | end 50 | 51 | array_errors = [] 52 | 53 | unless input.is_a?(Array) 54 | array_errors << "'#{input}' is not a valid Array" 55 | end 56 | 57 | array_errors += \ 58 | Validation.validate(input, schema.type, schema.options) 59 | 60 | unless array_errors.empty? 61 | errors.add(:base, array_errors) 62 | return false 63 | end 64 | 65 | true 66 | end 67 | 68 | end 69 | 70 | class ArrayItemsValidator < Base 71 | 72 | def initialize(input, schema) 73 | super 74 | 75 | @output = [] 76 | @errors = Errors.new 77 | end 78 | 79 | def validate 80 | input.each_with_index do |item, index| 81 | 82 | if !schema.required? && item.nil? 83 | next 84 | end 85 | 86 | item_validator = \ 87 | if schema.type == Coercion::Hash 88 | HashValidator.validate(item, schema) 89 | else 90 | TypeValidator.validate(item, schema) 91 | end 92 | 93 | output << item_validator.serialize 94 | 95 | if !item_validator.valid? 96 | # added errors for the index of that invalid array value 97 | errors.add("#{index}", item_validator.serialize_errors) 98 | end 99 | end 100 | 101 | self 102 | end 103 | 104 | end 105 | 106 | end 107 | end 108 | -------------------------------------------------------------------------------- /lib/compel/validators/hash_validator.rb: -------------------------------------------------------------------------------- 1 | # Validates values of an hash recursively 2 | # output is an hash with coerced values 3 | # errors is a Compel::Errors 4 | module Compel 5 | module Validators 6 | 7 | class HashValidator < Base 8 | 9 | attr_reader :keys_schemas 10 | 11 | def initialize(input, schema) 12 | super 13 | 14 | @errors = Errors.new 15 | @keys_schemas = schema.options[:keys][:value] 16 | end 17 | 18 | def validate 19 | unless root_hash_valid? 20 | return self 21 | end 22 | 23 | keys_validator = \ 24 | HashKeysValidator.validate(input, keys_schemas) 25 | 26 | @errors = keys_validator.errors 27 | @output = keys_validator.output 28 | 29 | self 30 | end 31 | 32 | def serialize 33 | coerced = output.is_a?(Hash) ? input.merge(output) : {} 34 | 35 | coerced.tap do |hash| 36 | if !errors.empty? 37 | hash[:errors] = serialize_errors 38 | end 39 | end 40 | end 41 | 42 | def serialize_errors 43 | errors.to_hash 44 | end 45 | 46 | private 47 | 48 | def root_hash_valid? 49 | if !schema.required? && input.nil? 50 | return false 51 | end 52 | 53 | root_hash = TypeValidator.validate(input, schema) 54 | 55 | unless root_hash.valid? 56 | errors.add(:base, root_hash.errors) 57 | return false 58 | end 59 | 60 | true 61 | end 62 | 63 | end 64 | 65 | class HashKeysValidator < Base 66 | 67 | attr_reader :schemas 68 | 69 | def initialize(input, schemas) 70 | super 71 | 72 | @output = {} 73 | @errors = Errors.new 74 | @schemas = schemas 75 | end 76 | 77 | def validate 78 | schemas.keys.each do |key| 79 | value = output[key].nil? ? input[key] : output[key] 80 | 81 | validator = TypeValidator.validate(value, schemas[key]) 82 | 83 | unless validator.output.nil? 84 | output[key] = validator.output 85 | end 86 | 87 | unless validator.valid? 88 | errors.add(key, validator.errors) 89 | next 90 | end 91 | 92 | if schemas[key].type == Coercion::Hash && 93 | (schemas[key].required? || 94 | !input[key].nil?) 95 | 96 | hash_validator = HashValidator.validate(input[key], schemas[key]) 97 | 98 | errors.add(key, hash_validator.errors) 99 | output[key] = hash_validator.output 100 | end 101 | 102 | end 103 | 104 | self 105 | end 106 | 107 | end 108 | 109 | end 110 | end 111 | -------------------------------------------------------------------------------- /spec/compel/validation_spec.rb: -------------------------------------------------------------------------------- 1 | describe Compel::Validation do 2 | 3 | context 'required' do 4 | 5 | it 'should validate without errors' do 6 | errors = Compel::Validation.validate(123, Compel::Coercion::Integer, { required: { value: true } }) 7 | 8 | expect(errors.empty?).to eq(true) 9 | end 10 | 11 | it 'should validate with error' do 12 | errors = Compel::Validation.validate(nil, Compel::Coercion::Integer, { required: { value: true } }) 13 | 14 | expect(errors.empty?).to eq(false) 15 | expect(errors).to eq(['is required']) 16 | end 17 | 18 | end 19 | 20 | context 'length' do 21 | 22 | it 'should validate without errors' do 23 | errors = Compel::Validation.validate(123, Compel::Coercion::Integer, { length: { value: 3 } }) 24 | 25 | expect(errors.empty?).to eq(true) 26 | end 27 | 28 | end 29 | 30 | context 'in, range' do 31 | 32 | def expect_be_in_range(range, value) 33 | [:in, :range].each do |key| 34 | errors = Compel::Validation.validate(value, Compel::Coercion::String, { key => { value: range } }) 35 | yield errors 36 | end 37 | end 38 | 39 | it 'should validate without errors' do 40 | expect_be_in_range(['PT', 'UK'], 'PT') do |errors| 41 | expect(errors.empty?).to eq(true) 42 | end 43 | end 44 | 45 | it 'should validate with errors' do 46 | expect_be_in_range(['PT', 'UK'], 'US') do |errors| 47 | expect(errors).to include('must be within ["PT", "UK"]') 48 | end 49 | end 50 | 51 | context 'range' do 52 | 53 | it 'should validate without errors' do 54 | errors = Compel::Validation.validate(2, Compel::Coercion::Integer, range: { value: (1..3) }) 55 | 56 | expect(errors.empty?).to eq(true) 57 | end 58 | 59 | it 'should validate with errors' do 60 | errors = Compel::Validation.validate(4, Compel::Coercion::Integer, range: { value: (1..3) }) 61 | 62 | expect(errors).to include('must be within 1..3') 63 | end 64 | 65 | end 66 | 67 | end 68 | 69 | context 'format' do 70 | 71 | it 'should validate with errors' do 72 | format = /^abcd/ 73 | errors = Compel::Validation.validate('acb', Compel::Coercion::String, format: { value: format }) 74 | 75 | expect(errors).to include("must match format ^abcd") 76 | end 77 | 78 | it 'should validate without errors' do 79 | format = /abcd/ 80 | errors = Compel::Validation.validate('abcd', Compel::Coercion::String, format: { value: format }) 81 | expect(errors.empty?).to eq(true) 82 | end 83 | 84 | end 85 | 86 | context 'is' do 87 | 88 | it 'should validate with errors' do 89 | errors = Compel::Validation.validate('abcd', Compel::Coercion::Integer, is: { value: 123 }) 90 | expect(errors).to include('must be 123') 91 | end 92 | 93 | it 'should validate with errors 1' do 94 | errors = Compel::Validation.validate(nil, Compel::Coercion::Integer, is: { value: 123 }) 95 | expect(errors).to include('must be 123') 96 | end 97 | 98 | it 'should validate without errors' do 99 | errors = Compel::Validation.validate(123, Compel::Coercion::Integer, is: { value: 123 }) 100 | expect(errors.empty?).to eq(true) 101 | end 102 | 103 | end 104 | 105 | context 'min' do 106 | 107 | it 'should validate without errors for Integer' do 108 | errors = Compel::Validation.validate(2, Compel::Coercion::Integer, min: { value: 1 }) 109 | 110 | expect(errors.empty?).to be true 111 | end 112 | 113 | it 'should validate without errors for String' do 114 | errors = Compel::Validation.validate('b', Compel::Coercion::String, min: { value: 'a' }) 115 | 116 | expect(errors.empty?).to be true 117 | end 118 | 119 | it 'should validate with errors' do 120 | errors = Compel::Validation.validate(1, Compel::Coercion::Integer, min: { value: 3 }) 121 | 122 | expect(errors).to include('cannot be less than 3') 123 | end 124 | 125 | it 'should validate with errors for String' do 126 | errors = Compel::Validation.validate('a', Compel::Coercion::String, min: { value: 'b' }) 127 | 128 | expect(errors).to include('cannot be less than b') 129 | end 130 | 131 | it 'should validate with errors for Float' do 132 | errors = Compel::Validation.validate(1.54, Compel::Coercion::Float, min: { value: 1.55 }) 133 | 134 | expect(errors).to include('cannot be less than 1.55') 135 | end 136 | 137 | end 138 | 139 | context 'max' do 140 | 141 | it 'should validate without errors' do 142 | errors = Compel::Validation.validate(5, Compel::Coercion::Integer, min: { value: 5 }) 143 | 144 | expect(errors.empty?).to be true 145 | end 146 | 147 | it 'should validate with errors' do 148 | errors = Compel::Validation.validate(3, Compel::Coercion::Integer, max: { value: 2 }) 149 | 150 | expect(errors).to include('cannot be greater than 2') 151 | end 152 | 153 | it 'should validate without errors for Integer' do 154 | errors = Compel::Validation.validate(2, Compel::Coercion::Integer, max: { value: 3 }) 155 | 156 | expect(errors.empty?).to be true 157 | end 158 | 159 | it 'should validate without errors for String' do 160 | errors = Compel::Validation.validate('b', Compel::Coercion::String, max: { value: 'd' }) 161 | 162 | expect(errors.empty?).to be true 163 | end 164 | 165 | it 'should validate with errors for String' do 166 | errors = Compel::Validation.validate('c', Compel::Coercion::String, max: { value: 'b' }) 167 | 168 | expect(errors).to include('cannot be greater than b') 169 | end 170 | 171 | it 'should validate with errors for Float' do 172 | errors = Compel::Validation.validate(1.56, Compel::Coercion::Float, max: { value: 1.55 }) 173 | 174 | expect(errors).to include('cannot be greater than 1.55') 175 | end 176 | 177 | end 178 | 179 | context 'min_length' do 180 | 181 | it 'should validate with errors 1' do 182 | errors = Compel::Validation.validate('a', Compel::Coercion::String, min_length: { value: 2 }) 183 | 184 | expect(errors).to include('cannot have length less than 2') 185 | end 186 | 187 | it 'should validate without errors' do 188 | errors = Compel::Validation.validate('ab', Compel::Coercion::String, min_length: { value: 2 }) 189 | 190 | expect(errors.empty?).to eq(true) 191 | end 192 | 193 | it 'should validate without errors 1' do 194 | errors = Compel::Validation.validate(3, Compel::Coercion::Integer, min_length: { value: 2 }) 195 | 196 | expect(errors).to include('cannot have length less than 2') 197 | end 198 | 199 | end 200 | 201 | context 'max_length' do 202 | 203 | it 'should validate with errors 1' do 204 | errors = Compel::Validation.validate('abcdef', Compel::Coercion::String, max_length: { value: 5 }) 205 | 206 | expect(errors).to include('cannot have length greater than 5') 207 | end 208 | 209 | it 'should validate without errors' do 210 | errors = Compel::Validation.validate('abcde', Compel::Coercion::String, max_length: { value: 5 }) 211 | 212 | expect(errors.empty?).to eq(true) 213 | end 214 | 215 | it 'should validate without errors 1' do 216 | errors = Compel::Validation.validate(1, Compel::Coercion::Integer, max_length: { value: 2 }) 217 | 218 | expect(errors.empty?).to eq(true) 219 | end 220 | 221 | end 222 | 223 | end 224 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Compel 2 | ========================== 3 | ![](https://travis-ci.org/joaquimadraz/compel.svg) 4 | [![Code Climate](https://codeclimate.com/github/joaquimadraz/compel/badges/gpa.svg)](https://codeclimate.com/github/joaquimadraz/compel) 5 | [![Test Coverage](https://codeclimate.com/github/joaquimadraz/compel/badges/coverage.svg)](https://codeclimate.com/github/joaquimadraz/compel/coverage) 6 | 7 | Ruby Object Coercion and Validation 8 | 9 | This is a straight forward way to validate any Ruby object: just give an object and the schema. 10 | 11 | The motivation was to create an integration for [RestMyCase](https://github.com/goncalvesjoao/rest_my_case) to have validations before any business logic execution and to build a easy way coerce and validate params on [Sinatra](https://github.com/sinatra/sinatra). 12 | 13 | The schema builder is based on [Joi](https://github.com/hapijs/joi). 14 | 15 | ### Usage 16 | 17 | ```ruby 18 | object = { 19 | first_name: 'Joaquim', 20 | birth_date: '1989-0', 21 | address: { 22 | line_one: 'Lisboa', 23 | post_code: '1100', 24 | country_code: 'PT' 25 | } 26 | } 27 | 28 | schema = Compel.hash.keys({ 29 | first_name: Compel.string.required, 30 | last_name: Compel.string.required, 31 | birth_date: Compel.datetime, 32 | address: Compel.hash.keys({ 33 | line_one: Compel.string.required, 34 | line_two: Compel.string.default('-'), 35 | post_code: Compel.string.format(/^\d{4}-\d{3}$/).required, 36 | country_code: Compel.string.in(['PT', 'GB']).default('PT') 37 | }) 38 | }) 39 | 40 | Compel.run(object, schema) # or schema.validate(object) 41 | ``` 42 | 43 | Will return a `Compel::Result` object: 44 | 45 | ```ruby 46 | => ["is required"], 49 | "birth_date" => ["'1989-0' is not a parsable datetime with format: %FT%T"], 50 | "address" => { 51 | "post_code" => ["must match format ^\\d{4}-\\d{3}$"] 52 | } 53 | }, 54 | @valid=false, 55 | @value={ 56 | "first_name" => "Joaquim", 57 | "birth_date" => "1989-0", 58 | "address" => { 59 | "line_one" => "Lisboa", 60 | "post_code" => "1100", 61 | "country_code" => "PT", 62 | "line_two" => "-" 63 | }, 64 | "errors" => { 65 | "last_name" => ["is required"], 66 | "birth_date" => ["'1989-0' is not a parsable datetime with format: %FT%T"], 67 | "address" => { 68 | "post_code" => ["must match format ^\\d{4}-\\d{3}$"] 69 | } 70 | } 71 | }> 72 | ``` 73 | 74 | There are 4 ways to run validations: 75 | 76 | Method | Behaviour 77 | ------------- | ------------- 78 | `#run` | Validates and returns a `Compel::Result` (see below) 79 | `#run!` | Validates and raises `Compel::InvalidObjectError` exception with the coerced params and errors. 80 | `#run?` | Validates and returns true or false. 81 | `schema#validate` | Check below 82 | 83 | ========================== 84 | 85 | ### Schema Builder API 86 | 87 | #### Compel#any 88 | `Any` referes to any type that is available to coerce with Compel. 89 | Methods `length`, `min_length` and `max_length` turn the object to validate into a `string` to compare the length. 90 | 91 | **Methods**: 92 | - `is(``value``)` 93 | - `required` 94 | - `default(``value``)` 95 | - `length(``integer``)` 96 | - `min_length(``integer``)` 97 | - `max_length(``integer``)` 98 | - `if` 99 | - `if(->(value){ value == 1 })` 100 | - `if{|value| value == 1 }` 101 | - `if{:custom_validation} # Check the specs for now, I'm rewriting the docs ;)` 102 | 103 | ========================== 104 | 105 | #### Compel#array 106 | 107 | **Methods**: 108 | - `#items(``schema``)` 109 | 110 | **Examples**: 111 | ```ruby 112 | . [1, 2, 3] 113 | . [{ a: 1, b: 2} 114 | . { a: 3, b: 4 }] 115 | ``` 116 | 117 | ========================== 118 | 119 | #### Compel#hash 120 | 121 | **Methods**: 122 | - `keys(``schema_hash``)` 123 | 124 | **Examples**: 125 | ```ruby 126 | . { a: 1, b: 2, c: 3 } 127 | ``` 128 | 129 | ========================== 130 | 131 | #### Compel#date 132 | 133 | **Methods**: 134 | - `format(``ruby_date_format``)` 135 | - `iso8601`, set format to: `%Y-%m-%d` 136 | 137 | ========================== 138 | 139 | #### Compel#datetime & Compel#time 140 | 141 | **Methods**: 142 | - `format(``ruby_date_format``)` 143 | - `iso8601`, set format to: `%FT%T` 144 | 145 | ========================== 146 | 147 | #### Compel#json 148 | 149 | **Examples**: 150 | ```ruby 151 | . "{\"a\":1,\"b\":2,\"c\":3}" 152 | ``` 153 | 154 | ========================== 155 | 156 | #### Compel#boolean 157 | 158 | **Examples**: 159 | ```ruby 160 | . 1/0 161 | . true/false 162 | . 't'/'f' 163 | . 'yes'/'no' 164 | . 'y'/'n' 165 | ``` 166 | 167 | ========================== 168 | 169 | #### Compel#string 170 | 171 | **Methods**: 172 | - `in(``array``)` 173 | - `min(``value``)` 174 | - `max(``value``)` 175 | - `format(``regexp``)` 176 | - `email` 177 | - `url` 178 | 179 | ========================== 180 | 181 | #### Compel#integer 182 | 183 | **Methods**: 184 | - `in(``array``)` 185 | - `min(``value``)` 186 | - `max(``value``)` 187 | 188 | ========================== 189 | 190 | #### Compel#float 191 | 192 | **Methods**: 193 | - `in(``array``)` 194 | - `min(``value``)` 195 | - `max(``value``)` 196 | 197 | ========================== 198 | 199 | ### Schema Validate 200 | 201 | For straight forward validations, you can call `#validate` on schema and it will return a `Compel::Result` object. 202 | 203 | ```ruby 204 | result = Compel.string 205 | .format(/^\d{4}-\d{3}$/) 206 | .required 207 | .validate('1234') 208 | 209 | puts result.errors 210 | # => ["must match format ^\\d{4}-\\d{3}$"] 211 | ``` 212 | 213 | #### Compel Result 214 | 215 | Simple object that encapsulates a validation result. 216 | 217 | Method | Behaviour 218 | ------------- | ------------- 219 | `#value` | the coerced value or the input value is invalid 220 | `#errors` | array of errors is any. 221 | `#valid?` | `true` or `false` 222 | `#raise?` | raises a `Compel::InvalidObjectError` if invalid, otherwise returns `#value` 223 | 224 | #### Custom Options 225 | 226 | `Custom error message` 227 | 228 | Examples: 229 | ```ruby 230 | schema = Compel.string.required(message: 'this is really required') 231 | 232 | result = schema.validate(nil) 233 | 234 | p result.errors 235 | => ["this is really required"] 236 | 237 | schema = Compel.string.is('Hello', message: 'give me an Hello!') 238 | 239 | result = schema.validate(nil) 240 | 241 | p result.errors 242 | => ["give me an Hello!"] 243 | ``` 244 | 245 | ========================== 246 | 247 | ### Sinatra Integration 248 | 249 | If you want to use with `Sinatra`, here's an example: 250 | 251 | ```ruby 252 | class App < Sinatra::Base 253 | 254 | set :show_exceptions, false 255 | set :raise_errors, true 256 | 257 | before do 258 | content_type :json 259 | end 260 | 261 | helpers do 262 | 263 | def compel(schema) 264 | params.merge! Compel.run!(params, Compel.hash.keys(schema)) 265 | end 266 | 267 | end 268 | 269 | error Compel::InvalidObjectError do |exception| 270 | status 400 271 | { errors: exception.object[:errors] }.to_json 272 | end 273 | 274 | configure :development do 275 | set :show_exceptions, false 276 | set :raise_errors, true 277 | end 278 | 279 | post '/api/posts' do 280 | compel({ 281 | post: Compel.hash.keys({ 282 | title: Compel.string.required, 283 | body: Compel.string, 284 | published: Compel.boolean.default(false) 285 | }).required 286 | }) 287 | 288 | params.to_json 289 | end 290 | 291 | end 292 | ``` 293 | 294 | ###Installation 295 | 296 | Add this line to your application's Gemfile: 297 | 298 | gem 'compel', '~> 0.5.0' 299 | 300 | And then execute: 301 | 302 | $ bundle 303 | 304 | ### Get in touch 305 | 306 | If you have any questions, write an issue or get in touch [@joaquimadraz](https://twitter.com/joaquimadraz) 307 | 308 | -------------------------------------------------------------------------------- /spec/compel/coercion_spec.rb: -------------------------------------------------------------------------------- 1 | # encoding: UTF-8 2 | 3 | describe Compel::Coercion do 4 | 5 | context 'Type coercion' do 6 | 7 | context 'Integer' do 8 | 9 | it 'should coerce' do 10 | value = Compel::Coercion.coerce!(123, Compel::Coercion::Integer) 11 | expect(value).to eq(123) 12 | end 13 | 14 | it 'should coerce 1' do 15 | value = Compel::Coercion.coerce!('123', Compel::Coercion::Integer) 16 | expect(value).to eq(123) 17 | end 18 | 19 | it 'should coerce 2' do 20 | value = Compel::Coercion.coerce!(123.3, Compel::Coercion::Integer) 21 | expect(value).to eq(123) 22 | end 23 | 24 | it 'should not coerce' do 25 | expect { Compel::Coercion.coerce!('123abc', Compel::Coercion::Integer) }.to \ 26 | raise_error Compel::TypeError, "'123abc' is not a valid Integer" 27 | end 28 | 29 | end 30 | 31 | context 'Float' do 32 | 33 | it 'should coerce' do 34 | value = Compel::Coercion.coerce!('1.2', Compel::Coercion::Float) 35 | 36 | expect(value).to eq(1.2) 37 | end 38 | 39 | it 'should not coerce' do 40 | expect { Compel::Coercion.coerce!('1.a233', Compel::Coercion::Float) }.to \ 41 | raise_error Compel::TypeError, "'1.a233' is not a valid Float" 42 | end 43 | 44 | end 45 | 46 | context 'String' do 47 | 48 | it 'should coerce' do 49 | value = Compel::Coercion.coerce!('abc', Compel::Coercion::String) 50 | 51 | expect(value).to eq('abc') 52 | end 53 | 54 | it 'should not coerce' do 55 | value = 1.2 56 | 57 | expect { Compel::Coercion.coerce!(value, Compel::Coercion::String) }.to \ 58 | raise_error Compel::TypeError, "'#{value}' is not a valid String" 59 | end 60 | 61 | end 62 | 63 | context 'Date' do 64 | 65 | it 'should coerce with default format' do 66 | value = Compel::Coercion.coerce!('2015-12-22', Compel::Coercion::Date) 67 | 68 | expect(value).to eq(Date.parse('2015-12-22')) 69 | end 70 | 71 | it 'should coerce with format' do 72 | value = Compel::Coercion.coerce! \ 73 | '22-12-2015', Compel::Coercion::Date, { format: { value: '%d-%m-%Y' } } 74 | 75 | expect(value).to eq(Date.strptime('22-12-2015', '%d-%m-%Y')) 76 | end 77 | 78 | it 'should not coerce' do 79 | value = '2015-0' 80 | 81 | expect { Compel::Coercion.coerce!(value, Compel::Coercion::Date) }.to \ 82 | raise_error \ 83 | Compel::TypeError, 84 | "'#{value}' is not a parsable date with format: %Y-%m-%d" 85 | end 86 | 87 | it 'should not coerce 1' do 88 | default_format = '%Y-%m-%d' 89 | value = '22-12-2015' 90 | 91 | expect { Compel::Coercion.coerce!(value, Compel::Coercion::Date) }.to \ 92 | raise_error \ 93 | Compel::TypeError, 94 | "'#{value}' is not a parsable date with format: #{default_format}" 95 | end 96 | 97 | end 98 | 99 | context 'DateTime & Time' do 100 | 101 | it 'should coerce' do 102 | [DateTime, Time].each do |time_klass| 103 | value = Compel::Coercion.coerce! \ 104 | '2015-12-22', Compel::Coercion.const_get("#{time_klass}"), format: { value: '%Y-%m-%d' } 105 | 106 | expect(value).to be_a time_klass 107 | expect(value.year).to eq(2015) 108 | expect(value.month).to eq(12) 109 | expect(value.day).to eq(22) 110 | end 111 | 112 | end 113 | 114 | it 'should coerce iso8601 format' do 115 | [DateTime, Time].each do |time_klass| 116 | value = Compel::Coercion.coerce! \ 117 | '2015-12-22T09:30:10', Compel::Coercion.const_get("#{time_klass}") 118 | 119 | expect(value).to be_a time_klass 120 | expect(value.year).to eq(2015) 121 | expect(value.month).to eq(12) 122 | expect(value.day).to eq(22) 123 | expect(value.hour).to eq(9) 124 | expect(value.min).to eq(30) 125 | expect(value.sec).to eq(10) 126 | end 127 | end 128 | 129 | it 'should not coerce' do 130 | default_format = '%FT%T' 131 | value = '22-12-2015' 132 | 133 | [DateTime, Time].each do |time_klass| 134 | type_down_cased = "#{time_klass}".downcase 135 | 136 | expect { Compel::Coercion.coerce!('22-12-2015', Compel::Coercion.const_get("#{time_klass}")) }.to \ 137 | raise_error \ 138 | Compel::TypeError, 139 | "'#{value}' is not a parsable #{type_down_cased} with format: #{default_format}" 140 | end 141 | 142 | end 143 | 144 | end 145 | 146 | context 'Hash' do 147 | 148 | it 'should coerce' do 149 | value = Compel::Coercion.coerce!({ 150 | first_name: 'Joaquim', 151 | last_name: 'Adráz' 152 | }, Compel::Coercion::Hash) 153 | 154 | expect(value).to eq({ 155 | first_name: 'Joaquim', 156 | last_name: 'Adráz' 157 | }) 158 | end 159 | 160 | it 'should coerce 1' do 161 | value = Compel::Coercion.coerce!({ 162 | 'first_name' => 'Joaquim', 163 | 'last_name' => 'Adráz' 164 | }, Compel::Coercion::Hash) 165 | 166 | expect(value).to eq({ 167 | first_name: 'Joaquim', 168 | last_name: 'Adráz' 169 | }) 170 | end 171 | 172 | it 'should coerce 2' do 173 | value = Compel::Coercion.coerce!({ 174 | first_name: 'Joaquim', 175 | last_name: 'Adráz' 176 | }, Compel::Coercion::Hash) 177 | 178 | expect(value).to eq({ 179 | first_name: 'Joaquim', 180 | last_name: 'Adráz' 181 | }) 182 | end 183 | 184 | it 'should not coerce' do 185 | expect { Compel::Coercion.coerce!(123, Compel::Coercion::Hash) }.to \ 186 | raise_error Compel::TypeError, "'123' is not a valid Hash" 187 | end 188 | 189 | it 'should not coerce 1' do 190 | expect { Compel::Coercion.coerce!('hash', Compel::Coercion::Hash) }.to \ 191 | raise_error Compel::TypeError, "'hash' is not a valid Hash" 192 | end 193 | 194 | it 'should not coerce 2' do 195 | expect { Compel::Coercion.coerce!(['hash'], Compel::Coercion::Hash) }.to \ 196 | raise_error Compel::TypeError, "'[\"hash\"]' is not a valid Hash" 197 | end 198 | 199 | end 200 | 201 | context 'JSON' do 202 | 203 | it 'should coerce' do 204 | value = Compel::Coercion.coerce! \ 205 | "{\"first_name\":\"Joaquim\",\"last_name\":\"Adráz\"}", 206 | Compel::Coercion::JSON 207 | 208 | expect(value).to eq({ 209 | 'first_name' => 'Joaquim', 210 | 'last_name' => 'Adráz' 211 | }) 212 | end 213 | 214 | end 215 | 216 | context 'Boolean' do 217 | 218 | it 'should coerce false' do 219 | value = Compel::Coercion.coerce!('f', Compel::Coercion::Boolean) 220 | 221 | expect(value).to eq(false) 222 | end 223 | 224 | it 'should coerce false 1' do 225 | value = Compel::Coercion.coerce!('0', Compel::Coercion::Boolean) 226 | 227 | expect(value).to eq(false) 228 | end 229 | 230 | it 'should coerce true' do 231 | value = Compel::Coercion.coerce!('t', Compel::Coercion::Boolean) 232 | 233 | expect(value).to eq(true) 234 | end 235 | 236 | it 'should coerce true 1' do 237 | value = Compel::Coercion.coerce!('true', Compel::Coercion::Boolean) 238 | 239 | expect(value).to eq(true) 240 | end 241 | 242 | it 'should not coerce' do 243 | expect{ Compel::Coercion.coerce!('trye', Compel::Coercion::Boolean) }.to \ 244 | raise_error Compel::TypeError, "'trye' is not a valid Boolean" 245 | end 246 | 247 | end 248 | 249 | context 'Array' do 250 | 251 | it 'should not coerce' do 252 | expect { Compel::Coercion.coerce!(123, Compel::Coercion::Array) }.to \ 253 | raise_error Compel::TypeError, "'123' is not a valid Array" 254 | end 255 | 256 | it 'should coerce' do 257 | value = Compel::Coercion.coerce!([1, 2], Compel::Coercion::Array) 258 | 259 | expect(value).to eq([1, 2]) 260 | end 261 | 262 | end 263 | 264 | end 265 | 266 | end 267 | -------------------------------------------------------------------------------- /spec/compel/compel_spec.rb: -------------------------------------------------------------------------------- 1 | # encoding: UTF-8 2 | 3 | describe Compel do 4 | 5 | context '#run' do 6 | 7 | context 'User validation example' do 8 | 9 | def user_schema 10 | Compel.hash.keys({ 11 | user: Compel.hash.keys({ 12 | first_name: Compel.string.required, 13 | last_name: Compel.string.required, 14 | birth_date: Compel.datetime, 15 | age: Compel.integer, 16 | admin: Compel.boolean, 17 | blog_role: Compel.hash.keys({ 18 | admin: Compel.boolean.required 19 | }) 20 | }).required 21 | }).required 22 | end 23 | 24 | context 'valid' do 25 | 26 | it 'return coerced values' do 27 | object = { 28 | user: { 29 | first_name: 'Joaquim', 30 | last_name: 'Adráz', 31 | birth_date: '1989-08-06T09:00:00', 32 | age: '26', 33 | admin: 'f', 34 | blog_role: { 35 | admin: '0' 36 | } 37 | } 38 | } 39 | 40 | result = Compel.run(object, user_schema) 41 | 42 | expect(result.valid?).to be true 43 | expect(result.value).to eq \ 44 | user: { 45 | first_name: 'Joaquim', 46 | last_name: 'Adráz', 47 | birth_date: DateTime.parse('1989-08-06T09:00:00'), 48 | age: 26, 49 | admin: false, 50 | blog_role: { 51 | admin: false 52 | } 53 | } 54 | end 55 | 56 | end 57 | 58 | context 'invalid' do 59 | 60 | it 'should not compel and leave other keys untouched' do 61 | object = { 62 | other_param: 1, 63 | user: { 64 | first_name: 'Joaquim' 65 | } 66 | } 67 | 68 | result = Compel.run(object, user_schema) 69 | 70 | expect(result.valid?).to be false 71 | expect(result.value).to eq \ 72 | other_param: 1, 73 | user: { 74 | first_name: 'Joaquim', 75 | }, 76 | errors: { 77 | user: { 78 | last_name: ['is required'] 79 | } 80 | } 81 | end 82 | 83 | it 'should not compel for invalid hash' do 84 | result = Compel.run(1, user_schema) 85 | 86 | expect(result.valid?).to be false 87 | expect(result.errors[:base]).to include("'1' is not a valid Hash") 88 | end 89 | 90 | it 'should not compel for missing hash' do 91 | result = Compel.run(nil, user_schema) 92 | 93 | expect(result.valid?).to be false 94 | expect(result.errors[:base]).to include('is required') 95 | end 96 | 97 | it 'should not compel for empty hash' do 98 | result = Compel.run({}, user_schema) 99 | 100 | expect(result.valid?).to be false 101 | expect(result.errors[:user]).to include('is required') 102 | end 103 | 104 | it 'should not compel for missing required key' do 105 | object = { 106 | user: { 107 | first_name: 'Joaquim' 108 | } 109 | } 110 | 111 | result = Compel.run(object, user_schema) 112 | 113 | expect(result.valid?).to be false 114 | expect(result.value).to eq \ 115 | user:{ 116 | first_name: 'Joaquim', 117 | }, 118 | errors: { 119 | user: { 120 | last_name: ['is required'] 121 | } 122 | } 123 | end 124 | 125 | end 126 | 127 | end 128 | 129 | context 'Address validation example' do 130 | 131 | def address_schema 132 | Compel.hash.keys({ 133 | address: Compel.hash.keys({ 134 | line_one: Compel.string.required, 135 | line_two: Compel.string, 136 | post_code: Compel.hash.keys({ 137 | prefix: Compel.integer.length(4).required, 138 | suffix: Compel.integer.length(3), 139 | county: Compel.hash.keys({ 140 | code: Compel.string.length(2).required, 141 | name: Compel.string 142 | }) 143 | }).required 144 | }).required 145 | }) 146 | end 147 | 148 | context 'valid' do 149 | 150 | it 'should compel with #run?' do 151 | object = { 152 | address: { 153 | line_one: 'Lisbon', 154 | line_two: 'Portugal', 155 | post_code: { 156 | prefix: 1100, 157 | suffix: 100 158 | } 159 | } 160 | } 161 | 162 | expect(Compel.run?(object, address_schema)).to eq(true) 163 | end 164 | 165 | end 166 | 167 | context 'invalid' do 168 | 169 | it 'should not compel for missing required keys' do 170 | object = { 171 | address: { 172 | line_two: 'Portugal' 173 | } 174 | } 175 | 176 | result = Compel.run(object, address_schema) 177 | 178 | expect(result.valid?).to be false 179 | expect(result.value).to eq \ 180 | address: { 181 | line_two: 'Portugal' 182 | }, 183 | errors: { 184 | address: { 185 | line_one: ['is required'], 186 | post_code: ['is required'] 187 | } 188 | } 189 | end 190 | 191 | it 'should not compel missing key and length invalid' do 192 | object = { 193 | address: { 194 | line_two: 'Portugal', 195 | post_code: { 196 | prefix: '1', 197 | county: { 198 | code: 'LX' 199 | } 200 | } 201 | } 202 | } 203 | 204 | result = Compel.run(object, address_schema) 205 | 206 | expect(result.valid?).to be false 207 | expect(result.value).to eq \ 208 | address: { 209 | line_two: 'Portugal', 210 | post_code: { 211 | prefix: 1, 212 | county: { 213 | code: 'LX' 214 | } 215 | } 216 | }, 217 | errors: { 218 | address: { 219 | line_one: ['is required'], 220 | post_code: { 221 | prefix: ['cannot have length different than 4'] 222 | } 223 | } 224 | } 225 | end 226 | 227 | it 'should not compel for givin invalid optional value' do 228 | object = { 229 | address: { 230 | line_one: 'Line', 231 | post_code: { 232 | prefix: '1100', 233 | suffix: '100', 234 | county: {} 235 | }, 236 | } 237 | } 238 | 239 | result = Compel.run(object, address_schema) 240 | 241 | expect(result.valid?).to be false 242 | expect(result.value).to eq \ 243 | address: { 244 | line_one: 'Line', 245 | post_code: { 246 | prefix: 1100, 247 | suffix: 100, 248 | county: {} 249 | } 250 | }, 251 | errors: { 252 | address: { 253 | post_code: { 254 | county: { 255 | code: ['is required'] 256 | } 257 | } 258 | } 259 | } 260 | 261 | end 262 | 263 | it 'should not compel for missing required root key' do 264 | object = { 265 | address: nil 266 | } 267 | 268 | result = Compel.run(object, address_schema) 269 | 270 | expect(result.valid?).to be false 271 | expect(result.value).to eq \ 272 | address: nil, 273 | errors: { 274 | address: ['is required'] 275 | } 276 | end 277 | 278 | it 'should not compel for empty object' do 279 | result = Compel.run({}, address_schema) 280 | 281 | expect(result.valid?).to be false 282 | expect(result.value).to eq \ 283 | errors: { 284 | address: ['is required'] 285 | } 286 | end 287 | 288 | end 289 | 290 | end 291 | 292 | context 'Boolean' do 293 | 294 | it 'it not compel for invalid boolean' do 295 | result = Compel.run(nil, Compel.boolean.required) 296 | 297 | expect(result.valid?).to be false 298 | expect(result.errors).to include('is required') 299 | end 300 | 301 | context 'required option' do 302 | 303 | it 'should compel with valid option' do 304 | expect(Compel.run?(1, Compel.boolean.required)).to be true 305 | end 306 | 307 | it 'should not compel for missing required boolean' do 308 | schema = Compel.hash.keys({ 309 | admin: Compel.boolean.required 310 | }) 311 | 312 | result = Compel.run({ admin: nil }, schema) 313 | 314 | expect(result.valid?).to be false 315 | expect(result.errors[:admin]).to include('is required') 316 | end 317 | 318 | end 319 | 320 | context 'default option' do 321 | 322 | it 'should compel with default option set' do 323 | result = Compel.run(nil, Compel.boolean.default(false)) 324 | 325 | expect(result.value).to be false 326 | end 327 | 328 | end 329 | 330 | context 'is option' do 331 | 332 | it 'should compel' do 333 | expect(Compel.run?(1, Compel.boolean.is(true))).to be true 334 | end 335 | 336 | it 'should not compel' do 337 | result = Compel.run(0, Compel.boolean.is(true)) 338 | 339 | expect(result.valid?).to be false 340 | expect(result.errors).to include('must be true') 341 | end 342 | 343 | end 344 | 345 | end 346 | 347 | context 'String' do 348 | 349 | context 'format option' do 350 | 351 | it 'should compel' do 352 | schema = Compel.string.format(/^\d{4}-\d{3}$/) 353 | 354 | expect(Compel.run?('1100-100', schema)).to be true 355 | end 356 | 357 | it 'should not compel' do 358 | schema = Compel.string.format(/^\d{4}-\d{3}$/) 359 | 360 | result = Compel.run('110-100', schema) 361 | 362 | expect(result.errors).to include('must match format ^\\d{4}-\\d{3}$') 363 | end 364 | 365 | it 'should not compel with custom message' do 366 | schema = Compel.string.format(/^\d{4}-\d{3}$/, message: 'this format is not good') 367 | 368 | result = Compel.run('110-100', schema) 369 | 370 | expect(result.errors).to include('this format is not good') 371 | end 372 | 373 | end 374 | 375 | end 376 | 377 | context 'Time' do 378 | 379 | context 'format option' do 380 | 381 | it 'should not compel' do 382 | schema = Compel.time 383 | result = Compel.run('1989-08-06', schema) 384 | 385 | expect(result.valid?).to be false 386 | expect(result.errors).to \ 387 | include("'1989-08-06' is not a parsable time with format: %FT%T") 388 | end 389 | 390 | it 'should compel with format' do 391 | schema = Compel.time.format('%Y-%m-%d') 392 | result = Compel.run('1989-08-06', schema) 393 | 394 | expect(result.valid?).to be true 395 | expect(result.value).to eq(Time.new(1989, 8, 6)) 396 | end 397 | 398 | it 'should compel by default' do 399 | schema = Compel.time 400 | result = Compel.run('1989-08-06T09:00:00', schema) 401 | 402 | expect(result.valid?).to be true 403 | expect(result.value).to eq(Time.new(1989, 8, 6, 9)) 404 | end 405 | 406 | it 'should compel with iso8601 format' do 407 | schema = Compel.time.iso8601 408 | result = Compel.run('1989-08-06T09:00:00', schema) 409 | 410 | expect(result.valid?).to be true 411 | expect(result.value).to eq(Time.new(1989, 8, 6, 9)) 412 | end 413 | 414 | end 415 | 416 | end 417 | 418 | end 419 | 420 | context 'DateTime' do 421 | 422 | context 'format option' do 423 | 424 | it 'should not compel' do 425 | schema = Compel.hash.keys({ 426 | birth_date: Compel.datetime 427 | }) 428 | 429 | result = Compel.run({ birth_date: '1989-08-06' }, schema) 430 | 431 | expect(result.valid?).to be false 432 | expect(result.errors[:birth_date]).to \ 433 | include("'1989-08-06' is not a parsable datetime with format: %FT%T") 434 | end 435 | 436 | it 'should compel with format' do 437 | schema = Compel.hash.keys({ 438 | birth_date: Compel.datetime.format('%Y-%m-%d') 439 | }) 440 | 441 | result = Compel.run({ birth_date: '1989-08-06' }, schema) 442 | 443 | expect(result.valid?).to be true 444 | expect(result.value[:birth_date]).to eq(DateTime.new(1989, 8, 6)) 445 | end 446 | 447 | it 'should compel by default' do 448 | schema = Compel.hash.keys({ 449 | birth_date: Compel.datetime 450 | }) 451 | 452 | result = Compel.run({ birth_date: '1989-08-06T09:00:00' }, schema) 453 | 454 | expect(result.valid?).to be true 455 | expect(result.value[:birth_date]).to eq(DateTime.new(1989, 8, 6, 9)) 456 | end 457 | 458 | it 'should compel with iso8601 format' do 459 | schema = Compel.hash.keys({ 460 | birth_date: Compel.datetime.iso8601 461 | }) 462 | 463 | result = Compel.run({ birth_date: '1989-08-06T09:00:00' }, schema) 464 | 465 | expect(result.valid?).to be true 466 | expect(result.value[:birth_date]).to eq(DateTime.new(1989, 8, 6, 9)) 467 | end 468 | 469 | end 470 | 471 | end 472 | 473 | context 'Compel methods' do 474 | 475 | context '#run!' do 476 | 477 | context 'Other Values' do 478 | 479 | it 'should compel valid integer' do 480 | result = Compel.run!(1, Compel.integer.required) 481 | 482 | expect(result).to eq(1) 483 | end 484 | 485 | it 'should not compel for invalid integer' do 486 | expect{ Compel.run!('abc', Compel.integer.required) }.to \ 487 | raise_error Compel::InvalidObjectError, 'object has errors' 488 | end 489 | 490 | end 491 | 492 | context 'User validation example' do 493 | 494 | def make_the_call(method, hash) 495 | schema = Compel.hash.keys({ 496 | first_name: Compel.string.required, 497 | last_name: Compel.string.required, 498 | birth_date: Compel.datetime 499 | }) 500 | 501 | Compel.send(method, hash, schema) 502 | end 503 | 504 | it 'should compel' do 505 | hash = { 506 | first_name: 'Joaquim', 507 | last_name: 'Adráz', 508 | birth_date: DateTime.new(1988, 12, 24) 509 | } 510 | 511 | expect(make_the_call(:run!, hash)).to \ 512 | eq \ 513 | first_name: 'Joaquim', 514 | last_name: 'Adráz', 515 | birth_date: DateTime.new(1988, 12, 24) 516 | end 517 | 518 | it 'should raise InvalidObjectError exception for missing required key' do 519 | hash = { 520 | first_name: 'Joaquim' 521 | } 522 | 523 | expect{ make_the_call(:run!, hash) }.to \ 524 | raise_error Compel::InvalidObjectError, 'object has errors' 525 | end 526 | 527 | it 'should raise InvalidObjectError exception with errors' do 528 | hash = { 529 | first_name: 'Joaquim' 530 | } 531 | 532 | expect{ make_the_call(:run!, hash) }.to raise_error do |exception| 533 | expect(exception.object).to eq \ 534 | first_name: 'Joaquim', 535 | errors: { 536 | last_name: ['is required'] 537 | } 538 | end 539 | end 540 | 541 | it 'should raise InvalidObjectError exception for missing hash' do 542 | expect{ make_the_call(:run!, {}) }.to \ 543 | raise_error Compel::InvalidObjectError, 'object has errors' 544 | end 545 | 546 | end 547 | 548 | end 549 | 550 | end 551 | 552 | end 553 | -------------------------------------------------------------------------------- /spec/compel/builder_spec.rb: -------------------------------------------------------------------------------- 1 | # encoding: UTF-8 2 | 3 | describe Compel::Builder do 4 | 5 | context 'Schema' do 6 | 7 | context 'Build' do 8 | 9 | it 'should build new Schema for given type' do 10 | builder = Compel.string 11 | 12 | expect(builder.type).to be(Compel::Coercion::String) 13 | expect(builder.options.keys).to include(:required) 14 | expect(builder.required?).to be false 15 | expect(builder.default_value).to be nil 16 | end 17 | 18 | context 'Builder::CommonValue' do 19 | 20 | context '#in, #range' do 21 | 22 | subject(:builder) { Compel.string } 23 | 24 | it 'should have value' do 25 | [:in, :range].each do |method| 26 | builder.send(method, ["#{method}"]) 27 | expect(builder.options[method][:value]).to eq(["#{method}"]) 28 | end 29 | end 30 | 31 | end 32 | 33 | context '#min, #max' do 34 | 35 | subject(:builder) { Compel.string } 36 | 37 | it 'should have value' do 38 | [:min, :max].each do |method| 39 | builder.send(method, "#{method}") 40 | expect(builder.options[method][:value]).to eq("#{method}") 41 | end 42 | end 43 | 44 | end 45 | 46 | end 47 | 48 | context 'Builder::Common' do 49 | 50 | subject(:builder) { Compel.string } 51 | 52 | context '#is, #default' do 53 | 54 | it 'should have value' do 55 | [:is, :default].each do |method| 56 | builder.send(method, "#{method}") 57 | expect(builder.options[method][:value]).to eq("#{method}") 58 | end 59 | end 60 | 61 | end 62 | 63 | context '#required' do 64 | 65 | it 'should be false' do 66 | expect(builder.required?).to be false 67 | end 68 | 69 | it 'should be true' do 70 | builder.required 71 | 72 | expect(builder.required?).to be true 73 | end 74 | 75 | end 76 | 77 | context '#length' do 78 | 79 | it 'should have value' do 80 | builder.length(5) 81 | 82 | expect(builder.options[:length][:value]).to be 5 83 | end 84 | 85 | it 'should raise exception for invalid type' do 86 | expect { builder.length('abc') }.to \ 87 | raise_error Compel::TypeError, "'abc' is not a valid Integer" 88 | end 89 | 90 | end 91 | 92 | end 93 | 94 | context 'Date' do 95 | 96 | subject(:builder) { Compel.date } 97 | 98 | context '#format' do 99 | 100 | it 'should have value' do 101 | builder.format('%d-%m-%Y') 102 | 103 | expect(builder.options[:format][:value]).to eq('%d-%m-%Y') 104 | end 105 | 106 | it 'should have value' do 107 | builder.iso8601 108 | 109 | expect(builder.options[:format][:value]).to eq('%Y-%m-%d') 110 | end 111 | 112 | end 113 | 114 | context '#max' do 115 | 116 | it 'should set max value with string and coerce' do 117 | builder.max('2016-01-01') 118 | 119 | expect(builder.options[:max][:value]).to eq(Date.new(2016, 1, 1)) 120 | end 121 | 122 | end 123 | 124 | context '#in' do 125 | 126 | it 'should set max value with string and coerce' do 127 | builder.in(['2016-01-01', '2016-01-02']) 128 | 129 | expect(builder.options[:in][:value]).to include(Date.new(2016, 1, 1)) 130 | expect(builder.options[:in][:value]).to include(Date.new(2016, 1, 2)) 131 | end 132 | 133 | end 134 | 135 | end 136 | 137 | context 'Date, DateTime and Time' do 138 | 139 | def each_date_builder 140 | [Date, DateTime, Time].each do |klass| 141 | builder = Compel.send("#{klass.to_s.downcase}") 142 | 143 | yield builder, klass 144 | end 145 | end 146 | 147 | context 'in' do 148 | 149 | def expect_raise_error_for(builder, values, klass) 150 | expect{ builder.in(values) }.to \ 151 | raise_error \ 152 | Compel::TypeError, 153 | "All Builder::#{klass} #in values must be a valid #{klass}" 154 | end 155 | 156 | it 'should set in value' do 157 | each_date_builder do |builder, klass| 158 | builder.in([klass.new(2016, 1, 1), klass.new(2016, 1, 2)]) 159 | 160 | expect(builder.options[:in][:value].length).to eq 2 161 | end 162 | end 163 | 164 | it 'should raise exception for invalid #in values' do 165 | each_date_builder do |builder, klass| 166 | expect_raise_error_for(builder, [klass.new(2016, 1, 1), 'invalid_date'], klass) 167 | end 168 | end 169 | 170 | end 171 | 172 | context '#max' do 173 | 174 | it 'should set max value' do 175 | each_date_builder do |builder, klass| 176 | builder.max(klass.new(2016, 1, 1)) 177 | 178 | expect(builder.options[:max][:value]).to eq(klass.new(2016, 1, 1)) 179 | end 180 | end 181 | 182 | it 'should raise exception for invalid value' do 183 | each_date_builder do |builder, klass| 184 | expect{ builder.max(1) }.to \ 185 | raise_error \ 186 | Compel::TypeError, 187 | "Builder::#{klass} #max value must be a valid #{klass}" 188 | end 189 | end 190 | 191 | end 192 | 193 | context '#min' do 194 | 195 | it 'should set min value' do 196 | each_date_builder do |builder, klass| 197 | builder.min(klass.new(2016, 1, 1)) 198 | 199 | expect(builder.options[:min][:value]).to eq(klass.new(2016, 1, 1)) 200 | end 201 | end 202 | 203 | it 'should raise exception for invalid value' do 204 | each_date_builder do |builder, klass| 205 | expect{ builder.min(1) }.to \ 206 | raise_error \ 207 | Compel::TypeError, 208 | "Builder::#{klass} #min value must be a valid #{klass}" 209 | end 210 | end 211 | 212 | end 213 | 214 | end 215 | 216 | context 'String' do 217 | 218 | subject(:builder) { Compel.string } 219 | 220 | context '#in' do 221 | 222 | it 'should set in value' do 223 | builder.in(['a', 'b']) 224 | 225 | expect(builder.options[:in][:value].length).to eq 2 226 | end 227 | 228 | it 'should raise exception for invalid item on array' do 229 | expect{ builder.in([1, 'b']) }.to \ 230 | raise_error \ 231 | Compel::TypeError, 232 | "All Builder::String #in values must be a valid String" 233 | end 234 | 235 | end 236 | 237 | context '#format' do 238 | 239 | it 'should raise exception for invalid type' do 240 | expect { builder.format('abc') }.to \ 241 | raise_error Compel::TypeError, "'abc' is not a valid Regexp" 242 | end 243 | 244 | it 'should have value' do 245 | builder.format(/1/) 246 | 247 | expect(builder.options[:format][:value]).to eq(/1/) 248 | end 249 | 250 | end 251 | 252 | context '#min_length' do 253 | 254 | it 'should raise exception for invalid type' do 255 | expect { builder.min_length('a') }.to \ 256 | raise_error Compel::TypeError, "'a' is not a valid Integer" 257 | end 258 | 259 | it 'should have value' do 260 | builder.min_length(4) 261 | 262 | expect(builder.options[:min_length][:value]).to eq(4) 263 | end 264 | 265 | end 266 | 267 | context '#max_length' do 268 | 269 | it 'should raise exception for invalid type' do 270 | expect { builder.max_length('a') }.to \ 271 | raise_error Compel::TypeError, "'a' is not a valid Integer" 272 | end 273 | 274 | it 'should have value' do 275 | builder.max_length(10) 276 | 277 | expect(builder.options[:max_length][:value]).to eq(10) 278 | end 279 | 280 | end 281 | 282 | end 283 | 284 | context 'Hash' do 285 | 286 | it 'should build Schema' do 287 | schema = Compel.hash.keys({ 288 | a: Compel.float, 289 | b: Compel.string, 290 | c: Compel.hash.keys({ 291 | cc: Compel.integer.length(1) 292 | }), 293 | d: Compel.json, 294 | e: Compel.time, 295 | f: Compel.datetime, 296 | g: Compel.date, 297 | h: Compel.integer 298 | }) 299 | 300 | keys_schemas = schema.options[:keys][:value] 301 | 302 | expect(keys_schemas[:a].type).to be Compel::Coercion::Float 303 | expect(keys_schemas[:b].type).to be Compel::Coercion::String 304 | expect(keys_schemas[:c].type).to be Compel::Coercion::Hash 305 | expect(keys_schemas[:d].type).to be Compel::Coercion::JSON 306 | expect(keys_schemas[:e].type).to be Compel::Coercion::Time 307 | expect(keys_schemas[:f].type).to be Compel::Coercion::DateTime 308 | expect(keys_schemas[:g].type).to be Compel::Coercion::Date 309 | expect(keys_schemas[:h].type).to be Compel::Coercion::Integer 310 | end 311 | 312 | it 'should raise error for invalid #keys' do 313 | expect{ Compel.hash.keys(nil) }.to \ 314 | raise_error(Compel::TypeError, 'Builder::Hash keys must be an Hash') 315 | end 316 | 317 | it 'should raise error for invalid #keys 1' do 318 | expect{ Compel.hash.keys(1) }.to \ 319 | raise_error(Compel::TypeError, 'Builder::Hash keys must be an Hash') 320 | end 321 | 322 | it 'should raise error for invalid #keys Schema' do 323 | expect{ Compel.hash.keys({ a: 1 }) }.to \ 324 | raise_error(Compel::TypeError, 'All Builder::Hash keys must be a valid Schema') 325 | end 326 | 327 | end 328 | 329 | context 'Array' do 330 | 331 | it 'should raise exception for invalid type' do 332 | expect { Compel.array.items('a') }.to \ 333 | raise_error Compel::TypeError, "#items must be a valid Schema" 334 | end 335 | 336 | it 'should raise exception for invalid type' do 337 | expect { Compel.array.items('a') }.to \ 338 | raise_error Compel::TypeError, "#items must be a valid Schema" 339 | end 340 | 341 | it 'should have value' do 342 | builder = Compel.array.items(Compel.integer) 343 | 344 | expect(builder.options[:items][:value].class).to be(Compel::Builder::Integer) 345 | end 346 | 347 | end 348 | 349 | context 'Integer' do 350 | 351 | context 'min' do 352 | 353 | it 'should build schema' do 354 | builder = Compel.integer.min(10) 355 | 356 | expect(builder.options[:min][:value]).to eq(10) 357 | end 358 | 359 | it 'should raise exception for invalid value' do 360 | expect{ Compel.integer.min('ten') }.to \ 361 | raise_error \ 362 | Compel::TypeError, 'Builder::Integer #min value must be a valid Integer' 363 | end 364 | 365 | end 366 | 367 | end 368 | 369 | context 'Any' do 370 | 371 | context '#if' do 372 | 373 | it 'should have a proc' do 374 | _proc = Proc.new {|value| value == 1 } 375 | 376 | schema = Compel.any.if(_proc) 377 | 378 | expect(schema.options[:if][:value]).to eq _proc 379 | end 380 | 381 | it 'should have a block with string or symbol value' do 382 | schema = Compel.any.if{:is_valid_one} 383 | expect(schema.options[:if][:value].call).to eq :is_valid_one 384 | 385 | schema = Compel.any.if{'is_valid_one'} 386 | expect(schema.options[:if][:value].call).to eq 'is_valid_one' 387 | end 388 | 389 | it 'should raise_error for missing value' do 390 | expect{ Compel.any.if() }.to \ 391 | raise_error Compel::TypeError, 'invalid proc for if' 392 | end 393 | 394 | it 'should raise_error for invalid proc' do 395 | expect{ Compel.any.if('proc') }.to \ 396 | raise_error Compel::TypeError, 'invalid proc for if' 397 | end 398 | 399 | it 'should raise_error for invalid proc arity' do 400 | expect{ Compel.any.if(Proc.new {|a, b| a == b }) }.to \ 401 | raise_error Compel::TypeError, 'invalid proc for if' 402 | end 403 | 404 | it 'should raise_error for invalid proc 1' do 405 | expect{ Compel.any.if(1) }.to \ 406 | raise_error Compel::TypeError, 'invalid proc for if' 407 | end 408 | 409 | end 410 | 411 | end 412 | 413 | end 414 | 415 | context 'Validate' do 416 | 417 | context 'Any' do 418 | 419 | context '#required' do 420 | 421 | context 'invalid' do 422 | 423 | it 'should validate a nil object' do 424 | schema = Compel.any.required 425 | 426 | expect(schema.validate(nil).errors).to \ 427 | include('is required') 428 | end 429 | 430 | it 'should have an array for error messages' do 431 | schema = Compel.any.required(message: 'this is required') 432 | expect(schema.validate(nil).errors.class).to eq Array 433 | 434 | schema = Compel.any.required 435 | expect(schema.validate(nil).errors.class).to eq Array 436 | end 437 | 438 | it 'should use custom error message' do 439 | schema = Compel.any.required(message: 'this is required') 440 | 441 | expect(schema.validate(nil).errors).to \ 442 | include('this is required') 443 | end 444 | 445 | end 446 | 447 | context 'valid' do 448 | 449 | it 'should validate nested hash object' do 450 | schema = Compel.hash.keys({ 451 | a: Compel.any.required 452 | }); 453 | 454 | result = schema.validate(a: { b: 1 }) 455 | 456 | expect(result.valid?).to be true 457 | end 458 | 459 | it 'should validate nested hash object 1' do 460 | schema = Compel.hash.keys({ 461 | a: Compel.any.required 462 | }); 463 | 464 | result = schema.validate(a: []) 465 | 466 | expect(result.valid?).to be true 467 | end 468 | 469 | it 'should validate nested hash object 2' do 470 | schema = Compel.hash.keys({ 471 | a: Compel.any.required 472 | }); 473 | 474 | result = schema.validate(a: 1) 475 | 476 | expect(result.valid?).to be true 477 | end 478 | 479 | it 'should validate an array object' do 480 | schema = Compel.any.required 481 | 482 | result = schema.validate([1, 2]) 483 | 484 | expect(result.valid?).to be true 485 | end 486 | 487 | it 'should validate an array object 1' do 488 | schema = Compel.any.required 489 | 490 | result = schema.validate([]) 491 | 492 | expect(result.valid?).to be true 493 | end 494 | 495 | it 'should validate a string object' do 496 | schema = Compel.any.required 497 | 498 | result = schema.validate('test') 499 | 500 | expect(result.valid?).to be true 501 | end 502 | 503 | end 504 | 505 | end 506 | 507 | context '#is' do 508 | 509 | context 'invalid' do 510 | 511 | it 'should validate an integer' do 512 | schema = Compel.any.is(123) 513 | 514 | expect(schema.validate(122).errors).to \ 515 | include('must be 123') 516 | 517 | expect(schema.validate('onetwothree').errors).to \ 518 | include('must be 123') 519 | end 520 | 521 | it 'should validate an array' do 522 | schema = Compel.any.is([1, 2, 3]) 523 | 524 | expect(schema.validate([1]).errors).to \ 525 | include('must be [1, 2, 3]') 526 | 527 | expect(schema.validate([]).errors).to \ 528 | include('must be [1, 2, 3]') 529 | end 530 | 531 | it 'should use custom error message' do 532 | schema = Compel.any.is(1, message: 'not one') 533 | 534 | expect(schema.validate('two').errors).to \ 535 | include('not one') 536 | end 537 | 538 | end 539 | 540 | context 'valid' do 541 | 542 | it 'should validate an array' do 543 | schema = Compel.any.is([1, 2, 3]) 544 | 545 | result = schema.validate([1, 2, 3]) 546 | 547 | expect(result.valid?).to be true 548 | end 549 | 550 | end 551 | 552 | end 553 | 554 | context '#length' do 555 | 556 | context 'invalid' do 557 | 558 | it 'should not validate an instance object' do 559 | schema = Compel.any.length(1) 560 | 561 | expect(schema.validate(OpenStruct).errors).to \ 562 | include('cannot have length different than 1') 563 | end 564 | 565 | it 'should not validate a constant object' do 566 | schema = Compel.any.length(1) 567 | 568 | expect(schema.validate(OpenStruct).errors).to \ 569 | include('cannot have length different than 1') 570 | end 571 | 572 | it 'should use custom error message' do 573 | schema = Compel.any.length(2, message: '({{value}}) does not have the right size') 574 | 575 | expect(schema.validate(123).errors).to \ 576 | include('(123) does not have the right size') 577 | end 578 | 579 | end 580 | 581 | context 'valid' do 582 | 583 | it 'should validate a constant object' do 584 | schema = Compel.any.length(10) 585 | 586 | result = schema.validate(OpenStruct) 587 | 588 | expect(result.valid?).to be true 589 | end 590 | 591 | it 'should validate a constant object' do 592 | schema = Compel.any.length(10) 593 | 594 | result = schema.validate(OpenStruct) 595 | 596 | expect(result.valid?).to be true 597 | end 598 | 599 | it 'should validate a constant object' do 600 | schema = Compel.any.length(2) 601 | 602 | expect(schema.validate(12).valid?).to eq(true) 603 | end 604 | 605 | end 606 | 607 | end 608 | 609 | context '#min_length' do 610 | 611 | context 'invalid' do 612 | 613 | it 'should use custom error message' do 614 | schema = Compel.any.min_length(2, message: 'min is two') 615 | 616 | expect(schema.validate(1).errors).to \ 617 | include('min is two') 618 | end 619 | 620 | end 621 | 622 | end 623 | 624 | context '#max_length' do 625 | 626 | context 'invalid' do 627 | 628 | it 'should use custom error message' do 629 | schema = Compel.any.max_length(2, message: 'max is two') 630 | 631 | expect(schema.validate(123).errors).to \ 632 | include('max is two') 633 | end 634 | 635 | end 636 | 637 | end 638 | 639 | context '#if' do 640 | 641 | class CustomValidationsKlass 642 | 643 | attr_reader :value 644 | 645 | def initialize(value) 646 | @value = value 647 | end 648 | 649 | def validate 650 | Compel.any.if{:is_custom_valid?}.validate(value) 651 | end 652 | 653 | def validate_with_lambda(lambda) 654 | Compel.any.if(lambda).validate(value) 655 | end 656 | 657 | private 658 | 659 | def is_custom_valid?(value) 660 | value == assert_value 661 | end 662 | 663 | def assert_value 664 | [1, 2, 3] 665 | end 666 | 667 | end 668 | 669 | context 'invalid' do 670 | 671 | it 'should validate with custom method' do 672 | def is_valid_one(value) 673 | value == 1 674 | end 675 | 676 | result = Compel.any.if{:is_valid_one}.validate(2) 677 | 678 | expect(result.valid?).to eq(false) 679 | expect(result.errors).to include('is invalid') 680 | end 681 | 682 | it 'should validate with lambda' do 683 | result = Compel.any.if(Proc.new {|value| value == 2 }).validate(1) 684 | 685 | expect(result.valid?).to eq(false) 686 | expect(result.errors).to include('is invalid') 687 | end 688 | 689 | it 'should validate within an instance method' do 690 | result = CustomValidationsKlass.new(1).validate 691 | 692 | expect(result.valid?).to eq(false) 693 | expect(result.errors).to include('is invalid') 694 | end 695 | 696 | it 'should validate within an instance method 1' do 697 | result = \ 698 | CustomValidationsKlass.new('two') 699 | .validate_with_lambda(Proc.new {|value| value == [1, 2, 3] }) 700 | 701 | expect(result.valid?).to eq(false) 702 | expect(result.errors).to include('is invalid') 703 | end 704 | 705 | it 'should use custom message with parsed value' do 706 | schema = \ 707 | Compel.any.if(Proc.new {|value| value == 2 }, message: 'give me a {{value}}!') 708 | 709 | result = schema.validate(1) 710 | 711 | expect(result.valid?).to eq(false) 712 | expect(result.errors).to include('give me a 1!') 713 | end 714 | 715 | it 'should validate a date value' do 716 | to_validate = '1969-01-01T00:00:00' 717 | 718 | def validate_time(value) 719 | value > Time.at(0) 720 | end 721 | 722 | result = Compel.time.if{:validate_time}.validate(to_validate) 723 | 724 | expect(result.valid?).to eq(false) 725 | expect(result.errors).to include('is invalid') 726 | end 727 | 728 | it 'should raise_error for invalid custom method arity' do 729 | def custom_method_arity_two(value, extra_arg) 730 | false 731 | end 732 | 733 | def custom_method_arity_zero 734 | false 735 | end 736 | 737 | expect{ Compel.integer.if{:custom_method_arity_two}.validate(1) }.to \ 738 | raise_error ArgumentError 739 | 740 | expect{ Compel.integer.if{:custom_method_arity_zero}.validate(1) }.to \ 741 | raise_error ArgumentError 742 | end 743 | 744 | end 745 | 746 | context 'valid' do 747 | 748 | it 'should validate with custom method' do 749 | def is_valid_one(value) 750 | value == 1 751 | end 752 | 753 | result = Compel.any.if{:is_valid_one}.validate(1) 754 | 755 | expect(result.valid?).to eq(true) 756 | end 757 | 758 | it 'should validate with custom method 1' do 759 | def is_valid_one(value) 760 | value == 1 761 | end 762 | 763 | result = Compel.any.if{|value| value == 1 }.validate(1) 764 | 765 | expect(result.valid?).to eq(true) 766 | end 767 | 768 | it 'should validate with lambda' do 769 | result = Compel.any.if(Proc.new {|value| value == 2 }).validate(2) 770 | 771 | expect(result.valid?).to eq(true) 772 | end 773 | 774 | it 'should validate within an instance method' do 775 | result = CustomValidationsKlass.new([1, 2, 3]).validate 776 | 777 | expect(result.valid?).to eq(true) 778 | end 779 | 780 | it 'should validate within an instance method' do 781 | result = CustomValidationsKlass.new([1, 2, 3]).validate 782 | expect(result.valid?).to eq(true) 783 | end 784 | 785 | it 'should validate within an instance method 1' do 786 | result = \ 787 | CustomValidationsKlass.new([1, 2, 3]) 788 | .validate_with_lambda(Proc.new {|value| value == [1, 2, 3] }) 789 | 790 | expect(result.valid?).to eq(true) 791 | end 792 | 793 | it 'should validate a date value' do 794 | to_validate = '1969-01-01T00:00:00' 795 | 796 | def validate_time(value) 797 | value < Time.at(0) 798 | end 799 | 800 | result = Compel.time.if{:validate_time}.validate(to_validate) 801 | 802 | expect(result.valid?).to eq(true) 803 | end 804 | 805 | end 806 | 807 | end 808 | 809 | end 810 | 811 | context 'Hash' do 812 | 813 | it 'should validate Hash schema' do 814 | object = { 815 | first_name: 'Joaquim', 816 | birth_date: '1989-0', 817 | address: { 818 | line_one: 'Lisboa', 819 | post_code: '1100', 820 | country_code: 'PT' 821 | } 822 | } 823 | 824 | schema = Compel.hash.keys({ 825 | first_name: Compel.string.required, 826 | last_name: Compel.string.required, 827 | birth_date: Compel.date.iso8601, 828 | address: Compel.hash.keys({ 829 | line_one: Compel.string.required, 830 | line_two: Compel.string.default('-'), 831 | post_code: Compel.string.format(/^\d{4}-\d{3}$/).required, 832 | country_code: Compel.string.in(['PT', 'GB']).default('PT') 833 | }) 834 | }) 835 | 836 | result = schema.validate(object) 837 | 838 | expect(result.value).to \ 839 | eq({ 840 | first_name: 'Joaquim', 841 | birth_date: '1989-0', 842 | address: { 843 | line_one: 'Lisboa', 844 | post_code: '1100', 845 | country_code: 'PT', 846 | line_two: '-' 847 | }, 848 | errors: { 849 | last_name: ['is required'], 850 | birth_date: ["'1989-0' is not a parsable date with format: %Y-%m-%d"], 851 | address: { 852 | post_code: ["must match format ^\\d{4}-\\d{3}$"] 853 | } 854 | } 855 | }) 856 | end 857 | 858 | it 'should validate hash object from schema' do 859 | schema = Compel.hash.keys({ 860 | a: Compel.float.required 861 | }) 862 | 863 | expect(schema.validate({ a: nil }).errors[:a]).to \ 864 | include('is required') 865 | end 866 | 867 | context '#required' do 868 | 869 | it 'should validate empty keys option' do 870 | schema = Compel.hash.required 871 | 872 | expect(schema.validate({ a: 1 }).valid?).to be true 873 | end 874 | 875 | it 'should validate nil' do 876 | schema = Compel.hash.required 877 | 878 | result = schema.validate(nil) 879 | 880 | expect(result.valid?).to be false 881 | expect(result.errors[:base]).to \ 882 | include('is required') 883 | end 884 | 885 | end 886 | 887 | context '#is' do 888 | 889 | it 'should validate with errors' do 890 | value = { a: 1, b: 2, c: { d: 3, e: 4 }} 891 | schema = Compel.hash.is(value) 892 | 893 | result = schema.validate({ a: 1, b: 2, c: 3 }) 894 | 895 | expect(result.errors[:base]).to \ 896 | include("must be #{value.to_hash}") 897 | end 898 | 899 | it 'should validate without errors' do 900 | schema = Compel.hash.is({ a: 1, b: 2, c: 3 }) 901 | 902 | result = schema.validate({ 'a' => 1, 'b' => 2, 'c' => 3 }) 903 | expect(result.valid?).to be true 904 | 905 | result = schema.validate({ :a => 1, :b => 2, :c => 3 }) 906 | expect(result.valid?).to be true 907 | end 908 | 909 | end 910 | 911 | context '#length' do 912 | 913 | it 'should validate empty keys with errors' do 914 | result = Compel.hash.length(2).validate({ a: 1 }) 915 | 916 | expect(result.valid?).to be false 917 | expect(result.errors[:base]).to \ 918 | include('cannot have length different than 2') 919 | end 920 | 921 | end 922 | 923 | end 924 | 925 | context 'String' do 926 | 927 | it 'should validate Type schema' do 928 | schema = Compel.string.format(/^\d{4}-\d{3}$/).required 929 | result = schema.validate('1234') 930 | 931 | expect(result.errors).to \ 932 | include("must match format ^\\d{4}-\\d{3}$") 933 | end 934 | 935 | context '#url' do 936 | 937 | it 'should validate' do 938 | result = Compel.string.url.validate('http://example.com') 939 | 940 | expect(result.valid?).to be true 941 | end 942 | 943 | it 'should validate' do 944 | result = Compel.string.url.validate('http://app.com/posts/1/comments') 945 | 946 | expect(result.valid?).to be true 947 | end 948 | 949 | it 'should not validate' do 950 | result = Compel.string.url.validate('www.example.com') 951 | 952 | expect(result.valid?).to be false 953 | end 954 | 955 | it 'should not validate' do 956 | result = Compel.string.url.validate('url') 957 | 958 | expect(result.valid?).to be false 959 | end 960 | 961 | it 'should not validate and use custom error' do 962 | result = Compel.string.url(message: 'not an URL').validate('url') 963 | 964 | expect(result.errors).to include('not an URL') 965 | end 966 | 967 | end 968 | 969 | context '#email' do 970 | 971 | it 'should validate' do 972 | result = Compel.string.email.validate('example@gmail.com') 973 | 974 | expect(result.valid?).to be true 975 | end 976 | 977 | it 'should not validate' do 978 | result = Compel.string.email.validate('example@gmail') 979 | 980 | expect(result.valid?).to be false 981 | end 982 | 983 | it 'should not validate' do 984 | result = Compel.string.email.validate('email') 985 | 986 | expect(result.valid?).to be false 987 | end 988 | 989 | it 'should not validate and use custom error' do 990 | result = Compel.string.email(message: 'not an EMAIL').validate('email') 991 | 992 | expect(result.errors).to include('not an EMAIL') 993 | end 994 | 995 | end 996 | 997 | end 998 | 999 | context 'Array' do 1000 | 1001 | it 'should validate nil without errors' do 1002 | result = Compel.array.validate(nil) 1003 | 1004 | expect(result.valid?).to be true 1005 | end 1006 | 1007 | it 'should validate nil with errors' do 1008 | result = Compel.array.required.validate(nil) 1009 | 1010 | expect(result.errors[:base]).to include('is required') 1011 | end 1012 | 1013 | it 'should validate with errors for invalid array' do 1014 | result = Compel.array.required.validate(1) 1015 | 1016 | expect(result.errors[:base]).to include("'1' is not a valid Array") 1017 | end 1018 | 1019 | context '#items' do 1020 | 1021 | it 'should validate without items' do 1022 | result = Compel.array.validate([1, 2, 3]) 1023 | 1024 | expect(result.valid?).to be true 1025 | expect(result.value).to eq([1, 2, 3]) 1026 | end 1027 | 1028 | it 'should validate all items' do 1029 | result = Compel.array.items(Compel.integer).validate([1, '2', nil]) 1030 | 1031 | expect(result.valid?).to be true 1032 | expect(result.value).to eq([1, 2]) 1033 | end 1034 | 1035 | it 'should validate all items with errors' do 1036 | result = Compel.array.items(Compel.float.required).validate([1, 'a', nil]) 1037 | 1038 | expect(result.valid?).to be false 1039 | expect(result.errors['1']).to include("'a' is not a valid Float") 1040 | expect(result.errors['2']).to include('is required') 1041 | end 1042 | 1043 | it 'should coerce all hash items' do 1044 | builder = Compel.array.items(Compel.hash.keys({ 1045 | a: Compel.string.required, 1046 | b: Compel.integer 1047 | })) 1048 | 1049 | result = builder.validate([ 1050 | { a: 'A', b: '1' }, 1051 | { a: 'B' }, 1052 | { a: 'C', b: 3 }, 1053 | ]) 1054 | 1055 | expect(result.valid?).to be true 1056 | expect(result.value).to eq \ 1057 | [ 1058 | { a: 'A', b: 1 }, 1059 | { a: 'B' }, 1060 | { a: 'C', b: 3 } 1061 | ] 1062 | end 1063 | 1064 | it 'should coerce all hash items with errors' do 1065 | builder = Compel.array.items(Compel.hash.keys({ 1066 | a: Compel.string.required, 1067 | b: Compel.string.format(/^abc$/).required 1068 | })) 1069 | 1070 | result = builder.validate([ 1071 | { a: 'A', b: 'abc' }, 1072 | { a: 'B' }, 1073 | { a: 'C', b: 'abcd' }, 1074 | ]) 1075 | 1076 | expect(result.valid?).to be false 1077 | expect(result.errors['1'][:b]).to include('is required') 1078 | expect(result.errors['2'][:b]).to include('must match format ^abc$') 1079 | 1080 | expect(result.value[0][:a]).to eq('A') 1081 | expect(result.value[0][:b]).to eq('abc') 1082 | 1083 | expect(result.value[1][:a]).to eq('B') 1084 | expect(result.value[1][:b]).to be_nil 1085 | expect(result.value[1][:errors][:b]).to include('is required') 1086 | 1087 | expect(result.value[2][:a]).to eq('C') 1088 | expect(result.value[2][:b]).to eq('abcd') 1089 | expect(result.value[2][:errors][:b]).to \ 1090 | include('must match format ^abc$') 1091 | end 1092 | 1093 | it 'should coerce array with hash items and nested array keys with errors' do 1094 | builder = Compel.array.items(Compel.hash.keys({ 1095 | a: Compel.string, 1096 | b: Compel.array.items(Compel.integer.required).required 1097 | })) 1098 | 1099 | result = builder.validate([ 1100 | { a: 'C' }, 1101 | { b: [1, 2, 3] }, 1102 | { b: ['1', nil, 'a'] } 1103 | ]) 1104 | 1105 | expect(result.valid?).to be false 1106 | 1107 | expect(result.value[0][:a]).to eq('C') 1108 | expect(result.value[0][:b]).to be_nil 1109 | expect(result.value[1][:a]).to be_nil 1110 | expect(result.value[1][:b]).to eq([1, 2, 3]) 1111 | expect(result.value[2][:a]).to be_nil 1112 | expect(result.value[2][:b]).to eq([1, nil, 'a']) 1113 | 1114 | expect(result.errors['0'][:b]).to include('is required') 1115 | expect(result.errors['2'][:b]['1']).to include('is required') 1116 | expect(result.errors['2'][:b]['2']).to \ 1117 | include("'a' is not a valid Integer") 1118 | end 1119 | 1120 | it 'should coerce hash with array of hashes with errors' do 1121 | schema = Compel.hash.keys( 1122 | actions: Compel.array.items( 1123 | Compel.hash.keys( 1124 | a: Compel.string.required, 1125 | b: Compel.string.format(/^abc$/) 1126 | ) 1127 | ) 1128 | ) 1129 | 1130 | object = { 1131 | other_key: 1, 1132 | actions: [ 1133 | { a: 'A', b: 'abc' }, 1134 | { a: 'B' }, 1135 | { a: 'C', b: 'abcd' } 1136 | ] 1137 | } 1138 | 1139 | result = schema.validate(object) 1140 | 1141 | expect(result.value[:actions][2][:errors][:b]).to \ 1142 | include('must match format ^abc$') 1143 | end 1144 | 1145 | end 1146 | 1147 | context '#is' do 1148 | 1149 | it 'should validate with errors' do 1150 | value = [1, 2, 3] 1151 | result = Compel.array.is(value).validate([1, 2]) 1152 | 1153 | expect(result.valid?).to be false 1154 | expect(result.errors[:base]).to include("must be #{value}") 1155 | end 1156 | 1157 | it 'should validate without errors' do 1158 | result = Compel.array.is(['a', 'b', 'c']).validate(['a', 'b', 'c']) 1159 | 1160 | expect(result.valid?).to be true 1161 | end 1162 | 1163 | end 1164 | 1165 | context '#min_length' do 1166 | 1167 | it 'should validate empty array without errors' do 1168 | result = Compel.array.min_length(1).validate([]) 1169 | 1170 | expect(result.valid?).to be false 1171 | 1172 | expect(result.errors[:base]).to include \ 1173 | 'cannot have length less than 1' 1174 | end 1175 | 1176 | end 1177 | 1178 | context '#max_length' do 1179 | 1180 | it 'should validate empty array without errors' do 1181 | result = Compel.array.max_length(2).validate([1, 2, 3]) 1182 | 1183 | expect(result.valid?).to be false 1184 | 1185 | expect(result.errors[:base]).to include \ 1186 | 'cannot have length greater than 2' 1187 | end 1188 | 1189 | end 1190 | 1191 | end 1192 | 1193 | context 'DateTime' do 1194 | 1195 | it 'should validate with errors' do 1196 | result = Compel.datetime.validate('1989-0') 1197 | 1198 | expect(result.valid?).to be false 1199 | expect(result.errors).to \ 1200 | include("'1989-0' is not a parsable datetime with format: %FT%T") 1201 | end 1202 | 1203 | end 1204 | 1205 | context 'Integer' do 1206 | 1207 | context '#in' do 1208 | 1209 | context 'invalid' do 1210 | 1211 | it 'should use custom error message' do 1212 | schema = Compel.integer.in([1, 2], message: 'not in 1 or 2') 1213 | 1214 | expect(schema.validate(3).errors).to \ 1215 | include('not in 1 or 2') 1216 | end 1217 | 1218 | end 1219 | 1220 | end 1221 | 1222 | context '#range' do 1223 | 1224 | context 'invalid' do 1225 | 1226 | it 'should use custom error message' do 1227 | schema = Compel.integer.range([1, 2], message: 'not between 1 or 2') 1228 | 1229 | expect(schema.validate(3).errors).to \ 1230 | include('not between 1 or 2') 1231 | end 1232 | 1233 | end 1234 | 1235 | end 1236 | 1237 | context '#min' do 1238 | 1239 | context 'invalid' do 1240 | 1241 | it 'should use custom error message' do 1242 | schema = Compel.integer.min(5, message: 'min is five') 1243 | 1244 | expect(schema.validate(4).errors).to \ 1245 | include('min is five') 1246 | end 1247 | 1248 | end 1249 | 1250 | end 1251 | 1252 | context '#max' do 1253 | 1254 | context 'invalid' do 1255 | 1256 | it 'should use custom error message' do 1257 | schema = Compel.integer.max(5, message: 'max is five') 1258 | 1259 | expect(schema.validate(6).errors).to \ 1260 | include('max is five') 1261 | end 1262 | 1263 | end 1264 | 1265 | end 1266 | 1267 | end 1268 | 1269 | end 1270 | 1271 | end 1272 | 1273 | end 1274 | --------------------------------------------------------------------------------