Skip to content

Commit b93b84b

Browse files
authored
Merge pull request #16 from jsonjoy-com/copilot/fix-15
[WIP] Refactor type `validateSchema()` methods
2 parents ed849b5 + 875f74f commit b93b84b

File tree

16 files changed

+778
-171
lines changed

16 files changed

+778
-171
lines changed

src/schema/__tests__/validate.spec.ts

Lines changed: 564 additions & 0 deletions
Large diffs are not rendered by default.

src/schema/validate.ts

Lines changed: 210 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import type {Display} from './common';
2-
import type {TExample, TType, WithValidator} from './schema';
2+
import type {TExample, TType, WithValidator, Schema} from './schema';
33

44
export const validateDisplay = ({title, description, intro}: Display): void => {
55
if (title !== undefined && typeof title !== 'string') throw new Error('INVALID_TITLE');
@@ -44,3 +44,212 @@ export const validateMinMax = (min: number | undefined, max: number | undefined)
4444
}
4545
if (min !== undefined && max !== undefined && min > max) throw new Error('MIN_MAX');
4646
};
47+
48+
// Individual schema validation functions for each type
49+
50+
const validateAnySchema = (schema: any): void => {
51+
validateTType(schema, 'any');
52+
};
53+
54+
const validateBooleanSchema = (schema: any): void => {
55+
validateTType(schema, 'bool');
56+
};
57+
58+
const validateNumberSchema = (schema: any): void => {
59+
validateTType(schema, 'num');
60+
validateWithValidator(schema);
61+
const {format, gt, gte, lt, lte} = schema;
62+
63+
if (gt !== undefined && typeof gt !== 'number') throw new Error('GT_TYPE');
64+
if (gte !== undefined && typeof gte !== 'number') throw new Error('GTE_TYPE');
65+
if (lt !== undefined && typeof lt !== 'number') throw new Error('LT_TYPE');
66+
if (lte !== undefined && typeof lte !== 'number') throw new Error('LTE_TYPE');
67+
if (gt !== undefined && gte !== undefined) throw new Error('GT_GTE');
68+
if (lt !== undefined && lte !== undefined) throw new Error('LT_LTE');
69+
if ((gt !== undefined || gte !== undefined) && (lt !== undefined || lte !== undefined))
70+
if ((gt ?? gte)! > (lt ?? lte)!) throw new Error('GT_LT');
71+
72+
if (format !== undefined) {
73+
if (typeof format !== 'string') throw new Error('FORMAT_TYPE');
74+
if (!format) throw new Error('FORMAT_EMPTY');
75+
switch (format) {
76+
case 'i':
77+
case 'u':
78+
case 'f':
79+
case 'i8':
80+
case 'i16':
81+
case 'i32':
82+
case 'i64':
83+
case 'u8':
84+
case 'u16':
85+
case 'u32':
86+
case 'u64':
87+
case 'f32':
88+
case 'f64':
89+
break;
90+
default:
91+
throw new Error('FORMAT_INVALID');
92+
}
93+
}
94+
};
95+
96+
const validateStringSchema = (schema: any): void => {
97+
validateTType(schema, 'str');
98+
validateWithValidator(schema);
99+
const {min, max, ascii, noJsonEscape, format} = schema;
100+
101+
validateMinMax(min, max);
102+
103+
if (ascii !== undefined) {
104+
if (typeof ascii !== 'boolean') throw new Error('ASCII');
105+
}
106+
if (noJsonEscape !== undefined) {
107+
if (typeof noJsonEscape !== 'boolean') throw new Error('NO_JSON_ESCAPE_TYPE');
108+
}
109+
if (format !== undefined) {
110+
if (format !== 'ascii' && format !== 'utf8') {
111+
throw new Error('INVALID_STRING_FORMAT');
112+
}
113+
// If both format and ascii are specified, they should be consistent
114+
if (ascii !== undefined && format === 'ascii' && !ascii) {
115+
throw new Error('FORMAT_ASCII_MISMATCH');
116+
}
117+
}
118+
};
119+
120+
const binaryFormats = new Set(['bencode', 'bson', 'cbor', 'ion', 'json', 'msgpack', 'resp3', 'ubjson']);
121+
122+
const validateBinarySchema = (schema: any, validateChildSchema: (schema: Schema) => void): void => {
123+
validateTType(schema, 'bin');
124+
const {min, max, format} = schema;
125+
validateMinMax(min, max);
126+
if (format !== undefined) {
127+
if (!binaryFormats.has(format)) throw new Error('FORMAT');
128+
}
129+
validateChildSchema(schema.type);
130+
};
131+
132+
const validateArraySchema = (schema: any, validateChildSchema: (schema: Schema) => void): void => {
133+
validateTType(schema, 'arr');
134+
const {min, max} = schema;
135+
validateMinMax(min, max);
136+
validateChildSchema(schema.type);
137+
};
138+
139+
const validateConstSchema = (schema: any): void => {
140+
validateTType(schema, 'const');
141+
};
142+
143+
const validateTupleSchema = (schema: any, validateChildSchema: (schema: Schema) => void): void => {
144+
validateTType(schema, 'tup');
145+
validateWithValidator(schema);
146+
const {types} = schema;
147+
if (!Array.isArray(types)) throw new Error('TYPES_TYPE');
148+
for (const type of types) validateChildSchema(type);
149+
};
150+
151+
const validateObjectSchema = (schema: any, validateChildSchema: (schema: Schema) => void): void => {
152+
validateTType(schema, 'obj');
153+
validateWithValidator(schema);
154+
const {fields, unknownFields} = schema;
155+
if (!Array.isArray(fields)) throw new Error('FIELDS_TYPE');
156+
if (unknownFields !== undefined && typeof unknownFields !== 'boolean') throw new Error('UNKNOWN_FIELDS_TYPE');
157+
for (const field of fields) validateChildSchema(field);
158+
};
159+
160+
const validateFieldSchema = (schema: any, validateChildSchema: (schema: Schema) => void): void => {
161+
validateTType(schema, 'field');
162+
const {key, optional} = schema;
163+
if (typeof key !== 'string') throw new Error('KEY_TYPE');
164+
if (optional !== undefined && typeof optional !== 'boolean') throw new Error('OPTIONAL_TYPE');
165+
validateChildSchema(schema.type);
166+
};
167+
168+
const validateMapSchema = (schema: any, validateChildSchema: (schema: Schema) => void): void => {
169+
validateTType(schema, 'map');
170+
validateChildSchema(schema.type);
171+
};
172+
173+
const validateRefSchema = (schema: any): void => {
174+
validateTType(schema, 'ref');
175+
const {ref} = schema;
176+
if (typeof ref !== 'string') throw new Error('REF_TYPE');
177+
if (!ref) throw new Error('REF_EMPTY');
178+
};
179+
180+
const validateOrSchema = (schema: any, validateChildSchema: (schema: Schema) => void): void => {
181+
validateTType(schema, 'or');
182+
const {types, discriminator} = schema;
183+
if (!discriminator || (discriminator[0] === 'num' && discriminator[1] === -1)) throw new Error('DISCRIMINATOR');
184+
if (!Array.isArray(types)) throw new Error('TYPES_TYPE');
185+
if (!types.length) throw new Error('TYPES_LENGTH');
186+
for (const type of types) validateChildSchema(type);
187+
};
188+
189+
const validateFunctionSchema = (schema: any, validateChildSchema: (schema: Schema) => void): void => {
190+
validateTType(schema, 'fn');
191+
validateChildSchema(schema.req);
192+
validateChildSchema(schema.res);
193+
};
194+
195+
const validateFunctionStreamingSchema = (schema: any, validateChildSchema: (schema: Schema) => void): void => {
196+
validateTType(schema, 'fn$');
197+
validateChildSchema(schema.req);
198+
validateChildSchema(schema.res);
199+
};
200+
201+
/**
202+
* Main router function that validates a schema based on its kind.
203+
* This replaces the individual validateSchema() methods from type classes.
204+
*/
205+
export const validateSchema = (schema: Schema): void => {
206+
switch (schema.kind) {
207+
case 'any':
208+
validateAnySchema(schema);
209+
break;
210+
case 'bool':
211+
validateBooleanSchema(schema);
212+
break;
213+
case 'num':
214+
validateNumberSchema(schema);
215+
break;
216+
case 'str':
217+
validateStringSchema(schema);
218+
break;
219+
case 'bin':
220+
validateBinarySchema(schema, validateSchema);
221+
break;
222+
case 'arr':
223+
validateArraySchema(schema, validateSchema);
224+
break;
225+
case 'const':
226+
validateConstSchema(schema);
227+
break;
228+
case 'tup':
229+
validateTupleSchema(schema, validateSchema);
230+
break;
231+
case 'obj':
232+
validateObjectSchema(schema, validateSchema);
233+
break;
234+
case 'field':
235+
validateFieldSchema(schema, validateSchema);
236+
break;
237+
case 'map':
238+
validateMapSchema(schema, validateSchema);
239+
break;
240+
case 'ref':
241+
validateRefSchema(schema);
242+
break;
243+
case 'or':
244+
validateOrSchema(schema, validateSchema);
245+
break;
246+
case 'fn':
247+
validateFunctionSchema(schema, validateSchema);
248+
break;
249+
case 'fn$':
250+
validateFunctionStreamingSchema(schema, validateSchema);
251+
break;
252+
default:
253+
throw new Error(`Unknown schema kind: ${(schema as any).kind}`);
254+
}
255+
};

