Skip to content

Instantly share code, notes, and snippets.

@kenkeiter
Last active August 3, 2018 19:34
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save kenkeiter/4009563 to your computer and use it in GitHub Desktop.
Save kenkeiter/4009563 to your computer and use it in GitHub Desktop.
Guard-like things in Ruby
class Proc
def call_with_vars(vars, *args)
Struct.new(*vars.keys).new(*vars.values).instance_exec(*args, &self)
end
end
module Touchy
class GuardError < Exception; end
class GuardSignatureError < GuardError; end
class GuardCriteriaError < GuardError; end
module Guards
def self.included(base)
base.send :extend, ClassMethods
# Install a wrapper proc which wraps the next encountered method in
# with a proxy which validates the guard.
base.instance_variable_set :@guard, lambda{ |*tests|
raise GuardCriteriaError, "No criteria provided." if tests.empty?
base.instance_variable_set :@guard_next, lambda{ |scope, meth|
base.send(:guard_method, meth, tests, scope)
}
}
end
module ClassMethods
def guard_method(meth, tests, scope = nil)
scope = self if scope.nil?
# clear guard so that it's not triggered while we manipulate methods
self.instance_variable_set :@guard_next, nil
# convert the newly defined method to a proc, memoize its signature, and remove it
pattern_proc = scope.instance_method(meth)
scope.instance_eval do
@_guards ||= {} # define guard chain hash for scope.
@_method_params ||= {}
pattern_proc_signature = instance_method(meth).parameters
if @_method_params.has_key?(meth) and not (@_method_params[meth] <=> pattern_proc_signature)
raise GuardSignatureError, "Method signature does not match previously defined signature."
else
@_method_params[meth] = pattern_proc_signature
end
end
scope.send :undef_method, meth
guard_chain = scope.instance_variable_get :@_guards
# track the method in the list of guards, if it's not already.
guard_chain[meth] ||= []
guard_chain[meth] << [tests, pattern_proc]
# define a replacement method which evaluates the guard chain.
# side note: this could be *wayyyy* more efficient.
scope.send :define_method, meth do |*args, &block|
params = scope.instance_variable_get(:@_method_params)[meth]
test_vars, _args = Hash[params.map{|t, n| [n, nil] }], args.dup
test_vars[:instance] = self
params.select{|t, _| t == :req or t == :opt }.each{ |_, name| test_vars[name] = _args.shift }
params.select{|t, _| t == :block }.each{ |_, name| test_vars[name] = block }
params.select{|t, _| t == :rest }.each{ |_, name| test_vars[name] = _args }
guard_chain[meth].each do |tests, matched_pattern_proc|
if tests.all?{ |test| !!test.call_with_vars(test_vars) }
return matched_pattern_proc.clone.bind(self).call(*args, &block)
end
end
end
end
def singleton_method_added(name)
super(name)
singleton = (class << self; self; end)
if wrap = @guard_next
wrap[singleton, name] if wrap
end
end
def method_added(name)
super(name)
if wrap = @guard_next
wrap[self, name] if wrap
end
end
end
end
end
if $0 == __FILE__
class Person
include Touchy::Guards
def initialize(name)
@name = name
end
@guard[->{ age < 21 }]
def serve_drink(age)
puts "You're too young for a drink, #{@name}"
end
@guard[->{ age >= 21 }]
def serve_drink(age)
puts "Here's your drink, #{@name}!"
end
end
Person.new('Jack').serve_drink(18)
Person.new('Jill').serve_drink(21)
end
@kenkeiter
Copy link
Author

DO NOT USE THIS EVER FOR ANY REASON GOD DAMN WHY DO I HAVE TO EVEN TELL PEOPLE THAT

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment