Last active
August 3, 2018 19:34
-
-
Save kenkeiter/4009563 to your computer and use it in GitHub Desktop.
Guard-like things in Ruby
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
DO NOT USE THIS EVER FOR ANY REASON GOD DAMN WHY DO I HAVE TO EVEN TELL PEOPLE THAT