@nxgt/typespec 0.4.0 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md 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, and each operation's id is its
306
- name, as written:
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 listPosts(...PageParameters): Page<Post>; // listPosts
316
- @get getPost(@path postId: uuid): Post | NotFound; // getPost
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
- On an interface, it names that interface's operations, and those of the
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 name in one service, in two
323
- interfaces or copied by `extends`, are an error, `duplicate-operation-id`. More in
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, and the ids it refuses;
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
- function operationIds(context, target) {
100
- context.program.stateSet($lib.stateKeys.operationIds).add(target);
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.name : resolveOperationId(program, 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=EF940BB8E619B41E64756E2164756E21
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`.\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';\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.\n */\nexport function operationIds(\n\tcontext: DecoratorContext,\n\ttarget: Interface | Namespace,\n): void {\n\tcontext.program.stateSet($lib.stateKeys.operationIds).add(target);\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 ? operation.name : 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",
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": "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",
11
- "debugId": "EF940BB8E619B41E64756E2164756E21",
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;;;;;;;;;;uCA2Bf,CAAC"}
1
+ {"version":3,"file":"lib.d.ts","sourceRoot":"","sources":["../src/lib.ts"],"names":[],"mappings":"AAGA,eAAO,MAAM,IAAI;;;;;;;;;;;;;+DAsCf,CAAC"}
@@ -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;;;;GAIG;AACH,OAAO,EACN,KAAK,gBAAgB,EACrB,KAAK,SAAS,EAId,KAAK,SAAS,EAGd,KAAK,OAAO,EACZ,MAAM,oBAAoB,CAAC;AAQ5B;;;;GAIG;AACH,wBAAgB,YAAY,CAC3B,OAAO,EAAE,gBAAgB,EACzB,MAAM,EAAE,SAAS,GAAG,SAAS,GAC3B,IAAI,CAEN;AAoED;;;;;;;;;GASG;AACH,wBAAgB,oBAAoB,CAAC,OAAO,EAAE,OAAO,GAAG,IAAI,CAmC3D"}
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"}
@@ -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, as written |
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` makes each operation's OpenAPI id its name, exactly as
4
- written, so the generated client's methods are the names in your spec.
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 as written:
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 listPosts(...PageParameters): Page<Post>; // listPosts
38
- @get getPost(@path postId: uuid): Post | NotFound; // getPost
39
- @delete deletePost(@path postId: uuid): NoContentResponse | NotFound; // deletePost
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 `Posts_listPosts` and
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
- Name each operation as the client should call it: `listPosts`, not `list`.
60
- An operation named `list` gets the id `list`, and a second `list` in another
61
- interface is an error ([below](#two-operations-named-alike)).
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
- ## Where it goes
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
- extern dec operationIds(target: Interface | Namespace);
105
+ @route("/health") @get op list(): void; // list
67
106
  ```
68
107
 
69
- | Marked | Named as written |
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 listPosts(): Post[]; // listPosts
234
+ @get list(): Post[]; // listPosts
85
235
  }
86
236
 
87
237
  @route("/drafts")
88
238
  interface Drafts {
89
- @get listDrafts(): Post[]; // Drafts_listDrafts
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, whether it is written
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") getPost(@path postId: uuid): Post | NotFound; // 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 as written:
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 listAll(): Item[];
293
+ @get list(): Item[];
125
294
  }
126
295
 
127
296
  @route("/pets")
128
- interface Pets extends Listable<Pet> {} // listAll
297
+ interface Pets extends Listable<Pet> {} // listPets
298
+
299
+ @route("/toys")
300
+ interface Toys extends Listable<Toy> {} // listToys
129
301
  ```
130
302
 
131
- A second interface extending `Listable` would name its operation `listAll`
132
- too, which is an error: see below.
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
- - one name is in two interfaces: neither the namespace nor the interface is
158
- part of the id, so `Store.Pets` and `Shelter.Pets` both give `listPets`;
159
- - an interface `extends` another: it copies its operations and their
160
- names, so `Drafts extends Posts` gives a second `getPost`, and two
161
- interfaces extending one template share every operation;
162
- - an `@operationId` written on another operation is a name
163
- `@operationIds` gives;
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 name: `op listPets()`
166
- beside `Pets.listPets`.
343
+ emitter names after itself, has a marked operation's id: `op listPets()`
344
+ beside a marked `Pets.list`.
167
345
 
168
- Declare distinct operations rather than extending:
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 getPost(@path postId: uuid): Post | NotFound;
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 `extends`, and declare each copied operation again with its own
359
+ Or keep the names, and declare each copied operation again with its own
183
360
  `@operationId`:
184
361
 
185
362
  ```tsp
@@ -34,20 +34,20 @@ model AuthorFilters {
34
34
 
35
35
  @route("/authors")
36
36
  interface Authors {
37
- @get listAuthors(
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 getAuthor(@path id: uuid): Author | NotFound;
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, and `@operationIds` gives it the id `listAuthors`
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
@@ -6,12 +6,16 @@ generated by `@nxgt/openapi-codegen` in CI before it ships.
6
6
 
7
7
  ## Now
8
8
 
9
- - **Operation ids as written.** `@operationIds` names each operation
10
- exactly as the spec writes it, `findPostComments`, on an interface or on
11
- the whole service namespace, instead of `<operation><Interface>`.
12
- - **No `Resource` template.** A resource's operations are written by hand,
13
- each with the name it has in the client; `SortParameters` stays, for the
14
- sort of a list.
9
+ - **Verbs in operation ids.** In an interface `@operationIds` marks, a known
10
+ verb takes the interface's resource: `list` in `Users` is `listUsers`,
11
+ `create` is `createUser`, `findById` is `findUserById`, so a resource's
12
+ operations are written once as `list`, `get`, `create`, and each
13
+ interface names its own. The singular comes from a short English rule
14
+ (`Categories`, `People`), and `singular`, `plural` and `verbs` options name
15
+ what the rule does not know. Any other name, and any operation outside an
16
+ interface, keeps its id as written; an operation named exactly a verb,
17
+ or a verb followed by `By…` (`findById`), changes id, and keeps the old
18
+ one with an `@operationId`; a `list` beside a `listUsers` now collides.
15
19
 
16
20
  ## Later
17
21
 
@@ -21,6 +25,16 @@ generated by `@nxgt/openapi-codegen` in CI before it ships.
21
25
 
22
26
  ## Shipped
23
27
 
28
+ ### 0.4.0
29
+
30
+ - **Operation ids as written.** `@operationIds` names each operation
31
+ exactly as the spec writes it, `findPostComments`, on an interface, or on a
32
+ namespace to cover every operation in it, instead of
33
+ `<operation><Interface>`. Ids are unique per service document.
34
+ - **No `Resource` template.** A resource's operations are written by hand,
35
+ each with the name it has in the client; `SortParameters` stays, for the
36
+ sort of a list.
37
+
24
38
  ### 0.3.0
25
39
 
26
40
  - **Resource ids in the singular.** `Resource` named `read`, `create`,
@@ -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
- - one name is in two interfaces: `Store.Pets` and `Shelter.Pets` both
59
- declare `listPets`;
60
- - an interface `extends` another, and so copies its operations and their
61
- names: `Drafts extends Posts` gives a second `getPost`, and two interfaces
62
- extending one template share every operation;
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
- without its interface or its namespace. `@typespec/openapi3` would emit both
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. Declare distinct operations
78
- rather than extending:
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
@@ -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`. On an interface, its operations; on a namespace,
11
- * every operation in it, however deep. An operation with its own
12
- * `@operationId` keeps it. An id another operation of the same service has is an
13
- * error, `duplicate-operation-id`.
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 listPosts(): Post[];
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(target: Interface | Namespace);
47
+ extern dec operationIds(
48
+ target: Interface | Namespace,
49
+ options?: valueof OperationIdsOptions
50
+ );
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nxgt/typespec",
3
- "version": "0.4.0",
3
+ "version": "0.5.0",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",