Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow additional optional and rest arguments in classes that implement interfaces #1

Merged
merged 2 commits into from
Apr 19, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Gemfile.lock
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
PATH
remote: .
specs:
interfaceable (0.1.1)
interfaceable (0.1.2)

GEM
remote: https://rubygems.org/
Expand Down
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
Loading