@nori-zk/proof-conversion
Version:
Verifying zkVM proofs inside o1js circuits, to generate Mina compatible proof
224 lines • 8.64 kB
JavaScript
// ============================================================================
// CORE TYPES & REGISTRY
// ============================================================================
/**
* Global registry mapping validator functions to their metadata.
* Used by diagnostic and display functions to provide detailed error messages.
*/
const GUARD_REGISTRY = new Map();
/**
* Retrieves metadata for a registered validator function.
*
* @template T - The validator's return type
* @template O - The constraint type (defaults to never)
* @param fn - The validator function to look up
* @returns The validator's metadata, or undefined if not registered
*/
export const getMeta = (fn) => GUARD_REGISTRY.get(fn);
// Implementation
export function guard(fn, meta) {
if (!fn.name || fn.name === 'anonymous') {
throw new TypeError(`Named function required: ${String(fn).slice(0, 50)}`);
}
const entry = {
name: (meta?.name || fn.name).replace(/^is/, ''),
};
if (meta?.inner) {
entry.inner = meta.inner;
}
if (meta?.constraint !== undefined) {
entry.constraint = meta.constraint;
entry.printConstraint = meta.printConstraint;
entry.printDiagnosis = meta.printDiagnosis;
}
GUARD_REGISTRY.set(fn, entry);
return fn;
}
// ============================================================================
// TYPE DISPLAY
// ============================================================================
/**
* Generates a human-readable type name for a validator function.
* Includes constraints and nested types in the output.
*
* @template T - The validator's return type
* @param fn - The validator function
* @returns A formatted string representing the full type (e.g., "Array<String>", "Number(0..255)")
*
* @example
* getFullTypeName(isString) // "String"
* getFullTypeName(isUint8) // "Number(0..255)"
* getFullTypeName(isStringArray) // "Array<String>"
*/
export const getFullTypeName = (fn) => {
const meta = getMeta(fn);
if (!meta)
return 'unknown';
let display = meta.name;
if (meta.printConstraint && meta.constraint !== undefined) {
display += meta.printConstraint(meta.constraint);
}
if (meta.inner) {
display += `<${getFullTypeName(meta.inner)}>`;
}
return display;
};
// ============================================================================
// IDENTIFICATION - Scans registry to identify value type
// ============================================================================
/**
* Identifies the runtime type of a value using registered validators.
* Returns a human-readable string describing the value's type structure.
*
* For primitives, tries to match against registered validators.
* For arrays and objects, recursively identifies nested structure.
*
* @param val - The value to identify
* @returns A string describing the value's type (e.g., "string", "[number, string]", "{x: string, y: number}")
*
* @example
* identify("hello") // "string"
* identify([1, 2, 3]) // "[number, number, number]"
* identify({ x: "a", y: 5 }) // "{x: string, y: number}"
*/
export const identify = (val) => {
// Try to identify using registered validators (primitives only, no constraints)
for (const [validator, meta] of GUARD_REGISTRY) {
if (!meta.inner && !meta.constraint && validator(val)) {
return meta.name;
}
}
// Recurse into arrays to show element types
if (Array.isArray(val)) {
const elementTypes = val.map((elem) => identify(elem));
return `[${elementTypes.join(', ')}]`;
}
// Recurse into objects to show property types
if (val && typeof val === 'object') {
const obj = val;
const constructorName = obj.constructor?.name;
if (constructorName && constructorName !== 'Object') {
return constructorName;
}
const propTypes = Object.entries(obj)
.map(([key, value]) => `${key}: ${identify(value)}`)
.join(', ');
return `{${propTypes}}`;
}
// Special cases last
if (val === null)
return 'null';
if (val === undefined)
return 'undefined';
// Final fallback for primitives
return typeof val;
};
/**
* Recursively converts a schema definition into a human-readable description.
* Replaces validator functions with their type names while preserving structure.
*
* @param schema - The schema definition to describe
* @returns A human-readable description of the schema structure
*
* @example
* describeSchema({ x: isString, y: isNumber })
* // Returns: { x: "String", y: "Number" }
*
* describeSchema({ items: isStringArray })
* // Returns: { items: "Array<String>" }
*/
export const describeSchema = (schema) => {
// Literal primitives
if (typeof schema === 'string')
return schema;
if (typeof schema === 'number')
return schema;
if (typeof schema === 'boolean')
return schema;
if (schema === null)
return null;
// Validator function
if (typeof schema === 'function') {
return getFullTypeName(schema);
}
// Nested object schema
if (typeof schema === 'object') {
const result = {};
for (const [key, value] of Object.entries(schema)) {
result[key] = describeSchema(value);
}
return result;
}
return 'unknown';
};
// ============================================================================
// DIAGNOSE - Recursive with bespoke diagnostics and preserved path
// ============================================================================
/**
* Recursively validates a value and collects detailed error messages.
* Provides path-aware diagnostics showing exactly where and why validation failed.
*
* The function:
* 1. Validates the value using the provided validator
* 2. If validation fails, adds a detailed error message with path
* 3. If validation passes but the type has inner validators (array/object elements),
* recursively validates nested values
* 4. Returns all collected errors with proper indentation and context
*
* @template T - The expected type
* @param fn - The validator function to use
* @param val - The value to validate
* @param path - The current path for error reporting (default: "value")
* @param errors - Accumulated error messages (internal use)
* @returns Array of error messages (empty if validation succeeds)
*
* @example
* const validator = isArrayOfLength(isUint8, 3);
* const errors = diagnose(validator, [1, 2, 300]);
* // Returns: [
* // "value should have type Array[3]<Number(0..255)>",
* // " value[2]: expected Number(0..255), got 300 which exceeds maximum 255"
* // ]
*/
export const diagnose = (fn, val, path = 'value', errors = []) => {
const meta = getMeta(fn);
// Check constraint (guard function checks type + bounds, not inner elements)
if (!fn(val)) {
// Constraint failed - add error with path information
if (meta?.printDiagnosis && meta.constraint !== undefined) {
errors.push(`${path}: expected ${getFullTypeName(fn)}, ${meta.printDiagnosis(meta.constraint, val)}`);
}
else {
errors.push(`${path}: expected ${getFullTypeName(fn) || 'valid value'}, got ${identify(val)}`);
}
// Don't recurse if constraint failed
return errors;
}
// Constraint passed - recurse into inner elements and collect all errors
if (meta?.inner) {
const elementErrorsBefore = errors.length;
if (Array.isArray(val)) {
// Recurse into array elements
for (let i = 0; i < val.length; i++) {
diagnose(meta.inner, val[i], `${path}[${i}]`, errors);
}
}
else if (val && typeof val === 'object') {
// Recurse into object properties (Object.entries works for both arrays and objects)
for (const [key, value] of Object.entries(val)) {
diagnose(meta.inner, value, `${path}.${key}`, errors);
}
}
// If elements had errors, prepend parent type context and indent child errors
if (errors.length > elementErrorsBefore) {
// Indent all element errors
for (let i = elementErrorsBefore; i < errors.length; i++) {
errors[i] = ` ${errors[i]}`;
}
// Prepend parent type context
errors.splice(elementErrorsBefore, 0, `${path} should have type ${getFullTypeName(fn)}`);
}
}
return errors;
};
//# sourceMappingURL=core.js.map