Skip to content

Commit

Permalink
feat: add help endpoint. (#311)
Browse files Browse the repository at this point in the history
  • Loading branch information
princerajpoot20 authored Sep 15, 2023
1 parent 8febc25 commit 81cd935
Show file tree
Hide file tree
Showing 4 changed files with 255 additions and 0 deletions.
62 changes: 62 additions & 0 deletions openapi.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -226,6 +226,49 @@ paths:
schema:
$ref: '#/components/schemas/Problem'

/help:
get:
summary: Retrieve help information for the given command.
operationId: help
tags:
- help
parameters:
- name: command
in: query
style: form
explode: true
description: The command for which help information is needed.
required: true
schema:
type: string
responses:
"200":
description: Help information retrieved successfully.
content:
application/json:
schema:
oneOf:
- $ref: '#/components/schemas/HelpListResponse'
- $ref: '#/components/schemas/HelpCommandResponse'
"400":
description: Bad request
content:
application/json:
schema:
$ref: '#/components/schemas/Problem'
"404":
description: Command not found
content:
application/problem+json:
schema:
$ref: '#/components/schemas/Problem'
default:
description: Unexpected problem.
content:
application/json:
schema:
$ref: "#/components/schemas/Problem"

/diff:
post:
summary: Compare the given AsyncAPI documents.
Expand Down Expand Up @@ -415,6 +458,25 @@ components:
type: [object, string]
description: The diff between the two AsyncAPI documents.

HelpListResponse:
type: object
properties:
commands:
type: array
items:
type: string
description: A list of all available commands.
HelpCommandResponse:
type: object
description: Detailed help information for a specific command.
properties:
command:
type: string
description: The name of the command.
description:
type: string
description: Detailed description of the command.

Problem:
type: object
properties:
Expand Down
92 changes: 92 additions & 0 deletions src/controllers/help.controller.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
import { Router, Request, Response, NextFunction } from 'express';
import { Controller } from '../interfaces';
import { ProblemException } from '../exceptions/problem.exception';
import { getAppOpenAPI } from '../utils/app-openapi';

const getCommandsFromRequest = (req: Request): string[] => {
return req.params.command ? req.params.command.split('/').filter(cmd => cmd.trim()) : [];
};

const isKeyValid = (key: string, obj: any): boolean => {
return Object.keys(obj).includes(key);
};

const getPathKeysMatchingCommands = (commands: string[], pathKeys: string[]): string | undefined => {
if (!Array.isArray(pathKeys) || !pathKeys.every(key => typeof key === 'string')) {
return undefined;
}
return pathKeys.find(pathKey => {
const pathParts = pathKey.split('/').filter(part => part !== '');
return pathParts.every((pathPart, i) => {
const command = commands[Number(i)];
return pathPart === command || pathPart.startsWith('{');
});
});
};

const getFullRequestBodySpec = (operationDetails: any) => {
return isKeyValid('requestBody', operationDetails) ? operationDetails.requestBody.content['application/json'].schema : null;
};

const buildResponseObject = (matchedPathKey: string, method: string, operationDetails: any, requestBodySchema: any) => {
return {
command: matchedPathKey,
method: method.toUpperCase(),
summary: operationDetails.summary || '',
requestBody: requestBodySchema
};
};

export class HelpController implements Controller {
public basepath = '/help';

public async boot(): Promise<Router> {
const router: Router = Router();

router.get('/help/:command*?', async (req: Request, res: Response, next: NextFunction) => {
const commands = getCommandsFromRequest(req);
let openapiSpec: any;

try {
openapiSpec = await getAppOpenAPI();
} catch (err) {
return next(err);
}

if (commands.length === 0) {
const routes = isKeyValid('paths', openapiSpec) ? Object.keys(openapiSpec.paths).map(path => ({ command: path.replace(/^\//, ''), url: `${this.basepath}${path}` })) : [];
return res.json(routes);
}

const pathKeys = isKeyValid('paths', openapiSpec) ? Object.keys(openapiSpec.paths) : [];
const matchedPathKey = getPathKeysMatchingCommands(commands, pathKeys);

if (!matchedPathKey) {
return next(new ProblemException({
type: 'invalid-asyncapi-command',
title: 'Invalid AsyncAPI Command',
status: 404,
detail: 'The given AsyncAPI command is not valid.'
}));
}

const pathInfo = isKeyValid(matchedPathKey, openapiSpec.paths) ? openapiSpec.paths[String(matchedPathKey)] : undefined;
const method = commands.length > 1 ? 'get' : 'post';
const operationDetails = isKeyValid(method, pathInfo) ? pathInfo[String(method)] : undefined;
if (!operationDetails) {
return next(new ProblemException({
type: 'invalid-asyncapi-command',
title: 'Invalid AsyncAPI Command',
status: 404,
detail: 'The given AsyncAPI command is not valid.'
}));
}

const requestBodySchema = getFullRequestBodySpec(operationDetails);

return res.json(buildResponseObject(matchedPathKey, method, operationDetails, requestBodySchema));
});

return router;
}
}
99 changes: 99 additions & 0 deletions src/controllers/tests/help.controller.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
import request from 'supertest';
import { App } from '../../app';
import { HelpController } from '../help.controller';
import { getAppOpenAPI } from '../../utils/app-openapi';

jest.mock('../../utils/app-openapi', () => ({
getAppOpenAPI: jest.fn(),
}));

describe('HelpController', () => {
let app;
beforeAll(async () => {
app = new App([new HelpController()]);
await app.init();
});

describe('[GET] /help', () => {
it('should return all commands', async () => {
(getAppOpenAPI as jest.Mock).mockResolvedValue({
paths: {
'/validate': {},
'/parse': {},
'/generate': {},
'/convert': {},
'/bundle': {},
'/help': {},
'/diff': {}
}
});

const response = await request(app.getServer())
.get('/v1/help')
.expect(200);

expect(response.body).toEqual([
{
command: 'validate',
url: '/help/validate'
},
{
command: 'parse',
url: '/help/parse'
},
{
command: 'generate',
url: '/help/generate'
},
{
command: 'convert',
url: '/help/convert'
},
{
command: 'bundle',
url: '/help/bundle'
},
{
command: 'help',
url: '/help/help'
},
{
command: 'diff',
url: '/help/diff'
}
]);
});

it('should return 404 error for an invalid command', async () => {
const response = await request(app.getServer())
.get('/v1/help/invalidCommand')
.expect(404);

expect(response.body).toEqual({
type: 'https://api.asyncapi.com/problem/invalid-asyncapi-command',
title: 'Invalid AsyncAPI Command',
status: 404,
detail: 'The given AsyncAPI command is not valid.'
});
});

it('should return 404 error for a command without a method', async () => {
(getAppOpenAPI as jest.Mock).mockResolvedValue({
paths: {
'/someCommand': {}
}
});

const response = await request(app.getServer())
.get('/v1/help/someCommand')
.expect(404);

expect(response.body).toEqual({
type: 'https://api.asyncapi.com/problem/invalid-asyncapi-command',
title: 'Invalid AsyncAPI Command',
status: 404,
detail: 'The given AsyncAPI command is not valid.'
});
});
});
});
2 changes: 2 additions & 0 deletions src/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import { ConvertController } from './controllers/convert.controller';
import { BundleController } from './controllers/bundle.controller';
import { DiffController } from './controllers/diff.controller';
import { DocsController } from './controllers/docs.controller';
import { HelpController } from './controllers/help.controller';

async function main() {
const app = new App([
Expand All @@ -22,6 +23,7 @@ async function main() {
new BundleController(),
new DiffController(),
new DocsController(),
new HelpController(),
]);
await app.init();
app.listen();
Expand Down

0 comments on commit 81cd935

Please sign in to comment.