@raven-js/cortex
Version:
Zero-dependency machine learning, AI, and data processing library for modern JavaScript
304 lines (285 loc) • 9.39 kB
JavaScript
/**
* @author Anonyfox <max@anonyfox.com>
* @license MIT
* @see {@link https://github.com/Anonyfox/ravenjs}
* @see {@link https://ravenjs.dev}
* @see {@link https://anonyfox.com}
*/
/**
* @file JSON Schema implementation for data structure validation and type safety.
*
* Provides schema definition, validation, and serialization using vanilla JavaScript classes.
* Supports primitive types, nested schemas, arrays, and field metadata with optional fields.
*/
/**
* @typedef {string | number | boolean} PrimitiveType
* @typedef {PrimitiveType | Schema | Array<PrimitiveType | Schema>} SchemaField
*/
/**
* @typedef {Object} JsonSchemaObject
* @property {string} type
* @property {Object<string, any>} properties
* @property {string[]} required
*/
/**
* Abstract base class for type-safe JSON schema validation and data structure definition.
*
* Enables compile-time safety through JSDoc typing and runtime validation.
* Supports primitive types, nested schemas, arrays, and field metadata with optional fields.
* Provides lazy validation and direct JSON serialization capabilities.
*
* @example
* // Define nested schema structures
* class Address extends Schema {
* street = Schema.field("", { description: "Street address" });
* city = Schema.field("", { description: "City name" });
* zip = Schema.field("", { description: "ZIP code" });
* }
*
* class User extends Schema {
* name = Schema.field("", { description: "User's full name" });
* age = Schema.field(0, { description: "User's age in years" });
* address = Schema.field(new Address(), { description: "Home address" });
* email = Schema.field("", { description: "Email address", optional: true });
* }
*
* // Generate JSON Schema
* const user = new User();
* console.log(user.toJSON());
*
* @example
* // Validate and deserialize JSON data
* const userData = {
* name: "Alice Cooper",
* age: 30,
* address: { street: "123 Main St", city: "Springfield", zip: "12345" }
* };
*
* const user = new User();
* const isValid = user.validate(userData);
* if (isValid) {
* user.fromJSON(userData);
* console.log(user.name.value); // "Alice Cooper"
* }
*/
export class Schema {
/**
* Serialize the schema definition to JSON Schema format.
* Generates standard JSON Schema v4 compatible output.
*
* @returns {string} JSON Schema as formatted string
*/
toJSON() {
return JSON.stringify(this.#buildSchema(this), null, 2);
}
/**
* Deserialize JSON data into schema instance fields.
* Validates structure and types while populating field values.
*
* @param {object | string} json - JSON data to deserialize
* @throws {Error} Missing required fields or type mismatches
*/
fromJSON(json) {
const data = typeof json === "string" ? JSON.parse(json) : json;
this.#populateFromJson(this, data);
}
/**
* Validate JSON data against schema without modification.
* Non-destructive validation for data integrity checking.
*
* @param {object | string} json - JSON data to validate
* @returns {boolean} True if data matches schema structure
*/
validate(json) {
try {
const data = typeof json === "string" ? JSON.parse(json) : json;
this.#validateJson(this, data);
return true;
} catch (_error) {
// Silent validation failure - caller checks boolean result
return false;
}
}
/**
* Build JSON Schema object from instance field definitions.
* Introspects class properties to generate schema structure.
*
* @param {Schema} instance - Schema instance to analyze
* @returns {JsonSchemaObject} JSON Schema object
*/
#buildSchema(instance) {
/** @type {JsonSchemaObject} */
const schema = { type: "object", properties: {}, required: [] };
for (const [key, field] of Object.entries(instance)) {
let fieldSchema;
let isOptional = false;
let description = "";
// Handle fields with metadata vs plain values
if (field?.metadata) {
isOptional = field.metadata.optional || false;
description = field.metadata.description || "";
fieldSchema = this.#getFieldSchema(field.value);
} else {
fieldSchema = this.#getFieldSchema(field);
}
schema.properties[key] = { ...fieldSchema };
if (description) {
schema.properties[key].description = description;
}
if (!isOptional) {
schema.required.push(key);
}
}
return schema;
}
/**
* Generate schema definition for individual field types.
* Handles primitives, nested schemas, and arrays recursively.
*
* @param {SchemaField} field - Field to analyze
* @returns {object} Field schema definition
*/
#getFieldSchema(field) {
if (field instanceof Schema) {
return this.#buildSchema(field);
}
if (Array.isArray(field)) {
const arrayItem = field[0];
return {
type: "array",
items:
arrayItem instanceof Schema
? this.#buildSchema(arrayItem)
: { type: typeof arrayItem },
};
}
return { type: typeof field };
}
/**
* Populate instance fields from validated JSON data.
* Handles nested schemas and type conversion during assignment.
*
* @param {Schema} instance - Target schema instance
* @param {Record<string, any>} jsonData - Source JSON data
* @throws {Error} Missing required fields or validation failures
*/
#populateFromJson(instance, jsonData) {
for (const [key, field] of Object.entries(instance)) {
if (field?.metadata) {
if (jsonData[key] === undefined) {
if (!field.metadata.optional) {
throw new Error(`Missing required property: ${key}`);
}
// Keep existing default value for optional fields
} else {
field.value = this.#deserializeField(field.value, jsonData[key]);
}
} else {
if (jsonData[key] === undefined) {
throw new Error(`Missing required property: ${key}`);
}
instance[/** @type {keyof instance} */ (key)] = this.#deserializeField(
field,
jsonData[key],
);
}
}
}
/**
* Validate JSON data structure against schema definition.
* Comprehensive type and structure validation with detailed error messages.
*
* @param {Schema} instance - Schema definition to validate against
* @param {Record<string, any>} jsonData - JSON data to validate
* @throws {Error} Detailed validation failure messages
*/
#validateJson(instance, jsonData) {
for (const [key, field] of Object.entries(instance)) {
const value = jsonData[key];
if (field?.metadata) {
if (!field.metadata.optional && value === undefined) {
throw new Error(`Missing required property: ${key}`);
}
if (value !== undefined) {
this.#validateField(field.value, value, key);
}
} else {
if (value === undefined) {
throw new Error(`Missing required property: ${key}`);
}
this.#validateField(field, value, key);
}
}
}
/**
* Validate individual field value against expected type.
* Recursive validation for nested schemas and arrays.
*
* @param {SchemaField} field - Expected field definition
* @param {any} value - Actual value to validate
* @param {string} key - Field name for error context
* @throws {Error} Type mismatch or structure validation errors
*/
#validateField(field, value, key) {
if (field instanceof Schema) {
if (typeof value !== "object" || value === null) {
throw new Error(`Invalid type for ${key}: expected object`);
}
// Recursively validate nested schema
field.#validateJson(field, value);
} else if (Array.isArray(field)) {
if (!Array.isArray(value)) {
throw new Error(`Invalid type for ${key}: expected array`);
}
const arrayItem = field[0];
value.forEach((item, index) => {
this.#validateField(arrayItem, item, `${key}[${index}]`);
});
} else if (value !== undefined && typeof value !== typeof field) {
throw new Error(
`Invalid type for ${key}: expected ${typeof field}, got ${typeof value}`,
);
}
}
/**
* Deserialize field value with appropriate type conversion.
* Handles nested schema instantiation and array processing.
*
* @param {SchemaField} field - Field type definition
* @param {any} value - Value to deserialize
* @returns {any} Deserialized value with correct types
*/
#deserializeField(field, value) {
if (field instanceof Schema) {
const NestedSchemaConstructor = /** @type {new () => Schema} */ (
field.constructor
);
const nestedInstance = new NestedSchemaConstructor();
nestedInstance.fromJSON(value);
return nestedInstance;
}
if (Array.isArray(field)) {
const arrayItem = field[0];
return value.map((/** @type {any} */ item) =>
arrayItem instanceof Schema
? this.#deserializeField(arrayItem, item)
: item,
);
}
return value;
}
/**
* Create schema field with metadata and validation rules.
* Factory method for defining typed fields with descriptions and constraints.
*
* @template T
* @param {T} value - Default value and type inference
* @param {object} [options] - Field configuration options
* @param {string} [options.description] - Human-readable field description
* @param {boolean} [options.optional=false] - Whether field is optional
* @returns {{ value: T, metadata: { description: string, optional: boolean } }} Field with metadata
*/
static field(value, { description = "", optional = false } = {}) {
return { value, metadata: { description, optional } };
}
}