diff --git a/README.md b/README.md index f810743..f9141ed 100644 --- a/README.md +++ b/README.md @@ -57,6 +57,24 @@ Will fail because of method signature mismatch: - expected arguments: (req, req) - actual arguments: (req, opt=) +Classes may define additional optional or rest arguments. + +```ruby +module Carrier + def call(number); end + + def text(number, text); end +end + +class Giffgaff + def call(number, *opts); end + + def text(number, text, opt1 = nil, opt2 = nil); end +end +``` + +This will not generate any errors since `Giffgaff` implements the required methods with correct arguments only adding new optional ones. + ### Rails Mix in `Interfaceable` before any of the application code is loaded. For example, in the initializer. For extra peace of mind, you can noop interface checking in production: diff --git a/lib/interfaceable/implementation_check.rb b/lib/interfaceable/implementation_check.rb index 14f5e12..43a7907 100644 --- a/lib/interfaceable/implementation_check.rb +++ b/lib/interfaceable/implementation_check.rb @@ -73,6 +73,16 @@ def own_methods(methods) methods - Object.methods end + OPTIONAL_PARAMETERS = %w[opt rest keyrest] + + def check_if_parameters_are_compatible(expected_parameters, actual_parameters) + return false if actual_parameters.length < expected_parameters.length + return false if actual_parameters.take(expected_parameters.length) != expected_parameters + + additional_parameters = actual_parameters[expected_parameters.length..] + additional_parameters.all? { OPTIONAL_PARAMETERS.include?(_1) } + end + # rubocop:disable Metrics/MethodLength def check_method_signature(expected_parameters, actual_parameters) expected_keyword_parameters, expected_positional_parameters = simplify_parameters( @@ -85,6 +95,9 @@ def check_method_signature(expected_parameters, actual_parameters) return if expected_positional_parameters == actual_positional_parameters && expected_keyword_parameters == actual_keyword_parameters + return if check_if_parameters_are_compatible(expected_positional_parameters, actual_positional_parameters) && + check_if_parameters_are_compatible(expected_keyword_parameters, actual_keyword_parameters) + { expected_positional_parameters: expected_positional_parameters, expected_keyword_parameters: expected_keyword_parameters, diff --git a/spec/implementation_check_spec.rb b/spec/implementation_check_spec.rb index d45bd8a..13de6d2 100644 --- a/spec/implementation_check_spec.rb +++ b/spec/implementation_check_spec.rb @@ -51,6 +51,20 @@ def foo(aaa, baz = 3, bar:, fuga: 2); end ) end + it 'accepts additional optional arguments' do + interface = Module.new do + def foo(aaa, bbb); end + end + klass = Class.new do + def foo(aaa, baz, bar = 5, err = nil); end + end + + errors = Interfaceable::ImplementationCheck.new(klass).perform([interface]) + + # allow the class to define additional optional arguments + expect(errors).to be_empty + end + it 'checks class method signature' do interface = Module.new do def self.foo(aaa, baz = 3, bar:, fuga: 2); end @@ -104,30 +118,50 @@ def self.foo(aaa, bar = 1); end ) end - it 'checks **opts argument' do + it 'accepts additional *rest argument' do interface = Module.new do - def foo(aaa, baz = 3, *args, foo:); end + def self.foo(aaa, baz = 3); end end klass = Class.new do - def foo(aaa, bar = 1, *args, foo:, **opts); end + def self.foo(aaa, bar = 1, *args); end end errors = Interfaceable::ImplementationCheck.new(klass).perform([interface]) + # allow class to define an additional rest argument + expect(errors).to be_empty + end + + it 'checks **opts argument' do + interface = Module.new do + def foo(aaa, baz = 3, *args, foo:, **options); end + end + klass = Class.new do + def foo(aaa, bar = 1, *args, foo:); end + end + errors = Interfaceable::ImplementationCheck.new(klass).perform([interface]) + expect(errors[interface][:instance_method_signature_errors]).to eq( { foo: { - expected: ['req', 'opt', 'rest', :foo], - actual: ['req', 'opt', 'rest', :foo, 'keyrest'] + expected: ['req', 'opt', 'rest', :foo, 'keyrest'], + actual: ['req', 'opt', 'rest', :foo] } } ) + end + it 'accepts additional **opts argument' do interface = Module.new do - def foo(aaa, baz = 3, *args, foo:, **options); end + def foo(aaa, baz = 3, *args, foo:); end + end + klass = Class.new do + def foo(aaa, bar = 1, *args, foo:, **opts); end end errors = Interfaceable::ImplementationCheck.new(klass).perform([interface]) + + # allow the class to have additional rest parameters expect(errors).to be_empty end end