Skip to content

Commit

Permalink
Merge pull request #46 from lifeomic/description-field
Browse files Browse the repository at this point in the history
feat: support a Description field on endpoints
  • Loading branch information
swain authored Aug 2, 2022
2 parents ced0a50 + 3215bac commit f122b9e
Show file tree
Hide file tree
Showing 7 changed files with 94 additions and 26 deletions.
16 changes: 15 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,27 +16,34 @@ 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.
type: string
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:
Expand All @@ -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:
Expand All @@ -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.

Expand Down
45 changes: 39 additions & 6 deletions src/generate-axios-client.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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) =>
Expand Down Expand Up @@ -40,6 +49,7 @@ describe('generate', () => {
},
'PUT /posts/:id': {
Name: 'putPost',
Description: LONG_DESCRIPTION,
Request: {
type: 'object',
additionalProperties: false,
Expand Down Expand Up @@ -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 = [];
Expand Down Expand Up @@ -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<AxiosResponse<Endpoints["GET /posts"]["Response"]>>;
/**
* 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<AxiosResponse<Endpoints["PUT /posts/:id"]["Response"]>>;
/**
* 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<T extends { nextPageToken?: string; pageSize?: string }, Item>(
request: (
data: T,
Expand All @@ -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);
});
});
});
37 changes: 27 additions & 10 deletions src/generate-axios-client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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<AxiosResponse<Endpoints['${endpoint}']['Response']>>`;
})
.join('\n\n')}
${PAGINATE_JSDOC}
paginate<T extends { nextPageToken?: string; pageSize?: string }, Item>(
request: (
data: T,
Expand Down Expand Up @@ -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 = [];
Expand Down
1 change: 1 addition & 0 deletions src/meta-schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
11 changes: 6 additions & 5 deletions src/openapi.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'],
Expand Down Expand Up @@ -133,7 +134,7 @@ describe('toOpenAPISpec', () => {
],
responses: {
'200': {
description: 'TODO',
description: 'This endpoint has a description',
content: {
'application/json': {
schema: {
Expand Down Expand Up @@ -170,7 +171,7 @@ describe('toOpenAPISpec', () => {
},
responses: {
'200': {
description: 'TODO',
description: 'None',
content: {
'application/json': {
schema: {
Expand Down Expand Up @@ -204,7 +205,7 @@ describe('toOpenAPISpec', () => {
},
},
},
description: 'TODO',
description: 'None',
},
},
},
Expand All @@ -222,7 +223,7 @@ describe('toOpenAPISpec', () => {
],
responses: {
'200': {
description: 'TODO',
description: 'None',
content: {
'application/json': {
schema: {
Expand Down Expand Up @@ -263,7 +264,7 @@ describe('toOpenAPISpec', () => {
},
responses: {
'200': {
description: 'TODO',
description: 'None',
content: {
'application/json': {
schema: {
Expand Down
9 changes: 5 additions & 4 deletions src/openapi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import type { JSONSchema4 } from 'json-schema';

export type EndpointDefinition = {
Name: string;
Description?: string;
Request?: JSONSchema4;
Response: JSONSchema4;
};
Expand Down

0 comments on commit f122b9e

Please sign in to comment.