Skip to content

Commit 6fc41d8

Browse files
authored
Merge pull request #5 from jsonjoy-com/bin
`bin` kind size limits
2 parents 29ca552 + 0b626fc commit 6fc41d8

File tree

6 files changed

+186
-31
lines changed

6 files changed

+186
-31
lines changed

biome.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,8 @@
2828
"noCommaOperator": "off",
2929
"useSingleVarDeclarator": "off",
3030
"noUnusedTemplateLiteral": "off",
31-
"useDefaultParameterLast": "off"
31+
"useDefaultParameterLast": "off",
32+
"useEnumInitializers": "off"
3233
},
3334
"suspicious": {
3435
"noExplicitAny": "off",

src/codegen/validator/__tests__/codegen.spec.ts

Lines changed: 110 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import {b} from '@jsonjoy.com/util/lib/buffers/b';
12
import {ValidationError} from '../../../constants';
23
import {type OrSchema, s, type Schema} from '../../../schema';
34
import {TypeSystem} from '../../../system';
@@ -125,7 +126,7 @@ describe('"str" type', () => {
125126
exec(type, null, {code: 'STR', errno: ValidationError.STR, message: 'Not a string.', path: []});
126127
});
127128

128-
test('validates minLength', () => {
129+
test('validates "min"', () => {
129130
const type = s.String({min: 3});
130131
exec(type, 'asdf', null);
131132
exec(type, '', {
@@ -142,7 +143,7 @@ describe('"str" type', () => {
142143
});
143144
});
144145

145-
test('validates maxLength', () => {
146+
test('validates "max"', () => {
146147
const type = s.String({max: 5});
147148
exec(type, '', null);
148149
exec(type, 'asdf', null);
@@ -161,7 +162,7 @@ describe('"str" type', () => {
161162
});
162163
});
163164

164-
test('validates minLength and maxLength', () => {
165+
test('validates "min" and "max"', () => {
165166
const type = s.String({min: 3, max: 5});
166167
exec(type, 'aaa', null);
167168
exec(type, 'bbbb', null);
@@ -186,6 +187,112 @@ describe('"str" type', () => {
186187
});
187188
});
188189

190+
test('validates "min" and "max" of equal size', () => {
191+
const type = s.String({min: 4, max: 4});
192+
exec(type, 'aaa', {
193+
code: 'STR_LEN',
194+
errno: ValidationError.STR_LEN,
195+
message: 'Invalid string length.',
196+
path: [],
197+
});
198+
exec(type, 'bbbb', null);
199+
exec(type, 'vvvvv', {
200+
code: 'STR_LEN',
201+
errno: ValidationError.STR_LEN,
202+
message: 'Invalid string length.',
203+
path: [],
204+
});
205+
exec(type, '', {
206+
code: 'STR_LEN',
207+
errno: ValidationError.STR_LEN,
208+
message: 'Invalid string length.',
209+
path: [],
210+
});
211+
exec(type, 'asdfdf', {
212+
code: 'STR_LEN',
213+
errno: ValidationError.STR_LEN,
214+
message: 'Invalid string length.',
215+
path: [],
216+
});
217+
exec(type, 'aasdf sdfdf', {
218+
code: 'STR_LEN',
219+
errno: ValidationError.STR_LEN,
220+
message: 'Invalid string length.',
221+
path: [],
222+
});
223+
});
224+
});
225+
226+
describe('"bin" type', () => {
227+
test('validates a binary blob', () => {
228+
const type = s.bin;
229+
exec(type, b(), null);
230+
exec(type, b(1, 2, 3), null);
231+
exec(type, 123, {code: 'BIN', errno: ValidationError.BIN, message: 'Not a binary.', path: []});
232+
exec(type, null, {code: 'BIN', errno: ValidationError.BIN, message: 'Not a binary.', path: []});
233+
});
234+
235+
test('validates "min"', () => {
236+
const type = s.Binary(s.any, {min: 3});
237+
exec(type, b(1, 2, 3, 4), null);
238+
exec(type, b(), {
239+
code: 'BIN_LEN',
240+
errno: ValidationError.BIN_LEN,
241+
message: 'Invalid binary length.',
242+
path: [],
243+
});
244+
exec(type, b(1, 2), {
245+
code: 'BIN_LEN',
246+
errno: ValidationError.BIN_LEN,
247+
message: 'Invalid binary length.',
248+
path: [],
249+
});
250+
});
251+
252+
test('validates "max"', () => {
253+
const type = s.Binary(s.any, {max: 5});
254+
exec(type, b(), null);
255+
exec(type, b(1, 2, 3, 4), null);
256+
exec(type, b(1, 2, 3, 4, 5), null);
257+
exec(type, b(1, 2, 3, 4, 5, 6), {
258+
code: 'BIN_LEN',
259+
errno: ValidationError.BIN_LEN,
260+
message: 'Invalid binary length.',
261+
path: [],
262+
});
263+
exec(type, b(1, 2, 3, 4, 5, 6, 7, 8, 9), {
264+
code: 'BIN_LEN',
265+
errno: ValidationError.BIN_LEN,
266+
message: 'Invalid binary length.',
267+
path: [],
268+
});
269+
});
270+
271+
test('validates "min" and "max"', () => {
272+
const type = s.Binary(s.any, {min: 3, max: 5});
273+
exec(type, b(1, 2, 3), null);
274+
exec(type, b(1, 2, 3, 4), null);
275+
exec(type, b(1, 2, 3, 4, 5), null);
276+
exec(type, b(), {
277+
code: 'BIN_LEN',
278+
errno: ValidationError.BIN_LEN,
279+
message: 'Invalid binary length.',
280+
path: [],
281+
});
282+
exec(type, b(1, 2, 3, 4, 5, 6), {
283+
code: 'BIN_LEN',
284+
errno: ValidationError.BIN_LEN,
285+
message: 'Invalid binary length.',
286+
path: [],
287+
});
288+
exec(type, b(1, 2, 3, 4, 5, 6, 7, 8, 9), {
289+
code: 'BIN_LEN',
290+
errno: ValidationError.BIN_LEN,
291+
message: 'Invalid binary length.',
292+
path: [],
293+
});
294+
});
295+
189296
test('validates minLength and maxLength of equal size', () => {
190297
const type = s.String({min: 4, max: 4});
191298
exec(type, 'aaa', {

src/constants.ts

Lines changed: 30 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,28 +1,34 @@
1-
/** Validation error codes. */
1+
/**
2+
* Validation error codes.
3+
*
4+
* ATTENTION: Only add new error codes at the end of the list !!!
5+
* =========
6+
*/
27
export enum ValidationError {
38
STR = 0,
4-
NUM = 1,
5-
BOOL = 2,
6-
ARR = 3,
7-
TUP = 4,
8-
OBJ = 5,
9-
MAP = 6,
10-
KEY = 7,
11-
KEYS = 8,
12-
BIN = 9,
13-
OR = 10,
14-
REF = 11,
15-
ENUM = 12,
16-
CONST = 13,
17-
VALIDATION = 14,
18-
INT = 15,
19-
UINT = 16,
20-
STR_LEN = 17,
21-
ARR_LEN = 18,
22-
GT = 19,
23-
GTE = 20,
24-
LT = 21,
25-
LTE = 22,
9+
NUM,
10+
BOOL,
11+
ARR,
12+
TUP,
13+
OBJ,
14+
MAP,
15+
KEY,
16+
KEYS,
17+
BIN,
18+
OR,
19+
REF,
20+
ENUM,
21+
CONST,
22+
VALIDATION,
23+
INT,
24+
UINT,
25+
STR_LEN,
26+
ARR_LEN,
27+
GT,
28+
GTE,
29+
LT,
30+
LTE,
31+
BIN_LEN,
2632
}
2733

2834
/** Human-readable error messages by error code. */
@@ -45,6 +51,7 @@ export const ValidationErrorMessage = {
4551
[ValidationError.INT]: 'Not an integer.',
4652
[ValidationError.UINT]: 'Not an unsigned integer.',
4753
[ValidationError.STR_LEN]: 'Invalid string length.',
54+
[ValidationError.BIN_LEN]: 'Invalid binary length.',
4855
[ValidationError.ARR_LEN]: 'Invalid array length.',
4956
[ValidationError.GT]: 'Value is too small.',
5057
[ValidationError.GTE]: 'Value is too small.',

src/schema/SchemaBuilder.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -88,7 +88,8 @@ export class SchemaBuilder {
8888
return {kind: 'str', ...(a || {})};
8989
}
9090

91-
public Binary<T extends Schema>(type: T, options: Optional<BinarySchema> = {}): BinarySchema {
91+
// public Binary<T extends Schema>(options: Optional<BinarySchema<T>> & Pick<BinarySchema<T>, 'type'>): BinarySchema<T>;
92+
public Binary<T extends Schema>(type: T, options: Optional<Omit<BinarySchema, 'type'>> = {}): BinarySchema<T> {
9293
return {
9394
kind: 'bin',
9495
type,

src/schema/schema.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -142,10 +142,18 @@ export interface StringSchema extends TType<string>, WithValidator {
142142
*/
143143
export interface BinarySchema<T extends TType = any> extends TType, WithValidator {
144144
kind: 'bin';
145+
145146
/** Type of value encoded in the binary data. */
146147
type: T;
148+
147149
/** Codec used for encoding the binary data. */
148150
format?: 'json' | 'cbor' | 'msgpack' | 'resp3' | 'ion' | 'bson' | 'ubjson' | 'bencode';
151+
152+
/** Minimum size in octets. */
153+
min?: number;
154+
155+
/** Maximum size in octets. */
156+
max?: number;
149157
}
150158

151159
/**

src/type/classes/BinaryType.ts

Lines changed: 34 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ 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 {validateTType} from '../../schema/validate';
7+
import {validateMinMax, validateTType} from '../../schema/validate';
88
import type {ValidatorCodegenContext} from '../../codegen/validator/ValidatorCodegenContext';
99
import type {ValidationPath} from '../../codegen/validator/types';
1010
import {ValidationError} from '../../constants';
@@ -23,6 +23,17 @@ import type {json_string} from '@jsonjoy.com/util/lib/json-brand';
2323
import type * as ts from '../../typescript/types';
2424
import type {TypeExportContext} from '../../system/TypeExportContext';
2525

26+
const formats = new Set<schema.BinarySchema['format']>([
27+
'bencode',
28+
'bson',
29+
'cbor',
30+
'ion',
31+
'json',
32+
'msgpack',
33+
'resp3',
34+
'ubjson',
35+
]);
36+
2637
export class BinaryType<T extends Type> extends AbstractType<schema.BinarySchema> {
2738
protected schema: schema.BinarySchema;
2839

@@ -54,7 +65,13 @@ export class BinaryType<T extends Type> extends AbstractType<schema.BinarySchema
5465
}
5566

5667
public validateSchema(): void {
57-
validateTType(this.getSchema(), 'bin');
68+
const schema = this.getSchema();
69+
validateTType(schema, 'bin');
70+
const {min, max, format} = schema;
71+
validateMinMax(min, max);
72+
if (format !== undefined) {
73+
if (!formats.has(format)) throw new Error('FORMAT');
74+
}
5875
this.type.validateSchema();
5976
}
6077

@@ -63,8 +80,22 @@ export class BinaryType<T extends Type> extends AbstractType<schema.BinarySchema
6380
const err = ctx.err(ValidationError.BIN, path);
6481
ctx.js(
6582
// prettier-ignore
66-
/* js */ `if(!(${r} instanceof Uint8Array)${hasBuffer ? /* js */ ` && !Buffer.isBuffer(${r})` : ''}) return ${err};`,
83+
`if(!(${r} instanceof Uint8Array)${hasBuffer ? ` && !Buffer.isBuffer(${r})` : ''}) return ${err};`,
6784
);
85+
const {min, max} = this.schema;
86+
if (typeof min === 'number' && min === max) {
87+
const err = ctx.err(ValidationError.BIN_LEN, path);
88+
ctx.js(`if(${r}.length !== ${min}) return ${err};`);
89+
} else {
90+
if (typeof min === 'number') {
91+
const err = ctx.err(ValidationError.BIN_LEN, path);
92+
ctx.js(`if(${r}.length < ${min}) return ${err};`);
93+
}
94+
if (typeof max === 'number') {
95+
const err = ctx.err(ValidationError.BIN_LEN, path);
96+
ctx.js(`if(${r}.length > ${max}) return ${err};`);
97+
}
98+
}
6899
ctx.emitCustomValidators(this, path, r);
69100
}
70101

0 commit comments

Comments
 (0)