@@ -8,45 +8,139 @@ import * as TJS from 'typescript-json-schema';
88import type { PartialConfig } from './getConfig' ;
99import { getConfig } from './getConfig' ;
1010
11- export default ( configs ?: PartialConfig ) =>
12- getConfig ( configs ) . forEach ( ( config ) => {
13- const tree = getDirentTree ( config . input ) ;
14-
15- const createFilePaths = ( tree : DirentTree ) : string [ ] => {
16- return tree . children . flatMap ( ( child ) =>
17- child . isDir ? createFilePaths ( child . tree ) : tree . path ,
18- ) ;
19- } ;
11+ export const toOpenAPI = ( params : {
12+ input : string ;
13+ template ?: OpenAPIV3_1 . Document | string ;
14+ } ) : string => {
15+ const tree = getDirentTree ( params . input ) ;
16+
17+ const createFilePaths = ( tree : DirentTree ) : string [ ] => {
18+ return tree . children . flatMap ( ( child ) =>
19+ child . isDir ? createFilePaths ( child . tree ) : tree . path ,
20+ ) ;
21+ } ;
2022
21- const paths = createFilePaths ( tree ) ;
23+ const paths = createFilePaths ( tree ) ;
2224
23- const typeFile = `${ paths
24- . map (
25- ( p , i ) => `import type { Methods as Methods${ i } } from '${ p . replace ( config . input , '.' ) } '` ,
26- )
27- . join ( '\n' ) }
25+ const typeFile = `${ paths
26+ . map ( ( p , i ) => `import type { Methods as Methods${ i } } from '${ p . replace ( params . input , '.' ) } '` )
27+ . join ( '\n' ) }
2828
2929type AllMethods = [${ paths . map ( ( _ , i ) => `Methods${ i } ` ) . join ( ', ' ) } ]` ;
3030
31- const typeFilePath = join ( config . input , '@tmp-type.ts' ) ;
31+ const typeFilePath = join ( params . input , `@openapi-${ Date . now ( ) } .ts` ) ;
32+
33+ writeFileSync ( typeFilePath , typeFile , 'utf8' ) ;
34+
35+ const compilerOptions : TJS . CompilerOptions = {
36+ strictNullChecks : true ,
37+ rootDir : process . cwd ( ) ,
38+ baseUrl : process . cwd ( ) ,
39+ // @ts -expect-error dont match ScriptTarget
40+ target : 'ES2022' ,
41+ } ;
42+
43+ const program = TJS . getProgramFromFiles ( [ typeFilePath ] , compilerOptions ) ;
44+ const schema = TJS . generateSchema ( program , 'AllMethods' , { required : true } ) ;
45+ const doc : OpenAPIV3_1 . Document = {
46+ ...( typeof params . template === 'string'
47+ ? JSON . parse ( readFileSync ( params . template , 'utf8' ) )
48+ : params . template ) ,
49+ paths : { } ,
50+ components : { schemas : schema ?. definitions as any } ,
51+ } ;
52+
53+ unlinkSync ( typeFilePath ) ;
54+
55+ ( schema ?. items as TJS . Definition [ ] ) ?. forEach ( ( def , i ) => {
56+ const parameters : { name : string ; in : 'path' | 'query' ; required : boolean ; schema : any } [ ] = [ ] ;
57+
58+ let path = paths [ i ] ;
59+
60+ if ( path . includes ( '/_' ) ) {
61+ parameters . push (
62+ ...path
63+ . split ( '/' )
64+ . filter ( ( p ) => p . startsWith ( '_' ) )
65+ . map ( ( p ) => ( {
66+ name : p . slice ( 1 ) . split ( '@' ) [ 0 ] ,
67+ in : 'path' as const ,
68+ required : true ,
69+ schema : [ 'number' , 'string' ] . includes ( p . slice ( 1 ) . split ( '@' ) [ 1 ] )
70+ ? { type : p . slice ( 1 ) . split ( '@' ) [ 1 ] }
71+ : { anyOf : [ { type : 'number' } , { type : 'string' } ] } ,
72+ } ) ) ,
73+ ) ;
3274
33- writeFileSync ( typeFilePath , typeFile , 'utf8' ) ;
75+ path = path . replace ( / \/ _ ( [ ^ / @ ] + ) ( @ [ ^ / ] + ) ? / g, '/{$1}' ) ;
76+ }
77+
78+ path = path . replace ( params . input , '' ) || '/' ;
79+
80+ doc . paths ! [ path ] = Object . entries ( def . properties ! ) . reduce ( ( dict , [ method , val ] ) => {
81+ const params = [ ...parameters ] ;
82+ // const required = ((val as TJS.Definition).required ?? []) as (keyof AspidaMethodParams)[];
83+ const props = ( val as TJS . Definition ) . properties as {
84+ [ Key in keyof AspidaMethodParams ] : TJS . Definition ;
85+ } ;
86+
87+ if ( props . query ) {
88+ const def = ( props . query . properties ??
89+ schema ?. definitions ?. [ props . query . $ref ! . split ( '/' ) . at ( - 1 ) ! ] ) as TJS . Definition ;
90+
91+ params . push (
92+ ...Object . entries ( def ) . map ( ( [ name , value ] ) => ( {
93+ name,
94+ in : 'query' as const ,
95+ required : props . query ?. required ?. includes ( name ) ?? false ,
96+ schema : value ,
97+ } ) ) ,
98+ ) ;
99+ }
34100
35- const compilerOptions : TJS . CompilerOptions = {
36- strictNullChecks : true ,
37- rootDir : process . cwd ( ) ,
38- baseUrl : process . cwd ( ) ,
39- // @ts -expect-error dont match ScriptTarget
40- target : 'ES2022' ,
41- } ;
101+ const reqFormat = props . reqFormat ?. $ref ;
102+ const reqContentType =
103+ ( ( props . reqHeaders ?. properties ?. [ 'content-type' ] as TJS . Definition ) ?. const ??
104+ reqFormat ?. includes ( 'FormData' ) )
105+ ? 'multipart/form-data'
106+ : reqFormat ?. includes ( 'URLSearchParams' )
107+ ? 'application/x-www-form-urlencoded'
108+ : 'application/json' ;
109+ const resContentType =
110+ ( ( props . resHeaders ?. properties ?. [ 'content-type' ] as TJS . Definition ) ?. const as string ) ??
111+ 'application/json' ;
112+
113+ return {
114+ ...dict ,
115+ [ method ] : {
116+ tags : path === '/' ? undefined : path . split ( '/{' ) [ 0 ] . replace ( / ^ \/ / , '' ) . split ( '/' ) ,
117+ parameters : params ,
118+ requestBody :
119+ props . reqBody === undefined
120+ ? undefined
121+ : { content : { [ reqContentType ] : { schema : props . reqBody } } } ,
122+ responses :
123+ props . resBody === undefined
124+ ? undefined
125+ : {
126+ [ ( props . status ?. const as string ) ?? '2XX' ] : {
127+ content : { [ resContentType ] : { schema : props . resBody } } ,
128+ } ,
129+ } ,
130+ } ,
131+ } ;
132+ } , { } ) ;
133+ } ) ;
134+
135+ return JSON . stringify ( doc , null , 2 ) . replaceAll ( '#/definitions' , '#/components/schemas' ) ;
136+ } ;
42137
43- const program = TJS . getProgramFromFiles ( [ typeFilePath ] , compilerOptions ) ;
44- const schema = TJS . generateSchema ( program , 'AllMethods' , { required : true } ) ;
138+ export default ( configs ?: PartialConfig ) =>
139+ getConfig ( configs ) . forEach ( ( config ) => {
45140 const existingDoc : OpenAPIV3_1 . Document | undefined = existsSync ( config . output )
46141 ? JSON . parse ( readFileSync ( config . output , 'utf8' ) )
47142 : undefined ;
48-
49- const doc : OpenAPIV3_1 . Document = {
143+ const template : OpenAPIV3_1 . Document = {
50144 openapi : '3.1.0' ,
51145 info : {
52146 title : `${ config . output . split ( '/' ) . at ( - 1 ) ?. replace ( '.json' , '' ) } api` ,
@@ -55,95 +149,8 @@ type AllMethods = [${paths.map((_, i) => `Methods${i}`).join(', ')}]`;
55149 servers : config . baseURL ? [ { url : config . baseURL } ] : undefined ,
56150 ...existingDoc ,
57151 paths : { } ,
58- components : { schemas : schema ?. definitions as any } ,
152+ components : { } ,
59153 } ;
60154
61- unlinkSync ( typeFilePath ) ;
62-
63- ( schema ?. items as TJS . Definition [ ] ) ?. forEach ( ( def , i ) => {
64- const parameters : { name : string ; in : 'path' | 'query' ; required : boolean ; schema : any } [ ] =
65- [ ] ;
66-
67- let path = paths [ i ] ;
68-
69- if ( path . includes ( '/_' ) ) {
70- parameters . push (
71- ...path
72- . split ( '/' )
73- . filter ( ( p ) => p . startsWith ( '_' ) )
74- . map ( ( p ) => ( {
75- name : p . slice ( 1 ) . split ( '@' ) [ 0 ] ,
76- in : 'path' as const ,
77- required : true ,
78- schema : [ 'number' , 'string' ] . includes ( p . slice ( 1 ) . split ( '@' ) [ 1 ] )
79- ? { type : p . slice ( 1 ) . split ( '@' ) [ 1 ] }
80- : { anyOf : [ { type : 'number' } , { type : 'string' } ] } ,
81- } ) ) ,
82- ) ;
83-
84- path = path . replace ( / \/ _ ( [ ^ / @ ] + ) ( @ [ ^ / ] + ) ? / g, '/{$1}' ) ;
85- }
86-
87- path = path . replace ( config . input , '' ) || '/' ;
88-
89- doc . paths ! [ path ] = Object . entries ( def . properties ! ) . reduce ( ( dict , [ method , val ] ) => {
90- const params = [ ...parameters ] ;
91- // const required = ((val as TJS.Definition).required ?? []) as (keyof AspidaMethodParams)[];
92- const props = ( val as TJS . Definition ) . properties as {
93- [ Key in keyof AspidaMethodParams ] : TJS . Definition ;
94- } ;
95-
96- if ( props . query ) {
97- const def = ( props . query . properties ??
98- schema ?. definitions ?. [ props . query . $ref ! . split ( '/' ) . at ( - 1 ) ! ] ) as TJS . Definition ;
99-
100- params . push (
101- ...Object . entries ( def ) . map ( ( [ name , value ] ) => ( {
102- name,
103- in : 'query' as const ,
104- required : props . query ?. required ?. includes ( name ) ?? false ,
105- schema : value ,
106- } ) ) ,
107- ) ;
108- }
109-
110- const reqFormat = props . reqFormat ?. $ref ;
111- const reqContentType =
112- ( ( props . reqHeaders ?. properties ?. [ 'content-type' ] as TJS . Definition ) ?. const ??
113- reqFormat ?. includes ( 'FormData' ) )
114- ? 'multipart/form-data'
115- : reqFormat ?. includes ( 'URLSearchParams' )
116- ? 'application/x-www-form-urlencoded'
117- : 'application/json' ;
118- const resContentType =
119- ( ( props . resHeaders ?. properties ?. [ 'content-type' ] as TJS . Definition ) ?. const as string ) ??
120- 'application/json' ;
121-
122- return {
123- ...dict ,
124- [ method ] : {
125- tags : path === '/' ? undefined : path . split ( '/{' ) [ 0 ] . replace ( / ^ \/ / , '' ) . split ( '/' ) ,
126- parameters : params ,
127- requestBody :
128- props . reqBody === undefined
129- ? undefined
130- : { content : { [ reqContentType ] : { schema : props . reqBody } } } ,
131- responses :
132- props . resBody === undefined
133- ? undefined
134- : {
135- [ ( props . status ?. const as string ) ?? '2XX' ] : {
136- content : { [ resContentType ] : { schema : props . resBody } } ,
137- } ,
138- } ,
139- } ,
140- } ;
141- } , { } ) ;
142- } ) ;
143-
144- writeFileSync (
145- config . output ,
146- JSON . stringify ( doc , null , 2 ) . replaceAll ( '#/definitions' , '#/components/schemas' ) ,
147- 'utf8' ,
148- ) ;
155+ writeFileSync ( config . output , toOpenAPI ( { input : config . input , template } ) , 'utf8' ) ;
149156 } ) ;
0 commit comments