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.
Files changed (166) hide show
  1. package/.husky/pre-commit +2 -1
  2. package/.prettierignore +2 -1
  3. package/CHANGELOG.md +19 -24
  4. package/CODE_OF_CONDUCT.md +128 -0
  5. package/README.md +362 -219
  6. package/dist/src/emit_routedTypemap.d.ts +4 -0
  7. package/dist/src/emit_routedTypemap.js +83 -0
  8. package/dist/src/emit_routedTypemap.js.map +1 -0
  9. package/dist/src/emit_routes.d.ts +3 -2
  10. package/dist/src/emit_routes.js +73 -47
  11. package/dist/src/emit_routes.js.map +1 -1
  12. package/dist/src/emit_types.d.ts +3 -9
  13. package/dist/src/emit_types.js +109 -74
  14. package/dist/src/emit_types.js.map +1 -1
  15. package/dist/src/emitter.d.ts +2 -6
  16. package/dist/src/emitter.js +18 -76
  17. package/dist/src/emitter.js.map +1 -1
  18. package/dist/src/helpers/appendableString.d.ts +15 -0
  19. package/dist/src/helpers/appendableString.js +41 -0
  20. package/dist/src/helpers/appendableString.js.map +1 -0
  21. package/dist/src/helpers/arrays.d.ts +3 -0
  22. package/dist/src/helpers/arrays.js +23 -0
  23. package/dist/src/helpers/arrays.js.map +1 -0
  24. package/dist/src/{helper_autogenerateWarning.d.ts → helpers/autogenerateWarning.d.ts} +1 -1
  25. package/dist/src/{helper_autogenerateWarning.js → helpers/autogenerateWarning.js} +1 -2
  26. package/dist/src/helpers/autogenerateWarning.js.map +1 -0
  27. package/dist/src/helpers/buildTypeMap.d.ts +12 -0
  28. package/dist/src/helpers/buildTypeMap.js +44 -0
  29. package/dist/src/helpers/buildTypeMap.js.map +1 -0
  30. package/dist/src/helpers/diagnostics.d.ts +4 -0
  31. package/dist/src/helpers/diagnostics.js +15 -0
  32. package/dist/src/helpers/diagnostics.js.map +1 -0
  33. package/dist/src/helpers/getImports.d.ts +2 -0
  34. package/dist/src/helpers/getImports.js +3 -0
  35. package/dist/src/helpers/getImports.js.map +1 -0
  36. package/dist/src/helpers/namespaces.d.ts +4 -0
  37. package/dist/src/helpers/namespaces.js +14 -0
  38. package/dist/src/helpers/namespaces.js.map +1 -0
  39. package/dist/src/helpers/visibilityHelperFile.d.ts +4 -0
  40. package/dist/src/helpers/visibilityHelperFile.js +57 -0
  41. package/dist/src/helpers/visibilityHelperFile.js.map +1 -0
  42. package/dist/src/lib.d.ts +4 -1
  43. package/dist/src/lib.js +14 -3
  44. package/dist/src/lib.js.map +1 -1
  45. package/dist/src/parseOptions.d.ts +8 -0
  46. package/dist/src/parseOptions.js +26 -0
  47. package/dist/src/parseOptions.js.map +1 -0
  48. package/dist/src/resolve/Resolvable.d.ts +32 -0
  49. package/dist/src/resolve/Resolvable.js +180 -0
  50. package/dist/src/resolve/Resolvable.js.map +1 -0
  51. package/dist/src/resolve/Resolvable_helpers.d.ts +42 -0
  52. package/dist/src/resolve/Resolvable_helpers.js +19 -0
  53. package/dist/src/resolve/Resolvable_helpers.js.map +1 -0
  54. package/dist/src/resolve/operationTypemap.d.ts +21 -0
  55. package/dist/src/resolve/operationTypemap.js +138 -0
  56. package/dist/src/resolve/operationTypemap.js.map +1 -0
  57. package/dist/src/resolve/types/Enum.d.ts +21 -0
  58. package/dist/src/resolve/types/Enum.js +61 -0
  59. package/dist/src/resolve/types/Enum.js.map +1 -0
  60. package/dist/src/resolve/types/Model.Indexed.d.ts +12 -0
  61. package/dist/src/resolve/types/Model.Indexed.js +69 -0
  62. package/dist/src/resolve/types/Model.Indexed.js.map +1 -0
  63. package/dist/src/resolve/types/Model.Shaped.d.ts +17 -0
  64. package/dist/src/resolve/types/Model.Shaped.js +210 -0
  65. package/dist/src/resolve/types/Model.Shaped.js.map +1 -0
  66. package/dist/src/resolve/types/Scalar.d.ts +9 -0
  67. package/dist/src/resolve/types/Scalar.js +109 -0
  68. package/dist/src/resolve/types/Scalar.js.map +1 -0
  69. package/dist/src/resolve/types/Simple.d.ts +11 -0
  70. package/dist/src/resolve/types/Simple.js +49 -0
  71. package/dist/src/resolve/types/Simple.js.map +1 -0
  72. package/dist/src/resolve/types/Tuple.d.ts +9 -0
  73. package/dist/src/resolve/types/Tuple.js +38 -0
  74. package/dist/src/resolve/types/Tuple.js.map +1 -0
  75. package/dist/src/resolve/types/Union.d.ts +9 -0
  76. package/dist/src/resolve/types/Union.js +36 -0
  77. package/dist/src/resolve/types/Union.js.map +1 -0
  78. package/eslint.config.js +12 -4
  79. package/package.json +2 -2
  80. package/src/emit_routedTypemap.ts +128 -0
  81. package/src/emit_routes.ts +108 -61
  82. package/src/emit_types.ts +137 -92
  83. package/src/emitter.ts +19 -107
  84. package/src/helpers/appendableString.ts +52 -0
  85. package/src/helpers/arrays.ts +21 -0
  86. package/src/{helper_autogenerateWarning.ts → helpers/autogenerateWarning.ts} +0 -1
  87. package/src/helpers/buildTypeMap.ts +72 -0
  88. package/src/helpers/diagnostics.ts +19 -0
  89. package/src/helpers/getImports.ts +9 -0
  90. package/src/helpers/namespaces.ts +18 -0
  91. package/src/helpers/visibilityHelperFile.ts +63 -0
  92. package/src/lib.ts +25 -4
  93. package/src/parseOptions.ts +26 -0
  94. package/src/resolve/Resolvable.ts +267 -0
  95. package/src/resolve/Resolvable_helpers.ts +65 -0
  96. package/src/resolve/operationTypemap.ts +212 -0
  97. package/src/resolve/types/Enum.ts +92 -0
  98. package/src/resolve/types/Model.Indexed.ts +113 -0
  99. package/src/resolve/types/Model.Shaped.ts +291 -0
  100. package/src/resolve/types/Scalar.ts +140 -0
  101. package/src/resolve/types/Simple.ts +88 -0
  102. package/src/resolve/types/Tuple.ts +56 -0
  103. package/src/resolve/types/Union.ts +52 -0
  104. package/test/helpers/integrationTest-novis.tsp +51 -0
  105. package/test/helpers/integrationTest.tsp +53 -0
  106. package/test/helpers/largeModel.tsp +40 -0
  107. package/test/{runner.ts → helpers/runner.ts} +1 -1
  108. package/test/helpers/ts.ts +11 -0
  109. package/test/helpers/wrapper.ts +144 -0
  110. package/test/routes/routes.target.ts +35 -0
  111. package/test/routes/routes.test.ts +22 -0
  112. package/test/typeguards/combined.test.ts +78 -0
  113. package/test/typeguards/enum.test.ts +10 -0
  114. package/test/typeguards/model.indexed.test.ts +68 -0
  115. package/test/typeguards/model.shaped.test.ts +38 -0
  116. package/test/typeguards/scalar.test.ts +62 -0
  117. package/test/typeguards/simple.test.ts +35 -0
  118. package/test/typeguards/tuple.test.ts +34 -0
  119. package/test/typeguards/union.test.ts +29 -0
  120. package/test/typemap/typemap-novis.target.ts +38 -0
  121. package/test/typemap/typemap.target.ts +39 -0
  122. package/test/typemap/typemap.test.ts +48 -0
  123. package/test/types/combined.test.ts +71 -0
  124. package/test/types/enum.test.ts +57 -0
  125. package/test/types/model.indexed.test.ts +46 -0
  126. package/test/types/model.shaped.test.ts +23 -0
  127. package/test/types/scalar.test.ts +53 -0
  128. package/test/types/simple.test.ts +20 -0
  129. package/test/types/tuple.test.ts +29 -0
  130. package/test/types/union.test.ts +20 -0
  131. package/tsconfig.json +1 -0
  132. package/dist/src/emit_mapped_types.d.ts +0 -2
  133. package/dist/src/emit_mapped_types.js +0 -124
  134. package/dist/src/emit_mapped_types.js.map +0 -1
  135. package/dist/src/emit_types_resolve.d.ts +0 -22
  136. package/dist/src/emit_types_resolve.js +0 -217
  137. package/dist/src/emit_types_resolve.js.map +0 -1
  138. package/dist/src/emit_types_typeguards.d.ts +0 -17
  139. package/dist/src/emit_types_typeguards.js +0 -121
  140. package/dist/src/emit_types_typeguards.js.map +0 -1
  141. package/dist/src/helper_autogenerateWarning.js.map +0 -1
  142. package/src/emit_mapped_types.ts +0 -155
  143. package/src/emit_types_resolve.ts +0 -280
  144. package/src/emit_types_typeguards.ts +0 -178
  145. package/test/main.test.ts +0 -83
  146. package/test/out/.gitkeep +0 -0
  147. package/test/targets/enum.routed-types.ts +0 -59
  148. package/test/targets/enum.routes.ts +0 -29
  149. package/test/targets/enum.target.ts +0 -39
  150. package/test/targets/enum.tsp +0 -36
  151. package/test/targets/pr8.routed-types.ts +0 -131
  152. package/test/targets/pr8.routes.ts +0 -30
  153. package/test/targets/pr8.target.ts +0 -97
  154. package/test/targets/pr8.tsp +0 -62
  155. package/test/targets/simple-routes.routed-types.ts +0 -64
  156. package/test/targets/simple-routes.routes.ts +0 -29
  157. package/test/targets/simple-routes.target.ts +0 -21
  158. package/test/targets/simple-routes.tsp +0 -23
  159. package/test/targets/union.routed-types.ts +0 -59
  160. package/test/targets/union.routes.ts +0 -23
  161. package/test/targets/union.target.ts +0 -59
  162. package/test/targets/union.tsp +0 -38
  163. package/test/targets/visibility.routed-types.ts +0 -81
  164. package/test/targets/visibility.routes.ts +0 -38
  165. package/test/targets/visibility.target.ts +0 -36
  166. 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
