module InternalApi
The InternalApi
module provides one public method (`.internal_api`) available on any Ruby module. This method takes as its single argument any object has public methods. When called, the (public) methods of the caller will no longer be directly accessible.
This is deliberately designed to not depend on any gems, C-extensions, or any Ruby features specific to a minor version.
Constants
- LoaderMutex
- VERSION
Public Instance Methods
check_caller!(protector)
click to toggle source
# File lib/internal_api.rb, line 50 def check_caller!(protector) allowed_caller_methods = InternalApi.public_method_cache[protector] # NB: `caller` is much slower than `caller_locations` caller_locations.each do |location| # This calculation is quadratic but as the backtrace is finite and these # comparisons take only tens of nanoseconds each this is fast enough for # production use. allowed_caller_methods.each do |path, range| if location.path == path && range.include?(location.lineno) return path, range end end end raise_violation!(caller_locations[1].label, protector) end
debug(line)
click to toggle source
# File lib/internal_api.rb, line 83 def debug(line) puts "InternalApi: #{line}" if $DEBUG end
protect(protectee, protector)
click to toggle source
Rewrites all public methods on the protectee (the Ruby class or module that received the 'internal_api' message), replacing them with a method that checks the backtrace and ensures at least one line matches one of the public methods of the protector.
# File lib/internal_api.rb, line 35 def protect(protectee, protector) calculate_public_methods!(protector) # Extract the eigenclass of any object # https://medium.com/@ethan.reid.roberts/rubys-anonymous-eigenclass-putting-the-ei-in-team-ebc1e8f8d668 eigenclass = (class << protectee; self; end) # Rewrite future public singleton methods Rewriter.add_singleton_rewrite_hooks!(protectee, protector) # Rewrite eigenclass' future public instance methods Rewriter.add_instance_rewrite_hooks!(eigenclass, protector) # Rewrite eigenclass' future public singleton methods Rewriter.add_singleton_rewrite_hooks!(eigenclass, protector) end
public_method_cache()
click to toggle source
# File lib/internal_api.rb, line 79 def public_method_cache @public_method_cache ||= {} end
rewrite_method!(protectee, internal_method, protector)
click to toggle source
# File lib/internal_api.rb, line 66 def rewrite_method!(protectee, internal_method, protector) protectee.instance_eval do # We create a new pointer to the original method alias_method "_internal_api_#{internal_method}", internal_method # And overwrite it define_method internal_method do |*args, &block| InternalApi.check_caller!(protector) send("_internal_api_#{internal_method}", *args, &block) end end end
Private Instance Methods
calculate_public_methods!(mod)
click to toggle source
# File lib/internal_api.rb, line 96 def calculate_public_methods!(mod) LoaderMutex.synchronize do return if InternalApi.public_method_cache.key?(mod) # We cache the public methods because this requires a fairly exhaustive, # recursive lookup of Ruby method hierarchy to perform: # # https://github.com/ruby/ruby/blob/c3cf1ef9bbacac6ae5abc99046db173e258dc7ca/class.c#L1206-L1238 # # > Benchmark.measure { 10_000.times { Object.new }}.real # => 0.0033939999993890524 # >> Benchmark.measure { 10_000.times { Object.public_methods }}.real # => 0.1327720000408589800 # # It's up to the user to avoid adding new public methods to the protected # code after app initialization. source_ranges = FullMethodSourceLocation.public_method_source_ranges(mod) unless source_ranges raise InternalApi::UnreachableCodeError, "#{self} is protected by #{protector}," \ ' which has no public methods' end InternalApi.public_method_cache[mod] = source_ranges end end
raise_violation!(label, protector)
click to toggle source
# File lib/internal_api.rb, line 89 def raise_violation!(label, protector) message = "#{label.inspect} is protected by `#{protector.name}`" \ " and can only execute when a `#{protector.name}`" \ ' method is in the backtrace' raise InternalApi::ViolationError, message end