require_relative "sig/version" require_relative "sig/kernel" module Sig class ArgumentTypeError < ArgumentError end class ResultTypeError < RuntimeError end def self.define(object, expected_arguments, expected_result = nil, method_name) no_argument_checks = expected_arguments.nil? expected_arguments = Array(expected_arguments) if expected_arguments.last.is_a?(Hash) expected_keyword_arguments = expected_arguments.delete_at(-1) else expected_keyword_arguments = nil end method_visibility = get_method_visibility_or_raise(object, method_name) signature_checker = get_or_create_signature_checker(object) signature_checker.send :define_method, method_name do |*arguments, **keyword_arguments| if keyword_arguments.empty? ::Sig.check_arguments(expected_arguments, arguments) unless no_argument_checks result = super(*arguments) else ::Sig.check_arguments_with_keywords(expected_arguments, arguments, expected_keyword_arguments, keyword_arguments) unless no_argument_checks result = super(*arguments, **keyword_arguments) end ::Sig.check_result(expected_result, result) unless expected_result.nil? result end signature_checker.send(method_visibility, method_name) method_name end def self.get_method_visibility_or_raise(object, method_name) case when object.private_method_defined?(method_name) :private when object.protected_method_defined?(method_name) :protected when object.public_method_defined?(method_name) :public else raise ArgumentError, "No method with name :#{method_name} for object #{object.inspect}" end end def self.get_or_create_signature_checker(object) unless checker = object.instance_variable_get(:@_sig) checker = object.instance_variable_set(:@_sig, Module.new) def checker.inspect() "#" end object.prepend(checker) end checker end def self.check_arguments(expected_arguments, arguments) errors = "" arguments.each_with_index{ |argument, index| if error = valid_or_formatted_error(expected_arguments[index], argument) errors << error end } unless errors.empty? raise ArgumentTypeError, errors end end def self.check_arguments_with_keywords(expected_arguments, arguments, expected_keyword_arguments, keyword_arguments) errors = "" arguments.each_with_index{ |argument, index| if error = valid_or_formatted_error(expected_arguments[index], argument) errors << error end } if expected_keyword_arguments keyword_arguments.each{ |key, keyword_argument| if error = valid_or_formatted_error(expected_keyword_arguments[key], keyword_argument) errors << error end } elsif error = valid_or_formatted_error(expected_arguments[arguments.size], keyword_arguments) errors << error end unless errors.empty? raise ArgumentTypeError, errors end end def self.check_result(expected_result, result) unless matches? expected_result, result raise ResultTypeError, format_error(expected_result, result) end end def self.matches?(expected, value) # Runtime eval the constants expected = constantize(expected) # Match case expected when Array expected.any?{ |expected_element| matches? expected_element, value } when Module value.is_a?(expected) when Symbol value.respond_to?(expected) when Proc !!expected.call(value) when Regexp !!(expected =~ String(value)) when Range expected.include?(value) when true !!value when false !value when nil true else raise ArgumentError, "Invalid signature definition: Unknown behavior #{expected}" end end def self.constantize(expected) if expected.is_a? String return Object.const_get(expected) elsif expected.is_a? Array return expected.map { |el| constantize(el) } end expected end def self.valid_or_formatted_error(expected_argument, argument) if !expected_argument.nil? && !matches?(expected_argument, argument) format_error(expected_argument, argument) end end def self.format_error(expected, value) case expected when Array expected.map{ |expected_element| format_error(expected_element, value) }*" OR" when Module, String "\n- Expected #{value.inspect} to be a #{expected}, but is a #{value.class}" when Symbol "\n- Expected #{value.inspect} to respond to :#{expected}" when Proc "\n- Expected #{value.inspect} to return a truthy value for proc #{expected}" when Regexp "\n- Expected stringified #{value.inspect} to match #{expected.inspect}" when Range "\n- Expected #{value.inspect} to be included in #{expected.inspect}" when true "\n- Expected #{value.inspect} to be truthy" when false "\n- Expected #{value.inspect} to be falsy" end end end