Another cool way you could do this: Ruby methods can ask for their caller_locations. They work like this:
# Source
$ cat example_for_hn.rb
def foo
bar
end
def bar
baz
end
def baz
(c1, c2) = caller_locations.first(2)
puts "Parent caller: '#{c1.label}' in '#{c1.path}'"
puts "Grandparent caller: '#{c2.label}' in '#{c2.path}'"
end
foo
# Demo
$ ruby example_for_hn.rb
Parent caller: 'bar' in 'example_for_hn.rb'
Grandparent caller: 'foo' in 'example_for_hn.rb'
So, you could define a method decorating class method like so:
module Shitlist
def shitlist(method_name, whitelist)
original_method = instance_method(method_name)
undef_method(method_name)
define_method(method_name) do |*args, &block|
call = caller_locations.first
passes_whitelist = whitelist.any? do |label, file_pattern|
call.label == label && call.absolute_path.end_with?(file_pattern)
end
unless passes_whitelist
fail "Shitlisted method! Permitted callers: #{whitelist}"
end
original_method.bind(self).call(*args, &block)
end
end
end
and then extend classes with it to use the decorator:
class Example
extend Shitlist
def not_on_shitlist
qux
end
def baz
qux
end
def qux
puts 'Only some methods can call me :)'
end
shitlist :qux, 'baz' => 'shitlist.rb'
end
If I run this example (full source: https://git.io/JLOdV), the non-whitelisted caller throws an error:
$ ruby shitlist.rb
Only some methods can call me :)
Traceback (most recent call last):
2: from shitlist.rb:44:in `<main>'
1: from shitlist.rb:25:in `not_on_shitlist'
shitlist.rb:13:in `block in shitlist': Shitlisted method! Permitted callers: {"baz"=>"shitlist.rb"} (RuntimeError)
---
Of course, you might not want this hijacked method with tracing inside something performance critical. You could always configure the implementation to be a no-op in production.
Of course, you might not want this hijacked method with tracing inside something performance critical. You could always configure the implementation to be a no-op in production.