|
RSpec.shared_examples_for "an interface" do |objects, class_methods: false| |
|
let(:class_methods) { class_methods } |
|
|
|
it "classes have compatible interfaces" do |
|
objects.each_cons(2) do |left, right| |
|
left_params = params_for(left, method(:normalize_params)) |
|
right_params = params_for(right, method(:normalize_params)) |
|
|
|
diff = (left_params - right_params) + (right_params - left_params) |
|
|
|
expect(left_params).to( |
|
match_array(right_params), [ |
|
"Expected #{left} and #{right} to have compatible method ", |
|
"signatures, but the following signatures do not match: \n\n" |
|
].join + method_signatures(left, right, diff) |
|
) |
|
end |
|
end |
|
|
|
def method_signatures(left, right, diff) |
|
methods = diff.map(&:first).uniq |
|
left_params = params_for(left).to_h.slice(*methods) |
|
right_params = params_for(right).to_h.slice(*methods) |
|
|
|
methods.map do |m, params| |
|
<<~DIFF |
|
#{join_signature(left, m, left_params)} |
|
#{join_signature(right, m, right_params)} |
|
DIFF |
|
end.join("\n") |
|
end |
|
|
|
def join_signature(object, m, params) |
|
m_for_display = class_methods ? "self.#{m}" : m |
|
|
|
signature = if params[m] |
|
"#{m_for_display}(#{params[m].join(", ")})" |
|
else |
|
"#{m_for_display} not defined" |
|
end |
|
|
|
"#{object}: #{signature}" |
|
end |
|
|
|
# Normalizes method parameters to enable interface comparison. For |
|
# example, two methods may use different names for positional |
|
# arguments, but if the parameter types and order match, they #{}hould |
|
# be considered equivalent. This method replaces argument names with |
|
# sequential placeholders when appropriate, focusing the comparison on |
|
# parameter structure rather than naming. |
|
def normalize_params(params) |
|
sequential_name = ("a".."z").to_enum |
|
|
|
params.map do |type, name| |
|
if [:req, :opt, :rest, :keyrest, :block].include?(type) |
|
name = sequential_name.next |
|
end |
|
|
|
[type, name] |
|
end |
|
end |
|
|
|
def params_for(object, params_processor = -> { _1 }) |
|
methods = class_methods ? object.public_methods : object.public_instance_methods |
|
|
|
(methods - Object.methods).map do |m| |
|
params = object.instance_method(m).parameters |
|
args = params_processor.call(params).map do |type, name| |
|
case type |
|
when :key then "#{name}: :opt" |
|
when :keyreq then "#{name}:" |
|
when :keyrest then "**#{name}" |
|
when :block then "&#{name}" |
|
when :req then name.to_s |
|
when :opt then "#{name} = :opt" |
|
when :rest then "*#{name}" |
|
end |
|
end |
|
|
|
[m, args] |
|
end |
|
end |
|
end |