@travetto/schema 4.0.0-rc.0 → 4.0.0-rc.2
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/README.md +11 -0
- package/__index__.ts +1 -0
- package/package.json +3 -3
- package/src/bind-util.ts +2 -1
- package/src/data.ts +216 -0
package/README.md
CHANGED
|
@@ -373,3 +373,14 @@ Validation Failed {
|
|
|
373
373
|
]
|
|
374
374
|
}
|
|
375
375
|
```
|
|
376
|
+
|
|
377
|
+
## Data Utilities
|
|
378
|
+
Data utilities for binding values, and type conversion. Currently [DataUtil](https://github.com/travetto/travetto/tree/main/module/schema/src/data.ts#L8) includes:
|
|
379
|
+
* `deepAssign(a, b, mode ?)` which allows for deep assignment of `b` onto `a`, the `mode` determines how aggressive the assignment is, and how flexible it is. `mode` can have any of the following values:
|
|
380
|
+
* `loose`, which is the default is the most lenient. It will not error out, and overwrites will always happen
|
|
381
|
+
* `coerce`, will attempt to force values from `b` to fit the types of `a`, and if it can't it will error out
|
|
382
|
+
* `strict`, will error out if the types do not match
|
|
383
|
+
|
|
384
|
+
* `coerceType(input: unknown, type: Class<unknown>, strict = true)` which allows for converting an input type into a specified `type` instance, or throw an error if the types are incompatible.
|
|
385
|
+
* `shallowClone<T = unknown>(a: T): T` will shallowly clone a field
|
|
386
|
+
* `filterByKeys<T>(obj: T, exclude: (string | RegExp)[]): T` will filter a given object, and return a plain object (if applicable) with fields excluded using the values in the `exclude` input
|
package/__index__.ts
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@travetto/schema",
|
|
3
|
-
"version": "4.0.0-rc.
|
|
3
|
+
"version": "4.0.0-rc.2",
|
|
4
4
|
"description": "Data type registry for runtime validation, reflection and binding.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"schema",
|
|
@@ -27,10 +27,10 @@
|
|
|
27
27
|
"directory": "module/schema"
|
|
28
28
|
},
|
|
29
29
|
"dependencies": {
|
|
30
|
-
"@travetto/registry": "^4.0.0-rc.
|
|
30
|
+
"@travetto/registry": "^4.0.0-rc.2"
|
|
31
31
|
},
|
|
32
32
|
"peerDependencies": {
|
|
33
|
-
"@travetto/transformer": "^4.0.0-rc.
|
|
33
|
+
"@travetto/transformer": "^4.0.0-rc.2"
|
|
34
34
|
},
|
|
35
35
|
"peerDependenciesMeta": {
|
|
36
36
|
"@travetto/transformer": {
|
package/src/bind-util.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import { Class, ConcreteClass, TypedObject, ObjectUtil
|
|
1
|
+
import { Class, ConcreteClass, TypedObject, ObjectUtil } from '@travetto/base';
|
|
2
2
|
|
|
3
|
+
import { DataUtil } from './data';
|
|
3
4
|
import { AllViewⲐ } from './internal/types';
|
|
4
5
|
import { SchemaRegistry } from './service/registry';
|
|
5
6
|
import { FieldConfig } from './service/types';
|
package/src/data.ts
ADDED
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
import { Class, ClassInstance, TypedObject, ObjectUtil } from '@travetto/base';
|
|
2
|
+
|
|
3
|
+
const REGEX_PAT = /[\/](.*)[\/](i|g|m|s)?/;
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Utilities for data conversion and binding
|
|
7
|
+
*/
|
|
8
|
+
export class DataUtil {
|
|
9
|
+
|
|
10
|
+
static #deepAssignRaw(a: unknown, b: unknown, mode: 'replace' | 'loose' | 'strict' | 'coerce' = 'loose'): unknown {
|
|
11
|
+
const isEmptyA = a === undefined || a === null;
|
|
12
|
+
const isEmptyB = b === undefined || b === null;
|
|
13
|
+
const isArrA = Array.isArray(a);
|
|
14
|
+
const isArrB = Array.isArray(b);
|
|
15
|
+
const isSimpA = !isEmptyA && ObjectUtil.isSimple(a);
|
|
16
|
+
const isSimpB = !isEmptyB && ObjectUtil.isSimple(b);
|
|
17
|
+
|
|
18
|
+
let ret: unknown;
|
|
19
|
+
|
|
20
|
+
if (isEmptyA || isEmptyB) { // If no `a`, `b` always wins
|
|
21
|
+
if (mode === 'replace' || b === null || !isEmptyB) {
|
|
22
|
+
ret = isEmptyB ? b : this.shallowClone(b);
|
|
23
|
+
} else if (!isEmptyA) {
|
|
24
|
+
ret = this.shallowClone(a);
|
|
25
|
+
} else {
|
|
26
|
+
ret = undefined;
|
|
27
|
+
}
|
|
28
|
+
} else {
|
|
29
|
+
if (isArrA !== isArrB || isSimpA !== isSimpB) {
|
|
30
|
+
throw new Error(`Cannot merge differing types ${a} and ${b}`);
|
|
31
|
+
}
|
|
32
|
+
if (isArrB) { // Arrays
|
|
33
|
+
ret = a; // Write onto A
|
|
34
|
+
if (mode === 'replace') {
|
|
35
|
+
ret = b;
|
|
36
|
+
} else {
|
|
37
|
+
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
|
38
|
+
const retArr = ret as unknown[];
|
|
39
|
+
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
|
40
|
+
const bArr = b as unknown[];
|
|
41
|
+
for (let i = 0; i < bArr.length; i++) {
|
|
42
|
+
retArr[i] = this.#deepAssignRaw(retArr[i], bArr[i], mode);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
} else if (isSimpB) { // Scalars
|
|
46
|
+
const match = typeof a === typeof b;
|
|
47
|
+
ret = b;
|
|
48
|
+
|
|
49
|
+
if (!match) { // If types do not match
|
|
50
|
+
if (mode === 'strict') { // Bail on strict
|
|
51
|
+
throw new Error(`Cannot merge ${a} [${typeof a}] with ${b} [${typeof b}]`);
|
|
52
|
+
} else if (mode === 'coerce') { // Force on coerce
|
|
53
|
+
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
|
54
|
+
ret = this.coerceType(b, (a as ClassInstance).constructor, false);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
} else { // Object merge
|
|
58
|
+
ret = a;
|
|
59
|
+
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
|
60
|
+
const bObj = b as Record<string, unknown>;
|
|
61
|
+
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
|
62
|
+
const retObj = ret as Record<string, unknown>;
|
|
63
|
+
|
|
64
|
+
for (const key of Object.keys(bObj)) {
|
|
65
|
+
retObj[key] = this.#deepAssignRaw(retObj[key], bObj[key], mode);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
return ret;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Create regex from string, including flags
|
|
74
|
+
* @param input Convert input to a regex
|
|
75
|
+
*/
|
|
76
|
+
static toRegex(input: string | RegExp): RegExp {
|
|
77
|
+
if (input instanceof RegExp) {
|
|
78
|
+
return input;
|
|
79
|
+
} else if (REGEX_PAT.test(input)) {
|
|
80
|
+
const [, pat, mod] = input.match(REGEX_PAT) ?? [];
|
|
81
|
+
return new RegExp(pat, mod);
|
|
82
|
+
} else {
|
|
83
|
+
return new RegExp(input);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Coerce an input of any type to the class provided
|
|
89
|
+
* @param input Input value
|
|
90
|
+
* @param type Class to coerce to (String, Boolean, Number, Date, RegEx, Object)
|
|
91
|
+
* @param strict Should a failure to coerce throw an error?
|
|
92
|
+
*/
|
|
93
|
+
static coerceType(input: unknown, type: typeof String, strict?: boolean): string;
|
|
94
|
+
static coerceType(input: unknown, type: typeof Number, strict?: boolean): number;
|
|
95
|
+
static coerceType(input: unknown, type: typeof Boolean, strict?: boolean): boolean;
|
|
96
|
+
static coerceType(input: unknown, type: typeof Date, strict?: boolean): Date;
|
|
97
|
+
static coerceType(input: unknown, type: typeof RegExp, strict?: boolean): RegExp;
|
|
98
|
+
static coerceType<T>(input: unknown, type: Class<T>, strict?: boolean): T;
|
|
99
|
+
static coerceType(input: unknown, type: Class<unknown>, strict = true): unknown {
|
|
100
|
+
// Do nothing
|
|
101
|
+
if (input === null || input === undefined) {
|
|
102
|
+
return input;
|
|
103
|
+
} else if (!strict && type !== String && input === '') {
|
|
104
|
+
return undefined; // treat empty string as undefined for non-strings in non-strict mode
|
|
105
|
+
} else if (type && input instanceof type) {
|
|
106
|
+
return input;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
switch (type) {
|
|
110
|
+
case Date: {
|
|
111
|
+
const res = typeof input === 'number' || /^[-]?\d+$/.test(`${input}`) ?
|
|
112
|
+
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
|
113
|
+
new Date(parseInt(input as string, 10)) : new Date(input as Date);
|
|
114
|
+
if (strict && Number.isNaN(res.getTime())) {
|
|
115
|
+
throw new Error(`Invalid date value: ${input}`);
|
|
116
|
+
}
|
|
117
|
+
return res;
|
|
118
|
+
}
|
|
119
|
+
case Number: {
|
|
120
|
+
const res = `${input}`.includes('.') ? parseFloat(`${input}`) : parseInt(`${input}`, 10);
|
|
121
|
+
if (strict && Number.isNaN(res)) {
|
|
122
|
+
throw new Error(`Invalid numeric value: ${input}`);
|
|
123
|
+
}
|
|
124
|
+
return res;
|
|
125
|
+
}
|
|
126
|
+
case Boolean: {
|
|
127
|
+
const match = `${input}`.match(/^((?<TRUE>true|yes|1|on)|false|no|off|0)$/i);
|
|
128
|
+
if (strict && !match) {
|
|
129
|
+
throw new Error(`Invalid boolean value: ${input}`);
|
|
130
|
+
}
|
|
131
|
+
return !!match?.groups?.TRUE;
|
|
132
|
+
}
|
|
133
|
+
case RegExp: {
|
|
134
|
+
if (typeof input === 'string') {
|
|
135
|
+
try {
|
|
136
|
+
return this.toRegex(input);
|
|
137
|
+
} catch {
|
|
138
|
+
if (strict) {
|
|
139
|
+
throw new Error(`Invalid regex: ${input}`);
|
|
140
|
+
} else {
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
} else if (strict) {
|
|
145
|
+
throw new Error('Invalid regex type');
|
|
146
|
+
} else {
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
case Object: {
|
|
151
|
+
if (!strict || ObjectUtil.isPlainObject(input)) {
|
|
152
|
+
return input;
|
|
153
|
+
} else {
|
|
154
|
+
throw new Error('Invalid object type');
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
case undefined:
|
|
158
|
+
case String: return `${input}`;
|
|
159
|
+
}
|
|
160
|
+
if (!strict || ObjectUtil.isPlainObject(input)) {
|
|
161
|
+
return input;
|
|
162
|
+
} else {
|
|
163
|
+
throw new Error(`Unknown type ${type.name}`);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* Clone top level properties to a new object
|
|
169
|
+
* @param o Object to clone
|
|
170
|
+
*/
|
|
171
|
+
static shallowClone<T = unknown>(a: T): T {
|
|
172
|
+
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
|
173
|
+
return (Array.isArray(a) ? a.slice(0) : (ObjectUtil.isSimple(a) ? a : { ...(a as {}) })) as T;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Deep assign from b to a
|
|
178
|
+
* @param a The target
|
|
179
|
+
* @param b The source
|
|
180
|
+
* @param mode How the assignment should be handled
|
|
181
|
+
*/
|
|
182
|
+
static deepAssign<T, U>(a: T, b: U, mode: | 'replace' | 'loose' | 'strict' | 'coerce' = 'loose'): T & U {
|
|
183
|
+
if (!a || ObjectUtil.isSimple(a)) {
|
|
184
|
+
throw new Error(`Cannot merge onto a simple value, ${a}`);
|
|
185
|
+
}
|
|
186
|
+
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
|
187
|
+
return this.#deepAssignRaw(a, b, mode) as T & U;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* Filter object by excluding specific keys
|
|
192
|
+
* @param obj A value to filter, primitives will be untouched
|
|
193
|
+
* @param exclude Strings or patterns to exclude against
|
|
194
|
+
* @returns
|
|
195
|
+
*/
|
|
196
|
+
static filterByKeys<T>(obj: T, exclude: (string | RegExp)[]): T {
|
|
197
|
+
if (obj !== null && obj !== undefined && typeof obj === 'object') {
|
|
198
|
+
const out: Partial<T> = {};
|
|
199
|
+
for (const key of TypedObject.keys(obj)) {
|
|
200
|
+
if (!exclude.some(r => typeof key === 'string' && (typeof r === 'string' ? r === key : r.test(key)))) {
|
|
201
|
+
const val = obj[key];
|
|
202
|
+
if (typeof val === 'object') {
|
|
203
|
+
out[key] = this.filterByKeys(val, exclude);
|
|
204
|
+
} else {
|
|
205
|
+
out[key] = val;
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
|
210
|
+
return out as T;
|
|
211
|
+
} else {
|
|
212
|
+
return obj;
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
}
|