diff --git a/lib/errgonomic.rb b/lib/errgonomic.rb index 348e7df..4a61e4b 100644 --- a/lib/errgonomic.rb +++ b/lib/errgonomic.rb @@ -5,6 +5,9 @@ # A more opinionated blend with Rails presence. require_relative 'errgonomic/presence' +# Bring in a subtle and manual type checker. +require_relative 'errgonomic/type' + # Bring in our Option and Result. require_relative 'errgonomic/option' require_relative 'errgonomic/result' diff --git a/lib/errgonomic/type.rb b/lib/errgonomic/type.rb new file mode 100644 index 0000000..48e4938 --- /dev/null +++ b/lib/errgonomic/type.rb @@ -0,0 +1,67 @@ +# frozen_string_literal: true + +class Object + # Returns the receiver if it matches the expected type, otherwise raises a TypeMismatchError. + # This is useful for enforcing type expectations in method arguments. + # + # @param type [Class] The expected type or module the receiver should be. + # @param message [String] Optional error message to raise if type doesn't match. + # @return [Object] The receiver if it is of the expected type. + # @example + # 'hello'.type_or_raise!(String) #=> "hello" + # 123.type_or_raise!(String, "We need a string!") #=> raise Errgonomic::TypeMismatchError, "We need a string!" + # 123.type_or_raise!(String) #=> raise Errgonomic::TypeMismatchError, "Expected String but got Integer" + def type_or_raise!(type, message = nil) + message ||= "Expected #{type} but got #{self.class}" + raise Errgonomic::TypeMismatchError, message unless is_a?(type) + + self + end + + alias type_or_raise type_or_raise! + + # Returns the receiver if it matches the expected type, otherwise returns the default value. + # + # @param type [Class] The expected type or module the receiver should be. + # @param default [Object] The value to return if type doesn't match. + # @return [Object] The receiver if it is of the expected type, otherwise the default value. + # @example + # 'hello'.type_or(String, 'default') # => "hello" + # 123.type_or(String, 'default') # => "default" + def type_or(type, default) + return self if is_a?(type) + + default + end + + # Returns the receiver if it matches the expected type, otherwise returns the result of the block. + # Useful when constructing the default value is expensive. + # + # @param type [Class] The expected type or module the receiver should be. + # @param block [Proc] The block to call if type doesn't match. + # @return [Object] The receiver if it is of the expected type, otherwise the block result. + # @example + # 'hello'.type_or_else(String) { 'default' } # => "hello" + # 123.type_or_else(String) { 'default' } # => "default" + def type_or_else(type, &block) + return self if is_a?(type) + + block.call + end + + # Returns the receiver if it does not match the expected type, otherwise raises a TypeMismatchError. + # + # @param type [Class] The type or module the receiver should not be. + # @param message [String] Optional error message to raise if type matches. + # @return [Object] The receiver if it is not of the specified type. + # @example + # 'hello'.not_type_or_raise!(Integer) #=> "hello" + # 123.not_type_or_raise!(Integer, "We dont want an integer!") #=> raise Errgonomic::TypeMismatchError, "We dont want an integer!" + # 123.not_type_or_raise!(Integer) #=> raise Errgonomic::TypeMismatchError, "Expected anything but Integer but got Integer" + def not_type_or_raise!(type, message = nil) + message ||= "Expected anything but #{type} but got #{self.class}" + raise Errgonomic::TypeMismatchError, message if is_a?(type) + + self + end +end