Skip to content

Commit

Permalink
Merge pull request #1 from Verseth/master
Browse files Browse the repository at this point in the history
Allow additional optional and rest arguments in classes that implement interfaces
  • Loading branch information
artemave authored Apr 19, 2024
2 parents 0138892 + 0687c75 commit 34d2da4
Show file tree
Hide file tree
Showing 3 changed files with 71 additions and 6 deletions.
18 changes: 18 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
13 changes: 13 additions & 0 deletions lib/interfaceable/implementation_check.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand All @@ -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,
Expand Down
46 changes: 40 additions & 6 deletions spec/implementation_check_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down

0 comments on commit 34d2da4

Please sign in to comment.