From d8d1585c8dc267bdd31cb21bd12de2531359a0b4 Mon Sep 17 00:00:00 2001 From: Mateusz Drewniak Date: Fri, 19 Apr 2024 14:30:54 +0200 Subject: [PATCH 1/2] Allow additional optional and rest arguments in classes --- Gemfile.lock | 2 +- lib/interfaceable/implementation_check.rb | 13 +++++ spec/implementation_check_spec.rb | 67 ++++++++++++++++++++--- 3 files changed, 73 insertions(+), 9 deletions(-) diff --git a/Gemfile.lock b/Gemfile.lock index df0410c..1bf5245 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,7 +1,7 @@ PATH remote: . specs: - interfaceable (0.1.1) + interfaceable (0.1.2) GEM remote: https://rubygems.org/ 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..b7a4b8b 100644 --- a/spec/implementation_check_spec.rb +++ b/spec/implementation_check_spec.rb @@ -49,6 +49,29 @@ def foo(aaa, baz = 3, bar:, fuga: 2); end } } ) + + interface = Module.new do + def foo(aaa, bbb); end + end + klass = Class.new do + def foo(aaa, baz); end + end + + errors = Interfaceable::ImplementationCheck.new(klass).perform([interface]) + + expect(errors).to be_empty + + 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 @@ -102,6 +125,26 @@ def self.foo(aaa, bar = 1); end } } ) + + klass = Class.new do + def self.foo(aaa, bar = 1, *args); end + end + + errors = Interfaceable::ImplementationCheck.new(klass).perform([interface]) + + expect(errors).to be_empty + + interface = Module.new do + def self.foo(aaa, baz = 3); end + end + klass = Class.new do + 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 @@ -114,14 +157,8 @@ def foo(aaa, bar = 1, *args, foo:, **opts); 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'] - } - } - ) + # allow the class to have additional rest parameters + expect(errors).to be_empty interface = Module.new do def foo(aaa, baz = 3, *args, foo:, **options); end @@ -129,6 +166,20 @@ def foo(aaa, baz = 3, *args, foo:, **options); end errors = Interfaceable::ImplementationCheck.new(klass).perform([interface]) expect(errors).to be_empty + + 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, 'keyrest'], + actual: ['req', 'opt', 'rest', :foo] + } + } + ) end end # rubocop:enable Metrics/BlockLength From 0687c7533d4916f3bed02a69178ee54edba37fc0 Mon Sep 17 00:00:00 2001 From: Mateusz Drewniak Date: Fri, 19 Apr 2024 16:10:31 +0200 Subject: [PATCH 2/2] Add new test cases and update README --- README.md | 18 +++++++++++ spec/implementation_check_spec.rb | 53 +++++++++++-------------------- 2 files changed, 36 insertions(+), 35 deletions(-) diff --git a/README.md b/README.md index 34efd2b..fc21bd1 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/spec/implementation_check_spec.rb b/spec/implementation_check_spec.rb index b7a4b8b..13de6d2 100644 --- a/spec/implementation_check_spec.rb +++ b/spec/implementation_check_spec.rb @@ -49,18 +49,9 @@ def foo(aaa, baz = 3, bar:, fuga: 2); end } } ) + end - interface = Module.new do - def foo(aaa, bbb); end - end - klass = Class.new do - def foo(aaa, baz); end - end - - errors = Interfaceable::ImplementationCheck.new(klass).perform([interface]) - - expect(errors).to be_empty - + it 'accepts additional optional arguments' do interface = Module.new do def foo(aaa, bbb); end end @@ -125,15 +116,9 @@ def self.foo(aaa, bar = 1); end } } ) + end - klass = Class.new do - def self.foo(aaa, bar = 1, *args); end - end - - errors = Interfaceable::ImplementationCheck.new(klass).perform([interface]) - - expect(errors).to be_empty - + it 'accepts additional *rest argument' do interface = Module.new do def self.foo(aaa, baz = 3); end end @@ -148,25 +133,9 @@ def self.foo(aaa, bar = 1, *args); end end it 'checks **opts argument' do - interface = Module.new do - 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 - interface = Module.new do def foo(aaa, baz = 3, *args, foo:, **options); end end - - errors = Interfaceable::ImplementationCheck.new(klass).perform([interface]) - expect(errors).to be_empty - klass = Class.new do def foo(aaa, bar = 1, *args, foo:); end end @@ -181,5 +150,19 @@ def foo(aaa, bar = 1, *args, foo:); end } ) end + + it 'accepts additional **opts argument' do + interface = Module.new do + 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 # rubocop:enable Metrics/BlockLength