- Currently, this library is tailored to my specific use case, which is defining HTTP APIs.
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
- - ts files exporting every model present in a namespace
11
- - 1 file for each nested namespace
12
- - exports models, enums and unions
13
- - does NOT export aliases (see below)
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
- - [typespec-typescript-emitter](#typespec-typescript-emitter)
21
- - [Installation](#installation)
22
- - [Configuration](#configuration)
23
- - [Types Emitter](#types-emitter)
24
- - [Aliases](#aliases)
25
- - [Routes Emitter](#routes-emitter)
26
- - [Routed Typemap](#routed-typemap)
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-namespace: "string"
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-namespace` **(required)**: name of the most outer namespace. As the TypeSpec docs recommend, your project is expected to consist of one or more nested namespaces. Here, you need to specify the most outer / general namespace you want emitted.
56
- - `out-dir`: output directory. Must be an absolute path; replacers like `{cwd}` are permitted.
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, *IF* type-output is enabled.
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 (routed typemap only, see [example](#routed-typemap)).
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 Emitter
69
+ ## Emitter: Types
65
70
 
66
- This emitter will traverse your configured root namespace and all nested namespaces, generating a `{namespace-name}.ts`-file.
71
+ All examples in this section use this input:
67
72
 
68
- The emitter can handle `Model`s, `Enum`s and `Union`s. ~~`Alias`'s~~ are *not* emitted - more on that [later](#aliases). It will also preserve docs as JSDoc-style comments.
73
+ ```ts
74
+ namespace Showcase {
75
+ enum Status {
76
+ Status1,
77
+ Status2
78
+ }
69
79
 
70
- The emitter should be able to handle most basic TS contructs, like scalars, literals, object, arrays, tuples and intrinsics (eg. `null`).
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
- > [!IMPORTANT]
73
- > These types do not respect most transformative decorators, notable `@visibility`.
74
- > This is because it's non-trivial to do, has unexpected problems and the functionality is *somewhat* already there, in the form of [Routed Typemaps](#routed-typemap).
75
- > For additional information, see [#7](https://github.com/crowbait/typespec-typescript-emitter/issues/7).
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
- Example:
107
+ @post
108
+ op create(@body body: InnerNamespaceModel): OkResponse;
78
109
 
79
- ```ts
80
- namespace myProject { // remember to set in config!
81
- enum ReadStatus {
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
- ...will be transformed into:
117
+ Naturally, you can also split your declarations into multiple files and import them.
118
+
119
+ ### Types
106
120
 
107
121
  ```ts
108
- /* /path/to/outdir/MyProject.ts */
109
- export enum ReadStatus {
110
- Never,
111
- Once,
112
- Often
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
- // if `enable-typeguards` is set to true
124
- export function isBook(arg: any): arg is Book {
125
- return (
126
- (arg['author'] !== undefined) &&
127
- (arg['title'] !== undefined && typeof arg['title'] === 'string') &&
128
- (arg['subtitle'] !== undefined) &&
129
- (arg['read'] !== undefined) &&
130
- (arg['chapterTitles'] === undefined || Array.isArray(arg['chapterTitles']))
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
- // the other namespace will be emitted to `/path/to/outdir/SubNameSpace.ts`
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
- Typeguards *should* create comprehensive checks that adhere as strictly to the source model as possible.
138
- If you find a case where the typeguard is looser than it needs to be, please report that as a bug.
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
- ### Aliases
160
+ If you're wondering why `InnerNamespaceModel` looks funny, check out the [lifecycle visibility](#lifecycle-visibility) section.
141
161
 
142
- There seems to be no way to extract aliases from TypeSpec's emitter framework. Because of that, `Alias`es are ignored by the emitter (or, to be more precise: `Alias`es reach the emitter already resolved. They won't be exported as their own type but directly substituted where they're needed).
162
+ ### Typeguards
143
163
 
144
- That means, if you want something to be emitted, it can't be an alias:
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
- model Demo {
148
- prop1: string,
149
- prop2: int32
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
- // will not be emitted:
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
- // will be emitted:
156
- model Derived2 {...OmitProperties<Demo, "prop1">};
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
- ## Routes Emitter
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
- **This emitter depends on your use of the `TypeSpec.Http` library**.
236
+ This emitter allows you to work with that.
162
237
 
163
- If you're using `TypeSpec.Http` to define your API routes and endpoints, this library offers an emitter to export a `routes` object.
164
- It will generate a nested object containing information about every `op` you have defined, nested by namespace.
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
- - `method`: HTTP method
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
- Example:
243
+ Any type that is lifecycle-enabled gets a type parameter:
174
244
 
175
245
  ```ts
176
- @server("https://api.example.com", "Server")
177
- namespace myProject { // remember to set in config!
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
- @get
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
- @route("/subroute")
189
- namespace sub {
190
- @post
191
- @route("post/{post_param}")
192
- @useAuth(BearerAuth)
193
- op postSomething(
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
- ...will be transformed into:
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
- /* /path/to/outdir/routes_{root-namespace}.ts */
205
- export const routes_myProject = {
206
- getSomething: {
207
- method: 'GET',
208
- path: '/',
209
- getUrl: (): string => `/`,
210
- auth: [null]
211
- },
212
- getSmthElse: {
213
- method: 'GET',
214
- path: '/{param}',
215
- getUrl: (params: {param: string}): string => `/${params.param}`,
216
- auth: [null, "BASIC"]
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
- ## Routed Typemap
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
- This emitter produces a Typemap (a typescript type indexed by string keys mapping other types) based on your HTTP routes and verbs.
232
- In short, this allows you to select a type *used in a body of your HTTP ops* using it's path and verb. This includes request and response bodies. Path parameters are not relevant for this emitter; those are already handled in the Routes object.
233
- This can be helpful when, for example, building a wrapper around your API.
285
+ ```ts
286
+ export enum Status {
287
+ STATUS_1 = 'STATUS_1',
288
+ STATUS_2 = 'STATUS_2'
289
+ }
290
+ ```
234
291
 
235
- > [!NOTE]
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
- Example:
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
- @route("/typemap")
243
- namespace namespaceA.typemap {
244
- enum State {
245
- ACTIVE,
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 ModelA {
254
- id: int32,
255
- name: string,
256
- state: State,
257
- cond: Condition
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 getAll(): {@body body: ModelA[]} | {@statusCode status: 418, @body body: "Me teapot"};
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
- @post
264
- op add(@body body: ModelA): OkResponse;
331
+ @post
332
+ op create(@body body: InnerNamespaceModel): OkResponse;
265
333
 
266
- @post
267
- @route("{id}")
268
- op getOne(
269
- @path id: int32
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
- /* /path/to/outdir/routedTypemap_{root-namespace}.ts */
278
- export type types_namespaceA = {
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
- id: number,
284
- name: string,
285
- state: 0 | 1, // this would be `'ACTIVE' | 'INACTIVE'` with `string-nominal-enums: true`
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
- ['/typemap/{id}']: {
303
- ['POST']: {
393
+ ['/inner/{id}']: {
394
+ ['DELETE']: {
304
395
  request: null
305
- response: {status: 200, body: {
306
- id: number,
307
- name: string
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
- ...which can be accessed like this:
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
- // One could also use `typeof routes_namespace.testSimple.update.method` instead of 'POST'.
431
+ // You could also use `typeof routes_namespace.testSimple.update.method` instead of 'POST'.
329
432
  ```
330
433
 
331
- > [!NOTE]
332
- > Observe how the emitter
333
- >
334
- > - assumes a `200` status code for an op's response type if you didn't define any (first responses on `getAll` and `getOne` ops)
335
- > - assumes the entire response type is the body of no body is explicitely decorated (418 on `getOne` op) (this is non-standard; I have seen quite a few projects not realizing the response definition should have a body *in* it and treating the whole thing as a body; so while technically "wrong", this accomodates those projects. You can easily define a truly empty body using `{}`, `""`, `null`..., see 419 on `getOne` op)
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
- Additional notes:
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
- - There is currently no built-in way of accessing typeguards from paths their types may be associated with.
340
- - Models are not reused in or imported by this emitter. Reasoning involves "no runtime overhead either way", "simpler code", "self-contained emitter modules" and "you're not supposed to rummage around in the generated files anyway, just import them"; this has been touched upon in [#4](https://github.com/crowbait/typespec-typescript-emitter/issues/4#issuecomment-2720955282) and [#6](https://github.com/crowbait/typespec-typescript-emitter/issues/6#issuecomment-3049999155).
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