@nxgt/typespec 0.13.2 → 0.14.0
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/README.md +34 -0
- package/dist/components.d.ts +17 -0
- package/dist/components.d.ts.map +1 -0
- package/dist/document-text.d.ts +7 -0
- package/dist/document-text.d.ts.map +1 -0
- package/dist/emitter.d.ts +3 -1
- package/dist/emitter.d.ts.map +1 -1
- package/dist/index.d.ts +4 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +501 -38
- package/dist/index.js.map +13 -7
- package/dist/lib.d.ts +25 -1
- package/dist/lib.d.ts.map +1 -1
- package/dist/marks.d.ts +32 -0
- package/dist/marks.d.ts.map +1 -0
- package/dist/query-operations.d.ts +1 -6
- package/dist/query-operations.d.ts.map +1 -1
- package/dist/response-marks.d.ts +10 -0
- package/dist/response-marks.d.ts.map +1 -0
- package/dist/server-variables.d.ts +10 -0
- package/dist/server-variables.d.ts.map +1 -0
- package/dist/shared-components.d.ts +11 -0
- package/dist/shared-components.d.ts.map +1 -0
- package/docs/README.md +1 -1
- package/docs/guide/emitter.md +147 -1
- package/docs/roadmap.md +8 -0
- package/docs/troubleshooting.md +92 -0
- package/lib/components.tsp +65 -0
- package/lib/main.tsp +1 -0
- package/package.json +2 -2
package/docs/guide/emitter.md
CHANGED
|
@@ -2,7 +2,9 @@
|
|
|
2
2
|
|
|
3
3
|
`@nxgt/typespec` is a TypeSpec emitter as well as a library: it compiles a
|
|
4
4
|
spec to OpenAPI with `@typespec/openapi3`, and writes each `@queryMethod` as a
|
|
5
|
-
real `QUERY` when the document is OpenAPI 3.2.
|
|
5
|
+
real `QUERY` when the document is OpenAPI 3.2. It also emits a shared
|
|
6
|
+
response or header once under `components`, and `servers` on one path
|
|
7
|
+
([below](#shared-responses-shared-headers-and-path-servers)).
|
|
6
8
|
|
|
7
9
|
## The smallest setup
|
|
8
10
|
|
|
@@ -98,6 +100,150 @@ where `@typespec/openapi3` wrote `tsp-output/@typespec/openapi3`. Set
|
|
|
98
100
|
`@typespec/openapi3` is loaded only when emitting, so a spec that only
|
|
99
101
|
imports the library, `tsp compile --no-emit`, does not need it installed.
|
|
100
102
|
|
|
103
|
+
## Shared responses, shared headers and path servers
|
|
104
|
+
|
|
105
|
+
`@typespec/openapi3` writes every response and header in full at each
|
|
106
|
+
operation, and `@server` only reaches the document's `servers`. Three
|
|
107
|
+
decorators ask the emitter for the rest, with no post-processing of the JSON.
|
|
108
|
+
They need this emitter: with `@typespec/openapi3` in `emit` each is the error
|
|
109
|
+
`shared-needs-nxgt-emitter`, since it would write the copies or drop the
|
|
110
|
+
servers without a word.
|
|
111
|
+
|
|
112
|
+
```tsp
|
|
113
|
+
import "@typespec/http";
|
|
114
|
+
import "@nxgt/typespec";
|
|
115
|
+
|
|
116
|
+
using Http;
|
|
117
|
+
using Nxgt;
|
|
118
|
+
|
|
119
|
+
@service(#{ title: "Example" })
|
|
120
|
+
@server("https://api.example.com", "Main")
|
|
121
|
+
namespace Example;
|
|
122
|
+
|
|
123
|
+
model NoStore {
|
|
124
|
+
@sharedHeader("NoStore")
|
|
125
|
+
@header("cache-control")
|
|
126
|
+
cacheControl: "no-store";
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
@sharedResponse
|
|
130
|
+
@error
|
|
131
|
+
model Unauthorized {
|
|
132
|
+
...NoStore;
|
|
133
|
+
@statusCode code: 401;
|
|
134
|
+
message: string;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
@route("/items") @get op listItems(): { @body items: string[] } | Unauthorized;
|
|
138
|
+
|
|
139
|
+
@route("/users")
|
|
140
|
+
@pathServer("https://users.example.com", "Users")
|
|
141
|
+
@get op listUsers(): { @body users: string[] } | Unauthorized;
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
```yaml
|
|
145
|
+
# openapi.yaml (extract)
|
|
146
|
+
paths:
|
|
147
|
+
/items:
|
|
148
|
+
get:
|
|
149
|
+
responses:
|
|
150
|
+
'401':
|
|
151
|
+
$ref: '#/components/responses/Unauthorized'
|
|
152
|
+
/users:
|
|
153
|
+
servers:
|
|
154
|
+
- url: https://users.example.com
|
|
155
|
+
description: Users
|
|
156
|
+
get:
|
|
157
|
+
responses:
|
|
158
|
+
'401':
|
|
159
|
+
$ref: '#/components/responses/Unauthorized'
|
|
160
|
+
components:
|
|
161
|
+
headers:
|
|
162
|
+
NoStore:
|
|
163
|
+
required: true
|
|
164
|
+
schema: { type: string, enum: [no-store] }
|
|
165
|
+
responses:
|
|
166
|
+
Unauthorized:
|
|
167
|
+
description: Access is unauthorized.
|
|
168
|
+
headers:
|
|
169
|
+
cache-control:
|
|
170
|
+
$ref: '#/components/headers/NoStore'
|
|
171
|
+
content:
|
|
172
|
+
application/json:
|
|
173
|
+
schema:
|
|
174
|
+
$ref: '#/components/schemas/Unauthorized'
|
|
175
|
+
servers:
|
|
176
|
+
- url: https://api.example.com
|
|
177
|
+
description: Main
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
| Decorator | On | Result |
|
|
181
|
+
| --- | --- | --- |
|
|
182
|
+
| `@sharedResponse(name?)` | a response model | each reply of that model, whatever the operation, is one entry of `components.responses` (named after the model unless `name` is given) and a `$ref` at each use |
|
|
183
|
+
| `@sharedHeader(name?)` | a model property that is a `@header` | the header is one entry of `components.headers` (named after the property unless `name` is given) and a `$ref` at each response that carries it |
|
|
184
|
+
| `@pathServer(url, description?, parameters?)` | an operation | `servers` on the operation's path item; the other paths keep the document's. Repeated on the operations of one path, the servers add up |
|
|
185
|
+
|
|
186
|
+
What to know:
|
|
187
|
+
|
|
188
|
+
- **A response header only.** `components.headers` holds response headers.
|
|
189
|
+
`@sharedHeader` on a request `@header` or on a body property is the error
|
|
190
|
+
`shared-header-misplaced`; the mark never stays in the document.
|
|
191
|
+
- **A shared response is the whole reply**: description, headers and content.
|
|
192
|
+
A model with several statuses (`@statusCode code: 400 | 404`) is one
|
|
193
|
+
component per status, `Name400` and `Name404`, since their descriptions
|
|
194
|
+
differ.
|
|
195
|
+
Two shared responses, or two shared headers, of one name that differ are the
|
|
196
|
+
error `shared-component-conflict`: the first is the component, the other
|
|
197
|
+
stays inline. A name is letters,
|
|
198
|
+
digits, `.`, `_` and `-` (`invalid-component-name` otherwise).
|
|
199
|
+
- **A `@pathServer` is for a path item, not an operation**: `get` and `post`
|
|
200
|
+
of one route share it. The same server on several operations of one path is
|
|
201
|
+
written once; one URL with a different description or variables on another
|
|
202
|
+
is the error `path-server-conflict`, and the first is the one emitted.
|
|
203
|
+
- **Server variables** are `@server`'s third parameter: each property is a
|
|
204
|
+
variable, its default the variable's `default` (`""` without one), its
|
|
205
|
+
`@doc` the `description`, and a literal, enum or union of literals type its
|
|
206
|
+
`enum`. As with `@server`, a variable is a string (a string, a string enum
|
|
207
|
+
or a union of strings: `path-server-invalid-variable` otherwise), and each
|
|
208
|
+
`{name}` of the URL needs its property (`path-server-missing-variable`).
|
|
209
|
+
|
|
210
|
+
```tsp
|
|
211
|
+
@route("/jmap")
|
|
212
|
+
@pathServer("{origin}{basePath}", "Mail", {
|
|
213
|
+
@doc("The mail origin") origin: string = "https://mail.example.com",
|
|
214
|
+
@doc("Where it is mounted") basePath: string = "/jmap",
|
|
215
|
+
})
|
|
216
|
+
@get op session(): string;
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
```yaml
|
|
220
|
+
/jmap:
|
|
221
|
+
servers:
|
|
222
|
+
- url: '{origin}{basePath}'
|
|
223
|
+
description: Mail
|
|
224
|
+
variables:
|
|
225
|
+
origin:
|
|
226
|
+
default: https://mail.example.com
|
|
227
|
+
description: The mail origin
|
|
228
|
+
basePath:
|
|
229
|
+
default: /jmap
|
|
230
|
+
description: Where it is mounted
|
|
231
|
+
```
|
|
232
|
+
- **Several services** keep their own: each decorator marks the object the
|
|
233
|
+
emitter writes for its own operation or header, and the emitter reads and
|
|
234
|
+
takes off those marks per document, so the same path in two services, a
|
|
235
|
+
dotted service name or a nested service never mixes them.
|
|
236
|
+
- **Operations of a `@sharedRoute`** merge into one in the document; their
|
|
237
|
+
servers add up, and each one's shared responses are hoisted.
|
|
238
|
+
- **`@typespec/versioning` is not verified.** A shared response is marked on
|
|
239
|
+
the operation at emit time, and nothing here tests a versioned service,
|
|
240
|
+
whose operations the versioning library clones: shared responses on a
|
|
241
|
+
versioned service may stay inline, and a `@pathServer`'s servers may be missing.
|
|
242
|
+
- **Nothing changes without them.** A spec that uses none of the three emits
|
|
243
|
+
byte-identical output, and the hoisting is the same in 3.1 and 3.2.
|
|
244
|
+
- `@nxgt/openapi-codegen` generates a document with `$ref` responses and
|
|
245
|
+
headers as it does the inline copies.
|
|
246
|
+
|
|
101
247
|
## What follows downstream
|
|
102
248
|
|
|
103
249
|
| Step | What it does with the `query` operation |
|
package/docs/roadmap.md
CHANGED
|
@@ -25,6 +25,14 @@ Nothing planned beyond what is under Later.
|
|
|
25
25
|
|
|
26
26
|
## Shipped
|
|
27
27
|
|
|
28
|
+
### 0.14.0
|
|
29
|
+
|
|
30
|
+
- **Shared responses, shared headers and path servers.** `@sharedResponse`
|
|
31
|
+
and `@sharedHeader` emit a response and a header once under
|
|
32
|
+
`components.responses` and `components.headers`, a `$ref` at each use, and
|
|
33
|
+
`@pathServer` puts `servers` on one path item, from the spec alone with no
|
|
34
|
+
post-processing, with `@server`'s variables.
|
|
35
|
+
|
|
28
36
|
### 0.13.2
|
|
29
37
|
|
|
30
38
|
- **Exact scalar rules in the generated code.** `@nxgt/openapi-codegen`
|
package/docs/troubleshooting.md
CHANGED
|
@@ -361,6 +361,98 @@ options:
|
|
|
361
361
|
openapi-versions: ['3.2.0']
|
|
362
362
|
```
|
|
363
363
|
|
|
364
|
+
## `The server URL https://{tenant}.example.com of listUsers has {tenant}, which is not a property of its parameters. Add the variable, or take the braces out.`
|
|
365
|
+
|
|
366
|
+
**When:** `tsp compile`; the error `path-server-missing-variable` on a
|
|
367
|
+
`@pathServer` whose URL has a `{name}` its third parameter lacks.
|
|
368
|
+
|
|
369
|
+
**Fix:** add the variable, as `@server` asks:
|
|
370
|
+
|
|
371
|
+
```tsp
|
|
372
|
+
@pathServer("https://{tenant}.example.com", "Tenant", { tenant: string = "main" })
|
|
373
|
+
```
|
|
374
|
+
|
|
375
|
+
## `The variable port of the @pathServer of listUsers is not a string, a string enum or a union of strings: an OpenAPI server variable is a string.`
|
|
376
|
+
|
|
377
|
+
**When:** `tsp compile`; the error `path-server-invalid-variable`, for a
|
|
378
|
+
variable typed `int32`, `boolean`… The default of a server variable is a string.
|
|
379
|
+
|
|
380
|
+
**Fix:** type it as a string: `port: string = "443"`, or `"443" | "8443"`.
|
|
381
|
+
|
|
382
|
+
## `Trace is marked @sharedHeader outside a response header: it applies to a @header of a reply only, not to a request header or a body property.`
|
|
383
|
+
|
|
384
|
+
**When:** `tsp compile`; the error `shared-header-misplaced`, for a
|
|
385
|
+
`@sharedHeader` property that ends up in a request (a `@header` parameter) or
|
|
386
|
+
in a body schema.
|
|
387
|
+
|
|
388
|
+
**Why:** `components.headers` holds response headers; a request header is a
|
|
389
|
+
parameter and a body property is a schema.
|
|
390
|
+
|
|
391
|
+
**Fix:** take `@sharedHeader` off, or keep it on a `@header` of a reply model.
|
|
392
|
+
|
|
393
|
+
## `Operations sharing the route GET /m reply 400 with different responses, one of them a @sharedResponse: @sharedRoute merges them into one reply, which cannot be a component. Give them the same shared response, or none.`
|
|
394
|
+
|
|
395
|
+
**When:** `tsp compile`; the error `shared-response-merged`, for operations
|
|
396
|
+
with `@sharedRoute` that answer one status with different responses (two shared
|
|
397
|
+
ones of different names, or a shared one and a plain one).
|
|
398
|
+
|
|
399
|
+
**Why:** the operations are one in the document, with one reply per status,
|
|
400
|
+
which cannot be a component and something else.
|
|
401
|
+
|
|
402
|
+
**Fix:** answer that status with the same `@sharedResponse` in each, or with
|
|
403
|
+
none:
|
|
404
|
+
|
|
405
|
+
```tsp
|
|
406
|
+
@route("/m") @sharedRoute @get op one(): string | Gone;
|
|
407
|
+
@route("/m") @sharedRoute @get op two(): string | Gone;
|
|
408
|
+
```
|
|
409
|
+
|
|
410
|
+
## `Two shared responses named Failure differ: a component is emitted once. Give each its own name, or make them the same.`
|
|
411
|
+
|
|
412
|
+
**When:** `tsp compile`; the error `shared-component-conflict`, for two
|
|
413
|
+
`@sharedResponse` models (or two `@sharedHeader` properties, as `headers`) that
|
|
414
|
+
take one name and do not emit the same response or header.
|
|
415
|
+
|
|
416
|
+
**Why:** a component has one definition, and the emitter will not pick one.
|
|
417
|
+
The first is the component; the other stays inline in the document.
|
|
418
|
+
|
|
419
|
+
**Fix:** give each its own name, or make them one model:
|
|
420
|
+
|
|
421
|
+
```tsp
|
|
422
|
+
@sharedResponse("BadInput") @error model A { @statusCode code: 400; a: string; }
|
|
423
|
+
@sharedResponse("Refused") @error model B { @statusCode code: 400; b: string; }
|
|
424
|
+
```
|
|
425
|
+
|
|
426
|
+
## `Two @pathServer of /users give the server https://a.example.com a description or variables that differ. Give the path one definition of it.`
|
|
427
|
+
|
|
428
|
+
**When:** `tsp compile`; the error `path-server-conflict`. The first definition is emitted.
|
|
429
|
+
|
|
430
|
+
**Why:** operations of one path share its `servers`; the same URL cannot be
|
|
431
|
+
described two ways. The first definition is the one emitted.
|
|
432
|
+
|
|
433
|
+
**Fix:** give every `@pathServer` of that URL on the path the same
|
|
434
|
+
description and variables, or put it on one operation.
|
|
435
|
+
|
|
436
|
+
## `"not ok" is not a name OpenAPI allows for a component: use letters, digits, '.', '_' and '-'.`
|
|
437
|
+
|
|
438
|
+
**When:** `tsp compile`; the error `invalid-component-name`, on a name given to
|
|
439
|
+
`@sharedResponse` or `@sharedHeader` (or a model or property name used as one).
|
|
440
|
+
|
|
441
|
+
**Fix:** pass a name of letters, digits, `.`, `_` and `-`:
|
|
442
|
+
`@sharedResponse("NotOk")`.
|
|
443
|
+
|
|
444
|
+
## `Unauthorized is marked @sharedResponse: emit with @nxgt/typespec, which writes it; @typespec/openapi3 would write it inline or drop it.`
|
|
445
|
+
|
|
446
|
+
**When:** `tsp compile` with `@typespec/openapi3` in `emit`; the error
|
|
447
|
+
`shared-needs-nxgt-emitter` for each `@sharedResponse`, `@sharedHeader` and
|
|
448
|
+
`@pathServer`.
|
|
449
|
+
|
|
450
|
+
**Why:** only the emitter `@nxgt/typespec` reads them: `@typespec/openapi3`
|
|
451
|
+
would write the copies, leak `x-nxgt-component` on a header, or leave out the
|
|
452
|
+
path's `servers`.
|
|
453
|
+
|
|
454
|
+
**Fix:** emit with `@nxgt/typespec` ([The emitter](guide/emitter.md)).
|
|
455
|
+
|
|
364
456
|
## `The emitter @nxgt/typespec runs @typespec/openapi3, which is not installed: add it beside @nxgt/typespec.`
|
|
365
457
|
|
|
366
458
|
**When:** `tsp compile` with `@nxgt/typespec` in `emit`; the error
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import "../dist/index.js";
|
|
2
|
+
|
|
3
|
+
using TypeSpec.Reflection;
|
|
4
|
+
|
|
5
|
+
namespace Nxgt;
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Emits a response model once, under `components.responses`, with a `$ref`
|
|
9
|
+
* at each operation that returns it, instead of a full copy at each. The
|
|
10
|
+
* name is the model's unless one is given. Two shared responses of one name
|
|
11
|
+
* that differ are the error `shared-component-conflict`. Only
|
|
12
|
+
* `@nxgt/typespec` writes it: `@typespec/openapi3` would write the copies.
|
|
13
|
+
*
|
|
14
|
+
* @example
|
|
15
|
+
* ```typespec
|
|
16
|
+
* @sharedResponse
|
|
17
|
+
* @error model Unauthorized { @statusCode code: 401; message: string; }
|
|
18
|
+
* ```
|
|
19
|
+
*/
|
|
20
|
+
extern dec sharedResponse(target: Model, name?: valueof string);
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Emits a response header once, under `components.headers`, with a `$ref`
|
|
24
|
+
* at each response that carries it. The name is the property's unless one is
|
|
25
|
+
* given. For a response header only; a request header is a parameter.
|
|
26
|
+
*
|
|
27
|
+
* @example
|
|
28
|
+
* ```typespec
|
|
29
|
+
* model NoStore {
|
|
30
|
+
* @sharedHeader("NoStore") @header("cache-control") cacheControl: "no-store";
|
|
31
|
+
* }
|
|
32
|
+
* ```
|
|
33
|
+
*/
|
|
34
|
+
extern dec sharedHeader(target: ModelProperty, name?: valueof string);
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Puts `servers` on the path item of the operation: the other paths keep the
|
|
38
|
+
* document's. Repeated on the operations of one path, the servers add up; the
|
|
39
|
+
* same server twice is one, and one URL with two different definitions is the
|
|
40
|
+
* error `path-server-conflict`. The third parameter is `@server`'s: each
|
|
41
|
+
* property is a variable of the URL, its default the variable's `default`
|
|
42
|
+
* and its doc comment the `description`.
|
|
43
|
+
*
|
|
44
|
+
* @param url The server's URL, with `{variable}` placeholders if any.
|
|
45
|
+
* @param description What the server is.
|
|
46
|
+
* @param parameters The URL's variables.
|
|
47
|
+
*
|
|
48
|
+
* @example
|
|
49
|
+
* ```typespec
|
|
50
|
+
* @route("/files")
|
|
51
|
+
* @pathServer("{origin}{basePath}", "Files", {
|
|
52
|
+
* @doc("The server's origin")
|
|
53
|
+
* origin: string = "https://files.example.com",
|
|
54
|
+
* @doc("Where the API is mounted")
|
|
55
|
+
* basePath: string = "/files",
|
|
56
|
+
* })
|
|
57
|
+
* @get op listFiles(): string[];
|
|
58
|
+
* ```
|
|
59
|
+
*/
|
|
60
|
+
extern dec pathServer(
|
|
61
|
+
target: Operation,
|
|
62
|
+
url: valueof string,
|
|
63
|
+
description?: valueof string,
|
|
64
|
+
parameters?: Record<unknown>
|
|
65
|
+
);
|
package/lib/main.tsp
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nxgt/typespec",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.14.0",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -61,7 +61,7 @@
|
|
|
61
61
|
}
|
|
62
62
|
},
|
|
63
63
|
"devDependencies": {
|
|
64
|
-
"@nxgt/openapi-codegen": "^0.
|
|
64
|
+
"@nxgt/openapi-codegen": "^0.11.0",
|
|
65
65
|
"@nxgt/openapi-hono": "^0.3.3",
|
|
66
66
|
"@nxgt/zod": "^0.1.3",
|
|
67
67
|
"@types/bun": "^1.4.0",
|