Skip to content

feat(massimo-cli): add --path-map to declare a path map of the client operations - #245

Open
mirko-pira wants to merge 1 commit into
platformatic:mainfrom
mirko-pira:feat/path-map
Open

mirko-pira wants to merge 1 commit into
platformatic:mainfrom
mirko-pira:feat/path-map

Conversation

@mirko-pira

Copy link
Copy Markdown
Contributor

Problem

Code that knows a URL template and an HTTP method (a generic fetch wrapper, a router, a mock server) cannot reach the generated types of that operation. The types are named after the operationId, and Massimo derives that id from the path when it is missing and renames it when it repeats (generateOperationId, called at openapi-generator.js:106). A consumer that wants to go from a path to its types has to reimplement that naming, and drifts the moment it changes.

Change

A new opt-in flag:

massimo <url> --name movies --path-map

With it the types file also declares <Name>Paths, keyed by path template and then method. From the new test fixture (excerpt):

export interface MoviesPaths {
  '/movies/{id}': {
    'get': { 'request': GetMoviesIdRequest; 'responses': GetMoviesIdResponses };
    'delete': { 'request': DeleteMovieRequest; 'responses': DeleteMovieResponses };
  }
  '/accounts/{accountId}/folders': {
    'post': { 'request': CreateFolderRequest; 'responses': CreateFolderResponses };
  }
  '/folders': {
    'post': { 'request': PostCreateFolderRequest; 'responses': PostCreateFolderResponses };
  }
}

export type MoviesPathRequest<P extends keyof MoviesPaths, M extends keyof MoviesPaths[P]> = MoviesPaths[P][M] extends { 'request': infer R } ? R : never
export type MoviesPathResponses<P extends keyof MoviesPaths, M extends keyof MoviesPaths[P]> = MoviesPaths[P][M] extends { 'responses': infer R } ? R : never

GET /movies/{id} has no operationId, and the second createFolder became postCreateFolder: the map carries the names the client actually uses. The two aliases look up an entry when the path and the method are type parameters. The obvious MoviesPaths[P][M]['request'] does not compile (TS2536), because the per-path types share no constraint that proves the member exists; hence infer.

writeOperations now returns, for every operation, the request and responses type names it puts in the client method signature (built at openapi-common.js:33 and responses-writer.js:56), and writePathMap prints exactly those, so the map cannot disagree with the client. The Node types and the --frontend types both get the map.

Names: <Name>Paths rather than a fixed Paths, so two clients in one project do not clash. If an operationId moviesPath already declares MoviesPathRequest, the aliases become MoviesPathRequest2 and MoviesPathResponses2; MoviesPaths never gets a suffix. With --named-schemas as well, a component schema called like one of the three names gets the Schema suffix, like any other taken name.

Why a separate flag

#218 added --named-schemas and shipped in 1.6.0. The map names only operation types, which every mode declares and which --named-schemas never renames (openapi-generator.js:143). So the map is the same text with or without named schemas, and is useful without them. Putting it under --named-schemas would change the output of a released flag: three new exports, a schema rename in the collision case, and a different warning. As its own flag it changes nothing for anyone who does not pass it.

Compatibility

The flag is off by default. I generated every OpenAPI fixture under packages/massimo-cli/test/fixtures with main 8c10018 and with this branch: the 47 specs on main (one YAML, and four without an openapi key) plus the new fixture, in four modes (--types-only, full client, --frontend, --frontend --language ts), each plain and with --named-schemas. That is 768 runs (384 pairs) and 1,048 files, with identical bytes and identical exit codes; the only non-zero exit, common-parameters/openapi.json with --frontend, is the same on both sides. The 1.6.0 warning for --named-schemas on a spec without components.schemas is unchanged.

Size: on the Stripe spec, --types-only --path-map adds 7.0% to the declaration file. Petstore 3 and Stripe already fail tsc on main because a default response prints as FullResponse<..., default> (responses-writer.js:51, #16); the error count is the same with and without --path-map, and with that status patched to number the map check compiles with 0 errors.

Tests

test/cli-openapi-path-map.test.js, with a fixture that has an operation without an operationId, one operationId on two paths, a 204-only response, and schemas named like the map and an alias.

  • The map is emitted for --types-only and --frontend --language ts. A strict tsc check (skipLibCheck: false) asserts each entry is the request and responses type of the matching client method, the aliases resolve every entry for a generic path and method, and an unknown path or method is an error.
  • With --named-schemas, a schema named like the map is renamed with the Schema suffix.
  • An operationId <name>Path suffixes the aliases, plain and with --named-schemas.
  • A spec without components.schemas gives no warning with --path-map.
  • Without the flag the output equals the output with it once the map and its aliases are removed, in every mode; with --named-schemas, once the two expected schema renames (and their comments) are undone as well.

Revert proofs: computing the operation type names only under --named-schemas makes the plain suffix test fail its MoviesPathRequest2 assertion, before tsc runs; compiling that output on its own gives TS2300 (duplicate identifier MoviesPathRequest and MoviesPathResponses). Indexing the map directly instead of infer makes the four map tests fail their alias text assertions, and the two suffix tests fail their strict tsc check with TS2536; a separate compile of a two-path spec gives the same TS2536.

Suites on this commit: pnpm test in packages/massimo-cli passes 174 of 174 tests over 24 files, with a clean tsd posttest; pnpm run lint passes; root pnpm run lint:markdown passes and rewrites nothing. I did not run the Playwright e2e.

Docs: one help line for --path-map, and a ### Path Map section in the README after Named Schemas.

This PR was prepared with AI assistance (Claude). I reviewed the change, and both contributors signed off per CONTRIBUTING.md.

… operations

The generated types name each operation's request and responses types
after its operationId, so code that only knows the path template and
the HTTP method (a generic fetch wrapper, a router, a mock server) has
no way to reach them, and an operation without operationId or with a
repeated one gets a name that is hard to predict.

With `--path-map` the generated types also declare `<Name>Paths`, an
interface keyed by path template, then by method, whose entries name
the request and responses types the client method uses.
`writeOperations` now returns one entry per operation with the names
it declared, so the map follows the ids generated from a path and the
renames of repeated ids instead of recomputing them. Two aliases,
`<Name>PathRequest<P, M>` and `<Name>PathResponses<P, M>`, resolve an
entry for a generic path and method through `infer`: indexing
`<Name>Paths[P][M]['request']` does not compile there (TS2536).

The map names only operation types, which every mode declares, so the
option does not depend on `--named-schemas` and is a flag of its own.
An alias whose name an operation type already has (an operationId of
`<name>Path`) gets a numeric suffix; for that the operation type names
are computed whenever either flag is set. With both flags a component
schema named like the map or an alias is renamed with the `Schema`
suffix, like any other taken name.

The flag is off by default: without it the output, `--named-schemas`
included, is unchanged.

Co-Authored-By: Claude <noreply@anthropic.com>
Signed-off-by: Claude <noreply@anthropic.com>
Signed-off-by: Mirko Pira <mirkodev@outlook.com>

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant