staticql
Version:
Type-safe query engine for static content including Markdown, YAML, JSON, and more.
92 lines (91 loc) • 3.18 kB
JavaScript
/**
* Performs a simple runtime validation of a value against a JSON-like schema.
*
* Supported types: "string", "number", "integer", "boolean", "date", "null", "array", "object".
*
* @param data - The value to validate.
* @param schema - The validation schema object.
* @throws Error if the data does not conform to the schema.
*/
export function simpleValidate(data, schema, path = "", originData = null) {
if (!originData)
originData = JSON.stringify(data);
const expectedType = schema.type;
if (!expectedType)
return;
const types = Array.isArray(expectedType) ? expectedType : [expectedType];
const fullPath = path || "value";
// Handle null
if (data === null) {
if (!types.includes("null")) {
throw new Error(`Expected ${types.join(" or ")} at '${fullPath}', got null, at: ${originData}`);
}
return;
}
// Handle arrays
if (types.includes("array")) {
if (!Array.isArray(data)) {
throw new Error(`Expected array at '${fullPath}', got ${typeof data}, at: ${originData}`);
}
if (schema.items) {
for (let i = 0; i < data.length; i++) {
simpleValidate(data[i], schema.items, `${fullPath}[${i}]`);
}
}
return;
}
// Handle objects
if (types.includes("object")) {
if (typeof data !== "object" || Array.isArray(data)) {
throw new Error(`Expected object at '${fullPath}', got ${typeof data}`);
}
for (const key of schema.required ?? []) {
if (!(key in data)) {
throw new Error(`Missing required field: '${fullPath}.${key}', at: ${originData}`);
}
}
for (const [key, propSchema] of Object.entries(schema.properties ?? {})) {
const val = data[key];
if (val !== undefined) {
simpleValidate(val, propSchema, `${fullPath}.${key}`, originData);
}
}
return;
}
// Handle primitives
const actualType = typeof data;
let valid = false;
for (const type of types) {
switch (type) {
case "string":
if (actualType === "string")
valid = true;
break;
case "number":
if (actualType === "number")
valid = true;
break;
case "integer":
if (actualType === "number" && Number.isInteger(data))
valid = true;
break;
case "boolean":
if (actualType === "boolean")
valid = true;
break;
case "date":
if ((typeof data === "string" || typeof data === "object") &&
!isNaN(Date.parse(data)))
valid = true;
break;
case "null":
// already handled
break;
}
if (valid)
break;
}
if (!valid) {
throw new Error(`Expected ${types.join(" or ")} at '${fullPath}', got ${actualType}, at: ${originData}`);
}
}