# Module:Parameter validation

local util = {  
empty = function( s )  
return s == nil or type( s ) == 'string' and mw.text.trim( s ) == ''  
end  
,  
extract_options = function ( , optionsPrefix )  
optionsPrefix = optionsPrefix or 'options'  

local options, n, more = {}  
if .args['module_options'] then  
local module_options = mw.loadData( .args['module_options'] )  
if type( module_options ) ~= 'table' then return {} end  
local title = mw.title.getCurrentTitle  
local local_ptions = module_options[ title.namespace ] or module_options[ title.nsText ] or {}  
for k, v in pairs( local_ptions ) do options[k] = v end  
end  

repeat  
ok, more = pcall( mw.text.jsonDecode, .args[optionsPrefix .. ( n or '' )] )  
if ok and type( more ) == 'table' then  
for k, v in pairs( more ) do options[k] = v end  
end  
n = ( n or 0 ) + 1  
until not ok  

return options  
end  
,  
build_namelist = function ( template_name, sp )  
local res = { template_name }  
if sp then  
if type( sp ) == 'string' then sp = { sp } end  
for _, p in ipairs( sp ) do table.insert( res, template_name .. '/' .. p ) end  
end  
return res  
end  
,  
table_empty = function( t ) -- normally, test if next(t) is nil, but for some perverse reason, non-empty tables returned by loadData return nil...  
if type( t ) ~= 'table' then return true end  
for a, b in pairs( t ) do return false end  
return true  
end  
,  
}  

local function _readTemplateData( templateName )  
local title = mw.title.makeTitle( 0, templateName )  
local templateContent = title and title.exists and title:getContent -- template's raw content  
local capture = templateContent and mw.ustring.match( templateContent, '<templatedata%s*>(.*)</templatedata%s*>' ) -- templatedata as text  
-- capture = capture and mw.ustring.gsub( capture, '"(%d+)"', tonumber ) -- convert "1": {} to 1: {}. .args uses numerical indexes for order-based params.  
local trailingComma = capture and mw.ustring.find( capture, ',%s*[%]%}]' ) -- look for ,] or ,} : jsonDecode allows it, but it's verbotten in json  
if capture and not trailingComma then return pcall( mw.text.jsonDecode, capture ) end  
return false  
end  

local function readTemplateData( templateName )  
if type( templateName ) == 'string' then  
templateName = { templateName, templateName .. '/' .. docSubPage }  
end  
if type( templateName ) == "table" then  
for _, name in ipairs( templateName ) do  
local td, result = _readTemplateData( name )  
if td then return result end  
end  
end  
return nil  
end  

-- this is the function to be called by other modules. it expects the , and then an optional list of subpages, e.g. { "Documentation" }.  
-- if second parameter is nil, only tempalte page will be searched for templatedata.  
function calculateViolations( , subpages )  
-- used for parameter type validy test. keyed by TD 'type' string. values are function(val) returning bool.  
local type_validators = {  
['number'] = function( s ) return mw.language.getContentLanguage:parseFormattedNumber( s ) end  
}  
function compatible( typ, val )  
local func = type_validators[typ]  
return type( func ) ~= 'function' or util.empty( val ) or func( val )  
end  

local t_frame = :getParent  
local t_args, template_name = t_frame.args, t_frame:getTitle  
template_name = mw.ustring.gsub( template_name, '/sandbox', '', 1 )  
local td_source = util.build_namelist( template_name, subpages )  
if .args['td_source'] then  
table.insert(td_source, .args['td_source'])  
end  

local templatedata = readTemplateData( td_source )  
local td_params = templatedata and templatedata.params  
local all_aliases, all_series = {}, {}  

if not td_params then return { ['no-templatedata'] = { [*] = * } } end  
-- from this point on, we know templatedata is valid.  

local res = {} -- before returning to caller, we'll prune empty tables  

-- allow for aliases  
for x, p in pairs( td_params ) do for y, alias in ipairs( p.aliases or {} ) do  
p['primary'] = x  
td_params[x] = p  
all_aliases[alias] = p  
if tonumber(alias) then all_aliases[tonumber(alias)] = p end  
end end  

-- handle undeclared and deprecated  
local already_seen = {}  
local series = .args['series']  
for p_name, value in pairs( t_args ) do  
local tp_param, noval, numeric, table_name = td_params[p_name] or all_aliases[p_name], util.empty( value ), tonumber( p_name )  
local hasval = not noval  

if not tp_param and series then -- 2nd chance. check to see if series  
for s_name, p in pairs(td_params) do  
if mw.ustring.match( p_name, '^' .. s_name .. '%d+' .. '$') then  
-- mw.log('found p_name '.. p_name .. ' s_name:' .. s_name, ' p is:', p) debugging series support  
tp_param = p  
end -- don't bother breaking. td always correct.  
end  
end  

