@atscript/typescript 0.1.19 → 0.1.20

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.
@@ -0,0 +1,323 @@
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
+ } from '@atscript/typescript/utils'
28
+ ```
29
+
30
+ ## `forAnnotatedType(def, handlers)` — Type-Safe Dispatch
31
+
32
+ Dispatches over `TAtscriptAnnotatedType` by its `type.kind`, providing type-narrowed handlers:
33
+
34
+ ```ts
35
+ import { forAnnotatedType } from '@atscript/typescript/utils'
36
+
37
+ const description = forAnnotatedType(someType, {
38
+ final(d) { return `${d.type.designType}` },
39
+ object(d) { return `object(${d.type.props.size} props)` },
40
+ array(d) { return `array` },
41
+ union(d) { return `union(${d.type.items.length})` },
42
+ intersection(d) { return `intersection(${d.type.items.length})` },
43
+ tuple(d) { return `[${d.type.items.length}]` },
44
+ phantom(d) { return `phantom` }, // optional — without it, phantoms go to final
45
+ })
46
+ ```
47
+
48
+ All handlers except `phantom` are required. Each handler receives the type with its `type` field narrowed to the specific kind.
49
+
50
+ ## `buildJsonSchema(type)` — Annotated Type → JSON Schema
51
+
52
+ Converts an annotated type into a standard JSON Schema object, translating validation metadata:
53
+
54
+ ```ts
55
+ import { buildJsonSchema } from '@atscript/typescript/utils'
56
+ import { User } from './models/user.as'
57
+
58
+ const schema = buildJsonSchema(User)
59
+ // {
60
+ // type: 'object',
61
+ // properties: {
62
+ // name: { type: 'string', minLength: 2, maxLength: 100 },
63
+ // age: { type: 'integer', minimum: 0, maximum: 150 },
64
+ // email: { type: 'string', pattern: '...' },
65
+ // },
66
+ // required: ['name', 'age']
67
+ // }
68
+ ```
69
+
70
+ ### Metadata → JSON Schema Mapping
71
+
72
+ | Annotation | JSON Schema |
73
+ |-----------|-------------|
74
+ | `@expect.minLength` on string | `minLength` |
75
+ | `@expect.maxLength` on string | `maxLength` |
76
+ | `@expect.minLength` on array | `minItems` |
77
+ | `@expect.maxLength` on array | `maxItems` |
78
+ | `@expect.min` | `minimum` |
79
+ | `@expect.max` | `maximum` |
80
+ | `@expect.int` | `type: 'integer'` (instead of `'number'`) |
81
+ | `@expect.pattern` (single) | `pattern` |
82
+ | `@expect.pattern` (multiple) | `allOf: [{ pattern }, ...]` |
83
+ | `@meta.required` on string | `minLength: 1` |
84
+ | optional property | not in `required` array |
85
+ | union | `anyOf` |
86
+ | intersection | `allOf` |
87
+ | tuple | `items` as array |
88
+ | phantom | empty object `{}` (excluded) |
89
+
90
+ ## `fromJsonSchema(schema)` — JSON Schema → Annotated Type
91
+
92
+ The inverse of `buildJsonSchema`. Creates a fully functional annotated type from a JSON Schema:
93
+
94
+ ```ts
95
+ import { fromJsonSchema } from '@atscript/typescript/utils'
96
+
97
+ const type = fromJsonSchema({
98
+ type: 'object',
99
+ properties: {
100
+ name: { type: 'string', minLength: 1 },
101
+ age: { type: 'integer', minimum: 0 },
102
+ },
103
+ required: ['name', 'age']
104
+ })
105
+
106
+ // The resulting type has a working validator
107
+ type.validator().validate({ name: 'Alice', age: 30 }) // passes
108
+ ```
109
+
110
+ Supports: `type`, `properties`, `required`, `items`, `anyOf`, `oneOf`, `allOf`, `enum`, `const`, `minLength`, `maxLength`, `minimum`, `maximum`, `pattern`, `minItems`, `maxItems`.
111
+
112
+ Does **not** support `$ref` — dereference schemas first.
113
+
114
+ ## `serializeAnnotatedType(type, options?)` — Serialize to JSON
115
+
116
+ Converts a runtime annotated type into a plain JSON-safe object for storage or transmission:
117
+
118
+ ```ts
119
+ import { serializeAnnotatedType } from '@atscript/typescript/utils'
120
+
121
+ const json = serializeAnnotatedType(User)
122
+ // json is a plain object safe for JSON.stringify()
123
+ const str = JSON.stringify(json)
124
+ ```
125
+
126
+ ### Serialization Options
127
+
128
+ ```ts
129
+ serializeAnnotatedType(User, {
130
+ // Strip specific annotation keys
131
+ ignoreAnnotations: ['meta.sensitive', 'mongo.collection'],
132
+
133
+ // Advanced per-annotation transform
134
+ processAnnotation(ctx) {
135
+ // ctx.key — annotation key (e.g. 'meta.label')
136
+ // ctx.value — annotation value
137
+ // ctx.path — property path (e.g. ['address', 'city'])
138
+ // ctx.kind — type kind at this node
139
+
140
+ // Return { key, value } to keep (possibly transformed)
141
+ // Return undefined to strip
142
+ if (ctx.key.startsWith('mongo.')) return undefined
143
+ return { key: ctx.key, value: ctx.value }
144
+ },
145
+ })
146
+ ```
147
+
148
+ ## `deserializeAnnotatedType(data)` — Restore from JSON
149
+
150
+ Restores a fully functional annotated type from its serialized form:
151
+
152
+ ```ts
153
+ import { deserializeAnnotatedType } from '@atscript/typescript/utils'
154
+
155
+ const type = deserializeAnnotatedType(json)
156
+
157
+ // Fully functional — validator works
158
+ type.validator().validate(someData)
159
+
160
+ // Metadata accessible
161
+ type.metadata.get('meta.label')
162
+ ```
163
+
164
+ Throws if the serialized version doesn't match `SERIALIZE_VERSION`.
165
+
166
+ ### `SERIALIZE_VERSION`
167
+
168
+ Current serialization format version (currently `1`). Used for forward compatibility:
169
+
170
+ ```ts
171
+ import { SERIALIZE_VERSION } from '@atscript/typescript/utils'
172
+ ```
173
+
174
+ ## `flattenAnnotatedType(type, options?)` — Flatten to Dot-Path Map
175
+
176
+ Flattens a nested object type into a `Map<string, TAtscriptAnnotatedType>` keyed by dot-separated paths:
177
+
178
+ ```ts
179
+ import { flattenAnnotatedType } from '@atscript/typescript/utils'
180
+
181
+ const flat = flattenAnnotatedType(User)
182
+ // Map {
183
+ // '' → root object type
184
+ // 'name' → string type (with metadata)
185
+ // 'age' → number type
186
+ // 'address' → nested object type
187
+ // 'address.street' → string type
188
+ // 'address.city' → string type
189
+ // }
190
+
191
+ for (const [path, type] of flat) {
192
+ const label = type.metadata.get('meta.label')
193
+ console.log(path || '(root)', label)
194
+ }
195
+ ```
196
+
197
+ ### Flatten Options
198
+
199
+ ```ts
200
+ flattenAnnotatedType(User, {
201
+ // Callback for each field (non-root)
202
+ onField(path, type, metadata) {
203
+ console.log(`Field: ${path}`)
204
+ },
205
+
206
+ // Tag top-level array fields with a metadata key
207
+ topLevelArrayTag: 'mongo.__topLevelArray',
208
+
209
+ // Skip phantom types
210
+ excludePhantomTypes: true,
211
+ })
212
+ ```
213
+
214
+ ### How Flattening Handles Complex Types
215
+
216
+ - **Objects**: recursed into — each property gets its own path
217
+ - **Arrays**: recursed into — element type's properties share the array's path prefix
218
+ - **Unions/Intersections/Tuples**: recursed into — if the same path appears in multiple branches, they're merged into a synthetic union
219
+ - **Primitives**: added directly at their path
220
+
221
+ ## `createDataFromAnnotatedType(type, options?)` — Create Default Data
222
+
223
+ Creates a data object matching the type's shape, using structural defaults or annotation values:
224
+
225
+ ```ts
226
+ import { createDataFromAnnotatedType } from '@atscript/typescript/utils'
227
+
228
+ // Empty structural defaults ('', 0, false, [], {})
229
+ const empty = createDataFromAnnotatedType(User)
230
+ // { name: '', age: 0, active: false, address: { street: '', city: '' } }
231
+
232
+ // Use @meta.default annotations
233
+ const defaults = createDataFromAnnotatedType(User, { mode: 'default' })
234
+
235
+ // Use @meta.example annotations
236
+ const example = createDataFromAnnotatedType(User, { mode: 'example' })
237
+
238
+ // Custom resolver function
239
+ const custom = createDataFromAnnotatedType(User, {
240
+ mode: (prop, path) => {
241
+ if (path === 'name') return 'John Doe'
242
+ if (path === 'age') return 25
243
+ return undefined // fall through to structural default
244
+ }
245
+ })
246
+ ```
247
+
248
+ ### Modes
249
+
250
+ | Mode | Behavior |
251
+ |------|----------|
252
+ | `'empty'` (default) | Structural defaults: `''`, `0`, `false`, `[]`, `{}`. Optional props omitted |
253
+ | `'default'` | Uses `@meta.default` annotations. Optional props only included if annotated |
254
+ | `'example'` | Uses `@meta.example` annotations. Optional props only included if annotated |
255
+ | `function` | Custom resolver per field. Return `undefined` to fall through |
256
+
257
+ ### Behavior Notes
258
+
259
+ - **Optional properties** are omitted unless the mode provides a value for them
260
+ - **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)
261
+ - **Annotation values**: strings are used as-is for string types; everything else is parsed via `JSON.parse`
262
+ - **Unions/Intersections**: defaults to first item's value
263
+ - **Phantom types**: skipped
264
+
265
+ ## `isAnnotatedType(value)` — Type Guard
266
+
267
+ ```ts
268
+ import { isAnnotatedType } from '@atscript/typescript/utils'
269
+
270
+ if (isAnnotatedType(value)) {
271
+ value.metadata // safe
272
+ value.type // safe
273
+ }
274
+ ```
275
+
276
+ ## `isAnnotatedTypeOfPrimitive(type)` — Check if Primitive
277
+
278
+ Returns `true` for final types and for unions/intersections/tuples whose all members are primitives:
279
+
280
+ ```ts
281
+ import { isAnnotatedTypeOfPrimitive } from '@atscript/typescript/utils'
282
+
283
+ isAnnotatedTypeOfPrimitive(stringType) // true
284
+ isAnnotatedTypeOfPrimitive(objectType) // false
285
+ isAnnotatedTypeOfPrimitive(unionOfStringAndNumber) // true
286
+ isAnnotatedTypeOfPrimitive(unionOfStringAndObject) // false
287
+ ```
288
+
289
+ ## `isPhantomType(def)` — Check if Phantom
290
+
291
+ ```ts
292
+ import { isPhantomType } from '@atscript/typescript/utils'
293
+
294
+ isPhantomType(someProperty) // true if designType === 'phantom'
295
+ ```
296
+
297
+ ## Type Exports
298
+
299
+ Key types you may need to import:
300
+
301
+ ```ts
302
+ import type {
303
+ TAtscriptAnnotatedType, // core annotated type
304
+ TAtscriptAnnotatedTypeConstructor, // annotated type that's also a class
305
+ TAtscriptTypeDef, // union of all type def shapes
306
+ TAtscriptTypeFinal, // primitive/literal type def
307
+ TAtscriptTypeObject, // object type def
308
+ TAtscriptTypeArray, // array type def
309
+ TAtscriptTypeComplex, // union/intersection/tuple type def
310
+ TMetadataMap, // typed metadata map
311
+ TAnnotatedTypeHandle, // fluent builder handle
312
+ InferDataType, // extract DataType from a type def
313
+ TValidatorOptions, // validator config
314
+ TValidatorPlugin, // plugin function type
315
+ TValidatorPluginContext, // plugin context
316
+ TSerializedAnnotatedType, // serialized type (top-level)
317
+ TSerializeOptions, // serialization options
318
+ TFlattenOptions, // flatten options
319
+ TCreateDataOptions, // createData options
320
+ TValueResolver, // custom resolver for createData
321
+ TJsonSchema, // JSON Schema object
322
+ } from '@atscript/typescript/utils'
323
+ ```
@@ -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
+ ```