https://en.wikipedia.org/w/index.php?action=history&feed=atom&title=Module%3AParameter_validation%2FsandboxModule:Parameter validation/sandbox - Revision history2025-05-25T23:59:40ZRevision history for this page on the wikiMediaWiki 1.45.0-wmf.2https://en.wikipedia.org/w/index.php?title=Module:Parameter_validation/sandbox&diff=1216634403&oldid=prevSilverLocust: Copying module to sandbox2024-04-01T04:47:12Z<p>Copying module to sandbox</p>
<p><b>New page</b></p><div>local util = {<br />
empty = function( s ) <br />
return s == nil or type( s ) == 'string' and mw.text.trim( s ) == '' <br />
end<br />
, <br />
extract_options = function ( frame, optionsPrefix )<br />
optionsPrefix = optionsPrefix or 'options' <br />
<br />
local options, n, more = {}<br />
if frame.args['module_options'] then<br />
local module_options = mw.loadData( frame.args['module_options'] ) <br />
if type( module_options ) ~= 'table' then return {} end<br />
local title = mw.title.getCurrentTitle()<br />
local local_ptions = module_options[ title.namespace ] or module_options[ title.nsText ] or {} <br />
for k, v in pairs( local_ptions ) do options[k] = v end<br />
end<br />
<br />
repeat<br />
ok, more = pcall( mw.text.jsonDecode, frame.args[optionsPrefix .. ( n or '' )] )<br />
if ok and type( more ) == 'table' then<br />
for k, v in pairs( more ) do options[k] = v end<br />
end<br />
n = ( n or 0 ) + 1<br />
until not ok<br />
<br />
return options<br />
end<br />
, <br />
build_namelist = function ( template_name, sp )<br />
local res = { template_name }<br />
if sp then<br />
if type( sp ) == 'string' then sp = { sp } end<br />
for _, p in ipairs( sp ) do table.insert( res, template_name .. '/' .. p ) end<br />
end<br />
return res<br />
end<br />
,<br />
table_empty = function( t ) -- normally, test if next(t) is nil, but for some perverse reason, non-empty tables returned by loadData return nil...<br />
if type( t ) ~= 'table' then return true end<br />
for a, b in pairs( t ) do return false end<br />
return true<br />
end<br />
,<br />
}<br />
<br />
local function _readTemplateData( templateName ) <br />
local title = mw.title.makeTitle( 0, templateName ) <br />
local templateContent = title and title.exists and title:getContent() -- template's raw content<br />
local capture = templateContent and mw.ustring.match( templateContent, '<templatedata%s*>(.*)</templatedata%s*>' ) -- templatedata as text<br />
-- capture = capture and mw.ustring.gsub( capture, '"(%d+)"', tonumber ) -- convert "1": {} to 1: {}. frame.args uses numerical indexes for order-based params.<br />
local trailingComma = capture and mw.ustring.find( capture, ',%s*[%]%}]' ) -- look for ,] or ,} : jsonDecode allows it, but it's verbotten in json<br />
if capture and not trailingComma then return pcall( mw.text.jsonDecode, capture ) end<br />
return false<br />
end<br />
<br />
local function readTemplateData( templateName )<br />
if type( templateName ) == 'string' then <br />
templateName = { templateName, templateName .. '/' .. docSubPage }<br />
end<br />
if type( templateName ) == "table" then<br />
for _, name in ipairs( templateName ) do<br />
local td, result = _readTemplateData( name ) <br />
if td then return result end<br />
end<br />
end<br />
return nil<br />
end<br />
<br />
<br />
-- this is the function to be called by other modules. it expects the frame, and then an optional list of subpages, e.g. { "Documentation" }.<br />
-- if second parameter is nil, only tempalte page will be searched for templatedata.<br />
function calculateViolations( frame, subpages )<br />
-- used for parameter type validy test. keyed by TD 'type' string. values are function(val) returning bool.<br />
local type_validators = { <br />
['number'] = function( s ) return mw.language.getContentLanguage():parseFormattedNumber( s ) end<br />
}<br />
function compatible( typ, val )<br />
local func = type_validators[typ]<br />
return type( func ) ~= 'function' or util.empty( val ) or func( val )<br />
end<br />
<br />
local t_frame = frame:getParent()<br />
local t_args, template_name = t_frame.args, t_frame:getTitle()<br />
template_name = mw.ustring.gsub( template_name, '/sandbox', '', 1 )<br />
local td_source = util.build_namelist( template_name, subpages )<br />
if frame.args['td_source'] then<br />
table.insert(td_source, frame.args['td_source'])<br />
end<br />
<br />
local templatedata = readTemplateData( td_source )<br />
local td_params = templatedata and templatedata.params<br />
local all_aliases, all_series = {}, {}<br />
<br />
if not td_params then return { ['no-templatedata'] = { [''] = '' } } end<br />
-- from this point on, we know templatedata is valid.<br />
<br />
local res = {} -- before returning to caller, we'll prune empty tables<br />
<br />
-- allow for aliases<br />
for x, p in pairs( td_params ) do for y, alias in ipairs( p.aliases or {} ) do<br />
p['primary'] = x<br />
td_params[x] = p<br />
all_aliases[alias] = p<br />
if tonumber(alias) then all_aliases[tonumber(alias)] = p end<br />
end end<br />
<br />
-- handle undeclared and deprecated<br />
local already_seen = {}<br />
local series = frame.args['series']<br />
for p_name, value in pairs( t_args ) do<br />
local tp_param, noval, numeric, table_name = td_params[p_name] or all_aliases[p_name], util.empty( value ), tonumber( p_name )<br />
local hasval = not noval<br />
<br />
if not tp_param and series then -- 2nd chance. check to see if series<br />
for s_name, p in pairs(td_params) do <br />
if mw.ustring.match( p_name, '^' .. s_name .. '%d+' .. '$') then <br />
-- mw.log('found p_name '.. p_name .. ' s_name:' .. s_name, ' p is:', p) debugging series support<br />
tp_param = p <br />
end -- don't bother breaking. td always correct.<br />
end <br />
end<br />
<br />
if not tp_param then -- not in TD: this is called undeclared<br />
-- calculate the relevant table for this undeclared parameter, based on parameter and value types<br />
table_name = <br />
noval and numeric and 'empty-undeclared-numeric' or<br />
noval and not numeric and 'empty-undeclared' or<br />
hasval and numeric and 'undeclared-numeric' or<br />
'undeclared' -- tzvototi nishar.<br />
else -- in td: test for deprecation and mistype. if deprecated, no further tests<br />
table_name = tp_param.deprecated and hasval and 'deprecated' <br />
or tp_param.deprecated and noval and 'empty-deprecated' <br />
or not compatible( tp_param.type, value ) and 'incompatible'<br />
or not series and already_seen[tp_param] and hasval and 'duplicate'<br />
<br />
if hasval and table_name ~= 'duplicate' then<br />
already_seen[tp_param] = p_name<br />
end<br />
end<br />
<br />
-- report it.<br />
if table_name then<br />
res[table_name] = res[table_name] or {}<br />
if table_name == 'duplicate' then<br />
local primary_param = tp_param['primary']<br />
local primaryData = res[table_name][primary_param]<br />
if not primaryData then<br />
primaryData = {}<br />
table.insert(primaryData, already_seen[tp_param])<br />
end<br />
table.insert(primaryData, p_name)<br />
res[table_name][primary_param] = primaryData<br />
else<br />
res[table_name][p_name] = value<br />
end<br />
end<br />
end<br />
<br />
-- check for empty/missing parameters declared "required" <br />
for p_name, param in pairs( td_params ) do <br />
if param.required and util.empty( t_args[p_name] ) then<br />
local is_alias<br />
for _, alias in ipairs( param.aliases or {} ) do is_alias = is_alias or not util.empty( t_args[alias] ) end<br />
if not is_alias then<br />
res['empty-required'] = res['empty-required'] or {} <br />
res['empty-required'][p_name] = '' <br />
end<br />
end<br />
end<br />
<br />
mw.logObject(res)<br />
<br />
return res<br />
end<br />
<br />
-- wraps report in hidden frame<br />
function wrapReport(report, template_name, options)<br />
mw.logObject(report)<br />
if util.empty( report ) then return '' end<br />
local naked = mw.title.new( template_name )['text']<br />
naked = mw.ustring.gsub(naked, 'Infobox', 'infobox', 1)<br />
<br />
report = ( options['wrapper-prefix'] or "<div class = 'paramvalidator-wrapper'><span class='paramvalidator-error'>" )<br />
.. report<br />
.. ( options['wrapper-suffix'] or "</span></div>" )<br />
<br />
report = mw.ustring.gsub( report, 'tname_naked', naked )<br />
report = mw.ustring.gsub( report, 'templatename', template_name )<br />
<br />
return report<br />
end<br />
<br />
-- this is the "user" version, called with {{#invoke:}} returns a string, as defined by the options parameter<br />
function validateParams( frame )<br />
local options, report, template_name = util.extract_options( frame ), '', frame:getParent():getTitle()<br />
<br />
local ignore = function( p_name )<br />
for _, pattern in ipairs( options['ignore'] or {} ) do<br />
if mw.ustring.match( p_name, '^' .. pattern .. '$' ) then return true end<br />
end<br />
return false<br />
end<br />
<br />
local replace_macros = function( error_type, s, param_names )<br />
function concat_and_escape( t , sep )<br />
sep = sep or ', '<br />
local s = table.concat( t, sep )<br />
return ( mw.ustring.gsub( s, '%%', '%%%%' ) )<br />
end<br />
<br />
if s and ( type( param_names ) == 'table' ) then<br />
local k_ar, kv_ar = {}, {}<br />
for k, v in pairs( param_names ) do<br />
table.insert( k_ar, k )<br />
if type(v) == 'table' then<br />
v = table.concat(v, ', ')<br />
end<br />
<br />
if error_type == 'duplicate' then<br />
table.insert( kv_ar, v)<br />
else<br />
table.insert( kv_ar, k .. ': ' .. v)<br />
end<br />
end<br />
<br />
s = mw.ustring.gsub( s, 'paramname', concat_and_escape( k_ar ) )<br />
s = mw.ustring.gsub( s, 'paramandvalue', concat_and_escape( kv_ar, ' AND ' ) )<br />
<br />
if mw.getCurrentFrame():preprocess( "{{REVISIONID}}" ) ~= "" then<br />
s = mw.ustring.gsub( s, "<div.*<%/div>", "", 1 )<br />
end<br />
end<br />
return s<br />
end<br />
<br />
local report_params = function( key, param_names )<br />
local res = replace_macros( key, options[key], param_names )<br />
res = frame:preprocess(res or '')<br />
report = report .. ( res or '' )<br />
return res<br />
end<br />
<br />
-- no option no work.<br />
if util.table_empty( options ) then return '' end<br />
<br />
-- get the errors.<br />
local violations = calculateViolations( frame, options['doc-subpage'] )<br />
-- special request of bora: use skip_empty_numeric<br />
if violations['empty-undeclared-numeric'] then <br />
for i = 1, tonumber( options['skip-empty-numeric'] ) or 0 do <br />
violations['empty-undeclared-numeric'][i] = nil <br />
end<br />
end<br />
<br />
-- handle ignore list, and prune empty violations - in that order!<br />
local offenders = 0<br />
for name, tab in pairs( violations ) do <br />
-- remove ignored parameters from all violations<br />
for pname in pairs( tab ) do if ignore( pname ) then tab[pname] = nil end end<br />
-- prune empty violations<br />
if util.table_empty( tab ) then violations[name] = nil end<br />
-- WORK IS DONE. report the errors.<br />
-- if report then count it.<br />
if violations[name] and report_params( name, tab ) then offenders = offenders + 1 end <br />
end<br />
<br />
if offenders > 1 then report_params( 'multiple' ) end<br />
if offenders ~= 0 then report_params( 'any' ) end -- could have tested for empty( report ), but since we count them anyway...<br />
return wrapReport(report, template_name, options)<br />
end<br />
<br />
return {<br />
['validateparams'] = validateParams,<br />
['calculateViolations'] = calculateViolations,<br />
['wrapReport'] = wrapReport<br />
}</div>SilverLocust