if not tp_param then -- not in TD: this is called undeclared  
-- calculate the relevant table for this undeclared parameter, based on parameter and value types  
table_name =  
noval and numeric and 'empty-undeclared-numeric' or  
noval and not numeric and 'empty-undeclared' or  
hasval and numeric and 'undeclared-numeric' or  
'undeclared' -- tzvototi nishar.  
else -- in td: test for deprecation and mistype. if deprecated, no further tests  
table_name = tp_param.deprecated and hasval and 'deprecated'  
or tp_param.deprecated and noval and 'empty-deprecated'  
or not compatible( tp_param.type, value ) and 'incompatible'  
or not series and already_seen[tp_param] and hasval and 'duplicate'  

if hasval and table_name ~= 'duplicate' then  
already_seen[tp_param] = p_name  
end  
end  

-- report it.  
if table_name then  
res[table_name] = res[table_name] or {}  
if table_name == 'duplicate' then  
local primary_param = tp_param['primary']  
local primaryData = res[table_name][primary_param]  
if not primaryData then  
primaryData = {}  
table.insert(primaryData, already_seen[tp_param])  
end  
table.insert(primaryData, p_name)  
res[table_name][primary_param] = primaryData  
else  
res[table_name][p_name] = value  
end  
end  
end  

-- check for empty/missing parameters declared "required"  
for p_name, param in pairs( td_params ) do  
if param.required and util.empty( t_args[p_name] ) then  
local is_alias  
for _, alias in ipairs( param.aliases or {} ) do is_alias = is_alias or not util.empty( t_args[alias] ) end  
if not is_alias then  
res['empty-required'] = res['empty-required'] or {}  
res['empty-required'][p_name] = ''  
end  
end  
end  

mw.logObject(res)  

return res  
end  

-- wraps report in hidden  
function wrapReport(report, template_name, options)  
mw.logObject(report)  
if util.empty( report ) then return '' end  
local naked = mw.title.new( template_name )['text']  
naked = mw.ustring.gsub(naked, 'Infobox', 'infobox', 1)  

report = ( options['wrapper-prefix'] or "<div><span>" )  
.. report  
.. ( options['wrapper-suffix'] or "</span></div>" )  

report = mw.ustring.gsub( report, 'tname_naked', naked )  
report = mw.ustring.gsub( report, 'templatename', template_name )  

return report  
end  

-- this is the "user" version, called with returns a string, as defined by the options parameter  
function validateParams  
local options, report, template_name = util.extract_options, '', :getParent:getTitle  

local ignore = function( p_name )  
for _, pattern in ipairs( options['ignore'] or {} ) do  
if mw.ustring.match( p_name, '^' .. pattern .. '$' ) then return true end  
end  
return false  
end  

local replace_macros = function( error_type, s, param_names )  
function concat_and_escape( t , sep )  
sep = sep or ', '  
local s = table.concat( t, sep )  
return ( mw.ustring.gsub( s, '%%', '%%%%' ) )  
end  

if s and ( type( param_names ) == 'table' ) then  
local k_ar, kv_ar = {}, {}  
for k, v in pairs( param_names ) do  
table.insert( k_ar, k )  
if type(v) == 'table' then  
v = table.concat(v, ', ')  
end  

if error_type == 'duplicate' then  
table.insert( kv_ar, v)  
else  
table.insert( kv_ar, k .. ': ' .. v)  
end  
end  

s = mw.ustring.gsub( s, 'paramname', concat_and_escape( k_ar ) )  
s = mw.ustring.gsub( s, 'paramandvalue', concat_and_escape( kv_ar, ' AND ' ) )  

if mw.getCurrentFrame:preprocess( "" ) ~= "" then  
s = mw.ustring.gsub( s, "<div.*<%/div>", "", 1 )  
end  
end  
return s  
end  

local report_params = function( key, param_names )  
local res = replace_macros( key, options[key], param_names )  
res = :preprocess(res or '')  
report = report .. ( res or '' )  
return res  
end  

-- no option no work.  
if util.table_empty( options ) then return '' end  

-- get the errors.  
local violations = calculateViolations( , options['doc-subpage'] )  
-- special request of bora: use skip_empty_numeric  
if violations['empty-undeclared-numeric'] then  
for i = 1, tonumber( options['skip-empty-numeric'] ) or 0 do  
violations['empty-undeclared-numeric'][i] = nil  
end  
end  

-- handle ignore list, and prune empty violations - in that order!  
local offenders = 0  
for name, tab in pairs( violations ) do  
-- remove ignored parameters from all violations  
for pname in pairs( tab ) do if ignore( pname ) then tab[pname] = nil end end  
-- prune empty violations  
if util.table_empty( tab ) then violations[name] = nil end  
-- WORK IS DONE. report the errors.  
-- if report then count it.  
if violations[name] and report_params( name, tab ) then offenders = offenders + 1 end  
end  

if offenders > 1 then report_params( 'multiple' ) end  
if offenders ~= 0 then report_params( 'any' ) end -- could have tested for empty( report ), but since we count them anyway...  
return wrapReport(report, template_name, options)  
end  

return {  
['validateparams'] = validateParams,  
['calculateViolations'] = calculateViolations,  
['wrapReport'] = wrapReport  
}