1- import type { MapKeysResult , MappingResolver } from '../types'
2- import { getStringValue , objectKeys , onlyExistedArrayIndexes } from '../utils'
1+ import { MapKeysResult , MappingResolver , NodeContext } from '../types'
2+ import {
3+ difference ,
4+ extractOperationBasePath ,
5+ getStringValue ,
6+ intersection ,
7+ objectKeys ,
8+ onlyExistedArrayIndexes ,
9+ removeExcessiveSlashes ,
10+ } from '../utils'
311import { mapPathParams } from './openapi3.utils'
12+ import { OpenAPIV3 } from 'openapi-types'
413
514export const singleOperationPathMappingResolver : MappingResolver < string > = ( before , after ) => {
615
@@ -23,32 +32,61 @@ export const singleOperationPathMappingResolver: MappingResolver<string> = (befo
2332 return result
2433}
2534
26- export const pathMappingResolver : MappingResolver < string > = ( before , after ) => {
35+ /**
36+ * Maps OpenAPI path keys between two versions of the spec by considering possible base path changes
37+ * defined in the root object `servers` field and path item object `servers` field.
38+ * This mapping normalizes (unifies) paths by removing any basePath prefixes
39+ * so that equivalent endpoints are recognized and correctly mapped even if the base path (URL prefix)
40+ * has changed. It does *not* handle server base paths defined at the operation level.
41+ * It also maps paths even if path parameters have changed.
42+ *
43+ * @param before - The "before" object representing a set of OpenAPI paths (mapping string keys to PathItemObject)
44+ * @param after - The "after" object representing a set of OpenAPI paths (mapping string keys to PathItemObject)
45+ * @param ctx - The NodeContext, used here to access the root OpenAPI Document for both "before" and "after"
46+ * @returns {MapKeysResult<string> } An object containing arrays of `added` and `removed` path keys, and
47+ * a mapping between old and new keys for matched paths.
48+ *
49+ * @remarks
50+ * This method does not support mapping when the base path is defined in the operation-level `servers`.
51+ * See related test: "Should match operation when prefix moved from operation object servers to path".
52+ */
53+ export const pathMappingResolver : MappingResolver < string > = ( before , after , ctx ) => {
2754
2855 const result : MapKeysResult < string > = { added : [ ] , removed : [ ] , mapped : { } }
2956
30- const originalBeforeKeys = objectKeys ( before )
31- const originalAfterKeys = objectKeys ( after )
32- const unifiedAfterKeys = originalAfterKeys . map ( hidePathParamNames )
57+ // current approach for mapping does not allow to match operations between versions
58+ // if base path is specified in the servers array of the operation object, so this case is not supported
59+ // see test "Should match operation when prefix moved from operation object servers to path"
60+ const unifyBeforePath = createPathUnifier ( ( ctx . before . root as OpenAPIV3 . Document ) . servers )
61+ const unifyAfterPath = createPathUnifier ( ( ctx . after . root as OpenAPIV3 . Document ) . servers )
3362
34- const notMappedAfterIndices = new Set ( originalAfterKeys . keys ( ) )
63+ const unifiedBeforeKeyToKey = Object . fromEntries ( objectKeys ( before ) . map ( key => [ unifyBeforePath ( key , ( before [ key ] as OpenAPIV3 . PathItemObject ) ?. servers ) , key ] ) )
64+ const unifiedAfterKeyToKey = Object . fromEntries ( objectKeys ( after ) . map ( key => [ unifyAfterPath ( key , ( after [ key ] as OpenAPIV3 . PathItemObject ) ?. servers ) , key ] ) )
3565
36- originalBeforeKeys . forEach ( beforeKey => {
37- const unifiedBeforePath = hidePathParamNames ( beforeKey )
38- const index = unifiedAfterKeys . indexOf ( unifiedBeforePath )
66+ const unifiedBeforeKeys = Object . keys ( unifiedBeforeKeyToKey )
67+ const unifiedAfterKeys = Object . keys ( unifiedAfterKeyToKey )
3968
40- if ( index < 0 ) {
41- // removed item
42- result . removed . push ( beforeKey )
43- } else {
44- // mapped items
45- result . mapped [ beforeKey ] = originalAfterKeys [ index ]
46- notMappedAfterIndices . delete ( index )
47- }
48- } )
69+ result . added = difference ( unifiedAfterKeys , unifiedBeforeKeys ) . map ( key => unifiedAfterKeyToKey [ key ] )
70+ result . removed = difference ( unifiedBeforeKeys , unifiedAfterKeys ) . map ( key => unifiedBeforeKeyToKey [ key ] )
71+ result . mapped = Object . fromEntries (
72+ intersection ( unifiedBeforeKeys , unifiedAfterKeys ) . map ( key => [ unifiedBeforeKeyToKey [ key ] , unifiedAfterKeyToKey [ key ] ] ) ,
73+ )
74+
75+ return result
76+ }
4977
50- // added items
51- notMappedAfterIndices . forEach ( ( notMappedIndex ) => result . added . push ( originalAfterKeys [ notMappedIndex ] ) )
78+ export const methodMappingResolver : MappingResolver < string > = ( before , after ) => {
79+
80+ const result : MapKeysResult < string > = { added : [ ] , removed : [ ] , mapped : { } }
81+
82+ const beforeKeys = objectKeys ( before )
83+ const afterKeys = objectKeys ( after )
84+
85+ result . added = difference ( afterKeys , beforeKeys )
86+ result . removed = difference ( beforeKeys , afterKeys )
87+
88+ const mapped = intersection ( beforeKeys , afterKeys )
89+ mapped . forEach ( key => result . mapped [ key ] = key )
5290
5391 return result
5492}
@@ -100,10 +138,10 @@ export const contentMediaTypeMappingResolver: MappingResolver<string> = (before,
100138 function mapExactMatches (
101139 getComparisonKey : ( key : string ) => string
102140 ) : void {
103-
141+
104142 for ( const beforeIndex of unmappedBeforeIndices ) {
105143 const beforeKey = getComparisonKey ( beforeKeys [ beforeIndex ] )
106-
144+
107145 // Find matching after index by iterating over the after indices set
108146 let matchingAfterIndex : number | undefined
109147 for ( const afterIndex of unmappedAfterIndices ) {
@@ -175,6 +213,14 @@ function isWildcardCompatible(beforeType: string, afterType: string): boolean {
175213 return true
176214}
177215
216+ export function createPathUnifier ( rootServers ?: OpenAPIV3 . ServerObject [ ] ) : ( path : string , pathServers ?: OpenAPIV3 . ServerObject [ ] ) => string {
217+ return ( path , pathServers ) => {
218+ // Prioritize path-level servers over root-level servers
219+ const serverPrefix = extractOperationBasePath ( pathServers || rootServers )
220+ return removeExcessiveSlashes ( `${ serverPrefix } ${ hidePathParamNames ( path ) } ` )
221+ }
222+ }
223+
178224export function hidePathParamNames ( path : string ) : string {
179225 return path . replace ( PATH_PARAMETER_REGEXP , PATH_PARAM_UNIFIED_PLACEHOLDER )
180226}
0 commit comments