src/type/classes/AbstractType.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -97,7 +97,10 @@ export abstract class AbstractType<S extends schema.Schema> implements BaseType<
9797
}
9898

9999
/** Validates own schema, throws on errors. */
100-
public abstract validateSchema(): void;
100+
public validateSchema(): void {
101+
const {validateSchema} = require('../../schema/validate');
102+
validateSchema(this.getSchema());
103+
}
101104

102105
public validate(value: unknown): void {
103106
const validator = this.validator('string');

src/type/classes/AnyType.ts

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
import type * as schema from '../../schema';
22
import {RandomJson} from '@jsonjoy.com/util/lib/json-random';
3-
import {validateTType} from '../../schema/validate';
43
import type {ValidatorCodegenContext} from '../../codegen/validator/ValidatorCodegenContext';
54
import type {ValidationPath} from '../../codegen/validator/types';
65
import type {JsonTextEncoderCodegenContext} from '../../codegen/json/JsonTextEncoderCodegenContext';
@@ -23,10 +22,6 @@ export class AnyType extends AbstractType<schema.AnySchema> {
2322
super();
2423
}
2524

26-
public validateSchema(): void {
27-
validateTType(this.getSchema(), 'any');
28-
}
29-
3025
public codegenValidator(ctx: ValidatorCodegenContext, path: ValidationPath, r: string): void {
3126
ctx.emitCustomValidators(this, path, r);
3227
}

src/type/classes/ArrayType.ts

Lines changed: 0 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@ import {JsExpression} from '@jsonjoy.com/util/lib/codegen/util/JsExpression';
22
import type {BinaryJsonEncoder} from '@jsonjoy.com/json-pack/lib/types';
33
import {printTree} from 'tree-dump/lib/printTree';
44
import * as schema from '../../schema';
5-
import {validateMinMax, validateTType} from '../../schema/validate';
65
import type {ValidatorCodegenContext} from '../../codegen/validator/ValidatorCodegenContext';
76
import type {ValidationPath} from '../../codegen/validator/types';
87
import {ValidationError} from '../../constants';
@@ -42,14 +41,6 @@ export class ArrayType<T extends Type> extends AbstractType<schema.ArraySchema<S
4241
return options as any;
4342
}
4443

