Difference between revisions of "Module:Util"

From Granblue Fantasy Wiki
Jump to navigation Jump to search
(remove_ref references have a hex counter)
Line 834: Line 834:
  
 
util.cargo = {}
 
util.cargo = {}
 +
 +
function util.cargo.escape_sqli(source)
 +
    local replacements = { ['"'] = '\\"', ["'"] = "\\'" }
 +
    return string.gsub( source, "['\"]", replacements )
 +
end
  
 
function util.cargo.declare(frame, args)
 
function util.cargo.declare(frame, args)

Revision as of 22:18, 7 October 2019

This is a programming module, do not invoke it directly.

Overview

Provides utility functions for programming modules.

Structure

Group Description
util.cast utilities for casting values (i.e. from arguments)
util.html shorthand functions for creating some html tags
util.misc miscellaneous functions

Tests

☑Y All tests passed.

Name Expected Actual
☑Y test_args_stats__errors
☑Y test_args_stats__min_max
☑Y test_args_stats__value
☑Y test_cast_boolean
☑Y test_cast_number
☑Y test_cast_number__error_invalid_number
☑Y test_cast_number__error_max
☑Y test_cast_number__error_min
☑Y test_cast_number__invalid_with_default
☑Y test_html_abbr
☑Y test_html_error
☑Y test_misc_add_category
☑Y test_misc_get_frame
☑Y test_misc_is_frame
☑Y test_smw_safeguard
☑Y test_string_split_args
☑Y test_table_find_in_nested_array__assoc

-- From https://pathofexile.gamepedia.com/Module:Util
-- Utility stuff

local xtable = require('Module:Table')
local util = {}

local string_format = string.format
local infinity = math.huge

local mw = mw
local cargo = mw.ext.cargo


local i18n = {
    bool_false = {'false', '0', 'disabled', 'off', 'no', '', 'deactivated'},
    args = {
        -- util.args.stat
        stat_infix = 'stat',
        stat_id = 'id',
        stat_min = 'min',
        stat_max = 'max',
        stat_value = 'value',
    
        -- util.args.weight_list
        spawn_weight_prefix = 'spawn_weight',
        generation_weight_prefix = 'generation_weight',
    },
    
    errors = {
        -- util.cast.factory.*
        missing_element = 'Element "%s" not found',
        
        -- util.cast.factory.percentage
        invalid_argument = 'Argument "%s" is invalid. Please check the documentation for acceptable values.',
        not_a_percentage = '%s must be a percentage (in range 0 to 100).',
        
        -- util.cast.boolean
        not_a_boolean = 'value "%s" of type "%s" is not a boolean',
        
        -- util.cast.number
        not_a_number = 'value "%s" of type "%s" is not an integer',
        number_too_small = '"%i" is too small. Minimum: "%i"',
        number_too_large = '"%i" is too large. Maximum: "%i"',
        
        -- util.cast.version
        malformed_version_string = 'Malformed version string "%s"',
        non_number_version_component = '"%s" has an non-number component',
        unrecognized_version_number = '"%s" is not a recognized version number',
        
        -- util.args.stats
        improper_stat = '%sstat%s is improperly set; id and either value or min/max must be specified.',
        
        -- util.args.weight_list
        invalid_weight = 'Both %s and %s must be specified',
        
        -- util.args.version
        too_many_versions = 'The number of results (%s) does not match the number version arguments (%s)',
        
        -- util.html.error
        module_error = 'Module Error: ',
        
        -- util.misc.raise_error_or_return
        invalid_raise_error_or_return_usage = 'Invalid usage of raise_error_or_return.',
        
        -- util.cargo.array_query
        duplicate_ids = 'Found duplicates for field "%s":\n %s', 
        missing_ids = 'Missing results for "%s" field with values: \n%s',
        
        -- util.smw.array_query
        duplicate_ids_found = 'Found multiple pages for id property "%s" with value "%s": %s, %s',
        missing_ids_found = 'No results were found for id property "%s" with the following values: %s',
        
        -- util.string.split_args
        number_of_arguments_too_large = 'Number of arguments near = is too large (%s).',
    },
}

-- ----------------------------------------------------------------------------
-- util.cast
-- ----------------------------------------------------------------------------

util.cast = {}

function util.cast.boolean(value)
    -- Takes an abitary value and casts it to a bool value
    --
    -- for strings false will be according to i18n.bool_false
    local t = type(value)
    if t == 'nil' then
        return false
    elseif t == 'boolean' then
        return value
    elseif t == 'number' then
        if value == 0 then return false end
        return true
    elseif t == 'string' then
        local tmp = string.lower(value)
        for _, v in ipairs(i18n.bool_false) do
            if v == tmp then
                return false
            end
        end
        return true
    else
        error(string.format(i18n.errors.not_a_boolean, tostring(value), t))
    end

end

