Skip to content

Commit

Permalink
feat(core): append additional yaml content to swagger.json
Browse files Browse the repository at this point in the history
  • Loading branch information
IceHe committed Jul 6, 2022
1 parent 78407fc commit 8f240b4
Show file tree
Hide file tree
Showing 5 changed files with 254 additions and 288 deletions.
2 changes: 2 additions & 0 deletions packages/core/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@
"iconv-lite": "0.6.3",
"inquirer": "^8.2.2",
"jose": "^4.0.0",
"js-yaml": "^4.1.0",
"koa": "^2.13.1",
"koa-body": "^5.0.0",
"koa-compose": "^4.1.0",
Expand Down Expand Up @@ -81,6 +82,7 @@
"@types/etag": "^1.8.1",
"@types/inquirer": "^8.2.1",
"@types/jest": "^27.4.1",
"@types/js-yaml": "^4.0.5",
"@types/koa": "^2.13.3",
"@types/koa-compose": "^3.2.5",
"@types/koa-logger": "^3.1.1",
Expand Down
76 changes: 74 additions & 2 deletions packages/core/src/routes/swagger.test.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { load } from 'js-yaml';
import Koa from 'koa';
import Router from 'koa-router';
import request from 'supertest';
Expand All @@ -7,9 +8,22 @@ import koaGuard from '@/middleware/koa-guard';
import koaPagination from '@/middleware/koa-pagination';
import { AnonymousRouter } from '@/routes/types';

import swaggerRoutes, { paginationParameters } from './swagger';
import swaggerRoutes, { defaultResponses, paginationParameters } from './swagger';

const createSwaggerRequest = (
const mockSwagger = {
openapi: '3.0.1',
info: {
title: 'Logto Core',
version: '0.1.0',
},
paths: {},
};

jest.mock('js-yaml', () => ({
load: jest.fn(() => mockSwagger),
}));

export const createSwaggerRequest = (
allRouters: Array<Router<unknown, any>>,
swaggerRouter: AnonymousRouter = new Router()
) => {
Expand Down Expand Up @@ -233,4 +247,62 @@ describe('GET /swagger.json', () => {
})
);
});

describe('should build responses', () => {
it('should use "defaultResponses" if there is no custom "responses" from the additional swagger', async () => {
(load as jest.Mock).mockReturnValueOnce({
...mockSwagger,
paths: { '/api/mock': { delete: {} } },
});

const swaggerRequest = createSwaggerRequest([mockRouter]);
const response = await swaggerRequest.get('/swagger.json');
expect(response.body.paths).toMatchObject({
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
'/api/mock': expect.objectContaining({
delete: expect.objectContaining({ responses: defaultResponses }),
}),
/* eslint-enable @typescript-eslint/no-unsafe-assignment */
});
});

it('should use custom "responses" from the additional swagger if it exists', async () => {
(load as jest.Mock).mockReturnValueOnce({
...mockSwagger,
paths: {
'/api/mock': {
get: {
responses: {
'204': { description: 'No Content' },
},
},
patch: {
responses: {
'202': { description: 'Accepted' },
},
},
},
},
});

const swaggerRequest = createSwaggerRequest([mockRouter]);
const response = await swaggerRequest.get('/swagger.json');
expect(response.body.paths).toMatchObject({
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
'/api/mock': {
get: expect.objectContaining({
responses: {
'204': { description: 'No Content' },
},
}),
patch: expect.objectContaining({
responses: {
'202': { description: 'Accepted' },
},
}),
},
/* eslint-enable @typescript-eslint/no-unsafe-assignment */
});
});
});
});
52 changes: 34 additions & 18 deletions packages/core/src/routes/swagger.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,14 @@
import { readFileSync } from 'fs';

import { toTitle } from '@silverhand/essentials';
import deepmerge from 'deepmerge';
import { load } from 'js-yaml';
import Router, { IMiddleware } from 'koa-router';
import { OpenAPIV3 } from 'openapi-types';
import { ZodObject, ZodOptional } from 'zod';

