@bedrockio/yada 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.eslintrc +13 -0
- package/README.md +750 -0
- package/babel.config.json +17 -0
- package/jest.config.json +5 -0
- package/package.json +38 -0
- package/scripts/build +4 -0
- package/src/Schema.js +262 -0
- package/src/TypeSchema.js +23 -0
- package/src/__tests__/index.js +1855 -0
- package/src/array.js +131 -0
- package/src/boolean.js +25 -0
- package/src/date.js +130 -0
- package/src/errors.js +92 -0
- package/src/index.js +51 -0
- package/src/localization.js +38 -0
- package/src/messages.js +72 -0
- package/src/number.js +84 -0
- package/src/object.js +102 -0
- package/src/password.js +61 -0
- package/src/string.js +303 -0
- package/src/utils.js +22 -0
package/jest.config.json
ADDED
package/package.json
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@bedrockio/yada",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Validation library inspired by Joi.",
|
|
5
|
+
"scripts": {
|
|
6
|
+
"test": "NODE_OPTIONS=--experimental-vm-modules jest",
|
|
7
|
+
"build": "scripts/build",
|
|
8
|
+
"lint": "eslint"
|
|
9
|
+
},
|
|
10
|
+
"type": "module",
|
|
11
|
+
"repository": "https://github.com/bedrockio/yada",
|
|
12
|
+
"author": "Andrew Plummer <plummer.andrew@gmail.com>",
|
|
13
|
+
"license": "MIT",
|
|
14
|
+
"dependencies": {
|
|
15
|
+
"validator": "^13.7.0"
|
|
16
|
+
},
|
|
17
|
+
"devDependencies": {
|
|
18
|
+
"@babel/cli": "^7.19.3",
|
|
19
|
+
"@babel/core": "^7.19.6",
|
|
20
|
+
"@babel/preset-env": "^7.19.4",
|
|
21
|
+
"@bedrockio/prettier-config": "^1.0.2",
|
|
22
|
+
"eslint": "^8.26.0",
|
|
23
|
+
"eslint-plugin-bedrock": "^1.0.17",
|
|
24
|
+
"jest": "^29.2.2",
|
|
25
|
+
"prettier": "^2.7.1"
|
|
26
|
+
},
|
|
27
|
+
"exports": {
|
|
28
|
+
".": {
|
|
29
|
+
"import": "./src/index.js",
|
|
30
|
+
"require": "./dist/cjs/index.js"
|
|
31
|
+
}
|
|
32
|
+
},
|
|
33
|
+
"prettier": "@bedrockio/prettier-config",
|
|
34
|
+
"volta": {
|
|
35
|
+
"node": "18.13.0",
|
|
36
|
+
"yarn": "1.22.19"
|
|
37
|
+
}
|
|
38
|
+
}
|
package/scripts/build
ADDED
package/src/Schema.js
ADDED
|
@@ -0,0 +1,262 @@
|
|
|
1
|
+
import {
|
|
2
|
+
isSchemaError,
|
|
3
|
+
ValidationError,
|
|
4
|
+
AssertionError,
|
|
5
|
+
LocalizedError,
|
|
6
|
+
ArrayError,
|
|
7
|
+
} from './errors';
|
|
8
|
+
|
|
9
|
+
const INITIAL_TYPES = ['default', 'required', 'type', 'transform'];
|
|
10
|
+
const REQUIRED_TYPES = ['default', 'required'];
|
|
11
|
+
|
|
12
|
+
export default class Schema {
|
|
13
|
+
constructor(meta = {}) {
|
|
14
|
+
this.assertions = [];
|
|
15
|
+
this.meta = meta;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
// Public
|
|
19
|
+
|
|
20
|
+
required() {
|
|
21
|
+
return this.clone({ required: true }).assert('required', (val) => {
|
|
22
|
+
if (val === undefined) {
|
|
23
|
+
throw new LocalizedError('Value is required.');
|
|
24
|
+
}
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
default(value) {
|
|
29
|
+
return this.clone({ default: value }).assert('default', (val) => {
|
|
30
|
+
if (val === undefined) {
|
|
31
|
+
return value;
|
|
32
|
+
}
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
custom(...args) {
|
|
37
|
+
const type = args.length > 1 ? args[0] : 'custom';
|
|
38
|
+
const fn = args.length > 1 ? args[1] : args[0];
|
|
39
|
+
if (!type) {
|
|
40
|
+
throw new Error('Assertion type required.');
|
|
41
|
+
} else if (!fn) {
|
|
42
|
+
throw new Error('Assertion function required.');
|
|
43
|
+
}
|
|
44
|
+
return this.clone().assert(type, async (val, options) => {
|
|
45
|
+
return await fn(val, options);
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
allow(...set) {
|
|
50
|
+
return this.assertEnum(set, true);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
reject(...set) {
|
|
54
|
+
return this.assertEnum(set, false);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
message(message) {
|
|
58
|
+
return this.clone({ message });
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
tag(tags) {
|
|
62
|
+
return this.clone({
|
|
63
|
+
tags: {
|
|
64
|
+
...this.meta.tags,
|
|
65
|
+
...tags,
|
|
66
|
+
},
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
description(description) {
|
|
71
|
+
return this.tag({
|
|
72
|
+
description,
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
options(options) {
|
|
77
|
+
return this.clone({ ...options });
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
async validate(value, options = {}) {
|
|
81
|
+
let details = [];
|
|
82
|
+
|
|
83
|
+
options = {
|
|
84
|
+
root: value,
|
|
85
|
+
...options,
|
|
86
|
+
...this.meta,
|
|
87
|
+
original: value,
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
for (let assertion of this.assertions) {
|
|
91
|
+
if (!assertion.required && value === undefined) {
|
|
92
|
+
break;
|
|
93
|
+
}
|
|
94
|
+
try {
|
|
95
|
+
const result = await this.runAssertion(assertion, value, options);
|
|
96
|
+
if (result !== undefined) {
|
|
97
|
+
value = result;
|
|
98
|
+
}
|
|
99
|
+
} catch (error) {
|
|
100
|
+
if (error instanceof ArrayError) {
|
|
101
|
+
details = [...details, ...error.details];
|
|
102
|
+
} else {
|
|
103
|
+
details.push(error);
|
|
104
|
+
}
|
|
105
|
+
if (assertion.halt) {
|
|
106
|
+
break;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
if (details.length) {
|
|
112
|
+
const { message = 'Input failed validation.' } = this.meta;
|
|
113
|
+
throw new ValidationError(message, details);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
return value;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
clone(meta) {
|
|
120
|
+
const clone = Object.create(this.constructor.prototype);
|
|
121
|
+
clone.assertions = [...this.assertions];
|
|
122
|
+
clone.meta = { ...this.meta, ...meta };
|
|
123
|
+
return clone;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// Private
|
|
127
|
+
|
|
128
|
+
assertEnum(set, allow) {
|
|
129
|
+
if (set.length === 1 && Array.isArray(set[0])) {
|
|
130
|
+
set = set[0];
|
|
131
|
+
}
|
|
132
|
+
const types = set.map((el) => {
|
|
133
|
+
if (!isSchema(el)) {
|
|
134
|
+
el = JSON.stringify(el);
|
|
135
|
+
}
|
|
136
|
+
return el;
|
|
137
|
+
});
|
|
138
|
+
const msg = `${allow ? 'Must' : 'Must not'} be one of [{types}].`;
|
|
139
|
+
return this.clone({ enum: set }).assert('enum', async (val, options) => {
|
|
140
|
+
if (val !== undefined) {
|
|
141
|
+
for (let el of set) {
|
|
142
|
+
if (isSchema(el)) {
|
|
143
|
+
try {
|
|
144
|
+
await el.validate(val, options);
|
|
145
|
+
return;
|
|
146
|
+
} catch (error) {
|
|
147
|
+
continue;
|
|
148
|
+
}
|
|
149
|
+
} else if ((el === val) === allow) {
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
throw new LocalizedError(msg, {
|
|
154
|
+
types: types.join(', '),
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
assert(type, fn) {
|
|
161
|
+
this.pushAssertion({
|
|
162
|
+
halt: INITIAL_TYPES.includes(type),
|
|
163
|
+
required: REQUIRED_TYPES.includes(type),
|
|
164
|
+
type,
|
|
165
|
+
fn,
|
|
166
|
+
});
|
|
167
|
+
return this;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
pushAssertion(assertion) {
|
|
171
|
+
this.assertions.push(assertion);
|
|
172
|
+
this.assertions.sort((a, b) => {
|
|
173
|
+
return this.getSortIndex(a.type) - this.getSortIndex(b.type);
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
transform(fn) {
|
|
178
|
+
this.assert('transform', (val, options) => {
|
|
179
|
+
if (val !== undefined) {
|
|
180
|
+
return fn(val, options);
|
|
181
|
+
}
|
|
182
|
+
});
|
|
183
|
+
return this;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
getSortIndex(type) {
|
|
187
|
+
const index = INITIAL_TYPES.indexOf(type);
|
|
188
|
+
return index === -1 ? INITIAL_TYPES.length : index;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
async runAssertion(assertion, value, options = {}) {
|
|
192
|
+
const { type, fn } = assertion;
|
|
193
|
+
try {
|
|
194
|
+
return await fn(value, options);
|
|
195
|
+
} catch (error) {
|
|
196
|
+
if (isSchemaError(error)) {
|
|
197
|
+
throw error;
|
|
198
|
+
}
|
|
199
|
+
throw new AssertionError(error.message, type, error);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
toOpenApi(extra) {
|
|
204
|
+
const { required, format, tags, default: defaultValue } = this.meta;
|
|
205
|
+
return {
|
|
206
|
+
...(required && {
|
|
207
|
+
required: true,
|
|
208
|
+
}),
|
|
209
|
+
...(defaultValue && {
|
|
210
|
+
default: defaultValue,
|
|
211
|
+
}),
|
|
212
|
+
...(format && {
|
|
213
|
+
format,
|
|
214
|
+
}),
|
|
215
|
+
...this.enumToOpenApi(),
|
|
216
|
+
...tags,
|
|
217
|
+
...extra,
|
|
218
|
+
};
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
enumToOpenApi() {
|
|
222
|
+
const { enum: allowed } = this.meta;
|
|
223
|
+
if (allowed?.length) {
|
|
224
|
+
const type = typeof allowed[0];
|
|
225
|
+
const allowEnum = allowed.every((entry) => {
|
|
226
|
+
const entryType = typeof entry;
|
|
227
|
+
return entryType !== 'object' && entryType === type;
|
|
228
|
+
});
|
|
229
|
+
if (allowEnum) {
|
|
230
|
+
return {
|
|
231
|
+
type,
|
|
232
|
+
enum: allowed,
|
|
233
|
+
};
|
|
234
|
+
} else {
|
|
235
|
+
const oneOf = [];
|
|
236
|
+
for (let entry of allowed) {
|
|
237
|
+
if (isSchema(entry)) {
|
|
238
|
+
oneOf.push(entry.toOpenApi());
|
|
239
|
+
} else {
|
|
240
|
+
const type = typeof entry;
|
|
241
|
+
let forType = oneOf.find((el) => {
|
|
242
|
+
return el.type === type;
|
|
243
|
+
});
|
|
244
|
+
if (!forType) {
|
|
245
|
+
forType = {
|
|
246
|
+
type,
|
|
247
|
+
enum: [],
|
|
248
|
+
};
|
|
249
|
+
oneOf.push(forType);
|
|
250
|
+
}
|
|
251
|
+
forType.enum.push(entry);
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
return { oneOf };
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
export function isSchema(arg) {
|
|
261
|
+
return arg instanceof Schema;
|
|
262
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import Schema from './Schema';
|
|
2
|
+
|
|
3
|
+
export default class TypeSchema extends Schema {
|
|
4
|
+
constructor(Class, meta) {
|
|
5
|
+
const type = Class.name.toLowerCase();
|
|
6
|
+
super({ type, ...meta });
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
format(name, fn) {
|
|
10
|
+
return this.clone({ format: name }).assert('format', fn);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
toString() {
|
|
14
|
+
return this.meta.type;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
toOpenApi(extra) {
|
|
18
|
+
return {
|
|
19
|
+
type: this.meta.type,
|
|
20
|
+
...super.toOpenApi(extra),
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
}
|