Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
564 changes: 564 additions & 0 deletions src/schema/__tests__/validate.spec.ts

Large diffs are not rendered by default.

211 changes: 210 additions & 1 deletion src/schema/validate.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import type {Display} from './common';
import type {TExample, TType, WithValidator} from './schema';
import type {TExample, TType, WithValidator, Schema} from './schema';

export const validateDisplay = ({title, description, intro}: Display): void => {
if (title !== undefined && typeof title !== 'string') throw new Error('INVALID_TITLE');
Expand Down Expand Up @@ -44,3 +44,212 @@ export const validateMinMax = (min: number | undefined, max: number | undefined)
}
if (min !== undefined && max !== undefined && min > max) throw new Error('MIN_MAX');
};

// Individual schema validation functions for each type

const validateAnySchema = (schema: any): void => {
validateTType(schema, 'any');
};

const validateBooleanSchema = (schema: any): void => {
validateTType(schema, 'bool');
};

const validateNumberSchema = (schema: any): void => {
validateTType(schema, 'num');
validateWithValidator(schema);
const {format, gt, gte, lt, lte} = schema;

if (gt !== undefined && typeof gt !== 'number') throw new Error('GT_TYPE');
if (gte !== undefined && typeof gte !== 'number') throw new Error('GTE_TYPE');
if (lt !== undefined && typeof lt !== 'number') throw new Error('LT_TYPE');
if (lte !== undefined && typeof lte !== 'number') throw new Error('LTE_TYPE');
if (gt !== undefined && gte !== undefined) throw new Error('GT_GTE');
if (lt !== undefined && lte !== undefined) throw new Error('LT_LTE');
if ((gt !== undefined || gte !== undefined) && (lt !== undefined || lte !== undefined))
if ((gt ?? gte)! > (lt ?? lte)!) throw new Error('GT_LT');

if (format !== undefined) {
if (typeof format !== 'string') throw new Error('FORMAT_TYPE');
if (!format) throw new Error('FORMAT_EMPTY');
switch (format) {
case 'i':
case 'u':
case 'f':
case 'i8':
case 'i16':
case 'i32':
case 'i64':
case 'u8':
case 'u16':
case 'u32':
case 'u64':
case 'f32':
case 'f64':
break;
default:
throw new Error('FORMAT_INVALID');
}
}
};

const validateStringSchema = (schema: any): void => {
validateTType(schema, 'str');
validateWithValidator(schema);
const {min, max, ascii, noJsonEscape, format} = schema;

validateMinMax(min, max);

if (ascii !== undefined) {
if (typeof ascii !== 'boolean') throw new Error('ASCII');
}
if (noJsonEscape !== undefined) {
if (typeof noJsonEscape !== 'boolean') throw new Error('NO_JSON_ESCAPE_TYPE');
}
if (format !== undefined) {
if (format !== 'ascii' && format !== 'utf8') {
throw new Error('INVALID_STRING_FORMAT');
}
// If both format and ascii are specified, they should be consistent
if (ascii !== undefined && format === 'ascii' && !ascii) {
throw new Error('FORMAT_ASCII_MISMATCH');
}
}
};

const binaryFormats = new Set(['bencode', 'bson', 'cbor', 'ion', 'json', 'msgpack', 'resp3', 'ubjson']);

const validateBinarySchema = (schema: any, validateChildSchema: (schema: Schema) => void): void => {
validateTType(schema, 'bin');
const {min, max, format} = schema;
validateMinMax(min, max);
if (format !== undefined) {
if (!binaryFormats.has(format)) throw new Error('FORMAT');
}
validateChildSchema(schema.type);
};

const validateArraySchema = (schema: any, validateChildSchema: (schema: Schema) => void): void => {
validateTType(schema, 'arr');
const {min, max} = schema;
validateMinMax(min, max);
validateChildSchema(schema.type);
};

const validateConstSchema = (schema: any): void => {
validateTType(schema, 'const');
};

const validateTupleSchema = (schema: any, validateChildSchema: (schema: Schema) => void): void => {
validateTType(schema, 'tup');
validateWithValidator(schema);
const {types} = schema;
if (!Array.isArray(types)) throw new Error('TYPES_TYPE');
for (const type of types) validateChildSchema(type);
};

const validateObjectSchema = (schema: any, validateChildSchema: (schema: Schema) => void): void => {
validateTType(schema, 'obj');
validateWithValidator(schema);
const {fields, unknownFields} = schema;
if (!Array.isArray(fields)) throw new Error('FIELDS_TYPE');
if (unknownFields !== undefined && typeof unknownFields !== 'boolean') throw new Error('UNKNOWN_FIELDS_TYPE');
for (const field of fields) validateChildSchema(field);
};

const validateFieldSchema = (schema: any, validateChildSchema: (schema: Schema) => void): void => {
validateTType(schema, 'field');
const {key, optional} = schema;
if (typeof key !== 'string') throw new Error('KEY_TYPE');
if (optional !== undefined && typeof optional !== 'boolean') throw new Error('OPTIONAL_TYPE');
validateChildSchema(schema.type);
};

