@nxgt/typespec 0.4.1 → 0.5.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 +43 -9
- package/dist/index.js +118 -4
- package/dist/index.js.map +7 -5
- package/dist/lib.d.ts +4 -1
- package/dist/lib.d.ts.map +1 -1
- package/dist/operation-ids.d.ts +6 -3
- package/dist/operation-ids.d.ts.map +1 -1
- package/dist/resource.d.ts +16 -0
- package/dist/resource.d.ts.map +1 -0
- package/dist/verbs.d.ts +29 -0
- package/dist/verbs.d.ts.map +1 -0
- package/docs/README.md +1 -1
- package/docs/guide/operation-ids.md +217 -40
- package/docs/guide/sorting.md +5 -5
- package/docs/roadmap.md +11 -0
- package/docs/troubleshooting.md +87 -9
- package/lib/operation-ids.tsp +29 -6
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -302,8 +302,8 @@ with the handler, in [Sorting](docs/guide/sorting.md).
|
|
|
302
302
|
|
|
303
303
|
TypeSpec names an operation after its interface, `Posts_listPosts`, and the
|
|
304
304
|
generator turns that id into the client's method and its types' prefix. Put
|
|
305
|
-
`@operationIds` on the service namespace
|
|
306
|
-
|
|
305
|
+
`@operationIds` on the service namespace: in an interface, a known verb takes
|
|
306
|
+
the interface's resource, and any other name is the id as written:
|
|
307
307
|
|
|
308
308
|
```tsp
|
|
309
309
|
@service(#{ title: "Blog" })
|
|
@@ -312,15 +312,33 @@ namespace Blog;
|
|
|
312
312
|
|
|
313
313
|
@route("/posts")
|
|
314
314
|
interface Posts {
|
|
315
|
-
@get
|
|
316
|
-
@get
|
|
315
|
+
@get list(...PageParameters): Page<Post>; // listPosts
|
|
316
|
+
@get @route("/{postId}") get(@path postId: uuid): Post | NotFound; // getPost
|
|
317
|
+
@get @route("/by-slug/{slug}") findBySlug(@path slug: string): Post | NotFound; // findPostBySlug
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
@route("/posts/{postId}/comments")
|
|
321
|
+
interface Comments {
|
|
322
|
+
@get findPostComments(@path postId: uuid, ...CursorPageParameters): CursorPage<Comment>; // findPostComments
|
|
317
323
|
}
|
|
318
324
|
```
|
|
319
325
|
|
|
320
|
-
|
|
326
|
+
| Verbs | Take | In `Users` |
|
|
327
|
+
| --- | --- | --- |
|
|
328
|
+
| `list`, `read`, `find`, `search`, `count`, `createMany`, `updateMany`, `deleteMany` | the plural: the interface's name | `listUsers`, `deleteManyUsers` |
|
|
329
|
+
| `get`, `create`, `update`, `patch`, `replace`, `upsert`, `delete` | the singular, by a short English rule | `createUser` |
|
|
330
|
+
| a verb, then `By…` | the singular before `By`; the plural after a `*Many` verb | `findUserById`, `deleteManyUsersByTeam` |
|
|
331
|
+
|
|
332
|
+
The singular rule knows `Categories`, `Addresses`, `Statuses` and `People`;
|
|
333
|
+
name the others with `@operationIds(#{ singular: "Member" })` on the
|
|
334
|
+
interface, and add verbs with `#{ verbs: #{ archive: "singular" } }`. Verbs
|
|
335
|
+
carry down: an interface has those of its namespaces, outermost first, then
|
|
336
|
+
those of the interfaces it extends, then its own, and the last one wins. An
|
|
337
|
+
operation outside an interface keeps its name, even `list`. On an interface,
|
|
338
|
+
`@operationIds` names that interface's operations, and those of the
|
|
321
339
|
interfaces extending it. An operation's own `@operationId` wins; the others
|
|
322
|
-
keep the emitter's names. Two operations of one
|
|
323
|
-
|
|
340
|
+
keep the emitter's names. Two operations of one id in one service are an
|
|
341
|
+
error, `duplicate-operation-id`. More in
|
|
324
342
|
[Operation ids](docs/guide/operation-ids.md).
|
|
325
343
|
|
|
326
344
|
## API
|
|
@@ -329,13 +347,18 @@ interfaces or copied by `extends`, are an error, `duplicate-operation-id`. More
|
|
|
329
347
|
|
|
330
348
|
| Decorator | On | What it does |
|
|
331
349
|
| --- | --- | --- |
|
|
332
|
-
| `@operationIds` | a namespace or an interface | makes each operation's id its name, as written, in the namespace however deep or in the interface, unless it has an `@operationId` |
|
|
350
|
+
| `@operationIds(options?: OperationIdsOptions)` | a namespace or an interface | makes each operation's id its name, as written, or a known verb with the interface's resource (`list` in `Users` is `listUsers`), in the namespace however deep or in the interface, unless it has an `@operationId` |
|
|
351
|
+
|
|
352
|
+
`OperationIdsOptions` is `#{ singular?: string, plural?: string, verbs?: Record<"singular" | "plural"> }`:
|
|
353
|
+
`singular` and `plural` name an interface's resource, and `verbs` adds or
|
|
354
|
+
overrides verbs, on a namespace or an interface.
|
|
333
355
|
|
|
334
356
|
### Diagnostics
|
|
335
357
|
|
|
336
358
|
| Code | Reported when |
|
|
337
359
|
| --- | --- |
|
|
338
360
|
| `duplicate-operation-id` | error: an operation `@operationIds` names has the id of another operation in its service's document |
|
|
361
|
+
| `resource-name-on-namespace` | error: `@operationIds` on a namespace is given `singular` or `plural`, which name an interface's resource |
|
|
339
362
|
| `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 |
|
|
340
363
|
| `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 |
|
|
341
364
|
|
|
@@ -428,6 +451,16 @@ envelope and the rate limit's headers.
|
|
|
428
451
|
Two plain bodies, `Post | Draft`, bodies of different content types, and
|
|
429
452
|
two `@error` models without a `@statusCode` pass
|
|
430
453
|
([troubleshooting](docs/troubleshooting.md)).
|
|
454
|
+
- **An operation named exactly a verb, or a verb followed by `By…`,
|
|
455
|
+
changes id on upgrade.** Before 0.5.0, `list` in a marked `Users` was
|
|
456
|
+
`list` and `findById` was `findById`; they are now `listUsers` and
|
|
457
|
+
`findUserById`, and the generated client's methods follow. A `list`
|
|
458
|
+
beside a `listUsers` now collides with it. Keep the old id with its own
|
|
459
|
+
`@operationId` (from `@typespec/openapi`):
|
|
460
|
+
|
|
461
|
+
```tsp
|
|
462
|
+
@get @operationId("list") list(): User[];
|
|
463
|
+
```
|
|
431
464
|
- **Both versions at once write two folders.** With
|
|
432
465
|
`openapi-versions: ['3.1.0', '3.2.0']`, the emitter writes
|
|
433
466
|
`3.1.0/openapi.yaml` and `3.2.0/openapi.yaml`: point the generator's input
|
|
@@ -454,7 +487,8 @@ envelope and the rate limit's headers.
|
|
|
454
487
|
- [Sorting](docs/guide/sorting.md): `orderBy` and `direction` beside a
|
|
455
488
|
list's filters, and the `@nxgt/drizzle` call they map to;
|
|
456
489
|
- [Operation ids](docs/guide/operation-ids.md): `@operationIds`, each
|
|
457
|
-
operation named as written,
|
|
490
|
+
operation named as written or a verb with its resource, the options, and
|
|
491
|
+
the ids it refuses;
|
|
458
492
|
- [troubleshooting](docs/troubleshooting.md);
|
|
459
493
|
- [the roadmap](docs/roadmap.md): what is coming.
|
|
460
494
|
|
package/dist/index.js
CHANGED
|
@@ -15,6 +15,12 @@ var $lib = createTypeSpecLibrary({
|
|
|
15
15
|
default: paramMessage`Two operations are named ${"id"}: an OpenAPI operation id must be unique.`
|
|
16
16
|
}
|
|
17
17
|
},
|
|
18
|
+
"resource-name-on-namespace": {
|
|
19
|
+
severity: "error",
|
|
20
|
+
messages: {
|
|
21
|
+
default: "`singular` and `plural` name an interface's resource: a namespace has none. Put them on the interface."
|
|
22
|
+
}
|
|
23
|
+
},
|
|
18
24
|
"duplicate-status-reply": {
|
|
19
25
|
severity: "error",
|
|
20
26
|
messages: {
|
|
@@ -31,6 +37,9 @@ var $lib = createTypeSpecLibrary({
|
|
|
31
37
|
state: {
|
|
32
38
|
operationIds: {
|
|
33
39
|
description: "The interfaces and namespaces marked with @operationIds"
|
|
40
|
+
},
|
|
41
|
+
operationIdsOptions: {
|
|
42
|
+
description: "Each marked interface's or namespace's @operationIds options"
|
|
34
43
|
}
|
|
35
44
|
}
|
|
36
45
|
});
|
|
@@ -96,8 +105,113 @@ import {
|
|
|
96
105
|
resolveOperationId,
|
|
97
106
|
setOperationId
|
|
98
107
|
} from "@typespec/openapi";
|
|
99
|
-
|
|
100
|
-
|
|
108
|
+
|
|
109
|
+
// src/verbs.ts
|
|
110
|
+
var VERBS = {
|
|
111
|
+
list: "plural",
|
|
112
|
+
read: "plural",
|
|
113
|
+
find: "plural",
|
|
114
|
+
search: "plural",
|
|
115
|
+
count: "plural",
|
|
116
|
+
createMany: "plural",
|
|
117
|
+
updateMany: "plural",
|
|
118
|
+
deleteMany: "plural",
|
|
119
|
+
get: "singular",
|
|
120
|
+
create: "singular",
|
|
121
|
+
update: "singular",
|
|
122
|
+
patch: "singular",
|
|
123
|
+
replace: "singular",
|
|
124
|
+
upsert: "singular",
|
|
125
|
+
delete: "singular"
|
|
126
|
+
};
|
|
127
|
+
var IRREGULAR = {
|
|
128
|
+
people: "person",
|
|
129
|
+
children: "child",
|
|
130
|
+
men: "man",
|
|
131
|
+
women: "woman",
|
|
132
|
+
mice: "mouse",
|
|
133
|
+
feet: "foot",
|
|
134
|
+
teeth: "tooth",
|
|
135
|
+
geese: "goose",
|
|
136
|
+
movies: "movie",
|
|
137
|
+
cookies: "cookie",
|
|
138
|
+
series: "series",
|
|
139
|
+
species: "species",
|
|
140
|
+
news: "news"
|
|
141
|
+
};
|
|
142
|
+
var US_ES = /(stat|bon|camp|vir|b|foc|cens|corp|radi|syllab)uses$/i;
|
|
143
|
+
var SIBILANT_ES = /(ss|x|zz|sh|tch|nch|rch|oach)es$/i;
|
|
144
|
+
function singularOf(plural) {
|
|
145
|
+
const word = /[A-Z]?[a-z0-9]*$/.exec(plural)?.[0] ?? "";
|
|
146
|
+
const head = plural.slice(0, plural.length - word.length);
|
|
147
|
+
const key = word.toLowerCase();
|
|
148
|
+
const irregular = Object.hasOwn(IRREGULAR, key) ? IRREGULAR[key] : undefined;
|
|
149
|
+
if (irregular !== undefined) {
|
|
150
|
+
const cased = word[0] === word[0]?.toUpperCase();
|
|
151
|
+
return `${head}${cased ? irregular[0]?.toUpperCase() + irregular.slice(1) : irregular}`;
|
|
152
|
+
}
|
|
153
|
+
if (/ies$/.test(word) && word.length > 4)
|
|
154
|
+
return `${head}${word.slice(0, -3)}y`;
|
|
155
|
+
if (US_ES.test(word) || SIBILANT_ES.test(word)) {
|
|
156
|
+
return `${head}${word.slice(0, -2)}`;
|
|
157
|
+
}
|
|
158
|
+
if (/[^su]s$/.test(word))
|
|
159
|
+
return `${head}${word.slice(0, -1)}`;
|
|
160
|
+
return plural;
|
|
161
|
+
}
|
|
162
|
+
function idWithVerb(name, resource, verbs) {
|
|
163
|
+
if (Object.hasOwn(verbs, name)) {
|
|
164
|
+
return `${name}${resource()[verbs[name]]}`;
|
|
165
|
+
}
|
|
166
|
+
for (const by of name.matchAll(/By[A-Z]/g)) {
|
|
167
|
+
const verb = name.slice(0, by.index);
|
|
168
|
+
if (verb === "" || !Object.hasOwn(verbs, verb))
|
|
169
|
+
continue;
|
|
170
|
+
const names = resource();
|
|
171
|
+
const noun = verb.endsWith("Many") ? names.plural : names.singular;
|
|
172
|
+
return `${verb}${noun}${name.slice(by.index)}`;
|
|
173
|
+
}
|
|
174
|
+
return;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// src/resource.ts
|
|
178
|
+
function optionsOf(program, target) {
|
|
179
|
+
return program.stateMap($lib.stateKeys.operationIdsOptions).get(target);
|
|
180
|
+
}
|
|
181
|
+
function lineageOf(target) {
|
|
182
|
+
return [target, ...target.sourceInterfaces.flatMap(lineageOf)];
|
|
183
|
+
}
|
|
184
|
+
function verbsOf(program, target) {
|
|
185
|
+
const namespaces = [];
|
|
186
|
+
for (let at = target.namespace;at !== undefined; at = at.namespace) {
|
|
187
|
+
namespaces.unshift(at);
|
|
188
|
+
}
|
|
189
|
+
const layers = [...namespaces, ...lineageOf(target).reverse()];
|
|
190
|
+
return Object.assign({}, VERBS, ...layers.map((layer) => optionsOf(program, layer)?.verbs ?? {}));
|
|
191
|
+
}
|
|
192
|
+
function resourceOf(program, target) {
|
|
193
|
+
const options = optionsOf(program, target);
|
|
194
|
+
const plural = options?.plural || target.name;
|
|
195
|
+
return { plural, singular: options?.singular || singularOf(plural) };
|
|
196
|
+
}
|
|
197
|
+
function idOf(program, operation) {
|
|
198
|
+
const container = operation.interface;
|
|
199
|
+
if (container === undefined)
|
|
200
|
+
return operation.name;
|
|
201
|
+
return idWithVerb(operation.name, () => resourceOf(program, container), verbsOf(program, container)) ?? operation.name;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// src/operation-ids.ts
|
|
205
|
+
function operationIds(context, target, options = {}) {
|
|
206
|
+
const { program } = context;
|
|
207
|
+
program.stateSet($lib.stateKeys.operationIds).add(target);
|
|
208
|
+
program.stateMap($lib.stateKeys.operationIdsOptions).set(target, options);
|
|
209
|
+
if (target.kind === "Namespace" && (options.singular !== undefined || options.plural !== undefined)) {
|
|
210
|
+
$lib.reportDiagnostic(program, {
|
|
211
|
+
code: "resource-name-on-namespace",
|
|
212
|
+
target: context.decoratorTarget
|
|
213
|
+
});
|
|
214
|
+
}
|
|
101
215
|
}
|
|
102
216
|
function isMarkedNamespace(program, target) {
|
|
103
217
|
for (let at = target;at !== undefined; at = at.namespace) {
|
|
@@ -139,7 +253,7 @@ function validateOperationIds(program) {
|
|
|
139
253
|
return;
|
|
140
254
|
const named = isNamed(program, operation);
|
|
141
255
|
const own = getOperationId(program, operation);
|
|
142
|
-
const id = own ?? (named ? operation
|
|
256
|
+
const id = own ?? (named ? idOf(program, operation) : resolveOperationId(program, operation));
|
|
143
257
|
const services = servicesOf(program, operation);
|
|
144
258
|
const emitted = services.length > 0 || !hasServices;
|
|
145
259
|
let duplicate = false;
|
|
@@ -177,5 +291,5 @@ export {
|
|
|
177
291
|
$onValidate
|
|
178
292
|
};
|
|
179
293
|
|
|
180
|
-
//# debugId=
|
|
294
|
+
//# debugId=FE2157CFD8BDF58A64756E2164756E21
|
|
181
295
|
//# sourceMappingURL=index.js.map
|
package/dist/index.js.map
CHANGED
|
@@ -1,13 +1,15 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
|
-
"sources": ["../src/one-reply-per-status.ts", "../src/lib.ts", "../src/operation-ids.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/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'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},\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
|
|
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 */\nfunction 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\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\tif (Object.hasOwn(verbs, name)) {\n\t\treturn `${name}${resource()[verbs[name] as Grammar]}`;\n\t}\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)) continue;\n\t\tconst names = resource();\n\t\tconst noun = verb.endsWith('Many') ? names.plural : names.singular;\n\t\treturn `${verb}${noun}${name.slice(by.index)}`;\n\t}\n\treturn undefined;\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} 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",
|
|
8
10
|
"/**\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';\n\nexport { $lib } from './lib';\n\nexport function $onValidate(program: Program): void {\n\tvalidateOperationIds(program);\n\tvalidateOneReplyPerStatus(program);\n}\n\nexport const $decorators = {\n\tNxgt: { operationIds },\n};\n"
|
|
9
11
|
],
|
|
10
|
-
"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,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,EACD;AACD,CAAC;;;
|
|
11
|
-
"debugId": "
|
|
12
|
+
"mappings": "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",
|
|
13
|
+
"debugId": "FE2157CFD8BDF58A64756E2164756E21",
|
|
12
14
|
"names": []
|
|
13
15
|
}
|
package/dist/lib.d.ts
CHANGED
|
@@ -2,11 +2,14 @@ export declare const $lib: import("@typespec/compiler").TypeSpecLibrary<{
|
|
|
2
2
|
'duplicate-operation-id': {
|
|
3
3
|
readonly default: import("@typespec/compiler").CallableMessage<["id"]>;
|
|
4
4
|
};
|
|
5
|
+
'resource-name-on-namespace': {
|
|
6
|
+
readonly default: "`singular` and `plural` name an interface's resource: a namespace has none. Put them on the interface.";
|
|
7
|
+
};
|
|
5
8
|
'duplicate-status-reply': {
|
|
6
9
|
readonly default: import("@typespec/compiler").CallableMessage<["operation", "status"]>;
|
|
7
10
|
};
|
|
8
11
|
'merged-status-reply': {
|
|
9
12
|
readonly default: import("@typespec/compiler").CallableMessage<["operation", "status"]>;
|
|
10
13
|
};
|
|
11
|
-
}, Record<string, any>, "operationIds">;
|
|
14
|
+
}, Record<string, any>, "operationIds" | "operationIdsOptions">;
|
|
12
15
|
//# sourceMappingURL=lib.d.ts.map
|
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;;;;;;;;;;;;;+DAsCf,CAAC"}
|
package/dist/operation-ids.d.ts
CHANGED
|
@@ -1,15 +1,18 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* `@operationIds`: the id of each operation is its name, as written:
|
|
3
3
|
* `@get findPostComments()` is `findPostComments`, where `@typespec/openapi3`
|
|
4
|
-
* would write `Posts_findPostComments`.
|
|
4
|
+
* would write `Posts_findPostComments`. In an interface, a known verb takes
|
|
5
|
+
* the interface's resource: `create` in `Users` is `createUser` (`verbs.ts`).
|
|
5
6
|
*/
|
|
6
7
|
import { type DecoratorContext, type Interface, type Namespace, type Program } from '@typespec/compiler';
|
|
8
|
+
import { type OperationIdsOptions } from './resource';
|
|
7
9
|
/**
|
|
8
10
|
* Marks the interface or the namespace; its ids are set once the program is
|
|
9
11
|
* checked. On a template, TypeSpec runs it on each instance, which
|
|
10
|
-
* `isMarkedInterface` follows to the interfaces extending it.
|
|
12
|
+
* `isMarkedInterface` follows to the interfaces extending it. A namespace
|
|
13
|
+
* has no resource to name: `singular` and `plural` are an interface's.
|
|
11
14
|
*/
|
|
12
|
-
export declare function operationIds(context: DecoratorContext, target: Interface | Namespace): void;
|
|
15
|
+
export declare function operationIds(context: DecoratorContext, target: Interface | Namespace, options?: OperationIdsOptions): void;
|
|
13
16
|
/**
|
|
14
17
|
* Sets the ids, after every decorator, so the order of `@operationId` and
|
|
15
18
|
* `@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
|
|
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,EAGd,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;AAoED;;;;;;;;;GASG;AACH,wBAAgB,oBAAoB,CAAC,OAAO,EAAE,OAAO,GAAG,IAAI,CAqC3D"}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What `@operationIds` names an operation of an interface after: the
|
|
3
|
+
* interface's resource, its singular and plural, and the verbs that take
|
|
4
|
+
* them, layered from the library's, the namespaces', and the interface's.
|
|
5
|
+
*/
|
|
6
|
+
import type { Operation, Program } from '@typespec/compiler';
|
|
7
|
+
import { type Grammar } from './verbs';
|
|
8
|
+
/** `@operationIds`' options, as `OperationIdsOptions` declares them. */
|
|
9
|
+
export interface OperationIdsOptions {
|
|
10
|
+
readonly singular?: string;
|
|
11
|
+
readonly plural?: string;
|
|
12
|
+
readonly verbs?: Readonly<Record<string, Grammar>>;
|
|
13
|
+
}
|
|
14
|
+
/** The id `@operationIds` gives: the name as written, or a verb with its resource. */
|
|
15
|
+
export declare function idOf(program: Program, operation: Operation): string;
|
|
16
|
+
//# sourceMappingURL=resource.d.ts.map
|
|
@@ -0,0 +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,EAKZ,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"}
|
package/dist/verbs.d.ts
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The verbs `@operationIds` completes with an interface's resource: `create`
|
|
3
|
+
* in `Users` is `createUser`, `list` is `listUsers`, and `findById` is
|
|
4
|
+
* `findUserById`. Any other name is the id as written.
|
|
5
|
+
*/
|
|
6
|
+
/** Whether a verb takes the resource's singular or its plural. */
|
|
7
|
+
export type Grammar = 'singular' | 'plural';
|
|
8
|
+
/** A resource's two names: `User` and `Users`. */
|
|
9
|
+
export interface Resource {
|
|
10
|
+
readonly singular: string;
|
|
11
|
+
readonly plural: string;
|
|
12
|
+
}
|
|
13
|
+
/** The library's own verbs. */
|
|
14
|
+
export declare const VERBS: Readonly<Record<string, Grammar>>;
|
|
15
|
+
/**
|
|
16
|
+
* The singular of a plural name, by a short English rule on its last word:
|
|
17
|
+
* `Users`, `BlogPosts`, `Categories`, `Addresses`, `Statuses`, `People`. A
|
|
18
|
+
* guess: it misses some (`Heroes`, `Analysis`), which `singular` names.
|
|
19
|
+
*/
|
|
20
|
+
export declare function singularOf(plural: string): string;
|
|
21
|
+
/**
|
|
22
|
+
* The id of an operation of a resource: the verb and the resource's singular
|
|
23
|
+
* or plural, or the verb, the resource and the rest of a `By` name:
|
|
24
|
+
* `findById` to `findUserById`, the singular, but `deleteManyByTeam` to
|
|
25
|
+
* `deleteManyUsersByTeam`, the plural of a `*Many` verb. `undefined` when
|
|
26
|
+
* the name is none of them. The resource is taken only then.
|
|
27
|
+
*/
|
|
28
|
+
export declare function idWithVerb(name: string, resource: () => Resource, verbs: Readonly<Record<string, Grammar>>): string | undefined;
|
|
29
|
+
//# sourceMappingURL=verbs.d.ts.map
|
|
@@ -0,0 +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,CAgBnD,CAAC;AAyBF;;;;GAIG;AACH,wBAAgB,UAAU,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,CAgBjD;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"}
|
package/docs/README.md
CHANGED
|
@@ -10,6 +10,6 @@ The [package README](../README.md) is the short version.
|
|
|
10
10
|
| [Scalars and columns](guide/columns.md) | typing ids and addresses, and the columns a row carries |
|
|
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
|
-
| [Operation ids](guide/operation-ids.md) | naming each operation, and so the generated client's method
|
|
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
14
|
| [Troubleshooting](troubleshooting.md) | the compiler or the generator refused the spec |
|
|
15
15
|
| [Roadmap](roadmap.md) | wondering what is coming |
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
# Operation ids
|
|
2
2
|
|
|
3
|
-
`@operationIds`
|
|
4
|
-
|
|
3
|
+
`@operationIds` gives each operation the OpenAPI id the generated client
|
|
4
|
+
should call it by: a known verb in an interface takes the interface's
|
|
5
|
+
resource, `list` in `Posts` is `listPosts`, and any other name is the id
|
|
6
|
+
exactly as written.
|
|
5
7
|
|
|
6
8
|
An operation id is what `@nxgt/openapi-codegen` names everything after: the
|
|
7
9
|
client's method, the `operations` entry, and the prefix of the operation's
|
|
@@ -9,7 +11,7 @@ types (`ListPostsQuery`, `zGetPostParam`).
|
|
|
9
11
|
|
|
10
12
|
## On the service namespace
|
|
11
13
|
|
|
12
|
-
Mark the namespace once, and every operation in it is named
|
|
14
|
+
Mark the namespace once, and every operation in it is named:
|
|
13
15
|
|
|
14
16
|
```tsp
|
|
15
17
|
import "@typespec/http";
|
|
@@ -34,9 +36,10 @@ model Comment {
|
|
|
34
36
|
|
|
35
37
|
@route("/posts")
|
|
36
38
|
interface Posts {
|
|
37
|
-
@get
|
|
38
|
-
@get
|
|
39
|
-
@
|
|
39
|
+
@get list(...PageParameters): Page<Post>; // listPosts
|
|
40
|
+
@get @route("/{postId}") get(@path postId: uuid): Post | NotFound; // getPost
|
|
41
|
+
@get @route("/by-slug/{slug}") findBySlug(@path slug: string): Post | NotFound; // findPostBySlug
|
|
42
|
+
@delete @route("/{postId}") delete(@path postId: uuid): NoContentResponse | NotFound; // deletePost
|
|
40
43
|
}
|
|
41
44
|
|
|
42
45
|
@route("/posts/{postId}/comments")
|
|
@@ -45,7 +48,7 @@ interface Comments {
|
|
|
45
48
|
}
|
|
46
49
|
```
|
|
47
50
|
|
|
48
|
-
Alone, `@typespec/openapi3` would write `
|
|
51
|
+
Alone, `@typespec/openapi3` would write `Posts_list` and
|
|
49
52
|
`Comments_findPostComments`. The generated `operations` has one entry per
|
|
50
53
|
id:
|
|
51
54
|
|
|
@@ -53,20 +56,167 @@ id:
|
|
|
53
56
|
import { operations } from './generated/operations';
|
|
54
57
|
|
|
55
58
|
Object.keys(operations);
|
|
56
|
-
// ['listPosts', 'getPost', 'deletePost', 'findPostComments']
|
|
59
|
+
// ['listPosts', 'getPost', 'findPostBySlug', 'deletePost', 'findPostComments']
|
|
57
60
|
```
|
|
58
61
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
interface
|
|
62
|
+
## Verbs
|
|
63
|
+
|
|
64
|
+
In an interface, an operation named exactly one of these verbs is completed
|
|
65
|
+
with the interface's resource:
|
|
66
|
+
|
|
67
|
+
| Verb | Takes | In `Users` | In `Categories` |
|
|
68
|
+
| --- | --- | --- | --- |
|
|
69
|
+
| `list` | the plural | `listUsers` | `listCategories` |
|
|
70
|
+
| `read` | the plural | `readUsers` | `readCategories` |
|
|
71
|
+
| `find` | the plural | `findUsers` | `findCategories` |
|
|
72
|
+
| `search` | the plural | `searchUsers` | `searchCategories` |
|
|
73
|
+
| `count` | the plural | `countUsers` | `countCategories` |
|
|
74
|
+
| `createMany` | the plural | `createManyUsers` | `createManyCategories` |
|
|
75
|
+
| `updateMany` | the plural | `updateManyUsers` | `updateManyCategories` |
|
|
76
|
+
| `deleteMany` | the plural | `deleteManyUsers` | `deleteManyCategories` |
|
|
77
|
+
| `get` | the singular | `getUser` | `getCategory` |
|
|
78
|
+
| `create` | the singular | `createUser` | `createCategory` |
|
|
79
|
+
| `update` | the singular | `updateUser` | `updateCategory` |
|
|
80
|
+
| `patch` | the singular | `patchUser` | `patchCategory` |
|
|
81
|
+
| `replace` | the singular | `replaceUser` | `replaceCategory` |
|
|
82
|
+
| `upsert` | the singular | `upsertUser` | `upsertCategory` |
|
|
83
|
+
| `delete` | the singular | `deleteUser` | `deleteCategory` |
|
|
84
|
+
|
|
85
|
+
A verb followed by `By` and a capital letter takes the singular before
|
|
86
|
+
`By`, whichever form the verb takes alone, except a `*Many` verb, which
|
|
87
|
+
takes the plural:
|
|
62
88
|
|
|
63
|
-
|
|
89
|
+
```tsp
|
|
90
|
+
@route("/users")
|
|
91
|
+
interface Users {
|
|
92
|
+
@get @route("/{id}") findById(@path id: uuid): User | NotFound; // findUserById
|
|
93
|
+
@get @route("/by-email/{email}") getByEmail(@path email: email): User | NotFound; // getUserByEmail
|
|
94
|
+
@delete @route("/by-team/{teamId}") deleteManyByTeam(@path teamId: uuid): void; // deleteManyUsersByTeam
|
|
95
|
+
}
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
Any other name is the id as written: `findActiveUsers` is
|
|
99
|
+
`findActiveUsers`, and so is `listUsers`. The verb must be the whole name,
|
|
100
|
+
or the whole part before `By`: `listAll` and `listing` are not verbs.
|
|
101
|
+
|
|
102
|
+
Outside an interface, the id is always the name as written, even a verb:
|
|
64
103
|
|
|
65
104
|
```tsp
|
|
66
|
-
|
|
105
|
+
@route("/health") @get op list(): void; // list
|
|
67
106
|
```
|
|
68
107
|
|
|
69
|
-
|
|
108
|
+
## The resource
|
|
109
|
+
|
|
110
|
+
The plural is the interface's name. The singular comes from a short English
|
|
111
|
+
rule on its last word, so `BlogPosts` is `BlogPost`:
|
|
112
|
+
|
|
113
|
+
| Interface | Singular |
|
|
114
|
+
| --- | --- |
|
|
115
|
+
| `Users`, `BlogPosts`, `APIKeys` | `User`, `BlogPost`, `APIKey` |
|
|
116
|
+
| `Categories` | `Category` |
|
|
117
|
+
| `Addresses`, `Boxes`, `Matches`, `Branches`, `Wishes` | `Address`, `Box`, `Match`, `Branch`, `Wish` |
|
|
118
|
+
| `Statuses`, `Buses` | `Status`, `Bus` |
|
|
119
|
+
| `Houses`, `Caches`, `Pies` | `House`, `Cache`, `Pie` |
|
|
120
|
+
| `People`, `Children`, `Movies` | `Person`, `Child`, `Movie` |
|
|
121
|
+
| `Series`, `Status`, `Access`, `Staff` | unchanged |
|
|
122
|
+
|
|
123
|
+
The rule is short, not a dictionary: a name it does not know stays as it
|
|
124
|
+
is, so `create` in `Staff` is `createStaff`. Name the resource yourself:
|
|
125
|
+
|
|
126
|
+
```tsp
|
|
127
|
+
@route("/staff")
|
|
128
|
+
@operationIds(#{ singular: "Member" })
|
|
129
|
+
interface Staff {
|
|
130
|
+
@get list(): Member[]; // listStaff
|
|
131
|
+
@post create(): Member; // createMember
|
|
132
|
+
}
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
## Options
|
|
136
|
+
|
|
137
|
+
```tsp
|
|
138
|
+
model OperationIdsOptions {
|
|
139
|
+
singular?: string;
|
|
140
|
+
plural?: string;
|
|
141
|
+
verbs?: Record<"singular" | "plural">;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
extern dec operationIds(
|
|
145
|
+
target: Interface | Namespace,
|
|
146
|
+
options?: valueof OperationIdsOptions
|
|
147
|
+
);
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
| Option | Type | Default | Effect |
|
|
151
|
+
| --- | --- | --- | --- |
|
|
152
|
+
| `singular` | `string` | the plural, by the English rule | the resource's singular; on an interface only |
|
|
153
|
+
| `plural` | `string` | the interface's name | the resource's plural, and the source of the singular when `singular` is not given; on an interface only |
|
|
154
|
+
| `verbs` | `Record<"singular" \| "plural">` | the verbs above | more verbs, or a verb that takes the other form; on a namespace or an interface |
|
|
155
|
+
|
|
156
|
+
`plural` alone renames both, the singular following by the rule:
|
|
157
|
+
|
|
158
|
+
```tsp
|
|
159
|
+
@route("/team")
|
|
160
|
+
@operationIds(#{ plural: "People" })
|
|
161
|
+
interface Team {
|
|
162
|
+
@get list(): Person[]; // listPeople
|
|
163
|
+
@post create(): Person; // createPerson
|
|
164
|
+
}
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
`verbs` adds a verb, or changes the form one takes:
|
|
168
|
+
|
|
169
|
+
```tsp
|
|
170
|
+
@service
|
|
171
|
+
@operationIds(#{ verbs: #{ archive: "singular", export: "plural" } })
|
|
172
|
+
namespace Shop;
|
|
173
|
+
|
|
174
|
+
@route("/users")
|
|
175
|
+
interface Users {
|
|
176
|
+
@post @route("/{id}/archive") archive(@path id: uuid): void; // archiveUser
|
|
177
|
+
@get @route("/export") export(): User[]; // exportUsers
|
|
178
|
+
}
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
On a namespace, `verbs` reaches every interface in it, however deep. On an
|
|
182
|
+
interface, its `verbs` override the namespace's, verb by verb:
|
|
183
|
+
|
|
184
|
+
```tsp
|
|
185
|
+
@route("/staff")
|
|
186
|
+
@operationIds(#{ singular: "Member", verbs: #{ export: "singular" } })
|
|
187
|
+
interface Staff {
|
|
188
|
+
@get export(): Member; // exportMember, where the namespace alone gives exportStaff
|
|
189
|
+
}
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
`singular` and `plural` on a namespace are an error: a namespace holds many
|
|
193
|
+
interfaces, and has no resource of its own
|
|
194
|
+
([troubleshooting](../troubleshooting.md#singular-and-plural-name-an-interfaces-resource-a-namespace-has-none)).
|
|
195
|
+
|
|
196
|
+
```text
|
|
197
|
+
error @nxgt/typespec/resource-name-on-namespace: `singular` and `plural` name an interface's resource: a namespace has none. Put them on the interface.
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
## Precedence
|
|
201
|
+
|
|
202
|
+
For each operation `@operationIds` names, the first that applies gives the
|
|
203
|
+
id:
|
|
204
|
+
|
|
205
|
+
1. its own `@operationId`, written before or after `@operationIds`;
|
|
206
|
+
2. outside an interface: its name, as written;
|
|
207
|
+
3. a verb, or a verb followed by `By…`: the verb with the interface's
|
|
208
|
+
resource;
|
|
209
|
+
4. its name, as written.
|
|
210
|
+
|
|
211
|
+
The verbs are the library's, overridden by each marked namespace's around
|
|
212
|
+
the interface from the outermost in, then by the interfaces it extends,
|
|
213
|
+
then by its own. The resource is the interface's own `singular` and
|
|
214
|
+
`plural`, else its name and that name's singular: an interface never takes
|
|
215
|
+
the names of one it extends, or two extending it would collide.
|
|
216
|
+
|
|
217
|
+
## Where it goes
|
|
218
|
+
|
|
219
|
+
| Marked | Named |
|
|
70
220
|
| --- | --- |
|
|
71
221
|
| a namespace | every operation in it, however deep: in its interfaces, in the namespaces it contains, and declared in it with `op` |
|
|
72
222
|
| an interface | its operations, and those of each interface that `extends` it |
|
|
@@ -81,12 +231,12 @@ namespace Blog;
|
|
|
81
231
|
@route("/posts")
|
|
82
232
|
@operationIds
|
|
83
233
|
interface Posts {
|
|
84
|
-
@get
|
|
234
|
+
@get list(): Post[]; // listPosts
|
|
85
235
|
}
|
|
86
236
|
|
|
87
237
|
@route("/drafts")
|
|
88
238
|
interface Drafts {
|
|
89
|
-
@get
|
|
239
|
+
@get list(): Post[]; // Drafts_list
|
|
90
240
|
}
|
|
91
241
|
```
|
|
92
242
|
|
|
@@ -95,8 +245,7 @@ does not reach a `namespace Legacy` declared next to it.
|
|
|
95
245
|
|
|
96
246
|
## An operation's own `@operationId`
|
|
97
247
|
|
|
98
|
-
An operation with its own `@operationId` keeps it,
|
|
99
|
-
before or after `@operationIds`:
|
|
248
|
+
An operation with its own `@operationId` keeps it, verb or not:
|
|
100
249
|
|
|
101
250
|
```tsp
|
|
102
251
|
import "@typespec/openapi";
|
|
@@ -105,31 +254,56 @@ using OpenAPI;
|
|
|
105
254
|
|
|
106
255
|
@route("/posts")
|
|
107
256
|
interface Posts {
|
|
108
|
-
@get @operationId("fetchPost")
|
|
257
|
+
@get @route("/{postId}") @operationId("fetchPost") get(@path postId: uuid): Post | NotFound; // fetchPost
|
|
109
258
|
}
|
|
110
259
|
```
|
|
111
260
|
|
|
112
261
|
`import "@typespec/openapi"` and `using OpenAPI` are only needed for
|
|
113
262
|
`@operationId` ([troubleshooting](../troubleshooting.md#unknown-decorator-operationid)).
|
|
114
263
|
|
|
264
|
+
### Upgrading from 0.4
|
|
265
|
+
|
|
266
|
+
Before 0.5.0, every name was the id as written. An operation of a marked
|
|
267
|
+
interface named exactly a verb, such as `list` or `create`, or a verb
|
|
268
|
+
followed by `By…`, such as `findById` (now `findUserById`), gets a new id,
|
|
269
|
+
and the generated client's method changes with it. Give it an
|
|
270
|
+
`@operationId` to keep the old one:
|
|
271
|
+
|
|
272
|
+
```tsp
|
|
273
|
+
@route("/users")
|
|
274
|
+
interface Users {
|
|
275
|
+
@get @operationId("list") list(): User[]; // list, as before
|
|
276
|
+
}
|
|
277
|
+
```
|
|
278
|
+
|
|
279
|
+
A name that already carried its resource, `listUsers`, is unchanged. An
|
|
280
|
+
interface that declared both `list` and `listUsers` now names two operations
|
|
281
|
+
`listUsers`, which is an error: rename one, or give `list` its
|
|
282
|
+
`@operationId`.
|
|
283
|
+
|
|
115
284
|
## On a template
|
|
116
285
|
|
|
117
286
|
An interface template is never emitted, only the interfaces that extend one
|
|
118
287
|
of its instances. When the template is marked, or the interface that extends
|
|
119
|
-
it is in a marked namespace, those are named
|
|
288
|
+
it is in a marked namespace, those are named, and a verb takes the resource
|
|
289
|
+
of the interface that extends it:
|
|
120
290
|
|
|
121
291
|
```tsp
|
|
122
|
-
@operationIds
|
|
123
292
|
interface Listable<Item> {
|
|
124
|
-
@get
|
|
293
|
+
@get list(): Item[];
|
|
125
294
|
}
|
|
126
295
|
|
|
127
296
|
@route("/pets")
|
|
128
|
-
interface Pets extends Listable<Pet> {} //
|
|
297
|
+
interface Pets extends Listable<Pet> {} // listPets
|
|
298
|
+
|
|
299
|
+
@route("/toys")
|
|
300
|
+
interface Toys extends Listable<Toy> {} // listToys
|
|
129
301
|
```
|
|
130
302
|
|
|
131
|
-
A
|
|
132
|
-
|
|
303
|
+
A name that is not a verb, `listAll`, would be `listAll` in both, which is
|
|
304
|
+
an error: see below. A `singular` or `plural` names only the interface that
|
|
305
|
+
carries it: on a template, or on an interface others extend, it has no
|
|
306
|
+
effect on the interfaces extending it, which each take their own name.
|
|
133
307
|
|
|
134
308
|
An operation template is never emitted either, only the operations declared
|
|
135
309
|
from it. In a marked namespace, each is named as written:
|
|
@@ -154,32 +328,35 @@ error @nxgt/typespec/duplicate-operation-id: Two operations are named listPets:
|
|
|
154
328
|
|
|
155
329
|
It happens when:
|
|
156
330
|
|
|
157
|
-
-
|
|
158
|
-
|
|
159
|
-
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
331
|
+
- a verb and the id it gives are both in one interface: `list` and
|
|
332
|
+
`listUsers` in `Users` are both `listUsers`;
|
|
333
|
+
- one name is in two interfaces of one name, or a name that is not a verb in
|
|
334
|
+
two interfaces: neither the namespace nor the interface is part of it, so
|
|
335
|
+
`Store.Pets` and `Shelter.Pets` both give `listPets`, whether they declare
|
|
336
|
+
`list` or `listPets`;
|
|
337
|
+
- an interface `extends` another and copies a name that is not a verb:
|
|
338
|
+
`Drafts extends Posts` gives a second `getPost`, where `get` would give
|
|
339
|
+
`getDraft`;
|
|
340
|
+
- an `@operationId` written on another operation is an id `@operationIds`
|
|
341
|
+
gives;
|
|
164
342
|
- an unmarked operation declared in the service namespace, which the
|
|
165
|
-
emitter names after itself, has a marked operation's
|
|
166
|
-
beside `Pets.
|
|
343
|
+
emitter names after itself, has a marked operation's id: `op listPets()`
|
|
344
|
+
beside a marked `Pets.list`.
|
|
167
345
|
|
|
168
|
-
|
|
346
|
+
Name the operations with verbs, and each interface extending them gets its
|
|
347
|
+
own ids:
|
|
169
348
|
|
|
170
349
|
```tsp
|
|
171
350
|
@route("/posts")
|
|
172
351
|
interface Posts {
|
|
173
|
-
@get
|
|
352
|
+
@get @route("/{postId}") get(@path postId: uuid): Post | NotFound; // getPost
|
|
174
353
|
}
|
|
175
354
|
|
|
176
355
|
@route("/drafts")
|
|
177
|
-
interface Drafts {
|
|
178
|
-
@get getDraft(@path draftId: uuid): Post | NotFound;
|
|
179
|
-
}
|
|
356
|
+
interface Drafts extends Posts {} // getDraft
|
|
180
357
|
```
|
|
181
358
|
|
|
182
|
-
Or keep
|
|
359
|
+
Or keep the names, and declare each copied operation again with its own
|
|
183
360
|
`@operationId`:
|
|
184
361
|
|
|
185
362
|
```tsp
|
package/docs/guide/sorting.md
CHANGED
|
@@ -34,20 +34,20 @@ model AuthorFilters {
|
|
|
34
34
|
|
|
35
35
|
@route("/authors")
|
|
36
36
|
interface Authors {
|
|
37
|
-
@get
|
|
37
|
+
@get list(
|
|
38
38
|
...AuthorFilters,
|
|
39
39
|
...PageParameters,
|
|
40
40
|
...SortParameters<"name" | "createdAt">,
|
|
41
|
-
): Page<Author>;
|
|
41
|
+
): Page<Author>; // listAuthors
|
|
42
42
|
|
|
43
|
-
@get
|
|
43
|
+
@get @route("/{id}") get(@path id: uuid): Author | NotFound; // getAuthor
|
|
44
44
|
}
|
|
45
45
|
```
|
|
46
46
|
|
|
47
47
|
`GET /authors?name=Ada&orderBy=name&direction=desc` is one page of the
|
|
48
48
|
authors named Ada, by name, descending. The list is an operation like any
|
|
49
|
-
other
|
|
50
|
-
([Operation ids](operation-ids.md)).
|
|
49
|
+
other: `@operationIds` completes the verb `list` with the interface's name,
|
|
50
|
+
and gives it the id `listAuthors` ([Operation ids](operation-ids.md#verbs)).
|
|
51
51
|
|
|
52
52
|
## The query
|
|
53
53
|
|
package/docs/roadmap.md
CHANGED
|
@@ -16,6 +16,17 @@ Nothing in progress.
|
|
|
16
16
|
|
|
17
17
|
## Shipped
|
|
18
18
|
|
|
19
|
+
### 0.5.0
|
|
20
|
+
|
|
21
|
+
- **Verbs in operation ids.** In an interface `@operationIds` marks, a known
|
|
22
|
+
verb takes the interface's resource: `list` in `Users` is `listUsers`,
|
|
23
|
+
`create` is `createUser`, `findById` is `findUserById`, so a resource's
|
|
24
|
+
operations are written once as `list`, `get`, `create`, and each
|
|
25
|
+
interface names its own. The singular comes from a short English rule
|
|
26
|
+
(`Categories`, `People`), and `singular`, `plural` and `verbs` options name
|
|
27
|
+
what the rule does not know. Any other name, and any operation outside an
|
|
28
|
+
interface, keeps its id as written.
|
|
29
|
+
|
|
19
30
|
### 0.4.0
|
|
20
31
|
|
|
21
32
|
- **Operation ids as written.** `@operationIds` names each operation
|
package/docs/troubleshooting.md
CHANGED
|
@@ -55,11 +55,14 @@ namespace, so they share the spec's schema names.
|
|
|
55
55
|
`@operationIds` names has the id of another operation of the same service
|
|
56
56
|
(two `@service` namespaces are two documents, and may reuse a name):
|
|
57
57
|
|
|
58
|
-
-
|
|
59
|
-
|
|
60
|
-
-
|
|
61
|
-
|
|
62
|
-
|
|
58
|
+
- a verb and the id it gives are both in one interface: `list` and
|
|
59
|
+
`listUsers` in `Users` are both `listUsers`;
|
|
60
|
+
- one name is in two interfaces of one name, or a name that is not a verb in
|
|
61
|
+
two interfaces: `Store.Pets` and `Shelter.Pets` both give `listPets`,
|
|
62
|
+
whether they declare `list` or `listPets`;
|
|
63
|
+
- an interface `extends` another, and so copies names that are not verbs:
|
|
64
|
+
`Drafts extends Posts` gives a second `getPost`, and two interfaces
|
|
65
|
+
extending one template share every such name;
|
|
63
66
|
- an `@operationId` written on another operation is a name `@operationIds`
|
|
64
67
|
gives;
|
|
65
68
|
- an unmarked operation declared in the service namespace, which the
|
|
@@ -70,12 +73,25 @@ namespace, so they share the spec's schema names.
|
|
|
70
73
|
error @nxgt/typespec/duplicate-operation-id: Two operations are named listPets: an OpenAPI operation id must be unique.
|
|
71
74
|
```
|
|
72
75
|
|
|
73
|
-
**Why:** `@operationIds` makes each operation's id its name, as written,
|
|
74
|
-
|
|
76
|
+
**Why:** `@operationIds` makes each operation's id its name, as written, or
|
|
77
|
+
a verb with its interface's resource, without its namespace. `@typespec/openapi3` would emit both
|
|
75
78
|
ids without a word, and the generator would then refuse the spec.
|
|
76
79
|
|
|
77
|
-
**Fix:** give each operation a name of its own.
|
|
78
|
-
|
|
80
|
+
**Fix:** give each operation a name of its own. In one interface, keep the
|
|
81
|
+
verb or the full name, not both:
|
|
82
|
+
|
|
83
|
+
```tsp
|
|
84
|
+
@route("/users")
|
|
85
|
+
interface Users {
|
|
86
|
+
@get list(): User[]; // listUsers
|
|
87
|
+
@get @route("/active") findActiveUsers(): User[]; // findActiveUsers
|
|
88
|
+
}
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
Name the operations of an interface others extend with verbs, so each takes
|
|
92
|
+
its own resource (`get` is `getPost` in `Posts`, `getDraft` in
|
|
93
|
+
`Drafts extends Posts`), or declare distinct operations rather than
|
|
94
|
+
extending:
|
|
79
95
|
|
|
80
96
|
```tsp
|
|
81
97
|
@route("/posts")
|
|
@@ -104,6 +120,68 @@ interface Drafts extends Posts {
|
|
|
104
120
|
}
|
|
105
121
|
```
|
|
106
122
|
|
|
123
|
+
## `` `singular` and `plural` name an interface's resource: a namespace has none ``
|
|
124
|
+
|
|
125
|
+
**When:** `tsp compile` runs on a spec that gives `@operationIds` on a
|
|
126
|
+
namespace a `singular` or a `plural`:
|
|
127
|
+
|
|
128
|
+
```tsp
|
|
129
|
+
@service
|
|
130
|
+
@operationIds(#{ singular: "User" })
|
|
131
|
+
namespace Shop;
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
```text
|
|
135
|
+
error @nxgt/typespec/resource-name-on-namespace: `singular` and `plural` name an interface's resource: a namespace has none. Put them on the interface.
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
**Why:** the resource a verb takes is an interface's: `list` in `Users` is
|
|
139
|
+
`listUsers`. A namespace holds many interfaces, so one name for all of them
|
|
140
|
+
would give each the same ids.
|
|
141
|
+
|
|
142
|
+
**Fix:** keep the namespace's `verbs`, and name the resource on the
|
|
143
|
+
interface whose name the rule gets wrong
|
|
144
|
+
([Operation ids](guide/operation-ids.md#the-resource)):
|
|
145
|
+
|
|
146
|
+
```tsp
|
|
147
|
+
@service
|
|
148
|
+
@operationIds
|
|
149
|
+
namespace Shop;
|
|
150
|
+
|
|
151
|
+
@route("/staff")
|
|
152
|
+
@operationIds(#{ singular: "Member" })
|
|
153
|
+
interface Staff {
|
|
154
|
+
@post create(): Member; // createMember
|
|
155
|
+
}
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
## `Argument of type '{ verbs: … }' is not assignable to parameter of type 'Nxgt.OperationIdsOptions'`
|
|
159
|
+
|
|
160
|
+
**When:** `tsp compile` runs on a spec whose `@operationIds` gives a verb
|
|
161
|
+
something other than `"singular"` or `"plural"`:
|
|
162
|
+
|
|
163
|
+
```tsp
|
|
164
|
+
@service
|
|
165
|
+
@operationIds(#{ verbs: #{ archive: "bogus" } })
|
|
166
|
+
namespace Shop;
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
```text
|
|
170
|
+
error invalid-argument: Argument of type '{ verbs: { archive: "bogus" } }' is not assignable to parameter of type 'Nxgt.OperationIdsOptions'
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
**Why:** a verb only says which name of the resource follows it: the
|
|
174
|
+
singular, `archiveUser`, or the plural, `archiveUsers`.
|
|
175
|
+
|
|
176
|
+
**Fix:** give each verb `"singular"` or `"plural"`
|
|
177
|
+
([Operation ids](guide/operation-ids.md)):
|
|
178
|
+
|
|
179
|
+
```tsp
|
|
180
|
+
@service
|
|
181
|
+
@operationIds(#{ verbs: #{ archive: "singular" } })
|
|
182
|
+
namespace Shop;
|
|
183
|
+
```
|
|
184
|
+
|
|
107
185
|
## `declares a reply without a body and one with a body of status …`
|
|
108
186
|
|
|
109
187
|
**When:** an operation declares, for one status, a reply without a body and
|
package/lib/operation-ids.tsp
CHANGED
|
@@ -4,13 +4,31 @@ using TypeSpec.Reflection;
|
|
|
4
4
|
|
|
5
5
|
namespace Nxgt;
|
|
6
6
|
|
|
7
|
+
/**
|
|
8
|
+
* `@operationIds`' options.
|
|
9
|
+
*/
|
|
10
|
+
model OperationIdsOptions {
|
|
11
|
+
/** The resource's singular, on an interface: `Person` in `People`. By default, the interface's name in the singular. */
|
|
12
|
+
singular?: string;
|
|
13
|
+
|
|
14
|
+
/** The resource's plural, on an interface. By default, the interface's name. */
|
|
15
|
+
plural?: string;
|
|
16
|
+
|
|
17
|
+
/** More verbs, each taking the resource's singular or its plural: `#{ archive: "singular" }`. */
|
|
18
|
+
verbs?: Record<"singular" | "plural">;
|
|
19
|
+
}
|
|
20
|
+
|
|
7
21
|
/**
|
|
8
22
|
* The id of each operation is its name, as written: `findPostComments` is
|
|
9
23
|
* `findPostComments`, where `@typespec/openapi3` would write
|
|
10
|
-
* `Posts_findPostComments`.
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
24
|
+
* `Posts_findPostComments`. In an interface, a known verb takes the
|
|
25
|
+
* interface's resource: `list` in `Users` is `listUsers`, `create` is
|
|
26
|
+
* `createUser`, `findById` is `findUserById`. The resource is the
|
|
27
|
+
* interface's name, and its singular by a short English rule; `singular`
|
|
28
|
+
* and `plural` name it. `verbs` adds verbs. On an interface, its operations;
|
|
29
|
+
* on a namespace, every operation in it, however deep. An operation with its
|
|
30
|
+
* own `@operationId` keeps it. An id another operation of the same service
|
|
31
|
+
* has is an error, `duplicate-operation-id`.
|
|
14
32
|
*
|
|
15
33
|
* @example
|
|
16
34
|
* ```typespec
|
|
@@ -20,8 +38,13 @@ namespace Nxgt;
|
|
|
20
38
|
*
|
|
21
39
|
* @route("/posts")
|
|
22
40
|
* interface Posts {
|
|
23
|
-
* @get
|
|
41
|
+
* @get list(): Post[]; // listPosts
|
|
42
|
+
* @get findById(@path id: uuid): Post; // findPostById
|
|
43
|
+
* @get findPostComments(): Comment[]; // findPostComments
|
|
24
44
|
* }
|
|
25
45
|
* ```
|
|
26
46
|
*/
|
|
27
|
-
extern dec operationIds(
|
|
47
|
+
extern dec operationIds(
|
|
48
|
+
target: Interface | Namespace,
|
|
49
|
+
options?: valueof OperationIdsOptions
|
|
50
|
+
);
|