@atscript/typescript 0.1.19 → 0.1.21
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/dist/cli.cjs +17 -2
- package/dist/index.cjs +17 -2
- package/dist/index.d.ts +6 -0
- package/dist/index.mjs +17 -2
- package/dist/utils.cjs +96 -8
- package/dist/utils.d.ts +45 -2
- package/dist/utils.mjs +94 -8
- package/package.json +9 -5
- package/scripts/setup-skills.js +78 -0
- package/skills/atscript-typescript/.gitkeep +0 -0
- package/skills/atscript-typescript/SKILL.md +44 -0
- package/skills/atscript-typescript/annotations.md +240 -0
- package/skills/atscript-typescript/codegen.md +126 -0
- package/skills/atscript-typescript/core.md +164 -0
- package/skills/atscript-typescript/runtime.md +276 -0
- package/skills/atscript-typescript/syntax.md +252 -0
- package/skills/atscript-typescript/utilities.md +329 -0
- package/skills/atscript-typescript/validation.md +293 -0
|
@@ -0,0 +1,329 @@
|
|
|
1
|
+
# Utility Functions — @atscript/typescript
|
|
2
|
+
|
|
3
|
+
> All publicly exported utility functions: serialization, flattening, JSON Schema, data creation, and type traversal.
|
|
4
|
+
|
|
5
|
+
## Exports Overview
|
|
6
|
+
|
|
7
|
+
All utilities are exported from `@atscript/typescript/utils`:
|
|
8
|
+
|
|
9
|
+
```ts
|
|
10
|
+
import {
|
|
11
|
+
// Type construction
|
|
12
|
+
defineAnnotatedType, annotate,
|
|
13
|
+
// Type checking
|
|
14
|
+
isAnnotatedType, isAnnotatedTypeOfPrimitive, isPhantomType,
|
|
15
|
+
// Type traversal
|
|
16
|
+
forAnnotatedType,
|
|
17
|
+
// Validation
|
|
18
|
+
Validator, ValidatorError,
|
|
19
|
+
// JSON Schema
|
|
20
|
+
buildJsonSchema, fromJsonSchema,
|
|
21
|
+
// Serialization
|
|
22
|
+
serializeAnnotatedType, deserializeAnnotatedType, SERIALIZE_VERSION,
|
|
23
|
+
// Flattening
|
|
24
|
+
flattenAnnotatedType,
|
|
25
|
+
// Data creation
|
|
26
|
+
createDataFromAnnotatedType,
|
|
27
|
+
// Feature gating (used by generated code)
|
|
28
|
+
throwFeatureDisabled,
|
|
29
|
+
} from '@atscript/typescript/utils'
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
### `throwFeatureDisabled(feature, option, annotation)`
|
|
33
|
+
|
|
34
|
+
Throws a runtime error indicating a feature is disabled. Used by generated `.js` files to avoid duplicating the error message string across all classes. Called as `$d("JSON Schema", "jsonSchema", "emit.jsonSchema")` in generated code when `jsonSchema: false`.
|
|
35
|
+
|
|
36
|
+
## `forAnnotatedType(def, handlers)` — Type-Safe Dispatch
|
|
37
|
+
|
|
38
|
+
Dispatches over `TAtscriptAnnotatedType` by its `type.kind`, providing type-narrowed handlers:
|
|
39
|
+
|
|
40
|
+
```ts
|
|
41
|
+
import { forAnnotatedType } from '@atscript/typescript/utils'
|
|
42
|
+
|
|
43
|
+
const description = forAnnotatedType(someType, {
|
|
44
|
+
final(d) { return `${d.type.designType}` },
|
|
45
|
+
object(d) { return `object(${d.type.props.size} props)` },
|
|
46
|
+
array(d) { return `array` },
|
|
47
|
+
union(d) { return `union(${d.type.items.length})` },
|
|
48
|
+
intersection(d) { return `intersection(${d.type.items.length})` },
|
|
49
|
+
tuple(d) { return `[${d.type.items.length}]` },
|
|
50
|
+
phantom(d) { return `phantom` }, // optional — without it, phantoms go to final
|
|
51
|
+
})
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
All handlers except `phantom` are required. Each handler receives the type with its `type` field narrowed to the specific kind.
|
|
55
|
+
|
|
56
|
+
## `buildJsonSchema(type)` — Annotated Type → JSON Schema
|
|
57
|
+
|
|
58
|
+
Converts an annotated type into a standard JSON Schema object, translating validation metadata:
|
|
59
|
+
|
|
60
|
+
```ts
|
|
61
|
+
import { buildJsonSchema } from '@atscript/typescript/utils'
|
|
62
|
+
import { User } from './models/user.as'
|
|
63
|
+
|
|
64
|
+
const schema = buildJsonSchema(User)
|
|
65
|
+
// {
|
|
66
|
+
// type: 'object',
|
|
67
|
+
// properties: {
|
|
68
|
+
// name: { type: 'string', minLength: 2, maxLength: 100 },
|
|
69
|
+
// age: { type: 'integer', minimum: 0, maximum: 150 },
|
|
70
|
+
// email: { type: 'string', pattern: '...' },
|
|
71
|
+
// },
|
|
72
|
+
// required: ['name', 'age']
|
|
73
|
+
// }
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
### Metadata → JSON Schema Mapping
|
|
77
|
+
|
|
78
|
+
| Annotation | JSON Schema |
|
|
79
|
+
|-----------|-------------|
|
|
80
|
+
| `@expect.minLength` on string | `minLength` |
|
|
81
|
+
| `@expect.maxLength` on string | `maxLength` |
|
|
82
|
+
| `@expect.minLength` on array | `minItems` |
|
|
83
|
+
| `@expect.maxLength` on array | `maxItems` |
|
|
84
|
+
| `@expect.min` | `minimum` |
|
|
85
|
+
| `@expect.max` | `maximum` |
|
|
86
|
+
| `@expect.int` | `type: 'integer'` (instead of `'number'`) |
|
|
87
|
+
| `@expect.pattern` (single) | `pattern` |
|
|
88
|
+
| `@expect.pattern` (multiple) | `allOf: [{ pattern }, ...]` |
|
|
89
|
+
| `@meta.required` on string | `minLength: 1` |
|
|
90
|
+
| optional property | not in `required` array |
|
|
91
|
+
| union | `anyOf` |
|
|
92
|
+
| intersection | `allOf` |
|
|
93
|
+
| tuple | `items` as array |
|
|
94
|
+
| phantom | empty object `{}` (excluded) |
|
|
95
|
+
|
|
96
|
+
## `fromJsonSchema(schema)` — JSON Schema → Annotated Type
|
|
97
|
+
|
|
98
|
+
The inverse of `buildJsonSchema`. Creates a fully functional annotated type from a JSON Schema:
|
|
99
|
+
|
|
100
|
+
```ts
|
|
101
|
+
import { fromJsonSchema } from '@atscript/typescript/utils'
|
|
102
|
+
|
|
103
|
+
const type = fromJsonSchema({
|
|
104
|
+
type: 'object',
|
|
105
|
+
properties: {
|
|
106
|
+
name: { type: 'string', minLength: 1 },
|
|
107
|
+
age: { type: 'integer', minimum: 0 },
|
|
108
|
+
},
|
|
109
|
+
required: ['name', 'age']
|
|
110
|
+
})
|
|
111
|
+
|
|
112
|
+
// The resulting type has a working validator
|
|
113
|
+
type.validator().validate({ name: 'Alice', age: 30 }) // passes
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
Supports: `type`, `properties`, `required`, `items`, `anyOf`, `oneOf`, `allOf`, `enum`, `const`, `minLength`, `maxLength`, `minimum`, `maximum`, `pattern`, `minItems`, `maxItems`.
|
|
117
|
+
|
|
118
|
+
Does **not** support `$ref` — dereference schemas first.
|
|
119
|
+
|
|
120
|
+
## `serializeAnnotatedType(type, options?)` — Serialize to JSON
|
|
121
|
+
|
|
122
|
+
Converts a runtime annotated type into a plain JSON-safe object for storage or transmission:
|
|
123
|
+
|
|
124
|
+
```ts
|
|
125
|
+
import { serializeAnnotatedType } from '@atscript/typescript/utils'
|
|
126
|
+
|
|
127
|
+
const json = serializeAnnotatedType(User)
|
|
128
|
+
// json is a plain object safe for JSON.stringify()
|
|
129
|
+
const str = JSON.stringify(json)
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
### Serialization Options
|
|
133
|
+
|
|
134
|
+
```ts
|
|
135
|
+
serializeAnnotatedType(User, {
|
|
136
|
+
// Strip specific annotation keys
|
|
137
|
+
ignoreAnnotations: ['meta.sensitive', 'mongo.collection'],
|
|
138
|
+
|
|
139
|
+
// Advanced per-annotation transform
|
|
140
|
+
processAnnotation(ctx) {
|
|
141
|
+
// ctx.key — annotation key (e.g. 'meta.label')
|
|
142
|
+
// ctx.value — annotation value
|
|
143
|
+
// ctx.path — property path (e.g. ['address', 'city'])
|
|
144
|
+
// ctx.kind — type kind at this node
|
|
145
|
+
|
|
146
|
+
// Return { key, value } to keep (possibly transformed)
|
|
147
|
+
// Return undefined to strip
|
|
148
|
+
if (ctx.key.startsWith('mongo.')) return undefined
|
|
149
|
+
return { key: ctx.key, value: ctx.value }
|
|
150
|
+
},
|
|
151
|
+
})
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
## `deserializeAnnotatedType(data)` — Restore from JSON
|
|
155
|
+
|
|
156
|
+
Restores a fully functional annotated type from its serialized form:
|
|
157
|
+
|
|
158
|
+
```ts
|
|
159
|
+
import { deserializeAnnotatedType } from '@atscript/typescript/utils'
|
|
160
|
+
|
|
161
|
+
const type = deserializeAnnotatedType(json)
|
|
162
|
+
|
|
163
|
+
// Fully functional — validator works
|
|
164
|
+
type.validator().validate(someData)
|
|
165
|
+
|
|
166
|
+
// Metadata accessible
|
|
167
|
+
type.metadata.get('meta.label')
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
Throws if the serialized version doesn't match `SERIALIZE_VERSION`.
|
|
171
|
+
|
|
172
|
+
### `SERIALIZE_VERSION`
|
|
173
|
+
|
|
174
|
+
Current serialization format version (currently `1`). Used for forward compatibility:
|
|
175
|
+
|
|
176
|
+
```ts
|
|
177
|
+
import { SERIALIZE_VERSION } from '@atscript/typescript/utils'
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
## `flattenAnnotatedType(type, options?)` — Flatten to Dot-Path Map
|
|
181
|
+
|
|
182
|
+
Flattens a nested object type into a `Map<string, TAtscriptAnnotatedType>` keyed by dot-separated paths:
|
|
183
|
+
|
|
184
|
+
```ts
|
|
185
|
+
import { flattenAnnotatedType } from '@atscript/typescript/utils'
|
|
186
|
+
|
|
187
|
+
const flat = flattenAnnotatedType(User)
|
|
188
|
+
// Map {
|
|
189
|
+
// '' → root object type
|
|
190
|
+
// 'name' → string type (with metadata)
|
|
191
|
+
// 'age' → number type
|
|
192
|
+
// 'address' → nested object type
|
|
193
|
+
// 'address.street' → string type
|
|
194
|
+
// 'address.city' → string type
|
|
195
|
+
// }
|
|
196
|
+
|
|
197
|
+
for (const [path, type] of flat) {
|
|
198
|
+
const label = type.metadata.get('meta.label')
|
|
199
|
+
console.log(path || '(root)', label)
|
|
200
|
+
}
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
### Flatten Options
|
|
204
|
+
|
|
205
|
+
```ts
|
|
206
|
+
flattenAnnotatedType(User, {
|
|
207
|
+
// Callback for each field (non-root)
|
|
208
|
+
onField(path, type, metadata) {
|
|
209
|
+
console.log(`Field: ${path}`)
|
|
210
|
+
},
|
|
211
|
+
|
|
212
|
+
// Tag top-level array fields with a metadata key
|
|
213
|
+
topLevelArrayTag: 'mongo.__topLevelArray',
|
|
214
|
+
|
|
215
|
+
// Skip phantom types
|
|
216
|
+
excludePhantomTypes: true,
|
|
217
|
+
})
|
|
218
|
+
```
|
|
219
|
+
|
|
220
|
+
### How Flattening Handles Complex Types
|
|
221
|
+
|
|
222
|
+
- **Objects**: recursed into — each property gets its own path
|
|
223
|
+
- **Arrays**: recursed into — element type's properties share the array's path prefix
|
|
224
|
+
- **Unions/Intersections/Tuples**: recursed into — if the same path appears in multiple branches, they're merged into a synthetic union
|
|
225
|
+
- **Primitives**: added directly at their path
|
|
226
|
+
|
|
227
|
+
## `createDataFromAnnotatedType(type, options?)` — Create Default Data
|
|
228
|
+
|
|
229
|
+
Creates a data object matching the type's shape, using structural defaults or annotation values:
|
|
230
|
+
|
|
231
|
+
```ts
|
|
232
|
+
import { createDataFromAnnotatedType } from '@atscript/typescript/utils'
|
|
233
|
+
|
|
234
|
+
// Empty structural defaults ('', 0, false, [], {})
|
|
235
|
+
const empty = createDataFromAnnotatedType(User)
|
|
236
|
+
// { name: '', age: 0, active: false, address: { street: '', city: '' } }
|
|
237
|
+
|
|
238
|
+
// Use @meta.default annotations
|
|
239
|
+
const defaults = createDataFromAnnotatedType(User, { mode: 'default' })
|
|
240
|
+
|
|
241
|
+
// Use @meta.example annotations
|
|
242
|
+
const example = createDataFromAnnotatedType(User, { mode: 'example' })
|
|
243
|
+
|
|
244
|
+
// Custom resolver function
|
|
245
|
+
const custom = createDataFromAnnotatedType(User, {
|
|
246
|
+
mode: (prop, path) => {
|
|
247
|
+
if (path === 'name') return 'John Doe'
|
|
248
|
+
if (path === 'age') return 25
|
|
249
|
+
return undefined // fall through to structural default
|
|
250
|
+
}
|
|
251
|
+
})
|
|
252
|
+
```
|
|
253
|
+
|
|
254
|
+
### Modes
|
|
255
|
+
|
|
256
|
+
| Mode | Behavior |
|
|
257
|
+
|------|----------|
|
|
258
|
+
| `'empty'` (default) | Structural defaults: `''`, `0`, `false`, `[]`, `{}`. Optional props omitted |
|
|
259
|
+
| `'default'` | Uses `@meta.default` annotations. Optional props only included if annotated |
|
|
260
|
+
| `'example'` | Uses `@meta.example` annotations. Optional props only included if annotated |
|
|
261
|
+
| `function` | Custom resolver per field. Return `undefined` to fall through |
|
|
262
|
+
|
|
263
|
+
### Behavior Notes
|
|
264
|
+
|
|
265
|
+
- **Optional properties** are omitted unless the mode provides a value for them
|
|
266
|
+
- **Complex types** (object, array): if a `@meta.default`/`@meta.example` annotation is set and passes validation, the entire subtree is replaced (no recursion into inner props)
|
|
267
|
+
- **Annotation values**: strings are used as-is for string types; everything else is parsed via `JSON.parse`
|
|
268
|
+
- **Unions/Intersections**: defaults to first item's value
|
|
269
|
+
- **Phantom types**: skipped
|
|
270
|
+
|
|
271
|
+
## `isAnnotatedType(value)` — Type Guard
|
|
272
|
+
|
|
273
|
+
```ts
|
|
274
|
+
import { isAnnotatedType } from '@atscript/typescript/utils'
|
|
275
|
+
|
|
276
|
+
if (isAnnotatedType(value)) {
|
|
277
|
+
value.metadata // safe
|
|
278
|
+
value.type // safe
|
|
279
|
+
}
|
|
280
|
+
```
|
|
281
|
+
|
|
282
|
+
## `isAnnotatedTypeOfPrimitive(type)` — Check if Primitive
|
|
283
|
+
|
|
284
|
+
Returns `true` for final types and for unions/intersections/tuples whose all members are primitives:
|
|
285
|
+
|
|
286
|
+
```ts
|
|
287
|
+
import { isAnnotatedTypeOfPrimitive } from '@atscript/typescript/utils'
|
|
288
|
+
|
|
289
|
+
isAnnotatedTypeOfPrimitive(stringType) // true
|
|
290
|
+
isAnnotatedTypeOfPrimitive(objectType) // false
|
|
291
|
+
isAnnotatedTypeOfPrimitive(unionOfStringAndNumber) // true
|
|
292
|
+
isAnnotatedTypeOfPrimitive(unionOfStringAndObject) // false
|
|
293
|
+
```
|
|
294
|
+
|
|
295
|
+
## `isPhantomType(def)` — Check if Phantom
|
|
296
|
+
|
|
297
|
+
```ts
|
|
298
|
+
import { isPhantomType } from '@atscript/typescript/utils'
|
|
299
|
+
|
|
300
|
+
isPhantomType(someProperty) // true if designType === 'phantom'
|
|
301
|
+
```
|
|
302
|
+
|
|
303
|
+
## Type Exports
|
|
304
|
+
|
|
305
|
+
Key types you may need to import:
|
|
306
|
+
|
|
307
|
+
```ts
|
|
308
|
+
import type {
|
|
309
|
+
TAtscriptAnnotatedType, // core annotated type
|
|
310
|
+
TAtscriptAnnotatedTypeConstructor, // annotated type that's also a class
|
|
311
|
+
TAtscriptTypeDef, // union of all type def shapes
|
|
312
|
+
TAtscriptTypeFinal, // primitive/literal type def
|
|
313
|
+
TAtscriptTypeObject, // object type def
|
|
314
|
+
TAtscriptTypeArray, // array type def
|
|
315
|
+
TAtscriptTypeComplex, // union/intersection/tuple type def
|
|
316
|
+
TMetadataMap, // typed metadata map
|
|
317
|
+
TAnnotatedTypeHandle, // fluent builder handle
|
|
318
|
+
InferDataType, // extract DataType from a type def
|
|
319
|
+
TValidatorOptions, // validator config
|
|
320
|
+
TValidatorPlugin, // plugin function type
|
|
321
|
+
TValidatorPluginContext, // plugin context
|
|
322
|
+
TSerializedAnnotatedType, // serialized type (top-level)
|
|
323
|
+
TSerializeOptions, // serialization options
|
|
324
|
+
TFlattenOptions, // flatten options
|
|
325
|
+
TCreateDataOptions, // createData options
|
|
326
|
+
TValueResolver, // custom resolver for createData
|
|
327
|
+
TJsonSchema, // JSON Schema object
|
|
328
|
+
} from '@atscript/typescript/utils'
|
|
329
|
+
```
|
|
@@ -0,0 +1,293 @@
|
|
|
1
|
+
# Validation — @atscript/typescript
|
|
2
|
+
|
|
3
|
+
> Runtime data validation, type guards, error handling, and custom validator plugins.
|
|
4
|
+
|
|
5
|
+
## Basic Usage
|
|
6
|
+
|
|
7
|
+
Every generated `.as` interface/type has a `.validator()` factory:
|
|
8
|
+
|
|
9
|
+
```ts
|
|
10
|
+
import { User } from './models/user.as'
|
|
11
|
+
|
|
12
|
+
// Create a validator
|
|
13
|
+
const validator = User.validator()
|
|
14
|
+
|
|
15
|
+
// Validate — throws ValidatorError on failure
|
|
16
|
+
validator.validate(data)
|
|
17
|
+
|
|
18
|
+
// Safe mode — returns boolean, no throw
|
|
19
|
+
if (validator.validate(data, true)) {
|
|
20
|
+
// data is narrowed to User (type guard)
|
|
21
|
+
data.name // TypeScript knows this exists
|
|
22
|
+
}
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
## The `Validator` Class
|
|
26
|
+
|
|
27
|
+
```ts
|
|
28
|
+
import { Validator } from '@atscript/typescript/utils'
|
|
29
|
+
|
|
30
|
+
// Create from any annotated type
|
|
31
|
+
const validator = new Validator(someAnnotatedType, {
|
|
32
|
+
// Options (all optional):
|
|
33
|
+
partial: false, // allow missing required props
|
|
34
|
+
unknownProps: 'error', // 'error' | 'strip' | 'ignore'
|
|
35
|
+
errorLimit: 10, // max errors before stopping
|
|
36
|
+
plugins: [], // custom validator plugins
|
|
37
|
+
skipList: new Set(), // property paths to skip
|
|
38
|
+
})
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
### `validate(value, safe?, context?)`
|
|
42
|
+
|
|
43
|
+
```ts
|
|
44
|
+
// Throwing mode (default) — throws ValidatorError on failure
|
|
45
|
+
validator.validate(data)
|
|
46
|
+
|
|
47
|
+
// Safe mode — returns false instead of throwing
|
|
48
|
+
const isValid = validator.validate(data, true)
|
|
49
|
+
|
|
50
|
+
// With external context — passed to plugins
|
|
51
|
+
validator.validate(data, true, { userId: '123' })
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
The `validate` method is a **TypeScript type guard** — when it returns `true`, the value is narrowed to the interface's data type.
|
|
55
|
+
|
|
56
|
+
## Validator Options
|
|
57
|
+
|
|
58
|
+
### `partial` — Allow Missing Properties
|
|
59
|
+
|
|
60
|
+
```ts
|
|
61
|
+
// Top-level properties only
|
|
62
|
+
User.validator({ partial: true }).validate(data, true)
|
|
63
|
+
|
|
64
|
+
// All levels (deep partial)
|
|
65
|
+
User.validator({ partial: 'deep' }).validate(data, true)
|
|
66
|
+
|
|
67
|
+
// Custom function — decide per object type
|
|
68
|
+
User.validator({
|
|
69
|
+
partial: (objectType, path) => {
|
|
70
|
+
return path === '' // only root object is partial
|
|
71
|
+
}
|
|
72
|
+
}).validate(data, true)
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
### `unknownProps` — Handle Extra Properties
|
|
76
|
+
|
|
77
|
+
```ts
|
|
78
|
+
// Error on unknown properties (default)
|
|
79
|
+
User.validator({ unknownProps: 'error' }).validate(data, true)
|
|
80
|
+
|
|
81
|
+
// Silently remove unknown properties from the value
|
|
82
|
+
User.validator({ unknownProps: 'strip' }).validate(data, true)
|
|
83
|
+
|
|
84
|
+
// Ignore unknown properties
|
|
85
|
+
User.validator({ unknownProps: 'ignore' }).validate(data, true)
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
**Note**: `'strip'` mutates the input object — it deletes unknown keys.
|
|
89
|
+
|
|
90
|
+
### `skipList` — Skip Specific Paths
|
|
91
|
+
|
|
92
|
+
```ts
|
|
93
|
+
User.validator({
|
|
94
|
+
skipList: new Set(['password', 'address.zip'])
|
|
95
|
+
}).validate(data, true)
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
### `replace` — Substitute Type at Runtime
|
|
99
|
+
|
|
100
|
+
```ts
|
|
101
|
+
User.validator({
|
|
102
|
+
replace: (type, path) => {
|
|
103
|
+
if (path === 'status') return customStatusType
|
|
104
|
+
return type
|
|
105
|
+
}
|
|
106
|
+
}).validate(data, true)
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
## Error Handling
|
|
110
|
+
|
|
111
|
+
### `ValidatorError`
|
|
112
|
+
|
|
113
|
+
When `validate()` throws (non-safe mode), it throws a `ValidatorError`:
|
|
114
|
+
|
|
115
|
+
```ts
|
|
116
|
+
import { ValidatorError } from '@atscript/typescript/utils'
|
|
117
|
+
|
|
118
|
+
try {
|
|
119
|
+
validator.validate(data)
|
|
120
|
+
} catch (e) {
|
|
121
|
+
if (e instanceof ValidatorError) {
|
|
122
|
+
// e.message — first error message (with path prefix)
|
|
123
|
+
// e.errors — full structured error array
|
|
124
|
+
console.log(e.errors)
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
### Error Structure
|
|
130
|
+
|
|
131
|
+
```ts
|
|
132
|
+
interface TError {
|
|
133
|
+
path: string // dot-separated path, e.g. "address.city"
|
|
134
|
+
message: string // human-readable error message
|
|
135
|
+
details?: TError[] // nested errors (for unions — shows why each branch failed)
|
|
136
|
+
}
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
### Reading Errors in Safe Mode
|
|
140
|
+
|
|
141
|
+
```ts
|
|
142
|
+
const validator = User.validator()
|
|
143
|
+
if (!validator.validate(data, true)) {
|
|
144
|
+
// Errors are on the validator instance
|
|
145
|
+
for (const error of validator.errors) {
|
|
146
|
+
console.log(`${error.path}: ${error.message}`)
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
### Error Examples
|
|
152
|
+
|
|
153
|
+
```ts
|
|
154
|
+
// Missing required property
|
|
155
|
+
{ path: 'name', message: 'Expected string, got undefined' }
|
|
156
|
+
|
|
157
|
+
// Wrong type
|
|
158
|
+
{ path: 'age', message: 'Expected number, got string' }
|
|
159
|
+
|
|
160
|
+
// Pattern validation
|
|
161
|
+
{ path: 'email', message: 'Value is expected to match pattern "^[^\\s@]+@[^\\s@]+\\.[^\\s@]+$"' }
|
|
162
|
+
|
|
163
|
+
// Custom annotation message
|
|
164
|
+
{ path: 'name', message: 'Name is required' } // from @meta.required "Name is required"
|
|
165
|
+
|
|
166
|
+
// Unknown property
|
|
167
|
+
{ path: 'foo', message: 'Unexpected property' }
|
|
168
|
+
|
|
169
|
+
// Union — shows why each branch failed
|
|
170
|
+
{
|
|
171
|
+
path: 'data',
|
|
172
|
+
message: 'Value does not match any of the allowed types: [string(0)], [number(1)]',
|
|
173
|
+
details: [
|
|
174
|
+
{ path: 'data', message: 'Expected string, got boolean' },
|
|
175
|
+
{ path: 'data', message: 'Expected number, got boolean' },
|
|
176
|
+
]
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// Array validation
|
|
180
|
+
{ path: '2.name', message: 'Expected string, got number' } // 3rd element's name is wrong
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
### Error Limit
|
|
184
|
+
|
|
185
|
+
By default, the validator stops collecting errors after 10. Customize:
|
|
186
|
+
|
|
187
|
+
```ts
|
|
188
|
+
User.validator({ errorLimit: 50 }).validate(data, true)
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
## What Gets Validated
|
|
192
|
+
|
|
193
|
+
| Type Kind | Validation |
|
|
194
|
+
|-----------|-----------|
|
|
195
|
+
| `string` | Type check + `@meta.required` (non-empty) + `@expect.minLength/maxLength` + `@expect.pattern` |
|
|
196
|
+
| `number` | Type check + `@expect.int` + `@expect.min/max` |
|
|
197
|
+
| `boolean` | Type check + `@meta.required` (must be true) |
|
|
198
|
+
| `null` | Exact `null` check |
|
|
199
|
+
| `undefined` | Exact `undefined` check |
|
|
200
|
+
| `any` | Always passes |
|
|
201
|
+
| `never` | Always fails |
|
|
202
|
+
| `phantom` | Always passes (skipped) |
|
|
203
|
+
| `object` | Recursively validates all props, handles unknown props, pattern props |
|
|
204
|
+
| `array` | Type check + `@expect.minLength/maxLength` + recursively validates each element |
|
|
205
|
+
| `union` | At least one branch must pass |
|
|
206
|
+
| `intersection` | All branches must pass |
|
|
207
|
+
| `tuple` | Array length must match + each element validated against its position |
|
|
208
|
+
| `literal` | Exact value match |
|
|
209
|
+
| `optional` | `undefined` is accepted; if value is present, validated against inner type |
|
|
210
|
+
|
|
211
|
+
## Custom Validator Plugins
|
|
212
|
+
|
|
213
|
+
Plugins intercept validation at every node in the type tree. They can accept, reject, or defer to default validation.
|
|
214
|
+
|
|
215
|
+
### Plugin Signature
|
|
216
|
+
|
|
217
|
+
```ts
|
|
218
|
+
type TValidatorPlugin = (
|
|
219
|
+
ctx: TValidatorPluginContext,
|
|
220
|
+
def: TAtscriptAnnotatedType,
|
|
221
|
+
value: any
|
|
222
|
+
) => boolean | undefined
|
|
223
|
+
// ↑ true = accept, false = reject, undefined = fall through to default
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
### Plugin Context
|
|
227
|
+
|
|
228
|
+
```ts
|
|
229
|
+
interface TValidatorPluginContext {
|
|
230
|
+
opts: TValidatorOptions // current validator options
|
|
231
|
+
validateAnnotatedType(def, value) // call default validation for a specific type
|
|
232
|
+
error(message, path?, details?) // report an error
|
|
233
|
+
path: string // current dot-separated path
|
|
234
|
+
context: unknown // external context from validate(data, safe, context)
|
|
235
|
+
}
|
|
236
|
+
```
|
|
237
|
+
|
|
238
|
+
### Plugin Example — Custom Date Validation
|
|
239
|
+
|
|
240
|
+
```ts
|
|
241
|
+
const datePlugin: TValidatorPlugin = (ctx, def, value) => {
|
|
242
|
+
// Only intercept string types tagged as dates
|
|
243
|
+
if (def.type.kind === '' && def.type.tags.has('date')) {
|
|
244
|
+
if (typeof value !== 'string') {
|
|
245
|
+
ctx.error('Expected date string')
|
|
246
|
+
return false
|
|
247
|
+
}
|
|
248
|
+
const parsed = Date.parse(value)
|
|
249
|
+
if (isNaN(parsed)) {
|
|
250
|
+
ctx.error(`Invalid date: "${value}"`)
|
|
251
|
+
return false
|
|
252
|
+
}
|
|
253
|
+
return true
|
|
254
|
+
}
|
|
255
|
+
// Return undefined to fall through to default validation
|
|
256
|
+
return undefined
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
User.validator({ plugins: [datePlugin] }).validate(data, true)
|
|
260
|
+
```
|
|
261
|
+
|
|
262
|
+
### Plugin Return Values
|
|
263
|
+
|
|
264
|
+
| Return | Meaning |
|
|
265
|
+
|--------|---------|
|
|
266
|
+
| `true` | Value is accepted — skip all further validation for this node |
|
|
267
|
+
| `false` | Value is rejected — error should be reported via `ctx.error()` before returning |
|
|
268
|
+
| `undefined` | Plugin doesn't handle this type — fall through to next plugin or default validation |
|
|
269
|
+
|
|
270
|
+
### Plugin Example — Coerce String to Number
|
|
271
|
+
|
|
272
|
+
```ts
|
|
273
|
+
const coercePlugin: TValidatorPlugin = (ctx, def, value) => {
|
|
274
|
+
if (def.type.kind === '' && def.type.designType === 'number' && typeof value === 'string') {
|
|
275
|
+
const num = Number(value)
|
|
276
|
+
if (!isNaN(num)) {
|
|
277
|
+
// Validate the coerced value against the full type (respects @expect.min etc.)
|
|
278
|
+
return ctx.validateAnnotatedType(def, num)
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
return undefined
|
|
282
|
+
}
|
|
283
|
+
```
|
|
284
|
+
|
|
285
|
+
### Multiple Plugins
|
|
286
|
+
|
|
287
|
+
Plugins run in order. The first plugin to return `true` or `false` wins — subsequent plugins and default validation are skipped for that node:
|
|
288
|
+
|
|
289
|
+
```ts
|
|
290
|
+
User.validator({
|
|
291
|
+
plugins: [authPlugin, datePlugin, coercePlugin]
|
|
292
|
+
}).validate(data, true)
|
|
293
|
+
```
|