const validateMapSchema = (schema: any, validateChildSchema: (schema: Schema) => void): void => {
validateTType(schema, 'map');
validateChildSchema(schema.type);
};

const validateRefSchema = (schema: any): void => {
validateTType(schema, 'ref');
const {ref} = schema;
if (typeof ref !== 'string') throw new Error('REF_TYPE');
if (!ref) throw new Error('REF_EMPTY');
};

const validateOrSchema = (schema: any, validateChildSchema: (schema: Schema) => void): void => {
validateTType(schema, 'or');
const {types, discriminator} = schema;
if (!discriminator || (discriminator[0] === 'num' && discriminator[1] === -1)) throw new Error('DISCRIMINATOR');
if (!Array.isArray(types)) throw new Error('TYPES_TYPE');
if (!types.length) throw new Error('TYPES_LENGTH');
for (const type of types) validateChildSchema(type);
};

const validateFunctionSchema = (schema: any, validateChildSchema: (schema: Schema) => void): void => {
validateTType(schema, 'fn');
validateChildSchema(schema.req);
validateChildSchema(schema.res);
};

const validateFunctionStreamingSchema = (schema: any, validateChildSchema: (schema: Schema) => void): void => {
validateTType(schema, 'fn$');
validateChildSchema(schema.req);
validateChildSchema(schema.res);
};

/**
* Main router function that validates a schema based on its kind.
* This replaces the individual validateSchema() methods from type classes.
*/
export const validateSchema = (schema: Schema): void => {
switch (schema.kind) {
case 'any':
validateAnySchema(schema);
break;
case 'bool':
validateBooleanSchema(schema);
break;
case 'num':
validateNumberSchema(schema);
break;
case 'str':
validateStringSchema(schema);
break;
case 'bin':
validateBinarySchema(schema, validateSchema);
break;
case 'arr':
validateArraySchema(schema, validateSchema);
break;
case 'const':
validateConstSchema(schema);
break;
case 'tup':
validateTupleSchema(schema, validateSchema);
break;
case 'obj':
validateObjectSchema(schema, validateSchema);
break;
case 'field':
validateFieldSchema(schema, validateSchema);
break;
case 'map':
validateMapSchema(schema, validateSchema);
break;
case 'ref':
validateRefSchema(schema);
break;
case 'or':
validateOrSchema(schema, validateSchema);
break;
case 'fn':
validateFunctionSchema(schema, validateSchema);
break;
case 'fn$':
validateFunctionStreamingSchema(schema, validateSchema);
break;
default:
throw new Error(`Unknown schema kind: ${(schema as any).kind}`);
}
};
5 changes: 4 additions & 1 deletion src/type/classes/AbstractType.ts
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,10 @@ export abstract class AbstractType<S extends schema.Schema> implements BaseType<
}

/** Validates own schema, throws on errors. */
public abstract validateSchema(): void;
public validateSchema(): void {
const {validateSchema} = require('../../schema/validate');
validateSchema(this.getSchema());
}

public validate(value: unknown): void {
const validator = this.validator('string');
Expand Down
5 changes: 0 additions & 5 deletions src/type/classes/AnyType.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import type * as schema from '../../schema';
import {RandomJson} from '@jsonjoy.com/util/lib/json-random';
import {validateTType} from '../../schema/validate';
import type {ValidatorCodegenContext} from '../../codegen/validator/ValidatorCodegenContext';
import type {ValidationPath} from '../../codegen/validator/types';
import type {JsonTextEncoderCodegenContext} from '../../codegen/json/JsonTextEncoderCodegenContext';
Expand All @@ -23,10 +22,6 @@ export class AnyType extends AbstractType<schema.AnySchema> {
super();
}

public validateSchema(): void {
validateTType(this.getSchema(), 'any');
}

public codegenValidator(ctx: ValidatorCodegenContext, path: ValidationPath, r: string): void {
ctx.emitCustomValidators(this, path, r);
}
Expand Down
9 changes: 0 additions & 9 deletions src/type/classes/ArrayType.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ import {JsExpression} from '@jsonjoy.com/util/lib/codegen/util/JsExpression';
import type {BinaryJsonEncoder} from '@jsonjoy.com/json-pack/lib/types';
import {printTree} from 'tree-dump/lib/printTree';
import * as schema from '../../schema';
import {validateMinMax, validateTType} from '../../schema/validate';
import type {ValidatorCodegenContext} from '../../codegen/validator/ValidatorCodegenContext';
import type {ValidationPath} from '../../codegen/validator/types';
import {ValidationError} from '../../constants';
Expand Down Expand Up @@ -42,14 +41,6 @@ export class ArrayType<T extends Type> extends AbstractType<schema.ArraySchema<S
return options as any;
}

public validateSchema(): void {
const schema = this.getSchema();
validateTType(schema, 'arr');
const {min, max} = schema;
validateMinMax(min, max);
this.type.validateSchema();
}