45-
public validateSchema(): void {
46-
const schema = this.getSchema();
47-
validateTType(schema, 'arr');
48-
const {min, max} = schema;
49-
validateMinMax(min, max);
50-
this.type.validateSchema();
51-
}
52-
5344
public codegenValidator(ctx: ValidatorCodegenContext, path: ValidationPath, r: string): void {
5445
const rl = ctx.codegen.getRegister();
5546
const ri = ctx.codegen.getRegister();

src/type/classes/BinaryType.ts

Lines changed: 0 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@ import {printTree} from 'tree-dump/lib/printTree';
44
import * as schema from '../../schema';
55
import {RandomJson} from '@jsonjoy.com/util/lib/json-random';
66
import {stringifyBinary} from '@jsonjoy.com/json-pack/lib/json-binary';
7-
import {validateMinMax, validateTType} from '../../schema/validate';
87
import type {ValidatorCodegenContext} from '../../codegen/validator/ValidatorCodegenContext';
98
import type {ValidationPath} from '../../codegen/validator/types';
109
import {ValidationError} from '../../constants';
@@ -57,17 +56,6 @@ export class BinaryType<T extends Type> extends AbstractType<schema.BinarySchema
5756
return options as any;
5857
}
5958

60-
public validateSchema(): void {
61-
const schema = this.getSchema();
62-
validateTType(schema, 'bin');
63-
const {min, max, format} = schema;
64-
validateMinMax(min, max);
65-
if (format !== undefined) {
66-
if (!formats.has(format)) throw new Error('FORMAT');
67-
}
68-
this.type.validateSchema();
69-
}
70-
7159
public codegenValidator(ctx: ValidatorCodegenContext, path: ValidationPath, r: string): void {
7260
const hasBuffer = typeof Buffer === 'function';
7361
const err = ctx.err(ValidationError.BIN, path);

src/type/classes/BooleanType.ts

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -26,10 +26,6 @@ export class BooleanType extends AbstractType<schema.BooleanSchema> {
2626
super();
2727
}
2828

29-
public validateSchema(): void {
30-
validateTType(this.getSchema(), 'bool');
31-
}
32-
3329
public codegenValidator(ctx: ValidatorCodegenContext, path: ValidationPath, r: string): void {
3430
const err = ctx.err(ValidationError.BOOL, path);
3531
ctx.js(/* js */ `if(typeof ${r} !== "boolean") return ${err};`);

src/type/classes/ConstType.ts

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
import {cloneBinary} from '@jsonjoy.com/util/lib/json-clone';
2-
import {validateTType} from '../../schema/validate';
32
import {ValidationError} from '../../constants';
43
import {maxEncodingCapacity} from '@jsonjoy.com/util/lib/json-size';
54
import {AbstractType} from './AbstractType';
@@ -39,10 +38,6 @@ export class ConstType<V = any> extends AbstractType<schema.ConstSchema<V>> {
3938
return options as any;
4039
}
4140

42-
public validateSchema(): void {
43-
validateTType(this.getSchema(), 'const');
44-
}
45-
4641
public codegenValidator(ctx: ValidatorCodegenContext, path: ValidationPath, r: string): void {
4742
const value = this.schema.value;
4843
const equals = deepEqualCodegen(value);

src/type/classes/FunctionType.ts

Lines changed: 0 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
import {printTree} from 'tree-dump/lib/printTree';
22
import * as schema from '../../schema';
3-
import {validateTType} from '../../schema/validate';
43
import {AbstractType} from './AbstractType';
54
import type {SchemaOf, Type} from '../types';
65
import type * as ts from '../../typescript/types';
@@ -50,13 +49,6 @@ export class FunctionType<Req extends Type, Res extends Type> extends AbstractTy
5049
};
5150
}
5251

53-
public validateSchema(): void {
54-
const schema = this.getSchema();
55-
validateTType(schema, 'fn');
56-
this.req.validateSchema();
57-
this.res.validateSchema();
58-
}
59-
6052
public random(): unknown {
6153
return async () => this.res.random();
6254
}
@@ -129,13 +121,6 @@ export class FunctionStreamingType<Req extends Type, Res extends Type> extends A
129121
};
130122
}
131123

