@nxgt/typespec 0.6.1 → 0.8.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 +72 -3
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +123 -1
- package/dist/index.js.map +10 -5
- package/dist/linter.d.ts +2 -0
- package/dist/linter.d.ts.map +1 -0
- package/dist/operation-ids.d.ts +9 -0
- package/dist/operation-ids.d.ts.map +1 -1
- package/dist/rules/error-body-shape.d.ts +4 -0
- package/dist/rules/error-body-shape.d.ts.map +1 -0
- package/dist/rules/list-returns-page.d.ts +4 -0
- package/dist/rules/list-returns-page.d.ts.map +1 -0
- package/dist/rules/service-operation-ids.d.ts +4 -0
- package/dist/rules/service-operation-ids.d.ts.map +1 -0
- package/dist/rules/written.d.ts +8 -0
- package/dist/rules/written.d.ts.map +1 -0
- package/docs/README.md +1 -0
- package/docs/guide/errors.md +65 -0
- package/docs/guide/linter.md +257 -0
- package/docs/roadmap.md +13 -1
- package/docs/troubleshooting.md +142 -0
- package/lib/errors.tsp +28 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -131,6 +131,24 @@ The generator also declares the validators' own 400, `ValidationErrorBody`,
|
|
|
131
131
|
on every operation that takes a parameter or a body. With `BadRequest`, a
|
|
132
132
|
400 reply is typed `BadRequestBody | ValidationErrorBody`.
|
|
133
133
|
|
|
134
|
+
Each verb has an alias for the errors it usually answers, so a resource's
|
|
135
|
+
operations do not repeat the same unions. Combine them, and add a response
|
|
136
|
+
an alias leaves out:
|
|
137
|
+
|
|
138
|
+
```tsp
|
|
139
|
+
@route("/users")
|
|
140
|
+
interface Users {
|
|
141
|
+
@get list(...PageParameters): Page<User> | ListErrors;
|
|
142
|
+
@get get(@path id: uuid): User | GetErrors;
|
|
143
|
+
@post create(@body user: User): User | CreateErrors | AuthErrors | TooManyRequests;
|
|
144
|
+
@patch update(@path id: uuid, @body user: User): User | UpdateErrors;
|
|
145
|
+
@delete delete(@path id: uuid): NoContentResponse | DeleteErrors;
|
|
146
|
+
}
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
An alias is exactly its union, so the OpenAPI it emits is the same as
|
|
150
|
+
writing the responses out.
|
|
151
|
+
|
|
134
152
|
### Pagination
|
|
135
153
|
|
|
136
154
|
Spread the query of a page into the operation's parameters, and answer the
|
|
@@ -363,6 +381,34 @@ The verbs `verbs` adds are not checked, nor an operation with its own
|
|
|
363
381
|
`@operationId`. More, with each verb's methods, in
|
|
364
382
|
[Operation ids](docs/guide/operation-ids.md).
|
|
365
383
|
|
|
384
|
+
### Linter
|
|
385
|
+
|
|
386
|
+
Extend the library's ruleset in `tspconfig.yaml`, and `tsp compile` warns
|
|
387
|
+
where the spec strays from these conventions. Nothing runs without it:
|
|
388
|
+
|
|
389
|
+
```yaml
|
|
390
|
+
# api/tspconfig.yaml
|
|
391
|
+
linter:
|
|
392
|
+
extends:
|
|
393
|
+
- '@nxgt/typespec/recommended'
|
|
394
|
+
```
|
|
395
|
+
|
|
396
|
+
| Rule | Warns when | Fix |
|
|
397
|
+
| --- | --- | --- |
|
|
398
|
+
| `list-returns-page` | an operation named `list`, `search` or `query`, alone or before a capital letter (`listUsers`, not `listing`), answers an array | return `Page<Item>` or `CursorPage<Item>` |
|
|
399
|
+
| `service-operation-ids` | a `@service` namespace has no `@operationIds`, on itself or on a namespace around it | mark the namespace with `@operationIds` |
|
|
400
|
+
| `error-body-shape` | a reply of status 400 or above, or `default`, has a body without `status`, `message` and `timestamp` | declare one of the library's errors, or a body that spreads or extends `ErrorBody<Status>` |
|
|
401
|
+
|
|
402
|
+
```text
|
|
403
|
+
warning @nxgt/typespec/list-returns-page: listAll returns an array: return Page<Item> or CursorPage<Item>, which can carry a total or a next cursor.
|
|
404
|
+
```
|
|
405
|
+
|
|
406
|
+
A reply without a body, such as `AuthenticationRequired`, passes
|
|
407
|
+
`error-body-shape`. Turn a rule off for the spec under `linter: disable:`
|
|
408
|
+
with a reason, or for one operation with
|
|
409
|
+
`#suppress "@nxgt/typespec/<rule>" "<reason>"`. More, with a failing and a
|
|
410
|
+
passing example of each rule, in [Linter](docs/guide/linter.md).
|
|
411
|
+
|
|
366
412
|
## API
|
|
367
413
|
|
|
368
414
|
### Decorators
|
|
@@ -385,6 +431,17 @@ overrides verbs, on a namespace or an interface.
|
|
|
385
431
|
| `merged-status-reply` | warning: an operation declares two replies with a body of one status code, which the emitter merges under the first one's description |
|
|
386
432
|
| `verb-method-mismatch` | warning: an operation `@operationIds` names after one of the library's verbs, in an interface, is sent with a method that verb does not name: `create` with a `GET` |
|
|
387
433
|
|
|
434
|
+
### Linter rules
|
|
435
|
+
|
|
436
|
+
In the ruleset `@nxgt/typespec/recommended`; each is a warning, and named
|
|
437
|
+
`@nxgt/typespec/<rule>` in `tspconfig.yaml` and `#suppress`.
|
|
438
|
+
|
|
439
|
+
| Rule | Reported when |
|
|
440
|
+
| --- | --- |
|
|
441
|
+
| `list-returns-page` | an operation named `list`, `search` or `query`, alone or before a capital letter, has a success reply whose body is an array |
|
|
442
|
+
| `service-operation-ids` | a `@service` namespace is not marked with `@operationIds`, itself or by a namespace around it |
|
|
443
|
+
| `error-body-shape` | a reply of status 400 or above, of a range from 400 up, or `default`, has a body that is not a model with `status`, `message` and `timestamp` |
|
|
444
|
+
|
|
388
445
|
### Models
|
|
389
446
|
|
|
390
447
|
| Model | What it is |
|
|
@@ -445,6 +502,15 @@ envelope and the rate limit's headers.
|
|
|
445
502
|
`@error` model with its `@statusCode` and body, and
|
|
446
503
|
`ErrorResponse<Status>` for any other status.
|
|
447
504
|
|
|
505
|
+
| Alias | What it is |
|
|
506
|
+
| --- | --- |
|
|
507
|
+
| `ListErrors` | `BadRequest` |
|
|
508
|
+
| `GetErrors` | `NotFound` |
|
|
509
|
+
| `CreateErrors` | `BadRequest \| Conflict` |
|
|
510
|
+
| `UpdateErrors` | `BadRequest \| NotFound \| Conflict` |
|
|
511
|
+
| `DeleteErrors` | `NotFound` |
|
|
512
|
+
| `AuthErrors` | `Unauthorized \| Forbidden`, with the envelope; a route that answers without a body declares `AuthenticationRequired \| AccessDenied` instead |
|
|
513
|
+
|
|
448
514
|
## Traps
|
|
449
515
|
|
|
450
516
|
- **Do not declare a schema named `ValidationErrorBody`.** The generator
|
|
@@ -466,7 +532,8 @@ envelope and the rate limit's headers.
|
|
|
466
532
|
- **One status, one reply.** `@typespec/openapi3` merges two replies of one
|
|
467
533
|
status into one. With `AuthenticationRequired` and `Unauthorized`, the 401
|
|
468
534
|
keeps the envelope and the reply without a body is lost: the library
|
|
469
|
-
refuses it, `duplicate-status-reply`. With `Conflict`
|
|
535
|
+
refuses it, `duplicate-status-reply`. With `Conflict` (which
|
|
536
|
+
`CreateErrors` and `UpdateErrors` hold) beside
|
|
470
537
|
`IdempotencyInProgress`, or `UnprocessableEntity` beside
|
|
471
538
|
`IdempotencyKeyReused`, both bodies stay, under the first reply's
|
|
472
539
|
description: the library warns, `merged-status-reply`. Declare the one the
|
|
@@ -503,8 +570,8 @@ envelope and the rate limit's headers.
|
|
|
503
570
|
|
|
504
571
|
## Documentation
|
|
505
572
|
|
|
506
|
-
- [Error replies](docs/guide/errors.md): the envelope, each response,
|
|
507
|
-
what the generator makes of them;
|
|
573
|
+
- [Error replies](docs/guide/errors.md): the envelope, each response, the
|
|
574
|
+
aliases by verb, and what the generator makes of them;
|
|
508
575
|
- [Pagination](docs/guide/pagination.md): offset and cursor pages, and
|
|
509
576
|
what a handler answers;
|
|
510
577
|
- [Authentication](docs/guide/auth.md): `JanusAuth`, and the guards'
|
|
@@ -518,6 +585,8 @@ envelope and the rate limit's headers.
|
|
|
518
585
|
- [Operation ids](docs/guide/operation-ids.md): `@operationIds`, each
|
|
519
586
|
operation named as written or a verb with its resource, the method each
|
|
520
587
|
verb is sent with, the options, and the ids it refuses;
|
|
588
|
+
- [Linter](docs/guide/linter.md): the ruleset `@nxgt/typespec/recommended`,
|
|
589
|
+
each rule with a failing and a passing spec, and how to turn one off;
|
|
521
590
|
- [troubleshooting](docs/troubleshooting.md);
|
|
522
591
|
- [the roadmap](docs/roadmap.md): what is coming.
|
|
523
592
|
|
package/dist/index.d.ts
CHANGED
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
import type { Program } from '@typespec/compiler';
|
|
10
10
|
import { operationIds } from './operation-ids';
|
|
11
11
|
export { $lib } from './lib';
|
|
12
|
+
export { $linter } from './linter';
|
|
12
13
|
export declare function $onValidate(program: Program): void;
|
|
13
14
|
export declare const $decorators: {
|
|
14
15
|
Nxgt: {
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AACH,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,oBAAoB,CAAC;AAElD,OAAO,EAAE,YAAY,EAAwB,MAAM,iBAAiB,CAAC;AAGrE,OAAO,EAAE,IAAI,EAAE,MAAM,OAAO,CAAC;
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AACH,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,oBAAoB,CAAC;AAElD,OAAO,EAAE,YAAY,EAAwB,MAAM,iBAAiB,CAAC;AAGrE,OAAO,EAAE,IAAI,EAAE,MAAM,OAAO,CAAC;AAC7B,OAAO,EAAE,OAAO,EAAE,MAAM,UAAU,CAAC;AAEnC,wBAAgB,WAAW,CAAC,OAAO,EAAE,OAAO,GAAG,IAAI,CAMlD;AAED,eAAO,MAAM,WAAW;;;;CAEvB,CAAC"}
|
package/dist/index.js
CHANGED
|
@@ -351,6 +351,127 @@ function validateVerbMethods(program) {
|
|
|
351
351
|
}
|
|
352
352
|
}
|
|
353
353
|
}
|
|
354
|
+
// src/linter.ts
|
|
355
|
+
import { defineLinter } from "@typespec/compiler";
|
|
356
|
+
|
|
357
|
+
// src/rules/error-body-shape.ts
|
|
358
|
+
import {
|
|
359
|
+
createRule,
|
|
360
|
+
isVoidType as isVoidType2,
|
|
361
|
+
paramMessage as paramMessage2,
|
|
362
|
+
walkPropertiesInherited
|
|
363
|
+
} from "@typespec/compiler";
|
|
364
|
+
import { getHttpOperation } from "@typespec/http";
|
|
365
|
+
|
|
366
|
+
// src/rules/written.ts
|
|
367
|
+
import { isTemplateDeclaration } from "@typespec/compiler";
|
|
368
|
+
function isWritten(operation) {
|
|
369
|
+
const container = operation.interface;
|
|
370
|
+
return isEmitted(operation) && !isTemplateDeclaration(operation) && (container === undefined || !isTemplateDeclaration(container));
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
// src/rules/error-body-shape.ts
|
|
374
|
+
var ENVELOPE = ["status", "message", "timestamp"];
|
|
375
|
+
function isEnvelope(body) {
|
|
376
|
+
const names = new Set([...walkPropertiesInherited(body)].map(({ name }) => name));
|
|
377
|
+
return ENVELOPE.every((name) => names.has(name));
|
|
378
|
+
}
|
|
379
|
+
function statusOf2(code) {
|
|
380
|
+
return typeof code === "object" ? `${code.start}-${code.end}` : `${code}`;
|
|
381
|
+
}
|
|
382
|
+
var errorBodyShape = createRule({
|
|
383
|
+
name: "error-body-shape",
|
|
384
|
+
severity: "warning",
|
|
385
|
+
description: "An error reply has the nxgt envelope: status, message and timestamp.",
|
|
386
|
+
messages: {
|
|
387
|
+
default: paramMessage2`${"operation"} answers ${"status"} with a body that is not the nxgt envelope: declare BadRequest, NotFound or another of the library's errors, or a body that spreads or extends ErrorBody<Status>.`
|
|
388
|
+
},
|
|
389
|
+
create: (context) => ({
|
|
390
|
+
operation(operation) {
|
|
391
|
+
if (!isWritten(operation))
|
|
392
|
+
return;
|
|
393
|
+
const [http] = getHttpOperation(context.program, operation);
|
|
394
|
+
for (const { statusCodes, responses } of http.responses) {
|
|
395
|
+
const error = statusCodes === "*" || (typeof statusCodes === "number" ? statusCodes >= 400 : statusCodes.start >= 400);
|
|
396
|
+
if (!error)
|
|
397
|
+
continue;
|
|
398
|
+
const wrong = responses.some(({ body }) => {
|
|
399
|
+
if (body === undefined || isVoidType2(body.type))
|
|
400
|
+
return false;
|
|
401
|
+
return body.type.kind !== "Model" || !isEnvelope(body.type);
|
|
402
|
+
});
|
|
403
|
+
if (!wrong)
|
|
404
|
+
continue;
|
|
405
|
+
context.reportDiagnostic({
|
|
406
|
+
format: { operation: operation.name, status: statusOf2(statusCodes) },
|
|
407
|
+
target: operation
|
|
408
|
+
});
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
})
|
|
412
|
+
});
|
|
413
|
+
|
|
414
|
+
// src/rules/list-returns-page.ts
|
|
415
|
+
import { createRule as createRule2, isArrayModelType, paramMessage as paramMessage3 } from "@typespec/compiler";
|
|
416
|
+
import { getHttpOperation as getHttpOperation2 } from "@typespec/http";
|
|
417
|
+
var LIST = /^(list|search|query)(?=[A-Z]|$)/;
|
|
418
|
+
var listReturnsPage = createRule2({
|
|
419
|
+
name: "list-returns-page",
|
|
420
|
+
severity: "warning",
|
|
421
|
+
description: "A list, search or query returns Page<Item> or CursorPage<Item>.",
|
|
422
|
+
messages: {
|
|
423
|
+
default: paramMessage3`${"operation"} returns an array: return Page<Item> or CursorPage<Item>, which can carry a total or a next cursor.`
|
|
424
|
+
},
|
|
425
|
+
create: (context) => ({
|
|
426
|
+
operation(operation) {
|
|
427
|
+
if (!isWritten(operation) || !LIST.test(operation.name))
|
|
428
|
+
return;
|
|
429
|
+
const [http] = getHttpOperation2(context.program, operation);
|
|
430
|
+
const array = http.responses.some(({ statusCodes, responses }) => typeof statusCodes === "number" && statusCodes >= 200 && statusCodes < 300 && responses.some(({ body }) => body?.type.kind === "Model" && isArrayModelType(body.type)));
|
|
431
|
+
if (!array)
|
|
432
|
+
return;
|
|
433
|
+
context.reportDiagnostic({
|
|
434
|
+
format: { operation: operation.name },
|
|
435
|
+
target: operation
|
|
436
|
+
});
|
|
437
|
+
}
|
|
438
|
+
})
|
|
439
|
+
});
|
|
440
|
+
|
|
441
|
+
// src/rules/service-operation-ids.ts
|
|
442
|
+
import { createRule as createRule3, isService as isService2, paramMessage as paramMessage4 } from "@typespec/compiler";
|
|
443
|
+
var serviceOperationIds = createRule3({
|
|
444
|
+
name: "service-operation-ids",
|
|
445
|
+
severity: "warning",
|
|
446
|
+
description: "A service namespace marks its operation ids with @operationIds.",
|
|
447
|
+
messages: {
|
|
448
|
+
default: paramMessage4`The service ${"service"} has no @operationIds: its ids are the emitter's, such as Users_list. Mark the namespace with @operationIds.`
|
|
449
|
+
},
|
|
450
|
+
create: (context) => ({
|
|
451
|
+
namespace(namespace) {
|
|
452
|
+
const { program } = context;
|
|
453
|
+
if (!isService2(program, namespace))
|
|
454
|
+
return;
|
|
455
|
+
if (isMarkedNamespace(program, namespace))
|
|
456
|
+
return;
|
|
457
|
+
context.reportDiagnostic({
|
|
458
|
+
format: { service: namespace.name },
|
|
459
|
+
target: namespace
|
|
460
|
+
});
|
|
461
|
+
}
|
|
462
|
+
})
|
|
463
|
+
});
|
|
464
|
+
|
|
465
|
+
// src/linter.ts
|
|
466
|
+
var rules = [listReturnsPage, serviceOperationIds, errorBodyShape];
|
|
467
|
+
var $linter = defineLinter({
|
|
468
|
+
rules,
|
|
469
|
+
ruleSets: {
|
|
470
|
+
recommended: {
|
|
471
|
+
enable: Object.fromEntries(rules.map(({ name }) => [`@nxgt/typespec/${name}`, true]))
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
});
|
|
354
475
|
|
|
355
476
|
// src/index.ts
|
|
356
477
|
function $onValidate(program) {
|
|
@@ -364,8 +485,9 @@ var $decorators = {
|
|
|
364
485
|
export {
|
|
365
486
|
$decorators,
|
|
366
487
|
$lib,
|
|
488
|
+
$linter,
|
|
367
489
|
$onValidate
|
|
368
490
|
};
|
|
369
491
|
|
|
370
|
-
//# debugId=
|
|
492
|
+
//# debugId=FB766460AB7EEFAD64756E2164756E21
|
|
371
493
|
//# sourceMappingURL=index.js.map
|
package/dist/index.js.map
CHANGED
|
@@ -1,16 +1,21 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
|
-
"sources": ["../src/one-reply-per-status.ts", "../src/lib.ts", "../src/operation-ids.ts", "../src/verbs.ts", "../src/resource.ts", "../src/verb-methods.ts", "../src/index.ts"],
|
|
3
|
+
"sources": ["../src/one-reply-per-status.ts", "../src/lib.ts", "../src/operation-ids.ts", "../src/verbs.ts", "../src/resource.ts", "../src/verb-methods.ts", "../src/linter.ts", "../src/rules/error-body-shape.ts", "../src/rules/written.ts", "../src/rules/list-returns-page.ts", "../src/rules/service-operation-ids.ts", "../src/index.ts"],
|
|
4
4
|
"sourcesContent": [
|
|
5
5
|
"/**\n * One reply per status: `@typespec/openapi3` merges two replies of one\n * status into one without a word. A reply without a body beside one with a\n * body is lost in the merge, an error; a `void` body is none, as the emitter\n * sees it. Two bodies of one content type are merged under the first reply's\n * description, a warning where the status is a code. Under `*` or a range,\n * where undocumented `@error` models share one description, they pass.\n * Bodies of different content types are one reply, negotiated, and pass.\n */\nimport { isVoidType, type Operation, type Program } from '@typespec/compiler';\nimport {\n\tgetAllHttpServices,\n\ttype HttpOperationResponse,\n\ttype HttpStatusCodesEntry,\n} from '@typespec/http';\nimport { $lib } from './lib';\n\nfunction statusOf(code: HttpStatusCodesEntry): string {\n\treturn typeof code === 'object' ? `${code.start}-${code.end}` : `${code}`;\n}\n\n/** What the emitter's merge costs: a reply, a description, or nothing. */\nfunction mergeOf({\n\tstatusCodes,\n\tresponses,\n}: HttpOperationResponse):\n\t| 'duplicate-status-reply'\n\t| 'merged-status-reply'\n\t| undefined {\n\tconst bodies = responses.flatMap(({ body }) =>\n\t\tbody && !isVoidType(body.type) ? [body] : [],\n\t);\n\tif (bodies.length > 0 && bodies.length < responses.length) {\n\t\treturn 'duplicate-status-reply';\n\t}\n\tif (typeof statusCodes !== 'number') return undefined;\n\tconst seen = new Set<string>();\n\tfor (const body of bodies) {\n\t\tif (body.contentTypes.some((type) => seen.has(type))) {\n\t\t\treturn 'merged-status-reply';\n\t\t}\n\t\tfor (const type of body.contentTypes) seen.add(type);\n\t}\n\treturn undefined;\n}\n\n/**\n * Checks each operation of each service, as the emitter sees it, once: a\n * service nested in another is listed in both. The services' own\n * diagnostics are the emitter's to report.\n */\nexport function validateOneReplyPerStatus(program: Program): void {\n\tconst [services] = getAllHttpServices(program);\n\tconst checked = new Set<Operation>();\n\tfor (const service of services) {\n\t\tfor (const operation of service.operations) {\n\t\t\tif (checked.has(operation.operation)) continue;\n\t\t\tchecked.add(operation.operation);\n\t\t\tfor (const response of operation.responses) {\n\t\t\t\tconst code = mergeOf(response);\n\t\t\t\tif (code === undefined) continue;\n\t\t\t\t$lib.reportDiagnostic(program, {\n\t\t\t\t\tcode,\n\t\t\t\t\tformat: {\n\t\t\t\t\t\toperation: operation.operation.name,\n\t\t\t\t\t\tstatus: statusOf(response.statusCodes),\n\t\t\t\t\t},\n\t\t\t\t\ttarget: operation.operation,\n\t\t\t\t});\n\t\t\t}\n\t\t}\n\t}\n}\n",
|
|
6
6
|
"/** The library's definition: its name, diagnostics and state keys. */\nimport { createTypeSpecLibrary, paramMessage } from '@typespec/compiler';\n\nexport const $lib = createTypeSpecLibrary({\n\tname: '@nxgt/typespec',\n\tdiagnostics: {\n\t\t'duplicate-operation-id': {\n\t\t\tseverity: 'error',\n\t\t\tmessages: {\n\t\t\t\tdefault: paramMessage`Two operations are named ${'id'}: an OpenAPI operation id must be unique.`,\n\t\t\t},\n\t\t},\n\t\t'resource-name-on-namespace': {\n\t\t\tseverity: 'error',\n\t\t\tmessages: {\n\t\t\t\tdefault:\n\t\t\t\t\t\"`singular` and `plural` name an interface's resource: a namespace has none. Put them on the interface.\",\n\t\t\t},\n\t\t},\n\t\t'verb-method-mismatch': {\n\t\t\tseverity: 'warning',\n\t\t\tmessages: {\n\t\t\t\tdefault: paramMessage`${'operation'} is sent with ${'method'}, where its verb ${'verb'} is sent with ${'expected'}. Give it that method, or a name that is not a verb.`,\n\t\t\t},\n\t\t},\n\t\t'duplicate-status-reply': {\n\t\t\tseverity: 'error',\n\t\t\tmessages: {\n\t\t\t\tdefault: paramMessage`${'operation'} declares a reply without a body and one with a body of status ${'status'}: the emitter merges them into one, and the reply without a body is lost. Declare the one the route sends.`,\n\t\t\t},\n\t\t},\n\t\t'merged-status-reply': {\n\t\t\tseverity: 'warning',\n\t\t\tmessages: {\n\t\t\t\tdefault: paramMessage`${'operation'} declares two replies with a body of status ${'status'}: the emitter merges their bodies under the first one's description. Declare the one the route sends.`,\n\t\t\t},\n\t\t},\n\t},\n\tstate: {\n\t\toperationIds: {\n\t\t\tdescription: 'The interfaces and namespaces marked with @operationIds',\n\t\t},\n\t\toperationIdsOptions: {\n\t\t\tdescription:\n\t\t\t\t\"Each marked interface's or namespace's @operationIds options\",\n\t\t},\n\t},\n});\n",
|
|
7
|
-
"/**\n * `@operationIds`: the id of each operation is its name, as written:\n * `@get findPostComments()` is `findPostComments`, where `@typespec/openapi3`\n * would write `Posts_findPostComments`. In an interface, a known verb takes\n * the interface's resource: `create` in `Users` is `createUser` (`verbs.ts`).\n */\nimport {\n\ttype DecoratorContext,\n\ttype Interface,\n\tisService,\n\tisTemplateInstance,\n\tlistServices,\n\ttype Namespace,\n\tnavigateProgram,\n\ttype Operation,\n\ttype Program,\n} from '@typespec/compiler';\nimport {\n\tgetOperationId,\n\tresolveOperationId,\n\tsetOperationId,\n} from '@typespec/openapi';\nimport { $lib } from './lib';\nimport { idOf, type OperationIdsOptions } from './resource';\n\n/**\n * Marks the interface or the namespace; its ids are set once the program is\n * checked. On a template, TypeSpec runs it on each instance, which\n * `isMarkedInterface` follows to the interfaces extending it. A namespace\n * has no resource to name: `singular` and `plural` are an interface's.\n */\nexport function operationIds(\n\tcontext: DecoratorContext,\n\ttarget: Interface | Namespace,\n\toptions: OperationIdsOptions = {},\n): void {\n\tconst { program } = context;\n\tprogram.stateSet($lib.stateKeys.operationIds).add(target);\n\tprogram.stateMap($lib.stateKeys.operationIdsOptions).set(target, options);\n\tif (\n\t\ttarget.kind === 'Namespace' &&\n\t\t(options.singular !== undefined || options.plural !== undefined)\n\t) {\n\t\t$lib.reportDiagnostic(program, {\n\t\t\tcode: 'resource-name-on-namespace',\n\t\t\ttarget: context.decoratorTarget,\n\t\t});\n\t}\n}\n\n/** Marked itself, or in a marked namespace, however deep. */\
|
|
7
|
+
"/**\n * `@operationIds`: the id of each operation is its name, as written:\n * `@get findPostComments()` is `findPostComments`, where `@typespec/openapi3`\n * would write `Posts_findPostComments`. In an interface, a known verb takes\n * the interface's resource: `create` in `Users` is `createUser` (`verbs.ts`).\n */\nimport {\n\ttype DecoratorContext,\n\ttype Interface,\n\tisService,\n\tisTemplateInstance,\n\tlistServices,\n\ttype Namespace,\n\tnavigateProgram,\n\ttype Operation,\n\ttype Program,\n} from '@typespec/compiler';\nimport {\n\tgetOperationId,\n\tresolveOperationId,\n\tsetOperationId,\n} from '@typespec/openapi';\nimport { $lib } from './lib';\nimport { idOf, type OperationIdsOptions } from './resource';\n\n/**\n * Marks the interface or the namespace; its ids are set once the program is\n * checked. On a template, TypeSpec runs it on each instance, which\n * `isMarkedInterface` follows to the interfaces extending it. A namespace\n * has no resource to name: `singular` and `plural` are an interface's.\n */\nexport function operationIds(\n\tcontext: DecoratorContext,\n\ttarget: Interface | Namespace,\n\toptions: OperationIdsOptions = {},\n): void {\n\tconst { program } = context;\n\tprogram.stateSet($lib.stateKeys.operationIds).add(target);\n\tprogram.stateMap($lib.stateKeys.operationIdsOptions).set(target, options);\n\tif (\n\t\ttarget.kind === 'Namespace' &&\n\t\t(options.singular !== undefined || options.plural !== undefined)\n\t) {\n\t\t$lib.reportDiagnostic(program, {\n\t\t\tcode: 'resource-name-on-namespace',\n\t\t\ttarget: context.decoratorTarget,\n\t\t});\n\t}\n}\n\n/** Marked itself, or in a marked namespace, however deep. */\nexport function isMarkedNamespace(\n\tprogram: Program,\n\ttarget: Namespace | undefined,\n): boolean {\n\tfor (let at = target; at !== undefined; at = at.namespace) {\n\t\tif (program.stateSet($lib.stateKeys.operationIds).has(at)) return true;\n\t}\n\treturn false;\n}\n\n/** Marked itself, or extending a marked interface: an instance of a marked template included. */\nfunction isMarkedInterface(program: Program, target: Interface): boolean {\n\treturn (\n\t\tprogram.stateSet($lib.stateKeys.operationIds).has(target) ||\n\t\ttarget.sourceInterfaces.some((source) => isMarkedInterface(program, source))\n\t);\n}\n\n/**\n * Whether `@operationIds` names the operation's id, or checks it: its\n * interface or one of its namespaces is marked. An interface template's\n * instance is never emitted; only the interfaces extending it are.\n */\nexport function isNamed(program: Program, operation: Operation): boolean {\n\tconst container = operation.interface;\n\tif (container === undefined) {\n\t\treturn isMarkedNamespace(program, operation.namespace);\n\t}\n\treturn (\n\t\t!isTemplateInstance(container) &&\n\t\t(isMarkedInterface(program, container) ||\n\t\t\tisMarkedNamespace(program, container.namespace))\n\t);\n}\n\n/**\n * Whether the emitter writes the operation: not an instance of an operation\n * template, `Read<Pet>`, nor an operation of an interface template's\n * instance. An interface extending an instance copies its operations, which\n * are instances too, and emitted.\n */\nexport function isEmitted(operation: Operation): boolean {\n\tconst container = operation.interface;\n\tif (container === undefined) return !isTemplateInstance(operation);\n\treturn !isTemplateInstance(container);\n}\n\n/**\n * The services whose document the operation is in: each `@service`\n * namespace around it, a nested one and the one outside it both. None, in a\n * program without a service, is the one document the emitter writes; in a\n * program with one, no document at all.\n */\nfunction servicesOf(program: Program, operation: Operation): Namespace[] {\n\tconst services: Namespace[] = [];\n\tfor (\n\t\tlet at = operation.interface?.namespace ?? operation.namespace;\n\t\tat !== undefined;\n\t\tat = at.namespace\n\t) {\n\t\tif (isService(program, at)) services.push(at);\n\t}\n\treturn services;\n}\n\n/**\n * Sets the ids, after every decorator, so the order of `@operationId` and\n * `@operationIds` in the source does not matter. Two operations with one id\n * in one service's document, one of them named by `@operationIds`, are an\n * error: one name in two interfaces, an interface that `extends` another and\n * so copies its operations, or an `@operationId` written elsewhere.\n * `@typespec/openapi3` would emit both without a word. An operation\n * template's instance, such as `Read<Pet>`, is never emitted; only the\n * operations declared from it (`op readPet is Read<Pet>`) are.\n */\nexport function validateOperationIds(program: Program): void {\n\tconst seen = new Map<Namespace | undefined, Map<string, boolean>>();\n\tconst hasServices = listServices(program).length > 0;\n\tnavigateProgram(program, {\n\t\toperation(operation) {\n\t\t\tif (!isEmitted(operation)) return;\n\t\t\tconst named = isNamed(program, operation);\n\t\t\tconst own = getOperationId(program, operation);\n\t\t\t// Elsewhere, only checked: the id `@typespec/openapi3` gives under its\n\t\t\t// default strategy.\n\t\t\tconst id =\n\t\t\t\town ??\n\t\t\t\t(named\n\t\t\t\t\t? idOf(program, operation)\n\t\t\t\t\t: resolveOperationId(program, operation));\n\t\t\tconst services = servicesOf(program, operation);\n\t\t\t// Outside every service of a program that has one: in no document.\n\t\t\tconst emitted = services.length > 0 || !hasServices;\n\t\t\tlet duplicate = false;\n\t\t\tfor (const service of services.length > 0 ? services : [undefined]) {\n\t\t\t\tconst ids = seen.get(service) ?? new Map<string, boolean>();\n\t\t\t\tseen.set(service, ids);\n\t\t\t\tif (emitted && ids.has(id) && (named || ids.get(id))) duplicate = true;\n\t\t\t\tids.set(id, named || (ids.get(id) ?? false));\n\t\t\t}\n\t\t\tif (duplicate) {\n\t\t\t\t$lib.reportDiagnostic(program, {\n\t\t\t\t\tcode: 'duplicate-operation-id',\n\t\t\t\t\tformat: { id },\n\t\t\t\t\ttarget: operation,\n\t\t\t\t});\n\t\t\t}\n\t\t\t// Only the ids it gives: an explicit id on any other operation would\n\t\t\t// override the emitter's `operation-id-strategy`.\n\t\t\tif (named && own === undefined) setOperationId(program, operation, id);\n\t\t},\n\t});\n}\n",
|
|
8
8
|
"/**\n * The verbs `@operationIds` completes with an interface's resource: `create`\n * in `Users` is `createUser`, `list` is `listUsers`, and `findById` is\n * `findUserById`. Any other name is the id as written.\n */\n\n/** Whether a verb takes the resource's singular or its plural. */\nexport type Grammar = 'singular' | 'plural';\n\n/** A resource's two names: `User` and `Users`. */\nexport interface Resource {\n\treadonly singular: string;\n\treadonly plural: string;\n}\n\n/** The library's own verbs. */\nexport const VERBS: Readonly<Record<string, Grammar>> = {\n\tlist: 'plural',\n\tread: 'plural',\n\tfind: 'plural',\n\tsearch: 'plural',\n\tquery: 'plural',\n\tcount: 'plural',\n\tcreateMany: 'plural',\n\tupdateMany: 'plural',\n\tdeleteMany: 'plural',\n\tget: 'singular',\n\tcreate: 'singular',\n\tupdate: 'singular',\n\tpatch: 'singular',\n\treplace: 'singular',\n\tupsert: 'singular',\n\tdelete: 'singular',\n};\n\n/**\n * The HTTP methods each of the library's verbs is sent with: a read is a\n * `GET` (or a `HEAD`), a search may carry its criteria in a `POST` body, a\n * bulk delete its ids. `QUERY` joins `search` and `query` once\n * `@typespec/http` declares it.\n */\nexport const METHODS: Readonly<Record<string, readonly string[]>> = {\n\tlist: ['get', 'head'],\n\tread: ['get', 'head'],\n\tfind: ['get', 'head'],\n\tcount: ['get', 'head'],\n\tget: ['get', 'head'],\n\tsearch: ['get', 'post'],\n\tquery: ['get', 'post'],\n\tcreate: ['post'],\n\tcreateMany: ['post'],\n\tupdate: ['put', 'patch'],\n\tupdateMany: ['put', 'patch'],\n\tpatch: ['patch'],\n\treplace: ['put'],\n\tupsert: ['put'],\n\tdelete: ['delete'],\n\tdeleteMany: ['delete', 'post'],\n};\n\n/** Plurals the rules below get wrong, by their singular. */\nconst IRREGULAR: Readonly<Record<string, string>> = {\n\tpeople: 'person',\n\tchildren: 'child',\n\tmen: 'man',\n\twomen: 'woman',\n\tmice: 'mouse',\n\tfeet: 'foot',\n\tteeth: 'tooth',\n\tgeese: 'goose',\n\tmovies: 'movie',\n\tcookies: 'cookie',\n\tseries: 'series',\n\tspecies: 'species',\n\tnews: 'news',\n};\n\n/** `-es` plurals of a singular in `-us`: `Statuses`, not `Houses`. */\nconst US_ES = /(stat|bon|camp|vir|b|foc|cens|corp|radi|syllab)uses$/i;\n\n/** `-es` plurals of a singular in a sibilant: `Boxes`, `Matches`, `Wishes`. */\nconst SIBILANT_ES = /(ss|x|zz|sh|tch|nch|rch|oach)es$/i;\n\n/**\n * The singular of a plural name, by a short English rule on its last word:\n * `Users`, `BlogPosts`, `Categories`, `Addresses`, `Statuses`, `People`. A\n * guess: it misses some (`Heroes`, `Analysis`), which `singular` names.\n */\nexport function singularOf(plural: string): string {\n\tconst word = /[A-Z]?[a-z0-9]*$/.exec(plural)?.[0] ?? '';\n\tconst head = plural.slice(0, plural.length - word.length);\n\tconst key = word.toLowerCase();\n\tconst irregular = Object.hasOwn(IRREGULAR, key) ? IRREGULAR[key] : undefined;\n\tif (irregular !== undefined) {\n\t\tconst cased = word[0] === word[0]?.toUpperCase();\n\t\treturn `${head}${cased ? irregular[0]?.toUpperCase() + irregular.slice(1) : irregular}`;\n\t}\n\tif (/ies$/.test(word) && word.length > 4)\n\t\treturn `${head}${word.slice(0, -3)}y`;\n\tif (US_ES.test(word) || SIBILANT_ES.test(word)) {\n\t\treturn `${head}${word.slice(0, -2)}`;\n\t}\n\tif (/[^su]s$/.test(word)) return `${head}${word.slice(0, -1)}`;\n\treturn plural;\n}\n\n/**\n * The verb a name starts with, and the rest: the whole name, `list`, or the\n * verb before a `By`, `findById`. `undefined` when the name is neither.\n */\nexport function verbIn(\n\tname: string,\n\tverbs: Readonly<Record<string, Grammar>>,\n): { readonly verb: string; readonly rest: string } | undefined {\n\tif (Object.hasOwn(verbs, name)) return { verb: name, rest: '' };\n\tfor (const by of name.matchAll(/By[A-Z]/g)) {\n\t\tconst verb = name.slice(0, by.index);\n\t\tif (verb !== '' && Object.hasOwn(verbs, verb)) {\n\t\t\treturn { verb, rest: name.slice(by.index) };\n\t\t}\n\t}\n\treturn undefined;\n}\n\n/**\n * The id of an operation of a resource: the verb and the resource's singular\n * or plural, or the verb, the resource and the rest of a `By` name:\n * `findById` to `findUserById`, the singular, but `deleteManyByTeam` to\n * `deleteManyUsersByTeam`, the plural of a `*Many` verb. `undefined` when\n * the name is none of them. The resource is taken only then.\n */\nexport function idWithVerb(\n\tname: string,\n\tresource: () => Resource,\n\tverbs: Readonly<Record<string, Grammar>>,\n): string | undefined {\n\tconst found = verbIn(name, verbs);\n\tif (found === undefined) return undefined;\n\tconst { verb, rest } = found;\n\tconst names = resource();\n\tconst noun =\n\t\trest === ''\n\t\t\t? names[verbs[verb] as Grammar]\n\t\t\t: verb.endsWith('Many')\n\t\t\t\t? names.plural\n\t\t\t\t: names.singular;\n\treturn `${verb}${noun}${rest}`;\n}\n",
|
|
9
9
|
"/**\n * What `@operationIds` names an operation of an interface after: the\n * interface's resource, its singular and plural, and the verbs that take\n * them, layered from the library's, the namespaces', and the interface's.\n */\nimport type {\n\tInterface,\n\tNamespace,\n\tOperation,\n\tProgram,\n} from '@typespec/compiler';\nimport { $lib } from './lib';\nimport {\n\ttype Grammar,\n\tidWithVerb,\n\ttype Resource,\n\tsingularOf,\n\tVERBS,\n\tverbIn,\n} from './verbs';\n\n/** `@operationIds`' options, as `OperationIdsOptions` declares them. */\nexport interface OperationIdsOptions {\n\treadonly singular?: string;\n\treadonly plural?: string;\n\treadonly verbs?: Readonly<Record<string, Grammar>>;\n}\n\nfunction optionsOf(\n\tprogram: Program,\n\ttarget: Interface | Namespace,\n): OperationIdsOptions | undefined {\n\treturn program.stateMap($lib.stateKeys.operationIdsOptions).get(target);\n}\n\n/** The interface, then those it extends, however deep. */\nfunction lineageOf(target: Interface): Interface[] {\n\treturn [target, ...target.sourceInterfaces.flatMap(lineageOf)];\n}\n\n/**\n * The verbs an interface's operations take: the library's, then each marked\n * namespace's around it, outermost first, then its lineage's, its own last,\n * each overriding the one before.\n */\nfunction verbsOf(program: Program, target: Interface): Record<string, Grammar> {\n\tconst namespaces: Namespace[] = [];\n\tfor (let at = target.namespace; at !== undefined; at = at.namespace) {\n\t\tnamespaces.unshift(at);\n\t}\n\tconst layers = [...namespaces, ...lineageOf(target).reverse()];\n\treturn Object.assign(\n\t\t{},\n\t\tVERBS,\n\t\t...layers.map((layer) => optionsOf(program, layer)?.verbs ?? {}),\n\t);\n}\n\n/**\n * An interface's resource: its own `plural`, or its name, and its own\n * `singular`, or the plural's singular. Never an interface it extends: two\n * interfaces extending one would share its names, and collide.\n */\nfunction resourceOf(program: Program, target: Interface): Resource {\n\tconst options = optionsOf(program, target);\n\tconst plural = options?.plural || target.name;\n\treturn { plural, singular: options?.singular || singularOf(plural) };\n}\n\n/** The id `@operationIds` gives: the name as written, or a verb with its resource. */\nexport function idOf(program: Program, operation: Operation): string {\n\tconst container = operation.interface;\n\tif (container === undefined) return operation.name;\n\treturn (\n\t\tidWithVerb(\n\t\t\toperation.name,\n\t\t\t() => resourceOf(program, container),\n\t\t\tverbsOf(program, container),\n\t\t) ?? operation.name\n\t);\n}\n\n/** The verb an operation of an interface is named with, `find` in `findById`. */\nexport function verbOf(\n\tprogram: Program,\n\toperation: Operation,\n): string | undefined {\n\tconst container = operation.interface;\n\tif (container === undefined) return undefined;\n\treturn verbIn(operation.name, verbsOf(program, container))?.verb;\n}\n",
|
|
10
10
|
"/**\n * A verb names what an operation does, and its method says the same:\n * `create` in `Users` is `createUser`, and a `GET` would tell a cache, a\n * proxy or a retry it is safe. An operation `@operationIds` names after one\n * of the library's verbs is sent with that verb's methods (`verbs.ts`), or\n * warned of. A verb `verbs` adds has no method to check, and an operation's\n * own `@operationId` is an id that is no verb. It runs before\n * `validateOperationIds` sets the others.\n */\nimport type { Operation, Program } from '@typespec/compiler';\nimport { getAllHttpServices } from '@typespec/http';\nimport { getOperationId } from '@typespec/openapi';\nimport { $lib } from './lib';\nimport { isNamed } from './operation-ids';\nimport { verbOf } from './resource';\nimport { METHODS } from './verbs';\n\n/** `PUT or PATCH`. */\nfunction methodsOf(methods: readonly string[]): string {\n\treturn methods.map((method) => method.toUpperCase()).join(' or ');\n}\n\n/** Checks each operation of each service once, as the emitter sends it. */\nexport function validateVerbMethods(program: Program): void {\n\tconst [services] = getAllHttpServices(program);\n\tconst checked = new Set<Operation>();\n\tfor (const service of services) {\n\t\tfor (const { operation, verb: method } of service.operations) {\n\t\t\tif (checked.has(operation)) continue;\n\t\t\tchecked.add(operation);\n\t\t\tif (!isNamed(program, operation)) continue;\n\t\t\tif (getOperationId(program, operation) !== undefined) continue;\n\t\t\tconst verb = verbOf(program, operation);\n\t\t\tif (verb === undefined || !Object.hasOwn(METHODS, verb)) continue;\n\t\t\tconst expected = METHODS[verb] as readonly string[];\n\t\t\tif (expected.includes(method)) continue;\n\t\t\t$lib.reportDiagnostic(program, {\n\t\t\t\tcode: 'verb-method-mismatch',\n\t\t\t\tformat: {\n\t\t\t\t\toperation: operation.name,\n\t\t\t\t\tmethod: method.toUpperCase(),\n\t\t\t\t\tverb,\n\t\t\t\t\texpected: methodsOf(expected),\n\t\t\t\t},\n\t\t\t\ttarget: operation,\n\t\t\t});\n\t\t}\n\t}\n}\n",
|
|
11
|
-
"/**\n * The
|
|
11
|
+
"/**\n * The library's linter: rules a spec opts into in its `tspconfig.yaml`,\n * each a warning, each one it can turn off.\n *\n * ```yaml\n * linter:\n * extends:\n * - \"@nxgt/typespec/recommended\"\n * ```\n */\nimport { defineLinter } from '@typespec/compiler';\nimport { errorBodyShape } from './rules/error-body-shape';\nimport { listReturnsPage } from './rules/list-returns-page';\nimport { serviceOperationIds } from './rules/service-operation-ids';\n\nconst rules = [listReturnsPage, serviceOperationIds, errorBodyShape];\n\nexport const $linter = defineLinter({\n\trules,\n\truleSets: {\n\t\trecommended: {\n\t\t\tenable: Object.fromEntries(\n\t\t\t\trules.map(({ name }) => [`@nxgt/typespec/${name}`, true]),\n\t\t\t),\n\t\t},\n\t},\n});\n",
|
|
12
|
+
"/**\n * An error reply's body is the envelope `@nxgt/openapi-hono` sends: a\n * `status`, a `message` and a `timestamp`, as `ErrorBody<Status>` declares\n * them. A reply without a body passes: `AuthenticationRequired` has none.\n */\nimport {\n\tcreateRule,\n\tisVoidType,\n\ttype Model,\n\tparamMessage,\n\twalkPropertiesInherited,\n} from '@typespec/compiler';\nimport { getHttpOperation } from '@typespec/http';\nimport { isWritten } from './written';\n\nconst ENVELOPE = ['status', 'message', 'timestamp'];\n\n/** Its own properties and those of the models it extends. */\nfunction isEnvelope(body: Model): boolean {\n\tconst names = new Set(\n\t\t[...walkPropertiesInherited(body)].map(({ name }) => name),\n\t);\n\treturn ENVELOPE.every((name) => names.has(name));\n}\n\nfunction statusOf(code: number | { start: number; end: number } | '*'): string {\n\treturn typeof code === 'object' ? `${code.start}-${code.end}` : `${code}`;\n}\n\nexport const errorBodyShape = createRule({\n\tname: 'error-body-shape',\n\tseverity: 'warning',\n\tdescription:\n\t\t'An error reply has the nxgt envelope: status, message and timestamp.',\n\tmessages: {\n\t\tdefault: paramMessage`${'operation'} answers ${'status'} with a body that is not the nxgt envelope: declare BadRequest, NotFound or another of the library's errors, or a body that spreads or extends ErrorBody<Status>.`,\n\t},\n\tcreate: (context) => ({\n\t\toperation(operation) {\n\t\t\tif (!isWritten(operation)) return;\n\t\t\tconst [http] = getHttpOperation(context.program, operation);\n\t\t\tfor (const { statusCodes, responses } of http.responses) {\n\t\t\t\tconst error =\n\t\t\t\t\tstatusCodes === '*' ||\n\t\t\t\t\t(typeof statusCodes === 'number'\n\t\t\t\t\t\t? statusCodes >= 400\n\t\t\t\t\t\t: statusCodes.start >= 400);\n\t\t\t\tif (!error) continue;\n\t\t\t\tconst wrong = responses.some(({ body }) => {\n\t\t\t\t\tif (body === undefined || isVoidType(body.type)) return false;\n\t\t\t\t\treturn body.type.kind !== 'Model' || !isEnvelope(body.type);\n\t\t\t\t});\n\t\t\t\tif (!wrong) continue;\n\t\t\t\tcontext.reportDiagnostic({\n\t\t\t\t\tformat: { operation: operation.name, status: statusOf(statusCodes) },\n\t\t\t\t\ttarget: operation,\n\t\t\t\t});\n\t\t\t}\n\t\t},\n\t}),\n});\n",
|
|
13
|
+
"/**\n * Whether the emitter writes an operation: not a template's declaration,\n * nor its instance (`Read<Pet>`), nor an operation of an interface\n * template's. Only the operations declared from them are checked.\n */\nimport { isTemplateDeclaration, type Operation } from '@typespec/compiler';\nimport { isEmitted } from '../operation-ids';\n\nexport function isWritten(operation: Operation): boolean {\n\tconst container = operation.interface;\n\treturn (\n\t\tisEmitted(operation) &&\n\t\t!isTemplateDeclaration(operation) &&\n\t\t(container === undefined || !isTemplateDeclaration(container))\n\t);\n}\n",
|
|
14
|
+
"/**\n * A list returns a page: `Page<Item>` or `CursorPage<Item>`, never a bare\n * array, which cannot grow a `total` or a `nextCursor` without breaking its\n * clients. A list is an operation named `list`, `search` or `query`, alone\n * or followed by more: `listUsers`, `searchPosts`.\n */\nimport { createRule, isArrayModelType, paramMessage } from '@typespec/compiler';\nimport { getHttpOperation } from '@typespec/http';\nimport { isWritten } from './written';\n\nconst LIST = /^(list|search|query)(?=[A-Z]|$)/;\n\nexport const listReturnsPage = createRule({\n\tname: 'list-returns-page',\n\tseverity: 'warning',\n\tdescription:\n\t\t'A list, search or query returns Page<Item> or CursorPage<Item>.',\n\tmessages: {\n\t\tdefault: paramMessage`${'operation'} returns an array: return Page<Item> or CursorPage<Item>, which can carry a total or a next cursor.`,\n\t},\n\tcreate: (context) => ({\n\t\toperation(operation) {\n\t\t\tif (!isWritten(operation) || !LIST.test(operation.name)) return;\n\t\t\tconst [http] = getHttpOperation(context.program, operation);\n\t\t\tconst array = http.responses.some(\n\t\t\t\t({ statusCodes, responses }) =>\n\t\t\t\t\ttypeof statusCodes === 'number' &&\n\t\t\t\t\tstatusCodes >= 200 &&\n\t\t\t\t\tstatusCodes < 300 &&\n\t\t\t\t\tresponses.some(\n\t\t\t\t\t\t({ body }) =>\n\t\t\t\t\t\t\tbody?.type.kind === 'Model' && isArrayModelType(body.type),\n\t\t\t\t\t),\n\t\t\t);\n\t\t\tif (!array) return;\n\t\t\tcontext.reportDiagnostic({\n\t\t\t\tformat: { operation: operation.name },\n\t\t\t\ttarget: operation,\n\t\t\t});\n\t\t},\n\t}),\n});\n",
|
|
15
|
+
"/**\n * A service without `@operationIds` gets the emitter's ids: `Users_list`,\n * which the generated client's methods are named after.\n */\nimport { createRule, isService, paramMessage } from '@typespec/compiler';\nimport { isMarkedNamespace } from '../operation-ids';\n\nexport const serviceOperationIds = createRule({\n\tname: 'service-operation-ids',\n\tseverity: 'warning',\n\tdescription:\n\t\t'A service namespace marks its operation ids with @operationIds.',\n\tmessages: {\n\t\tdefault: paramMessage`The service ${'service'} has no @operationIds: its ids are the emitter's, such as Users_list. Mark the namespace with @operationIds.`,\n\t},\n\tcreate: (context) => ({\n\t\tnamespace(namespace) {\n\t\t\tconst { program } = context;\n\t\t\tif (!isService(program, namespace)) return;\n\t\t\tif (isMarkedNamespace(program, namespace)) return;\n\t\t\tcontext.reportDiagnostic({\n\t\t\t\tformat: { service: namespace.name },\n\t\t\t\ttarget: namespace,\n\t\t\t});\n\t\t},\n\t}),\n});\n",
|
|
16
|
+
"/**\n * The JavaScript side of the `@nxgt/typespec` library, which `lib/main.tsp`\n * imports from `dist/`: its name and diagnostics, and the decorators a\n * template cannot express. The conventions themselves are in `lib/`.\n *\n * A decorator is exported only through `$decorators`: a top-level `$name`\n * export would declare it a second time, in the global namespace.\n */\nimport type { Program } from '@typespec/compiler';\nimport { validateOneReplyPerStatus } from './one-reply-per-status';\nimport { operationIds, validateOperationIds } from './operation-ids';\nimport { validateVerbMethods } from './verb-methods';\n\nexport { $lib } from './lib';\nexport { $linter } from './linter';\n\nexport function $onValidate(program: Program): void {\n\t// Before the ids are set: only an `@operationId` written in the spec is\n\t// one then, and the operation it names is not checked.\n\tvalidateVerbMethods(program);\n\tvalidateOperationIds(program);\n\tvalidateOneReplyPerStatus(program);\n}\n\nexport const $decorators = {\n\tNxgt: { operationIds },\n};\n"
|
|
12
17
|
],
|
|
13
|
-
"mappings": "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",
|
|
14
|
-
"debugId": "
|
|
18
|
+
"mappings": "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",
|
|
19
|
+
"debugId": "FB766460AB7EEFAD64756E2164756E21",
|
|
15
20
|
"names": []
|
|
16
21
|
}
|
package/dist/linter.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"linter.d.ts","sourceRoot":"","sources":["../src/linter.ts"],"names":[],"mappings":"AAiBA,eAAO,MAAM,OAAO,+CASlB,CAAC"}
|
package/dist/operation-ids.d.ts
CHANGED
|
@@ -13,12 +13,21 @@ import { type OperationIdsOptions } from './resource';
|
|
|
13
13
|
* has no resource to name: `singular` and `plural` are an interface's.
|
|
14
14
|
*/
|
|
15
15
|
export declare function operationIds(context: DecoratorContext, target: Interface | Namespace, options?: OperationIdsOptions): void;
|
|
16
|
+
/** Marked itself, or in a marked namespace, however deep. */
|
|
17
|
+
export declare function isMarkedNamespace(program: Program, target: Namespace | undefined): boolean;
|
|
16
18
|
/**
|
|
17
19
|
* Whether `@operationIds` names the operation's id, or checks it: its
|
|
18
20
|
* interface or one of its namespaces is marked. An interface template's
|
|
19
21
|
* instance is never emitted; only the interfaces extending it are.
|
|
20
22
|
*/
|
|
21
23
|
export declare function isNamed(program: Program, operation: Operation): boolean;
|
|
24
|
+
/**
|
|
25
|
+
* Whether the emitter writes the operation: not an instance of an operation
|
|
26
|
+
* template, `Read<Pet>`, nor an operation of an interface template's
|
|
27
|
+
* instance. An interface extending an instance copies its operations, which
|
|
28
|
+
* are instances too, and emitted.
|
|
29
|
+
*/
|
|
30
|
+
export declare function isEmitted(operation: Operation): boolean;
|
|
22
31
|
/**
|
|
23
32
|
* Sets the ids, after every decorator, so the order of `@operationId` and
|
|
24
33
|
* `@operationIds` in the source does not matter. Two operations with one id
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"operation-ids.d.ts","sourceRoot":"","sources":["../src/operation-ids.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AACH,OAAO,EACN,KAAK,gBAAgB,EACrB,KAAK,SAAS,EAId,KAAK,SAAS,EAEd,KAAK,SAAS,EACd,KAAK,OAAO,EACZ,MAAM,oBAAoB,CAAC;AAO5B,OAAO,EAAQ,KAAK,mBAAmB,EAAE,MAAM,YAAY,CAAC;AAE5D;;;;;GAKG;AACH,wBAAgB,YAAY,CAC3B,OAAO,EAAE,gBAAgB,EACzB,MAAM,EAAE,SAAS,GAAG,SAAS,EAC7B,OAAO,GAAE,mBAAwB,GAC/B,IAAI,CAaN;
|
|
1
|
+
{"version":3,"file":"operation-ids.d.ts","sourceRoot":"","sources":["../src/operation-ids.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AACH,OAAO,EACN,KAAK,gBAAgB,EACrB,KAAK,SAAS,EAId,KAAK,SAAS,EAEd,KAAK,SAAS,EACd,KAAK,OAAO,EACZ,MAAM,oBAAoB,CAAC;AAO5B,OAAO,EAAQ,KAAK,mBAAmB,EAAE,MAAM,YAAY,CAAC;AAE5D;;;;;GAKG;AACH,wBAAgB,YAAY,CAC3B,OAAO,EAAE,gBAAgB,EACzB,MAAM,EAAE,SAAS,GAAG,SAAS,EAC7B,OAAO,GAAE,mBAAwB,GAC/B,IAAI,CAaN;AAED,6DAA6D;AAC7D,wBAAgB,iBAAiB,CAChC,OAAO,EAAE,OAAO,EAChB,MAAM,EAAE,SAAS,GAAG,SAAS,GAC3B,OAAO,CAKT;AAUD;;;;GAIG;AACH,wBAAgB,OAAO,CAAC,OAAO,EAAE,OAAO,EAAE,SAAS,EAAE,SAAS,GAAG,OAAO,CAUvE;AAED;;;;;GAKG;AACH,wBAAgB,SAAS,CAAC,SAAS,EAAE,SAAS,GAAG,OAAO,CAIvD;AAoBD;;;;;;;;;GASG;AACH,wBAAgB,oBAAoB,CAAC,OAAO,EAAE,OAAO,GAAG,IAAI,CAqC3D"}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
export declare const errorBodyShape: import("@typespec/compiler").LinterRuleDefinition<"error-body-shape", {
|
|
2
|
+
readonly default: import("@typespec/compiler").CallableMessage<["operation", "status"]>;
|
|
3
|
+
}, Record<string, never>>;
|
|
4
|
+
//# sourceMappingURL=error-body-shape.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"error-body-shape.d.ts","sourceRoot":"","sources":["../../src/rules/error-body-shape.ts"],"names":[],"mappings":"AA6BA,eAAO,MAAM,cAAc;;yBA+BzB,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"list-returns-page.d.ts","sourceRoot":"","sources":["../../src/rules/list-returns-page.ts"],"names":[],"mappings":"AAYA,eAAO,MAAM,eAAe;;yBA6B1B,CAAC"}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
export declare const serviceOperationIds: import("@typespec/compiler").LinterRuleDefinition<"service-operation-ids", {
|
|
2
|
+
readonly default: import("@typespec/compiler").CallableMessage<["service"]>;
|
|
3
|
+
}, Record<string, never>>;
|
|
4
|
+
//# sourceMappingURL=service-operation-ids.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"service-operation-ids.d.ts","sourceRoot":"","sources":["../../src/rules/service-operation-ids.ts"],"names":[],"mappings":"AAOA,eAAO,MAAM,mBAAmB;;yBAmB9B,CAAC"}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Whether the emitter writes an operation: not a template's declaration,
|
|
3
|
+
* nor its instance (`Read<Pet>`), nor an operation of an interface
|
|
4
|
+
* template's. Only the operations declared from them are checked.
|
|
5
|
+
*/
|
|
6
|
+
import { type Operation } from '@typespec/compiler';
|
|
7
|
+
export declare function isWritten(operation: Operation): boolean;
|
|
8
|
+
//# sourceMappingURL=written.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"written.d.ts","sourceRoot":"","sources":["../../src/rules/written.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AACH,OAAO,EAAyB,KAAK,SAAS,EAAE,MAAM,oBAAoB,CAAC;AAG3E,wBAAgB,SAAS,CAAC,SAAS,EAAE,SAAS,GAAG,OAAO,CAOvD"}
|
package/docs/README.md
CHANGED
|
@@ -11,5 +11,6 @@ The [package README](../README.md) is the short version.
|
|
|
11
11
|
| [Headers](guide/headers.md) | making a write idempotent, or declaring a rate limit |
|
|
12
12
|
| [Sorting](guide/sorting.md) | letting a client filter and sort a list |
|
|
13
13
|
| [Operation ids](guide/operation-ids.md) | naming each operation, and so the generated client's method: `list` in `Users` as `listUsers`, any other name as written |
|
|
14
|
+
| [Linter](guide/linter.md) | checking a spec against these conventions in `tsp compile`, or turning one rule off |
|
|
14
15
|
| [Troubleshooting](troubleshooting.md) | the compiler or the generator refused the spec |
|
|
15
16
|
| [Roadmap](roadmap.md) | wondering what is coming |
|
package/docs/guide/errors.md
CHANGED
|
@@ -173,6 +173,71 @@ Name them in an operation's return type, beside the success:
|
|
|
173
173
|
@get getPost(@path postId: string): Post | NotFound | Unauthorized | Forbidden;
|
|
174
174
|
```
|
|
175
175
|
|
|
176
|
+
## Aliases by verb
|
|
177
|
+
|
|
178
|
+
Most operations of a resource answer the same errors. Each verb has an alias
|
|
179
|
+
for them:
|
|
180
|
+
|
|
181
|
+
| Alias | Responses | Statuses |
|
|
182
|
+
| --- | --- | --- |
|
|
183
|
+
| `ListErrors` | `BadRequest` | 400 |
|
|
184
|
+
| `GetErrors` | `NotFound` | 404 |
|
|
185
|
+
| `CreateErrors` | `BadRequest \| Conflict` | 400, 409 |
|
|
186
|
+
| `UpdateErrors` | `BadRequest \| NotFound \| Conflict` | 400, 404, 409 |
|
|
187
|
+
| `DeleteErrors` | `NotFound` | 404 |
|
|
188
|
+
| `AuthErrors` | `Unauthorized \| Forbidden` | 401, 403 |
|
|
189
|
+
|
|
190
|
+
```tsp
|
|
191
|
+
import "@typespec/http";
|
|
192
|
+
import "@nxgt/typespec";
|
|
193
|
+
|
|
194
|
+
using Http;
|
|
195
|
+
using Nxgt;
|
|
196
|
+
|
|
197
|
+
@service
|
|
198
|
+
namespace Shop;
|
|
199
|
+
|
|
200
|
+
model User {
|
|
201
|
+
id: uuid;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
@route("/users")
|
|
205
|
+
interface Users {
|
|
206
|
+
@get list(...PageParameters): Page<User> | ListErrors;
|
|
207
|
+
@get get(@path id: uuid): User | GetErrors;
|
|
208
|
+
@post create(@body user: User): User | CreateErrors;
|
|
209
|
+
@patch update(@path id: uuid, @body user: User): User | UpdateErrors;
|
|
210
|
+
@delete delete(@path id: uuid): NoContentResponse | DeleteErrors;
|
|
211
|
+
@get @route("/me") me(): User | AuthErrors;
|
|
212
|
+
}
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
An alias is a union, and nothing more: it emits the same OpenAPI as the
|
|
216
|
+
responses written out. Combine aliases, and add a response an alias leaves
|
|
217
|
+
out:
|
|
218
|
+
|
|
219
|
+
```tsp
|
|
220
|
+
@post create(@body user: User): User | CreateErrors | AuthErrors | TooManyRequests;
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
A union of your own names the errors your routes share:
|
|
224
|
+
|
|
225
|
+
```tsp
|
|
226
|
+
alias WriteErrors = CreateErrors | AuthErrors | TooManyRequests;
|
|
227
|
+
```
|
|
228
|
+
|
|
229
|
+
Do not add a response the alias already holds. `UpdateErrors | NotFound`
|
|
230
|
+
declares the 404 twice, and the library warns, `merged-status-reply`. A 409
|
|
231
|
+
beside `CreateErrors` or `UpdateErrors`, such as `IdempotencyInProgress`,
|
|
232
|
+
warns too: declare the responses one by one there
|
|
233
|
+
([Headers](headers.md), [troubleshooting](../troubleshooting.md)).
|
|
234
|
+
|
|
235
|
+
`AuthErrors` carries the envelope, for a handler's own refusal. A route
|
|
236
|
+
whose guard answers the 401 and 403 without a body, as `@nxgt/janus-hono`'s
|
|
237
|
+
do, declares `AuthenticationRequired | AccessDenied` instead
|
|
238
|
+
([Authentication](auth.md)). The two in one operation are an error,
|
|
239
|
+
`duplicate-status-reply`.
|
|
240
|
+
|
|
176
241
|
## Another status, named
|
|
177
242
|
|
|
178
243
|
`ErrorResponse<503>` works, but its body is emitted inline and generated as
|
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
# Linter
|
|
2
|
+
|
|
3
|
+
The linter warns when a spec strays from the conventions the library gives
|
|
4
|
+
it: a list that returns an array, a service without `@operationIds`, an error
|
|
5
|
+
reply without the envelope.
|
|
6
|
+
|
|
7
|
+
## Turn it on
|
|
8
|
+
|
|
9
|
+
Extend the ruleset in `tspconfig.yaml`, beside the emitter:
|
|
10
|
+
|
|
11
|
+
```yaml
|
|
12
|
+
# api/tspconfig.yaml
|
|
13
|
+
emit:
|
|
14
|
+
- '@typespec/openapi3'
|
|
15
|
+
linter:
|
|
16
|
+
extends:
|
|
17
|
+
- '@nxgt/typespec/recommended'
|
|
18
|
+
options:
|
|
19
|
+
'@typespec/openapi3':
|
|
20
|
+
openapi-versions: ['3.1.0']
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
Nothing runs without it: importing `@nxgt/typespec` does not lint the spec.
|
|
24
|
+
Each rule is a warning, so `tsp compile` still emits:
|
|
25
|
+
|
|
26
|
+
```text
|
|
27
|
+
warning @nxgt/typespec/list-returns-page: listAll returns an array: return Page<Item> or CursorPage<Item>, which can carry a total or a next cursor.
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
`tsp compile api --warn-as-error` fails on any of them, for CI.
|
|
31
|
+
|
|
32
|
+
| Rule | Warns when |
|
|
33
|
+
| --- | --- |
|
|
34
|
+
| [`list-returns-page`](#list-returns-page) | a `list`, `search` or `query` returns an array |
|
|
35
|
+
| [`service-operation-ids`](#service-operation-ids) | a `@service` namespace has no `@operationIds` |
|
|
36
|
+
| [`error-body-shape`](#error-body-shape) | an error reply's body is not `status`, `message` and `timestamp` |
|
|
37
|
+
|
|
38
|
+
The library's own checks, such as `verb-method-mismatch` and
|
|
39
|
+
`duplicate-operation-id`, run whether or not the linter is on
|
|
40
|
+
([Operation ids](operation-ids.md)).
|
|
41
|
+
|
|
42
|
+
## `list-returns-page`
|
|
43
|
+
|
|
44
|
+
**Checks:** an operation whose name is `list`, `search` or `query`, alone or
|
|
45
|
+
followed by a capital letter (`listUsers`, `searchPosts`), with a success
|
|
46
|
+
reply whose body is an array. `listing` and `queryable` are not lists, and
|
|
47
|
+
are not checked. The name is checked as written, in an interface or not.
|
|
48
|
+
|
|
49
|
+
Only the operations the emitter writes are checked, by this rule and
|
|
50
|
+
`error-body-shape`: a template's declaration, its instance (`Read<Pet>`) and
|
|
51
|
+
the operations of an interface template are not, while an interface that
|
|
52
|
+
extends a template, `interface Users extends Crud<User> {}`, has its copied
|
|
53
|
+
operations checked.
|
|
54
|
+
|
|
55
|
+
**Why:** an array cannot grow a `total` or a `nextCursor` later without
|
|
56
|
+
breaking every client. `Page<Item>` and `CursorPage<Item>` can, and they are
|
|
57
|
+
the pages `@nxgt/drizzle` and `@nxgt/mongo` return.
|
|
58
|
+
|
|
59
|
+
Fails:
|
|
60
|
+
|
|
61
|
+
```tsp
|
|
62
|
+
@route("/users")
|
|
63
|
+
interface Users {
|
|
64
|
+
@get list(): User[];
|
|
65
|
+
}
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
```text
|
|
69
|
+
warning @nxgt/typespec/list-returns-page: list returns an array: return Page<Item> or CursorPage<Item>, which can carry a total or a next cursor.
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
Passes:
|
|
73
|
+
|
|
74
|
+
```tsp
|
|
75
|
+
@route("/users")
|
|
76
|
+
interface Users {
|
|
77
|
+
@get list(...PageParameters): Page<User>;
|
|
78
|
+
@get @route("/search") search(...CursorPageParameters): CursorPage<User>;
|
|
79
|
+
}
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
More on both pages in [Pagination](pagination.md).
|
|
83
|
+
|
|
84
|
+
## `service-operation-ids`
|
|
85
|
+
|
|
86
|
+
**Checks:** a `@service` namespace that is not marked with `@operationIds`,
|
|
87
|
+
neither itself nor a namespace around it.
|
|
88
|
+
|
|
89
|
+
**Why:** without it, the emitter names each operation after its interface,
|
|
90
|
+
`Users_list`, and the generated client's methods and types take that name.
|
|
91
|
+
`@operationIds` gives `listUsers` ([Operation ids](operation-ids.md)).
|
|
92
|
+
|
|
93
|
+
Fails:
|
|
94
|
+
|
|
95
|
+
```tsp
|
|
96
|
+
@service
|
|
97
|
+
namespace Shop {
|
|
98
|
+
@route("/health") @get op health(): void;
|
|
99
|
+
}
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
```text
|
|
103
|
+
warning @nxgt/typespec/service-operation-ids: The service Shop has no @operationIds: its ids are the emitter's, such as Users_list. Mark the namespace with @operationIds.
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
Passes, marked on the service or on a namespace around it:
|
|
107
|
+
|
|
108
|
+
```tsp
|
|
109
|
+
@service
|
|
110
|
+
@operationIds
|
|
111
|
+
namespace Shop {
|
|
112
|
+
@route("/health") @get op health(): void;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
@operationIds
|
|
116
|
+
namespace Admin {
|
|
117
|
+
@service
|
|
118
|
+
namespace Billing {
|
|
119
|
+
@route("/invoices") @get op invoices(): void;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
`@operationIds` on an interface alone does not count: the rule asks for the
|
|
125
|
+
namespace.
|
|
126
|
+
|
|
127
|
+
## `error-body-shape`
|
|
128
|
+
|
|
129
|
+
**Checks:** each reply of status 400 or above, of a range that starts at 400
|
|
130
|
+
or above (`5XX`), or of no status (`default`, an `@error` model without
|
|
131
|
+
`@statusCode`), that has a body. The body must be a model with a `status`, a
|
|
132
|
+
`message` and a `timestamp` property. A reply without a body passes, such as
|
|
133
|
+
`AuthenticationRequired`, `AccessDenied` or `ErrorWithoutBody<404>`.
|
|
134
|
+
|
|
135
|
+
**Why:** `@nxgt/openapi-hono` answers its own 400 and 500 with that envelope,
|
|
136
|
+
and a client written against it reads `message` from every error. A body of
|
|
137
|
+
another shape is one more case each client must handle.
|
|
138
|
+
|
|
139
|
+
Fails:
|
|
140
|
+
|
|
141
|
+
```tsp
|
|
142
|
+
model Teapot {
|
|
143
|
+
@statusCode _: 418;
|
|
144
|
+
@body body: { reason: string };
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
@route("/users")
|
|
148
|
+
interface Users {
|
|
149
|
+
@post create(@body user: User): User | Teapot;
|
|
150
|
+
}
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
```text
|
|
154
|
+
warning @nxgt/typespec/error-body-shape: create answers 418 with a body that is not the nxgt envelope: declare BadRequest, NotFound or another of the library's errors, or a body that spreads or extends ErrorBody<Status>.
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
A range is named by its bounds, `500-599`, and a reply without a status as
|
|
158
|
+
`*`.
|
|
159
|
+
|
|
160
|
+
Passes: one of the library's errors, `ErrorResponse<Status>`, or a body that
|
|
161
|
+
spreads or extends `ErrorBody<Status>` and adds its own fields
|
|
162
|
+
(`model TeapotBody extends ErrorBody<418> { reason: string; }` passes too):
|
|
163
|
+
|
|
164
|
+
```tsp
|
|
165
|
+
model TeapotBody {
|
|
166
|
+
...ErrorBody<418>;
|
|
167
|
+
reason: string;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
@error
|
|
171
|
+
model Teapot {
|
|
172
|
+
@statusCode _: 418;
|
|
173
|
+
@body body: TeapotBody;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
@route("/users")
|
|
177
|
+
interface Users {
|
|
178
|
+
@get @route("/{id}") get(@path id: uuid): User | NotFound | AuthenticationRequired;
|
|
179
|
+
@post create(@body user: User): User | BadRequest | Teapot | ErrorResponse<503>;
|
|
180
|
+
}
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
The rule checks the property names, not their types. Each error is in
|
|
184
|
+
[Error replies](errors.md).
|
|
185
|
+
|
|
186
|
+
## Turn a rule off
|
|
187
|
+
|
|
188
|
+
For the whole spec, in `tspconfig.yaml`, with the reason:
|
|
189
|
+
|
|
190
|
+
```yaml
|
|
191
|
+
linter:
|
|
192
|
+
extends:
|
|
193
|
+
- '@nxgt/typespec/recommended'
|
|
194
|
+
disable:
|
|
195
|
+
'@nxgt/typespec/error-body-shape': 'the legacy routes answer their own errors'
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
For one operation or one namespace, a `#suppress` line above it:
|
|
199
|
+
|
|
200
|
+
```tsp
|
|
201
|
+
@route("/users")
|
|
202
|
+
interface Users {
|
|
203
|
+
#suppress "@nxgt/typespec/list-returns-page" "a fixed set of ten"
|
|
204
|
+
@get @route("/top") listTop(): User[];
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
#suppress "@nxgt/typespec/service-operation-ids" "its clients already call Health_check"
|
|
208
|
+
@service
|
|
209
|
+
namespace Health {
|
|
210
|
+
@route("/health") @get op check(): void;
|
|
211
|
+
}
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
Or turn on only the rules you want, without the set:
|
|
215
|
+
|
|
216
|
+
```yaml
|
|
217
|
+
linter:
|
|
218
|
+
enable:
|
|
219
|
+
'@nxgt/typespec/list-returns-page': true
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
## A whole spec
|
|
223
|
+
|
|
224
|
+
The rules pass on a spec that uses the library throughout:
|
|
225
|
+
|
|
226
|
+
```tsp
|
|
227
|
+
// api/main.tsp
|
|
228
|
+
import "@typespec/http";
|
|
229
|
+
import "@nxgt/typespec";
|
|
230
|
+
|
|
231
|
+
using Http;
|
|
232
|
+
using Nxgt;
|
|
233
|
+
|
|
234
|
+
@service(#{ title: "Shop" })
|
|
235
|
+
@operationIds
|
|
236
|
+
namespace Shop;
|
|
237
|
+
|
|
238
|
+
model User {
|
|
239
|
+
@visibility(Lifecycle.Read) id: uuid;
|
|
240
|
+
name: string;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
@route("/users")
|
|
244
|
+
interface Users {
|
|
245
|
+
@get list(...PageParameters): Page<User> | BadRequest; // listUsers
|
|
246
|
+
@get @route("/search") search(...CursorPageParameters): CursorPage<User>; // searchUsers
|
|
247
|
+
@get @route("/{id}") get(@path id: uuid): User | NotFound | AuthenticationRequired; // getUser
|
|
248
|
+
@post create(@body user: User): User | BadRequest | Conflict; // createUser
|
|
249
|
+
}
|
|
250
|
+
```
|
|
251
|
+
|
|
252
|
+
```sh
|
|
253
|
+
bunx --no-install tsp compile api --warn-as-error
|
|
254
|
+
```
|
|
255
|
+
|
|
256
|
+
The messages, with their fixes, are also in
|
|
257
|
+
[troubleshooting](../troubleshooting.md).
|
package/docs/roadmap.md
CHANGED
|
@@ -6,7 +6,12 @@ generated by `@nxgt/openapi-codegen` in CI before it ships.
|
|
|
6
6
|
|
|
7
7
|
## Now
|
|
8
8
|
|
|
9
|
-
|
|
9
|
+
- **A linter.** The ruleset `@nxgt/typespec/recommended`, which a spec
|
|
10
|
+
extends in `tspconfig.yaml`, warns of a list that returns an array
|
|
11
|
+
(`list-returns-page`), a service without `@operationIds`
|
|
12
|
+
(`service-operation-ids`), and an error reply without the envelope
|
|
13
|
+
(`error-body-shape`). Each rule can be turned off, for the spec or for one
|
|
14
|
+
operation.
|
|
10
15
|
|
|
11
16
|
## Later
|
|
12
17
|
|
|
@@ -20,6 +25,13 @@ Nothing in progress.
|
|
|
20
25
|
|
|
21
26
|
## Shipped
|
|
22
27
|
|
|
28
|
+
### 0.7.0
|
|
29
|
+
|
|
30
|
+
- **Error aliases by verb.** `ListErrors`, `GetErrors`, `CreateErrors`,
|
|
31
|
+
`UpdateErrors`, `DeleteErrors` and `AuthErrors` name the errors each verb
|
|
32
|
+
usually answers, so an operation writes `User | GetErrors` instead of the
|
|
33
|
+
union. Each is exactly its union, and they combine.
|
|
34
|
+
|
|
23
35
|
### 0.6.0
|
|
24
36
|
|
|
25
37
|
- **Verbs name their methods.** An operation named after one of the
|
package/docs/troubleshooting.md
CHANGED
|
@@ -281,11 +281,153 @@ header either reply declares required becomes optional.
|
|
|
281
281
|
@post createPost(@body post: Post): Post | IdempotencyInProgress;
|
|
282
282
|
```
|
|
283
283
|
|
|
284
|
+
An alias can hide the duplicate: `CreateErrors` holds a `Conflict`, so
|
|
285
|
+
`Post | CreateErrors | IdempotencyInProgress` declares two 409s, and
|
|
286
|
+
`UpdateErrors | NotFound` two 404s. Spell the alias's other errors out
|
|
287
|
+
instead:
|
|
288
|
+
|
|
289
|
+
```tsp
|
|
290
|
+
@post createPost(@body post: Post): Post | BadRequest | IdempotencyInProgress;
|
|
291
|
+
```
|
|
292
|
+
|
|
284
293
|
Two plain bodies, `Post | Draft`, are one reply whose body is either; two
|
|
285
294
|
`@error` models without a `@statusCode` share `default`, and one
|
|
286
295
|
description; bodies of different content types are negotiated. All three
|
|
287
296
|
pass.
|
|
288
297
|
|
|
298
|
+
## `returns an array: return Page<Item> or CursorPage<Item>`
|
|
299
|
+
|
|
300
|
+
**When:** a warning, with the ruleset `@nxgt/typespec/recommended` on: an
|
|
301
|
+
operation named `list`, `search` or `query`, alone or before a capital
|
|
302
|
+
letter, has a success reply whose body is an array.
|
|
303
|
+
|
|
304
|
+
```tsp
|
|
305
|
+
@route("/users")
|
|
306
|
+
interface Users {
|
|
307
|
+
@get list(): User[];
|
|
308
|
+
}
|
|
309
|
+
```
|
|
310
|
+
|
|
311
|
+
```text
|
|
312
|
+
warning @nxgt/typespec/list-returns-page: list returns an array: return Page<Item> or CursorPage<Item>, which can carry a total or a next cursor.
|
|
313
|
+
```
|
|
314
|
+
|
|
315
|
+
**Why:** an array cannot grow a `total` or a `nextCursor` later without
|
|
316
|
+
breaking its clients. A page can.
|
|
317
|
+
|
|
318
|
+
**Fix:** return a page ([Pagination](guide/pagination.md)):
|
|
319
|
+
|
|
320
|
+
```tsp
|
|
321
|
+
@get list(...PageParameters): Page<User>;
|
|
322
|
+
```
|
|
323
|
+
|
|
324
|
+
Or, for a list that will never page, silence that one operation:
|
|
325
|
+
|
|
326
|
+
```tsp
|
|
327
|
+
#suppress "@nxgt/typespec/list-returns-page" "a fixed set of ten"
|
|
328
|
+
@get @route("/top") listTop(): User[];
|
|
329
|
+
```
|
|
330
|
+
|
|
331
|
+
## `The service … has no @operationIds: its ids are the emitter's, such as Users_list`
|
|
332
|
+
|
|
333
|
+
**When:** a warning, with the ruleset `@nxgt/typespec/recommended` on: a
|
|
334
|
+
`@service` namespace is not marked with `@operationIds`, neither itself nor
|
|
335
|
+
a namespace around it. `@operationIds` on its interfaces alone does not
|
|
336
|
+
count.
|
|
337
|
+
|
|
338
|
+
```tsp
|
|
339
|
+
@service
|
|
340
|
+
namespace Shop {
|
|
341
|
+
@route("/health") @get op health(): void;
|
|
342
|
+
}
|
|
343
|
+
```
|
|
344
|
+
|
|
345
|
+
```text
|
|
346
|
+
warning @nxgt/typespec/service-operation-ids: The service Shop has no @operationIds: its ids are the emitter's, such as Users_list. Mark the namespace with @operationIds.
|
|
347
|
+
```
|
|
348
|
+
|
|
349
|
+
**Why:** the emitter names each operation after its interface,
|
|
350
|
+
`Users_list`, and the generated client's methods and types take that name.
|
|
351
|
+
|
|
352
|
+
**Fix:** mark the namespace ([Operation ids](guide/operation-ids.md)):
|
|
353
|
+
|
|
354
|
+
```tsp
|
|
355
|
+
@service
|
|
356
|
+
@operationIds
|
|
357
|
+
namespace Shop {
|
|
358
|
+
@route("/health") @get op health(): void;
|
|
359
|
+
}
|
|
360
|
+
```
|
|
361
|
+
|
|
362
|
+
Marking an existing service renames its operations, and the generated
|
|
363
|
+
client's methods with them. To keep the old ids, silence the rule on that
|
|
364
|
+
namespace:
|
|
365
|
+
|
|
366
|
+
```tsp
|
|
367
|
+
#suppress "@nxgt/typespec/service-operation-ids" "its clients already call Health_check"
|
|
368
|
+
@service
|
|
369
|
+
namespace Health {
|
|
370
|
+
@route("/health") @get op check(): void;
|
|
371
|
+
}
|
|
372
|
+
```
|
|
373
|
+
|
|
374
|
+
## `answers … with a body that is not the nxgt envelope`
|
|
375
|
+
|
|
376
|
+
**When:** a warning, with the ruleset `@nxgt/typespec/recommended` on: a
|
|
377
|
+
reply of status 400 or above, of a range from 400 up, or `default` (an
|
|
378
|
+
`@error` model without `@statusCode`) has a body that is not a model with
|
|
379
|
+
`status`, `message` and `timestamp`.
|
|
380
|
+
|
|
381
|
+
```tsp
|
|
382
|
+
model Teapot {
|
|
383
|
+
@statusCode _: 418;
|
|
384
|
+
@body body: { reason: string };
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
@route("/users")
|
|
388
|
+
interface Users {
|
|
389
|
+
@post create(@body user: User): User | Teapot;
|
|
390
|
+
}
|
|
391
|
+
```
|
|
392
|
+
|
|
393
|
+
```text
|
|
394
|
+
warning @nxgt/typespec/error-body-shape: create answers 418 with a body that is not the nxgt envelope: declare BadRequest, NotFound or another of the library's errors, or a body that spreads or extends ErrorBody<Status>.
|
|
395
|
+
```
|
|
396
|
+
|
|
397
|
+
A range is named `500-599`, and `default` `*`.
|
|
398
|
+
|
|
399
|
+
**Why:** `@nxgt/openapi-hono` answers its own 400 and 500 with the envelope,
|
|
400
|
+
and a client reads `message` from every error. Another shape is one more
|
|
401
|
+
case for each client.
|
|
402
|
+
|
|
403
|
+
**Fix:** declare one of the library's errors, `ErrorResponse<Status>`, or a
|
|
404
|
+
body that spreads or extends `ErrorBody<Status>` ([Error replies](guide/errors.md)):
|
|
405
|
+
|
|
406
|
+
```tsp
|
|
407
|
+
model TeapotBody {
|
|
408
|
+
...ErrorBody<418>;
|
|
409
|
+
reason: string;
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
@error
|
|
413
|
+
model Teapot {
|
|
414
|
+
@statusCode _: 418;
|
|
415
|
+
@body body: TeapotBody;
|
|
416
|
+
}
|
|
417
|
+
```
|
|
418
|
+
|
|
419
|
+
A reply without a body, such as `AuthenticationRequired`, passes. For a
|
|
420
|
+
spec whose errors have another shape on purpose, turn the rule off in
|
|
421
|
+
`tspconfig.yaml` ([Linter](guide/linter.md#turn-a-rule-off)):
|
|
422
|
+
|
|
423
|
+
```yaml
|
|
424
|
+
linter:
|
|
425
|
+
extends:
|
|
426
|
+
- '@nxgt/typespec/recommended'
|
|
427
|
+
disable:
|
|
428
|
+
'@nxgt/typespec/error-body-shape': 'the legacy routes answer their own errors'
|
|
429
|
+
```
|
|
430
|
+
|
|
289
431
|
## `Unknown decorator @operationId`
|
|
290
432
|
|
|
291
433
|
**When:** a spec writes `@operationId` next to `@operationIds`.
|
package/lib/errors.tsp
CHANGED
|
@@ -146,3 +146,31 @@ model ErrorResponse<Status extends integer> {
|
|
|
146
146
|
@statusCode _: Status;
|
|
147
147
|
@body body: ErrorBody<Status>;
|
|
148
148
|
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* The errors a list declares: a query its validators refuse.
|
|
152
|
+
*
|
|
153
|
+
* ```tsp
|
|
154
|
+
* @get list(...PageParameters): Page<User> | ListErrors;
|
|
155
|
+
* ```
|
|
156
|
+
*/
|
|
157
|
+
alias ListErrors = BadRequest;
|
|
158
|
+
|
|
159
|
+
/** The errors a read of one declares: nothing at that id. */
|
|
160
|
+
alias GetErrors = NotFound;
|
|
161
|
+
|
|
162
|
+
/** The errors a create declares: a body refused, or one that conflicts. */
|
|
163
|
+
alias CreateErrors = BadRequest | Conflict;
|
|
164
|
+
|
|
165
|
+
/** The errors an update declares: a body refused, nothing at that id, or a conflict. */
|
|
166
|
+
alias UpdateErrors = BadRequest | NotFound | Conflict;
|
|
167
|
+
|
|
168
|
+
/** The errors a delete declares: nothing at that id. */
|
|
169
|
+
alias DeleteErrors = NotFound;
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* The errors of an operation behind authentication, with the nxgt envelope.
|
|
173
|
+
* A route that answers them without a body, as Janus does, declares
|
|
174
|
+
* `AuthenticationRequired | AccessDenied` instead.
|
|
175
|
+
*/
|
|
176
|
+
alias AuthErrors = Unauthorized | Forbidden;
|