From ac907df634534b05e7a45af79301d0bec29cfc0d Mon Sep 17 00:00:00 2001 From: Aleksa Sarai Date: Sat, 6 Nov 2021 16:09:12 +1100 Subject: [PATCH] util: add reversible table method wrapping helper In some cases, it's useful to be able to wrap a function and either replace its contents entirely or have some callback be run before calling the underlying function. The most obvious users for this feature are the Japanese and Korean keyboards (both of which need to wrap the inputbox methods with either their own versions or have basic callbacks be run before the method is executed). This is loosely based on how busted/luassert spies work. Signed-off-by: Aleksa Sarai --- frontend/util.lua | 84 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 84 insertions(+) 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