lib/kind/validator.rb in kind-5.0.0 vs lib/kind/validator.rb in kind-5.1.0
- old
+ new
@@ -1,7 +1,9 @@
# frozen_string_literal: true
+require 'kind'
+
module Kind
module Validator
DEFAULT_STRATEGIES = ::Set.new(%w[instance_of kind_of]).freeze
class InvalidDefinition < ArgumentError
@@ -35,6 +37,111 @@
else
raise InvalidDefaultStrategy.new(option)
end
end
end
+end
+
+require 'active_model'
+require 'active_model/validations'
+
+class KindValidator < ActiveModel::EachValidator
+ def validate_each(record, attribute, value)
+ return if options[:allow_nil] && value.nil?
+
+ return unless error = call_validation_for(attribute, value)
+
+ raise Kind::Error.new("#{attribute} #{error}") if options[:strict]
+
+ record.errors.add(attribute, error)
+ end
+
+ private
+
+ def call_validation_for(attribute, value)
+ expected = options[:with] || options[:in]
+
+ return validate_with_default_strategy(expected, value) if expected
+
+ return kind_of(expected, value) if expected = options[:of]
+ return kind_is(expected, value) if expected = options[:is]
+ return respond_to(expected, value) if expected = options[:respond_to]
+ return instance_of(expected, value) if expected = options[:instance_of]
+ return array_with(expected, value) if expected = options[:array_with]
+ return array_of(expected, value) if expected = options[:array_of]
+
+ raise Kind::Validator::InvalidDefinition.new(attribute)
+ end
+
+ def validate_with_default_strategy(expected, value)
+ send(Kind::Validator.default_strategy, expected, value)
+ end
+
+ def kind_of(expected, value)
+ types = Array(expected)
+
+ return if types.any? { |type| type === value }
+
+ "must be a kind of: #{types.map { |type| type.name }.join(', ')}"
+ end
+
+ CLASS_OR_MODULE = 'Class/Module'.freeze
+
+ def kind_is(expected, value)
+ return kind_is_not(expected, value) unless expected.kind_of?(::Array)
+
+ result = expected.map { |kind| kind_is_not(kind, value) }.tap(&:compact!)
+
+ result.empty? || result.size < expected.size ? nil : result.join(', ')
+ end
+
+ def kind_is_not(expected, value)
+ case expected
+ when ::Class
+ return if expected == Kind::Class[value] || value < expected
+
+ "must be the class or a subclass of `#{expected.name}`"
+ when ::Module
+ return if value.kind_of?(Class) && value <= expected
+ return if expected == Kind.of_module_or_class(value) || value.kind_of?(expected)
+
+ "must include the `#{expected.name}` module"
+ else
+ raise Kind::Error.new(CLASS_OR_MODULE, expected)
+ end
+ end
+
+ def respond_to(expected, value)
+ method_names = Array(expected)
+
+ expected_methods = method_names.select { |method_name| !value.respond_to?(method_name) }
+ expected_methods.map! { |method_name| "`#{method_name}`" }
+
+ return if expected_methods.empty?
+
+ methods = expected_methods.size == 1 ? 'method' : 'methods'
+
+ "must respond to the #{methods}: #{expected_methods.join(', ')}"
+ end
+
+ def instance_of(expected, value)
+ types = Array(expected)
+
+ return if types.any? { |type| value.instance_of?(type) }
+
+ "must be an instance of: #{types.map { |klass| klass.name }.join(', ')}"
+ end
+
+ def array_with(expected, value)
+ return if value.kind_of?(::Array) && !value.empty? && (value - Kind::Array[expected]).empty?
+
+ "must be an array with: #{expected.join(', ')}"
+ end
+
+ def array_of(expected, value)
+ types = Array(expected)
+
+ return if value.kind_of?(::Array) && !value.empty? && value.all? { |val| types.any? { |type| type === val } }
+
+ "must be an array of: #{types.map { |klass| klass.name }.join(', ')}"
+ end
end