Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
72 changes: 52 additions & 20 deletions src/openapi/openapi3.mapping.ts
Original file line number Diff line number Diff line change
Expand Up @@ -92,38 +92,70 @@ export const contentMediaTypeMappingResolver: MappingResolver<string> = (before,
const result: MapKeysResult<string> = { added: [], removed: [], mapped: {} }

const beforeKeys = objectKeys(before)
const _beforeKeys = beforeKeys.map((key) => key.split(';')[0] ?? '')
const afterKeys = objectKeys(after)
const _afterKeys = afterKeys.map((key) => key.split(';')[0] ?? '')

// Extract base media types (without parameters like charset, etc.)
const beforeBaseTypes = beforeKeys.map((key) => key.split(';')[0] ?? '')
const afterBaseTypes = afterKeys.map((key) => key.split(';')[0] ?? '')

const mappedIndex = new Set(afterKeys.keys())
const unmappedAfterIndices = new Set(afterKeys.keys())
const unmappedBeforeIndices = new Set(beforeKeys.keys())

// Map exact matches first
for (let i = 0; i < beforeKeys.length; i++) {
const _afterIndex = _afterKeys.findIndex((key) => {
const [afterType, afterSubType] = key.split('/')
const [beforeType, beforeSubType] = _beforeKeys[i].split('/')

if (afterType !== beforeType && afterType !== '*' && beforeType !== '*') { return false }
if (afterSubType !== beforeSubType && afterSubType !== '*' && beforeSubType !== '*') { return false }
return true
})
const beforeBaseType = beforeBaseTypes[i]
const afterIndex = afterBaseTypes.findIndex((afterBaseType, index) =>
afterBaseType === beforeBaseType && unmappedAfterIndices.has(index)
)

if (afterIndex >= 0) {
// Exact match found - map it
result.mapped[beforeKeys[i]] = afterKeys[afterIndex]
unmappedAfterIndices.delete(afterIndex)
unmappedBeforeIndices.delete(i)
}
}

if (_afterIndex < 0 || !mappedIndex.has(_afterIndex)) {
// removed item
result.removed.push(beforeKeys[i])
} else {
// mapped items
result.mapped[beforeKeys[i]] = afterKeys[_afterIndex]
mappedIndex.delete(_afterIndex)
// If exactly one unmapped item in both before and after, try wildcard matching
if (unmappedBeforeIndices.size === 1 && unmappedAfterIndices.size === 1) {
const beforeIndex = Array.from(unmappedBeforeIndices)[0]
const afterIndex = Array.from(unmappedAfterIndices)[0]
const beforeBaseType = beforeBaseTypes[beforeIndex]
const afterBaseType = afterBaseTypes[afterIndex]

// Check if they are compatible using wildcard matching
if (isWildcardCompatible(beforeBaseType, afterBaseType)) {
// Map them together
result.mapped[beforeKeys[beforeIndex]] = afterKeys[afterIndex]
unmappedAfterIndices.delete(afterIndex)
unmappedBeforeIndices.delete(beforeIndex)
}
}

// added items
mappedIndex.forEach((i) => result.added.push(afterKeys[i]))
// Mark remaining unmapped items as removed/added
unmappedBeforeIndices.forEach((index) => result.removed.push(beforeKeys[index]))
unmappedAfterIndices.forEach((index) => result.added.push(afterKeys[index]))

return result
}

function isWildcardCompatible(beforeType: string, afterType: string): boolean {
const [beforeMainType, beforeSubType] = beforeType.split('/')
const [afterMainType, afterSubType] = afterType.split('/')

// Check main type compatibility
if (beforeMainType !== afterMainType && beforeMainType !== '*' && afterMainType !== '*') {
return false
}

// Check sub type compatibility
if (beforeSubType !== afterSubType && beforeSubType !== '*' && afterSubType !== '*') {
return false
}

return true
}

export function hidePathParamNames(path: string): string {
return path.replace(PATH_PARAMETER_REGEXP, PATH_PARAM_UNIFIED_PLACEHOLDER)
}
Expand Down
32 changes: 21 additions & 11 deletions test/bugs.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,9 @@ import changeToNothingClassificationAfter from './helper/resources/change-to-not
import spearedParamsBefore from './helper/resources/speared-parameters/before.json'
import spearedParamsAfter from './helper/resources/speared-parameters/after.json'

import wildcardContentSchemaMediaTypeCombinedWithSpecificMediaTypeBefore from './helper/resources/wildcard-content-schema-media-type-combined-with-specific-media-type/before.json'
import wildcardContentSchemaMediaTypeCombinedWithSpecificMediaTypeAfter from './helper/resources/wildcard-content-schema-media-type-combined-with-specific-media-type/after.json'

import { diffsMatcher } from './helper/matchers'
import { TEST_DIFF_FLAG, TEST_ORIGINS_FLAG } from './helper'
import { JSON_SCHEMA_NODE_SYNTHETIC_TYPE_NOTHING } from '@netcracker/qubership-apihub-api-unifier'
Expand Down Expand Up @@ -97,16 +100,7 @@ describe('Real Data', () => {
const after: any = infinityAfter
const { diffs } = apiDiff(before, after, OPTIONS)
const responseContentPath = ['paths', '/api/v1/dictionaries/dictionary/item', 'get', 'responses', '200', 'content']
expect(diffs).toEqual(diffsMatcher([
expect.objectContaining({
beforeDeclarationPaths: [[...responseContentPath, '*/*']],
afterDeclarationPaths: [[...responseContentPath, 'application/json']],
action: DiffAction.rename,
beforeKey: '*/*',
afterKey: 'application/json',
type: nonBreaking,
scope: 'response',
}),
expect(diffs).toEqual(diffsMatcher([
expect.objectContaining({
afterDeclarationPaths: [['components', 'schemas', 'DictionaryItem', 'x-entity']],
afterValue: 'DictionaryItem',
Expand Down Expand Up @@ -210,10 +204,26 @@ describe('Real Data', () => {
}),
]))
})
it('spered parameters', () => {

it('speared parameters', () => {
const before: any = spearedParamsBefore
const after: any = spearedParamsAfter
const { diffs } = apiDiff(before, after, OPTIONS)
expect(diffs).toBeEmpty()
})

it('wildcard content schema media type in combination with specific media type', () => {
const before: any = wildcardContentSchemaMediaTypeCombinedWithSpecificMediaTypeBefore
const after: any = wildcardContentSchemaMediaTypeCombinedWithSpecificMediaTypeAfter
const { diffs } = apiDiff(before, after, OPTIONS)

expect(diffs).toEqual(diffsMatcher([
expect.objectContaining({
action: DiffAction.replace,
beforeDeclarationPaths: [['servers', 0, 'url']],
afterDeclarationPaths: [['servers', 0, 'url']],
type: annotation,
}),
]))
})
})
2 changes: 1 addition & 1 deletion test/helper/resources/ref-with-array-to-self/after.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
"200": {
"description": "OK",
"content": {
"application/json": {
"*/*": {
"schema": {
"type": "array",
"items": {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
{
"openapi": "3.0.1",
"servers": [
{
"url": "http://config-server.cbss-kue-coe-dv.cl.local:1111",
"description": "Generated server url"
}
],
"paths": {
"/path1": {
"get": {
"operationId": "retrieve_1",
"responses": {
"200": {
"description": "OK",
"content": {
"*/*": {
"schema": {
"type": "string"
}
},
"application/octet-stream": {
"schema": {
"type": "string",
"format": "byte"
}
}
}
},
"404": {
"description": "Not Found"
}
}
}
}
},
"components": {}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
{
"openapi": "3.0.1",
"servers": [
{
"url": "http://config-server:1111",
"description": "Generated server url"
}
],
"paths": {
"/path1": {
"get": {
"operationId": "retrieve_1",
"responses": {
"200": {
"description": "OK",
"content": {
"*/*": {
"schema": {
"type": "string"
}
},
"application/octet-stream": {
"schema": {
"type": "string",
"format": "byte"
}
}
}
},
"404": {
"description": "Not Found"
}
}
}
}
},
"components": {}
}
Loading