Skip to content

Instantly share code, notes, and snippets.

@kkuchta
Created March 19, 2023 06:47
Show Gist options
  • Save kkuchta/68ad9cc07639aa7e15aa08011f03382a to your computer and use it in GitHub Desktop.
Save kkuchta/68ad9cc07639aa7e15aa08011f03382a to your computer and use it in GitHub Desktop.
A definition of a .again method in ruby
# A wrapper class that defines ".again", but passes through all other methods to
# the wrapped class.
class AgainWrapper < BasicObject
def initialize(wrapped_object, again_method)
@again_method = again_method
@wrapped_object = wrapped_object
end
def again
@wrapped_object.send(@again_method)
end
def method_missing(method_name, *args, **kwargs, &block)
@wrapped_object.send(method_name, *args, **kwargs, &block)
end
def respond_to_missing?(method_name, include_all)
@wrapped_object.respond_to?(method_name, include_all)
end
end
# An example custom class to to test this all out on
class Example
attr_accessor :foo
def initialize(foo)
@foo = foo
end
def increment
@foo += 1
return self
end
end
# An example method on a core class
class String
def indent
return " " + self
end
end
# Go through every currently-defined class (by recursing through subclasses of
# Object).
def walk_subclasses(klass, &block)
block[klass]
klass.subclasses.each do
walk_subclasses(_1, &block)
end
end
walk_subclasses(Object) do |klass|
# Don't override methods on these classes to avoid stack overflows
next if AgainWrapper == klass || klass <= IO
# For each method on this class, wrap that method so that it calls the
# original method, but wraps the return value in AgainWrapper.
klass.instance_methods(false).each do |method_name|
# Skip a few methods that were problematic to override
if method_name == :method_missing || method_name == :respond_to_missing? || method_name == :again || method_name == :respond_to? || method_name == :exception || method_name == :to_s
next
end
alias_name = "__aliased__#{method_name}"
old_method = klass.alias_method(alias_name, method_name)
klass.define_method(method_name) do |*args, **kwargs, &block|
normal_return_value = self.send(alias_name, *args, **kwargs, &block)
# Don't wrap the results of methods returning true or false - you'll end
# up potentially returning a truthy value (an AgainWrapper instance)
# instead of a falsey one (false).
if normal_return_value == false || normal_return_value == true
normal_return_value
else
AgainWrapper.new(normal_return_value, method_name)
end
end
end
end
example = Example.new(1)
example.increment.again.again
puts example.foo # 4
result = "somestring".indent.again.again.again
puts result # " somestring"
mixed_objects_result = 123.to_s.indent.again.again.again
puts mixed_objects_result # " 123"
core_method_result = 123.succ.again.again
puts core_method_result # 126
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment