@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.
@@ -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
+ ```