From e18d43c37fe6c5bc3aa07bb89d9a0b0c513a84ae Mon Sep 17 00:00:00 2001 From: Alejandro Corredor Date: Mon, 12 Feb 2024 14:06:03 -0500 Subject: [PATCH] feat!: generate just typescript --- src/__snapshots__/router.test.ts.snap | 78 +++++++++++- src/bin/cli.ts | 9 +- src/generate-axios-client.test.ts | 163 ++++++++++++-------------- src/generate-axios-client.ts | 106 ++++++++--------- src/integration.axios.test.ts | 8 +- src/router.test.ts | 2 +- 6 files changed, 207 insertions(+), 159 deletions(-) diff --git a/src/__snapshots__/router.test.ts.snap b/src/__snapshots__/router.test.ts.snap index cc17460..7f38bf2 100644 --- a/src/__snapshots__/router.test.ts.snap +++ b/src/__snapshots__/router.test.ts.snap @@ -29,8 +29,38 @@ export type Endpoints = { }; }; -export declare class Client { - constructor(client: AxiosInstance); +/* eslint-disable */ + +const substituteParams = (url: string, params: Object) => + Object.entries(params).reduce( + (url, [name, value]) => url.replace(\\":\\" + name, encodeURIComponent(value)), + url + ); + +const removePathParams = (url: string, params: Object) => + Object.entries(params) + .filter(([key, value]) => value !== undefined) + .reduce( + (accum, [name, value]) => + url.includes(\\":\\" + name) ? accum : { ...accum, [name]: value }, + {} + ); + +const parseQueryParamsFromPagingLink = (link: string) => { + const params = new URLSearchParams(link.split(\\"?\\")[1]); + + return { + nextPageToken: params.get(\\"nextPageToken\\"), + pageSize: params.get(\\"pageSize\\"), + }; +}; + +export class Client { + client: AxiosInstance; + + constructor(client: AxiosInstance) { + this.client = client; + } /** * Executes the \`POST /items\` endpoint. @@ -44,7 +74,14 @@ export declare class Client { data: Endpoints[\\"POST /items\\"][\\"Request\\"] & Endpoints[\\"POST /items\\"][\\"PathParams\\"], config?: AxiosRequestConfig - ): Promise>; + ): Promise> { + return this.client.request({ + ...config, + method: \\"POST\\", + data: removePathParams(\\"/items\\", data), + url: substituteParams(\\"/items\\", data), + }); + } /** * Executes the \`GET /items/:id\` endpoint. @@ -58,14 +95,21 @@ export declare class Client { data: Endpoints[\\"GET /items/:id\\"][\\"Request\\"] & Endpoints[\\"GET /items/:id\\"][\\"PathParams\\"], config?: AxiosRequestConfig - ): Promise>; + ): Promise> { + return this.client.request({ + ...config, + method: \\"GET\\", + params: removePathParams(\\"/items/:id\\", data), + url: substituteParams(\\"/items/:id\\", data), + }); + } /** * Paginates exhaustively through the provided \`request\`, using the specified * \`data\`. A \`pageSize\` can be specified in the \`data\` to customize the * page size for pagination. */ - paginate( + async paginate( request: ( data: T, config?: AxiosRequestConfig @@ -74,7 +118,29 @@ export declare class Client { >, data: T, config?: AxiosRequestConfig - ): Promise; + ): Promise { + const result = []; + + let nextPageParams = {}; + do { + // @ts-expect-error + const response = await this[request.name]( + { ...nextPageParams, ...data }, + config + ); + + result.push(...response.data.items); + + nextPageParams = response.data.links.next + ? parseQueryParamsFromPagingLink(response.data.links.next) + : {}; + // @ts-expect-error + } while (!!nextPageParams.nextPageToken); + + return result; + } } + +module.exports.Client = Client; " `; diff --git a/src/bin/cli.ts b/src/bin/cli.ts index 9568887..7af8cff 100644 --- a/src/bin/cli.ts +++ b/src/bin/cli.ts @@ -125,16 +125,9 @@ const program = yargs(process.argv.slice(2)) outputClass: argv.name, }); - writeGeneratedFile(argv.output.replace('.ts', '.js'), output.javascript, { + writeGeneratedFile(argv.output, output.typescript, { format: argv.format, }); - writeGeneratedFile( - argv.output.replace('.ts', '.d.ts'), - output.declaration, - { - format: argv.format, - }, - ); }, ) .command( diff --git a/src/generate-axios-client.test.ts b/src/generate-axios-client.test.ts index 7289cb2..4b35077 100644 --- a/src/generate-axios-client.test.ts +++ b/src/generate-axios-client.test.ts @@ -16,13 +16,12 @@ It contains newlines. describe('generate', () => { const generateAndFormat = (input: GenerateAxiosClientInput) => generateAxiosClient(input).then((source) => ({ - javascript: format(source.javascript, { parser: 'babel' }), - declaration: format(source.declaration, { parser: 'typescript' }), + typescript: format(source.typescript, { parser: 'typescript' }), })); const FIXTURES: { input: GenerateAxiosClientInput; - expected: { javascript: string; declaration: string }; + expected: { typescript: string }; }[] = [ { input: { @@ -70,79 +69,7 @@ describe('generate', () => { }, }, expected: { - javascript: `/* eslint-disable */ - -const substituteParams = (url, params) => - Object.entries(params).reduce( - (url, [name, value]) => url.replace(":" + name, encodeURIComponent(value)), - url - ); - -const removePathParams = (url, params) => - Object.entries(params) - .filter(([key, value]) => value !== undefined) - .reduce( - (accum, [name, value]) => - url.includes(":" + name) ? accum : { ...accum, [name]: value }, - {} - ); - -const parseQueryParamsFromPagingLink = (link) => { - const params = new URLSearchParams(link.split("?")[1]); - - return { - nextPageToken: params.get("nextPageToken"), - pageSize: params.get("pageSize"), - }; -}; - -class Client { - constructor(client) { - this.client = client; - } - - getPosts(data, config) { - return this.client.request({ - ...config, - method: "GET", - params: removePathParams("/posts", data), - url: substituteParams("/posts", data), - }); - } - - putPost(data, config) { - return this.client.request({ - ...config, - method: "PUT", - data: removePathParams("/posts/:id", data), - url: substituteParams("/posts/:id", data), - }); - } - - async paginate(request, data, config) { - const result = []; - - let nextPageParams = {}; - do { - const response = await this[request.name]( - { ...nextPageParams, ...data }, - config - ); - - result.push(...response.data.items); - - nextPageParams = response.data.links.next - ? parseQueryParamsFromPagingLink(response.data.links.next) - : {}; - } while (!!nextPageParams.nextPageToken); - - return result; - } -} - -module.exports.Client = Client; -`, - declaration: `/* eslint-disable */ + typescript: `/* eslint-disable */ import { AxiosInstance, AxiosRequestConfig, AxiosResponse } from "axios"; export type Endpoints = { @@ -176,8 +103,38 @@ export type Endpoints = { }; }; -export declare class Client { - constructor(client: AxiosInstance); +/* eslint-disable */ + +const substituteParams = (url: string, params: Object) => + Object.entries(params).reduce( + (url, [name, value]) => url.replace(":" + name, encodeURIComponent(value)), + url + ); + +const removePathParams = (url: string, params: Object) => + Object.entries(params) + .filter(([key, value]) => value !== undefined) + .reduce( + (accum, [name, value]) => + url.includes(":" + name) ? accum : { ...accum, [name]: value }, + {} + ); + +const parseQueryParamsFromPagingLink = (link: string) => { + const params = new URLSearchParams(link.split("?")[1]); + + return { + nextPageToken: params.get("nextPageToken"), + pageSize: params.get("pageSize"), + }; +}; + +export class Client { + client: AxiosInstance; + + constructor(client: AxiosInstance) { + this.client = client; + } /** * Executes the \`GET /posts\` endpoint. @@ -191,7 +148,14 @@ export declare class Client { data: Endpoints["GET /posts"]["Request"] & Endpoints["GET /posts"]["PathParams"], config?: AxiosRequestConfig - ): Promise>; + ): Promise> { + return this.client.request({ + ...config, + method: "GET", + params: removePathParams("/posts", data), + url: substituteParams("/posts", data), + }); + } /** * This is a long description about a field. It contains lots of very long text. Sometimes the text might be over the desired line length. @@ -209,14 +173,21 @@ export declare class Client { data: Endpoints["PUT /posts/:id"]["Request"] & Endpoints["PUT /posts/:id"]["PathParams"], config?: AxiosRequestConfig - ): Promise>; + ): Promise> { + return this.client.request({ + ...config, + method: "PUT", + data: removePathParams("/posts/:id", data), + url: substituteParams("/posts/:id", data), + }); + } /** * Paginates exhaustively through the provided \`request\`, using the specified * \`data\`. A \`pageSize\` can be specified in the \`data\` to customize the * page size for pagination. */ - paginate( + async paginate( request: ( data: T, config?: AxiosRequestConfig @@ -225,8 +196,30 @@ export declare class Client { >, data: T, config?: AxiosRequestConfig - ): Promise; + ): Promise { + const result = []; + + let nextPageParams = {}; + do { + // @ts-expect-error + const response = await this[request.name]( + { ...nextPageParams, ...data }, + config + ); + + result.push(...response.data.items); + + nextPageParams = response.data.links.next + ? parseQueryParamsFromPagingLink(response.data.links.next) + : {}; + // @ts-expect-error + } while (!!nextPageParams.nextPageToken); + + return result; + } } + +module.exports.Client = Client; `, }, }, @@ -235,11 +228,9 @@ export declare class Client { FIXTURES.forEach(({ input, expected }, idx) => { test(`fixture ${idx}`, async () => { const result = await generateAndFormat(input); - expect(result.javascript).toStrictEqual(expected.javascript); - expect(result.declaration).toStrictEqual(expected.declaration); + expect(result.typescript).toStrictEqual(expected.typescript); - writeFileSync(`${__dirname}/test-generated.js`, result.javascript); - writeFileSync(`${__dirname}/test-generated.d.ts`, result.declaration); + writeFileSync(`${__dirname}/test-generated.ts`, result.typescript); }); }); }); diff --git a/src/generate-axios-client.ts b/src/generate-axios-client.ts index de57d96..3294b49 100644 --- a/src/generate-axios-client.ts +++ b/src/generate-axios-client.ts @@ -1,4 +1,4 @@ -import { OneSchemaDefinition } from '.'; +import { EndpointDefinition, OneSchemaDefinition } from '.'; import { generateEndpointTypes } from './generate-endpoints'; import { validateSchema } from './meta-schema'; @@ -8,8 +8,7 @@ export type GenerateAxiosClientInput = { }; export type GenerateAxiosClientOutput = { - javascript: string; - declaration: string; + typescript: string; }; const toJSDocLines = (docs: string): string => @@ -26,6 +25,27 @@ const PAGINATE_JSDOC = ` */ `.trim(); +const generateEndpointHelper = ([endpoint, { Name, Description }]: [ + string, + EndpointDefinition, +]) => { + return { + jsdoc: `/** + ${toJSDocLines(Description || `Executes the \`${endpoint}\` endpoint.`)} + * + * @param data The request data. + * @param config The Axios request overrides for the request. + * + * @returns An AxiosResponse object representing the response. + */`, + declaration: `${Name}( + data: Endpoints['${endpoint}']['Request'] & + Endpoints['${endpoint}']['PathParams'], + config?: AxiosRequestConfig + ): Promise>`, + }; +}; + export const generateAxiosClient = async ({ spec, outputClass, @@ -37,56 +57,20 @@ export const generateAxiosClient = async ({ "import { AxiosInstance, AxiosRequestConfig, AxiosResponse } from 'axios';", '', await generateEndpointTypes(spec), - ` -export declare class ${outputClass} { - - constructor(client: AxiosInstance); - - ${Object.entries(spec.Endpoints) - .map(([endpoint, { Name, Description }]) => { - return ` - /** - ${toJSDocLines( - Description || `Executes the \`${endpoint}\` endpoint.`, - )} - * - * @param data The request data. - * @param config The Axios request overrides for the request. - * - * @returns An AxiosResponse object representing the response. - */ - ${Name}( - data: Endpoints['${endpoint}']['Request'] & - Endpoints['${endpoint}']['PathParams'], - config?: AxiosRequestConfig - ): Promise>`; - }) - .join('\n\n')} - - ${PAGINATE_JSDOC} - paginate( - request: ( - data: T, - config?: AxiosRequestConfig - ) => Promise< - AxiosResponse<{ items: Item[]; links: { self: string; next?: string } }> - >, - data: T, - config?: AxiosRequestConfig - ): Promise; -}`.trim(), ].join('\n'); - const javascript = ` + const typescript = ` +${declaration} + /* eslint-disable */ -const substituteParams = (url, params) => +const substituteParams = (url: string, params: Object) => Object.entries(params).reduce( (url, [name, value]) => url.replace(":" + name, encodeURIComponent(value)), url ); -const removePathParams = (url, params) => +const removePathParams = (url: string, params: Object) => Object.entries(params) .filter(([key, value]) => value !== undefined) .reduce( @@ -95,7 +79,7 @@ const removePathParams = (url, params) => {} ); -const parseQueryParamsFromPagingLink = (link) => { +const parseQueryParamsFromPagingLink = (link: string) => { const params = new URLSearchParams(link.split('?')[1]); return { @@ -104,18 +88,23 @@ const parseQueryParamsFromPagingLink = (link) => { }; }; -class ${outputClass} { +export class ${outputClass} { + client: AxiosInstance; - constructor(client) { + constructor(client: AxiosInstance) { this.client = client; } ${Object.entries(spec.Endpoints) - .map(([endpoint, { Name }]) => { + .map((entry) => { + const [endpoint] = entry; const [method, url] = endpoint.split(' '); const useQueryParams = ['GET', 'DELETE'].includes(method); + const { jsdoc, declaration } = generateEndpointHelper(entry); + return ` - ${Name}(data, config) { + ${jsdoc} + ${declaration}{ return this.client.request({ ...config, method: '${method}', @@ -131,11 +120,22 @@ class ${outputClass} { }) .join('\n\n')} - async paginate(request, data, config) { + ${PAGINATE_JSDOC} + async paginate( + request: ( + data: T, + config?: AxiosRequestConfig, + ) => Promise< + AxiosResponse<{ items: Item[]; links: { self: string; next?: string } }> + >, + data: T, + config?: AxiosRequestConfig + ): Promise { const result = []; let nextPageParams = {}; do { + // @ts-expect-error const response = await this[request.name]( { ...nextPageParams, ...data }, config @@ -146,6 +146,7 @@ class ${outputClass} { nextPageParams = response.data.links.next ? parseQueryParamsFromPagingLink(response.data.links.next) : {}; + // @ts-expect-error } while (!!nextPageParams.nextPageToken); return result; @@ -153,10 +154,9 @@ class ${outputClass} { } module.exports.${outputClass} = ${outputClass}; - `.trim(); +`.trim(); return { - javascript, - declaration, + typescript, }; }; diff --git a/src/integration.axios.test.ts b/src/integration.axios.test.ts index d0b7dba..58f4534 100644 --- a/src/integration.axios.test.ts +++ b/src/integration.axios.test.ts @@ -13,8 +13,7 @@ const testGeneratedFile = (ext: string) => `${__dirname}/test-generated${ext}`; const generateAndFormat = (input: GenerateAxiosClientInput) => generateAxiosClient(input).then((source) => ({ - javascript: format(source.javascript, { parser: 'babel' }), - declaration: format(source.declaration, { parser: 'typescript' }), + typescript: format(source.typescript, { parser: 'typescript' }), })); const mockMiddleware = jest.fn(); @@ -122,10 +121,9 @@ beforeEach(async () => { const output = await generateAndFormat({ spec, outputClass: 'Service' }); - writeFileSync(testGeneratedFile('.js'), output.javascript); - writeFileSync(testGeneratedFile('.d.ts'), output.declaration); + writeFileSync(testGeneratedFile('.ts'), output.typescript); - const { Service } = await import(testGeneratedFile('.js')); + const { Service } = await import(testGeneratedFile('.ts')); client = new Service(context.client); }); diff --git a/src/router.test.ts b/src/router.test.ts index e095671..1568693 100644 --- a/src/router.test.ts +++ b/src/router.test.ts @@ -564,7 +564,7 @@ describe('introspection', () => { outputClass: 'Client', }); - const formattedDeclaration = format(clientCode.declaration, { + const formattedDeclaration = format(clientCode.typescript, { parser: 'typescript', });