@nori-zk/proof-conversion
Version:
Verifying zkVM proofs inside o1js circuits, to generate Mina compatible proof
95 lines • 4.15 kB
JavaScript
import { ValidationError } from './ValidationError.js';
import { diagnose, } from './guards/index.js';
// ============================================================================
// ASSERT EXACT STRUCTURE - With diagnose for error messages
// ============================================================================
/**
* Validates that an object exactly matches a schema definition.
* Uses TypeScript assertion signature to narrow the type on success.
*
* This function:
* 1. Checks that the value is an object (not array, null, or primitive)
* 2. Validates all required keys are present
* 3. Validates all values match their schema rules (validators or literals)
* 4. Rejects any unexpected extra keys
* 5. Recursively validates nested objects
*
* For validator functions (ValidatorFn), uses the diagnose function to provide
* detailed path-aware error messages. For literal values, checks exact equality.
*
* @template S - The schema object type
* @param obj - The value to validate
* @param schema - The schema definition (object with validators, literals, or nested schemas)
* @param context - Human-readable context for error messages (e.g., "Groth16 proof")
* @param pathPrefix - Internal parameter for recursive path tracking
* @throws {ValidationError} If validation fails, with detailed error messages
*
* @example
* const schema = {
* name: isString,
* age: isNumber,
* role: "admin" // literal value
* };
*
* assertExactStructure(data, schema, "User");
* // After this line, data is narrowed to: { name: string; age: number; role: "admin" }
*/
export function assertExactStructure(obj, schema, context, pathPrefix = '') {
const errors = [];
if (!obj || typeof obj !== 'object' || Array.isArray(obj)) {
const path = pathPrefix || 'root';
throw new ValidationError(`${path}: must be an object`);
}
const castObj = obj;
const actualKeys = Object.keys(castObj);
// Schemas at this level must be objects to have keys
const castSchema = schema;
const expectedKeys = Object.keys(castSchema);
for (const key of expectedKeys) {
// Build path using bracket notation for consistency: root["key"]
const currentPath = pathPrefix ? `${pathPrefix}["${key}"]` : key;
if (!(key in castObj)) {
errors.push(`${currentPath}: missing required key`);
continue;
}
const rule = castSchema[key];
const value = castObj[key];
if (typeof rule === 'function') {
// Use diagnose for registered guards - it already provides full path with consistent notation
const diagnosticErrors = diagnose(rule, value, currentPath);
if (diagnosticErrors.length > 0) {
errors.push(...diagnosticErrors);
}
}
else if (typeof rule === 'object' && rule !== null) {
// Recursively validate nested objects, passing down the path
try {
assertExactStructure(value, rule, context, currentPath);
}
catch (e) {
if (e instanceof ValidationError) {
// Extract error lines (skip the context header line)
const lines = e.message.split('\n');
const errorLines = lines.filter(line => line.trim() && !line.includes('validation failed:'));
errors.push(...errorLines);
}
}
}
else {
// Exact value comparison for primitives (string, number, boolean, null)
if (value !== rule) {
errors.push(`${currentPath}: must be exactly ${JSON.stringify(rule)}, got ${JSON.stringify(value)}`);
}
}
}
for (const key of actualKeys) {
if (!(key in castSchema)) {
const currentPath = pathPrefix ? `${pathPrefix}["${key}"]` : key;
errors.push(`${currentPath}: unexpected extra key`);
}
}
if (errors.length > 0) {
throw new ValidationError(`${context} validation failed:\n${errors.join('\n')}`);
}
}
//# sourceMappingURL=validation.js.map