function util.cast.number(value, args)
    -- Takes an abitary value and attempts to cast it to int
    --
    -- args:
    --  default: for strings, if default is nil and the conversion fails, an error will be returned
    --  min: error if <min
    --  max: error if >max
    if args == nil then
        args = {}
    end

    local t = type(value)
    local val

    if t == 'nil' then
        val = nil
    elseif t == 'boolean' then
        if value then
            val = 1
        else
            val = 0
        end
    elseif t == 'number' then
        val = value
    elseif t == 'string' then
        val = tonumber(value)
    end

    if val == nil then
        if args.default ~= nil then
            val = args.default
        else
            error(string.format(i18n.errors.not_a_number, tostring(value), t))
        end
    end

    if args.min ~= nil and val < args.min then
        error(string.format(i18n.errors.number_too_small, val, args.min))
    end

    if args.max ~= nil and val > args.max then
        error(string.format(i18n.errors.number_too_large, val, args.max))
    end

    return val
end

function util.cast.version(value, args)
    -- Takes a string value and returns as version number
    -- If the version number is invalid an error is raised
    --
    -- args:
    --  return_type: defaults to "table"
    --   table  - Returns the version number broken down into sub versions as a table
    --   string - Returns the version number as string
    --
    if args == nil then
        args = {}
    end

    local result
    if args.return_type == 'table' or args.return_type == nil then
        result = util.string.split(value, '%.')

        if #result ~= 3 then
            error(string.format(i18n.errors.malformed_version_string, value))
        end

        result[4] = string.match(result[3], '%a+')
        result[3] = string.match(result[3], '%d+')

        for i=1,3 do
            local v = tonumber(result[i])
            if v == nil then
                error(string.format(i18n.errors.non_number_version_component, value))
            end
            result[i] = v
        end
    elseif args.return_type == 'string' then
        result = string.match(value, '%d+%.%d+%.%d+%a*')
    end

    if result == nil then
        error(string.format(i18n.errors.unrecognized_version_number, value))
    end

    return result
end

--
-- util.cast.factory
--

-- This section is used to generate new functions for common argument parsing tasks based on specific options
--
-- All functions return a function which accepts two arguments:
--  tpl_args - arguments from the template 
--  frame - current frame object
--
-- All factory functions accept have two arguments on creation:
--  k - the key in the tpl_args to retrive the value from
--  args - any addtional arguments (see function for details)

util.cast.factory = {}

function util.cast.factory.array_table(k, args)
    -- Arguments:
    --  tbl - table to check against
    --  errmsg - error message if no element was found; should accept 1 parameter
    args = args or {}
    return function (tpl_args, frame)
        local elements
        
        if tpl_args[k] ~= nil then
            elements = util.string.split(tpl_args[k], ',%s*')
            for _, element in ipairs(elements) do 
                local r = util.table.find_in_nested_array{value=element, tbl=args.tbl, key='full'}
                if r == nil then
                    error(util.html.error{msg=string.format(args.errmsg or i18n.errors.missing_element, element)})
                end
            end
            tpl_args[args.key_out or k] = xtable:new(elements)
        end
    end
end

function util.cast.factory.table(k, args)
    args = args or {}
    return function (tpl_args, frame)
        args.value = tpl_args[k]
        local value = util.table.find_in_nested_array(args)
        if value == nil then
            error(util.html.error{msg=string.format(args.errmsg or i18n.errors.missing_element, element)})
        end
        tpl_args[args.key_out or k] = value
    end
end

function util.cast.factory.assoc_table(k, args)
    -- Arguments:
    --
    -- tbl
    -- errmsg
    -- key_out
    return function (tpl_args, frame)
        local elements
        
        if tpl_args[k] ~= nil then
            elements = util.string.split(tpl_args[k], ',%s*')
            for _, element in ipairs(elements) do 
                if args.tbl[element] == nil then
                    error(util.html.error{msg=string.format(args.errmsg or i18n.errors.missing_element, element)})
                end
            end
            tpl_args[args.key_out or k] = elements
        end
    end
end

function util.cast.factory.number(k, args)
    args = args or {}
    return function (tpl_args, frame)
        tpl_args[args.key_out or k] = tonumber(tpl_args[k])
    end
end

function util.cast.factory.boolean(k, args)
    args = args or {}
    return function(tpl_args, frame)
        if tpl_args[k] ~= nil then
            tpl_args[args.key_out or k] = util.cast.boolean(tpl_args[k])
        end
    end
end

function util.cast.factory.percentage(k, args)
    args = args or {}
    return function (tpl_args, frame)
        local v = tonumber(tpl_args[k])
        
        if v == nil then
            return util.html.error{msg=string.format(i18n.errors.invalid_argument, k)}
        end
        
        if v < 0 or v > 100 then
            return util.html.error{msg=string.format(i18n.errors.not_a_percentage, k)}
        end
        
        tpl_args[args.key_out or k] = v
    end
