@@ -40,13 +40,13 @@ function traverse_and_enqueue(node: any, queue: string[], visited: Set<string>,
4040export default class Filter {
4141 protected input : Record < string , any >
4242 protected output : Record < string , any >
43- protected targetPaths : string [ ]
43+ protected targetPathsMap : Map < string , Set < string > | null > // path -> operation-groups (null means all operations)
4444 protected excludedSchemas : Set < string >
4545 paths : Record < string , Record < string , OpenAPIV3 . PathItemObject > > = { } // namespace -> path -> path_item_object
4646
47- constructor ( input : Record < string , any > , targetPaths : string [ ] , excludedSchemas : Set < string > = new Set ( ) ) {
47+ constructor ( input : Record < string , any > , targetPathsMap : Map < string , Set < string > | null > , excludedSchemas : Set < string > = new Set ( ) ) {
4848 this . input = input ;
49- this . targetPaths = targetPaths ;
49+ this . targetPathsMap = targetPathsMap ;
5050 this . excludedSchemas = excludedSchemas ;
5151 if ( this . excludedSchemas . size > 0 ) {
5252 logger . info ( `Loaded ${ this . excludedSchemas . size } excluded schemas: ${ Array . from ( this . excludedSchemas ) . join ( ', ' ) } ` ) ;
@@ -67,14 +67,32 @@ export default class Filter {
6767
6868 filter ( ) : OpenAPIV3 . Document {
6969 this . output . info = this . input . info ;
70- for ( const p of this . targetPaths ) {
71- if ( this . input . paths [ p ] === undefined ) {
72- logger . error ( `Path not found in spec: ${ p } ` ) ;
70+
71+ for ( const [ targetPath , targetGroups ] of this . targetPathsMap ) {
72+ if ( this . input . paths [ targetPath ] === undefined ) {
73+ logger . error ( `Path not found in spec: ${ targetPath } ` ) ;
7374 continue ;
7475 }
75- this . output . paths [ p ] = this . input . paths [ p ] ;
76+
77+ const pathItem = this . input . paths [ targetPath ] ;
78+
79+ if ( targetGroups === null ) {
80+ this . output . paths [ targetPath ] = pathItem ;
81+ } else {
82+ const filteredPathItem : any = { } ;
83+ for ( const method of [ 'get' , 'post' , 'put' , 'delete' , 'head' ] as const ) {
84+ const operation = pathItem ?. [ method ] ;
85+ if ( operation && operation [ 'x-operation-group' ] && targetGroups . has ( operation [ 'x-operation-group' ] ) ) {
86+ filteredPathItem [ method ] = operation ;
87+ }
88+ }
89+ if ( Object . keys ( filteredPathItem ) . length > 0 ) {
90+ this . output . paths [ targetPath ] = filteredPathItem ;
91+ }
92+ }
7693 }
77- this . filter_by_max_parameters ( this . output . paths as OpenAPIV3 . PathsObject ) ;
94+
95+ this . mergeOperationsByGroup ( this . output . paths as OpenAPIV3 . PathsObject ) ;
7896 const queue : string [ ] = [ ] ;
7997 const visited : Set < string > = new Set ( ) ;
8098
@@ -101,65 +119,98 @@ export default class Filter {
101119 return this . output as OpenAPIV3 . Document ;
102120 }
103121
104- filter_by_max_parameters ( paths : OpenAPIV3 . PathsObject ) : void {
122+ /**
123+ * Merges operations with the same x-operation-group into a single operation.
124+ * - Validates that requestBody and responses are identical across paths
125+ * - Merges all parameters from different paths into a union
126+ */
127+ mergeOperationsByGroup ( paths : OpenAPIV3 . PathsObject ) : void {
105128 const new_paths : OpenAPIV3 . PathsObject = { } ;
106- let operation_map = new Map < string , Array < Record < string , OpenAPIV3 . PathItemObject > > > ( ) ;
129+
130+ // Group operations by x-operation-group
131+ type OperationInfo = {
132+ path : string ;
133+ method : string ;
134+ operation : OpenAPIV3 . OperationObject ;
135+ } ;
136+ const operationsByGroup = new Map < string , OperationInfo [ ] > ( ) ;
137+
107138 for ( const path in paths ) {
108139 const path_item = paths [ path ] ;
109140 if ( ! path_item ) continue ;
110141 for ( const method of Object . keys ( path_item ) as Array < keyof OpenAPIV3 . PathItemObject > ) {
111142 const operation = path_item [ method ] ;
112143 if ( operation != null && typeof operation === 'object' && 'x-operation-group' in operation ) {
113144 const group : string = operation [ 'x-operation-group' ] as string ;
114- if ( ! operation_map . get ( group ) ) {
115- operation_map . set ( group , [ ] ) ;
145+ if ( ! operationsByGroup . has ( group ) ) {
146+ operationsByGroup . set ( group , [ ] ) ;
116147 }
117- const group_map : Record < string , OpenAPIV3 . PathItemObject > = {
118- [ path ] : {
119- [ method ] : operation as OpenAPIV3 . OperationObject ,
120- } ,
121- } ;
122- operation_map . get ( group ) ?. push ( group_map ) ;
148+ operationsByGroup . get ( group ) ! . push ( {
149+ path,
150+ method,
151+ operation : operation as OpenAPIV3 . OperationObject
152+ } ) ;
123153 }
124154 }
125155 }
126156
157+ for ( const [ group , operations ] of operationsByGroup . entries ( ) ) {
158+ if ( operations . length === 0 ) continue ;
127159
128- for ( const operations of operation_map . values ( ) ) {
129- let max_parameters = - 1 ;
130- let max_path_item : OpenAPIV3 . PathItemObject | null = null ;
131- let max_path = '' ;
132- for ( const op of operations ) {
133- for ( const [ path , path_item ] of Object . entries ( op ) ) {
134- for ( const operation of Object . values ( path_item ) ) {
135- if ( operation != null && typeof operation === 'object' && Array . isArray ( ( operation as OpenAPIV3 . OperationObject ) . parameters ) ) {
136- const param_count = ( operation as OpenAPIV3 . OperationObject ) . parameters ?. length ?? 0 ;
137- if ( param_count > max_parameters ) {
138- max_parameters = param_count ;
139- max_path = path ;
140- max_path_item = path_item ;
141- }
142- }
143- }
144- }
145- }
146- if ( ( max_path_item ?. get ?. operationId ) != null ) {
147- max_path_item . get . operationId = max_path_item . get . operationId . replace ( / \. \d + $ / , '' ) ;
148- new_paths [ max_path ] = { ...new_paths [ max_path ] , get : max_path_item . get } ;
149- } else if ( ( max_path_item ?. post ?. operationId != null ) ) {
150- max_path_item . post . operationId = max_path_item . post . operationId . replace ( / \. \d + $ / , '' ) ;
151- new_paths [ max_path ] = { ...new_paths [ max_path ] , post : max_path_item . post } ;
152- } else if ( ( max_path_item ?. put ?. operationId != null ) ) {
153- max_path_item . put . operationId = max_path_item . put . operationId . replace ( / \. \d + $ / , '' ) ;
154- new_paths [ max_path ] = { ...new_paths [ max_path ] , put : max_path_item . put } ;
155- } else if ( ( max_path_item ?. delete ?. operationId != null ) ) {
156- max_path_item . delete . operationId = max_path_item . delete . operationId . replace ( / \. \d + $ / , '' ) ;
157- new_paths [ max_path ] = { ...new_paths [ max_path ] , delete : max_path_item . delete } ;
158- } else if ( ( max_path_item ?. head ?. operationId != null ) ) {
159- max_path_item . head . operationId = max_path_item . head . operationId . replace ( / \. \d + $ / , '' ) ;
160- new_paths [ max_path ] = { ...new_paths [ max_path ] , head : max_path_item . head } ;
160+ // 1. Validate requestBody and responses are consistent before merging
161+ this . validateConsistency ( group , operations ) ;
162+
163+ // 2. Merge parameters from all operations
164+ const mergedParams = this . mergeParameters ( operations ) ;
165+
166+ // 3. Create merged operation
167+ const firstOp = operations [ 0 ] ;
168+ const mergedOperation : OpenAPIV3 . OperationObject = {
169+ ...firstOp . operation ,
170+ operationId : firstOp . operation . operationId ?. replace ( / \. \d + $ / , '' ) ,
171+ parameters : mergedParams
172+ } ;
173+
174+ if ( ! new_paths [ firstOp . path ] ) {
175+ new_paths [ firstOp . path ] = { } ;
161176 }
177+ ( new_paths [ firstOp . path ] as any ) [ firstOp . method ] = mergedOperation ;
162178 }
179+
163180 this . output . paths = new_paths ;
164181 }
182+
183+ private validateConsistency ( group : string , operations : { path : string ; operation : OpenAPIV3 . OperationObject } [ ] ) : void {
184+ // Check requestBody consistency
185+ const requestBodies = operations
186+ . map ( op => JSON . stringify ( op . operation . requestBody ) )
187+ . filter ( rb => rb !== 'undefined' ) ;
188+ const uniqueRequestBodies = new Set ( requestBodies ) ;
189+ if ( uniqueRequestBodies . size > 1 ) {
190+ throw new Error ( `Operation group '${ group } ' has inconsistent requestBody across paths: ${ operations . map ( op => op . path ) . join ( ', ' ) } ` ) ;
191+ }
192+
193+ // Check responses consistency
194+ const responses = operations . map ( op => JSON . stringify ( op . operation . responses ) ) ;
195+ const uniqueResponses = new Set ( responses ) ;
196+ if ( uniqueResponses . size > 1 ) {
197+ throw new Error ( `Operation group '${ group } ' has inconsistent responses across paths: ${ operations . map ( op => op . path ) . join ( ', ' ) } ` ) ;
198+ }
199+ }
200+
201+ private mergeParameters ( operations : { operation : OpenAPIV3 . OperationObject } [ ] ) : ( OpenAPIV3 . ReferenceObject | OpenAPIV3 . ParameterObject ) [ ] {
202+ const allParams = new Map < string , OpenAPIV3 . ReferenceObject | OpenAPIV3 . ParameterObject > ( ) ;
203+ for ( const op of operations ) {
204+ const params = op . operation . parameters ?? [ ] ;
205+ for ( const param of params ) {
206+ const key = '$ref' in param
207+ ? param . $ref
208+ : `${ ( param as OpenAPIV3 . ParameterObject ) . name } :${ ( param as OpenAPIV3 . ParameterObject ) . in } ` ;
209+ if ( ! allParams . has ( key ) ) {
210+ allParams . set ( key , param ) ;
211+ }
212+ }
213+ }
214+ return Array . from ( allParams . values ( ) ) ;
215+ }
165216}
0 commit comments