From 4b69526ae53c3548ae0d9611e9bec5e92cb73c08 Mon Sep 17 00:00:00 2001 From: kinduff Date: Mon, 14 Apr 2025 16:47:38 -0600 Subject: [PATCH 1/3] Adds type checker for objects --- lib/errgonomic.rb | 3 +++ lib/errgonomic/type.rb | 53 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 56 insertions(+) create mode 100644 lib/errgonomic/type.rb 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..bba5dc6 --- /dev/null +++ b/lib/errgonomic/type.rb @@ -0,0 +1,53 @@ +# 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. + 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_method :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. + 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. + 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. + 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 From cc3d9df43c35b7af0d52730bf9f35d0d1990780b Mon Sep 17 00:00:00 2001 From: kinduff Date: Tue, 15 Apr 2025 14:06:25 -0600 Subject: [PATCH 2/3] Adds examples for yard --- lib/errgonomic/type.rb | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/lib/errgonomic/type.rb b/lib/errgonomic/type.rb index bba5dc6..2c80acf 100644 --- a/lib/errgonomic/type.rb +++ b/lib/errgonomic/type.rb @@ -7,6 +7,10 @@ class Object # @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) # => raises TypeMismatchError + # 123.type_or_raise!(String, "Not a string") # => raises TypeMismatchError with custom message def type_or_raise!(type, message = nil) message ||= "Expected #{type} but got #{self.class}" raise Errgonomic::TypeMismatchError, message unless is_a?(type) @@ -14,13 +18,16 @@ def type_or_raise!(type, message = nil) self end - alias_method :type_or_raise, :type_or_raise! + 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) @@ -33,6 +40,9 @@ def type_or(type, default) # @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) @@ -44,6 +54,10 @@ def type_or_else(type, &block) # @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) # => raises TypeMismatchError + # 123.not_type_or_raise!(Integer, "Not an integer") # => raises TypeMismatchError with custom message 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) From 49c1630c25670374243706e1fc36c3e22aedc80d Mon Sep 17 00:00:00 2001 From: kinduff Date: Tue, 15 Apr 2025 14:18:30 -0600 Subject: [PATCH 3/3] Fixes yard doctest --- lib/errgonomic/type.rb | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/lib/errgonomic/type.rb b/lib/errgonomic/type.rb index 2c80acf..48e4938 100644 --- a/lib/errgonomic/type.rb +++ b/lib/errgonomic/type.rb @@ -8,9 +8,9 @@ class Object # @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) # => raises TypeMismatchError - # 123.type_or_raise!(String, "Not a string") # => raises TypeMismatchError with custom message + # '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) @@ -26,8 +26,8 @@ def type_or_raise!(type, message = nil) # @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" + # 'hello'.type_or(String, 'default') # => "hello" + # 123.type_or(String, 'default') # => "default" def type_or(type, default) return self if is_a?(type) @@ -41,8 +41,8 @@ def type_or(type, default) # @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" + # '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) @@ -55,9 +55,9 @@ def type_or_else(type, &block) # @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) # => raises TypeMismatchError - # 123.not_type_or_raise!(Integer, "Not an integer") # => raises TypeMismatchError with custom message + # '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)