lua-users home
lua-l archive

[Date Prev][Date Next][Thread Prev][Thread Next] [Date Index] [Thread Index]


Here is my go - not particularly well tested, but with the following
additional ideas:

* A limit can be set on how "deep" the table may become
* The table can be "finalized" and can, together with all the
automagically created sub-tables, be turned into a non-extensible
table again.
* When accessing a non-existent key of a finalized table, an error
message is given that names the missing key.

One motivation for this was to use configuration files that only
should consist of entries like

    a.b.c = 1
    a.d[1] = "test"
    e.f = a.b.c

To prevent strange errors resulting from mis-configuration, if a
configuration file entry like "e.x" was required but wasn't present,
one would be alerted that this specific entry is missing.

This approach to configuration may not be suitable for end users, but
it has helped me a lot with fine-tuning a process requiring hundreds
of configuration entries.


2016-02-23 10:07 GMT+01:00 Dirk Laurie <dirk.laurie@gmail.com>:
> 2016-02-23 7:45 GMT+02:00 Philipp Janda <siffiejoe@gmx.net>:
>> Am 23.02.2016 um 06:24 schröbte Dirk Laurie:
>>
>>> A module is attached that contains the following comments:
>>>
>>> -- Self-initializing table. The point is that `tbl[key]` is never nil.
>>  ...
>>> Something like this may well have been done before. Please supply
>>> pointers; I'll gladly concede priority.
>>
>>
>> http://lua-users.org/wiki/AutomagicTables
>
> Thanks for the reference. Just getting to understand why that
> version manages to work without using rawget was an education.
>
> I like my version better. Don't we all? :-)
>
---
-- Module that allows creating auto-extending tables.
-- @module tablemagic



-- Tables with weak keys to avoid "memory leaks"
local weakKeyMetatable = {__mode = "k"}
local subtableDepths = setmetatable({}, weakKeyMetatable)
local names = setmetatable({}, weakKeyMetatable)
local errorMessages = setmetatable({}, weakKeyMetatable)


local autoExtensible__index = function(parentTable, key)
    -- We use the parent's metatable as the metatable for the subtable
    -- so that when we finalize it, we can simply replace the __index
    -- metamethod so that all sub tables of this table are finalized.
    local newSubtable = setmetatable({}, getmetatable(parentTable))
    local parentDepth = subtableDepths[parentTable]
    if parentDepth then
        if parentDepth < 2 then
            -- If only 1 nesting level is reached, this level is only allowed to be filled with
            -- primitive types, not with further auto-extending tables.
            error("The maximum table depth that was specified when calling createAutoExtendingTable() is reached.")
        end
        subtableDepths[newSubtable] = parentDepth - 1
    end
    local nameStep
    if type(key) == "string" and string.match(key, "^[%a_][%w_]*$") then
        nameStep = "."..key
    else
        nameStep = "["..tostring(key).."]"
    end
    names[newSubtable] = names[parentTable]..nameStep
    parentTable[key] = newSubtable

    return newSubtable
end



local finalized__index = function(t, key)
    local errorMessage = errorMessages[t] or "Key not present in finalized table:"
    error(errorMessage.."\n"..names[t].."."..key)
end


---
-- @function finalize
-- @tparam t table
--   The table to be finalized. After finalizing, it won't be auto-extensible any more.
--   The table must have been created by this module's `createAutoExtendingTable()`
--   function.
-- @tparam[opt] boolean protect
--   If `protect` evaluates to `true`, then the table will be made "read-only" (a `__newindex`
--   metamethod will throw an error on any attempt to create a new entry). Note that
--   `rawset()` will still be able to bypass that and create new values.
-- @tparam[opt] string errorMessage
--   When after finalizing the table with `tablemagic.finalize()`, a
--   non-existent key is accessed, the provided error message plus the
--   "complete path" of the failed key.
-- @treturn table
--   Returns the table to allow chaining.
local function finalize(t, protect, errorMessage)
    local name = names[t]
    -- if there is no registered name, then t is not an
    -- auto-extending table that was created by this module.
    if name == nil then
        error("Can not finalize table that was not created by createAutoextendingTable()")
    end
    errorMessages[t] = errorMessage
    local metatable = getmetatable(t)
    metatable.__index = finalized__index
    if protect then
        metatable.__newindex = protected__newindex
    end
    return t
end



---
-- @function createAutoExtendingTable
-- @tparam[opt] number depth
--   Maximum nesting depth. Default is infinite depth.
--   This number can be thought of as the number of dots that is allowed in a
--   table "path".
--
--    t = (require "lib.tablemagic").createAutoExtendingTable(3)
--    t.foo.bar.baz=0      -- O.K., 3 dots -> creates an entry at table depth 3
--    t.boo.far.foo.bar=1  -- not O.K., exceeds the "number of allowed dots"
-- @tparam[opt] t
--   A table may be provided that is turned into an auto-extending table.
--   No copy is created. If the provided table already has a metatable,
--   this metatable will be replaced by the metatable used for making the
--   table auto-extensible.
-- @tparam[opt] string name
--   An optional name for the auto extending table for more meaningful error
--   messages when accessing a non-existent key after the table has been
--   finalized by `finalize()`.  If used, it is recommended to set this
--   parameter the variable name by which the created auto-extending table is
--   typically accessible.
--
-- @treturn table
local function createAutoExtendingTable(depth, t, name)
    t = t or {}
    -- depth is the maximum nesting depth inside t subtables
    subtableDepths[t] = depth
    names[t] = name or ""
    return setmetatable(t, {__index = autoExtensible__index})
end


return {
    createAutoExtendingTable = createAutoExtendingTable,
    finalize = finalize
}