@nxgt/typespec 0.5.1 → 0.6.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +33 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +86 -10
- package/dist/index.js.map +9 -8
- package/dist/lib.d.ts +3 -0
- package/dist/lib.d.ts.map +1 -1
- package/dist/operation-ids.d.ts +7 -1
- package/dist/operation-ids.d.ts.map +1 -1
- package/dist/resource.d.ts +2 -0
- package/dist/resource.d.ts.map +1 -1
- package/dist/verb-methods.d.ts +13 -0
- package/dist/verb-methods.d.ts.map +1 -0
- package/dist/verbs.d.ts +15 -0
- package/dist/verbs.d.ts.map +1 -1
- package/docs/guide/operation-ids.md +89 -0
- package/docs/roadmap.md +12 -0
- package/docs/troubleshooting.md +49 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -325,7 +325,7 @@ interface Comments {
|
|
|
325
325
|
|
|
326
326
|
| Verbs | Take | In `Users` |
|
|
327
327
|
| --- | --- | --- |
|
|
328
|
-
| `list`, `read`, `find`, `search`, `count`, `createMany`, `updateMany`, `deleteMany` | the plural: the interface's name | `listUsers`, `deleteManyUsers` |
|
|
328
|
+
| `list`, `read`, `find`, `search`, `query`, `count`, `createMany`, `updateMany`, `deleteMany` | the plural: the interface's name | `listUsers`, `deleteManyUsers` |
|
|
329
329
|
| `get`, `create`, `update`, `patch`, `replace`, `upsert`, `delete` | the singular, by a short English rule | `createUser` |
|
|
330
330
|
| a verb, then `By…` | the singular before `By`; the plural after a `*Many` verb | `findUserById`, `deleteManyUsersByTeam` |
|
|
331
331
|
|
|
@@ -338,7 +338,29 @@ operation outside an interface keeps its name, even `list`. On an interface,
|
|
|
338
338
|
`@operationIds` names that interface's operations, and those of the
|
|
339
339
|
interfaces extending it. An operation's own `@operationId` wins; the others
|
|
340
340
|
keep the emitter's names. Two operations of one id in one service are an
|
|
341
|
-
error, `duplicate-operation-id`.
|
|
341
|
+
error, `duplicate-operation-id`.
|
|
342
|
+
|
|
343
|
+
A verb also names the method: an operation named after one of the library's
|
|
344
|
+
verbs, alone or before `By…`, and sent with another method is a warning,
|
|
345
|
+
`verb-method-mismatch`. A read is a `GET`, so a cache or a retry may treat
|
|
346
|
+
it as safe:
|
|
347
|
+
|
|
348
|
+
```tsp
|
|
349
|
+
@route("/users")
|
|
350
|
+
interface Users {
|
|
351
|
+
@post @route("/search") search(@body criteria: string): User[]; // fine: GET or POST
|
|
352
|
+
@post @route("/find") findById(@body id: uuid): User; // warning: find is GET or HEAD
|
|
353
|
+
}
|
|
354
|
+
```
|
|
355
|
+
|
|
356
|
+
```text
|
|
357
|
+
warning @nxgt/typespec/verb-method-mismatch: findById is sent with POST, where its verb find is sent with GET or HEAD. Give it that method, or a name that is not a verb.
|
|
358
|
+
```
|
|
359
|
+
|
|
360
|
+
Give it the method, a name that is not a verb, or a
|
|
361
|
+
`#suppress "@nxgt/typespec/verb-method-mismatch" "<reason>"` line above it.
|
|
362
|
+
The verbs `verbs` adds are not checked, nor an operation with its own
|
|
363
|
+
`@operationId`. More, with each verb's methods, in
|
|
342
364
|
[Operation ids](docs/guide/operation-ids.md).
|
|
343
365
|
|
|
344
366
|
## API
|
|
@@ -361,6 +383,7 @@ overrides verbs, on a namespace or an interface.
|
|
|
361
383
|
| `resource-name-on-namespace` | error: `@operationIds` on a namespace is given `singular` or `plural`, which name an interface's resource |
|
|
362
384
|
| `duplicate-status-reply` | error: an operation declares a reply without a body and one with a body of one status, which the emitter merges, losing the one without a body |
|
|
363
385
|
| `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
|
+
| `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` |
|
|
364
387
|
|
|
365
388
|
### Models
|
|
366
389
|
|
|
@@ -461,6 +484,12 @@ envelope and the rate limit's headers.
|
|
|
461
484
|
```tsp
|
|
462
485
|
@get @operationId("list") list(): User[];
|
|
463
486
|
```
|
|
487
|
+
|
|
488
|
+
Since 0.6.0, `query` is a verb too: `query` in `Users` was `query` and is
|
|
489
|
+
now `queryUsers`, and `queryByTeam` is `queryUserByTeam`.
|
|
490
|
+
`@operationId("query")` keeps the old one. A verb sent with a method it
|
|
491
|
+
does not name is now a warning, which fails a build run with
|
|
492
|
+
`--warn-as-error`.
|
|
464
493
|
- **Both versions at once write two folders.** With
|
|
465
494
|
`openapi-versions: ['3.1.0', '3.2.0']`, the emitter writes
|
|
466
495
|
`3.1.0/openapi.yaml` and `3.2.0/openapi.yaml`: point the generator's input
|
|
@@ -487,8 +516,8 @@ envelope and the rate limit's headers.
|
|
|
487
516
|
- [Sorting](docs/guide/sorting.md): `orderBy` and `direction` beside a
|
|
488
517
|
list's filters, and the `@nxgt/drizzle` call they map to;
|
|
489
518
|
- [Operation ids](docs/guide/operation-ids.md): `@operationIds`, each
|
|
490
|
-
operation named as written or a verb with its resource, the
|
|
491
|
-
the ids it refuses;
|
|
519
|
+
operation named as written or a verb with its resource, the method each
|
|
520
|
+
verb is sent with, the options, and the ids it refuses;
|
|
492
521
|
- [troubleshooting](docs/troubleshooting.md);
|
|
493
522
|
- [the roadmap](docs/roadmap.md): what is coming.
|
|
494
523
|
|
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;
|
|
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;AAE7B,wBAAgB,WAAW,CAAC,OAAO,EAAE,OAAO,GAAG,IAAI,CAMlD;AAED,eAAO,MAAM,WAAW;;;;CAEvB,CAAC"}
|
package/dist/index.js
CHANGED
|
@@ -21,6 +21,12 @@ var $lib = createTypeSpecLibrary({
|
|
|
21
21
|
default: "`singular` and `plural` name an interface's resource: a namespace has none. Put them on the interface."
|
|
22
22
|
}
|
|
23
23
|
},
|
|
24
|
+
"verb-method-mismatch": {
|
|
25
|
+
severity: "warning",
|
|
26
|
+
messages: {
|
|
27
|
+
default: 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.`
|
|
28
|
+
}
|
|
29
|
+
},
|
|
24
30
|
"duplicate-status-reply": {
|
|
25
31
|
severity: "error",
|
|
26
32
|
messages: {
|
|
@@ -112,6 +118,7 @@ var VERBS = {
|
|
|
112
118
|
read: "plural",
|
|
113
119
|
find: "plural",
|
|
114
120
|
search: "plural",
|
|
121
|
+
query: "plural",
|
|
115
122
|
count: "plural",
|
|
116
123
|
createMany: "plural",
|
|
117
124
|
updateMany: "plural",
|
|
@@ -124,6 +131,24 @@ var VERBS = {
|
|
|
124
131
|
upsert: "singular",
|
|
125
132
|
delete: "singular"
|
|
126
133
|
};
|
|
134
|
+
var METHODS = {
|
|
135
|
+
list: ["get", "head"],
|
|
136
|
+
read: ["get", "head"],
|
|
137
|
+
find: ["get", "head"],
|
|
138
|
+
count: ["get", "head"],
|
|
139
|
+
get: ["get", "head"],
|
|
140
|
+
search: ["get", "post"],
|
|
141
|
+
query: ["get", "post"],
|
|
142
|
+
create: ["post"],
|
|
143
|
+
createMany: ["post"],
|
|
144
|
+
update: ["put", "patch"],
|
|
145
|
+
updateMany: ["put", "patch"],
|
|
146
|
+
patch: ["patch"],
|
|
147
|
+
replace: ["put"],
|
|
148
|
+
upsert: ["put"],
|
|
149
|
+
delete: ["delete"],
|
|
150
|
+
deleteMany: ["delete", "post"]
|
|
151
|
+
};
|
|
127
152
|
var IRREGULAR = {
|
|
128
153
|
people: "person",
|
|
129
154
|
children: "child",
|
|
@@ -159,20 +184,26 @@ function singularOf(plural) {
|
|
|
159
184
|
return `${head}${word.slice(0, -1)}`;
|
|
160
185
|
return plural;
|
|
161
186
|
}
|
|
162
|
-
function
|
|
163
|
-
if (Object.hasOwn(verbs, name))
|
|
164
|
-
return
|
|
165
|
-
}
|
|
187
|
+
function verbIn(name, verbs) {
|
|
188
|
+
if (Object.hasOwn(verbs, name))
|
|
189
|
+
return { verb: name, rest: "" };
|
|
166
190
|
for (const by of name.matchAll(/By[A-Z]/g)) {
|
|
167
191
|
const verb = name.slice(0, by.index);
|
|
168
|
-
if (verb
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
const noun = verb.endsWith("Many") ? names.plural : names.singular;
|
|
172
|
-
return `${verb}${noun}${name.slice(by.index)}`;
|
|
192
|
+
if (verb !== "" && Object.hasOwn(verbs, verb)) {
|
|
193
|
+
return { verb, rest: name.slice(by.index) };
|
|
194
|
+
}
|
|
173
195
|
}
|
|
174
196
|
return;
|
|
175
197
|
}
|
|
198
|
+
function idWithVerb(name, resource, verbs) {
|
|
199
|
+
const found = verbIn(name, verbs);
|
|
200
|
+
if (found === undefined)
|
|
201
|
+
return;
|
|
202
|
+
const { verb, rest } = found;
|
|
203
|
+
const names = resource();
|
|
204
|
+
const noun = rest === "" ? names[verbs[verb]] : verb.endsWith("Many") ? names.plural : names.singular;
|
|
205
|
+
return `${verb}${noun}${rest}`;
|
|
206
|
+
}
|
|
176
207
|
|
|
177
208
|
// src/resource.ts
|
|
178
209
|
function optionsOf(program, target) {
|
|
@@ -200,6 +231,12 @@ function idOf(program, operation) {
|
|
|
200
231
|
return operation.name;
|
|
201
232
|
return idWithVerb(operation.name, () => resourceOf(program, container), verbsOf(program, container)) ?? operation.name;
|
|
202
233
|
}
|
|
234
|
+
function verbOf(program, operation) {
|
|
235
|
+
const container = operation.interface;
|
|
236
|
+
if (container === undefined)
|
|
237
|
+
return;
|
|
238
|
+
return verbIn(operation.name, verbsOf(program, container))?.verb;
|
|
239
|
+
}
|
|
203
240
|
|
|
204
241
|
// src/operation-ids.ts
|
|
205
242
|
function operationIds(context, target, options = {}) {
|
|
@@ -277,8 +314,47 @@ function validateOperationIds(program) {
|
|
|
277
314
|
});
|
|
278
315
|
}
|
|
279
316
|
|
|
317
|
+
// src/verb-methods.ts
|
|
318
|
+
import { getAllHttpServices as getAllHttpServices2 } from "@typespec/http";
|
|
319
|
+
import { getOperationId as getOperationId2 } from "@typespec/openapi";
|
|
320
|
+
function methodsOf(methods) {
|
|
321
|
+
return methods.map((method) => method.toUpperCase()).join(" or ");
|
|
322
|
+
}
|
|
323
|
+
function validateVerbMethods(program) {
|
|
324
|
+
const [services] = getAllHttpServices2(program);
|
|
325
|
+
const checked = new Set;
|
|
326
|
+
for (const service of services) {
|
|
327
|
+
for (const { operation, verb: method } of service.operations) {
|
|
328
|
+
if (checked.has(operation))
|
|
329
|
+
continue;
|
|
330
|
+
checked.add(operation);
|
|
331
|
+
if (!isNamed(program, operation))
|
|
332
|
+
continue;
|
|
333
|
+
if (getOperationId2(program, operation) !== undefined)
|
|
334
|
+
continue;
|
|
335
|
+
const verb = verbOf(program, operation);
|
|
336
|
+
if (verb === undefined || !Object.hasOwn(METHODS, verb))
|
|
337
|
+
continue;
|
|
338
|
+
const expected = METHODS[verb];
|
|
339
|
+
if (expected.includes(method))
|
|
340
|
+
continue;
|
|
341
|
+
$lib.reportDiagnostic(program, {
|
|
342
|
+
code: "verb-method-mismatch",
|
|
343
|
+
format: {
|
|
344
|
+
operation: operation.name,
|
|
345
|
+
method: method.toUpperCase(),
|
|
346
|
+
verb,
|
|
347
|
+
expected: methodsOf(expected)
|
|
348
|
+
},
|
|
349
|
+
target: operation
|
|
350
|
+
});
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
|
|
280
355
|
// src/index.ts
|
|
281
356
|
function $onValidate(program) {
|
|
357
|
+
validateVerbMethods(program);
|
|
282
358
|
validateOperationIds(program);
|
|
283
359
|
validateOneReplyPerStatus(program);
|
|
284
360
|
}
|
|
@@ -291,5 +367,5 @@ export {
|
|
|
291
367
|
$onValidate
|
|
292
368
|
};
|
|
293
369
|
|
|
294
|
-
//# debugId=
|
|
370
|
+
//# debugId=BED6F1BB763564AB64756E2164756E21
|
|
295
371
|
//# sourceMappingURL=index.js.map
|
package/dist/index.js.map
CHANGED
|
@@ -1,15 +1,16 @@
|
|
|
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/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/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
|
-
"/** 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'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. */\nfunction 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 */\
|
|
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\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/** 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 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\
|
|
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} 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",
|
|
10
|
-
"/**\n *
|
|
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. */\nfunction 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 */\nfunction 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
|
+
"/**\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
|
+
"/**\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
|
+
"/**\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 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';\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"
|
|
11
12
|
],
|
|
12
|
-
"mappings": ";AASA;AACA;AAAA;AAAA;;;ACTA;AAEO,IAAM,OAAO,sBAAsB;AAAA,EACzC,MAAM;AAAA,EACN,aAAa;AAAA,IACZ,0BAA0B;AAAA,MACzB,UAAU;AAAA,MACV,UAAU;AAAA,QACT,SAAS,wCAAwC;AAAA,MAClD;AAAA,IACD;AAAA,IACA,8BAA8B;AAAA,MAC7B,UAAU;AAAA,MACV,UAAU;AAAA,QACT,SACC;AAAA,MACF;AAAA,IACD;AAAA,IACA,0BAA0B;AAAA,MACzB,UAAU;AAAA,MACV,UAAU;AAAA,QACT,SAAS,eAAe,6EAA6E;AAAA,MACtG;AAAA,IACD;AAAA,IACA,uBAAuB;AAAA,MACtB,UAAU;AAAA,MACV,UAAU;AAAA,QACT,SAAS,eAAe,0DAA0D;AAAA,MACnF;AAAA,IACD;AAAA,EACD;AAAA,EACA,OAAO;AAAA,IACN,cAAc;AAAA,MACb,aAAa;AAAA,IACd;AAAA,IACA,qBAAqB;AAAA,MACpB,aACC;AAAA,IACF;AAAA,EACD;AACD,CAAC;;;
|
|
13
|
-
"debugId": "
|
|
13
|
+
"mappings": "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",
|
|
14
|
+
"debugId": "BED6F1BB763564AB64756E2164756E21",
|
|
14
15
|
"names": []
|
|
15
16
|
}
|
package/dist/lib.d.ts
CHANGED
|
@@ -5,6 +5,9 @@ export declare const $lib: import("@typespec/compiler").TypeSpecLibrary<{
|
|
|
5
5
|
'resource-name-on-namespace': {
|
|
6
6
|
readonly default: "`singular` and `plural` name an interface's resource: a namespace has none. Put them on the interface.";
|
|
7
7
|
};
|
|
8
|
+
'verb-method-mismatch': {
|
|
9
|
+
readonly default: import("@typespec/compiler").CallableMessage<["operation", "method", "verb", "expected"]>;
|
|
10
|
+
};
|
|
8
11
|
'duplicate-status-reply': {
|
|
9
12
|
readonly default: import("@typespec/compiler").CallableMessage<["operation", "status"]>;
|
|
10
13
|
};
|
package/dist/lib.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"lib.d.ts","sourceRoot":"","sources":["../src/lib.ts"],"names":[],"mappings":"AAGA,eAAO,MAAM,IAAI
|
|
1
|
+
{"version":3,"file":"lib.d.ts","sourceRoot":"","sources":["../src/lib.ts"],"names":[],"mappings":"AAGA,eAAO,MAAM,IAAI;;;;;;;;;;;;;;;;+DA4Cf,CAAC"}
|
package/dist/operation-ids.d.ts
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* would write `Posts_findPostComments`. In an interface, a known verb takes
|
|
5
5
|
* the interface's resource: `create` in `Users` is `createUser` (`verbs.ts`).
|
|
6
6
|
*/
|
|
7
|
-
import { type DecoratorContext, type Interface, type Namespace, type Program } from '@typespec/compiler';
|
|
7
|
+
import { type DecoratorContext, type Interface, type Namespace, type Operation, type Program } from '@typespec/compiler';
|
|
8
8
|
import { type OperationIdsOptions } from './resource';
|
|
9
9
|
/**
|
|
10
10
|
* Marks the interface or the namespace; its ids are set once the program is
|
|
@@ -13,6 +13,12 @@ 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
|
+
/**
|
|
17
|
+
* Whether `@operationIds` names the operation's id, or checks it: its
|
|
18
|
+
* interface or one of its namespaces is marked. An interface template's
|
|
19
|
+
* instance is never emitted; only the interfaces extending it are.
|
|
20
|
+
*/
|
|
21
|
+
export declare function isNamed(program: Program, operation: Operation): boolean;
|
|
16
22
|
/**
|
|
17
23
|
* Sets the ids, after every decorator, so the order of `@operationId` and
|
|
18
24
|
* `@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,
|
|
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;AAqBD;;;;GAIG;AACH,wBAAgB,OAAO,CAAC,OAAO,EAAE,OAAO,EAAE,SAAS,EAAE,SAAS,GAAG,OAAO,CAUvE;AAgCD;;;;;;;;;GASG;AACH,wBAAgB,oBAAoB,CAAC,OAAO,EAAE,OAAO,GAAG,IAAI,CAqC3D"}
|
package/dist/resource.d.ts
CHANGED
|
@@ -13,4 +13,6 @@ export interface OperationIdsOptions {
|
|
|
13
13
|
}
|
|
14
14
|
/** The id `@operationIds` gives: the name as written, or a verb with its resource. */
|
|
15
15
|
export declare function idOf(program: Program, operation: Operation): string;
|
|
16
|
+
/** The verb an operation of an interface is named with, `find` in `findById`. */
|
|
17
|
+
export declare function verbOf(program: Program, operation: Operation): string | undefined;
|
|
16
18
|
//# sourceMappingURL=resource.d.ts.map
|
package/dist/resource.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"resource.d.ts","sourceRoot":"","sources":["../src/resource.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AACH,OAAO,KAAK,EAGX,SAAS,EACT,OAAO,EACP,MAAM,oBAAoB,CAAC;AAE5B,OAAO,EACN,KAAK,OAAO,
|
|
1
|
+
{"version":3,"file":"resource.d.ts","sourceRoot":"","sources":["../src/resource.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AACH,OAAO,KAAK,EAGX,SAAS,EACT,OAAO,EACP,MAAM,oBAAoB,CAAC;AAE5B,OAAO,EACN,KAAK,OAAO,EAMZ,MAAM,SAAS,CAAC;AAEjB,wEAAwE;AACxE,MAAM,WAAW,mBAAmB;IACnC,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,KAAK,CAAC,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAAC;CACnD;AA2CD,sFAAsF;AACtF,wBAAgB,IAAI,CAAC,OAAO,EAAE,OAAO,EAAE,SAAS,EAAE,SAAS,GAAG,MAAM,CAUnE;AAED,iFAAiF;AACjF,wBAAgB,MAAM,CACrB,OAAO,EAAE,OAAO,EAChB,SAAS,EAAE,SAAS,GAClB,MAAM,GAAG,SAAS,CAIpB"}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A verb names what an operation does, and its method says the same:
|
|
3
|
+
* `create` in `Users` is `createUser`, and a `GET` would tell a cache, a
|
|
4
|
+
* proxy or a retry it is safe. An operation `@operationIds` names after one
|
|
5
|
+
* of the library's verbs is sent with that verb's methods (`verbs.ts`), or
|
|
6
|
+
* warned of. A verb `verbs` adds has no method to check, and an operation's
|
|
7
|
+
* own `@operationId` is an id that is no verb. It runs before
|
|
8
|
+
* `validateOperationIds` sets the others.
|
|
9
|
+
*/
|
|
10
|
+
import type { Program } from '@typespec/compiler';
|
|
11
|
+
/** Checks each operation of each service once, as the emitter sends it. */
|
|
12
|
+
export declare function validateVerbMethods(program: Program): void;
|
|
13
|
+
//# sourceMappingURL=verb-methods.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"verb-methods.d.ts","sourceRoot":"","sources":["../src/verb-methods.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AACH,OAAO,KAAK,EAAa,OAAO,EAAE,MAAM,oBAAoB,CAAC;AAa7D,2EAA2E;AAC3E,wBAAgB,mBAAmB,CAAC,OAAO,EAAE,OAAO,GAAG,IAAI,CAyB1D"}
|
package/dist/verbs.d.ts
CHANGED
|
@@ -12,12 +12,27 @@ export interface Resource {
|
|
|
12
12
|
}
|
|
13
13
|
/** The library's own verbs. */
|
|
14
14
|
export declare const VERBS: Readonly<Record<string, Grammar>>;
|
|
15
|
+
/**
|
|
16
|
+
* The HTTP methods each of the library's verbs is sent with: a read is a
|
|
17
|
+
* `GET` (or a `HEAD`), a search may carry its criteria in a `POST` body, a
|
|
18
|
+
* bulk delete its ids. `QUERY` joins `search` and `query` once
|
|
19
|
+
* `@typespec/http` declares it.
|
|
20
|
+
*/
|
|
21
|
+
export declare const METHODS: Readonly<Record<string, readonly string[]>>;
|
|
15
22
|
/**
|
|
16
23
|
* The singular of a plural name, by a short English rule on its last word:
|
|
17
24
|
* `Users`, `BlogPosts`, `Categories`, `Addresses`, `Statuses`, `People`. A
|
|
18
25
|
* guess: it misses some (`Heroes`, `Analysis`), which `singular` names.
|
|
19
26
|
*/
|
|
20
27
|
export declare function singularOf(plural: string): string;
|
|
28
|
+
/**
|
|
29
|
+
* The verb a name starts with, and the rest: the whole name, `list`, or the
|
|
30
|
+
* verb before a `By`, `findById`. `undefined` when the name is neither.
|
|
31
|
+
*/
|
|
32
|
+
export declare function verbIn(name: string, verbs: Readonly<Record<string, Grammar>>): {
|
|
33
|
+
readonly verb: string;
|
|
34
|
+
readonly rest: string;
|
|
35
|
+
} | undefined;
|
|
21
36
|
/**
|
|
22
37
|
* The id of an operation of a resource: the verb and the resource's singular
|
|
23
38
|
* or plural, or the verb, the resource and the rest of a `By` name:
|
package/dist/verbs.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"verbs.d.ts","sourceRoot":"","sources":["../src/verbs.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,kEAAkE;AAClE,MAAM,MAAM,OAAO,GAAG,UAAU,GAAG,QAAQ,CAAC;AAE5C,kDAAkD;AAClD,MAAM,WAAW,QAAQ;IACxB,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;CACxB;AAED,+BAA+B;AAC/B,eAAO,MAAM,KAAK,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,
|
|
1
|
+
{"version":3,"file":"verbs.d.ts","sourceRoot":"","sources":["../src/verbs.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,kEAAkE;AAClE,MAAM,MAAM,OAAO,GAAG,UAAU,GAAG,QAAQ,CAAC;AAE5C,kDAAkD;AAClD,MAAM,WAAW,QAAQ;IACxB,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;CACxB;AAED,+BAA+B;AAC/B,eAAO,MAAM,KAAK,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAiBnD,CAAC;AAEF;;;;;GAKG;AACH,eAAO,MAAM,OAAO,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,SAAS,MAAM,EAAE,CAAC,CAiB/D,CAAC;AAyBF;;;;GAIG;AACH,wBAAgB,UAAU,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,CAgBjD;AAED;;;GAGG;AACH,wBAAgB,MAAM,CACrB,IAAI,EAAE,MAAM,EACZ,KAAK,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,GACtC;IAAE,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;CAAE,GAAG,SAAS,CAS9D;AAED;;;;;;GAMG;AACH,wBAAgB,UAAU,CACzB,IAAI,EAAE,MAAM,EACZ,QAAQ,EAAE,MAAM,QAAQ,EACxB,KAAK,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,GACtC,MAAM,GAAG,SAAS,CAYpB"}
|
|
@@ -70,6 +70,7 @@ with the interface's resource:
|
|
|
70
70
|
| `read` | the plural | `readUsers` | `readCategories` |
|
|
71
71
|
| `find` | the plural | `findUsers` | `findCategories` |
|
|
72
72
|
| `search` | the plural | `searchUsers` | `searchCategories` |
|
|
73
|
+
| `query` | the plural | `queryUsers` | `queryCategories` |
|
|
73
74
|
| `count` | the plural | `countUsers` | `countCategories` |
|
|
74
75
|
| `createMany` | the plural | `createManyUsers` | `createManyCategories` |
|
|
75
76
|
| `updateMany` | the plural | `updateManyUsers` | `updateManyCategories` |
|
|
@@ -105,6 +106,76 @@ Outside an interface, the id is always the name as written, even a verb:
|
|
|
105
106
|
@route("/health") @get op list(): void; // list
|
|
106
107
|
```
|
|
107
108
|
|
|
109
|
+
## Verbs and methods
|
|
110
|
+
|
|
111
|
+
A verb names what an operation does, and so does its HTTP method: a `GET`
|
|
112
|
+
tells a cache, a proxy or a retry that the request is safe. An operation
|
|
113
|
+
`@operationIds` names after one of the library's verbs, alone or before
|
|
114
|
+
`By…`, is sent with one of that verb's methods, or the compiler warns:
|
|
115
|
+
|
|
116
|
+
| Verb | Sent with |
|
|
117
|
+
| --- | --- |
|
|
118
|
+
| `list`, `read`, `find`, `count`, `get` | `GET` or `HEAD` |
|
|
119
|
+
| `search`, `query` | `GET` or `POST`, for criteria too large for a query string |
|
|
120
|
+
| `create`, `createMany` | `POST` |
|
|
121
|
+
| `update`, `updateMany` | `PUT` or `PATCH` |
|
|
122
|
+
| `patch` | `PATCH` |
|
|
123
|
+
| `replace`, `upsert` | `PUT` |
|
|
124
|
+
| `delete` | `DELETE` |
|
|
125
|
+
| `deleteMany` | `DELETE` or `POST`, for ids in a body |
|
|
126
|
+
|
|
127
|
+
The method is the one `@typespec/http` gives the operation, the implicit one
|
|
128
|
+
included: an operation without `@get` or `@post` is a `GET`, or a `POST`
|
|
129
|
+
when it has a body.
|
|
130
|
+
|
|
131
|
+
```tsp
|
|
132
|
+
@route("/users")
|
|
133
|
+
interface Users {
|
|
134
|
+
@get list(...PageParameters): Page<User>; // GET: fine
|
|
135
|
+
@post @route("/search") search(@body criteria: string): User[]; // POST: fine
|
|
136
|
+
@post @route("/{id}") update(@path id: uuid, @body user: User): User; // warning
|
|
137
|
+
@route("/find") findById(@body id: uuid): User; // warning: a body makes it a POST
|
|
138
|
+
}
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
```text
|
|
142
|
+
warning @nxgt/typespec/verb-method-mismatch: update is sent with POST, where its verb update is sent with PUT or PATCH. Give it that method, or a name that is not a verb.
|
|
143
|
+
warning @nxgt/typespec/verb-method-mismatch: findById is sent with POST, where its verb find is sent with GET or HEAD. Give it that method, or a name that is not a verb.
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
Give the operation its verb's method, or a name that is not a verb, which
|
|
147
|
+
is then its id as written:
|
|
148
|
+
|
|
149
|
+
```tsp
|
|
150
|
+
@patch @route("/{id}") update(@path id: uuid, @body user: User): User; // updateUser
|
|
151
|
+
@post @route("/lookup") lookUpUser(@body id: uuid): User; // lookUpUser
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
When the route cannot change, silence that one operation with `#suppress`
|
|
155
|
+
and a reason; the id is unchanged:
|
|
156
|
+
|
|
157
|
+
```tsp
|
|
158
|
+
#suppress "@nxgt/typespec/verb-method-mismatch" "an older client posts its lookups"
|
|
159
|
+
@post @route("/lookup") getByEmail(@body email: email): User; // getUserByEmail
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
Not checked:
|
|
163
|
+
|
|
164
|
+
- a verb the `verbs` option adds, `archive` in
|
|
165
|
+
`#{ verbs: #{ archive: "singular" } }`: it has no method of its own. A
|
|
166
|
+
library verb the option only changes, `#{ verbs: #{ query: "singular" } }`,
|
|
167
|
+
keeps its methods and is checked;
|
|
168
|
+
- a name that is not a verb, `listActive` or `findPostComments`;
|
|
169
|
+
- an operation outside an interface, whose name is its id, even `list`;
|
|
170
|
+
- an operation with its own `@operationId`, whose id is as written,
|
|
171
|
+
whatever it says;
|
|
172
|
+
- an operation `@operationIds` does not name.
|
|
173
|
+
|
|
174
|
+
`QUERY`, the HTTP method for a safe request with a body, is not one of
|
|
175
|
+
`search` and `query`'s methods yet: `@typespec/http` 1.16 declares only
|
|
176
|
+
`GET`, `PUT`, `POST`, `PATCH`, `DELETE` and `HEAD`
|
|
177
|
+
([roadmap](../roadmap.md#later)).
|
|
178
|
+
|
|
108
179
|
## The resource
|
|
109
180
|
|
|
110
181
|
The plural is the interface's name. The singular comes from a short English
|
|
@@ -281,6 +352,24 @@ interface that declared both `list` and `listUsers` now names two operations
|
|
|
281
352
|
`listUsers`, which is an error: rename one, or give `list` its
|
|
282
353
|
`@operationId`.
|
|
283
354
|
|
|
355
|
+
### Upgrading from 0.5
|
|
356
|
+
|
|
357
|
+
`query` is a verb since 0.6.0. An operation named `query` in a marked
|
|
358
|
+
interface was `query`, and is now `queryUsers` in `Users`; `queryByTeam` is
|
|
359
|
+
now `queryUserByTeam`. Keep the old id with an `@operationId`:
|
|
360
|
+
|
|
361
|
+
```tsp
|
|
362
|
+
@route("/users")
|
|
363
|
+
interface Users {
|
|
364
|
+
@get @operationId("query") query(): User[]; // query, as before
|
|
365
|
+
}
|
|
366
|
+
```
|
|
367
|
+
|
|
368
|
+
An operation named after a verb and sent with another method now warns,
|
|
369
|
+
`verb-method-mismatch`: see [Verbs and methods](#verbs-and-methods). It is a
|
|
370
|
+
warning, so the spec still compiles, unless `tsp compile` runs with
|
|
371
|
+
`--warn-as-error`.
|
|
372
|
+
|
|
284
373
|
## On a template
|
|
285
374
|
|
|
286
375
|
An interface template is never emitted, only the interfaces that extend one
|
package/docs/roadmap.md
CHANGED
|
@@ -10,12 +10,24 @@ Nothing in progress.
|
|
|
10
10
|
|
|
11
11
|
## Later
|
|
12
12
|
|
|
13
|
+
- **`QUERY` for `search` and `query`.** The HTTP `QUERY` method, a safe
|
|
14
|
+
request with a body, joins `GET` and `POST` as a method of `search` and
|
|
15
|
+
`query` once `@typespec/http` declares it. `@nxgt/openapi-codegen`
|
|
16
|
+
already reads an OpenAPI 3.2 `query` operation.
|
|
13
17
|
- **`ETag` and conditional requests.** `ETag`, with `If-None-Match` and
|
|
14
18
|
`If-Match`, once the `@nxgt/*` packages send one the same way. The
|
|
15
19
|
optimistic lock is the body's `version` today (`Versioned`).
|
|
16
20
|
|
|
17
21
|
## Shipped
|
|
18
22
|
|
|
23
|
+
### 0.6.0
|
|
24
|
+
|
|
25
|
+
- **Verbs name their methods.** An operation named after one of the
|
|
26
|
+
library's verbs and sent with another method is a warning,
|
|
27
|
+
`verb-method-mismatch`: `create` is a `POST`, `list` a `GET` or a `HEAD`,
|
|
28
|
+
`search` a `GET` or a `POST`. `#suppress` silences one operation.
|
|
29
|
+
- **`query`, a verb.** `query` in `Users` is `queryUsers`, like `search`.
|
|
30
|
+
|
|
19
31
|
### 0.5.0
|
|
20
32
|
|
|
21
33
|
- **Verbs in operation ids.** In an interface `@operationIds` marks, a known
|
package/docs/troubleshooting.md
CHANGED
|
@@ -182,6 +182,55 @@ singular, `archiveUser`, or the plural, `archiveUsers`.
|
|
|
182
182
|
namespace Shop;
|
|
183
183
|
```
|
|
184
184
|
|
|
185
|
+
## `findById is sent with POST, where its verb find is sent with GET or HEAD`
|
|
186
|
+
|
|
187
|
+
**When:** a warning: an operation `@operationIds` names after one of the
|
|
188
|
+
library's verbs, in an interface, alone or before `By…`, is sent with a
|
|
189
|
+
method that verb does not name. The method may be implicit: an operation
|
|
190
|
+
without `@get` or `@post` that has a body is a `POST`.
|
|
191
|
+
|
|
192
|
+
```tsp
|
|
193
|
+
@route("/users")
|
|
194
|
+
interface Users {
|
|
195
|
+
@route("/find") findById(@body id: uuid): User;
|
|
196
|
+
}
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
```text
|
|
200
|
+
warning @nxgt/typespec/verb-method-mismatch: findById is sent with POST, where its verb find is sent with GET or HEAD. Give it that method, or a name that is not a verb.
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
**Why:** the verb and the method say the same thing twice, and here they
|
|
204
|
+
disagree: `find` is a read, and a `POST` tells a cache, a proxy or a retry
|
|
205
|
+
it is not safe. Each verb's methods are in
|
|
206
|
+
[Operation ids](guide/operation-ids.md#verbs-and-methods).
|
|
207
|
+
|
|
208
|
+
**Fix:** give it the verb's method:
|
|
209
|
+
|
|
210
|
+
```tsp
|
|
211
|
+
@get @route("/{id}") findById(@path id: uuid): User; // findUserById
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
Or a name that is not a verb, which is its id as written:
|
|
215
|
+
|
|
216
|
+
```tsp
|
|
217
|
+
@post @route("/find") lookUpUser(@body id: uuid): User; // lookUpUser
|
|
218
|
+
```
|
|
219
|
+
|
|
220
|
+
Or, when the route cannot change, its own `@operationId`, which keeps the
|
|
221
|
+
id it had and is not checked:
|
|
222
|
+
|
|
223
|
+
```tsp
|
|
224
|
+
@post @route("/find") @operationId("findById") findById(@body id: uuid): User;
|
|
225
|
+
```
|
|
226
|
+
|
|
227
|
+
Or silence that one operation with a reason:
|
|
228
|
+
|
|
229
|
+
```tsp
|
|
230
|
+
#suppress "@nxgt/typespec/verb-method-mismatch" "an older client posts its lookups"
|
|
231
|
+
@post @route("/find") findById(@body id: uuid): User;
|
|
232
|
+
```
|
|
233
|
+
|
|
185
234
|
## `declares a reply without a body and one with a body of status …`
|
|
186
235
|
|
|
187
236
|
**When:** an operation declares, for one status, a reply without a body and
|