Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
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
4 changes: 2 additions & 2 deletions src/components/compare/async.bwc.validation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,8 @@

import {
APIHUB_API_COMPATIBILITY_KIND_BWC,
APIHUB_API_COMPATIBILITY_KIND_NO_BWC,
ApihubApiCompatibilityKind,
isNoBwcOrExperimental,
} from '../../consts'
import { JsonPath } from '@netcracker/qubership-apihub-json-crawl'
import {
Expand Down Expand Up @@ -48,7 +48,7 @@ const toApiCompatibilityKind = (
beforeKind: ApihubApiCompatibilityKind,
afterKind: ApihubApiCompatibilityKind,
): ApiCompatibilityKind => {
return (beforeKind === APIHUB_API_COMPATIBILITY_KIND_NO_BWC || afterKind === APIHUB_API_COMPATIBILITY_KIND_NO_BWC)
return (isNoBwcOrExperimental(beforeKind) || isNoBwcOrExperimental(afterKind))
? API_COMPATIBILITY_KIND_NOT_BACKWARD_COMPATIBLE
: API_COMPATIBILITY_KIND_BACKWARD_COMPATIBLE
}
Expand Down
23 changes: 17 additions & 6 deletions src/components/compare/rest.bwc.validation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,8 @@

import {
APIHUB_API_COMPATIBILITY_KIND_BWC,
APIHUB_API_COMPATIBILITY_KIND_NO_BWC,
ApihubApiCompatibilityKind,
isNoBwcOrExperimental,
SPECIFICATION_EXTENSION_PREFIX,
} from '../../consts'
import { isObject, isValidHttpMethod } from '../../utils'
Expand All @@ -43,28 +43,29 @@ export const calculateOperationApiCompatibilityKind = (

// Handle operation removal: compatibility depends on the removed operation's kind
if (isOperationRemoved) {
return beforeKind === APIHUB_API_COMPATIBILITY_KIND_NO_BWC
return isNoBwcOrExperimental(beforeKind)
? API_COMPATIBILITY_KIND_NOT_BACKWARD_COMPATIBLE
: API_COMPATIBILITY_KIND_BACKWARD_COMPATIBLE
}

if (beforeKind === APIHUB_API_COMPATIBILITY_KIND_NO_BWC || afterKind === APIHUB_API_COMPATIBILITY_KIND_NO_BWC) {
if (isNoBwcOrExperimental(beforeKind) || isNoBwcOrExperimental(afterKind)) {
return API_COMPATIBILITY_KIND_NOT_BACKWARD_COMPATIBLE
}

return API_COMPATIBILITY_KIND_BACKWARD_COMPATIBLE
}

export const getMethodsApiCompatibilityKind = (pathItemObject: OpenAPIV3.PathItemObject, prevDocumentApiKind: ApihubApiCompatibilityKind): ApiCompatibilityKind => {
if (checkAllMethodsHaveSameApiKind(pathItemObject, APIHUB_API_COMPATIBILITY_KIND_NO_BWC)) {
// Predicate-based: covers mixed apiKinds in one pathItem (e.g. GET=no-BWC, POST=experimental)
if (checkAllMethodsMatchApiKind(pathItemObject, isNoBwcOrExperimental)) {
return API_COMPATIBILITY_KIND_NOT_BACKWARD_COMPATIBLE
}

if (checkAllMethodsHaveSameApiKind(pathItemObject, APIHUB_API_COMPATIBILITY_KIND_BWC)) {
return API_COMPATIBILITY_KIND_BACKWARD_COMPATIBLE
}

return prevDocumentApiKind === APIHUB_API_COMPATIBILITY_KIND_NO_BWC
return isNoBwcOrExperimental(prevDocumentApiKind)
? API_COMPATIBILITY_KIND_NOT_BACKWARD_COMPATIBLE
: API_COMPATIBILITY_KIND_BACKWARD_COMPATIBLE
}
Expand All @@ -73,6 +74,16 @@ const hasApiKind = (obj: OpenAPIV3.OperationObject, apiKind: ApihubApiCompatibil
return getApiKindProperty(obj) === apiKind
}

const checkAllMethodsMatchApiKind = (obj: OpenAPIV3.PathItemObject, predicate: (kind: ApihubApiCompatibilityKind | undefined) => boolean): boolean => {
if (!isObject(obj)) {
return false
}
const entries = Object.entries(obj)
return entries.length > 0 &&
entries.filter(([key, value]) => isValidHttpMethod(key) && isObject(value))
.every(([_, value]) => predicate(getApiKindProperty(value as OpenAPIV3.OperationObject)))
}

const isSpecificationExtension = (propertyKey?: PropertyKey): boolean => {
return propertyKey?.toString()?.startsWith(SPECIFICATION_EXTENSION_PREFIX) ?? false
}
Expand All @@ -98,7 +109,7 @@ export const createRestApiCompatibilityScopeFunction: ApiCompatibilityScopeFunct
prevDocumentApiKind = APIHUB_API_COMPATIBILITY_KIND_BWC,
currDocumentApiKind = APIHUB_API_COMPATIBILITY_KIND_BWC,
) => {
const defaultApiCompatibilityKind = (prevDocumentApiKind === APIHUB_API_COMPATIBILITY_KIND_NO_BWC || currDocumentApiKind === APIHUB_API_COMPATIBILITY_KIND_NO_BWC)
const defaultApiCompatibilityKind = (isNoBwcOrExperimental(prevDocumentApiKind) || isNoBwcOrExperimental(currDocumentApiKind))
? API_COMPATIBILITY_KIND_NOT_BACKWARD_COMPATIBLE
: API_COMPATIBILITY_KIND_BACKWARD_COMPATIBLE

Expand Down
4 changes: 4 additions & 0 deletions src/consts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,10 @@ export type ApihubApiCompatibilityKind = typeof APIHUB_API_COMPATIBILITY_KIND_BW
| typeof APIHUB_API_COMPATIBILITY_KIND_NO_BWC
| typeof APIHUB_API_COMPATIBILITY_KIND_EXPERIMENTAL

export const isNoBwcOrExperimental = (kind: ApihubApiCompatibilityKind | undefined): boolean => {
return kind === APIHUB_API_COMPATIBILITY_KIND_NO_BWC || kind === APIHUB_API_COMPATIBILITY_KIND_EXPERIMENTAL
}

export const API_KIND_LABEL = 'apihub/x-api-kind'

export const DOCUMENT_TYPE = {
Expand Down
149 changes: 149 additions & 0 deletions test/apiKinds.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -177,6 +177,7 @@ describe('Check Api Compatibility Function tests', () => {
const CURR_VERSION = 'v2'
const API_KIND_NO_BWC_LABEL = 'apihub/x-api-kind: no-BWC'
const API_KIND_BWC_LABEL = 'apihub/x-api-kind: BWC'
const API_KIND_EXPERIMENTAL_LABEL = 'apihub/x-api-kind: experimental'

describe('Calculate default ApiKind', () => {
test('should apply BWC api kind by default', async () => {
Expand Down Expand Up @@ -610,6 +611,154 @@ describe('Check Api Compatibility Function tests', () => {
})
})

describe('Experimental apiKind has same influence as no-bwc', () => {
describe('Experimental label tests', () => {
test('should apply file label experimental from previous document', async () => {
const result = await runApiKindTest('api-kinds/no-api-kind-in-documents', [API_KIND_EXPERIMENTAL_LABEL])
expect(result).toEqual(changesSummaryMatcher({ [RISKY_CHANGE_TYPE]: 1 }))
expect(result).toEqual(serializedComparisonDocumentMatcher([RISKY_CHANGE_TYPE]))
})

test('should apply file label experimental from current document', async () => {
const result = await runApiKindTest('api-kinds/no-api-kind-in-documents', [], [API_KIND_EXPERIMENTAL_LABEL])
expect(result).toEqual(changesSummaryMatcher({ [RISKY_CHANGE_TYPE]: 1 }))
expect(result).toEqual(serializedComparisonDocumentMatcher([RISKY_CHANGE_TYPE]))
})

test('should apply version label experimental from previous document', async () => {
const result = await runApiKindTest(
'api-kinds/no-api-kind-in-documents', [], [], [API_KIND_EXPERIMENTAL_LABEL],
)
expect(result).toEqual(changesSummaryMatcher({ [RISKY_CHANGE_TYPE]: 1 }))
expect(result).toEqual(serializedComparisonDocumentMatcher([RISKY_CHANGE_TYPE]))
})

test('should apply version label experimental from current document', async () => {
const result = await runApiKindTest(
'api-kinds/no-api-kind-in-documents', [], [], [], [API_KIND_EXPERIMENTAL_LABEL],
)
expect(result).toEqual(changesSummaryMatcher({ [RISKY_CHANGE_TYPE]: 1 }))
expect(result).toEqual(serializedComparisonDocumentMatcher([RISKY_CHANGE_TYPE]))
})
})

describe('Experimental info property tests', () => {
test('should apply info experimental property in previous document', async () => {
const result = await runApiKindTest('api-kinds/info-experimental-in-prev-document')
expect(result).toEqual(changesSummaryMatcher({ [RISKY_CHANGE_TYPE]: 1 }))
expect(result).toEqual(serializedComparisonDocumentMatcher([RISKY_CHANGE_TYPE]))
})

test('should apply info experimental property in current document', async () => {
const result = await runApiKindTest('api-kinds/info-experimental-in-curr-document')
expect(result).toEqual(changesSummaryMatcher({ [RISKY_CHANGE_TYPE]: 1 }))
expect(result).toEqual(serializedComparisonDocumentMatcher([RISKY_CHANGE_TYPE]))
})
})

describe('Experimental operation section tests', () => {
test('should apply operation experimental in current document', async () => {
const result = await runApiKindTest('api-kinds/operation-experimental-in-curr-document')
expect(result).toEqual(changesSummaryMatcher(OPERATION_RISKY_CHANGE_TYPES))
expect(result).toEqual(serializedComparisonDocumentMatcher([RISKY_CHANGE_TYPE]))
})

test('should apply operation experimental in previous document', async () => {
const result = await runApiKindTest('api-kinds/operation-experimental-in-prev-document')
expect(result).toEqual(changesSummaryMatcher(OPERATION_RISKY_CHANGE_TYPES))
expect(result).toEqual(serializedComparisonDocumentMatcher([RISKY_CHANGE_TYPE]))
})
})

describe('Remove experimental operations tests', () => {
test('should apply removed experimental operations as risky', async () => {
const result = await runApiKindTest('api-kinds/remove-operations-experimental')
expect(result).toEqual(changesSummaryMatcher({ [RISKY_CHANGE_TYPE]: 1 }))
expect(result).toEqual(serializedComparisonDocumentMatcher([RISKY_CHANGE_TYPE]))
})

test('should apply removed pathItem with experimental operation as risky', async () => {
const result = await runApiKindTest('api-kinds/remove-pathItem-operation-experimental-in-prev-document')
expect(result).toEqual(changesSummaryMatcher({ [RISKY_CHANGE_TYPE]: 1 }))
expect(result).toEqual(serializedComparisonDocumentMatcher([RISKY_CHANGE_TYPE]))
})
})
})

describe('Mixed no-bwc and experimental apiKind tests', () => {
describe('Mixed file label tests', () => {
test('should apply file label no-BWC in previous document and experimental in current document', async () => {
const result = await runApiKindTest(
'api-kinds/no-api-kind-in-documents', [API_KIND_NO_BWC_LABEL], [API_KIND_EXPERIMENTAL_LABEL],
)
expect(result).toEqual(changesSummaryMatcher({ [RISKY_CHANGE_TYPE]: 1 }))
expect(result).toEqual(serializedComparisonDocumentMatcher([RISKY_CHANGE_TYPE]))
})

test('should apply file label experimental in previous document and no-BWC in current document', async () => {
const result = await runApiKindTest(
'api-kinds/no-api-kind-in-documents', [API_KIND_EXPERIMENTAL_LABEL], [API_KIND_NO_BWC_LABEL],
)
expect(result).toEqual(changesSummaryMatcher({ [RISKY_CHANGE_TYPE]: 1 }))
expect(result).toEqual(serializedComparisonDocumentMatcher([RISKY_CHANGE_TYPE]))
})
})

describe('Mixed version label tests', () => {
test('should apply version label no-BWC in previous document and experimental in current document', async () => {
const result = await runApiKindTest(
'api-kinds/no-api-kind-in-documents', [], [], [API_KIND_NO_BWC_LABEL], [API_KIND_EXPERIMENTAL_LABEL],
)
expect(result).toEqual(changesSummaryMatcher({ [RISKY_CHANGE_TYPE]: 1 }))
expect(result).toEqual(serializedComparisonDocumentMatcher([RISKY_CHANGE_TYPE]))
})

test('should apply version label experimental in previous document and no-BWC in current document', async () => {
const result = await runApiKindTest(
'api-kinds/no-api-kind-in-documents', [], [], [API_KIND_EXPERIMENTAL_LABEL], [API_KIND_NO_BWC_LABEL],
)
expect(result).toEqual(changesSummaryMatcher({ [RISKY_CHANGE_TYPE]: 1 }))
expect(result).toEqual(serializedComparisonDocumentMatcher([RISKY_CHANGE_TYPE]))
})
})

describe('Mixed info property tests', () => {
test('should apply info no-BWC property in previous document and experimental property in current document', async () => {
const result = await runApiKindTest('api-kinds/info-noBWC-in-prev-document-info-experimental-in-curr-document')
expect(result).toEqual(changesSummaryMatcher({ [RISKY_CHANGE_TYPE]: 1 }))
expect(result).toEqual(serializedComparisonDocumentMatcher([RISKY_CHANGE_TYPE]))
})

test('should apply info experimental property in previous document and no-BWC property in current document', async () => {
const result = await runApiKindTest('api-kinds/info-experimental-in-prev-document-info-noBWC-in-curr-document')
expect(result).toEqual(changesSummaryMatcher({ [RISKY_CHANGE_TYPE]: 1 }))
expect(result).toEqual(serializedComparisonDocumentMatcher([RISKY_CHANGE_TYPE]))
})
})

describe('Mixed operation section tests', () => {
test('should apply operation no-BWC in previous document and experimental in current document', async () => {
const result = await runApiKindTest('api-kinds/operation-noBWC-in-prev-document-operation-experimental-in-curr-document')
expect(result).toEqual(changesSummaryMatcher(OPERATION_RISKY_CHANGE_TYPES))
expect(result).toEqual(serializedComparisonDocumentMatcher([RISKY_CHANGE_TYPE]))
})

test('should apply operation experimental in previous document and no-BWC in current document', async () => {
const result = await runApiKindTest('api-kinds/operation-experimental-in-prev-document-operation-noBWC-in-curr-document')
expect(result).toEqual(changesSummaryMatcher(OPERATION_RISKY_CHANGE_TYPES))
expect(result).toEqual(serializedComparisonDocumentMatcher([RISKY_CHANGE_TYPE]))
})
})

describe('Mixed apiKind within same pathItem tests', () => {
test('should apply removed pathItem with mixed no-BWC and experimental operations as risky', async () => {
const result = await runApiKindTest('api-kinds/remove-pathItem-operations-noBWC-and-experimental-in-prev-document')
expect(result).toEqual(changesSummaryMatcher({ [RISKY_CHANGE_TYPE]: 2 }))
expect(result).toEqual(serializedComparisonDocumentMatcher([RISKY_CHANGE_TYPE]))
})
})
})

describe('PathItem specification extensions tests', () => {
let calculateOperationApiCompatibilityKindSpy: ReturnType<typeof jest.spyOn>
let getMethodsApiCompatibilityKindSpy: ReturnType<typeof jest.spyOn>
Expand Down
9 changes: 9 additions & 0 deletions test/asyncapi-build-apikind.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import {
APIHUB_API_COMPATIBILITY_KIND_BWC,
APIHUB_API_COMPATIBILITY_KIND_EXPERIMENTAL,
APIHUB_API_COMPATIBILITY_KIND_NO_BWC,
ApihubApiCompatibilityKind,
ApiOperation,
Expand All @@ -20,6 +21,14 @@ describe('AsyncAPI apiKind calculation', () => {
[undefined, APIHUB_API_COMPATIBILITY_KIND_NO_BWC, APIHUB_API_COMPATIBILITY_KIND_NO_BWC],
[APIHUB_API_COMPATIBILITY_KIND_BWC, APIHUB_API_COMPATIBILITY_KIND_NO_BWC, APIHUB_API_COMPATIBILITY_KIND_BWC],
[APIHUB_API_COMPATIBILITY_KIND_NO_BWC, APIHUB_API_COMPATIBILITY_KIND_BWC, APIHUB_API_COMPATIBILITY_KIND_NO_BWC],
// experimental
[APIHUB_API_COMPATIBILITY_KIND_EXPERIMENTAL, undefined, APIHUB_API_COMPATIBILITY_KIND_EXPERIMENTAL],
[undefined, APIHUB_API_COMPATIBILITY_KIND_EXPERIMENTAL, APIHUB_API_COMPATIBILITY_KIND_EXPERIMENTAL],
[APIHUB_API_COMPATIBILITY_KIND_EXPERIMENTAL, APIHUB_API_COMPATIBILITY_KIND_BWC, APIHUB_API_COMPATIBILITY_KIND_EXPERIMENTAL],
[APIHUB_API_COMPATIBILITY_KIND_BWC, APIHUB_API_COMPATIBILITY_KIND_EXPERIMENTAL, APIHUB_API_COMPATIBILITY_KIND_BWC],
// mixed no-bwc and experimental
[APIHUB_API_COMPATIBILITY_KIND_EXPERIMENTAL, APIHUB_API_COMPATIBILITY_KIND_NO_BWC, APIHUB_API_COMPATIBILITY_KIND_EXPERIMENTAL],
[APIHUB_API_COMPATIBILITY_KIND_NO_BWC, APIHUB_API_COMPATIBILITY_KIND_EXPERIMENTAL, APIHUB_API_COMPATIBILITY_KIND_NO_BWC],
]
data.forEach(([operationApiKind, channelApiKind, expected]) => {
const result = calculateAsyncApiKind(operationApiKind as ApihubApiCompatibilityKind, channelApiKind as ApihubApiCompatibilityKind)
Expand Down
Loading
Loading