Skip to content

Instantly share code, notes, and snippets.

@SkyyySi
Last active December 22, 2024 13:35
Show Gist options
  • Save SkyyySi/d26554e1b10890debdda5f5ca66105c2 to your computer and use it in GitHub Desktop.
Save SkyyySi/d26554e1b10890debdda5f5ca66105c2 to your computer and use it in GitHub Desktop.
local getmetatable = getmetatable
local type = type
local assert = assert
local rawget = rawget
---@class private
local _M = {}
--- A helper function to check whether the type of a given parameter is correct,
--- generating an error message if it is not.
---@generic T
---@param function_name string
---@param expected_type `T` | type
---@param parameter_name string
---@param parameter_index integer
---@param parameter_value T
---@return boolean type_is_ok
---@return nil | string error_message
local function check_parameter_type(function_name, expected_type, parameter_name, parameter_index, parameter_value)
local parameter_type = type(parameter_value)
if parameter_type == expected_type then
return true, nil
end
return false, ("Wrong type of parameter #%d '%s' to function '%s()'! (expected %s but got %s)"):format(
parameter_index,
parameter_name,
function_name,
expected_type,
parameter_type
)
end
--- A helper function that converts a readable key name (like `"foo"`) into a
--- private key name (like `"__PRIVATE_MyClass::foo__"`).
---
--- The table `tb` **must** have a metatable with a `__name` string field.
---@generic K : string
---@generic V
---@param tb table<K, V>
---@param key string
---@return K | nil private_key
---@return nil | string error_message
local function private_key_helper(tb, key)
---@type metatable | nil
local mt = getmetatable(tb)
if type(mt) ~= "table" then
return nil, "The given table has no metatable!"
end
---@type string | nil
local name = rawget(mt, "__name")
if type(name) ~= "string" then
return nil, "The given table has no '__name' metatable field!"
end
local private_key = ("__PRIVATE__%s::%s__"):format(name, key)
return private_key, nil
end
--- Get the value of a private field `key` of a table `tb`.
---
--- The table `tb` **must** have a metatable with a `__name` string field.
---@generic K : string
---@generic V
---@param tb table<K, V>
---@param key string
---@return V value
function _M.get(tb, key)
assert(check_parameter_type("get", "table", "tb", 1, tb))
assert(check_parameter_type("get", "string", "key", 2, key))
local private_key = assert(private_key_helper(tb, key))
local value = tb[private_key]
return value
end
--- Set a private field `key` of a table `tb` to `value`.
---
--- The table `tb` **must** have a metatable with a `__name` string field.
---@generic K : string
---@generic V
---@param tb table<K, V>
---@param key string
---@param value V
---@return nil
function _M.set(tb, key, value)
assert(check_parameter_type("set", "table", "tb", 1, tb))
assert(check_parameter_type("set", "string", "key", 2, key))
local private_key = assert(private_key_helper(tb, key))
tb[private_key] = value
return nil
end
--------------------------------------------------------------------------------
--- Testing code ---
--------------------------------------------------------------------------------
---@param args string[]
---@retuen integer exit_status_code
function _M.main(args)
local print = print
local format = string.format
local test_class_name = "MyClass"
local test_key = "foo"
local test_value = "bar"
local test_tb = setmetatable({}, {
__name = test_class_name,
})
print(format([[>>> test_tb = setmetatable({}, {
>>> __name = %q,
>>> }]], test_class_name))
print()
print(format(">>> private.get(test_tb, %q) = %s", test_key, _M.get(test_tb, test_key)))
print(format(">>> private.set(test_tb, %q, %q)", test_key, test_value))
_M.set(test_tb, test_key, test_value)
print(format(">>> private.get(test_tb, %q) = %q", test_key, _M.get(test_tb, test_key)))
print()
for k, v in pairs(test_tb) do
print(format("test_tb[%q] = %q", k, v))
end
end
--- This is only `true` if this module is directly being executed, and not if
--- it is loaded with `require()`.
---
--- Note: `load()`, `dofile()`, etc. will also run this, so you're better off
--- not using them.
if ... == nil then
os.exit(_M.main(arg))
end
return _M
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment