Skip to content

Commit

Permalink
Merge pull request #12 from kylekthompson/kt-strict-method
Browse files Browse the repository at this point in the history
Introduce Strict::Method for validating method calls
  • Loading branch information
kylekthompson authored Oct 12, 2022
2 parents 2303938 + 1c04e43 commit 8c90dcd
Show file tree
Hide file tree
Showing 12 changed files with 875 additions and 0 deletions.
7 changes: 7 additions & 0 deletions lib/strict/accessor/module.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,13 @@
module Strict
module Accessor
class Module < ::Module
attr_reader :configuration

# rubocop:disable Metrics/MethodLength
def initialize(configuration)
super()

@configuration = configuration
const_set(Strict::Attributes::Configured::CONSTANT, configuration)
configuration.attributes.each do |attribute|
module_eval(
Expand All @@ -33,6 +36,10 @@ def #{attribute.name}=(value) # def name
end
end
# rubocop:enable Metrics/MethodLength

def inspect
"#<#{self.class} (#{configuration.attributes.map(&:name).join(', ')})>"
end
end
end
end
52 changes: 52 additions & 0 deletions lib/strict/method.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
# frozen_string_literal: true

module Strict
module Method
def self.extended(mod)
return if mod.singleton_class?

mod.singleton_class.extend(self)
end

def sig(&)
instance = singleton_class? ? self : singleton_class
instance.instance_variable_set(:@__strict_method_internal_last_sig_configuration, Methods::Dsl.run(&))
end

# rubocop:disable Metrics/MethodLength
def singleton_method_added(method_name)
super

sig = singleton_class.instance_variable_get(:@__strict_method_internal_last_sig_configuration)
singleton_class.instance_variable_set(:@__strict_method_internal_last_sig_configuration, nil)
return unless sig

verifiable_method = Methods::VerifiableMethod.new(
method: singleton_class.instance_method(method_name),
parameters: sig.parameters,
returns: sig.returns,
instance: false
)
verifiable_method.verify_definition!
singleton_class.prepend(Methods::Module.new(verifiable_method))
end

def method_added(method_name)
super

sig = singleton_class.instance_variable_get(:@__strict_method_internal_last_sig_configuration)
singleton_class.instance_variable_set(:@__strict_method_internal_last_sig_configuration, nil)
return unless sig

verifiable_method = Methods::VerifiableMethod.new(
method: instance_method(method_name),
parameters: sig.parameters,
returns: sig.returns,
instance: true
)
verifiable_method.verify_definition!
prepend(Methods::Module.new(verifiable_method))
end
# rubocop:enable Metrics/MethodLength
end
end
72 changes: 72 additions & 0 deletions lib/strict/method_call_error.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
# frozen_string_literal: true

module Strict
class MethodCallError < Error
attr_reader :verifiable_method, :remaining_args, :remaining_kwargs, :invalid_parameters, :missing_parameters

def initialize(verifiable_method:, remaining_args:, remaining_kwargs:, invalid_parameters:, missing_parameters:)
super(
message_from(verifiable_method:, remaining_args:, remaining_kwargs:, invalid_parameters:, missing_parameters:)
)

@verifiable_method = verifiable_method
@remaining_args = remaining_args
@remaining_kwargs = remaining_kwargs
@invalid_parameters = invalid_parameters
@missing_parameters = missing_parameters
end

private

def message_from(verifiable_method:, remaining_args:, remaining_kwargs:, invalid_parameters:, missing_parameters:)
details = [
invalid_parameters_message_from(invalid_parameters),
missing_parameters_message_from(missing_parameters),
remaining_args_message_from(remaining_args),
remaining_kwargs_message_from(remaining_kwargs)
].compact.join("\n")

"Calling #{verifiable_method} failed because:\n#{details}"
end

def invalid_parameters_message_from(invalid_parameters)
return nil unless invalid_parameters

details = invalid_parameters.map do |parameter, value|
" - #{parameter.name}: got #{value.inspect}, expected #{parameter.validator.inspect}"
end.join("\n")

" Some arguments were invalid:\n#{details}"
end

def missing_parameters_message_from(missing_parameters)
return nil unless missing_parameters

details = missing_parameters.map do |parameter_name|
" - #{parameter_name}"
end.join("\n")

" Some arguments were missing:\n#{details}"
end

def remaining_args_message_from(remaining_args)
return nil if remaining_args.none?

details = remaining_args.map do |arg|
" - #{arg.inspect}"
end.join("\n")

" Additional positional arguments were provided, but not defined:\n#{details}"
end

def remaining_kwargs_message_from(remaining_kwargs)
return nil if remaining_kwargs.none?

details = remaining_kwargs.map do |key, value|
" - #{key}: #{value.inspect}"
end.join("\n")

" Additional keyword arguments were provided, but not defined:\n#{details}"
end
end
end
46 changes: 46 additions & 0 deletions lib/strict/method_definition_error.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
# frozen_string_literal: true

module Strict
class MethodDefinitionError < Error
attr_reader :verifiable_method, :missing_parameters, :additional_parameters

def initialize(verifiable_method:, missing_parameters:, additional_parameters:)
super(message_from(verifiable_method:, missing_parameters:, additional_parameters:))

@verifiable_method = verifiable_method
@missing_parameters = missing_parameters
@additional_parameters = additional_parameters
end

private

def message_from(verifiable_method:, missing_parameters:, additional_parameters:)
details = [
missing_parameters_message_from(missing_parameters),
additional_parameters_message_from(additional_parameters)
].compact.join("\n")

"Defining #{verifiable_method} failed because:\n#{details}"
end

def missing_parameters_message_from(missing_parameters)
return nil unless missing_parameters.any?

details = missing_parameters.map do |parameter_name|
" - #{parameter_name}"
end.join("\n")

" Some parameters were in the `sig`, but were not in the parameter list:\n#{details}"
end

def additional_parameters_message_from(additional_parameters)
return nil unless additional_parameters.any?

details = additional_parameters.map do |parameter_name|
" - #{parameter_name}"
end.join("\n")

" Some parameters were not in the `sig`, but were in the parameter list:\n#{details}"
end
end
end
25 changes: 25 additions & 0 deletions lib/strict/method_return_error.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
# frozen_string_literal: true

module Strict
class MethodReturnError < Error
attr_reader :verifiable_method, :value

def initialize(verifiable_method:, value:)
super(message_from(verifiable_method:, value:))

@verifiable_method = verifiable_method
@value = value
end

private

def message_from(verifiable_method:, value:)
details = invalid_returns_message_from(verifiable_method, value)
"#{verifiable_method}'s return value was invalid because:\n#{details}"
end

def invalid_returns_message_from(verifiable_method, value)
" - got #{value.inspect}, expected #{verifiable_method.returns.validator.inspect}"
end
end
end
26 changes: 26 additions & 0 deletions lib/strict/methods/module.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
# frozen_string_literal: true

module Strict
module Methods
class Module < ::Module
attr_reader :verifiable_method

def initialize(verifiable_method)
super()

@verifiable_method = verifiable_method
define_method verifiable_method.name do |*args, **kwargs, &block|
args, kwargs = verifiable_method.verify_parameters!(*args, **kwargs)

super(*args, **kwargs, &block).tap do |value|
verifiable_method.verify_returns!(value)
end
end
end

def inspect
"#<#{self.class} (#{verifiable_method.name})>"
end
end
end
end
Loading

0 comments on commit 8c90dcd

Please sign in to comment.