Skip to content

Commit d85d403

Browse files
committed
feat: add full media type matches with first priority
- add full media type matches with first priority with corresponding tests - remove reduntant 'should map wildcard fallback after exact matches' test
1 parent 1291a05 commit d85d403

File tree

2 files changed

+101
-40
lines changed

2 files changed

+101
-40
lines changed

src/openapi/openapi3.mapping.ts

Lines changed: 36 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -93,35 +93,50 @@ export const contentMediaTypeMappingResolver: MappingResolver<string> = (before,
9393

9494
const beforeKeys = objectKeys(before)
9595
const afterKeys = objectKeys(after)
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] ?? '')
10096

10197
const unmappedAfterIndices = new Set(afterKeys.keys())
10298
const unmappedBeforeIndices = new Set(beforeKeys.keys())
10399

104-
// Map exact matches first
105-
for (let i = 0; i < beforeKeys.length; i++) {
106-
const beforeBaseType = beforeBaseTypes[i]
107-
const afterIndex = afterBaseTypes.findIndex((afterBaseType, index) =>
108-
afterBaseType === beforeBaseType && unmappedAfterIndices.has(index)
109-
)
100+
function mapExactMatches(
101+
getComparisonKey: (key: string) => string
102+
): void {
103+
104+
for (const beforeIndex of unmappedBeforeIndices) {
105+
const beforeKey = getComparisonKey(beforeKeys[beforeIndex])
106+
107+
// Find matching after index by iterating over the after indices set
108+
let matchingAfterIndex: number | undefined
109+
for (const afterIndex of unmappedAfterIndices) {
110+
const afterKey = getComparisonKey(afterKeys[afterIndex])
111+
if (afterKey === beforeKey) {
112+
matchingAfterIndex = afterIndex
113+
break
114+
}
115+
}
110116

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)
117+
if (matchingAfterIndex !== undefined) {
118+
// Match found - create mapping and remove from unmapped sets
119+
result.mapped[beforeKeys[beforeIndex]] = afterKeys[matchingAfterIndex]
120+
unmappedAfterIndices.delete(matchingAfterIndex)
121+
unmappedBeforeIndices.delete(beforeIndex)
122+
}
116123
}
117124
}
118125

126+
// First, map exact matches for full media type
127+
mapExactMatches((key) => key)
128+
129+
// After that, try to map media types by base type for remaining unmapped keys
130+
mapExactMatches(getMediaTypeBase)
131+
119132
// If exactly one unmapped item in both before and after, try wildcard matching
120133
if (unmappedBeforeIndices.size === 1 && unmappedAfterIndices.size === 1) {
121134
const beforeIndex = Array.from(unmappedBeforeIndices)[0]
122135
const afterIndex = Array.from(unmappedAfterIndices)[0]
123-
const beforeBaseType = beforeBaseTypes[beforeIndex]
124-
const afterBaseType = afterBaseTypes[afterIndex]
136+
const beforeKey = beforeKeys[beforeIndex]
137+
const afterKey = afterKeys[afterIndex]
138+
const beforeBaseType = getMediaTypeBase(beforeKey)
139+
const afterBaseType = getMediaTypeBase(afterKey)
125140

126141
// Check if they are compatible using wildcard matching
127142
if (isWildcardCompatible(beforeBaseType, afterBaseType)) {
@@ -139,6 +154,10 @@ export const contentMediaTypeMappingResolver: MappingResolver<string> = (before,
139154
return result
140155
}
141156

157+
function getMediaTypeBase(mediaType: string): string {
158+
return mediaType.split(';')[0] ?? ''
159+
}
160+
142161
function isWildcardCompatible(beforeType: string, afterType: string): boolean {
143162
const [beforeMainType, beforeSubType] = beforeType.split('/')
144163
const [afterMainType, afterSubType] = afterType.split('/')

test/openapi.contentMediaTypeMapping.test.ts

Lines changed: 65 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -119,29 +119,7 @@ describe('Content Media Type Mapping', () => {
119119
removed: [],
120120
mapped: { '*/*': 'application/*' }
121121
})
122-
})
123-
124-
it('should map wildcard fallback after exact matches', () => {
125-
const before = {
126-
'application/json': {},
127-
'*/*': {}
128-
}
129-
const after = {
130-
'application/json': {},
131-
'text/xml': {}
132-
}
133-
134-
const result = contentMediaTypeMappingResolver(before, after, mockContext)
135-
136-
expect(result).toEqual({
137-
added: [],
138-
removed: [],
139-
mapped: {
140-
'application/json': 'application/json',
141-
'*/*': 'text/xml'
142-
}
143-
})
144-
})
122+
})
145123
})
146124

147125
describe('No Mapping Cases', () => {
@@ -351,4 +329,68 @@ describe('Content Media Type Mapping', () => {
351329
})
352330
})
353331
})
332+
333+
describe('Full Media Type Matching Priority Tests', () => {
334+
it('should prioritize exact full media type matches over base type matches', () => {
335+
const before = {
336+
'application/json': {},
337+
'application/json; charset=utf-8': {}
338+
}
339+
const after = {
340+
'application/json; charset=utf-8': {},
341+
'application/json': {}
342+
}
343+
344+
const result = contentMediaTypeMappingResolver(before, after, mockContext)
345+
346+
expect(result).toEqual({
347+
added: [],
348+
removed: [],
349+
mapped: {
350+
'application/json': 'application/json',
351+
'application/json; charset=utf-8': 'application/json; charset=utf-8'
352+
}
353+
})
354+
})
355+
356+
it('should fall back to base type matching when no full media type matches exist', () => {
357+
const before = {
358+
'application/json; charset=utf-8': {}
359+
}
360+
const after = {
361+
'application/json; charset=iso-8859-1': {}
362+
}
363+
364+
const result = contentMediaTypeMappingResolver(before, after, mockContext)
365+
366+
expect(result).toEqual({
367+
added: [],
368+
removed: [],
369+
mapped: {
370+
'application/json; charset=utf-8': 'application/json; charset=iso-8859-1'
371+
}
372+
})
373+
})
374+
375+
it('should prioritize full media type matches even when base types could match differently', () => {
376+
const before = {
377+
'application/json; charset=utf-8': {},
378+
'application/xml': {}
379+
}
380+
const after = {
381+
'application/json; charset=utf-8': {},
382+
'application/json': {}
383+
}
384+
385+
const result = contentMediaTypeMappingResolver(before, after, mockContext)
386+
387+
expect(result).toEqual({
388+
added: ['application/json'],
389+
removed: ['application/xml'],
390+
mapped: {
391+
'application/json; charset=utf-8': 'application/json; charset=utf-8'
392+
}
393+
})
394+
})
395+
})
354396
})

0 commit comments

Comments
 (0)