Skip to content

Commit

Permalink
feat: endpoint response types (#35)
Browse files Browse the repository at this point in the history
This will allow for typeaheads for RequestInterface (e.g. for octokit.request) like it does for EndpointInterfeace, and sets types for responses
  • Loading branch information
gr2m authored Apr 8, 2020
1 parent 27412f2 commit 342954c
Show file tree
Hide file tree
Showing 8 changed files with 32,366 additions and 3,443 deletions.
24 changes: 24 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
"author": "Gregor Martynus (https://twitter.com/gr2m)",
"license": "MIT",
"devDependencies": {
"@gimenete/type-writer": "^0.1.5",
"@octokit/graphql": "^4.2.2",
"handlebars": "^4.7.6",
"lodash.set": "^4.3.2",
Expand Down
10 changes: 10 additions & 0 deletions scripts/update-endpoints/fetch-json.js
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,16 @@ const QUERY = `
type
required
}
responses {
code
description
examples {
data
}
}
renamed {
note
}
}
}`;

Expand Down
8,133 changes: 7,453 additions & 680 deletions scripts/update-endpoints/generated/endpoints.json

Large diffs are not rendered by default.

15 changes: 9 additions & 6 deletions scripts/update-endpoints/templates/endpoints.ts.template
Original file line number Diff line number Diff line change
@@ -1,27 +1,30 @@
// DO NOT EDIT THIS FILE
import { RequestHeaders } from "../RequestHeaders";
import { RequestRequestOptions } from "../RequestRequestOptions";
import { OctokitResponse } from "../OctokitResponse";
import { Url } from "../Url";
type AnyResponse = OctokitResponse<any>;

export interface Endpoints {
{{#each endpointsByRoute}}
"{{@key}}": [{{union this "optionsTypeName"}}, {{union this "requestOptionsTypeName"}}]
"{{@key}}": [{{union this "optionsTypeName"}}, {{union this "requestOptionsTypeName"}}, {{union this "responseTypeName"}}]
{{/each}}
}

{{#each options}}
type {{in.name}} = {
{{#each in.parameters}}
type {{parameters.name}} = {
{{#each parameters.parameters}}
{{&jsdoc}}
{{{name this}}}: {{{type this}}}
{{/each}}
}
type {{out.name}} = {
method: "{{out.method}}",
url: "{{out.url}}",
type {{request.name}} = {
method: "{{request.method}}",
url: "{{request.url}}",
headers: RequestHeaders,
request: RequestRequestOptions
}
{{&response}}
{{/each}}

{{#childParams}}
Expand Down
59 changes: 49 additions & 10 deletions scripts/update-endpoints/typescript.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ const { pascalCase } = require("pascal-case");
const prettier = require("prettier");
const { stringToJsdocComment } = require("string-to-jsdoc-comment");
const sortKeys = require("sort-keys");
const TypeWriter = require("@gimenete/type-writer");

const ENDPOINTS = require("./generated/endpoints.json");
const ENDPOINTS_PATH = resolve(
Expand Down Expand Up @@ -61,6 +62,8 @@ const typeMap = {
};

for (const endpoint of ENDPOINTS) {
if (endpoint.renamed) continue;

const route = `${endpoint.method} ${endpoint.url.replace(
/\{([^}]+)}/g,
":$1"
Expand All @@ -71,27 +74,47 @@ for (const endpoint of ENDPOINTS) {
}

endpointsByRoute[route].push({
optionsTypeName:
pascalCase(`${endpoint.scope} ${endpoint.id}`) + "Endpoint",
requestOptionsTypeName:
pascalCase(`${endpoint.scope} ${endpoint.id}`) + "RequestOptions",
optionsTypeName: pascalCase(`${endpoint.scope} ${endpoint.id} Endpoint`),
requestOptionsTypeName: pascalCase(
`${endpoint.scope} ${endpoint.id} RequestOptions`
),
responseTypeName: endpointToResponseTypeName(endpoint),
});
}

const options = [];
const childParams = {};

for (const endpoint of ENDPOINTS) {
if (endpoint.renamed) continue;

const typeWriter = new TypeWriter();
const { method, parameters } = endpoint;
const url = endpoint.url.replace(/\{([^}]+)}/g, ":$1");

const optionsTypeName =
pascalCase(`${endpoint.scope} ${endpoint.id}`) + "Endpoint";
const requestOptionsTypeName =
pascalCase(`${endpoint.scope} ${endpoint.id}`) + "RequestOptions";
const optionsTypeName = pascalCase(
`${endpoint.scope} ${endpoint.id} Endpoint`
);
const requestOptionsTypeName = pascalCase(
`${endpoint.scope} ${endpoint.id} RequestOptions`
);

const responses =
endpoint.responses.length && endpoint.responses[0].examples
? endpoint.responses[0].examples.map((example) =>
JSON.parse(example.data)
)
: undefined;
if (responses) {
typeWriter.add(responses, {
rootTypeName: endpointToResponseTypeName(endpoint),
});

typeWriter.generate("typescript");
}

options.push({
in: {
parameters: {
name: optionsTypeName,
parameters: parameters
.map(parameterize)
Expand Down Expand Up @@ -133,14 +156,19 @@ for (const endpoint of ENDPOINTS) {
})
.filter(Boolean),
},
out: {
request: {
name: requestOptionsTypeName,
method,
url,
},
response: responses ? typeWriter.generate("typescript") : "",
});

process.stdout.write(".");
}

console.log("\ndone.");

const result = template({
endpointsByRoute: sortKeys(endpointsByRoute, { deep: true }),
options,
Expand Down Expand Up @@ -176,3 +204,14 @@ function parameterize(parameter) {
jsdoc: stringToJsdocComment(parameter.description),
};
}

function endpointToResponseTypeName(endpoint) {
const hasResponses =
endpoint.responses.length && endpoint.responses[0].examples;

if (hasResponses) {
return pascalCase(`${endpoint.scope} ${endpoint.id} ResponseData`);
}

return "any";
}
13 changes: 10 additions & 3 deletions src/RequestInterface.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ import { OctokitResponse } from "./OctokitResponse";
import { RequestParameters } from "./RequestParameters";
import { Route } from "./Route";

import { Endpoints } from "./generated/Endpoints";

export interface RequestInterface {
/**
* Sends a request based on endpoint options
Expand All @@ -18,9 +20,14 @@ export interface RequestInterface {
* @param {string} route Request method + URL. Example: `'GET /orgs/:org'`
* @param {object} [parameters] URL, query or body parameters, as well as `headers`, `mediaType.{format|previews}`, `request`, or `baseUrl`.
*/
<T = any>(route: Route, parameters?: RequestParameters): Promise<
OctokitResponse<T>
>;
<R extends Route>(
route: keyof Endpoints | R,
options?: R extends keyof Endpoints
? Endpoints[R][0] & RequestParameters
: RequestParameters
): R extends keyof Endpoints
? Promise<OctokitResponse<Endpoints[R][2]>>
: Promise<OctokitResponse<any>>;

/**
* Returns a new `endpoint` with updated route and parameters
Expand Down
Loading

0 comments on commit 342954c

Please # to comment.