UNPKG

@gati-framework/types

Version:

Gati Type System - TypeScript-first branded types and schema system

206 lines 6 kB
/** * @module serialization * @description GType serialization, deserialization, and fingerprinting */ import { createHash } from 'crypto'; import { GTYPE_SCHEMA_VERSION } from './gtype.js'; /** * Serialize GType to JSON string * * @param gtype - GType to serialize * @param pretty - Whether to pretty-print JSON * @returns JSON string * * @example * ```typescript * const serialized = serializeGType(myType, true); * console.log(serialized); * ``` */ export function serializeGType(gtype, pretty = false) { return JSON.stringify(gtype, null, pretty ? 2 : 0); } /** * Deserialize JSON string to GType * Handles version migration automatically * * @param json - JSON string * @returns Parsed GType * @throws {Error} If JSON is invalid or migration fails * * @example * ```typescript * const gtype = deserializeGType(jsonString); * ``` */ export function deserializeGType(json) { try { const parsed = JSON.parse(json); // Check if it's a GTypeSchema (has root property) const isSchema = 'root' in parsed; if (isSchema) { const schema = parsed; // Handle version migration if (!schema.version) { // Pre-v1.0 schema - add version field schema.version = '1.0'; } else if (schema.version !== GTYPE_SCHEMA_VERSION) { // Future version migration logic goes here // For now, we only support 1.0 if (schema.version > GTYPE_SCHEMA_VERSION) { throw new Error(`GType schema version ${schema.version} is newer than supported version ${GTYPE_SCHEMA_VERSION}. ` + `Please update @gati-framework/types.`); } // Migrate from older versions migrateSchema(schema); } return schema; } else { const gtype = parsed; // Add version field if missing (pre-v1.0) if (!gtype.version) { gtype.version = '1.0'; } return gtype; } } catch (error) { throw new Error(`Failed to deserialize GType: ${error instanceof Error ? error.message : 'Unknown error'}`); } } /** * Migrate schema from older versions to current version * * @param schema - Schema to migrate (mutates in place) */ function migrateSchema(schema) { // Future migration logic will go here // For now, we only support 1.0, so this is a no-op schema.version = GTYPE_SCHEMA_VERSION; } /** * Generate a stable fingerprint (SHA-256 hash) of a GType * Used for schema comparison and caching * * @param gtype - GType to fingerprint * @returns SHA-256 hash (hex string) * * @example * ```typescript * const hash1 = fingerprint(type1); * const hash2 = fingerprint(type2); * * if (hash1 === hash2) { * console.log('Types are identical'); * } * ``` */ export function fingerprint(gtype) { // Normalize: serialize without version field for stable hashing const normalized = normalizeForFingerprint(gtype); const serialized = serializeGType(normalized, false); return createHash('sha256').update(serialized).digest('hex'); } /** * Normalize GType for fingerprinting * Removes version field and sorts keys for consistent hashing * * @param gtype - GType to normalize * @returns Normalized copy */ function normalizeForFingerprint(gtype) { // Deep clone to avoid mutation const clone = JSON.parse(JSON.stringify(gtype)); // Remove version field for stable hashing across versions // Use destructuring to avoid delete operator issue with TypeScript const { version: _version, ...withoutVersion } = clone; // Sort object keys recursively for deterministic output return sortKeys(withoutVersion); } /** * Recursively sort object keys for deterministic serialization * * @param obj - Object to sort * @returns Sorted object */ function sortKeys(obj) { if (obj === null || typeof obj !== 'object') { return obj; } if (Array.isArray(obj)) { return obj.map(sortKeys); } const sorted = {}; const keys = Object.keys(obj).sort(); for (const key of keys) { sorted[key] = sortKeys(obj[key]); } return sorted; } /** * Compare two GTypes for structural equality * More efficient than comparing fingerprints for simple checks * * @param a - First GType * @param b - Second GType * @returns True if types are structurally equal * * @example * ```typescript * if (areGTypesEqual(type1, type2)) { * console.log('Types are equivalent'); * } * ``` */ export function areGTypesEqual(a, b) { // Fast path: fingerprint comparison return fingerprint(a) === fingerprint(b); } /** * Create a deep copy of a GType * * @param gtype - GType to clone * @returns Deep copy */ export function cloneGType(gtype) { return JSON.parse(JSON.stringify(gtype)); } /** * Validate a GType schema for correctness * * @param gtype - GType to validate * @returns Validation result with errors * * @example * ```typescript * const result = validateGTypeSchema(myType); * if (!result.valid) { * console.error('Schema errors:', result.errors); * } * ``` */ export function validateGTypeSchema(gtype) { const errors = []; // Check version field if (!gtype.version) { errors.push('Missing version field'); } else if (gtype.version > GTYPE_SCHEMA_VERSION) { errors.push(`Unsupported schema version: ${gtype.version}`); } // Check type field if (!('type' in gtype) && !('root' in gtype)) { errors.push('Missing type or root field'); } // Additional validation logic can be added here // - Check for circular references without $ref // - Validate constraint values // - Check for invalid type combinations return { valid: errors.length === 0, errors, }; } //# sourceMappingURL=serialization.js.map