@travetto/schema 7.1.4 → 8.0.0-alpha.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/README.md CHANGED
@@ -65,27 +65,27 @@ User:
65
65
  ### Fields
66
66
  This schema provides a powerful base for data binding and validation at runtime. Additionally there may be types that cannot be detected, or some information that the programmer would like to override. Below are the supported field decorators:
67
67
  * [@Field](https://github.com/travetto/travetto/tree/main/module/schema/src/decorator/field.ts#L24) defines a field that will be serialized.
68
- * [@Required](https://github.com/travetto/travetto/tree/main/module/schema/src/decorator/input.ts#L49) defines a that field should be required
69
- * [@Enum](https://github.com/travetto/travetto/tree/main/module/schema/src/decorator/input.ts#L58) defines the allowable values that a field can have
70
- * [@Match](https://github.com/travetto/travetto/tree/main/module/schema/src/decorator/input.ts#L84) defines a regular expression that the field value should match
71
- * [@MinLength](https://github.com/travetto/travetto/tree/main/module/schema/src/decorator/input.ts#L93) enforces min length of a string
72
- * [@MaxLength](https://github.com/travetto/travetto/tree/main/module/schema/src/decorator/input.ts#L104) enforces max length of a string
73
- * [@Min](https://github.com/travetto/travetto/tree/main/module/schema/src/decorator/input.ts#L93) enforces min value for a date or a number
74
- * [@Max](https://github.com/travetto/travetto/tree/main/module/schema/src/decorator/input.ts#L104) enforces max value for a date or a number
75
- * [@Email](https://github.com/travetto/travetto/tree/main/module/schema/src/decorator/input.ts#L134) ensures string field matches basic email regex
76
- * [@Telephone](https://github.com/travetto/travetto/tree/main/module/schema/src/decorator/input.ts#L142) ensures string field matches basic telephone regex
77
- * [@Url](https://github.com/travetto/travetto/tree/main/module/schema/src/decorator/input.ts#L150) ensures string field matches basic url regex
68
+ * [@Required](https://github.com/travetto/travetto/tree/main/module/schema/src/decorator/input.ts#L55) defines a that field should be required
69
+ * [@Enum](https://github.com/travetto/travetto/tree/main/module/schema/src/decorator/input.ts#L64) defines the allowable values that a field can have
70
+ * [@Match](https://github.com/travetto/travetto/tree/main/module/schema/src/decorator/input.ts#L90) defines a regular expression that the field value should match
71
+ * [@MinLength](https://github.com/travetto/travetto/tree/main/module/schema/src/decorator/input.ts#L99) enforces min length of a string
72
+ * [@MaxLength](https://github.com/travetto/travetto/tree/main/module/schema/src/decorator/input.ts#L110) enforces max length of a string
73
+ * [@Min](https://github.com/travetto/travetto/tree/main/module/schema/src/decorator/input.ts#L99) enforces min value for a date or a number
74
+ * [@Max](https://github.com/travetto/travetto/tree/main/module/schema/src/decorator/input.ts#L110) enforces max value for a date or a number
75
+ * [@Email](https://github.com/travetto/travetto/tree/main/module/schema/src/decorator/input.ts#L140) ensures string field matches basic email regex
76
+ * [@Telephone](https://github.com/travetto/travetto/tree/main/module/schema/src/decorator/input.ts#L148) ensures string field matches basic telephone regex
77
+ * [@Url](https://github.com/travetto/travetto/tree/main/module/schema/src/decorator/input.ts#L156) ensures string field matches basic url regex
78
78
  * [@Ignore](https://github.com/travetto/travetto/tree/main/module/schema/src/decorator/common.ts#L41) exclude from auto schema registration
79
- * [@Integer](https://github.com/travetto/travetto/tree/main/module/schema/src/decorator/input.ts#L166) ensures number passed in is only a whole number
80
- * [@Float](https://github.com/travetto/travetto/tree/main/module/schema/src/decorator/input.ts#L173) ensures number passed in allows fractional values
81
- * [@Currency](https://github.com/travetto/travetto/tree/main/module/schema/src/decorator/input.ts#L187) provides support for standard currency
82
- * [@Text](https://github.com/travetto/travetto/tree/main/module/schema/src/decorator/input.ts#L68) indicates that a field is expecting natural language input, not just discrete values
83
- * [@LongText](https://github.com/travetto/travetto/tree/main/module/schema/src/decorator/input.ts#L75) same as text, but expects longer form content
79
+ * [@Integer](https://github.com/travetto/travetto/tree/main/module/schema/src/decorator/input.ts#L172) ensures number passed in is only a whole number
80
+ * [@Float](https://github.com/travetto/travetto/tree/main/module/schema/src/decorator/input.ts#L179) ensures number passed in allows fractional values
81
+ * [@Currency](https://github.com/travetto/travetto/tree/main/module/schema/src/decorator/input.ts#L193) provides support for standard currency
82
+ * [@Text](https://github.com/travetto/travetto/tree/main/module/schema/src/decorator/input.ts#L74) indicates that a field is expecting natural language input, not just discrete values
83
+ * [@LongText](https://github.com/travetto/travetto/tree/main/module/schema/src/decorator/input.ts#L81) same as text, but expects longer form content
84
84
  * [@Readonly](https://github.com/travetto/travetto/tree/main/module/schema/src/decorator/field.ts#L40) defines a that field should not be bindable external to the class
85
85
  * [@Writeonly](https://github.com/travetto/travetto/tree/main/module/schema/src/decorator/field.ts#L33) defines a that field should not be exported in serialization, but that it can be bound to
86
86
  * [@Secret](https://github.com/travetto/travetto/tree/main/module/schema/src/decorator/field.ts#L48) marks a field as being sensitive. This is used by certain logging activities to ensure sensitive information is not logged out.
87
- * [@Specifier](https://github.com/travetto/travetto/tree/main/module/schema/src/decorator/input.ts#L195) attributes additional specifiers to a field, allowing for more specification beyond just the field's type.
88
- * [@DiscriminatorField](https://github.com/travetto/travetto/tree/main/module/schema/src/decorator/input.ts#L202) allows for promoting a given field as the owner of the sub type discriminator.
87
+ * [@Specifier](https://github.com/travetto/travetto/tree/main/module/schema/src/decorator/input.ts#L201) attributes additional specifiers to a field, allowing for more specification beyond just the field's type.
88
+ * [@DiscriminatorField](https://github.com/travetto/travetto/tree/main/module/schema/src/decorator/input.ts#L208) allows for promoting a given field as the owner of the sub type discriminator.
89
89
 
90
90
  Additionally, schemas can be nested to form more complex data structures that are able to bound and validated.
91
91
 
@@ -218,6 +218,7 @@ would produce an exception similar to following structure
218
218
  $ trv main doc/person-invalid-output.ts
219
219
 
220
220
  Validation Failed {
221
+ "$trv": "runtime",
221
222
  "message": "Validation errors have occurred",
222
223
  "category": "data",
223
224
  "type": "ValidationResultError",
@@ -297,7 +298,7 @@ export interface ValidationError {
297
298
  /**
298
299
  * Number to compare against
299
300
  */
300
- limit?: number | Date;
301
+ limit?: NumericLikeIntrinsic;
301
302
  /**
302
303
  * The type of the field
303
304
  */
@@ -330,35 +331,39 @@ export type Point = [number, number];
330
331
  **Code: Point Implementation**
331
332
  ```typescript
332
333
  import { DataUtil } from '../data.ts';
334
+ import { SchemaTypeUtil } from '../type-config.ts';
333
335
 
334
336
  const InvalidSymbol = Symbol();
335
337
 
336
338
  /**
337
- * Point Contract
339
+ * Convert to tuple of two numbers
338
340
  */
339
- export class PointContract {
340
-
341
- /**
342
- * Validate we have an actual point
343
- */
344
- static validateSchema(input: unknown): 'type' | undefined {
345
- const bound = this.bindSchema(input);
346
- return bound !== InvalidSymbol && bound && !isNaN(bound[0]) && !isNaN(bound[1]) ? undefined : 'type';
341
+ function bindPoint(input: unknown): [number, number] | typeof InvalidSymbol | undefined {
342
+ if (Array.isArray(input) && input.length === 2) {
343
+ const [a, b] = input.map(value => DataUtil.coerceType(value, Number, false));
344
+ return [a, b];
345
+ } else {
346
+ return InvalidSymbol;
347
347
  }
348
+ }
348
349
 
349
- /**
350
- * Convert to tuple of two numbers
351
- */
352
- static bindSchema(input: unknown): [number, number] | typeof InvalidSymbol | undefined {
353
- if (Array.isArray(input) && input.length === 2) {
354
- const [a, b] = input.map(value => DataUtil.coerceType(value, Number, false));
355
- return [a, b];
356
- } else {
357
- return InvalidSymbol;
358
- }
359
- }
350
+ /**
351
+ * Validate we have an actual point
352
+ */
353
+ function validatePoint(input: unknown): 'type' | undefined {
354
+ const bound = bindPoint(input);
355
+ return bound !== InvalidSymbol && bound && !isNaN(bound[0]) && !isNaN(bound[1]) ? undefined : 'type';
360
356
  }
361
357
 
358
+ /**
359
+ * Point Contract
360
+ */
361
+ export class PointContract { }
362
+
363
+ SchemaTypeUtil.setSchemaTypeConfig(PointContract, {
364
+ validate: validatePoint,
365
+ bind: bindPoint,
366
+ });
362
367
  Object.defineProperty(PointContract, 'name', { value: 'Point' });
363
368
  ```
364
369
 
@@ -384,6 +389,7 @@ All that happens now, is the type is exported, and the class above is able to pr
384
389
  $ trv main doc/custom-type-output.ts
385
390
 
386
391
  Validation Failed {
392
+ "$trv": "runtime",
387
393
  "message": "Validation errors have occurred",
388
394
  "category": "data",
389
395
  "type": "ValidationResultError",
package/__index__.ts CHANGED
@@ -14,5 +14,6 @@ export * from './src/bind-util.ts';
14
14
  export * from './src/data.ts';
15
15
  export * from './src/name.ts';
16
16
  export * from './src/types.ts';
17
+ export * from './src/type-config.ts';
17
18
  export * from './src/service/registry-index.ts';
18
19
  export * from './src/service/registry-adapter.ts';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@travetto/schema",
3
- "version": "7.1.4",
3
+ "version": "8.0.0-alpha.0",
4
4
  "type": "module",
5
5
  "description": "Data type registry for runtime validation, reflection and binding.",
6
6
  "keywords": [
@@ -28,10 +28,10 @@
28
28
  "directory": "module/schema"
29
29
  },
30
30
  "dependencies": {
31
- "@travetto/registry": "^7.1.4"
31
+ "@travetto/registry": "^8.0.0-alpha.0"
32
32
  },
33
33
  "peerDependencies": {
34
- "@travetto/transformer": "^7.1.3"
34
+ "@travetto/transformer": "^8.0.0-alpha.0"
35
35
  },
36
36
  "peerDependenciesMeta": {
37
37
  "@travetto/transformer": {
package/src/bind-util.ts CHANGED
@@ -3,6 +3,7 @@ import { castTo, type Class, classConstruct, asFull, TypedObject, castKey } from
3
3
  import { DataUtil } from './data.ts';
4
4
  import type { SchemaInputConfig, SchemaParameterConfig, SchemaFieldMap } from './service/types.ts';
5
5
  import { SchemaRegistryIndex } from './service/registry-index.ts';
6
+ import { SchemaTypeUtil } from './type-config.ts';
6
7
 
7
8
  type BindConfig = {
8
9
  view?: string;
@@ -25,8 +26,9 @@ export class BindUtil {
25
26
  * @param value The provided value
26
27
  */
27
28
  static #coerceType<T>(config: SchemaInputConfig, value: unknown): T | null | undefined {
28
- if (config.type?.bindSchema) {
29
- value = config.type.bindSchema(value);
29
+ const typeConfig = SchemaTypeUtil.getSchemaTypeConfig(config.type);
30
+ if (typeConfig?.bind) {
31
+ value = typeConfig.bind(value);
30
32
  } else {
31
33
  value = DataUtil.coerceType(value, config.type, false);
32
34
 
package/src/data.ts CHANGED
@@ -152,9 +152,11 @@ export class DataUtil {
152
152
  input :
153
153
  typeof input === 'number' ?
154
154
  new Date(input) :
155
- (typeof input === 'string' && /^[-]?\d+$/.test(input)) ?
156
- new Date(parseInt(input, 10)) :
157
- new Date(input.toString());
155
+ typeof input === 'bigint' ?
156
+ new Date(Number(input)) :
157
+ (typeof input === 'string' && /^[-]?\d+$/.test(input)) ?
158
+ new Date(parseInt(input, 10)) :
159
+ new Date(input.toString());
158
160
  }
159
161
  if (strict && value && Number.isNaN(value.getTime())) {
160
162
  throw new Error(`Invalid date value: ${input}`);
@@ -162,6 +164,9 @@ export class DataUtil {
162
164
  return value;
163
165
  }
164
166
  case Number: {
167
+ if (typeof input === 'bigint') {
168
+ return Number(input);
169
+ }
165
170
  const value = `${input}`.includes('.') ? parseFloat(`${input}`) : parseInt(`${input}`, 10);
166
171
  if (strict && Number.isNaN(value)) {
167
172
  throw new Error(`Invalid numeric value: ${input}`);
@@ -1,9 +1,15 @@
1
- import { type Any, type Class, type ClassInstance, getClass } from '@travetto/runtime';
1
+ import { type Any, type Class, type ClassInstance, getClass, type NumericLikeIntrinsic, type NumericPrimitive, type Primitive } from '@travetto/runtime';
2
2
 
3
3
  import { CommonRegex } from '../validate/regex.ts';
4
4
  import { CONSTRUCTOR_PROPERTY, type SchemaInputConfig } from '../service/types.ts';
5
5
  import { SchemaRegistryIndex } from '../service/registry-index.ts';
6
6
 
7
+ type StringType = string | string[];
8
+ type LengthType = string | unknown[] | Uint8Array | Uint16Array | Uint32Array;
9
+ type NumberType = NumericPrimitive | NumericPrimitive[];
10
+ type NumberLikeType = NumericLikeIntrinsic | NumericLikeIntrinsic[];
11
+ type EnumType = Exclude<Primitive, 'boolean'> | Exclude<Primitive, 'boolean'>[];
12
+
7
13
  type PropType<V> = (<T extends Partial<Record<K, V | Function>>, K extends string>(
8
14
  instance: T, property: K, idx?: TypedPropertyDescriptor<Any> | number
9
15
  ) => void);
@@ -55,7 +61,7 @@ export function Required(active = true, message?: string): PropType<unknown> { r
55
61
  * @augments `@travetto/schema:Input`
56
62
  * @kind decorator
57
63
  */
58
- export function Enum(values: string[], message?: string): PropType<string | number> {
64
+ export function Enum(values: string[], message?: string): PropType<EnumType> {
59
65
  message = message || `{path} is only allowed to be "${values.join('" or "')}"`;
60
66
  return input({ enum: { values, message } });
61
67
  }
@@ -65,14 +71,14 @@ export function Enum(values: string[], message?: string): PropType<string | numb
65
71
  * @augments `@travetto/schema:Input`
66
72
  * @kind decorator
67
73
  */
68
- export function Text(): PropType<string | string[]> { return input({ specifiers: ['text'] }); }
74
+ export function Text(): PropType<StringType> { return input({ specifiers: ['text'] }); }
69
75
 
70
76
  /**
71
77
  * Mark the input to indicate it's for long form text
72
78
  * @augments `@travetto/schema:Input`
73
79
  * @kind decorator
74
80
  */
75
- export function LongText(): PropType<string | string[]> { return input({ specifiers: ['text', 'long'] }); }
81
+ export function LongText(): PropType<StringType> { return input({ specifiers: ['text', 'long'] }); }
76
82
 
77
83
  /**
78
84
  * Require the input to match a specific RegExp
@@ -81,7 +87,7 @@ export function LongText(): PropType<string | string[]> { return input({ specifi
81
87
  * @augments `@travetto/schema:Input`
82
88
  * @kind decorator
83
89
  */
84
- export function Match(regex: RegExp, message?: string): PropType<string | string[]> { return input({ match: { regex, message } }); }
90
+ export function Match(regex: RegExp, message?: string): PropType<StringType> { return input({ match: { regex, message } }); }
85
91
 
86
92
  /**
87
93
  * The minimum length for the string or array
@@ -90,7 +96,7 @@ export function Match(regex: RegExp, message?: string): PropType<string | string
90
96
  * @augments `@travetto/schema:Input`
91
97
  * @kind decorator
92
98
  */
93
- export function MinLength(limit: number, message?: string): PropType<string | unknown[]> {
99
+ export function MinLength(limit: number, message?: string): PropType<LengthType> {
94
100
  return input({ minlength: { limit, message }, ...(limit === 0 ? { required: { active: false } } : {}) });
95
101
  }
96
102
 
@@ -101,7 +107,7 @@ export function MinLength(limit: number, message?: string): PropType<string | un
101
107
  * @augments `@travetto/schema:Input`
102
108
  * @kind decorator
103
109
  */
104
- export function MaxLength(limit: number, message?: string): PropType<string | unknown[]> { return input({ maxlength: { limit, message } }); }
110
+ export function MaxLength(limit: number, message?: string): PropType<LengthType> { return input({ maxlength: { limit, message } }); }
105
111
 
106
112
  /**
107
113
  * The minimum value
@@ -110,8 +116,8 @@ export function MaxLength(limit: number, message?: string): PropType<string | un
110
116
  * @augments `@travetto/schema:Input`
111
117
  * @kind decorator
112
118
  */
113
- export function Min<T extends number | Date>(limit: T, message?: string): PropType<Date | number> {
114
- return input({ min: { limit, message } });
119
+ export function Min(limit: NumericLikeIntrinsic, message?: string): PropType<NumberLikeType> {
120
+ return input<NumberLikeType>({ min: { limit, message } });
115
121
  }
116
122
 
117
123
  /**
@@ -121,8 +127,8 @@ export function Min<T extends number | Date>(limit: T, message?: string): PropTy
121
127
  * @augments `@travetto/schema:Input`
122
128
  * @kind decorator
123
129
  */
124
- export function Max<T extends number | Date>(limit: T, message?: string): PropType<Date | number> {
125
- return input({ max: { limit, message } });
130
+ export function Max(limit: NumericLikeIntrinsic, message?: string): PropType<NumberLikeType> {
131
+ return input<NumberLikeType>({ max: { limit, message } });
126
132
  }
127
133
 
128
134
  /**
@@ -131,7 +137,7 @@ export function Max<T extends number | Date>(limit: T, message?: string): PropTy
131
137
  * @augments `@travetto/schema:Input`
132
138
  * @kind decorator
133
139
  */
134
- export function Email(message?: string): PropType<string | string[]> { return Match(CommonRegex.email, message); }
140
+ export function Email(message?: string): PropType<StringType> { return Match(CommonRegex.email, message); }
135
141
 
136
142
  /**
137
143
  * Mark an input as an telephone number
@@ -139,7 +145,7 @@ export function Email(message?: string): PropType<string | string[]> { return Ma
139
145
  * @augments `@travetto/schema:Input`
140
146
  * @kind decorator
141
147
  */
142
- export function Telephone(message?: string): PropType<string | string[]> { return Match(CommonRegex.telephone, message); }
148
+ export function Telephone(message?: string): PropType<StringType> { return Match(CommonRegex.telephone, message); }
143
149
 
144
150
  /**
145
151
  * Mark an input as a url
@@ -147,7 +153,7 @@ export function Telephone(message?: string): PropType<string | string[]> { retur
147
153
  * @augments `@travetto/schema:Input`
148
154
  * @kind decorator
149
155
  */
150
- export function Url(message?: string): PropType<string | string[]> { return Match(CommonRegex.url, message); }
156
+ export function Url(message?: string): PropType<StringType> { return Match(CommonRegex.url, message); }
151
157
 
152
158
  /**
153
159
  * Determine the numeric precision of the value
@@ -163,7 +169,7 @@ export function Precision(digits: number, decimals?: number): PropType<number> {
163
169
  * @augments `@travetto/schema:Input`
164
170
  * @kind decorator
165
171
  */
166
- export function Integer(): PropType<number> { return Precision(0); }
172
+ export function Integer(): PropType<NumberType> { return Precision(0); }
167
173
 
168
174
  /**
169
175
  * Mark a number as a float
@@ -177,14 +183,14 @@ export function Float(): PropType<number> { return Precision(10, 7); }
177
183
  * @augments `@travetto/schema:Input`
178
184
  * @kind decorator
179
185
  */
180
- export function Long(): PropType<number> { return Precision(19, 0); }
186
+ export function Long(): PropType<NumberType> { return Precision(19, 0); }
181
187
 
182
188
  /**
183
189
  * Mark a number as a currency
184
190
  * @augments `@travetto/schema:Input`
185
191
  * @kind decorator
186
192
  */
187
- export function Currency(): PropType<number> { return Precision(13, 2); }
193
+ export function Currency(): PropType<NumberType> { return Precision(13, 2); }
188
194
 
189
195
  /**
190
196
  * Specifier for the input
@@ -1,31 +1,35 @@
1
1
  import { DataUtil } from '../data.ts';
2
+ import { SchemaTypeUtil } from '../type-config.ts';
2
3
 
3
4
  const InvalidSymbol = Symbol();
4
5
 
5
6
  /**
6
- * Point Contract
7
+ * Convert to tuple of two numbers
7
8
  */
8
- export class PointContract {
9
-
10
- /**
11
- * Validate we have an actual point
12
- */
13
- static validateSchema(input: unknown): 'type' | undefined {
14
- const bound = this.bindSchema(input);
15
- return bound !== InvalidSymbol && bound && !isNaN(bound[0]) && !isNaN(bound[1]) ? undefined : 'type';
9
+ function bindPoint(input: unknown): [number, number] | typeof InvalidSymbol | undefined {
10
+ if (Array.isArray(input) && input.length === 2) {
11
+ const [a, b] = input.map(value => DataUtil.coerceType(value, Number, false));
12
+ return [a, b];
13
+ } else {
14
+ return InvalidSymbol;
16
15
  }
16
+ }
17
17
 
18
- /**
19
- * Convert to tuple of two numbers
20
- */
21
- static bindSchema(input: unknown): [number, number] | typeof InvalidSymbol | undefined {
22
- if (Array.isArray(input) && input.length === 2) {
23
- const [a, b] = input.map(value => DataUtil.coerceType(value, Number, false));
24
- return [a, b];
25
- } else {
26
- return InvalidSymbol;
27
- }
28
- }
18
+ /**
19
+ * Validate we have an actual point
20
+ */
21
+ function validatePoint(input: unknown): 'type' | undefined {
22
+ const bound = bindPoint(input);
23
+ return bound !== InvalidSymbol && bound && !isNaN(bound[0]) && !isNaN(bound[1]) ? undefined : 'type';
29
24
  }
30
25
 
31
- Object.defineProperty(PointContract, 'name', { value: 'Point' });
26
+ /**
27
+ * Point Contract
28
+ */
29
+ export class PointContract { }
30
+
31
+ SchemaTypeUtil.setSchemaTypeConfig(PointContract, {
32
+ validate: validatePoint,
33
+ bind: bindPoint,
34
+ });
35
+ Object.defineProperty(PointContract, 'name', { value: 'Point' });
@@ -1,10 +1,10 @@
1
1
  import type { RegistryAdapter } from '@travetto/registry';
2
- import { AppError, castKey, castTo, type Class, describeFunction, safeAssign } from '@travetto/runtime';
2
+ import { RuntimeError, BinaryUtil, castKey, castTo, type Class, describeFunction, safeAssign } from '@travetto/runtime';
3
3
 
4
4
  import {
5
5
  type SchemaClassConfig, type SchemaMethodConfig, type SchemaFieldConfig,
6
6
  type SchemaParameterConfig, type SchemaInputConfig, type SchemaFieldMap, type SchemaCoreConfig,
7
- CONSTRUCTOR_PROPERTY
7
+ type SchemaBasicType, CONSTRUCTOR_PROPERTY
8
8
  } from './types.ts';
9
9
 
10
10
  export type SchemaDiscriminatedInfo = Required<Pick<SchemaClassConfig, 'discriminatedType' | 'discriminatedField' | 'discriminatedBase'>>;
@@ -32,6 +32,12 @@ function combineCore<T extends SchemaCoreConfig>(base: T, config: Partial<T>): T
32
32
  });
33
33
  }
34
34
 
35
+ function ensureBinary<T extends SchemaBasicType>(config?: T): void {
36
+ if (config?.type) {
37
+ config.binary = BinaryUtil.isBinaryTypeReference(config.type);
38
+ }
39
+ }
40
+
35
41
  function combineInputs<T extends SchemaInputConfig>(base: T, configs: Partial<T>[]): T {
36
42
  for (const config of configs) {
37
43
  if (config) {
@@ -48,6 +54,7 @@ function combineInputs<T extends SchemaInputConfig>(base: T, configs: Partial<T>
48
54
  });
49
55
  }
50
56
  combineCore(base, config);
57
+ ensureBinary(base);
51
58
  }
52
59
  return base;
53
60
  }
@@ -65,6 +72,7 @@ function combineMethods<T extends SchemaMethodConfig>(base: T, configs: Partial<
65
72
  safeAssign(base.parameters[param.index], param);
66
73
  }
67
74
  }
75
+ ensureBinary(config.returnType);
68
76
  }
69
77
  return base;
70
78
  }
@@ -290,7 +298,7 @@ export class SchemaRegistryAdapter implements RegistryAdapter<SchemaClassConfig>
290
298
  getMethod(method: string): SchemaMethodConfig {
291
299
  const methodConfig = this.#config.methods[method];
292
300
  if (!methodConfig) {
293
- throw new AppError(`Unknown method ${String(method)} on class ${this.#cls.Ⲑid}`);
301
+ throw new RuntimeError(`Unknown method ${String(method)} on class ${this.#cls.Ⲑid}`);
294
302
  }
295
303
  return methodConfig;
296
304
  }
@@ -304,7 +312,7 @@ export class SchemaRegistryAdapter implements RegistryAdapter<SchemaClassConfig>
304
312
  return this.#config.fields;
305
313
  }
306
314
  if (!this.#views.has(view)) {
307
- throw new AppError(`Unknown view ${view} for class ${this.#cls.Ⲑid}`);
315
+ throw new RuntimeError(`Unknown view ${view} for class ${this.#cls.Ⲑid}`);
308
316
  }
309
317
  return this.#views.get(view)!;
310
318
  }
@@ -313,14 +321,13 @@ export class SchemaRegistryAdapter implements RegistryAdapter<SchemaClassConfig>
313
321
  * Provides the prototype-derived descriptor for a property
314
322
  */
315
323
  getAccessorDescriptor(field: string): PropertyDescriptor {
316
- if (!this.#accessorDescriptors.has(field)) {
324
+ return this.#accessorDescriptors.getOrInsertComputed(field, () => {
317
325
  let proto = this.#cls.prototype;
318
326
  while (proto && !Object.hasOwn(proto, field)) {
319
327
  proto = proto.prototype;
320
328
  }
321
- this.#accessorDescriptors.set(field, Object.getOwnPropertyDescriptor(proto, field)!);
322
- }
323
- return this.#accessorDescriptors.get(field)!;
329
+ return Object.getOwnPropertyDescriptor(proto, field)!;
330
+ });
324
331
  }
325
332
 
326
333
  /**
@@ -1,5 +1,5 @@
1
1
  import { type RegistrationMethods, type RegistryIndex, RegistryIndexStore, Registry } from '@travetto/registry';
2
- import { AppError, castKey, castTo, type Class, classConstruct, getParentClass } from '@travetto/runtime';
2
+ import { RuntimeError, castKey, castTo, type Class, classConstruct, getParentClass } from '@travetto/runtime';
3
3
 
4
4
  import type { SchemaFieldConfig, SchemaClassConfig } from './types.ts';
5
5
  import { type SchemaDiscriminatedInfo, SchemaRegistryAdapter } from './registry-adapter.ts';
@@ -75,10 +75,7 @@ export class SchemaRegistryIndex implements RegistryIndex {
75
75
  return;
76
76
  }
77
77
  const base = this.getBaseClass(cls);
78
- if (!this.#byDiscriminatedTypes.has(base)) {
79
- this.#byDiscriminatedTypes.set(base, new Map());
80
- }
81
- this.#byDiscriminatedTypes.get(base)!.set(config.discriminatedType, cls);
78
+ this.#byDiscriminatedTypes.getOrInsert(base, new Map()).set(config.discriminatedType, cls);
82
79
  }
83
80
 
84
81
  beforeChangeSetComplete(): void {
@@ -97,7 +94,7 @@ export class SchemaRegistryIndex implements RegistryIndex {
97
94
  * Find base schema class for a given class
98
95
  */
99
96
  getBaseClass(cls: Class): Class {
100
- if (!this.#baseSchema.has(cls)) {
97
+ return this.#baseSchema.getOrInsertComputed(cls, () => {
101
98
  let config = this.getClassConfig(cls);
102
99
  let parent: Class | undefined = cls;
103
100
  while (parent && config.discriminatedType && !config.discriminatedBase) {
@@ -106,9 +103,8 @@ export class SchemaRegistryIndex implements RegistryIndex {
106
103
  config = this.store.getOptional(parent)?.get() ?? config;
107
104
  }
108
105
  }
109
- this.#baseSchema.set(cls, config.class);
110
- }
111
- return this.#baseSchema.get(cls)!;
106
+ return config.class;
107
+ });
112
108
  }
113
109
 
114
110
  /**
@@ -125,14 +121,14 @@ export class SchemaRegistryIndex implements RegistryIndex {
125
121
  const map = this.#byDiscriminatedTypes.get(base);
126
122
  const type = castTo<string>(item[castKey<T>(discriminatedField)]) ?? discriminatedType;
127
123
  if (!type) {
128
- throw new AppError(`Unable to resolve discriminated type for class ${base.name} without a type`);
124
+ throw new RuntimeError(`Unable to resolve discriminated type for class ${base.name} without a type`);
129
125
  }
130
126
  if (!map?.has(type)) {
131
- throw new AppError(`Unable to resolve discriminated type '${type}' for class ${base.name}`);
127
+ throw new RuntimeError(`Unable to resolve discriminated type '${type}' for class ${base.name}`);
132
128
  }
133
129
  const requested = map.get(type)!;
134
130
  if (!(classConstruct(requested) instanceof targetClass)) {
135
- throw new AppError(`Resolved discriminated type '${type}' for class ${base.name} is not an instance of requested type ${targetClass.name}`);
131
+ throw new RuntimeError(`Resolved discriminated type '${type}' for class ${base.name} is not an instance of requested type ${targetClass.name}`);
136
132
  }
137
133
  return requested;
138
134
  }
@@ -1,4 +1,4 @@
1
- import type { Any, Class, Primitive } from '@travetto/runtime';
1
+ import type { Any, Class, IntrinsicType, NumericLikeIntrinsic, Primitive } from '@travetto/runtime';
2
2
 
3
3
  import type { MethodValidatorFn, ValidatorFn } from '../validate/types.ts';
4
4
 
@@ -19,13 +19,14 @@ export type SchemaBasicType = {
19
19
  * Is the type an array
20
20
  */
21
21
  array?: boolean;
22
+ /**
23
+ * Is the type a binary type
24
+ */
25
+ binary?: boolean;
22
26
  /**
23
27
  * The class tied to the type
24
28
  */
25
- type: Class & {
26
- bindSchema?(input: unknown): undefined | unknown;
27
- validateSchema?(input: unknown): string | undefined;
28
- };
29
+ type: Class;
29
30
  /**
30
31
  * Is the field a foreign type
31
32
  */
@@ -167,11 +168,11 @@ export interface SchemaInputConfig extends SchemaCoreConfig, SchemaBasicType {
167
168
  /**
168
169
  * Minimum value configuration
169
170
  */
170
- min?: { limit: number | Date, message?: string };
171
+ min?: { limit: NumericLikeIntrinsic, message?: string };
171
172
  /**
172
173
  * Maximum value configuration
173
174
  */
174
- max?: { limit: number | Date, message?: string };
175
+ max?: { limit: NumericLikeIntrinsic, message?: string };
175
176
  /**
176
177
  * Minimum length configuration
177
178
  */
@@ -183,11 +184,11 @@ export interface SchemaInputConfig extends SchemaCoreConfig, SchemaBasicType {
183
184
  /**
184
185
  * Enumerated values
185
186
  */
186
- enum?: { values: (string | number | boolean)[], message: string };
187
+ enum?: { values: Primitive[], message: string };
187
188
  /**
188
189
  * Default value
189
190
  */
190
- default?: Primitive | [];
191
+ default?: IntrinsicType | [];
191
192
  }
192
193
 
193
194
  /**
@@ -0,0 +1,54 @@
1
+ import { isUint8Array, isUint16Array, isUint32Array, isArrayBuffer } from 'node:util/types';
2
+ import { Readable } from 'node:stream';
3
+
4
+ import { type Class, type BinaryArray, type BinaryType, type BinaryStream, BinaryUtil, toConcrete } from '@travetto/runtime';
5
+
6
+ type SchemaTypeConfig = {
7
+ /**
8
+ * Controls how inbound data is typed
9
+ */
10
+ bind?(input: unknown): undefined | unknown;
11
+ /**
12
+ * Controls how provided data is validated
13
+ */
14
+ validate?(input: unknown): string | undefined;
15
+ };
16
+
17
+ /**
18
+ * Utility for managing schema type configuration
19
+ */
20
+ export class SchemaTypeUtil {
21
+ static cache = new Map<Function, SchemaTypeConfig>();
22
+
23
+ static {
24
+ // Primitive Types
25
+ this.register(Date, value => value instanceof Date && !Number.isNaN(value.getTime()));
26
+ // Binary Types
27
+ this.register(Buffer, Buffer.isBuffer);
28
+ this.register(Uint8Array, isUint8Array);
29
+ this.register(Uint16Array, isUint16Array);
30
+ this.register(Uint32Array, isUint32Array);
31
+ this.register(ArrayBuffer, isArrayBuffer);
32
+ this.register(ReadableStream, value => value instanceof ReadableStream);
33
+ this.register(Readable, value => value instanceof Readable);
34
+ this.register(toConcrete<BinaryType>(), BinaryUtil.isBinaryType);
35
+ this.register(toConcrete<BinaryArray>(), BinaryUtil.isBinaryArray);
36
+ this.register(toConcrete<BinaryStream>(), BinaryUtil.isBinaryStream);
37
+ this.register(Blob, value => value instanceof Blob);
38
+ this.register(File, value => value instanceof File);
39
+ }
40
+
41
+ static register(type: Class | Function, fn: (value: unknown) => boolean): void {
42
+ SchemaTypeUtil.setSchemaTypeConfig(type, {
43
+ validate: (item: unknown) => fn(item) ? undefined : 'type'
44
+ });
45
+ }
46
+
47
+ static getSchemaTypeConfig(type: Function): SchemaTypeConfig | undefined {
48
+ return this.cache.get(type);
49
+ }
50
+
51
+ static setSchemaTypeConfig(type: Function, config: SchemaTypeConfig): void {
52
+ this.cache.set(type, config);
53
+ }
54
+ }
@@ -1,4 +1,4 @@
1
- import { type Class, AppError } from '@travetto/runtime';
1
+ import { type Class, RuntimeError } from '@travetto/runtime';
2
2
  import type { ValidationError } from './types.ts';
3
3
 
4
4
  /**
@@ -6,7 +6,7 @@ import type { ValidationError } from './types.ts';
6
6
  *
7
7
  * Hold all the validation errors for a given schema validation
8
8
  */
9
- export class ValidationResultError extends AppError<{ errors: ValidationError[] }> {
9
+ export class ValidationResultError extends RuntimeError<{ errors: ValidationError[] }> {
10
10
  constructor(errors: ValidationError[]) {
11
11
  super('Validation errors have occurred', { category: 'data', details: { errors } });
12
12
  }
@@ -16,7 +16,7 @@ export class ValidationResultError extends AppError<{ errors: ValidationError[]
16
16
  * Represents when a requested objects's type doesn't match the class being used to request.
17
17
  * Primarily applies to polymorphic types
18
18
  */
19
- export class TypeMismatchError extends AppError {
19
+ export class TypeMismatchError extends RuntimeError {
20
20
  constructor(cls: Class | string, type: string) {
21
21
  super(`Expected ${typeof cls === 'string' ? cls : cls.name} but found ${type}`, { category: 'data' });
22
22
  }
@@ -1,3 +1,5 @@
1
+ import type { NumericLikeIntrinsic } from '@travetto/runtime';
2
+
1
3
  export type ValidationKindCore = 'required' | 'match' | 'min' | 'max' | 'minlength' | 'maxlength' | 'enum' | 'type';
2
4
  export type ValidationKind = ValidationKindCore | string;
3
5
 
@@ -29,7 +31,7 @@ export interface ValidationError {
29
31
  /**
30
32
  * Number to compare against
31
33
  */
32
- limit?: number | Date;
34
+ limit?: NumericLikeIntrinsic;
33
35
  /**
34
36
  * The type of the field
35
37
  */
@@ -67,7 +69,7 @@ export interface ValidationResult {
67
69
  /**
68
70
  * Number to compare against
69
71
  */
70
- limit?: number | Date;
72
+ limit?: NumericLikeIntrinsic;
71
73
  }
72
74
 
73
75
  type OrPromise<T> = T | Promise<T>;
@@ -7,6 +7,11 @@ import { isValidationError, TypeMismatchError, ValidationResultError } from './e
7
7
  import { DataUtil } from '../data.ts';
8
8
  import { CommonRegexToName } from './regex.ts';
9
9
  import { SchemaRegistryIndex } from '../service/registry-index.ts';
10
+ import { SchemaTypeUtil } from '../type-config.ts';
11
+ import { UnknownType } from '../types.ts';
12
+
13
+ const PrimitiveTypes = new Set<Function>([String, Number, BigInt, Boolean]);
14
+ type NumericComparable = number | bigint | Date;
10
15
 
11
16
  /**
12
17
  * Get the schema config for Class/Schema config, including support for polymorphism
@@ -22,8 +27,12 @@ function isClassInstance<T>(value: unknown): value is ClassInstance<T> {
22
27
  return !DataUtil.isPlainObject(value) && value !== null && typeof value === 'object' && !!value.constructor;
23
28
  }
24
29
 
25
- function isRangeValue(value: unknown): value is number | string | Date {
26
- return typeof value === 'string' || typeof value === 'number' || value instanceof Date;
30
+ function isRangeValue(value: unknown): value is NumericComparable {
31
+ return typeof value === 'number' || typeof value === 'bigint' || value instanceof Date;
32
+ }
33
+
34
+ function isLengthValue(value: unknown): value is { length: number } {
35
+ return (typeof value === 'string' || (typeof value === 'object' && !!value && 'length' in value && typeof value.length === 'number'));
27
36
  }
28
37
 
29
38
  /**
@@ -72,7 +81,7 @@ export class SchemaValidator {
72
81
  const { type, array } = input;
73
82
  const complex = SchemaRegistryIndex.has(type);
74
83
 
75
- if (type === Object) {
84
+ if (type === Object || type === UnknownType) {
76
85
  return [];
77
86
  } else if (array) {
78
87
  if (!Array.isArray(value)) {
@@ -105,13 +114,10 @@ export class SchemaValidator {
105
114
  * @param key The bounds to check
106
115
  * @param value The value to validate
107
116
  */
108
- static #validateRange(input: SchemaInputConfig, key: 'min' | 'max', value: string | number | Date): boolean {
117
+ static #validateRange(input: SchemaInputConfig, key: 'min' | 'max', value: NumericComparable): boolean {
109
118
  const config = input[key]!;
110
- const parsed = (typeof value === 'string') ?
111
- (input.type === Date ? Date.parse(value) : parseInt(value, 10)) :
112
- (value instanceof Date ? value.getTime() : value);
113
-
114
- const boundary = (typeof config.limit === 'number' ? config.limit : config.limit.getTime());
119
+ const parsed = (value instanceof Date ? value.getTime() : value);
120
+ const boundary = (config.limit instanceof Date) ? config.limit.getTime() : config.limit;
115
121
  return key === 'min' ? parsed < boundary : parsed > boundary;
116
122
  }
117
123
 
@@ -122,37 +128,37 @@ export class SchemaValidator {
122
128
  * @param value The actual value
123
129
  */
124
130
  static #validateInput(input: SchemaInputConfig, value: unknown): ValidationResult[] {
125
- const criteria: ([string, SchemaInputConfig[ValidationKindCore]] | [string])[] = [];
126
-
127
- if (
128
- (input.type === String && (typeof value !== 'string')) ||
129
- (input.type === Number && ((typeof value !== 'number') || Number.isNaN(value))) ||
130
- (input.type === Date && (!(value instanceof Date) || Number.isNaN(value.getTime()))) ||
131
- (input.type === Boolean && typeof value !== 'boolean')
132
- ) {
133
- criteria.push(['type']);
134
- return [{ kind: 'type', type: input.type.name.toLowerCase() }];
135
- }
131
+ const criteria: [string, SchemaInputConfig[ValidationKindCore]][] = [];
132
+ const config = SchemaTypeUtil.getSchemaTypeConfig(input.type);
136
133
 
137
- if (input.type?.validateSchema) {
138
- const kind = input.type.validateSchema(value);
134
+ if (config?.validate) {
135
+ const kind = config.validate(value);
139
136
  switch (kind) {
140
137
  case undefined: break;
141
138
  case 'type': return [{ kind, type: input.type.name }];
142
- default:
143
- criteria.push([kind]);
139
+ default: return [{ kind, value }];
140
+ }
141
+ } else if (PrimitiveTypes.has(input.type)) {
142
+ if (typeof value !== input.type.name.toLowerCase()) {
143
+ return [{ kind: 'type', type: input.type.name.toLowerCase() }];
144
+ } else if (Number.isNaN(value)) {
145
+ return [{ kind: 'type', type: 'number' }];
146
+ }
147
+ } else if (SchemaRegistryIndex.has(input.type)) {
148
+ if (!(value instanceof input.type)) { // If not an instance of the type
149
+ return [{ kind: 'type', type: input.type.name }];
144
150
  }
145
151
  }
146
152
 
147
- if (input.match && !input.match.regex.test(`${value}`)) {
153
+ if (input.match && (typeof value !== 'string' || !input.match.regex.test(value))) {
148
154
  criteria.push(['match', input.match]);
149
155
  }
150
156
 
151
- if (input.minlength && `${value}`.length < input.minlength.limit) {
157
+ if (input.minlength && (!isLengthValue(value) || value.length < input.minlength.limit)) {
152
158
  criteria.push(['minlength', input.minlength]);
153
159
  }
154
160
 
155
- if (input.maxlength && `${value}`.length > input.maxlength.limit) {
161
+ if (input.maxlength && (!isLengthValue(value) || value.length > input.maxlength.limit)) {
156
162
  criteria.push(['maxlength', input.maxlength]);
157
163
  }
158
164
 
@@ -168,12 +174,7 @@ export class SchemaValidator {
168
174
  criteria.push(['max', input.max]);
169
175
  }
170
176
 
171
- const errors: ValidationResult[] = [];
172
- for (const [key, block] of criteria) {
173
- errors.push({ ...block, kind: key, value });
174
- }
175
-
176
- return errors;
177
+ return criteria.map(([key, block]) => ({ ...block, kind: key, value }));
177
178
  }
178
179
 
179
180
  /**
@@ -215,7 +215,7 @@ class ${uniqueId} extends ${type.mappedClassName} {
215
215
  }
216
216
 
217
217
  const resolved = this.toConcreteType(state, typeExpr, node, config?.root ?? node);
218
- const type = typeExpr.key === 'foreign' ? state.getConcreteType(node) :
218
+ const type = typeExpr.key === 'foreign' ? state.getConcreteType(node, state.factory.createIdentifier('Object')) :
219
219
  ts.isArrayLiteralExpression(resolved) ? resolved.elements[0] : resolved;
220
220
 
221
221
  params.unshift(LiteralUtil.fromLiteral(state.factory, {