import { isGuardMiddleware, WithGuardConfig } from '@/middleware/koa-guard';
import { isPaginationMiddleware, fallbackDefaultPageSize } from '@/middleware/koa-pagination';
import { fallbackDefaultPageSize, isPaginationMiddleware } from '@/middleware/koa-pagination';
import assertThat from '@/utils/assert-that';
import { zodTypeToSwagger } from '@/utils/zod';

Expand Down Expand Up @@ -62,15 +66,21 @@ const buildParameters = (
}));
};

function buildTag(path: string) {
const buildTag = (path: string) => {
const root = path.split('/')[1];

if (root?.startsWith('.')) {
return root;
}

return toTitle(root ?? 'General');
}
};

export const defaultResponses: OpenAPIV3.ResponsesObject = {
'200': {
description: 'OK',
},
};

const buildOperation = (stack: IMiddleware[], path: string): OpenAPIV3.OperationObject => {
const guard = stack.find((function_): function_ is WithGuardConfig<IMiddleware> =>
Expand Down Expand Up @@ -98,11 +108,7 @@ const buildOperation = (stack: IMiddleware[], path: string): OpenAPIV3.Operation
tags: [buildTag(path)],
parameters: [...pathParameters, ...queryParameters],
requestBody,
responses: {
'200': {
description: 'OK',
},
},
responses: defaultResponses,
};
};

Expand All @@ -124,21 +130,31 @@ export default function swaggerRoutes<T extends AnonymousRouter, R extends Route
)
);

const pathMap = new Map<string, MethodMap>();
const paths = new Map<string, MethodMap>();

// Group routes by path
for (const { path, method, operation } of routes) {
pathMap.set(path, { ...pathMap.get(path), [method]: operation });
paths.set(path, { ...paths.get(path), [method]: operation });
}

const document: OpenAPIV3.Document = {
openapi: '3.0.1',
info: {
title: 'Logto Core',
version: '0.1.0',
},
paths: Object.fromEntries(pathMap),
};
const additionalSwagger = load(
readFileSync('static/yaml/additional-swagger.yaml', 'utf-8')
) as OpenAPIV3.Document;

const document: OpenAPIV3.Document = deepmerge(
{ paths: Object.fromEntries(paths) },
additionalSwagger,
{
customMerge: (key) => {
// Overwrite OpenAPIV3.OperationObject.responses
// with the custom content from the additional swagger.
if (key === 'responses') {
// eslint-disable-next-line @typescript-eslint/no-unsafe-return
return (target, source) => source;
}
},
}
);

ctx.body = document;

Expand Down
64 changes: 64 additions & 0 deletions packages/core/static/yaml/additional-swagger.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
# SHOULD follow OpenAPI 3.0 Specification
# See https://swagger.io/docs/specification/basic-structure/
openapi: 3.0.1
info:
title: Logto Core
version: 0.1.0
paths:
/api/applications/:id:
delete:
responses:
'204':
description: No Content
/api/connectors/:id/test:
delete:
responses:
'204':
description: No Content
/api/me/password:
patch:
responses:
'204':
description: No Content
/api/resources/:id:
delete:
responses:
'204':
description: No Content
/api/session/sign-in/passwordless/sms/send-passcode:
post:
responses:
'204':
description: No Content
/api/session/sign-in/passwordless/email/send-passcode:
post:
responses:
'204':
description: No Content
/api/session/register/passwordless/sms/send-passcode:
post:
responses:
'204':
description: No Content
/api/session/register/passwordless/email/send-passcode:
post:
responses:
'204':
description: No Content
/api/status:
get:
responses:
'204':
description: No Content
/api/users/:userId:
delete:
responses:
'204':
description: No Content
/api/.well-known/sign-in-exp:
get:
responses:
'200':
description: OK
'304':
description: No Modified
Loading

0 comments on commit 8f240b4

Please sign in to comment.