local r2l = require("modules.regex") local schemas = require("core.schemas") local function typeCheck(entryType, typeName, value, path) if value then if entryType == "table" and type(value) ~= "table" then return { path = subpath, error = "Must be a table" } end if entryType == "string" and type(value) ~= "string" then return { path = subpath, error = "Must be a string" } end if entryType == "number" and type(value) ~= "number" then return { path = subpath, error = "Must be a number" } end if entryType == "function" and type(value) ~= "function" then return { path = subpath, error = "Must be a function" } end if entryType == "file" then if type(value) ~= "string" then return { path = subpath, error = "Must be a file path" } end if not fs.exists(value) or fs.isDir(value) then return { path = subpath, error = "File must exist" } end end if entryType == "color" then if type(value) ~= "number" then return { path = subpath, error = "Must be a color" } end m,n = math.frexp(value) if m ~= 0.5 or n < 1 or n > 16 then return { path = subpath, error = "Must be a color" } end end if entryType == "modem" then if type(value) ~= "string" then return { path = subpath, error = "Must be a modem name" } end if peripheral.getType(value) ~= "modem" then return { path = subpath, error = "Must refer to a modem" } end end if entryType == "speaker" then if type(value) ~= "string" then return { path = subpath, error = "Must be a speaker name" } end if peripheral.getType(value) ~= "speaker" then return { path = subpath, error = "Must refer to a speaker" } end end if entryType == "chest" then if type(value) ~= "string" then return { path = subpath, error = "Must be a chest name" } end -- If relative paths are fixed, add not turtle and if value == "left" or value == "right" or value == "front" or value == "back" or value == "top" or value == "bottom" then return { path = subpath, error = "Must be a network name" } end if not turtle and value == "self" then return { path = subpath, error = "Can only be self for turtles" } end if value ~= "self" then local chestMethods = peripheral.getMethods(value) if not chestMethods then return { path = subpath, error = "Must refer to a valid peripheral" } end local hasDropMethod = false for i = 1, #chestMethods do if chestMethods[i] == "drop" then hasDropMethod = true break end end if not hasDropMethod then return { path = subpath, error = "Must refer to an inventory" } end end end if entryType == "sound" then if type(value) ~= "table" then return { path = subpath, error = "Must be a sound" } end if not value.name or type(value.name) ~= "string" then return { path = subpath, error = "Sound must have a name" } end if not value.volume or type(value.volume) ~= "number" then return { path = subpath, error = "Sound must have a volume" } end if not value.pitch or type(value.pitch) ~= "number" then return { path = subpath, error = "Sound must have a pitch" } end end if entryType == "boolean" and type(value) ~= "boolean" then return { path = subpath, error = "Must be a boolean" } end if entryType:sub(1, 5) == "enum<" and entryType:sub(-1) == ">" then local enum = entryType:sub(6, -2) local found = false for enumValue in enum:gmatch("[^|]+") do enumValue = enumValue:sub(enumValue:find("'(.*)'")):sub(2, -2) if value == enumValue then found = true break end end if not found then if typeName then return { path = subpath, error = "Must be entryType " .. typeName .. " matching " .. enum } else return { path = subpath, error = "Must match " .. enum } end end end if entryType:sub(1, 6) == "regex<" and entryType:sub(-1) == ">" then local regexString = entryType:sub(7, -2) local regex = r2l.new(regexString) if not regex(value) then if typeName then return { path = subpath, error = "Must be entryType " .. typeName .. " matching " .. regexString } else return { path = subpath, error = "Must match " .. regexString } end end end end return nil end local function validate(config, schema, path) if not path then path = "" end if schema.__type then if schema.__type:sub(1, 5) == "array" then if schema.__type:sub(6, 6) == "?" and config == nil then return end if type(config) ~= "table" then return { path = path, error = "Must be an array" } end if schema.__min and #config < schema.__min then return { path = path, error = "Must have at least " .. schema.__min .. " entries" } end if schema.__max and #config > schema.__max then return { path = path, error = "Must have at most " .. schema.__max .. " entries" } end if schema.__entry then local validationErrors = {} for i = 1, #config do if type(config[i]) ~= "table" then local err = typeCheck(schema.__entry, schema.__entry, config[i], path .. "[" .. i .. "]") if err then table.insert(validationErrors, err) end else local errs = validate(config[i], schema.__entry, path .. "[" .. i .. "]") if errs and type(errs) == "table" and errs[1] then for _, err in ipairs(errs) do table.insert(validationErrors, err) end else table.insert(validationErrors, errs) end end end if #validationErrors > 0 then return validationErrors end end end else if not config then config = {} end local validationErrors = {} for k,v in pairs(schema) do subpath = path .. "." .. k if type(v) == "table" then local errs = validate(config[k], v, subpath) if errs and type(errs) == "table" and errs[1] then for _, err in ipairs(errs) do table.insert(validationErrors, err) end else table.insert(validationErrors, errs) end else -- If regex or enum, get type name -- E.g. regex<\w{10}>: address -> address local typeDef, typeName _, _, typeDef, typeName = v:find("^(%w+<.+>%??): (.+)$") if typeDef then v = typeDef end if v:sub(-1) ~= "?" and config[k] == nil then table.insert(validationErrors, { path = subpath, error = "Missing required config value" }) end if v:sub(-1) == "?" then v = v:sub(1, -2) end local err = typeCheck(v, typeName, config[k], subpath) if err then table.insert(validationErrors, err) end end end if #validationErrors > 0 then return validationErrors end end end function validationArrayToMap(validationErrors) local map = {} if not validationErrors then return map end for _, err in ipairs(validationErrors) do if err.path then map[err.path:gsub("%[(%d+)%]", "%.%1")] = err.error end end return map end local function validateConfig(config) return validate(config, schemas.configSchema, "config") end local function validateProducts(products) return validate(products, schemas.productsSchema, "products") end return { typeCheck = typeCheck, validate = validate, validateConfig = validateConfig, validateProducts = validateProducts, validationArrayToMap = validationArrayToMap }