end

-- ----------------------------------------------------------------------------
-- util.args
-- ----------------------------------------------------------------------------

util.args = {}

function util.args.stats(argtbl, args)
    -- in any prefix spaces should be included
    --
    -- argtbl: argument table to work with
    -- args:
    --  prefix: prefix if any
    --  frame: frame used to set subobjects; if not set dont set properties
    --  property_prefix: property prefix if any
    --  subobject_prefix: subobject prefix if any
    --  properties: table of properties to add if any
    args = args or {}
    args.prefix = args.prefix or ''

    local i = 0
    local stats = {}
    repeat
        i = i + 1
        local prefix = string.format('%s%s%s_%s', args.prefix, i18n.args.stat_infix, i, '%s')
        local id = {
            id = string.format(prefix, i18n.args.stat_id),
            min = string.format(prefix, i18n.args.stat_min),
            max = string.format(prefix, i18n.args.stat_max),
            value = string.format(prefix, i18n.args.stat_value),
        }

        local value = {}
        for key, args_key in pairs(id) do
            value[key] = argtbl[args_key]
        end


        if value.id ~= nil and ((value.min ~= nil and value.max ~= nil and value.value == nil) or (value.min == nil and value.max == nil and value.value ~= nil)) then
            if value.value then
                value.value = util.cast.number(value.value)
                argtbl[id.value] = value.value
            else
                value.min = util.cast.number(value.min)
                argtbl[id.min] = value.min
                value.max = util.cast.number(value.max)
                argtbl[id.max] = value.max

                -- Also set average value
                value.avg = (value.min + value.max)/2
                argtbl[string.format('%sstat%s_avg', args.prefix, i)] = value.avg
            end
            argtbl[string.format('%sstat%s', args.prefix, i)] = value
            stats[#stats+1] = value
        elseif util.table.has_all_value(value, {'id', 'min', 'max', 'value'}, nil) then
            value = nil
        -- all other cases should be improperly set value
        else
            error(string.format(i18n.errors.improper_stat, args.prefix, i))
        end
    until value == nil

    argtbl[string.format('%sstats', args.prefix)] = stats
end

function util.args.spawn_weight_list(argtbl, args)
    args = args or {}
    args.input_argument = i18n.args.spawn_weight_prefix
    args.output_argument = 'spawn_weights'
    args.cargo_table = 'spawn_weights'
    
    util.args.weight_list(argtbl, args)
end

function util.args.generation_weight_list(argtbl, args)
    args = args or {}
    args.input_argument = i18n.args.generation_weight_prefix
    args.output_argument = 'generation_weights'
    args.cargo_table = 'generation_weights'
    
    util.args.weight_list(argtbl, args)
end

function util.args.weight_list (argtbl, args)
    -- Parses a weighted pair of lists and sets properties
    --
    -- argtbl: argument table to work with
    -- args:
    --  output_argument - if set, set arguments to this value
    --  frame - if set, automtically set subobjects
    --  input_argument - input prefix for parsing the arguments from the argtbl
    --  subobject_name - name of the subobject 
    args = args or {}
    args.input_argument = args.input_argument or 'spawn_weight'

    local i = 0
    local id = nil
    local value = nil
    
    if args.output_argument then
        argtbl[args.output_argument] = {}
    end

    repeat
        i = i + 1
        id = {
            tag = string.format('%s%s_tag', args.input_argument, i),
            value = string.format('%s%s_value', args.input_argument, i),
        }
    
        value = {
            tag = argtbl[id.tag],
            value = argtbl[id.value],
        }
        
        if value.tag ~= nil and value.value ~= nil then
            if args.output_argument then
                argtbl[args.output_argument][i] = value
            end
            
            if args.frame and args.cargo_table then
                util.cargo.store(args.frame, {
                    _table = args.cargo_table,
                    ordinal = i,
                    tag = value.tag,
                    weight = util.cast.number(value.value, {min=0}),
                })
            end
        elseif not (value.tag == nil and value.value == nil) then
            error(string.format(i18n.errors.invalid_weight, id.tag, id.value))
        end
    until value.tag == nil
end

function util.args.version (argtbl, args)
    -- in any prefix spaces should be included
    --
    -- argtbl: argument table to work with
    -- args:
    --  frame: frame for queries
    --  set_properties: if defined, set properties on the page
    --  variables: table of prefixes
    args = args or {}
    args.variables = args.variables or {
        release = {},
        removal = {},
    }

    local version_ids = {}
    local version_keys = {}

    for key, data in pairs(args.variables) do
        local full_key = string.format('%s_version', key)
        if argtbl[full_key] ~= nil then
            local value = util.cast.version(argtbl[full_key], {return_type = 'string'})
            argtbl[full_key] = value
            data.value = value
            if data.property ~= nil then
                version_ids[#version_ids+1] = value
                version_keys[value] = key
            end
        end
    end

    -- no need to do a query if nothing was fetched
    if #version_ids > 0 then
        if args.frame == nil then
            error('Properties were set, but frame was not')
        end
        
        for i, id in ipairs(version_ids) do
            version_ids[i] = string.format('Versions.version="%s"', id)
        end

        local results = mw.ext.cargo.query(
            'Versions',
            'release_date, version',
            {
                where=table.concat(version_ids, ' OR '),
            }
        )

        if #results ~= #version_ids then
            error(string.format(i18n.too_many_versions, #results, #version_ids))
        end

        for _, row in ipairs(results) do
            local key = version_keys[row.version]
            argtbl[string.format('%s_date', key)] = row.release_date
        end
    end
end

-- ----------------------------------------------------------------------------
-- util.html
-- ----------------------------------------------------------------------------

util.html = {}
function util.html.abbr(abbr, text, class)
    return string.format('<abbr title="%s" class="%s">%s</abbr>', text or '', class or '', abbr or '')
end

function util.html.error(args)
    -- Create an error message box
    --
    -- Args:
    --  msg - message
    if args == nil then
        args = {}
    end

    local err = mw.html.create('span')
    err
        :attr('class', 'module-error')
        :wikitext(i18n.errors.module_error .. (args.msg or ''))
        :done()

    return tostring(err)
end

function util.html.poe_color(label, text)
    if text == nil or text == '' then
        return nil
    end
    return tostring(mw.html.create('em')
        :attr('class', 'tc -' .. label)
        :wikitext(text))
end

function util.html.tooltip(abbr, text, class)
    return string.format('<span class="tooltip-activator %s">%s<span class="tooltip-content">%s</span></span>', class or '', abbr or '', text or '')
end

util.html.td = {}
function util.html.td.na(args)
    --
    -- Args:
    --  as_tag
    args = args or {}
    -- N/A table row, requires mw.html.create instance to be passed
    local td = mw.html.create('td')
    td
        :attr('class', 'table-na')
        :wikitext('N/A')
        :done()
    if args.as_tag then
        return td
    else
        return tostring(td)
    end
end

-- ----------------------------------------------------------------------------
-- util.misc
-- ----------------------------------------------------------------------------

util.misc = {}
function util.misc.is_frame(frame)
    -- the type of the frame is a table containing the functions, so check whether some of these exist
    -- should be enough to avoid collisions.
    return not(frame == nil or type(frame) ~= 'table' or (frame.argumentPairs == nil and frame.callParserFunction == nil))
end

function util.misc.get_frame(frame)
    if util.misc.is_frame(frame) then
        return frame
    end
    return mw.getCurrentFrame()
end

util.misc.category_blacklist = {}
util.misc.category_blacklist.sub_pages = {
    doc = true,
    sandbox = true,
    sandbox2 = true,
    testcases = true,
}

util.misc.category_blacklist.namespaces = {
    Template = true,
    Template_talk = true,
    Module = true,
    Module_talk = true,
    User = true,
    User_talk = true,
}

function util.misc.add_category(categories, args)
    -- categories: table of categories
    -- args: table of extra arguments
    --  namespace: id of namespace to validate against
    --  ingore_blacklist: set to non-nil to ingore the blacklist
    --  sub_page_blacklist: blacklist of subpages to use (if empty, use default)
    --  namespace_blacklist: blacklist of namespaces to use (if empty, use default)
    if type(categories) == 'string' then
        categories = {categories}
    end

    if args == nil then
        args = {}
    end


    local title = mw.title.getCurrentTitle()
    local sub_blacklist = args.sub_page_blacklist or util.misc.category_blacklist.sub_pages
    local ns_blacklist = args.namespace_blacklist or util.misc.category_blacklist.namespaces

    if args.namespace ~= nil and title.namespace ~= args.namespace then
        return ''
    end

    if args.ingore_blacklist == nil and (sub_blacklist[title.subpageText] or ns_blacklist[title.subjectNsText]) then
        return ''
    end

    local cats = {}

    for i, cat in ipairs(categories) do
        cats[i] = string.format('[[Category:%s]]', cat)
    end
    return table.concat(cats)
end

function util.misc.raise_error_or_return(args)
    --
    -- Arguments:
    -- args: table of arguments to this function (must be set)
    --  One required:
    --  raise_required: Don't raise errors and return html errors instead unless raisae is set in arguments
    --  no_raise_required: Don't return html errors and raise errors insetad unless no_raise is set in arguments
    --
    --  Optional:
    --  msg: error message to raise or return, default: nil
    --  args: argument directory to validate against (e.x. template args), default: {}
    args.args = args.args or {}
    args.msg = args.msg or ''
    if args.raise_required ~= nil then
        if args.args.raise ~= nil then
            error(args.msg)
        else
            return util.html.error{msg=args.msg}
        end
    elseif args.no_raise_required ~= nil then
        if args.args.no_raise ~= nil then
            return util.html.error{msg=args.msg}
        else
            error(args.msg)
        end
    else
        error(i18n.errors.invalid_raise_error_or_return_usage)
    end
end

-- ----------------------------------------------------------------------------
-- util.smw
-- ----------------------------------------------------------------------------

util.smw = {}

util.smw.data = {}
util.smw.data.rejected_namespaces = xtable:new({'User'})

function util.smw._parser_function(frame, parser_function, args)
    -- Executes a semantic parser functions and sets the arguments args
    --
    -- This function is a helper for handling tables since +sep= parameter
    -- appears to be broken.
    --
    -- frame          : frame object
    -- parser_function: the whole parser function string
    -- args           : table of arguments
    for k, v in pairs(args) do
        if type(v) == 'table' then
            for _, value in ipairs(v) do
                frame:callParserFunction(parser_function, {[k] = value})
            end
            args[k] = nil
        elseif type(v) == 'boolean' then
            args[k] = tostring(v)
        end
    end
    frame:callParserFunction(parser_function, args)
end

function util.smw.set(frame, args)
    local success, err = pcall(function () util.smw._parser_function(frame, '#set:', args) end)
    if not success then
        mw.logObject(err)
   end
end

function util.smw.subobject(frame, id, args)
    local success, err = pcall(function () util.smw._parser_function(frame, '#subobject:' .. id, args) end)
    if not success then
        mw.logObject(err)
    end
end

function util.smw.query(query, frame)
    -- Executes a semantic media wiki #ask query and returns the result as an
    -- array containing each row as table.
    --
    -- query: table of query arguments to pass
    -- frame: current frame object

    -- the characters here for sep/header/propsep are control characters; I'm farily certain they should not appear in regular text.
    query.sep = '�'
    query.propsep = '<PROP>'
    query.headersep = '<HEAD>'
    query.format = 'array'
    query.headers = 'plain'

    local result
    local success, err = pcall(function () result = frame:callParserFunction('#ask', query) end)
    if not success then
        result = ''
        mw.logObject(err)
    end
    --local result = ''

    -- "<span class=\"smw-highlighter\" data-type=\"4\" data-state=\"inline\" data-title=\"Error\"><span class=\"smwtticon warning\"></span><div class=\"smwttcontent\">Some subquery has no valid condition.</div></span>"
    if mw.ustring.find(result, 'data%-title="Error"') ~= nil then
        error(mw.ustring.sub(result, mw.ustring.find(result, '<span class="smw-highlighter"', 1, true), -1))
    end

    local out = {}

    for row_string in string.gmatch(result, '[^�]+') do
        local row = {}
        for _, str in ipairs(util.string.split(row_string, query.propsep)) do
            local kv = util.string.split(str, query.headersep)
            if #kv == 1 then
                row[#row+1] = kv[1]
            elseif #kv == 2 then
                row[kv[1]] = kv[2]
            end
        end
        out[#out+1] = row
    end

    return out
end

function util.smw.array_query(args)
    -- Performs a long "OR" query from the given array and properties and returns the results with the property as key.
    -- This function is neccessary because on the limit on query size, so multiple queries will be peformed.
    -- 
    -- REQUIRED:
    --  frame: frame
    --  property: id property that will be used; will also be used to set the output table accordingly
    --  id_array: list of ids to perform queries for
    --
    -- OPTIONAL:
    --  conditions: any extra conditions
    --  query: fields to pass to the query (like the fields you want to return, other smw query options, etc)
    --  result_key: override property in the output table
    --  max_array_size: Maximum elements from the array to insert into the property field
    --                  May need to be lowered for more complex queries
    --                  Defaults to 9
    --  error_on_missing: If set to true, it will validate the number of results returned
    --
    -- RETURN:
    --  assoc table - associative table with the property as key containing the query results
    args.conditions = args.conditions or ''
    args.query = args.query or {}
    args.max_array_size = args.max_array_size or 11
    args.error_on_missing = args.error_on_missing or false
    args.result_key = args.result_key or args.property
    
    -- first field will hold the query
    table.insert(args.query, 1, '')

    local qresults = {}
            
    for i=0,(#args.id_array-1)/args.max_array_size do
        local query_ids_slice = {}
        for j=i*args.max_array_size+1, (i+1)*args.max_array_size do
            query_ids_slice[#query_ids_slice+1] = args.id_array[j]
        end
        
        -- we can just reuse the query object here, no need to recreate it
        args.query[1] = string.format('[[%s::%s]]', args.property, table.concat(query_ids_slice, '||')) .. args.conditions
        
        local results = util.smw.query(args.query, args.frame)
        for _, result in ipairs(results) do
            if qresults[result[args.result_key]] ~= nil then
                error(string.format(i18n.errors.duplicate_ids_found, args.result_key, result[args.result_key], qresults[result[args.result_key]][1], result[1]))
            end
            qresults[result[args.result_key]] = result
        end
    end
    
    if args.error_on_missing then
        local missing = {}
        for _, id in ipairs(args.id_array) do
            if qresults[id] == nil then
                missing[#missing+1] = id
            end
        end
        
        if #missing > 0 then
            error(string.format(i18n.errors.missing_ids_found, args.property, table.concat(missing, ', ')))
        end
    end
    
    return qresults
end


function util.smw.safeguard(args)
    -- Used for safeguarding data entry so it doesn't get added on user space stuff
    --
    -- Args:
    --  smw_ingore_safeguard - ingore safeguard and return true
    if args == nil then
        args = {}
    end

    if args.smw_ingore_safeguard then
        return true
    end

    local namespace = mw.site.namespaces[mw.title.getCurrentTitle().namespace].name
    if util.smw.data.rejected_namespaces:contains(namespace) then
        return false
    end

    return true
end

-- ----------------------------------------------------------------------------
-- util.cargo
-- ----------------------------------------------------------------------------

util.cargo = {}

function util.cargo.escape_sqli(source)
    local replacements = { ['"'] = '\\"', ["'"] = "\\'" }
    return string.gsub( source, "['\"]", replacements )
end

function util.cargo.declare(frame, args)
    return frame:callParserFunction('#cargo_declare:', args)
end

function util.cargo.attach(frame, args)
    return frame:callParserFunction('#cargo_attach:', args)
end

function util.cargo.store(frame, values, args)
    -- Calls the cargo_store parser function and ensures the values passed are casted properly
    --
    -- Value handling: 
    --  tables   - automatically concat
    --  booleans - automatically casted to 1 or 0 to ensure they're stored properly
    --
    -- Arguments:
    --  frame        - frame object
    --  values       - table of field/value pairs to store
    --  args         - any additional arguments 
    --   sep         - separator to use for concat
    --   store_empty - if specified, allow storing empty rows
    --   debug       - send the converted values to the lua debug log 
    args = args or {}
    args.sep = args.sep or {}
    local i = 0
    for k, v in pairs(values) do
        i = i + 1
        if type(v) == 'table' then
            values[k] = table.concat(v, args.sep[k] or ',')
        elseif type(v) == 'boolean' then
            if v == true then
                v = '1'
            elseif v == false then
                v = '0'
            end
            values[k] = v
        end
    end
    -- i must be greater then 1 since we at least expect the _table argument to be set, even if no values are set
    if i > 1 or args.store_empty then
        if args.debug ~= nil then
            mw.logObject(values)
        end
        return frame:callParserFunction('#cargo_store:', values)
    end
end

function util.cargo.declare_factory(args)
    -- Returns a function that can be called by templates to declare cargo tables
    --
    -- args
    --  data: data table
    --   table: name of cargo table
    --   fields: associative table with:
    --    field: name of the field to declare
    --    type: type of the  field
    return function (frame)
        frame = util.misc.get_frame(frame)
        
        local dcl_args = {}
        dcl_args._table = args.data.table
        for k, field_data in pairs(args.data.fields) do
            if field_data.field then
                dcl_args[field_data.field] = field_data.type
            end
        end
        
        return util.cargo.declare(frame, dcl_args)
    end
end

function util.cargo.attach_factory(args)
    -- Returns a function that can be called by templates to attach cargo tables
    --
    -- args
    --  data: data table
    --   table: name of cargo table
    --   fields: associative table with:
    --    field: name of the field to declare
    --    type: type of the  field
    return function (frame)
        frame = util.misc.get_frame(frame)
        
        local attach_args = {}
        attach_args._table = args.data.table
        
        return util.cargo.attach(frame, attach_args)
    end
end

--mw.logObject(p.cargo.map_results_to_id{results=mw.ext.cargo.query('mods,spawn_weights', 'mods._pageID,spawn_weights.tag', {where='mods.id="Strength1"', join='mods._pageID=spawn_weights._pageID'}), table_name='mods'})
function util.cargo.map_results_to_id(args)
    -- Maps the results passed to a table containing the _pageID as key and a table of rows for the particular page as values.
    --
    -- args
    --  results      : table of results returned from mw.ext.cargo.query to map to _pageID
    --  table_name   : name of the table that has the _pageID attribute
    --                 table_name._pageID has to be in the fields list of the original query or it will cause errors
    --  keep_page_id : if set, don't delete _pageID
    --
    -- return
    --  table
    --   key         : _pageID
    --   value       : array containing the found rows (in the order that they were found)
    local field = string.format('%s._pageID', args.table_name)
    local out = {}
    for _, row in ipairs(args.results) do
        local pid = row[field]
        if out[pid] then
            out[pid][#out[pid]+1] = row
        else
            out[pid] = {row}
        end
        -- discard the pageID, don't need this any longer in most cases
        if args.keep_page_id == nil then
            row[field] = nil
        end
    end
    
    return out
end

function util.cargo.query(tables, fields, query, args)
    -- Wrapper for mw.ext.cargo.query that helps to work around some bugs
    --
    -- Current workarounds:
    --  field names will be "aliased" to themselves
    --
    -- Takes 3 arguments:
    --  tables - array containing tables
    --  fields - array containing fields; these will automatically be renamed to the way they are specified to work around bugs when results are returned
    --  query  - array containing cargo sql clauses
    --  args
    --   args.keep_empty
    
    -- Cargo bug workaround
    args = args or {}
    for i, field in ipairs(fields) do
        -- already has some alternate name set, so do not do this.
        if string.find(field, '=') == nil then
            fields[i] = string.format('%s=%s', field, field)
        end
    end
    local results = cargo.query(table.concat(tables, ','), table.concat(fields, ','), query)
    if args.keep_empty == nil then
        for _, row in ipairs(results) do
            for k, v in pairs(row) do
                if v == "" then
                    row[k] = nil
                end
            end
        end
    end
    return results
end

function util.cargo.array_query(args)
    -- Performs a long "OR" query from the given array and field validating that there is only exactly one match returned
    -- 
    -- args:
    --  tables    - array of tables (see util.cargo.query)
    --  fields    - array of fields (see util.cargo.query)
    --  query     - array containing cargo sql clauses [optional] (see util.cargo.query)
    --  id_array - list of ids to query for
    --  id_field  - name of the id field, will be automatically added to fields
    --
    -- RETURN:
    --  table - results as given by mw.ext.cargo.query
    --  
    args.query = args.query or {}
    
    args.fields[#args.fields+1] = args.id_field
    
    local id_array = {}
    for i, id in ipairs(args.id_array) do
        id_array[i] = string.format('%s="%s"', args.id_field, id)
    end
    if args.query.where then
        args.query.where = string.format('(%s) AND (%s)', args.query.where, table.concat(id_array, ' OR '))
    else
        args.query.where = table.concat(id_array, ' OR ')
    end
    
    --
    -- Check for duplicates
    --
    
    -- The usage of distinct should elimate duplicates here from cargo being bugged while still showing actual data duplicates.
    local results = util.cargo.query(
        args.tables,
        {
            string.format('COUNT(DISTINCT %s._pageID)=count', args.tables[1]),
            args.id_field,
        },
        {
            join=args.query.join,
            where=args.query.where,
            groupBy=args.id_field,
            having=string.format('COUNT(DISTINCT %s._pageID) > 1', args.tables[1]), 
        }
    )
    if #results > 0 then
        out = {}
        for _, row in ipairs(results) do
            out[#out+1] = string.format('%s (%s pages found)', row[args.id_field], row['count'])
        end
        error(string.format(i18n.errors.duplicate_ids, args.id_field, table.concat(out, '\n')))
    end
    
    --
    -- Prepare query
    --
    if args.query.groupBy then
        args.query.groupBy = string.format('%s._pageID,%s', args.tables[1], args.query.groupBy)
    else
        args.query.groupBy = string.format('%s._pageID', args.tables[1])
    end
    
    local results = util.cargo.query(
        args.tables,
        args.fields,
        args.query
    )
    
    --
    -- Check missing results
    --
    if #results ~= #args.id_array then
        local missing = {}
        for _, id in ipairs(args.id_array) do
            missing[id] = true
        end
        for _, row in ipairs(results) do
            missing[row[args.id_field]] = nil
        end
        
        local missing_ids = {}
        for k, _ in pairs(missing) do
            missing_ids[#missing_ids+1] = k
        end
        
        error(string.format(i18n.errors.missing_ids, args.id_field, table.concat(missing_ids, '\n')))
    end
    
    return results
end

-- ----------------------------------------------------------------------------
-- util.string
-- ----------------------------------------------------------------------------

util.string = {}
function util.string.split(str, pattern)
    -- Splits string into a table
    --
    -- str: string to split
    -- pattern: pattern to use for splitting
    local out = {}
    local i = 1
    local split_start, split_end = string.find(str, pattern, i)
    while split_start do
        out[#out+1] = string.sub(str, i, split_start-1)
        i = split_end+1
        split_start, split_end = string.find(str, pattern, i)
    end
    out[#out+1] = string.sub(str, i)
    return out
end

function util.string.split_args(str, args)
    -- Splits arguments string into a table
    --
    -- str: String of arguments to split
    -- args: table of extra arguments
    --  sep: separator to use (default: ,)
    --  kvsep: separator to use for key value pairs (default: =)
    local out = {}

    if args == nil then
        args = {}
    end

    args.sep = args.sep or ','
    args.kvsep = args.kvsep or '='

    if str ~= nil then
        local row
        for _, str in ipairs(util.string.split(str, args.sep)) do
            row = util.string.split(str, args.kvsep)
            if #row == 1 then
                out[#out+1] = row[1]
            elseif #row == 2 then
                out[row[1]] = row[2]
            else
                error(string.format(i18n.number_of_arguments_too_large, #row))
            end
        end
    end

    return out
end

function util.string.remove_ref(str)
	local a, b = str:gsub([[.'"`UNIQ%-%-ref%-[%a%d]+%-QINU`"'.]], '')
	return a
end

-- ----------------------------------------------------------------------------
-- util.table
-- ----------------------------------------------------------------------------

util.table = {}
function util.table.length(tbl)
    -- Get length of a table when # doesn't work (usually when a table has a metatable)
    for i = 1, infinity do
        if tbl[i] == nil then
            return i - 1
        end
    end
end

function util.table.assoc_to_array(tbl, args)
    -- Turn associative array into an array, discarding the values
    local out = {}
    for key, _ in pairs(tbl) do
        out[#out+1] = key
    end
    return out
end

function util.table.has_all_value(tbl, keys, value)
    -- Whether all the table values with the specified keys are the specified value
    for _, k in ipairs(keys or {}) do
        if tbl[k] ~= value then
            return false
        end
    end
    return true
end

function util.table.has_one_value(tbl, keys, value)
    -- Whether one of table values with the specified keys is the specified value
    for _, k in ipairs(keys or {}) do
        if tbl[k] == value then
            return true
        end
    end
    return false
end

function util.table.find_in_nested_array(args)
    -- Iterates thoguh the given nested array and finds the given value
    --
    -- ex.
    -- data = {
    -- {a=5}, {a=6}}
    -- find_nested_array{arg=6, tbl=data, key='a'} -> 6
    -- find_nested_array(arg=10, tbl=data, key='a'} -> nil
    -- -> returns "6"

    --
    -- args: Table containing:
    --  value: value of the argument
    --  tbl: table of valid options
    --  key: key or table of key of in tbl
    --  rtrkey: if key is table, return this key instead of the value instead
    --  rtrvalue: default: true

    local rtr

    if type(args.key) == 'table' then
        for _, item in ipairs(args.tbl) do
            for _, k in ipairs(args.key) do
                if item[k] == args.value then
                    rtr = item
                    break
                end
            end
        end
    elseif args.key == nil then
        for _, item in ipairs(args.tbl) do
            if item == args.value then
                rtr = item
                break
            end
        end
    else
        for _, item in ipairs(args.tbl) do
            if item[args.key] == args.value then
                rtr = item
                break
            end
        end
    end

    if rtr == nil then
        return rtr
    end

    if args.rtrkey ~= nil then
        return rtr[args.rtrkey]
    elseif args.rtrvalue or args.rtrvalue == nil then
        return args.value
    else
        return rtr
    end
end

-- ----------------------------------------------------------------------------
-- util.Struct
-- ----------------------------------------------------------------------------

util.Struct = function(map)
    local this = {map = map}


    -- sets a value to a field
    function this:set(field, value)
        if not field or not value then
            error('One or more arguments are nils')
        end

        local _ = self.map[field]

        if not _ then
            error(string_format('Field "%s" doesn\'t exist', field))
        end

        if _.validate then
            _.value = _.validate(value)
        else
            _.value = value
        end

        -- this happen if 'validate' returns nil
        if _.required == true and _.value == nil then
            error(string_format('Field "%s" is required but has been set to nil', field))
        end
    end


    -- adds a new prop to a field
    function this:set_prop(field, prop, value)
        if not field or not prop or not value then
            error('One or more arguments are nils')
        end

        local _ = self.map[field]

        if not _ then
            error(string_format('Field "%s" doesn\'t exist', field))
        end

        _[prop] = value
    end


    -- gets a value from a field
    function this:get(field)
        if not field then
            error('Argument field is nil')
        end

        local _ = self.map[field]

        if not _ then
            error(string_format('Field "%s" doesn\'t exist', field))
        end

        return _.value
    end


    -- gets a value from a prop field
    function this:get_prop(field, prop)
        if not field or not prop then
            error('One or more arguments are nils')
        end

        local _ = self.map[field]

        if not _ then
            error(string_format('Field "%s" doesn\'t exist', field))
        end

        return _[prop]
    end


    -- shows a value from a field
    function this:show(field)
        if not field then
            error('Argument field is nil')
        end

        local _ = self.map[field]

        if not _ then
            error(string_format('Field "%s" doesn\'t exist', field))
        end

        if _.show then
            return _.show(_)
        else
            return _.value
        end
    end


    return this
end

-- ----------------------------------------------------------------------------

return util