11import IMountableItem from './IMountableItem' ;
22import describeRouteVariables from './describeRouteVariables' ;
33import 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
76interface IOpenApiOptions {
87 title : string ;
@@ -35,66 +34,116 @@ interface IParameterItemTypeOrRef {
3534interface IBuildParametersArguments {
3635 variableDefinitions : IOperationVariable [ ] ;
3736 variableLocation : string ;
37+ refLocation : string ;
3838}
3939
4040const PATH_VARIABLES_REGEX = / : ( [ A - Z a - 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