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)

Latest revision as of 22:18, 7 October 2019

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

Overview[edit source]

Provides utility functions for programming modules.

Structure[edit source]

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[edit source]

☑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