Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add help endpoint. #311

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
78f3dfd
Initial Draft for Help Endpoint
princerajpoot20 Jun 11, 2023
70d474d
[Update]: Change in the structure, and split will work with space
princerajpoot20 Jun 12, 2023
3d0c0c0
[feat] : Added functionality of fetching data from github repo and ma…
princerajpoot20 Jun 14, 2023
58fe75c
Update src/controllers/help.controller.ts
princerajpoot20 Jun 18, 2023
d6879d5
fix : Improved error handling, added commands.md file and corrected t…
princerajpoot20 Jun 18, 2023
bb28c77
fix: changed the parameter passing from request body to url and file …
princerajpoot20 Jun 20, 2023
acc3cd1
added axios dependency and updated openapi.yml
princerajpoot20 Jul 8, 2023
993d7ce
change response to json formate
princerajpoot20 Jul 28, 2023
3849fb1
Update response and added relative link
princerajpoot20 Aug 5, 2023
867496d
Added unit test and update responses
princerajpoot20 Aug 8, 2023
8cbc884
Used optional chaining to remove code smell
princerajpoot20 Aug 11, 2023
b44ee3d
Refactored code for clarity
princerajpoot20 Aug 11, 2023
5a005f6
Minor changes
princerajpoot20 Aug 11, 2023
ed228cd
refactor: Apply Problem schema, update schema : HelpListResponse, Hel…
princerajpoot20 Aug 19, 2023
457bfc9
Update version of axios
princerajpoot20 Aug 19, 2023
17f05f8
Minor changes
princerajpoot20 Aug 19, 2023
e747130
Used openapi spec from project itself
princerajpoot20 Sep 4, 2023
29bdfca
Fix failing checks: lint fix
princerajpoot20 Sep 8, 2023
f381db7
Fix Warning: unsafe regular expression and Object Injection Sink
princerajpoot20 Sep 9, 2023
7af427c
Fix Warnings: Generic Object Injection Sink
princerajpoot20 Sep 9, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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":
BOLT04 marked this conversation as resolved.
Show resolved Hide resolved
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