diff --git a/frontend/util.lua b/frontend/util.lua index 99779e953..f05f74c87 100644 --- a/frontend/util.lua +++ b/frontend/util.lua @@ -1344,4 +1344,88 @@ function util.stringEndsWith(str, ending) return ending == "" or str:sub(-#ending) == ending end +local WrappedFunction_mt = { + __call = function(self, ...) + if self.before_callback then + self.before_callback(self.target_table, ...) + end + if self.func then + return self.func(...) + end + end, +} + +--- Wrap (or replace) a table method with a custom method, in a revertable way. +-- This allows you extend the features of an existing module by modifying its +-- internal methods, and then revert them back to normal later if necessary. +-- +-- The most notable use-case for this is VirtualKeyboard's inputbox method +-- wrapping to allow keyboards to add more complicated state-machines to modify +-- how characters are input. +-- +-- The returned table is the same table `target_table[target_field_name]` is +-- set to. In addition to being callable, the new method has two sub-methods: +-- +-- * `:revert()` will un-wrap the method and revert it to the original state. +-- +-- Note that if a method is wrapped multiple times, reverting it will revert +-- it to the state of the method when util.wrapMethod was called (and if +-- called on the table returned from util.wrapMethod, that is the state when +-- that particular util.wrapMethod was called). +-- +-- * `:raw_call(...)` will call the original method with the given arguments +-- and return whatever it returns. +-- +-- This makes it more ergonomic to use the wrapped table methods in the case +-- where you've replaced the regular function with your own implementation +-- but you need to call the original functions inside your implementation. +-- +-- * `:raw_method_call(...)` will call the original method with the arguments +-- `(target_table, ...)` and return whatever it returns. Note that the +-- target_table used is the one associated with the util.wrapMethod call. +-- +-- This makes it more ergonomic to use the wrapped table methods in the case +-- where you've replaced the regular function with your own implementation +-- but you need to call the original functions inside your implementation. +-- +-- This is effectively short-hand for `:raw_call(target_table, ...)`. +-- +-- This is loosely based on busted/luassert's spies implementation (MIT). +-- +-- +-- @tparam table target_table The table whose method will be wrapped. +-- @tparam string target_field_name The name of the field to wrap. +-- @tparam nil|func new_func If non-nil, this function will be called instead of the original function after wrapping. +-- @tparam nil|func before_callback If non-nil, this function will be called (with the arguments (target_table, ...)) before the function is called. +function util.wrapMethod(target_table, target_field_name, new_func, before_callback) + local old_func = target_table[target_field_name] + local wrapped = setmetatable({ + target_table = target_table, + target_field_name = target_field_name, + old_func = old_func, + + before_callback = before_callback, + func = new_func or old_func, + + revert = function(self) + if not self.reverted then + self.target_table[self.target_field_name] = self.old_func + self.reverted = true + end + end, + + raw_call = function(self, ...) + if self.old_func then + return self.old_func(...) + end + end, + + raw_method_call = function(self, ...) + return self:raw_call(self.target_table, ...) + end, + }, WrappedFunction_mt) + target_table[target_field_name] = wrapped + return wrapped +end + return util