acacode / acacode/swagger-typescript-api

Colon suffix in path segment (AIP-136 custom method, e.g. /v1/items:search) becomes a phantom required path param

Open
#1,833 0 comments 3 reactions 0 assignees View on GitHub
Dominant language
TypeScript
Stars
4.1k
Forks
436
PR merge metrics
No merged PRs in 30d

Description

### Description

A colon inside a path segment is treated as an Express-style path parameter. Routes that use [AIP-136 custom methods](https://google.aip.dev/136), such as `/v1/items:search` or `/v1/items/{itemId}:archive`, come out with a required `string` argument that the spec does not declare. The colon suffix is also removed from the URL template.

This looks like the same problem as #532, which was filed as a question and is still open.

Version: 13.12.6. `parseRouteName` on `main` is unchanged.

### Reproduction

```json
{
"openapi": "3.0.3",
"info": { "title": "repro", "version": "1.0.0" },
"paths": {
"/v1/items:search": {
"post": {
"operationId": "searchItems",
"responses": { "200": { "description": "ok" } }
}
},
"/v1/items/{itemId}:archive": {
"post": {
"operationId": "archiveItem",
"parameters": [
{ "name": "itemId", "in": "path", "required": true, "schema": { "type": "string" } }
],
"responses": { "200": { "description": "ok" } }
}
}
}
}
```

```js
await generateApi({ input: "spec.json", output: false, httpClientType: "fetch" });
```

### Actual

```ts
searchItems: (search: string, params: RequestParams = {}) =>
this.request({
path: `/v1/items${search}`,
method: "POST",
...params,
}),
archiveItem: (itemId: string, archive: string, params: RequestParams = {}) =>
this.request({
path: `/v1/items/${itemId}${archive}`,
method: "POST",
...params,
}),
```

`searchItems()` requires an argument that has no meaning in the spec. If the caller passes a value, it is appended to the URL as is. The only argument that gives the correct URL is the literal `":search"`.

### Expected

```ts
searchItems: (params: RequestParams = {}) =>
this.request({ path: `/v1/items:search`, method: "POST", ...params }),
archiveItem: (itemId: string, params: RequestParams = {}) =>
this.request({ path: `/v1/items/${itemId}:archive`, method: "POST", ...params }),
```

### Cause and a possible fix

In `src/schema-routes/schema-routes.ts`, `parseRouteName` matches a colon parameter at any position:

```js
/({[\w[\\\]^`][-_.\w]*})|(:[\w[\\\]^`][-_.\w]*:?)/g
```

An Express-style parameter always starts a path segment. Requiring a preceding `/` keeps `/users/:userId` working and stops matching custom-method suffixes:

```js
/({[\w[\\\]^`][-_.\w]*})|((?<=\/):[\w[\\\]^`][-_.\w]*:?)/g
```

| route | current | with lookbehind |
|---|---|---|
| `/v1/items:search` | `[":search"]` | none |
| `/v1/items/{itemId}:archive` | `["{itemId}", ":archive"]` | `["{itemId}"]` |
| `/users/:userId` | `[":userId"]` | `[":userId"]` |
| `/users/:userId/posts/:postId` | `[":userId", ":postId"]` | `[":userId", ":postId"]` |

### Workaround

We use an `onCreateRoute` hook. It finds path params whose `$match` starts with `:`, puts the literal back into `request.path`, and removes the param from `request.parameters` and from `requestParams.typeData.content`.

Contributor guide

No contributing guide indexed for this repository

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.