Skip to content

Commit 76a3f5b

Browse files
committed
feat: support path item matching when base path is moved between path and servers array in root object or path item object
1 parent d1f085a commit 76a3f5b

File tree

16 files changed

+284
-20
lines changed

16 files changed

+284
-20
lines changed

src/openapi/openapi3.classify.ts

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import { emptySecurity, includeSecurity } from './openapi3.utils'
1515
import type { ClassifyRule, CompareContext } from '../types'
1616
import { DiffType } from '../types'
1717
import { createPathUnifier } from './openapi3.mapping'
18+
import { OpenAPIV3 } from 'openapi-types'
1819

1920
export const paramClassifyRule: ClassifyRule = [
2021
({ after }) => {
@@ -140,13 +141,18 @@ export const operationSecurityItemClassifyRule: ClassifyRule = [
140141
export const pathChangeClassifyRule: ClassifyRule = [
141142
nonBreaking,
142143
breaking,
143-
({ before, after }) => {
144+
({ before, after, parentContext }) => {
144145
const beforePath = before.key as string
145146
const afterPath = after.key as string
146-
const unifiedBeforePath = createPathUnifier(before)(beforePath)
147-
const unifiedAfterPath = createPathUnifier(after)(afterPath)
147+
const beforeRootServers = (parentContext?.before.root as OpenAPIV3.Document)?.servers
148+
const beforePathItemServers = (before.value as OpenAPIV3.PathItemObject)?.servers
149+
150+
const afterRootServers = (parentContext?.after.root as OpenAPIV3.Document)?.servers
151+
const afterPathItemServers = (after.value as OpenAPIV3.PathItemObject)?.servers
148152

153+
const unifiedBeforePath = createPathUnifier(beforeRootServers)(beforePath, beforePathItemServers)
154+
const unifiedAfterPath = createPathUnifier(afterRootServers)(afterPath, afterPathItemServers)
149155
// If unified paths are the same, it means only parameter names changed
150156
return unifiedBeforePath === unifiedAfterPath ? annotation : breaking
151-
}
157+
},
152158
]

src/openapi/openapi3.mapping.ts

Lines changed: 16 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import {
55
intersection,
66
objectKeys,
77
onlyExistedArrayIndexes,
8-
removeSlashes,
8+
removeExcessiveSlashes,
99
} from '../utils'
1010
import { mapPathParams } from './openapi3.utils'
1111
import { OpenAPIV3 } from 'openapi-types'
@@ -35,11 +35,14 @@ export const pathMappingResolver: MappingResolver<string> = (before, after, ctx)
3535

3636
const result: MapKeysResult<string> = { added: [], removed: [], mapped: {} }
3737

38-
const unifyBeforePath = createPathUnifier(ctx.before)
39-
const unifyAfterPath = createPathUnifier(ctx.after)
38+
// current approach for mapping does not allow to match operations between versions
39+
// if base path is specified in the servers array of the operation object, so this case is not supported
40+
// see test "Should match operation when prefix moved from operation object servers to path"
41+
const unifyBeforePath = createPathUnifier((ctx.before.root as OpenAPIV3.Document).servers)
42+
const unifyAfterPath = createPathUnifier((ctx.after.root as OpenAPIV3.Document).servers)
4043

41-
const unifiedBeforeKeyToKey = Object.fromEntries(objectKeys(before).map(key => [unifyBeforePath(key), key]))
42-
const unifiedAfterKeyToKey = Object.fromEntries(objectKeys(after).map(key => [unifyAfterPath(key), key]))
44+
const unifiedBeforeKeyToKey = Object.fromEntries(objectKeys(before).map(key => [unifyBeforePath(key, (before[key] as OpenAPIV3.PathItemObject)?.servers), key]))
45+
const unifiedAfterKeyToKey = Object.fromEntries(objectKeys(after).map(key => [unifyAfterPath(key, (after[key] as OpenAPIV3.PathItemObject)?.servers), key]))
4346

4447
const unifiedBeforeKeys = Object.keys(unifiedBeforeKeyToKey)
4548
const unifiedAfterKeys = Object.keys(unifiedAfterKeyToKey)
@@ -116,10 +119,10 @@ export const contentMediaTypeMappingResolver: MappingResolver<string> = (before,
116119
function mapExactMatches(
117120
getComparisonKey: (key: string) => string
118121
): void {
119-
122+
120123
for (const beforeIndex of unmappedBeforeIndices) {
121124
const beforeKey = getComparisonKey(beforeKeys[beforeIndex])
122-
125+
123126
// Find matching after index by iterating over the after indices set
124127
let matchingAfterIndex: number | undefined
125128
for (const afterIndex of unmappedAfterIndices) {
@@ -211,9 +214,12 @@ export const extractOperationBasePath = (servers?: OpenAPIV3.ServerObject[]): st
211214
}
212215
}
213216

214-
export function createPathUnifier(nodeContext: NodeContext): (path: string) => string {
215-
const serverPrefix = extractOperationBasePath((nodeContext.root as OpenAPIV3.Document).servers) // /api/v2
216-
return (path) => removeSlashes(`${serverPrefix}${hidePathParamNames(path)}`)
217+
export function createPathUnifier(rootServers?: OpenAPIV3.ServerObject[]): (path: string, pathServers?: OpenAPIV3.ServerObject[]) => string {
218+
return (path, pathServers) => {
219+
// Prioritize path-level servers over root-level servers
220+
const serverPrefix = extractOperationBasePath(pathServers || rootServers)
221+
return removeExcessiveSlashes(`${serverPrefix}${hidePathParamNames(path)}`)
222+
}
217223
}
218224

219225
export function hidePathParamNames(path: string): string {

src/utils.ts

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -232,13 +232,16 @@ export function difference(array1: string[], array2: string[]): string[] {
232232
return [...new Set(array1.filter(x => !set2.has(x)))]
233233
}
234234

235-
export function removeSlashes(input: string): string {
236-
return input.replace(/\//g, '')
235+
export function removeExcessiveSlashes(input: string): string {
236+
return input
237+
.replace(/\/+/g, '/') // Replace multiple consecutive slashes with single slash
238+
.replace(/^\//, '') // Remove leading slash
239+
.replace(/\/$/, '') // Remove trailing slash
237240
}
238241

239242
/**
240243
* Traverses the merged document starting from given obj to the bottom and aggregates the diffs with rollup from the bottom up.
241-
* Each object in the tree will have aggregatedDiffProperty only if there are diffs in the object or in the children,
244+
* Each object in the tree will have aggregatedDiffProperty only if there are diffs in the object or in the children,
242245
* otherwise the aggregatedDiffProperty is not added.
243246
* Note, that adding/removing the object itself is not included in the aggregation for this object,
244247
* you need retrieve this diffs from parent object if you need them.
@@ -263,7 +266,7 @@ export function aggregateDiffsWithRollup(obj: any, diffProperty: any, aggregated
263266
return obj[aggregatedDiffProperty]
264267
}
265268

266-
visited.add(obj)
269+
visited.add(obj)
267270

268271
// Process all children and collect their diffs
269272
const childrenDiffs = new Array<Set<Diff>>()
@@ -297,7 +300,7 @@ export function aggregateDiffsWithRollup(obj: any, diffProperty: any, aggregated
297300
// could reuse a child diffs if there is only one
298301
[obj[aggregatedDiffProperty]] = childrenDiffs
299302
}else{
300-
// no diffs- no aggregated diffs get assigned
303+
// no diffs- no aggregated diffs get assigned
301304
}
302305

303306
return obj[aggregatedDiffProperty]

test/helper/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
export * from './openapiBuilder'
2+
export * from './utils'
23

34
export const TEST_DIFF_FLAG = Symbol('test-diff')
45
export const TEST_INLINE_REF_FLAG = Symbol('test-inline-ref')
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
openapi: 3.0.3
2+
info:
3+
title: test
4+
version: 0.1.0
5+
servers:
6+
- url: https://example1.com/api/v2
7+
paths:
8+
/changed1:
9+
get:
10+
responses:
11+
'200':
12+
description: a2
13+
servers:
14+
- url: https://example1.com/api/v1
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
openapi: 3.0.3
2+
info:
3+
title: test
4+
version: 0.1.0
5+
servers:
6+
- url: https://example1.com/api/v1
7+
paths:
8+
/changed1:
9+
get:
10+
responses:
11+
'200':
12+
description: a1
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
openapi: 3.0.0
2+
info:
3+
version: 1.0.0
4+
title: Test API
5+
servers:
6+
- url: https://example.com
7+
paths:
8+
/api/v1/users:
9+
get:
10+
summary: Get users
11+
responses:
12+
'200':
13+
description: OK
14+
15+
16+
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
openapi: 3.0.0
2+
info:
3+
version: 1.0.0
4+
title: Test API
5+
servers:
6+
- url: https://example.com
7+
paths:
8+
/users:
9+
get:
10+
summary: Get users
11+
servers:
12+
- url: https://example.com/api/v1
13+
responses:
14+
'200':
15+
description: OK
16+
17+
18+
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
openapi: 3.0.0
2+
info:
3+
version: 1.0.0
4+
title: Test API
5+
paths:
6+
/api/v1/users:
7+
get:
8+
summary: Get users
9+
responses:
10+
'200':
11+
description: OK
12+
13+
14+
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
openapi: 3.0.0
2+
info:
3+
version: 1.0.0
4+
title: Test API
5+
servers:
6+
- url: https://example.com/api/v2
7+
paths:
8+
/users:
9+
servers:
10+
- url: https://example.com/api/v1
11+
get:
12+
summary: Get users
13+
responses:
14+
'200':
15+
description: OK
16+
17+
18+

0 commit comments

Comments
 (0)