Module:Parameter validation
Documentation for this module may be created at Module:Parameter validation/doc
local util = { empty = function( s ) return s == nil or type( s ) == 'string' and mw.text.trim( s ) == '' end , extract_options = function ( frame, optionsPrefix ) optionsPrefix = optionsPrefix or 'options' local options, n, more = {} if frame.args['module_options'] then local module_options = mw.loadData( frame.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, frame.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: {}. frame.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 frame, 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( frame, 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 = 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 frame.args['td_source'] then table.insert(td_source, frame.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 = frame.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 frame 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 class = 'paramvalidator-wrapper'><span class='paramvalidator-error'>" ) .. 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 {{#invoke:}} returns a string, as defined by the options parameter function validateParams( frame ) local options, report, template_name = util.extract_options( frame ), '', frame: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( "{{REVISIONID}}" ) ~= "" 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 = frame: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( frame, 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 }