@travetto/schema 4.0.0-rc.0 → 4.0.0-rc.1

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 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
@@ -12,5 +12,6 @@ export * from './src/validate/validator';
12
12
  export * from './src/validate/error';
13
13
  export * from './src/validate/types';
14
14
  export * from './src/bind-util';
15
+ export * from './src/data';
15
16
  export * from './src/name';
16
17
  export * from './src/types';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@travetto/schema",
3
- "version": "4.0.0-rc.0",
3
+ "version": "4.0.0-rc.1",
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.0"
30
+ "@travetto/registry": "^4.0.0-rc.1"
31
31
  },
32
32
  "peerDependencies": {
33
- "@travetto/transformer": "^4.0.0-rc.0"
33
+ "@travetto/transformer": "^4.0.0-rc.1"
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, DataUtil } from '@travetto/base';
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
+ }