public codegenValidator(ctx: ValidatorCodegenContext, path: ValidationPath, r: string): void {
const rl = ctx.codegen.getRegister();
const ri = ctx.codegen.getRegister();
Expand Down
12 changes: 0 additions & 12 deletions src/type/classes/BinaryType.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import {printTree} from 'tree-dump/lib/printTree';
import * as schema from '../../schema';
import {RandomJson} from '@jsonjoy.com/util/lib/json-random';
import {stringifyBinary} from '@jsonjoy.com/json-pack/lib/json-binary';
import {validateMinMax, validateTType} from '../../schema/validate';
import type {ValidatorCodegenContext} from '../../codegen/validator/ValidatorCodegenContext';
import type {ValidationPath} from '../../codegen/validator/types';
import {ValidationError} from '../../constants';
Expand Down Expand Up @@ -57,17 +56,6 @@ export class BinaryType<T extends Type> extends AbstractType<schema.BinarySchema
return options as any;
}

public validateSchema(): void {
const schema = this.getSchema();
validateTType(schema, 'bin');
const {min, max, format} = schema;
validateMinMax(min, max);
if (format !== undefined) {
if (!formats.has(format)) throw new Error('FORMAT');
}
this.type.validateSchema();
}

public codegenValidator(ctx: ValidatorCodegenContext, path: ValidationPath, r: string): void {
const hasBuffer = typeof Buffer === 'function';
const err = ctx.err(ValidationError.BIN, path);
Expand Down
4 changes: 0 additions & 4 deletions src/type/classes/BooleanType.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,10 +26,6 @@ export class BooleanType extends AbstractType<schema.BooleanSchema> {
super();
}

public validateSchema(): void {
validateTType(this.getSchema(), 'bool');
}

public codegenValidator(ctx: ValidatorCodegenContext, path: ValidationPath, r: string): void {
const err = ctx.err(ValidationError.BOOL, path);
ctx.js(/* js */ `if(typeof ${r} !== "boolean") return ${err};`);
Expand Down
5 changes: 0 additions & 5 deletions src/type/classes/ConstType.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import {cloneBinary} from '@jsonjoy.com/util/lib/json-clone';
import {validateTType} from '../../schema/validate';
import {ValidationError} from '../../constants';
import {maxEncodingCapacity} from '@jsonjoy.com/util/lib/json-size';
import {AbstractType} from './AbstractType';
Expand Down Expand Up @@ -39,10 +38,6 @@ export class ConstType<V = any> extends AbstractType<schema.ConstSchema<V>> {
return options as any;
}

public validateSchema(): void {
validateTType(this.getSchema(), 'const');
}

public codegenValidator(ctx: ValidatorCodegenContext, path: ValidationPath, r: string): void {
const value = this.schema.value;
const equals = deepEqualCodegen(value);
Expand Down
15 changes: 0 additions & 15 deletions src/type/classes/FunctionType.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import {printTree} from 'tree-dump/lib/printTree';
import * as schema from '../../schema';
import {validateTType} from '../../schema/validate';
import {AbstractType} from './AbstractType';
import type {SchemaOf, Type} from '../types';
import type * as ts from '../../typescript/types';
Expand Down Expand Up @@ -50,13 +49,6 @@ export class FunctionType<Req extends Type, Res extends Type> extends AbstractTy
};
}

public validateSchema(): void {
const schema = this.getSchema();
validateTType(schema, 'fn');
this.req.validateSchema();
this.res.validateSchema();
}

public random(): unknown {
return async () => this.res.random();
}
Expand Down Expand Up @@ -129,13 +121,6 @@ export class FunctionStreamingType<Req extends Type, Res extends Type> extends A
};
}

public validateSchema(): void {
const schema = this.getSchema();
validateTType(schema, 'fn$');
this.req.validateSchema();
this.res.validateSchema();
}

public random(): unknown {
return async () => this.res.random();
}
Expand Down
7 changes: 0 additions & 7 deletions src/type/classes/MapType.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import {asString} from '@jsonjoy.com/util/lib/strings/asString';
import {printTree} from 'tree-dump/lib/printTree';
import * as schema from '../../schema';
import {RandomJson} from '@jsonjoy.com/util/lib/json-random';
import {validateTType} from '../../schema/validate';
import type {ValidatorCodegenContext} from '../../codegen/validator/ValidatorCodegenContext';
import type {ValidationPath} from '../../codegen/validator/types';
import {ValidationError} from '../../constants';
Expand Down Expand Up @@ -46,12 +45,6 @@ export class MapType<T extends Type> extends AbstractType<schema.MapSchema<Schem
return options as any;
}

public validateSchema(): void {
const schema = this.getSchema();
validateTType(schema, 'map');
this.type.validateSchema();
}

public codegenValidator(ctx: ValidatorCodegenContext, path: ValidationPath, r: string): void {
const err = ctx.err(ValidationError.MAP, path);
ctx.js(`if (!${r} || (typeof ${r} !== 'object') || (${r}.constructor !== Object)) return ${err};`);
Expand Down
Loading