-
Notifications
You must be signed in to change notification settings - Fork 10
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
This adds support for typed methods, by generating endpoint and method types. This is largely taken from scripts in "@octokit/plugin-rest-endpoint-methods". As "@octokit/types" does not support GHEC types, we cannot import endpoint types directly. Instead, we generate them for this package from "@octokit/openapi-types-ghec".
- Loading branch information
1 parent
9ce4dd9
commit ed9bb18
Showing
10 changed files
with
2,871 additions
and
3 deletions.
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
117 changes: 117 additions & 0 deletions
117
scripts/update-endpoints/templates/endpoints.ts.template
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,117 @@ | ||
// DO NOT EDIT THIS FILE | ||
import type { paths } from "@octokit/openapi-types-ghec"; | ||
import type { OctokitResponse, RequestHeaders, RequestRequestOptions } from "@octokit/types"; | ||
|
||
/** | ||
* @license (MIT OR CC0-1.0) | ||
* @source https://github.com/sindresorhus/type-fest/blob/570e27f8fdaee37ef5d5e0fbf241e0212ff8fc1a/source/simplify.d.ts | ||
*/ | ||
export type Simplify<T> = {[KeyType in keyof T]: T[KeyType]} & {}; | ||
|
||
// https://stackoverflow.com/a/50375286/206879 | ||
type UnionToIntersection<U> = (U extends any ? (k: U) => void : never) extends ( | ||
k: infer I | ||
) => void | ||
? I | ||
: never; | ||
|
||
type ExtractParameters<T> = "parameters" extends keyof T | ||
? UnionToIntersection< | ||
{ | ||
// query parameter might be optional | ||
// https://github.com/octokit/types.ts/pull/555#issuecomment-1585105522 | ||
[K in keyof T["parameters"]]-?: T["parameters"][K]; | ||
}[keyof T["parameters"]] | ||
> | ||
: {}; | ||
type ExtractRequestBody<T> = "requestBody" extends keyof T | ||
? "content" extends keyof T["requestBody"] | ||
? "application/json" extends keyof T["requestBody"]["content"] | ||
? T["requestBody"]["content"]["application/json"] | ||
: { | ||
data: { | ||
[K in keyof T["requestBody"]["content"]]: T["requestBody"]["content"][K]; | ||
}[keyof T["requestBody"]["content"]]; | ||
} | ||
: "application/json" extends keyof T["requestBody"] | ||
? T["requestBody"]["application/json"] | ||
: { | ||
data: { | ||
[K in keyof T["requestBody"]]: T["requestBody"][K]; | ||
}[keyof T["requestBody"]]; | ||
} | ||
: {}; | ||
type ToOctokitParameters<T> = ExtractParameters<T> & ExtractRequestBody<Required<T>>; | ||
|
||
type Operation<Url extends keyof paths, Method extends keyof paths[Url]> = { | ||
parameters: Simplify<ToOctokitParameters<paths[Url][Method]>>; | ||
request: Method extends ReadOnlyMethods | ||
? { | ||
method: Method extends string ? Uppercase<Method> : never; | ||
url: Url; | ||
headers: RequestHeaders; | ||
request: RequestRequestOptions; | ||
} | ||
: { | ||
method: Method extends string ? Uppercase<Method> : never; | ||
url: Url; | ||
headers: RequestHeaders; | ||
request: RequestRequestOptions; | ||
data: ExtractRequestBody<paths[Url][Method]>; | ||
}; | ||
response: ExtractOctokitResponse<paths[Url][Method]>; | ||
}; | ||
|
||
type ReadOnlyMethods = "get" | "head"; | ||
type SuccessStatuses = 200 | 201 | 202 | 204 | 205; | ||
type RedirectStatuses = 301 | 302; | ||
type EmptyResponseStatuses = 201 | 204 | 205; | ||
type KnownJsonResponseTypes = | ||
| "application/json" | ||
| "application/octocat-stream" // GET /octocat | ||
| "application/scim+json" | ||
| "text/html" | ||
| "text/plain"; // GET /zen | ||
|
||
type SuccessResponseDataType<Responses> = { | ||
[K in SuccessStatuses & keyof Responses]: GetContentKeyIfPresent< | ||
Responses[K] | ||
> extends never | ||
? never | ||
: OctokitResponse<GetContentKeyIfPresent<Responses[K]>, K>; | ||
}[SuccessStatuses & keyof Responses]; | ||
type RedirectResponseDataType<Responses> = { | ||
[K in RedirectStatuses & keyof Responses]: OctokitResponse<unknown, K>; | ||
}[RedirectStatuses & keyof Responses]; | ||
type EmptyResponseDataType<Responses> = { | ||
[K in EmptyResponseStatuses & keyof Responses]: OctokitResponse<never, K>; | ||
}[EmptyResponseStatuses & keyof Responses]; | ||
|
||
type GetContentKeyIfPresent<T> = "content" extends keyof T | ||
? DataType<T["content"]> | ||
: DataType<T>; | ||
type DataType<T> = { | ||
[K in KnownJsonResponseTypes & keyof T]: T[K]; | ||
}[KnownJsonResponseTypes & keyof T]; | ||
type ExtractOctokitResponse<R> = "responses" extends keyof R | ||
? SuccessResponseDataType<R["responses"]> extends never | ||
? RedirectResponseDataType<R["responses"]> extends never | ||
? EmptyResponseDataType<R["responses"]> | ||
: RedirectResponseDataType<R["responses"]> | ||
: SuccessResponseDataType<R["responses"]> | ||
: unknown; | ||
|
||
export interface Endpoints { | ||
{{#each endpointsByRoute}} | ||
/** | ||
* @see {{documentationUrl}} | ||
{{#deprecated}} | ||
* @deprecated {{{.}}} | ||
{{/deprecated}} | ||
*/ | ||
"{{@key}}": Operation< | ||
"{{url}}", | ||
"{{method}}" | ||
>, | ||
{{/each}} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,160 @@ | ||
import { writeFileSync, readFileSync } from "node:fs"; | ||
import { join as pathJoin } from "node:path"; | ||
|
||
import camelCase from "lodash.camelcase"; | ||
import { format } from "prettier"; | ||
import { stringToJsdocComment } from "string-to-jsdoc-comment"; | ||
import sortKeys from "sort-keys"; | ||
|
||
const ENDPOINTS = JSON.parse(readFileSync(new URL("./generated/endpoints.json", import.meta.url), "utf-8")); | ||
import { isDeprecated } from "./util.js"; | ||
|
||
generateTypes(); | ||
|
||
async function generateTypes() { | ||
const ROUTES = await getRoutes(); | ||
|
||
const namespaces = Object.keys(ROUTES).reduce((namespaces, namespace) => { | ||
const methods = Object.keys(ROUTES[namespace]).reduce((methods, methodName) => { | ||
const entry = ROUTES[namespace][methodName]; | ||
|
||
const description = [entry.description, entry.deprecated && `@deprecated ${entry.deprecated}`].filter(Boolean).join("\n"); | ||
|
||
return methods.concat({ | ||
name: methodName, | ||
route: `${entry.method} ${entry.url}`, | ||
hasRequiredPreviews: entry.hasRequiredPreviews, | ||
jsdoc: stringToJsdocComment(description), | ||
}); | ||
}, []); | ||
|
||
return namespaces.concat({ | ||
namespace: camelCase(namespace), | ||
methods, | ||
}); | ||
}, []); | ||
|
||
const RestEndpointMethodParameterAndResponseTypes = []; | ||
for (const namespace of namespaces) { | ||
const namespaceMethods = []; | ||
for (const method of namespace.methods) { | ||
namespaceMethods.push( | ||
`${method.name}: { | ||
parameters: RequestParameters & Endpoints["${method.route}"]["parameters"], | ||
response: Endpoints["${method.route}"]["response"] | ||
}` | ||
); | ||
} | ||
|
||
RestEndpointMethodParameterAndResponseTypes.push(`${namespace.namespace}: { | ||
${namespaceMethods.join("\n")} | ||
}`); | ||
} | ||
|
||
const RestEndpointMethodNamespaceTypes = []; | ||
for (const namespace of namespaces) { | ||
const namespaceMethods = []; | ||
for (const method of namespace.methods) { | ||
namespaceMethods.push( | ||
[ | ||
method.jsdoc, | ||
`${method.name}: { | ||
(params?: RestEndpointMethodTypes["${namespace.namespace}"]["${method.name}"]["parameters"]): Promise<RestEndpointMethodTypes["${namespace.namespace}"]["${method.name}"]["response"]> | ||
defaults: RequestInterface["defaults"]; | ||
endpoint: EndpointInterface<{ url: string }>; | ||
}`, | ||
].join("\n") | ||
); | ||
} | ||
|
||
RestEndpointMethodNamespaceTypes.push(`${namespace.namespace}: { | ||
${namespaceMethods.join("\n")} | ||
}`); | ||
} | ||
|
||
const methodTypesSource = await format( | ||
[ | ||
`import type { EndpointInterface, RequestInterface } from "@octokit/types";`, | ||
`import type { RestEndpointMethodTypes } from "./parameters-and-response-types.js";`, | ||
"", | ||
`export type RestEndpointMethods = { | ||
${RestEndpointMethodNamespaceTypes.join("\n")} | ||
}`, | ||
].join("\n"), | ||
{ | ||
parser: "typescript", | ||
} | ||
); | ||
const parametersAndResponsesTypes = await format( | ||
[ | ||
`import type { RequestParameters } from "@octokit/types";`, | ||
`import type { Endpoints } from "./endpoints-types.js";`, | ||
"", | ||
`export type RestEndpointMethodTypes = { | ||
${RestEndpointMethodParameterAndResponseTypes.join("\n")} | ||
}`, | ||
].join("\n"), | ||
{ | ||
parser: "typescript", | ||
} | ||
); | ||
|
||
const methodTypesFilePath = pathJoin(process.cwd(), "src", "generated", "method-types.ts"); | ||
|
||
writeFileSync(methodTypesFilePath, methodTypesSource, "utf8"); | ||
console.log(`Types written to ${methodTypesFilePath}`); | ||
|
||
const parametersAndResponseFilePath = pathJoin(process.cwd(), "src", "generated", "parameters-and-response-types.ts"); | ||
|
||
writeFileSync(parametersAndResponseFilePath, parametersAndResponsesTypes, "utf8"); | ||
console.log(`Types written to ${parametersAndResponseFilePath}`); | ||
} | ||
|
||
async function getRoutes() { | ||
const newRoutes = {}; | ||
|
||
ENDPOINTS.forEach((endpoint) => { | ||
if (isDeprecated(endpoint)) return; | ||
|
||
const scope = endpoint.scope; | ||
|
||
if (!newRoutes[scope]) { | ||
newRoutes[scope] = {}; | ||
} | ||
|
||
const idName = endpoint.id; | ||
const url = endpoint.url | ||
.toLowerCase() | ||
// stecial case for "Upload a release asset": remove ":origin" prefix | ||
.replace(/^:origin/, ""); | ||
|
||
// new route | ||
newRoutes[scope][idName] = { | ||
method: endpoint.method, | ||
url, | ||
description: endpoint.description, | ||
hasRequiredPreviews: !!endpoint.previews.length, | ||
deprecated: newRoutes[scope][idName] ? newRoutes[scope][idName].deprecated : undefined, | ||
}; | ||
|
||
if (endpoint.renamed) { | ||
const { before, after } = endpoint.renamed; | ||
if (!newRoutes[before.scope]) { | ||
newRoutes[before.scope] = {}; | ||
} | ||
|
||
if (!newRoutes[before.scope][before.id]) { | ||
newRoutes[before.scope][before.id] = newRoutes[scope][idName]; | ||
} | ||
|
||
newRoutes[before.scope][before.id].deprecated = | ||
`octokit.rest.${before.scope}.${before.id}() has been renamed to octokit.rest.${after.scope}.${after.id}() (${endpoint.renamed.date})`; | ||
} | ||
|
||
if (endpoint.isDeprecated) { | ||
newRoutes[scope][idName].deprecated = `octokit.rest.${scope}.${idName}() is deprecated, see ${endpoint.documentationUrl}`; | ||
} | ||
}); | ||
|
||
return sortKeys(newRoutes, { deep: true }); | ||
} |
Oops, something went wrong.