UNPKG

7.38 kBPlain TextView Raw
1import { ClientException, Exception } from './logger';
2
3type NumberContraints = { min?: number; max?: number };
4type StringContraints = { minLen?: number; maxLen?: number; regexp?: RegExp };
5type ArrayContraints = { minSize?: number; maxSize?: number };
6type DateContraints = { min?: Date | undefined; max?: Date | undefined };
7
8type NullableMethods<T> = {
9 [P in Exclude<keyof T, 'nullable'>]: T[P] extends (value: infer Value, constraints: infer Constraints) => infer Return
10 ? (value: Value | undefined, constraints: Constraints) => Return | undefined
11 : never
12};
13
14class NullableAssert implements NullableMethods<Assert> {
15 signedFloat(value: number | undefined, constraints?: NumberContraints) {
16 return value === undefined ? undefined : this.assert.signedFloat(value, constraints);
17 }
18 uFloat(value: number | undefined, constraints?: NumberContraints) {
19 return value === undefined ? undefined : this.assert.uFloat(value, constraints);
20 }
21 signedInt(value: number | undefined, constraints?: NumberContraints) {
22 return value === undefined ? undefined : this.assert.signedInt(value, constraints);
23 }
24 uInt(value: number | undefined, constraints?: NumberContraints) {
25 return value === undefined ? undefined : this.assert.uInt(value, constraints);
26 }
27 string(value: string | undefined, constraints?: StringContraints) {
28 return value === undefined ? undefined : this.assert.string(value, constraints);
29 }
30 noEmptyString(value: string | undefined, constraints?: StringContraints) {
31 return value === undefined ? undefined : this.assert.noEmptyString(value, constraints);
32 }
33 date(value: Date | undefined, constraints?: DateContraints) {
34 return value === undefined ? undefined : this.assert.date(value, constraints);
35 }
36 array<T>(value: readonly T[] | undefined, constraints?: ArrayContraints) {
37 return value === undefined ? undefined : this.assert.array(value, constraints);
38 }
39 noEmptyArray<T>(value: readonly T[] | undefined, constraints?: ArrayContraints) {
40 return value === undefined ? undefined : this.assert.noEmptyArray(value, constraints);
41 }
42 bool(value: boolean | undefined) {
43 return value === undefined ? undefined : this.assert.bool(value);
44 }
45 email(value: string | undefined) {
46 return value === undefined ? undefined : this.assert.email(value);
47 }
48 object<T extends object>(value: T | undefined) {
49 return value === undefined ? undefined : this.assert.object(value);
50 }
51 noEmptyObject<T extends object>(value: T | undefined) {
52 return value === undefined ? undefined : this.assert.noEmptyObject(value);
53 }
54 union<T extends U, U>(value: T | undefined, union: readonly U[]) {
55 return value === undefined ? undefined : this.assert.union(value, union);
56 }
57 enum(value: string | number | undefined, Enum: { [key: number]: string }) {
58 return value === undefined ? undefined : this.assert.enum(value, Enum);
59 }
60 constructor(protected assert: Assert) {}
61}
62
63export class Assert {
64 constructor(protected ErrorFactory: typeof ClientException) {}
65
66 nullable = new NullableAssert(this);
67
68 protected error(name: string, json: object) {
69 throw new this.ErrorFactory(name, json);
70 }
71
72 protected validateNumber(value: number, constraints: NumberContraints | undefined) {
73 if (typeof value !== 'number') this.error('Value is not a number', { value });
74 if (Number.isNaN(value)) this.error('Value is NaN', { value });
75 if (!Number.isFinite(value)) this.error('Value is not finite', { value });
76 if (constraints !== undefined) {
77 this.between(value, constraints.min, constraints.max, 'Value');
78 }
79 }
80
81 protected between(value: number, min: number | undefined, max: number | undefined, error: string) {
82 if (min !== undefined && min > value) this.error(error + ' should be >= ', { min, value });
83 if (max !== undefined && max < value) this.error(error + ' should be <= ', { max, value });
84 }
85
86 protected validateUnsigned(value: number) {
87 if (value < 0) this.error('Value should be 0 or positive', { value });
88 }
89
90 protected validateInt(value: number) {
91 if (value !== Math.ceil(value)) this.error('Value should be integer', { value });
92 }
93
94 signedFloat(value: number, constraints?: NumberContraints) {
95 this.validateNumber(value, constraints);
96 return value;
97 }
98
99 uFloat(value: number, constraints?: NumberContraints) {
100 this.signedFloat(value, constraints);
101 this.validateUnsigned(value);
102 return value;
103 }
104 signedInt(value: number, constraints?: NumberContraints) {
105 this.validateNumber(value, constraints);
106 this.validateInt(value);
107 return value;
108 }
109 uInt(value: number, constraints?: NumberContraints) {
110 this.signedInt(value, constraints);
111 this.validateUnsigned(value);
112 return value;
113 }
114 string(value: string, constraints?: StringContraints) {
115 if (typeof value !== 'string') this.error('Value is not a string', { value });
116 if (constraints !== undefined) {
117 this.between(value.length, constraints.minLen, constraints.maxLen, 'String length');
118 if (constraints.regexp !== undefined && constraints.regexp.test(value)) {
119 this.error('String is not valueidated by regexp ', { regexp: constraints.regexp.toString(), value });
120 }
121 }
122 return value;
123 }
124 noEmptyString(value: string, constraints?: StringContraints) {
125 this.string(value, constraints);
126 if (value === '') this.error('String should not be empty', { value });
127 return value;
128 }
129 date(value: Date, constraints?: DateContraints) {
130 if (!(value instanceof Date)) this.error('Value is not a date', { value });
131 if (Number.isNaN(value.getTime())) this.error('Date is invalueid', { value });
132 if (constraints !== undefined) {
133 this.between(
134 value.getTime(),
135 constraints.min !== undefined ? constraints.min.getTime() : undefined,
136 constraints.max !== undefined ? constraints.max.getTime() : undefined,
137 'Date',
138 );
139 }
140 return value;
141 }
142 array<T>(value: readonly T[], constraints?: { minSize?: number; maxSize?: number }) {
143 if (!Array.isArray(value)) this.error('Value is not an array', { value });
144 if (constraints !== undefined) this.between(value.length, constraints.minSize, constraints.maxSize, 'Array.length');
145 return value;
146 }
147 noEmptyArray<T>(value: readonly T[], constraints?: { minSize?: number; maxSize?: number }) {
148 this.array(value, constraints);
149 if (value.length === 0) this.error('Array should have elements', { value });
150 return value;
151 }
152 bool(value: boolean) {
153 if (typeof value !== 'boolean') this.error('Value is not a boolean', { value });
154 return value;
155 }
156 email(value: string) {
157 this.string(value);
158 if (!/^\S+@\S+$/.test(value)) this.error('Value is an incorrect email', { value });
159 return value;
160 }
161 object<T extends object>(value: T) {
162 if (typeof value !== 'object' || value === null) this.error('Value is not an object', { value });
163 return value;
164 }
165 noEmptyObject<T extends object>(value: T) {
166 this.object(value);
167 if (Object.keys(value).length === 0) this.error('Object should have elements', { value });
168 return value;
169 }
170 union<T extends U, U>(value: T, union: readonly U[]): T {
171 if (!union.includes(value)) this.error(`Union doesn't have specified element`, { value });
172 return value;
173 }
174 enum(value: number | string, Enum: { [key: number]: string }) {
175 if (Enum[value as number] === undefined) this.error(`Enum doesn't have specified element`, { value });
176 return value;
177 }
178}
179
180export const assert = new Assert(Exception);
181export const clientValidation = new Assert(ClientException);