132-
public validateSchema(): void {
133-
const schema = this.getSchema();
134-
validateTType(schema, 'fn$');
135-
this.req.validateSchema();
136-
this.res.validateSchema();
137-
}
138-
139124
public random(): unknown {
140125
return async () => this.res.random();
141126
}

src/type/classes/MapType.ts

Lines changed: 0 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@ import {asString} from '@jsonjoy.com/util/lib/strings/asString';
44
import {printTree} from 'tree-dump/lib/printTree';
55
import * as schema from '../../schema';
66
import {RandomJson} from '@jsonjoy.com/util/lib/json-random';
7-
import {validateTType} from '../../schema/validate';
87
import type {ValidatorCodegenContext} from '../../codegen/validator/ValidatorCodegenContext';
98
import type {ValidationPath} from '../../codegen/validator/types';
109
import {ValidationError} from '../../constants';
@@ -46,12 +45,6 @@ export class MapType<T extends Type> extends AbstractType<schema.MapSchema<Schem
4645
return options as any;
4746
}
4847

49-
public validateSchema(): void {
50-
const schema = this.getSchema();
51-
validateTType(schema, 'map');
52-
this.type.validateSchema();
53-
}
54-
5548
public codegenValidator(ctx: ValidatorCodegenContext, path: ValidationPath, r: string): void {
5649
const err = ctx.err(ValidationError.MAP, path);
5750
ctx.js(`if (!${r} || (typeof ${r} !== 'object') || (${r}.constructor !== Object)) return ${err};`);

0 commit comments

Comments
 (0)