Module:UtilsSchema

local p = {} local h = {}

local utilsArg = require("Module:UtilsArg") local utilsTable = require("Module:UtilsTable")

local SYMBOLS = { optional = "[%s]", required = "%s!", default = "%s=%s", array = "{%s}", oneOf = "%s|%s", map = "<%s,%s>" }

p.TYPES = { oneOf = "oneOf", array = "array", record = "record", map = "map", }

function p.getTypeDefinitions(schemas, schemaName, formattingFn) local schema = h.resolveReferences(schemas[schemaName], h.collectReferences(schemas)) return h.getTypeDefs(schemaName, schema, formattingFn) end

function h.collectReferences(schemas, references) references = references or {} for k, schema in pairs(schemas) do references["#/" .. k] = schema h.walkSchema(function(schemaNode)			if schemaNode._id then				references[schemaNode._id] = schemaNode			end		end, schema) end return references end

function h.resolveReferences(schema, references) h.walkSchema(function(schemaNode)		if schemaNode._ref then			local resolvedSchema = utilsTable.merge({}, references[schemaNode._ref], schemaNode)			utilsTable.merge(schemaNode, resolvedSchema)			schemaNode._ref = nil		end	end, schema) return schema end

function h.walkSchema(fn, schemaNode, path) path = path or {} local continue = fn(schemaNode, path) if continue == false then return end if schemaNode.items then h.walkSchema(fn, schemaNode.items, path) end if schemaNode.keys then h.walkSchema(fn, schemaNode.keys, path) end if schemaNode.values then h.walkSchema(fn, schemaNode.values, path) end if schemaNode.properties then for i, v in ipairs(schemaNode.properties) do			local keyPath = utilsTable.concat(path, v.name) h.walkSchema(fn, v, keyPath) end end if schemaNode.oneOf then for i, v in ipairs(schemaNode.oneOf) do			h.walkSchema(fn, v, path) end end end

function h.getTypeDefs(schemaName, schema, formattingFn, parentSchema) local rawType = schema.type local symbolicType local subkeys if rawType == p.TYPES.record then subkeys = {} for _, prop in ipairs(schema.properties) do			local propDef = h.getTypeDefs(prop.name, prop, formattingFn, schema) table.insert(subkeys, propDef) end elseif rawType == p.TYPES.array then local subtypeKeys, subtype = h.getTypeDefs(nil, schema.items, formattingFn, schema) subkeys = subtypeKeys symbolicType = string.format(SYMBOLS.array, subtype) elseif rawType == p.TYPES.map then local _, keyType = h.getTypeDefs(nil, schema.keys, formattingFn, schema) local valueDef, valueType = h.getTypeDefs(nil, schema.values, formattingFn, schema) symbolicType = string.format(SYMBOLS.map, keyType, valueType) subkeys = valueDef elseif schema.oneOf then rawType = p.TYPES.oneOf subkeys = {} local subtypes = {} for i, subschema in ipairs(schema.oneOf) do			local keys, subtype = h.getTypeDefs(i == 1 and "" or "OR", subschema, formattingFn, schema) table.insert(subkeys, keys) if i == 1 then symbolicType = subtype else symbolicType = string.format(SYMBOLS.oneOf, symbolicType, subtype) end end end symbolicType = symbolicType or rawType local parentType if parentSchema then parentType = parentSchema.type or (parentSchema.oneOf and p.TYPES.oneOf) end local key = schemaName if schema.default then key = key and string.format(SYMBOLS.default, schemaName, schema.default) end if schema.required then symbolicType = string.format(SYMBOLS.required, symbolicType) elseif parentType ~= p.TYPES.oneOf and parentType ~= p.TYPES.array and parentType ~= p.TYPES.map then -- otherwise leads to nonsense like [[string], instead of [{string}|string]		symbolicType = string.format(SYMBOLS.optional, symbolicType)		key = key and string.format(SYMBOLS.optional, key)	end	if schema._showDoc == false then		subkeys = nil	end	local formattedDef = formattingFn({		key = key,		rawType = rawType,		symbolicType = symbolicType,		desc = schema.desc,		parentType = parentType,		subkeys = subkeys,	})	return formattedDef, symbolicType end

function p.validate(schemas, schemaName, data, dataName) local schema = h.resolveReferences(schemas[schemaName], h.collectReferences(schemas)) return h.validate(schema, data, dataName) end

function h.validate(schema, data, dataName, path) path = path or {} local errors = {} h.walkSchema(function(schemaNode, nodePath)		local nodeErrors = {}		mw.logObject(nodePath)		local value = utilsTable.property(nodePath)(data)		if schemaNode.oneOf then			for i, node in ipairs(schemaNode.oneOf) do				nodeErrors = utilsTable.concat(nodeErrors, h.validate(node, data, dataName, nodePath))			end			return false		end		if schemaNode.items then			for i, item in ipairs(value or {}) do				local itemPath = utilsTable.concat(nodePath, i)				nodeErrors = utilsTable.concat(nodeErrors, h.validate(schemaNode.items, data, dataName, itemPath))			end		end		local addIfError = h.addIfError(nodeErrors, nodePath)		local expectedType = h.getLuaType(type(schemaNode))		addIfError(utilsArg.type(expectedType)(value, dataName, nodePath))		if #nodeErrors > 0 then			errors = utilsTable.concat(errors, nodeErrors)			return false		end		return true	end, schema, path) return (#errors > 0 and "Category:Pages with invalid data" or nil), errors end

function h.addIfError(errorTable, path) return function(err, msg) if err then table.insert(errorTable, {				path = path,				msg = msg,			}) end end end

function h.getLuaType(schemaType) if schemaType == p.TYPES.array or schemaType == p.TYPES.record or schemaType == p.TYPES.map then return "table" end return schemaType end

p.Documentation = { {		wip = true, name = "validate", params = { {			 	name = "schemas", description = "Collection of related schemas to validate against.", },			{				name = "schemaName", description = "A key in the above table—the particular schema to validate against, which may refernce the other schemas provided.", },			{				name = "data", description = "The data to validate against the given schema.", },			{				name = "dataName", description = "A string used in warning messages to represent the data as a variable." },		},		returns = { "The name of an error category if there are validation errors, nil otherwise", "An array of paths and messages.", },		cases = { {				args = { {						stuff = { type = "record", properties = { {									name = "appearanceCount", type = "number", required = true, },								{									name = "magicWords", oneOf = { {											type = "string", },										{											type = "array", items = { type = "string" }										}									},								},							},						},					},					"stuff", {						magicWords = { "Kooloo", 0, "Limpah" }, },					"args", },			},		},	}, }

return p