Skip to content

Instantly share code, notes, and snippets.

@thiagoa
Created July 18, 2025 18:35
Show Gist options
  • Save thiagoa/4080ebc0ab1f2dc561c1a204450554a7 to your computer and use it in GitHub Desktop.
Save thiagoa/4080ebc0ab1f2dc561c1a204450554a7 to your computer and use it in GitHub Desktop.
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
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment