typespec-typescript-emitter 1.2.0 → 2.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.husky/pre-commit +2 -1
- package/.prettierignore +2 -1
- package/CHANGELOG.md +19 -24
- package/CODE_OF_CONDUCT.md +128 -0
- package/README.md +362 -219
- package/dist/src/emit_routedTypemap.d.ts +4 -0
- package/dist/src/emit_routedTypemap.js +83 -0
- package/dist/src/emit_routedTypemap.js.map +1 -0
- package/dist/src/emit_routes.d.ts +3 -2
- package/dist/src/emit_routes.js +73 -47
- package/dist/src/emit_routes.js.map +1 -1
- package/dist/src/emit_types.d.ts +3 -9
- package/dist/src/emit_types.js +109 -74
- package/dist/src/emit_types.js.map +1 -1
- package/dist/src/emitter.d.ts +2 -6
- package/dist/src/emitter.js +18 -76
- package/dist/src/emitter.js.map +1 -1
- package/dist/src/helpers/appendableString.d.ts +15 -0
- package/dist/src/helpers/appendableString.js +41 -0
- package/dist/src/helpers/appendableString.js.map +1 -0
- package/dist/src/helpers/arrays.d.ts +3 -0
- package/dist/src/helpers/arrays.js +23 -0
- package/dist/src/helpers/arrays.js.map +1 -0
- package/dist/src/{helper_autogenerateWarning.d.ts → helpers/autogenerateWarning.d.ts} +1 -1
- package/dist/src/{helper_autogenerateWarning.js → helpers/autogenerateWarning.js} +1 -2
- package/dist/src/helpers/autogenerateWarning.js.map +1 -0
- package/dist/src/helpers/buildTypeMap.d.ts +12 -0
- package/dist/src/helpers/buildTypeMap.js +44 -0
- package/dist/src/helpers/buildTypeMap.js.map +1 -0
- package/dist/src/helpers/diagnostics.d.ts +4 -0
- package/dist/src/helpers/diagnostics.js +15 -0
- package/dist/src/helpers/diagnostics.js.map +1 -0
- package/dist/src/helpers/getImports.d.ts +2 -0
- package/dist/src/helpers/getImports.js +3 -0
- package/dist/src/helpers/getImports.js.map +1 -0
- package/dist/src/helpers/namespaces.d.ts +4 -0
- package/dist/src/helpers/namespaces.js +14 -0
- package/dist/src/helpers/namespaces.js.map +1 -0
- package/dist/src/helpers/visibilityHelperFile.d.ts +4 -0
- package/dist/src/helpers/visibilityHelperFile.js +57 -0
- package/dist/src/helpers/visibilityHelperFile.js.map +1 -0
- package/dist/src/lib.d.ts +4 -1
- package/dist/src/lib.js +14 -3
- package/dist/src/lib.js.map +1 -1
- package/dist/src/parseOptions.d.ts +8 -0
- package/dist/src/parseOptions.js +26 -0
- package/dist/src/parseOptions.js.map +1 -0
- package/dist/src/resolve/Resolvable.d.ts +32 -0
- package/dist/src/resolve/Resolvable.js +180 -0
- package/dist/src/resolve/Resolvable.js.map +1 -0
- package/dist/src/resolve/Resolvable_helpers.d.ts +42 -0
- package/dist/src/resolve/Resolvable_helpers.js +19 -0
- package/dist/src/resolve/Resolvable_helpers.js.map +1 -0
- package/dist/src/resolve/operationTypemap.d.ts +21 -0
- package/dist/src/resolve/operationTypemap.js +138 -0
- package/dist/src/resolve/operationTypemap.js.map +1 -0
- package/dist/src/resolve/types/Enum.d.ts +21 -0
- package/dist/src/resolve/types/Enum.js +61 -0
- package/dist/src/resolve/types/Enum.js.map +1 -0
- package/dist/src/resolve/types/Model.Indexed.d.ts +12 -0
- package/dist/src/resolve/types/Model.Indexed.js +69 -0
- package/dist/src/resolve/types/Model.Indexed.js.map +1 -0
- package/dist/src/resolve/types/Model.Shaped.d.ts +17 -0
- package/dist/src/resolve/types/Model.Shaped.js +210 -0
- package/dist/src/resolve/types/Model.Shaped.js.map +1 -0
- package/dist/src/resolve/types/Scalar.d.ts +9 -0
- package/dist/src/resolve/types/Scalar.js +109 -0
- package/dist/src/resolve/types/Scalar.js.map +1 -0
- package/dist/src/resolve/types/Simple.d.ts +11 -0
- package/dist/src/resolve/types/Simple.js +49 -0
- package/dist/src/resolve/types/Simple.js.map +1 -0
- package/dist/src/resolve/types/Tuple.d.ts +9 -0
- package/dist/src/resolve/types/Tuple.js +38 -0
- package/dist/src/resolve/types/Tuple.js.map +1 -0
- package/dist/src/resolve/types/Union.d.ts +9 -0
- package/dist/src/resolve/types/Union.js +36 -0
- package/dist/src/resolve/types/Union.js.map +1 -0
- package/eslint.config.js +12 -4
- package/package.json +2 -2
- package/src/emit_routedTypemap.ts +128 -0
- package/src/emit_routes.ts +108 -61
- package/src/emit_types.ts +137 -92
- package/src/emitter.ts +19 -107
- package/src/helpers/appendableString.ts +52 -0
- package/src/helpers/arrays.ts +21 -0
- package/src/{helper_autogenerateWarning.ts → helpers/autogenerateWarning.ts} +0 -1
- package/src/helpers/buildTypeMap.ts +72 -0
- package/src/helpers/diagnostics.ts +19 -0
- package/src/helpers/getImports.ts +9 -0
- package/src/helpers/namespaces.ts +18 -0
- package/src/helpers/visibilityHelperFile.ts +63 -0
- package/src/lib.ts +25 -4
- package/src/parseOptions.ts +26 -0
- package/src/resolve/Resolvable.ts +267 -0
- package/src/resolve/Resolvable_helpers.ts +65 -0
- package/src/resolve/operationTypemap.ts +212 -0
- package/src/resolve/types/Enum.ts +92 -0
- package/src/resolve/types/Model.Indexed.ts +113 -0
- package/src/resolve/types/Model.Shaped.ts +291 -0
- package/src/resolve/types/Scalar.ts +140 -0
- package/src/resolve/types/Simple.ts +88 -0
- package/src/resolve/types/Tuple.ts +56 -0
- package/src/resolve/types/Union.ts +52 -0
- package/test/helpers/integrationTest-novis.tsp +51 -0
- package/test/helpers/integrationTest.tsp +53 -0
- package/test/helpers/largeModel.tsp +40 -0
- package/test/{runner.ts → helpers/runner.ts} +1 -1
- package/test/helpers/ts.ts +11 -0
- package/test/helpers/wrapper.ts +144 -0
- package/test/routes/routes.target.ts +35 -0
- package/test/routes/routes.test.ts +22 -0
- package/test/typeguards/combined.test.ts +78 -0
- package/test/typeguards/enum.test.ts +10 -0
- package/test/typeguards/model.indexed.test.ts +68 -0
- package/test/typeguards/model.shaped.test.ts +38 -0
- package/test/typeguards/scalar.test.ts +62 -0
- package/test/typeguards/simple.test.ts +35 -0
- package/test/typeguards/tuple.test.ts +34 -0
- package/test/typeguards/union.test.ts +29 -0
- package/test/typemap/typemap-novis.target.ts +38 -0
- package/test/typemap/typemap.target.ts +39 -0
- package/test/typemap/typemap.test.ts +48 -0
- package/test/types/combined.test.ts +71 -0
- package/test/types/enum.test.ts +57 -0
- package/test/types/model.indexed.test.ts +46 -0
- package/test/types/model.shaped.test.ts +23 -0
- package/test/types/scalar.test.ts +53 -0
- package/test/types/simple.test.ts +20 -0
- package/test/types/tuple.test.ts +29 -0
- package/test/types/union.test.ts +20 -0
- package/tsconfig.json +1 -0
- package/dist/src/emit_mapped_types.d.ts +0 -2
- package/dist/src/emit_mapped_types.js +0 -124
- package/dist/src/emit_mapped_types.js.map +0 -1
- package/dist/src/emit_types_resolve.d.ts +0 -22
- package/dist/src/emit_types_resolve.js +0 -217
- package/dist/src/emit_types_resolve.js.map +0 -1
- package/dist/src/emit_types_typeguards.d.ts +0 -17
- package/dist/src/emit_types_typeguards.js +0 -121
- package/dist/src/emit_types_typeguards.js.map +0 -1
- package/dist/src/helper_autogenerateWarning.js.map +0 -1
- package/src/emit_mapped_types.ts +0 -155
- package/src/emit_types_resolve.ts +0 -280
- package/src/emit_types_typeguards.ts +0 -178
- package/test/main.test.ts +0 -83
- package/test/out/.gitkeep +0 -0
- package/test/targets/enum.routed-types.ts +0 -59
- package/test/targets/enum.routes.ts +0 -29
- package/test/targets/enum.target.ts +0 -39
- package/test/targets/enum.tsp +0 -36
- package/test/targets/pr8.routed-types.ts +0 -131
- package/test/targets/pr8.routes.ts +0 -30
- package/test/targets/pr8.target.ts +0 -97
- package/test/targets/pr8.tsp +0 -62
- package/test/targets/simple-routes.routed-types.ts +0 -64
- package/test/targets/simple-routes.routes.ts +0 -29
- package/test/targets/simple-routes.target.ts +0 -21
- package/test/targets/simple-routes.tsp +0 -23
- package/test/targets/union.routed-types.ts +0 -59
- package/test/targets/union.routes.ts +0 -23
- package/test/targets/union.target.ts +0 -59
- package/test/targets/union.tsp +0 -38
- package/test/targets/visibility.routed-types.ts +0 -81
- package/test/targets/visibility.routes.ts +0 -38
- package/test/targets/visibility.target.ts +0 -36
- package/test/targets/visibility.tsp +0 -49
package/README.md
CHANGED
|
@@ -1,29 +1,32 @@
|
|
|
1
|
-
# typespec-typescript-emitter
|
|
1
|
+
# typespec-typescript-emitter <!-- omit from toc -->
|
|
2
2
|
|
|
3
|
-
This is a [TypeSpec](https://typespec.io) library aiming to provide TypeScript output to a TypeSpec project.
|
|
3
|
+
This is a [TypeSpec](https://typespec.io) library aiming to provide TypeScript (*TS*) output to a TypeSpec (*TSP*) project.
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
The 'routes'-emitter will only work on HTTP operations. **However**, exporting all models as types is independent of HTTP, and so may also benefit projects with a different usage scenario.
|
|
5
|
+
While this library is tailored to HTTP APIs, it can certainly be useful to other types of projects.
|
|
7
6
|
|
|
8
7
|
It can the following things:
|
|
9
8
|
|
|
10
|
-
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
- optional typeguards, *if* type export is enabled
|
|
15
|
-
- for `TypeSpec.Http`: ts file containing a nested object (by namespace-opname) containing information about every route (eg. url-from-parameters, method, etc.)
|
|
16
|
-
- for `TypeSpec.Http`: "routed typemap" mapping types to their routes (path and verb) (respects Lifecycle visibility)
|
|
9
|
+
- export TypeScript files containing each enum, scalar, model and union present in your TSP files
|
|
10
|
+
- generate narrow typeguards for all emitted types
|
|
11
|
+
- *for HTTP*: export a nested object containing information about every route (eg. url-from-parameters, method, etc.)
|
|
12
|
+
- *for HTTP*: export a "routed typemap", making expected request and response body types accessible using the operation's path
|
|
17
13
|
|
|
18
14
|
## Content <!-- omit from toc -->
|
|
19
15
|
|
|
20
|
-
- [
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
- [Types
|
|
24
|
-
|
|
25
|
-
- [
|
|
26
|
-
|
|
16
|
+
- [Installation](#installation)
|
|
17
|
+
- [Configuration](#configuration)
|
|
18
|
+
- [Emitter: Types](#emitter-types)
|
|
19
|
+
- [Types](#types)
|
|
20
|
+
- [Typeguards](#typeguards)
|
|
21
|
+
- [Lifecycle Visibility](#lifecycle-visibility)
|
|
22
|
+
- [In Types](#in-types)
|
|
23
|
+
- [In Typeguards](#in-typeguards)
|
|
24
|
+
- [Nominal Enums](#nominal-enums)
|
|
25
|
+
- [Emitter: Routes](#emitter-routes)
|
|
26
|
+
- [Emitter: Routed Typemap](#emitter-routed-typemap)
|
|
27
|
+
- [Contributing](#contributing)
|
|
28
|
+
- [Short Overview](#short-overview)
|
|
29
|
+
- [Todo](#todo)
|
|
27
30
|
|
|
28
31
|
## Installation
|
|
29
32
|
|
|
@@ -40,7 +43,9 @@ emit:
|
|
|
40
43
|
- "typespec-typescript-emitter"
|
|
41
44
|
options:
|
|
42
45
|
"typespec-typescript-emitter":
|
|
43
|
-
root-
|
|
46
|
+
root-namespaces:
|
|
47
|
+
- "namespace1"
|
|
48
|
+
- "namespace2"
|
|
44
49
|
out-dir: "{cwd}/path"
|
|
45
50
|
enable-types: true
|
|
46
51
|
enable-typeguards: false
|
|
@@ -52,271 +57,369 @@ options:
|
|
|
52
57
|
|
|
53
58
|
The following options are available:
|
|
54
59
|
|
|
55
|
-
- `root-
|
|
56
|
-
- `out-dir
|
|
60
|
+
- `root-namespaces` **(required)**: array of names of all namespaces in your program you want to emit from. You don't need to specify namespaces nested inside other namespaces, as the ones listed will be traversed recursively.
|
|
61
|
+
- `out-dir` **(required)**: output directory. Must be an absolute path; replacers like `{cwd}` are permitted.
|
|
57
62
|
- `enable-types` (default: true): enables output of TypeScript types.
|
|
58
|
-
- `enable-typeguards` (default: false): enables output of typeguards
|
|
59
|
-
- `enable-routes` (default: false): enables output of the HTTP-routes object.
|
|
60
|
-
- `enable-routed-typemap` (default: false): enables output of an indexable type mapping paths and HTTP verbs to request and response bodies.
|
|
61
|
-
- `string-nominal-enums` (default: false): outputs member names as strings instead of index values for enum members declared without explicit values
|
|
62
|
-
- `serializable-date-types` (default: false): outputs serializable types for typespec's dates types that match OpenApi spec. Types like `offsetDateTime`, `plainDate` and `utcDateTime` will be emitted as `string` and `unixTimestamp32` as `number`.
|
|
63
|
+
- `enable-typeguards` (default: false, **requires** `enable-types`): enables output of [typeguards](#typeguards).
|
|
64
|
+
- `enable-routes` (default: false): enables output of the [HTTP-routes object](#emitter-routes).
|
|
65
|
+
- `enable-routed-typemap` (default: false, **requires** `enable-types`): enables output of an [indexable type](#emitter-routed-typemap), mapping paths and HTTP verbs to request and response bodies.
|
|
66
|
+
- `string-nominal-enums` (default: false): outputs member names as strings instead of index values for enum members declared without explicit values.
|
|
67
|
+
- `serializable-date-types` (default: false): outputs serializable types for typespec's dates types that match OpenApi spec. Types like `offsetDateTime`, `plainDate` and `utcDateTime` will be emitted as `string` and `unixTimestamp32` as `number`. If disabled, all these types resolve to `Date`.
|
|
63
68
|
|
|
64
|
-
## Types
|
|
69
|
+
## Emitter: Types
|
|
65
70
|
|
|
66
|
-
|
|
71
|
+
All examples in this section use this input:
|
|
67
72
|
|
|
68
|
-
|
|
73
|
+
```ts
|
|
74
|
+
namespace Showcase {
|
|
75
|
+
enum Status {
|
|
76
|
+
Status1,
|
|
77
|
+
Status2
|
|
78
|
+
}
|
|
69
79
|
|
|
70
|
-
|
|
80
|
+
/** A showcase model. */
|
|
81
|
+
model Mdl {
|
|
82
|
+
status: Status,
|
|
83
|
+
something: string,
|
|
84
|
+
someNumber: int32,
|
|
85
|
+
nestedModel: {
|
|
86
|
+
name: string
|
|
87
|
+
}
|
|
88
|
+
}
|
|
71
89
|
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
90
|
+
@get
|
|
91
|
+
op getModel(): {@statusCode status: 200, @body body: Mdl};
|
|
92
|
+
|
|
93
|
+
@route("/inner")
|
|
94
|
+
namespace InnerNamespace {
|
|
95
|
+
scalar ID extends uint32;
|
|
96
|
+
scalar Name extends string;
|
|
97
|
+
|
|
98
|
+
model InnerNamespaceModel {
|
|
99
|
+
@visibility(Lifecycle.Read)
|
|
100
|
+
id: ID,
|
|
101
|
+
name: Name,
|
|
102
|
+
@visibility(Lifecycle.Create)
|
|
103
|
+
created?: unixTimestamp32,
|
|
104
|
+
parent: Mdl
|
|
105
|
+
}
|
|
76
106
|
|
|
77
|
-
|
|
107
|
+
@post
|
|
108
|
+
op create(@body body: InnerNamespaceModel): OkResponse;
|
|
78
109
|
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
Never,
|
|
83
|
-
Once,
|
|
84
|
-
Often
|
|
85
|
-
}
|
|
86
|
-
union Author {"unknown" | string}
|
|
87
|
-
model Book {
|
|
88
|
-
author: Author,
|
|
89
|
-
title: string,
|
|
90
|
-
subtitle: null | string,
|
|
91
|
-
read: ReadStatus,
|
|
92
|
-
chapterTitles?: string[]
|
|
110
|
+
@delete
|
|
111
|
+
@route("{id}")
|
|
112
|
+
op del(@path id: ID): {@statusCode status: 200, @body body: InnerNamespaceModel} | UnauthorizedResponse;
|
|
93
113
|
}
|
|
94
|
-
|
|
95
|
-
// you can either nest namespaces like this:
|
|
96
|
-
namespace subNameSpace {/* ... */}
|
|
97
|
-
// ... or specify them in (and import from) external files:
|
|
98
|
-
namespace myProject.subNameSpace {/* ... */} // this is in another file
|
|
99
|
-
// The emitted file will always have the name of the namespace it's
|
|
100
|
-
// *currently* investigating, in this case:
|
|
101
|
-
// `SubNameSpace.ts`
|
|
102
114
|
}
|
|
103
115
|
```
|
|
104
116
|
|
|
105
|
-
|
|
117
|
+
Naturally, you can also split your declarations into multiple files and import them.
|
|
118
|
+
|
|
119
|
+
### Types
|
|
106
120
|
|
|
107
121
|
```ts
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
}
|
|
114
|
-
export type Author = "unknown" | string;
|
|
115
|
-
export interface Book {
|
|
116
|
-
author: Author,
|
|
117
|
-
title: string,
|
|
118
|
-
subtitile: null | string,
|
|
119
|
-
read: ReadStatus,
|
|
120
|
-
chapterTitles?: string[]
|
|
122
|
+
// Showcase.ts
|
|
123
|
+
|
|
124
|
+
export enum Status {
|
|
125
|
+
Status1,
|
|
126
|
+
Status2
|
|
121
127
|
}
|
|
122
128
|
|
|
123
|
-
|
|
124
|
-
export
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
129
|
+
/** A showcase model. */
|
|
130
|
+
export type Mdl = {
|
|
131
|
+
status: Status,
|
|
132
|
+
something: string,
|
|
133
|
+
someNumber: number,
|
|
134
|
+
nestedModel: {
|
|
135
|
+
name: string
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
```
|
|
133
139
|
|
|
134
|
-
|
|
140
|
+
```ts
|
|
141
|
+
// Showcase.InnerNamespace.ts
|
|
142
|
+
|
|
143
|
+
export type ID = number
|
|
144
|
+
export type Name = string
|
|
145
|
+
export type InnerNamespaceModel<V extends Lifecycle = Lifecycle.All> = FilterLifecycle<{
|
|
146
|
+
id: ID,
|
|
147
|
+
name: Name,
|
|
148
|
+
created?: Date,
|
|
149
|
+
parent: Showcase.Mdl
|
|
150
|
+
}, {
|
|
151
|
+
'id': {vis: [Lifecycle.Read]},
|
|
152
|
+
'created': {vis: [Lifecycle.Create]}
|
|
153
|
+
}, V>
|
|
135
154
|
```
|
|
136
155
|
|
|
137
|
-
|
|
138
|
-
|
|
156
|
+
As you can see, the output is split into files per namespace.
|
|
157
|
+
The defined scalars are exported as types, as are the models, while enums are exported as-is (also see [nominal enums](#nominal-enums)).
|
|
158
|
+
You can also see how the already-known `Mdl` is referenced by name.
|
|
139
159
|
|
|
140
|
-
|
|
160
|
+
If you're wondering why `InnerNamespaceModel` looks funny, check out the [lifecycle visibility](#lifecycle-visibility) section.
|
|
141
161
|
|
|
142
|
-
|
|
162
|
+
### Typeguards
|
|
143
163
|
|
|
144
|
-
|
|
164
|
+
Setting the option `enable-typeguards` to `true` will generate typeguards for all exported types.
|
|
165
|
+
This is the output of our example:
|
|
145
166
|
|
|
146
167
|
```ts
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
168
|
+
// Showcase.ts
|
|
169
|
+
|
|
170
|
+
export type Mdl = {
|
|
171
|
+
status: Status,
|
|
172
|
+
something: string,
|
|
173
|
+
someNumber: number,
|
|
174
|
+
nestedModel: {
|
|
175
|
+
name: string
|
|
176
|
+
}
|
|
150
177
|
}
|
|
178
|
+
export function isMdl(t: any): t is Mdl {return (
|
|
179
|
+
t['status'] !== undefined && (true) &&
|
|
180
|
+
t['something'] !== undefined && (typeof t['something'] === 'string') &&
|
|
181
|
+
t['someNumber'] !== undefined && (typeof t['someNumber'] === 'number') &&
|
|
182
|
+
t['nestedModel'] !== undefined && (
|
|
183
|
+
t['nestedModel']['name'] !== undefined && (typeof t['nestedModel']['name'] === 'string')
|
|
184
|
+
)
|
|
185
|
+
)}
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
```ts
|
|
189
|
+
export type ID = number
|
|
190
|
+
export function isID(t: any): t is ID {return (typeof t === 'number')}
|
|
191
|
+
|
|
192
|
+
export type Name = string
|
|
193
|
+
export function isName(t: any): t is Name {return (typeof t === 'string')}
|
|
194
|
+
|
|
195
|
+
export type InnerNamespaceModel<V extends Lifecycle = Lifecycle.All> = FilterLifecycle<{
|
|
196
|
+
id: ID,
|
|
197
|
+
name: Name,
|
|
198
|
+
created?: Date,
|
|
199
|
+
parent: Showcase.Mdl
|
|
200
|
+
}, {
|
|
201
|
+
'id': {vis: [Lifecycle.Read]},
|
|
202
|
+
'created': {vis: [Lifecycle.Create]}
|
|
203
|
+
}, V>
|
|
204
|
+
export function isInnerNamespaceModel(t: any, vis: Lifecycle = Lifecycle.All): t is InnerNamespaceModel<typeof vis> {return (
|
|
205
|
+
((vis as any) !== Lifecycle.All && ![Lifecycle.Read].includes(vis) ? !('id' in t) : (t['id'] !== undefined && (isID(t['id'])))) &&
|
|
206
|
+
t['name'] !== undefined && (isName(t['name'])) &&
|
|
207
|
+
((vis as any) !== Lifecycle.All && ![Lifecycle.Create].includes(vis) ? !('created' in t) : (t['created'] === undefined || (t['created'] instanceof Date))) &&
|
|
208
|
+
t['parent'] !== undefined && (Showcase.isMdl(t['parent']))
|
|
209
|
+
)}
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
Typeguards are functions you can call to ensure some variable is exactly of the type you'd expect.
|
|
213
|
+
As you can see, already-known typeguards are resused (similar to types). [Lifecycle visibility](#lifecycle-visibility) is respected.
|
|
214
|
+
Typeguards are designed to be as restrictive as possible (except extra properties, those are not checked for). If you encounter one that is not as strict as it could be, please open an issue.
|
|
215
|
+
|
|
216
|
+
### Lifecycle Visibility
|
|
151
217
|
|
|
152
|
-
|
|
153
|
-
alias Derived1 = OmitProperties<Demo, "prop1">;
|
|
218
|
+
As you have probably notices, some parts of our example have more complex output than others:
|
|
154
219
|
|
|
155
|
-
|
|
156
|
-
|
|
220
|
+
```ts
|
|
221
|
+
export type InnerNamespaceModel<V extends Lifecycle = Lifecycle.All> = FilterLifecycle<{
|
|
222
|
+
id: ID,
|
|
223
|
+
name: Name,
|
|
224
|
+
created?: Date,
|
|
225
|
+
parent: Showcase.Mdl
|
|
226
|
+
}, {
|
|
227
|
+
'id': {vis: [Lifecycle.Read]},
|
|
228
|
+
'created': {vis: [Lifecycle.Create]}
|
|
229
|
+
}, V>
|
|
230
|
+
|
|
231
|
+
function isInnerNamespaceModel(t: any, vis: Lifecycle = Lifecycle.All) { /* ... */ }
|
|
157
232
|
```
|
|
158
233
|
|
|
159
|
-
|
|
234
|
+
This is the Lifecycle system. In TypeSpec, you can use [lifecycle visibility](https://typespec.io/docs/language-basics/visibility/#lifecycle-visibility) to specify which parts of a model are present during creation of a resource, reading it, updating it, et cetera.
|
|
160
235
|
|
|
161
|
-
|
|
236
|
+
This emitter allows you to work with that.
|
|
162
237
|
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
I contains the following data (per `op`):
|
|
238
|
+
Any type that has `@visibility` decorators *somewhere* will be "lifecycle-enabled". "Somewhere" does include nested types and extended types as well, so everything that is reference by the current type in any way.
|
|
239
|
+
Working with lifecycles involves use of the `Lifecycle` enum, conveniently emitted alongside your regular project output.
|
|
166
240
|
|
|
167
|
-
|
|
168
|
-
- `path`: Path (as defined in the `route` string; parameters are not substituted)
|
|
169
|
-
- `getUrl`: Function for generating a valid URL to this `op`; if the path has parameters, this function will have matching parameters
|
|
170
|
-
- `auth`: Array of valid authentication schemes (or `[null]`, if none)
|
|
171
|
-
Just as the types emitter, this emitter will also preserve docs as JSDoc-style comments.
|
|
241
|
+
#### In Types
|
|
172
242
|
|
|
173
|
-
|
|
243
|
+
Any type that is lifecycle-enabled gets a type parameter:
|
|
174
244
|
|
|
175
245
|
```ts
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
@get
|
|
179
|
-
op getSomething(): {@body body: string};
|
|
180
|
-
// if you want to use `typeguards-in-routes`, make sure
|
|
181
|
-
// to properly declare responses as a model with a `body`-property
|
|
246
|
+
type T<V extends Lifecycle = Lifecycle.All>
|
|
247
|
+
```
|
|
182
248
|
|
|
183
|
-
|
|
184
|
-
@route("{param}")
|
|
185
|
-
@useAuth(NoAuth | BasicAuth)
|
|
186
|
-
op getSmthElse(@path param: string): {@body body: string};
|
|
249
|
+
This parameter defaults to `All` (so you don't *have* to specify it), including all properties. If you access the type with `T<Lifecycle.Read>`, for example, all properties not visible on read will be excluded. This follows the normal TypeSpec behavior of *always* including all properties that do not have *any* visibility specified.
|
|
187
250
|
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
@path post_param: int32,
|
|
195
|
-
@body body: string
|
|
196
|
-
): {@body body: string};
|
|
197
|
-
}
|
|
198
|
-
}
|
|
251
|
+
#### In Typeguards
|
|
252
|
+
|
|
253
|
+
Let's look at the typeguard signature of a lifecycle-enabled type:
|
|
254
|
+
|
|
255
|
+
```ts
|
|
256
|
+
isInnerNamespaceModel(t: any, vis: Lifecycle = Lifecycle.All)
|
|
199
257
|
```
|
|
200
258
|
|
|
201
|
-
|
|
259
|
+
Again, the lifecycle defaults to `All` and works similar to the type parameter. Also similarly, any typeguard that calls another typeguard which *has* lifecycle visibility, will also have it.
|
|
260
|
+
|
|
261
|
+
### Nominal Enums
|
|
262
|
+
|
|
263
|
+
In TypeSpec (and TypeScript), enums can be declared "plain" or with values:
|
|
202
264
|
|
|
203
265
|
```ts
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
sub: {
|
|
219
|
-
postSomething: {
|
|
220
|
-
method: 'POST',
|
|
221
|
-
path: '/post/{post_param}',
|
|
222
|
-
getUrl: (params: {post_param: string}): string => `/post/${params.post_param}`,
|
|
223
|
-
auth: ["BEARER"]
|
|
224
|
-
}
|
|
225
|
-
}
|
|
226
|
-
} as const;
|
|
266
|
+
export enum Status {
|
|
267
|
+
STATUS_1,
|
|
268
|
+
STATUS_2
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
export enum StatusShifted {
|
|
272
|
+
STATUS_1 = 1,
|
|
273
|
+
STATUS_2 = 2
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
export enum StatusText {
|
|
277
|
+
STATUS_1 = 'Status 1',
|
|
278
|
+
STATUS_2 = 'Status 2'
|
|
279
|
+
}
|
|
227
280
|
```
|
|
228
281
|
|
|
229
|
-
|
|
282
|
+
The latter 2 will be emitted just as they are defined here, but the first example (the plain one) can be configured.
|
|
283
|
+
By default, it is emitted as-is, but that may be undesirable. The `string-nominal-enums` config option emits enums without explicitely declared values in a way that uses the enum member names as their values:
|
|
230
284
|
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
285
|
+
```ts
|
|
286
|
+
export enum Status {
|
|
287
|
+
STATUS_1 = 'STATUS_1',
|
|
288
|
+
STATUS_2 = 'STATUS_2'
|
|
289
|
+
}
|
|
290
|
+
```
|
|
234
291
|
|
|
235
|
-
|
|
236
|
-
> The Typemap is not nested! This means that, let's say the route "/user/account" will not be mapped to `{user: {account: /* ... */}}` (somewhat similar to how the Routes Emitter works), but to `{"/user/account": /* ... */}`. Crucially, those map keys are *the same as the `path` property in the Routes object* emitted from the Routes Emitter.
|
|
237
|
-
> This means you can select from the Typemap *using* the structured Routes object.
|
|
292
|
+
## Emitter: Routes
|
|
238
293
|
|
|
239
|
-
|
|
294
|
+
When enabled, this emitter will traverse your program to find all operations (`op`).
|
|
295
|
+
These are then compiled into a single, nested object:
|
|
240
296
|
|
|
241
297
|
```ts
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
INACTIVE
|
|
247
|
-
}
|
|
248
|
-
enum Condition {
|
|
249
|
-
NEW = 'new',
|
|
250
|
-
USED = 'used'
|
|
298
|
+
namespace Showcase {
|
|
299
|
+
enum Status {
|
|
300
|
+
Status1,
|
|
301
|
+
Status2
|
|
251
302
|
}
|
|
252
303
|
|
|
253
|
-
model
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
304
|
+
/** A showcase model. */
|
|
305
|
+
model Mdl {
|
|
306
|
+
status: Status,
|
|
307
|
+
something: string,
|
|
308
|
+
someNumber: int32,
|
|
309
|
+
nestedModel: {
|
|
310
|
+
name: string
|
|
311
|
+
}
|
|
258
312
|
}
|
|
259
313
|
|
|
260
314
|
@get
|
|
261
|
-
op
|
|
315
|
+
op getModel(): {@statusCode status: 200, @body body: Mdl};
|
|
316
|
+
|
|
317
|
+
@route("/inner")
|
|
318
|
+
namespace InnerNamespace {
|
|
319
|
+
scalar ID extends uint32;
|
|
320
|
+
scalar Name extends string;
|
|
321
|
+
|
|
322
|
+
model InnerNamespaceModel {
|
|
323
|
+
@visibility(Lifecycle.Read)
|
|
324
|
+
id: ID,
|
|
325
|
+
name: Name,
|
|
326
|
+
@visibility(Lifecycle.Create)
|
|
327
|
+
created?: unixTimestamp32,
|
|
328
|
+
parent: Mdl
|
|
329
|
+
}
|
|
262
330
|
|
|
263
|
-
|
|
264
|
-
|
|
331
|
+
@post
|
|
332
|
+
op create(@body body: InnerNamespaceModel): OkResponse;
|
|
265
333
|
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
): {@body body: ModelA}| NotFoundResponse | {@statusCode status: 418} | {@statusCode status: 419, @body body: null};
|
|
334
|
+
@delete
|
|
335
|
+
@route("{id}")
|
|
336
|
+
op del(@path id: ID): {@statusCode status: 200, @body body: InnerNamespaceModel} | UnauthorizedResponse;
|
|
337
|
+
}
|
|
271
338
|
}
|
|
272
339
|
```
|
|
273
340
|
|
|
274
|
-
...will be transformed into:
|
|
341
|
+
... will be transformed into:
|
|
342
|
+
|
|
343
|
+
```ts
|
|
344
|
+
export const routes_Showcase = {
|
|
345
|
+
getModel: {
|
|
346
|
+
verb: 'GET',
|
|
347
|
+
path: '/',
|
|
348
|
+
getUrl: (): string => `/`,
|
|
349
|
+
auth: [null]
|
|
350
|
+
},
|
|
351
|
+
InnerNamespace: {
|
|
352
|
+
create: {
|
|
353
|
+
verb: 'POST',
|
|
354
|
+
path: '/inner',
|
|
355
|
+
getUrl: (): string => `/inner`,
|
|
356
|
+
auth: [null]
|
|
357
|
+
},
|
|
358
|
+
del: {
|
|
359
|
+
verb: 'DELETE',
|
|
360
|
+
path: '/inner/{id}',
|
|
361
|
+
getUrl: (params: {id: string}): string => `/inner/${params.id}`,
|
|
362
|
+
auth: [null]
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
} as const;
|
|
366
|
+
```
|
|
367
|
+
|
|
368
|
+
The main use cases are:
|
|
369
|
+
|
|
370
|
+
- accessing URLs safely
|
|
371
|
+
- using the `path` property to access the [typemap](#emitter-routed-typemap)
|
|
372
|
+
|
|
373
|
+
## Emitter: Routed Typemap
|
|
374
|
+
|
|
375
|
+
When enabled, this emitter provides a single indexed type from which the request and response body types can be accessed (same input as [above](#emitter-routes)):
|
|
275
376
|
|
|
276
377
|
```ts
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
['/typemap']: {
|
|
378
|
+
export type types_Showcase<V extends Lifecycle = Lifecycle.All> = {
|
|
379
|
+
['/']: {
|
|
280
380
|
['GET']: {
|
|
281
381
|
request: null
|
|
282
|
-
response: {status: 200, body:
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
cond: 'new' | 'used'
|
|
287
|
-
}[]} | {status: 418, body: 'Me teapot'}
|
|
288
|
-
},
|
|
382
|
+
response: {status: 200, body: Showcase.Mdl}
|
|
383
|
+
}
|
|
384
|
+
},
|
|
385
|
+
['/inner']: {
|
|
289
386
|
['POST']: {
|
|
290
|
-
request:
|
|
291
|
-
id: number,
|
|
292
|
-
name: string,
|
|
293
|
-
state: 0 | 1, // this would be `'ACTIVE' | 'INACTIVE'` with `string-nominal-enums: true`
|
|
294
|
-
cond: 'new' | 'used'
|
|
295
|
-
}
|
|
387
|
+
request: Showcase_InnerNamespace.InnerNamespaceModel<V extends Lifecycle.All ? (Lifecycle.Create) : V>
|
|
296
388
|
response: {status: 200, body: {
|
|
297
|
-
/** The status code. */
|
|
298
389
|
statusCode: 200
|
|
299
390
|
}}
|
|
300
391
|
}
|
|
301
392
|
},
|
|
302
|
-
['/
|
|
303
|
-
['
|
|
393
|
+
['/inner/{id}']: {
|
|
394
|
+
['DELETE']: {
|
|
304
395
|
request: null
|
|
305
|
-
response: {status: 200, body: {
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
}} | {status: 404, body: {
|
|
309
|
-
/** The status code. */
|
|
310
|
-
statusCode: 404
|
|
311
|
-
}} | {status: 418, body: {
|
|
312
|
-
status: 418
|
|
313
|
-
}} | {status: 419, body: null}
|
|
396
|
+
response: {status: 200, body: Showcase_InnerNamespace.InnerNamespaceModel<V extends Lifecycle.All ? (Lifecycle.Read) : V>} | {status: 401, body: {
|
|
397
|
+
statusCode: 401
|
|
398
|
+
}}
|
|
314
399
|
}
|
|
315
400
|
}
|
|
316
401
|
};
|
|
317
402
|
```
|
|
318
403
|
|
|
319
|
-
|
|
404
|
+
> [!TIP]
|
|
405
|
+
> This type is not nested.
|
|
406
|
+
> Each route can be accessed by using the `path` property on the corresponding entry in the `routes` object.
|
|
407
|
+
|
|
408
|
+
This automatically applies [lifecycle visibilities](#lifecycle-visibility), where applicable. The assignment which HTTP verb leads to which visibility variant follows the logic TypeSpec uses internally:
|
|
409
|
+
|
|
410
|
+
| Verb | Lifecycles |
|
|
411
|
+
| -------- | ------------------ |
|
|
412
|
+
| `HEAD` | `Query` |
|
|
413
|
+
| `GET` | `Query` |
|
|
414
|
+
| `POST` | `Create` |
|
|
415
|
+
| `PUT` | `Create \| Update` |
|
|
416
|
+
| `PATCH` | `Update` |
|
|
417
|
+
| `DELETE` | `Delete` |
|
|
418
|
+
| Return | `Read` |
|
|
419
|
+
|
|
420
|
+
"Return" refers to *all* operation return types.
|
|
421
|
+
|
|
422
|
+
The typemap itself has a lifecycle visibility parameter. If you access the typemap using any type parameter (except `Lifecycle.All`, which is the default), the returned type will be forced to the visibility you specified, overriding the HTTP-verb-specific selection.
|
|
320
423
|
|
|
321
424
|
```ts
|
|
322
425
|
// Accessing type of response body directly by knowing path and verb
|
|
@@ -325,16 +428,56 @@ type T_update1 = types_namespaceA['/typemap']['POST']['response']['body']
|
|
|
325
428
|
// Accessing type of request body by indexing Routes object
|
|
326
429
|
// namespace "namespaceA.typemap", op "add"
|
|
327
430
|
type T_update2 = types_namespaceA[typeof routes_namespaceA.typemap.add.path]['POST']['request']
|
|
328
|
-
//
|
|
431
|
+
// You could also use `typeof routes_namespace.testSimple.update.method` instead of 'POST'.
|
|
329
432
|
```
|
|
330
433
|
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
434
|
+
## Contributing
|
|
435
|
+
|
|
436
|
+
Thank you very much for considering investing time into this project!
|
|
437
|
+
|
|
438
|
+
For the smoothest contributing experience, please consider these guidelines:
|
|
439
|
+
|
|
440
|
+
- please use [conventional commits](https://www.conventionalcommits.org/en/v1.0.0/#summary)
|
|
441
|
+
- if your contribution expands functionality, please consider drafting tests for it
|
|
442
|
+
|
|
443
|
+
You may find the following section helpful.
|
|
444
|
+
|
|
445
|
+
### Short Overview
|
|
446
|
+
|
|
447
|
+
This section roughly outlines the inner workings of the library.
|
|
448
|
+
|
|
449
|
+
- `lib.ts` defines primarily emitter options
|
|
450
|
+
- `emitter.ts` is the main entry point
|
|
451
|
+
|
|
452
|
+
`$onEmit` calls the actual emitters defined in `emit_*` files.
|
|
453
|
+
These each traverse the program, recursively collecting the objects they are interested in (emittable types, operations, ...) from the root namespaces specified by the user.
|
|
454
|
+
|
|
455
|
+
The primary resolution of types and typeguards starts in `resolve/Resolvable.ts`.
|
|
456
|
+
It defines an abstract class which both contains static functions to resolve types as well as inherited methods each type resolver implements and uses to recursively resolve.
|
|
457
|
+
Each resolvable type defines its methods in an inherited class, in `resolve/types/[type].ts`.
|
|
458
|
+
|
|
459
|
+
The primary flow of type resolution is quite simple:
|
|
460
|
+
|
|
461
|
+
- `static Resolvable.resolve` calls `static Resolvable.for`, which returns a `Resolvable` instance for the specific type (we will call this instance `rt`, for "resolvable type")
|
|
462
|
+
- `rt.resolve` first checks a list of all types found in the program - even if they have not been yet resolved, they will be and then will be emitted - so they should just be referenced. This returns the name and skips all further resolution, ending the process here.
|
|
463
|
+
- `rt.type` or `rt.typeguard` are invoked: these are defined for each type in its class. Depending on the type, these either resolve directly, ending the process here, or have other types "within" them (unions or models, for example, have this). In this case, `rt.resolveNested` is called, which finishes the recursive loop by calling `static Resolvable.resolve` on the "child" type.
|
|
464
|
+
|
|
465
|
+
Most of these methods do not return data, because they mutate an "output" object passed as a parameter. This has proven to be much more concise than passing return values up and down the chain.
|
|
466
|
+
Also to be considered is the `hasVisibility` flag showing up at many points. This is used to ultimately determine whether a type needs lifecycle visibility handling in any way (because if any part of it does, so does the whole thing).
|
|
467
|
+
|
|
468
|
+
### Todo
|
|
336
469
|
|
|
337
|
-
|
|
470
|
+
There are some things left to do, most of which I hoped to get ready for 2.0.0, however, that didn't work out.
|
|
471
|
+
My free time is too limited to get these things done without holding back the much needed fixes in 2.0.0 .
|
|
472
|
+
They will either be done when time permits or, perhaps, you might want to tackle some of this?
|
|
338
473
|
|
|
339
|
-
-
|
|
340
|
-
-
|
|
474
|
+
- [ ] additional tests (the current testing setup is by no means exhaustive)
|
|
475
|
+
- [ ] `extends` on models, including `is` and spread notation
|
|
476
|
+
- [ ] imports from other files; are naming collisions still possible?
|
|
477
|
+
- [ ] thorough tests on imports and reuses for all emitted type kinds (model, union, enum, scalar)
|
|
478
|
+
- [ ] support for generics
|
|
479
|
+
- [ ] (with new option) typeguards referenced in / accessible from routes object
|
|
480
|
+
- [ ] each file could export its "child" namespaces (from their respective files) via `export * from "rootNS.someNS.subNS.ts" as subNS;`, effectively making everything accessible by simply typing `rootNS.someNS.subNS.MyType`
|
|
481
|
+
- this will collide with imports from other files; these conflicts must be avoided when this option is set
|
|
482
|
+
- one dedicated file as "root" exports all specified root namespaces
|
|
483
|
+
- the `typemap` object can be used to generate lists of all namespaces within each namespace, using array reduction
|