This wiki is still in active development, many areas of the site are unstable or broken.
In the meantime, consider adding content to pages you like!
Module:BucketPut
From NBITTRPG Wiki
Documentation for this module may be created at Module:BucketPut/doc
local p = {}
local getArgs = require('Module:Arguments').getArgs
local yesno = require('Module:Yesno')
local lang = mw.language.getContentLanguage()
local O_TYPE = "type"
local O_DELIM = "delimiter"
local O_REQUIRED = "required"
local OPTIONS = { O_TYPE, O_DELIM, O_REQUIRED }
local O_ALIASES = {
delim = 'delimiter',
notnull = 'required',
}
local C_TARGS = "templateargs"
local C_SUB = "sub"
local C_NOSAVE = "nosave"
local C_LOG = "log"
local CONF_VALS = { C_TARGS, C_SUB, C_NOSAVE, C_LOG }
---Is `value` one of the `targets`?
---@param value string Type you want to check is in list.
---@param targets string[] List of targets that `value` could be.
---@return boolean
local function oneof(value, targets)
for _, target in ipairs(targets) do
if value == target then
return true
end
end
return false
end
---Parse a single value based on type name
---@param t string Type name
---@param v string Value to parse
---@return boolean success Did parsing succeed?
---@return any value Parsed value if success, string if fail.
function p.parse_typed_value(t, v)
-- probably should return an object instead
if oneof(t, { "text", "page" }) then
-- possible TODO with page
return true, v
end
if oneof(t, { "int", "integer" }) then
local val = tonumber(v)
if val ~= nil and val % 1 == 0 then
return true, val
else
return false, "integer"
end
end
if oneof(t, { "float", "double", "num", "number" }) then
local val = tonumber(v)
if val ~= nil then
return true, val
else
return false, "number"
end
end
if oneof(t, { "bool", "boolean", "yesno" }) then
local val = yesno(v)
if val ~= nil then
return true, val
else
return false, "boolean"
end
end
if oneof(t, { "date", "time", "datetime", "timestamp" }) then
local s, r = pcall(lang.formatDate, lang, "c", v)
if s then
return s, r
else
return false, "timestamp"
end
end
if oneof(t, { "unixtime" }) then
local s, r = pcall(lang.formatDate, lang, "U", v)
if s then
r = tonumber(r)
or error("Could not convert " .. r .. " to a number")
return s, r
else
return false, "Unix timestamp"
end
end
if oneof(t, { "json" }) then
local s, r = pcall(mw.text.jsonDecode, v)
if s then
return s, r
else
return false, "JSON"
end
end
error("Unsupported type: " .. mw.dumpObject(t))
end
---Recursively and safely add items of `keylist` to `args`.
---
---`add_to_args({}, { "a", "b", "c" }, "d")`
---will yield
---`{ a = { b = { c = "d" } } }`
---@param args table
---@param keylist (string | number)[]
---@param value any
local function add_to_args(args, keylist, value)
local nextvalue = args
for i, valuename in ipairs(keylist) do
if i == #keylist then
nextvalue[valuename] = value
else
nextvalue[valuename] = nextvalue[valuename] or {}
nextvalue = nextvalue[valuename]
end
end
end
---Parse specific key-value combination and modify output table.
---@param out_args table
---@param out_config table
---@param key string
---@param value string
---@return nil
function p.parse_args(out_args, out_config, key, value)
if type(key) ~= "string" then
return
end
value = mw.text.trim(value)
if value == '' then
return
end
-- basic conditions for parser working
local optkey, optname = key:match("^([%w_]+)%-(.+)$")
-- [%w_] matches all valid bucket field names at time of writing
-- (non-exhaustively, i.e. all valid names are matched by [%w_], but [%w_]
-- also matches some invalid names)
if optkey and optname then
if O_ALIASES[optname] then
optname = O_ALIASES[optname]
end
if not oneof(optname, OPTIONS) then
local fmt = "Invalid option %s for parameter %s"
error(fmt:format(optname, optkey))
end
return add_to_args(out_args, { optkey, optname }, value)
end
local ckey = key:match("^%+(.+)$")
if ckey then
if not oneof(ckey, CONF_VALS) then
error(ckey .. " not a recognised config option")
end
out_config[ckey] = value
-- modify out_config rather than out_args
return
end
return add_to_args(out_args, { key, 1 }, value)
-- none of the above apply so treat as raw arg
end
---Turn parsed argument object to single value to send to `mw.bucket.put`.
---@param schema table
---@param argname string
---@param data table
---@return any
function p.get_bucket_arg(schema, argname, data)
local item = data[1]
if not item then
if not yesno(data[O_REQUIRED]) then
return
end
error(("Required argument %s was not given"):format(argname))
end
item = mw.text.trim(item)
local type = data[O_TYPE] or schema[argname].type or "TEXT"
type = type:lower()
if not data[O_DELIM] then
local success, value_or_typename = p.parse_typed_value(type, item)
if success then
if value_or_typename == nil then
error(("Trying to save %s as nil"):format(mw.dumpObject(argname)))
end
return value_or_typename
-- value
else
local fmt = "Invalid %s given for parameter %s: %s"
error(fmt:format(value_or_typename, mw.dumpObject(argname), mw.dumpObject(item)))
-- typename
end
end
local delim = data[O_DELIM]
delim = delim:gsub('%f[\\]\\n', '\n')
delim = delim:gsub('\\\\n', '\\n')
--[[
removes a backslash from start, so:
\n -> newline
\\n -> \n
\\\n -> \\n
etc.
]]
local items = {}
for entry in mw.text.gsplit(item, delim) do
entry = mw.text.trim(entry)
if entry ~= "" then
local success, value_or_typename = p.parse_typed_value(type, entry)
if success then
table.insert(items, value_or_typename)
-- value
else
local fmt = "Invalid %s given for parameter %s: %s"
error(fmt:format(value_or_typename, mw.dumpObject(argname), mw.dumpObject(entry)))
-- typename
end
end
end
return items
end
---Turn list of parsed arguments into `mw.bucket.put` arg table.
---@param schema table
---@param out_args table
---@return table
function p.get_bucket_args(schema, out_args)
local bucket_args = {}
for arg, data in pairs(out_args) do
bucket_args[arg] = p.get_bucket_arg(schema, arg, data)
end
return bucket_args
end
---Main invokable function.
---@param frame any
function p.put(frame)
local args = getArgs(frame, { frameOnly = true })
local parent_args = getArgs(frame, { parentOnly = true })
local bucket_name = mw.text.trim(args[1] or '')
local schema = mw.loadJsonData('bucket:' .. bucket_name)
-- Replicate the behavior of $wgCargoStoreUseTemplateArgsFallback for specified arguments
local implicit_store_args = mw.text.trim(args['+' .. C_TARGS] or '')
for arg in implicit_store_args:gmatch("[^,]+") do
arg = mw.text.trim(arg)
args[arg] = parent_args[arg]
end
-- Convert all arguments to consistent format
local out_args = {}
local out_config = {}
for k, v in pairs(args) do
p.parse_args(out_args, out_config, k, v)
end
local bucket_args = p.get_bucket_args(schema, out_args)
if yesno(out_config[C_LOG]) then
mw.log("Passed args: " .. mw.dumpObject(args))
mw.log("out_args: " .. mw.dumpObject(out_args))
mw.log("out_config: " .. mw.dumpObject(out_config))
mw.log("bucket_args: " .. mw.dumpObject(bucket_args))
end
-- put changes to bucket
if not yesno(out_config[C_NOSAVE]) then
local bucket = mw.bucket(bucket_name)
if out_config[C_SUB] then
bucket.sub(out_config[C_SUB])
end
bucket.put(bucket_args)
end
end
return p
