Skip to content

Commit 1291a05

Browse files
committed
fix: correctly map several media types which are the same in before/after but has wildcards
1 parent 9671994 commit 1291a05

File tree

6 files changed

+504
-32
lines changed

6 files changed

+504
-32
lines changed

src/openapi/openapi3.mapping.ts

Lines changed: 52 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -92,38 +92,70 @@ export const contentMediaTypeMappingResolver: MappingResolver<string> = (before,
9292
const result: MapKeysResult<string> = { added: [], removed: [], mapped: {} }
9393

9494
const beforeKeys = objectKeys(before)
95-
const _beforeKeys = beforeKeys.map((key) => key.split(';')[0] ?? '')
9695
const afterKeys = objectKeys(after)
97-
const _afterKeys = afterKeys.map((key) => key.split(';')[0] ?? '')
96+
97+
// Extract base media types (without parameters like charset, etc.)
98+
const beforeBaseTypes = beforeKeys.map((key) => key.split(';')[0] ?? '')
99+
const afterBaseTypes = afterKeys.map((key) => key.split(';')[0] ?? '')
98100

99-
const mappedIndex = new Set(afterKeys.keys())
101+
const unmappedAfterIndices = new Set(afterKeys.keys())
102+
const unmappedBeforeIndices = new Set(beforeKeys.keys())
100103

104+
// Map exact matches first
101105
for (let i = 0; i < beforeKeys.length; i++) {
102-
const _afterIndex = _afterKeys.findIndex((key) => {
103-
const [afterType, afterSubType] = key.split('/')
104-
const [beforeType, beforeSubType] = _beforeKeys[i].split('/')
105-
106-
if (afterType !== beforeType && afterType !== '*' && beforeType !== '*') { return false }
107-
if (afterSubType !== beforeSubType && afterSubType !== '*' && beforeSubType !== '*') { return false }
108-
return true
109-
})
106+
const beforeBaseType = beforeBaseTypes[i]
107+
const afterIndex = afterBaseTypes.findIndex((afterBaseType, index) =>
108+
afterBaseType === beforeBaseType && unmappedAfterIndices.has(index)
109+
)
110+
111+
if (afterIndex >= 0) {
112+
// Exact match found - map it
113+
result.mapped[beforeKeys[i]] = afterKeys[afterIndex]
114+
unmappedAfterIndices.delete(afterIndex)
115+
unmappedBeforeIndices.delete(i)
116+
}
117+
}
110118

111-
if (_afterIndex < 0 || !mappedIndex.has(_afterIndex)) {
112-
// removed item
113-
result.removed.push(beforeKeys[i])
114-
} else {
115-
// mapped items
116-
result.mapped[beforeKeys[i]] = afterKeys[_afterIndex]
117-
mappedIndex.delete(_afterIndex)
119+
// If exactly one unmapped item in both before and after, try wildcard matching
120+
if (unmappedBeforeIndices.size === 1 && unmappedAfterIndices.size === 1) {
121+
const beforeIndex = Array.from(unmappedBeforeIndices)[0]
122+
const afterIndex = Array.from(unmappedAfterIndices)[0]
123+
const beforeBaseType = beforeBaseTypes[beforeIndex]
124+
const afterBaseType = afterBaseTypes[afterIndex]
125+
126+
// Check if they are compatible using wildcard matching
127+
if (isWildcardCompatible(beforeBaseType, afterBaseType)) {
128+
// Map them together
129+
result.mapped[beforeKeys[beforeIndex]] = afterKeys[afterIndex]
130+
unmappedAfterIndices.delete(afterIndex)
131+
unmappedBeforeIndices.delete(beforeIndex)
118132
}
119133
}
120134

121-
// added items
122-
mappedIndex.forEach((i) => result.added.push(afterKeys[i]))
135+
// Mark remaining unmapped items as removed/added
136+
unmappedBeforeIndices.forEach((index) => result.removed.push(beforeKeys[index]))
137+
unmappedAfterIndices.forEach((index) => result.added.push(afterKeys[index]))
123138

124139
return result
125140
}
126141

142+
function isWildcardCompatible(beforeType: string, afterType: string): boolean {
143+
const [beforeMainType, beforeSubType] = beforeType.split('/')
144+
const [afterMainType, afterSubType] = afterType.split('/')
145+
146+
// Check main type compatibility
147+
if (beforeMainType !== afterMainType && beforeMainType !== '*' && afterMainType !== '*') {
148+
return false
149+
}
150+
151+
// Check sub type compatibility
152+
if (beforeSubType !== afterSubType && beforeSubType !== '*' && afterSubType !== '*') {
153+
return false
154+
}
155+
156+
return true
157+
}
158+
127159
export function hidePathParamNames(path: string): string {
128160
return path.replace(PATH_PARAMETER_REGEXP, PATH_PARAM_UNIFIED_PLACEHOLDER)
129161
}

test/bugs.test.ts

Lines changed: 21 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,9 @@ import changeToNothingClassificationAfter from './helper/resources/change-to-not
2727
import spearedParamsBefore from './helper/resources/speared-parameters/before.json'
2828
import spearedParamsAfter from './helper/resources/speared-parameters/after.json'
2929

30+
import wildcardContentSchemaMediaTypeCombinedWithSpecificMediaTypeBefore from './helper/resources/wildcard-content-schema-media-type-combined-with-specific-media-type/before.json'
31+
import wildcardContentSchemaMediaTypeCombinedWithSpecificMediaTypeAfter from './helper/resources/wildcard-content-schema-media-type-combined-with-specific-media-type/after.json'
32+
3033
import { diffsMatcher } from './helper/matchers'
3134
import { TEST_DIFF_FLAG, TEST_ORIGINS_FLAG } from './helper'
3235
import { JSON_SCHEMA_NODE_SYNTHETIC_TYPE_NOTHING } from '@netcracker/qubership-apihub-api-unifier'
@@ -97,16 +100,7 @@ describe('Real Data', () => {
97100
const after: any = infinityAfter
98101
const { diffs } = apiDiff(before, after, OPTIONS)
99102
const responseContentPath = ['paths', '/api/v1/dictionaries/dictionary/item', 'get', 'responses', '200', 'content']
100-
expect(diffs).toEqual(diffsMatcher([
101-
expect.objectContaining({
102-
beforeDeclarationPaths: [[...responseContentPath, '*/*']],
103-
afterDeclarationPaths: [[...responseContentPath, 'application/json']],
104-
action: DiffAction.rename,
105-
beforeKey: '*/*',
106-
afterKey: 'application/json',
107-
type: nonBreaking,
108-
scope: 'response',
109-
}),
103+
expect(diffs).toEqual(diffsMatcher([
110104
expect.objectContaining({
111105
afterDeclarationPaths: [['components', 'schemas', 'DictionaryItem', 'x-entity']],
112106
afterValue: 'DictionaryItem',
@@ -210,10 +204,26 @@ describe('Real Data', () => {
210204
}),
211205
]))
212206
})
213-
it('spered parameters', () => {
207+
208+
it('speared parameters', () => {
214209
const before: any = spearedParamsBefore
215210
const after: any = spearedParamsAfter
216211
const { diffs } = apiDiff(before, after, OPTIONS)
217212
expect(diffs).toBeEmpty()
218213
})
214+
215+
it('wildcard content schema media type in combination with specific media type', () => {
216+
const before: any = wildcardContentSchemaMediaTypeCombinedWithSpecificMediaTypeBefore
217+
const after: any = wildcardContentSchemaMediaTypeCombinedWithSpecificMediaTypeAfter
218+
const { diffs } = apiDiff(before, after, OPTIONS)
219+
220+
expect(diffs).toEqual(diffsMatcher([
221+
expect.objectContaining({
222+
action: DiffAction.replace,
223+
beforeDeclarationPaths: [['servers', 0, 'url']],
224+
afterDeclarationPaths: [['servers', 0, 'url']],
225+
type: annotation,
226+
}),
227+
]))
228+
})
219229
})

test/helper/resources/ref-with-array-to-self/after.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
"200": {
88
"description": "OK",
99
"content": {
10-
"application/json": {
10+
"*/*": {
1111
"schema": {
1212
"type": "array",
1313
"items": {
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
{
2+
"openapi": "3.0.1",
3+
"servers": [
4+
{
5+
"url": "http://config-server.cbss-kue-coe-dv.cl.local:1111",
6+
"description": "Generated server url"
7+
}
8+
],
9+
"paths": {
10+
"/path1": {
11+
"get": {
12+
"operationId": "retrieve_1",
13+
"responses": {
14+
"200": {
15+
"description": "OK",
16+
"content": {
17+
"*/*": {
18+
"schema": {
19+
"type": "string"
20+
}
21+
},
22+
"application/octet-stream": {
23+
"schema": {
24+
"type": "string",
25+
"format": "byte"
26+
}
27+
}
28+
}
29+
},
30+
"404": {
31+
"description": "Not Found"
32+
}
33+
}
34+
}
35+
}
36+
},
37+
"components": {}
38+
}
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
{
2+
"openapi": "3.0.1",
3+
"servers": [
4+
{
5+
"url": "http://config-server:1111",
6+
"description": "Generated server url"
7+
}
8+
],
9+
"paths": {
10+
"/path1": {
11+
"get": {
12+
"operationId": "retrieve_1",
13+
"responses": {
14+
"200": {
15+
"description": "OK",
16+
"content": {
17+
"*/*": {
18+
"schema": {
19+
"type": "string"
20+
}
21+
},
22+
"application/octet-stream": {
23+
"schema": {
24+
"type": "string",
25+
"format": "byte"
26+
}
27+
}
28+
}
29+
},
30+
"404": {
31+
"description": "Not Found"
32+
}
33+
}
34+
}
35+
}
36+
},
37+
"components": {}
38+
}

0 commit comments

Comments
 (0)