From 3215baca8d19447c1fb97a32ccd7b452cde62a60 Mon Sep 17 00:00:00 2001 From: Swain Molster Date: Tue, 2 Aug 2022 10:27:07 -0400 Subject: [PATCH] feat: support a Description field on endpoints --- README.md | 16 ++++++++++- src/generate-axios-client.test.ts | 45 ++++++++++++++++++++++++++----- src/generate-axios-client.ts | 37 ++++++++++++++++++------- src/meta-schema.ts | 1 + src/openapi.test.ts | 11 ++++---- src/openapi.ts | 9 ++++--- src/types.ts | 1 + 7 files changed, 94 insertions(+), 26 deletions(-) diff --git a/README.md b/README.md index 0849ec1..542d5c5 100644 --- a/README.md +++ b/README.md @@ -16,7 +16,7 @@ First, define an API schema file. Schemas look like this: # schema.yml Resources: Item: - type: 'object' + type: object properties: id: description: The item's unique identifier. @@ -24,19 +24,26 @@ Resources: label: description: The item's label. type: string + Endpoints: PUT /items/:id: Name: upsertItem + Description: | + Upserts the specified item. + + This description can be long and multiline. It can even include **markdown**! Request: type: object properties: label: { type: string } Response: $ref: '#/definitions/Item' + GET /items/:id: Name: getItemById Response: $ref: '#/definitions/Item' + GET /items: Name: listItems Request: @@ -56,6 +63,10 @@ Let's look at one endpoint from the schema above, and break down its parts: ```yaml PUT /items/:id: Name: upsertItem + Description: | + Upserts the specified item. + + This description can be long and multiline. It can even include **markdown**! Request: type: object properties: @@ -70,6 +81,9 @@ PUT /items/:id: entry. This value is used for generating nice clients for the application. It should be alphanumeric and camelCased. +- The `Description` entry is a long-form Markdown-compatible description of how the endpoint works. + This description will be generated into JSDoc in generated code. + - The `Request` entry is a JSONSchema definition that describes a valid request object. `Request` schemas are optional for `GET` and `DELETE` endpoints. diff --git a/src/generate-axios-client.test.ts b/src/generate-axios-client.test.ts index a2b93b6..22ed988 100644 --- a/src/generate-axios-client.test.ts +++ b/src/generate-axios-client.test.ts @@ -3,6 +3,15 @@ import { GenerateAxiosClientInput, } from './generate-axios-client'; import { format } from 'prettier'; +import { writeFileSync } from 'fs'; + +const LONG_DESCRIPTION = ` +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. + +It contains newlines. + +## It contains markdown. +`.trim(); describe('generate', () => { const generateAndFormat = (input: GenerateAxiosClientInput) => @@ -40,6 +49,7 @@ describe('generate', () => { }, 'PUT /posts/:id': { Name: 'putPost', + Description: LONG_DESCRIPTION, Request: { type: 'object', additionalProperties: false, @@ -109,11 +119,6 @@ class Client { }); } - /** - * 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. - */ async paginate(request, data, config) { const result = []; @@ -167,18 +172,43 @@ export type Endpoints = { export declare class Client { constructor(client: AxiosInstance); + /** + * Executes the \`GET /posts\` endpoint. + * + * @param data The request data. + * @param config The Axios request overrides for the request. + * + * @returns An AxiosResponse object representing the response. + */ getPosts( - params: Endpoints["GET /posts"]["Request"] & + data: Endpoints["GET /posts"]["Request"] & Endpoints["GET /posts"]["PathParams"], config?: AxiosRequestConfig ): Promise>; + /** + * 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. + * + * It contains newlines. + * + * ## It contains markdown. + * + * @param data The request data. + * @param config The Axios request overrides for the request. + * + * @returns An AxiosResponse object representing the response. + */ putPost( data: Endpoints["PUT /posts/:id"]["Request"] & Endpoints["PUT /posts/:id"]["PathParams"], config?: AxiosRequestConfig ): Promise>; + /** + * 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( request: ( data: T, @@ -200,6 +230,9 @@ export declare class Client { const result = await generateAndFormat(input); expect(result.javascript).toStrictEqual(expected.javascript); expect(result.declaration).toStrictEqual(expected.declaration); + + writeFileSync(`${__dirname}/test-generated.js`, result.javascript); + writeFileSync(`${__dirname}/test-generated.d.ts`, result.declaration); }); }); }); diff --git a/src/generate-axios-client.ts b/src/generate-axios-client.ts index 1fa3283..de57d96 100644 --- a/src/generate-axios-client.ts +++ b/src/generate-axios-client.ts @@ -12,6 +12,20 @@ export type GenerateAxiosClientOutput = { declaration: string; }; +const toJSDocLines = (docs: string): string => + docs + .split('\n') + .map((line) => ` * ${line}`) + .join('\n'); + +const PAGINATE_JSDOC = ` +/** + * 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. + */ +`.trim(); + export const generateAxiosClient = async ({ spec, outputClass, @@ -29,19 +43,27 @@ export declare class ${outputClass} { constructor(client: AxiosInstance); ${Object.entries(spec.Endpoints) - .map(([endpoint, { Name }]) => { - const [method] = endpoint.split(' '); - const paramsName = method === 'GET' ? 'params' : 'data'; - + .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}( - ${paramsName}: Endpoints['${endpoint}']['Request'] & + data: Endpoints['${endpoint}']['Request'] & Endpoints['${endpoint}']['PathParams'], config?: AxiosRequestConfig ): Promise>`; }) .join('\n\n')} + ${PAGINATE_JSDOC} paginate( request: ( data: T, @@ -109,11 +131,6 @@ class ${outputClass} { }) .join('\n\n')} - /** - * 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. - */ async paginate(request, data, config) { const result = []; diff --git a/src/meta-schema.ts b/src/meta-schema.ts index f9712f4..b5428ee 100644 --- a/src/meta-schema.ts +++ b/src/meta-schema.ts @@ -36,6 +36,7 @@ const ONE_SCHEMA_META_SCHEMA: JSONSchema4 = { required: ['Name', 'Response'], properties: { Name: { type: 'string', pattern: '[a-zA-Z0-9]+' }, + Description: { type: 'string' }, Request: { // JSONSchema type: 'object', diff --git a/src/openapi.test.ts b/src/openapi.test.ts index df99e22..44240b1 100644 --- a/src/openapi.test.ts +++ b/src/openapi.test.ts @@ -16,6 +16,7 @@ const TEST_SPEC: OneSchemaDefinition = withAssumptions({ Endpoints: { 'GET /posts': { Name: 'getPosts', + Description: 'This endpoint has a description', Request: { type: 'object', required: ['sort'], @@ -133,7 +134,7 @@ describe('toOpenAPISpec', () => { ], responses: { '200': { - description: 'TODO', + description: 'This endpoint has a description', content: { 'application/json': { schema: { @@ -170,7 +171,7 @@ describe('toOpenAPISpec', () => { }, responses: { '200': { - description: 'TODO', + description: 'None', content: { 'application/json': { schema: { @@ -204,7 +205,7 @@ describe('toOpenAPISpec', () => { }, }, }, - description: 'TODO', + description: 'None', }, }, }, @@ -222,7 +223,7 @@ describe('toOpenAPISpec', () => { ], responses: { '200': { - description: 'TODO', + description: 'None', content: { 'application/json': { schema: { @@ -263,7 +264,7 @@ describe('toOpenAPISpec', () => { }, responses: { '200': { - description: 'TODO', + description: 'None', content: { 'application/json': { schema: { diff --git a/src/openapi.ts b/src/openapi.ts index e32e940..b940027 100644 --- a/src/openapi.ts +++ b/src/openapi.ts @@ -46,16 +46,17 @@ export const toOpenAPISpec = ( // eslint-disable-next-line @typescript-eslint/no-non-null-assertion openAPIDocument.components!.schemas = Resources; - for (const [endpoint, { Name, Request, Response }] of Object.entries( - Endpoints, - )) { + for (const [ + endpoint, + { Name, Description, Request, Response }, + ] of Object.entries(Endpoints)) { const [method, path] = endpoint.split(' '); const operation: OpenAPIV3_1.OperationObject = { operationId: Name, responses: { '200': { - description: 'TODO', + description: Description || 'None', content: { 'application/json': { // @ts-expect-error TS detects a mismatch between the JSONSchema types diff --git a/src/types.ts b/src/types.ts index 5a83e99..9f1b105 100644 --- a/src/types.ts +++ b/src/types.ts @@ -2,6 +2,7 @@ import type { JSONSchema4 } from 'json-schema'; export type EndpointDefinition = { Name: string; + Description?: string; Request?: JSONSchema4; Response: JSONSchema4; };