Skip to content

Commit 69e77ef

Browse files
Add in Open Api V3
1 parent ce95157 commit 69e77ef

File tree

4 files changed

+217
-96
lines changed

4 files changed

+217
-96
lines changed

IMountableItem.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,8 @@ export default interface IMountableItem {
1212
asMetal: () => void;
1313

1414
withOptions: (options: any) => this;
15-
setRouter: (router: Router) => this
15+
16+
onMount?: (router: Router) => this
1617
}
1718

1819
export interface IMountableItemClass {

OpenApi.ts

Lines changed: 206 additions & 79 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,7 @@
11
import IMountableItem from './IMountableItem';
22
import describeRouteVariables from './describeRouteVariables';
33
import Router from './Router';
4-
import Route, { IOperationVariable } from './Route';
5-
import express from 'express';
4+
import Route, { IOperationVariable } from './Route'; import express from 'express';
65

76
interface IOpenApiOptions {
87
title: string;
@@ -35,66 +34,116 @@ interface IParameterItemTypeOrRef {
3534
interface IBuildParametersArguments {
3635
variableDefinitions: IOperationVariable[];
3736
variableLocation: string;
37+
refLocation: string;
3838
}
3939

4040
const PATH_VARIABLES_REGEX = /:([A-Za-z]+)/g
4141

42-
function openApiPath(path: string): string {
43-
return path.replace(PATH_VARIABLES_REGEX, '{$1}');
42+
function translateScalarType(scalarType: string): string {
43+
switch (scalarType) {
44+
case 'Int':
45+
return 'number';
46+
case 'Boolean':
47+
return 'boolean';
48+
case 'String':
49+
default:
50+
return 'string';
51+
}
4452
}
4553

46-
function resolveRefOrType(variableType: string): IParameterItemTypeOrRef {
47-
switch (variableType) {
48-
case 'String':
49-
return { type: 'string' };
50-
case 'Boolean':
51-
return { type: 'boolean' };
52-
case 'Int':
53-
return { type: 'number' };
54+
function buildScalarDefinition(node: any): any {
55+
const scalarType = translateScalarType(node.name);
56+
57+
const scalarDoc: any = {
58+
type: scalarType,
59+
description: node.description,
60+
};
61+
62+
return scalarDoc;
63+
}
64+
65+
function buildObjectDefinition(node: any): any {
66+
const objectDoc: any = {};
67+
68+
objectDoc.type = 'object';
69+
objectDoc.properties = {};
70+
71+
node.inputFields.forEach((field: any) => {
72+
const { type: fieldNode } = field;
73+
74+
objectDoc.properties[field.name] = buildDefinition(fieldNode);
75+
});
76+
77+
return objectDoc;
78+
}
79+
80+
function buildDefinition(node: any): any {
81+
switch (node.kind) {
82+
case 'INPUT_OBJECT':
83+
return buildObjectDefinition(node);
84+
case 'ENUM':
85+
return buildEnumDefinition(node);
86+
case 'SCALAR':
5487
default:
55-
return { '$ref': `#/definitions/${variableType}` };
88+
return buildScalarDefinition(node);
5689
}
5790
}
5891

92+
function buildEnumDefinition(node: any): any {
93+
const enumDoc: any = {
94+
type: 'string',
95+
description: node.description,
96+
enum: [],
97+
};
98+
99+
node.enumValues.forEach((enumValue: any) => {
100+
enumDoc.enum.push(enumValue.name);
101+
});
102+
103+
return enumDoc;
104+
}
105+
106+
function openApiPath(path: string): string {
107+
return path.replace(PATH_VARIABLES_REGEX, '{$1}');
108+
}
109+
59110
// TODO: Return Type and Attempt to get description from graphql
60-
function buildParametersArray({ variableDefinitions, variableLocation }: IBuildParametersArguments): IParameter[] {
111+
function buildParametersArray({ variableDefinitions, variableLocation, refLocation }: IBuildParametersArguments): IParameter[] {
61112
return variableDefinitions.map(
62113
(variableDefinition: IOperationVariable): IParameter => {
63114
const parameter: IParameter = {
64115
name: variableDefinition.name,
65116
required: variableDefinition.required,
66-
default: variableDefinition.defaultValue,
117+
// default: variableDefinition.defaultValue,
67118
in: variableLocation,
68119
};
69120

70121
if (variableDefinition.array) {
71122
parameter.schema = {
72123
type: 'array',
73-
items: resolveRefOrType(variableDefinition.type),
124+
items: {
125+
'$ref': `${refLocation}/${variableDefinition.type}`
126+
},
74127
};
75128
} else {
76-
const refOrType = resolveRefOrType(variableDefinition.type);
77-
78-
if (refOrType.type) {
79-
parameter.type = refOrType.type;
80-
} else {
81-
parameter.schema = refOrType;
82-
}
129+
parameter.schema = {
130+
'$ref': `${refLocation}/${variableDefinition.type}`
131+
};
83132
}
84133

85134
return parameter;
86135
}
87136
);
88137
}
89138

90-
export class V2 implements IMountableItem {
139+
class MountableDocument implements IMountableItem {
91140
public path: string = '/docs/openapi/v2';
92141
public httpMethod: string = 'get';
93142

94-
constructor(private options: IOpenApiOptions, private router?: Router) {
143+
constructor(protected options: IOpenApiOptions, protected router?: Router) {
95144
}
96145

97-
setRouter(router: Router): this {
146+
onMount(router: Router): this {
98147
this.router = router;
99148

100149
return this;
@@ -106,7 +155,35 @@ export class V2 implements IMountableItem {
106155
return this;
107156
}
108157

109-
private async generateDocumentation(): Promise<any> {
158+
protected async generateDocumentation(): Promise<string> {
159+
return '';
160+
}
161+
162+
withOptions(options: {}): this {
163+
return this;
164+
}
165+
166+
asExpressRoute(): (req: express.Request, res: express.Response) => Promise<void> {
167+
const generateDoc = this.generateDocumentation();
168+
169+
return async (req: express.Request, res: express.Response) => {
170+
const doc = await generateDoc;
171+
172+
res
173+
.status(200)
174+
.json(doc);
175+
}
176+
}
177+
178+
asKoaRoute() {
179+
throw new Error('not yet implemented');
180+
}
181+
182+
asMetal() {
183+
throw new Error('not yet implemented');
184+
}
185+
186+
protected getRouter(): Router {
110187
const { router } = this;
111188

112189
if (!router) {
@@ -116,6 +193,14 @@ export class V2 implements IMountableItem {
116193
interface.
117194
`);
118195
}
196+
197+
return router;
198+
}
199+
}
200+
201+
export class V2 extends MountableDocument {
202+
protected async generateDocumentation(): Promise<any> {
203+
const router = this.getRouter();
119204

120205
try {
121206
const {
@@ -189,21 +274,24 @@ export class V2 implements IMountableItem {
189274
routeDoc.parameters.push(
190275
...buildParametersArray({
191276
variableDefinitions: route.queryVariables,
192-
variableLocation: 'query'
277+
variableLocation: 'query',
278+
refLocation: '#/definitions',
193279
})
194280
);
195281

196282
routeDoc.parameters.push(
197283
...buildParametersArray({
198284
variableDefinitions: route.pathVariables,
199-
variableLocation: 'path'
285+
variableLocation: 'path',
286+
refLocation: '#/definitions',
200287
})
201288
);
202289

203290
routeDoc.parameters.push(
204291
...buildParametersArray({
205292
variableDefinitions: route.bodyVariables,
206-
variableLocation: 'body'
293+
variableLocation: 'body',
294+
refLocation: '#/definitions',
207295
})
208296
);
209297
});
@@ -213,36 +301,7 @@ export class V2 implements IMountableItem {
213301
Object.keys(introspectedVariableDefinitions).forEach((variableName) => {
214302
const variableDefinition: any = introspectedVariableDefinitions[variableName];
215303

216-
const definitionDoc: any = page.definitions[variableName] = {};
217-
218-
if (variableDefinition.kind === 'INPUT_OBJECT') {
219-
definitionDoc.type = 'object';
220-
definitionDoc.properties = {};
221-
222-
variableDefinition.inputFields.forEach((field: any) => {
223-
const inputDoc: any = definitionDoc.properties[field.name] = {};
224-
225-
inputDoc.type = 'string';
226-
inputDoc.description = field.type.description;
227-
228-
if (field.type.kind === 'ENUM') {
229-
inputDoc.enum = [];
230-
231-
field.type.enumValues.forEach((enumValue: any) => {
232-
inputDoc.enum.push(enumValue.name);
233-
});
234-
}
235-
});
236-
}
237-
238-
if (variableDefinition.kind === 'ENUM') {
239-
definitionDoc.type = 'string';
240-
definitionDoc.enum = [];
241-
242-
variableDefinition.enumValues.forEach((enumValue: any) => {
243-
definitionDoc.enum.push(enumValue.name);
244-
});
245-
}
304+
page.definitions[variableName] = buildDefinition(variableDefinition);
246305
});
247306

248307
return page;
@@ -252,30 +311,98 @@ export class V2 implements IMountableItem {
252311
};
253312
}
254313
}
314+
}
255315

256-
withOptions(options: {}): this {
257-
return this;
258-
}
316+
export class V3 extends MountableDocument {
317+
protected async generateDocumentation(): Promise<any> {
318+
const { options } = this;
259319

260-
asExpressRoute(): (req: express.Request, res: express.Response) => void {
261-
const generateDoc = this.generateDocumentation();
320+
const doc: any = {};
262321

263-
return (req: express.Request, res: express.Response) => {
264-
generateDoc.then(
265-
(doc) => {
266-
res
267-
.status(200)
268-
.json(doc);
269-
}
270-
);
322+
doc.openapi = '3.0.0';
323+
doc.info = {};
324+
doc.info.title = options.title;
325+
doc.info.version = options.version;
326+
327+
doc.paths = {};
328+
doc.components = {};
329+
330+
if (options.license) {
331+
doc.license = {};
332+
doc.license.name = options.license;
271333
}
272-
}
273334

274-
asKoaRoute() {
275-
throw new Error('not yet implemented');
276-
}
335+
if (options.host) {
336+
doc.servers = [];
337+
338+
doc.servers.push({
339+
url: `${options.host}${options.basePath || ''}`,
340+
});
341+
}
342+
343+
const router = this.getRouter();
344+
345+
router.routes.forEach((route) => {
346+
const { path, httpMethod } = route;
347+
348+
const docPath = openApiPath(path);
349+
350+
if (!doc.paths[docPath]) {
351+
doc.paths[docPath] = {};
352+
}
353+
354+
const routeDoc: any = doc.paths[docPath][httpMethod] = {};
355+
356+
routeDoc.responses = {};
357+
358+
routeDoc.responses.default = {};
359+
routeDoc.responses.default.description = 'OK';
360+
361+
routeDoc.parameters = [];
362+
363+
routeDoc.parameters.push(
364+
...buildParametersArray({
365+
variableDefinitions: route.queryVariables,
366+
variableLocation: 'query',
367+
refLocation: '#/components/schemas',
368+
})
369+
);
370+
371+
routeDoc.parameters.push(
372+
...buildParametersArray({
373+
variableDefinitions: route.pathVariables,
374+
variableLocation: 'path',
375+
refLocation: '#/components/schemas',
376+
})
377+
);
378+
379+
routeDoc.parameters.push(
380+
...buildParametersArray({
381+
variableDefinitions: route.bodyVariables,
382+
variableLocation: 'body',
383+
refLocation: '#/components/schemas',
384+
})
385+
);
386+
});
387+
388+
doc.components = {};
389+
doc.components.schemas = {};
390+
391+
try {
392+
const introspectedVariableDefinitions: any = await describeRouteVariables(router);
393+
394+
Object.keys(introspectedVariableDefinitions).forEach((variableName) => {
395+
const variableDefinition: any = introspectedVariableDefinitions[variableName];
396+
397+
doc.components.schemas[variableName] = buildDefinition(variableDefinition);
398+
});
399+
400+
return doc;
401+
} catch (error) {
402+
return {
403+
error: error.message,
404+
};
405+
}
277406

278-
asMetal() {
279-
throw new Error('not yet implemented');
280407
}
281408
}

0 commit comments

Comments
 (0)