diff --git a/package.json b/package.json index 099d437..9f2afef 100644 --- a/package.json +++ b/package.json @@ -32,7 +32,7 @@ "fast-equals": "6.0.0" }, "devDependencies": { - "@netcracker/qubership-apihub-compatibility-suites": "dev", + "@netcracker/qubership-apihub-compatibility-suites": "feature-rules-reclassification-part-1", "@netcracker/qubership-apihub-graphapi": "1.0.9", "@netcracker/qubership-apihub-npm-gitflow": "3.1.0", "@types/jest": "30.0.0", diff --git a/src/jsonSchema/jsonSchema.classify.ts b/src/jsonSchema/jsonSchema.classify.ts index c759ced..828fbce 100644 --- a/src/jsonSchema/jsonSchema.classify.ts +++ b/src/jsonSchema/jsonSchema.classify.ts @@ -5,6 +5,7 @@ import { nonBreaking, PARENT_JUMP, risky, + riskyIf, strictResolveValueFromContext, unclassified, } from '../core' @@ -26,13 +27,16 @@ export const typeClassifier: ClassifyRule = [ ({ before, after }) => nonBreakingIf(isTypeAssignable(before.value, after.value, false)), breaking,//not tested breaking,//not tested - ({ before, after }) => nonBreakingIf(isTypeAssignable(before.value, after.value, true)), + ({ before, after }) => isTypeAssignable(before.value, after.value, true) ? risky : breaking, ] export const maxClassifier: ClassifyRule = [ breaking, nonBreaking, ({ before, after }) => breakingIf(!isNumber(before.value) || !isNumber(after.value) || before.value > after.value), + nonBreaking, + risky, + ({ before, after }) => riskyIf(isNumber(before.value) && isNumber(after.value) && before.value < after.value), ] export const minClassifier: ClassifyRule = [ @@ -48,6 +52,29 @@ export const minimumClassifier: ClassifyRule = [ }, nonBreaking, ({ before, after }) => breakingIf(!isNumber(before.value) || !isNumber(after.value) || before.value < after.value), + ({ before, after }) => { + const beforeExclusiveMinimum = strictResolveValueFromContext(before, PARENT_JUMP, 'exclusiveMinimum') + return nonBreakingIf(!isNumber(beforeExclusiveMinimum) || !isNumber(after.value) || beforeExclusiveMinimum < after.value) + }, + ({ before }) => { + const propertyName = before.parentContext?.key + const requiredArray = getArrayValue(strictResolveValueFromContext(before, PARENT_JUMP, PARENT_JUMP, PARENT_JUMP, 'required')) + if (isString(propertyName) && requiredArray?.includes(propertyName)) { + return breaking + } + return risky + }, + ({ before, after }) => { + if (!isNumber(before.value) || !isNumber(after.value) || before.value < after.value) { + return nonBreaking + } + const propertyName = before.parentContext?.key + const requiredArray = getArrayValue(strictResolveValueFromContext(before, PARENT_JUMP, PARENT_JUMP, PARENT_JUMP, 'required')) + if (isString(propertyName) && requiredArray?.includes(propertyName)) { + return breaking + } + return risky + }, ] export const maximumClassifier: ClassifyRule = [ @@ -63,6 +90,9 @@ export const exclusiveClassifier: ClassifyRule = [ ({ after }) => (after.value === true ? breaking : unclassified), ({ before }) => (before.value === true ? nonBreaking : unclassified), breakingIfAfterTrue, + ({ after }) => (after.value === true ? nonBreaking : unclassified), + ({ before }) => (before.value === true ? risky : unclassified), + ({ after }) => riskyIf(!after.value), ] //todo think about replace multipleOf in inverse case @@ -72,11 +102,12 @@ export const multipleOfClassifier: ClassifyRule = [ ({ before, after }) => breakingIfNotMultiple(before.value, after.value), nonBreaking, breaking, - breaking, + risky, ] export const requiredItemClassifyRule: ClassifyRule = [ - ({ after }) => (!isString(after.value) || isExist(strictResolveValueFromContext(after, PARENT_JUMP, PARENT_JUMP, 'properties', after.value, 'default')) ? nonBreaking : breaking), + // classification is the same, but we are keeping the code structure to be able to change it if needed + ({ after }) => (!isString(after.value) || isExist(strictResolveValueFromContext(after, PARENT_JUMP, PARENT_JUMP, 'properties', after.value, 'default')) ? breaking : breaking), nonBreaking, ({ after }) => (!isString(after.value) || isExist(strictResolveValueFromContext(after, PARENT_JUMP, PARENT_JUMP, 'properties', after.value, 'default')) ? nonBreaking : breaking), nonBreaking, @@ -90,7 +121,7 @@ export const propertyClassifyRule: ClassifyRule = [ !isExist(getKeyValue(after.value, 'default')) && getArrayValue((strictResolveValueFromContext(after, PARENT_JUMP, PARENT_JUMP, 'required')))?.includes(after.key) ? breaking : nonBreaking ), - breaking, + nonBreaking, unclassified, nonBreaking, ({ before }) => (getArrayValue(strictResolveValueFromContext(before, PARENT_JUMP, PARENT_JUMP, 'required'))?.includes(before.key) ? breaking : nonBreaking), diff --git a/src/jsonSchema/jsonSchema.rules.ts b/src/jsonSchema/jsonSchema.rules.ts index 9232ff9..5e2e319 100644 --- a/src/jsonSchema/jsonSchema.rules.ts +++ b/src/jsonSchema/jsonSchema.rules.ts @@ -119,7 +119,7 @@ export const jsonSchemaRules = ({ }, }, - '/format': simpleRule([breaking, nonBreaking, breaking, nonBreaking, breaking, breaking], resolveSchemaDescriptionTemplates('format')), + '/format': simpleRule([breaking, nonBreaking, breaking, nonBreaking, risky, risky], resolveSchemaDescriptionTemplates('format')), '/default': simpleRule([nonBreaking, breaking, breaking], resolveSchemaDescriptionTemplates('default value')), '/enum': { diff --git a/test/compatibility-suites/schemas/schema-test-runner-general.ts b/test/compatibility-suites/schemas/schema-test-runner-general.ts index ba4d007..ee805a1 100644 --- a/test/compatibility-suites/schemas/schema-test-runner-general.ts +++ b/test/compatibility-suites/schemas/schema-test-runner-general.ts @@ -165,7 +165,7 @@ export function runGeneralSchemaTests( action: DiffAction.replace, beforeDeclarationPaths: [[...commonPath, 'format']], afterDeclarationPaths: [[...commonPath, 'format']], - type: breaking, + type: expectedType(breaking, risky), }), ])) }) @@ -176,7 +176,7 @@ export function runGeneralSchemaTests( expect.objectContaining({ action: DiffAction.remove, beforeDeclarationPaths: [[...commonPath, 'format']], - type: expectedType(nonBreaking, breaking), + type: expectedType(nonBreaking, risky), }), ])) }) @@ -259,7 +259,7 @@ export function runGeneralSchemaTests( action: DiffAction.replace, beforeDeclarationPaths: [[...commonPath, 'maxLength']], afterDeclarationPaths: [[...commonPath, 'maxLength']], - type: expectedType(nonBreaking, breaking), + type: expectedType(nonBreaking, risky), }), ])) }) @@ -282,7 +282,7 @@ export function runGeneralSchemaTests( expect.objectContaining({ action: DiffAction.remove, beforeDeclarationPaths: [[...commonPath, 'maxLength']], - type: expectedType(nonBreaking, breaking), + type: expectedType(nonBreaking, risky), }), ])) }) @@ -339,7 +339,7 @@ export function runGeneralSchemaTests( action: DiffAction.replace, beforeDeclarationPaths: [[...commonPath, 'format']], afterDeclarationPaths: [[...commonPath, 'format']], - type: breaking, + type: expectedType(breaking, risky), }), ])) }) @@ -350,7 +350,7 @@ export function runGeneralSchemaTests( expect.objectContaining({ action: DiffAction.remove, beforeDeclarationPaths: [[...commonPath, 'format']], - type: expectedType(nonBreaking, breaking), + type: expectedType(nonBreaking, risky), }), ])) }) @@ -385,6 +385,18 @@ export function runGeneralSchemaTests( action: DiffAction.replace, beforeDeclarationPaths: [[...commonPath, 'minimum']], afterDeclarationPaths: [[...commonPath, 'minimum']], + type: expectedType(nonBreaking, risky), + }), + ])) + }) + + test('decrease-minimum-for-required-number-property', async () => { + const result = await compareFiles(suiteId, currentTestId(), suiteType) + expect(result).toEqual(diffsMatcher([ + expect.objectContaining({ + action: DiffAction.replace, + beforeDeclarationPaths: [[...commonPath, 'properties', 'prop1', 'minimum']], + afterDeclarationPaths: [[...commonPath, 'properties', 'prop1', 'minimum']], type: expectedType(nonBreaking, breaking), }), ])) @@ -396,6 +408,17 @@ export function runGeneralSchemaTests( expect.objectContaining({ action: DiffAction.remove, beforeDeclarationPaths: [[...commonPath, 'minimum']], + type: expectedType(nonBreaking, risky), + }), + ])) + }) + + test('remove-minimum-for-required-number-property', async () => { + const result = await compareFiles(suiteId, currentTestId(), suiteType) + expect(result).toEqual(diffsMatcher([ + expect.objectContaining({ + action: DiffAction.remove, + beforeDeclarationPaths: [[...commonPath, 'properties', 'prop1', 'minimum']], type: expectedType(nonBreaking, breaking), }), ])) @@ -465,7 +488,7 @@ export function runGeneralSchemaTests( action: DiffAction.replace, beforeDeclarationPaths: [[...commonPath, 'multipleOf']], afterDeclarationPaths: [[...commonPath, 'multipleOf']], - type: breaking, + type: expectedType(breaking, risky), }), ])) }) @@ -547,7 +570,7 @@ export function runGeneralSchemaTests( action: DiffAction.replace, beforeDeclarationPaths: [[...commonPath, 'maxItems']], afterDeclarationPaths: [[...commonPath, 'maxItems']], - type: expectedType(nonBreaking, breaking), + type: expectedType(nonBreaking, risky), }), ])) }) @@ -570,7 +593,7 @@ export function runGeneralSchemaTests( expect.objectContaining({ action: DiffAction.remove, beforeDeclarationPaths: [[...commonPath, 'maxItems']], - type: expectedType(nonBreaking, breaking), + type: expectedType(nonBreaking, risky), }), ])) }) @@ -628,7 +651,7 @@ export function runGeneralSchemaTests( expect.objectContaining({ action: DiffAction.remove, beforeDeclarationPaths: [[...commonPath, 'properties', 'prop2']], - type: expectedType(breaking, nonBreaking), + type: nonBreaking, }), ])) }) @@ -655,17 +678,17 @@ export function runGeneralSchemaTests( expect.objectContaining({ action: DiffAction.add, afterDeclarationPaths: [[...commonPath, 'required', 0]], - type: nonBreaking, + type: expectedType(breaking, nonBreaking), }), expect.objectContaining({ action: DiffAction.add, afterDeclarationPaths: [[...commonPath, 'required', 1]], - type: nonBreaking, + type: expectedType(breaking, nonBreaking), }), ])) }) - test('remove-required-property', async () => { + test('remove-required-status-from-property', async () => { const result = await compareFiles(suiteId, currentTestId(), suiteType) expect(result).toEqual(diffsMatcher([ expect.objectContaining({ @@ -676,6 +699,22 @@ export function runGeneralSchemaTests( ])) }) + test('remove-required-property-compliance', async () => { + const result = await compareFiles(suiteId, currentTestId(), suiteType) + expect(result).toEqual(diffsMatcher([ + expect.objectContaining({ + action: DiffAction.remove, + beforeDeclarationPaths: [[...commonPath, 'required', 0]], + type: expectedType(nonBreaking, breaking), + }), + expect.objectContaining({ + action: DiffAction.remove, + beforeDeclarationPaths: [[...commonPath, 'properties', 'prop2']], + type: expectedType(nonBreaking, breaking), + }), + ])) + }) + test('update-required-property', async () => { const result = await compareFiles(suiteId, currentTestId(), suiteType) expect(result).toEqual(diffsMatcher([ @@ -830,7 +869,7 @@ export function runGeneralSchemaTests( action: DiffAction.replace, beforeDeclarationPaths: [[...commonPath, 'maxProperties']], afterDeclarationPaths: [[...commonPath, 'maxProperties']], - type: expectedType(nonBreaking, breaking), + type: expectedType(nonBreaking, risky), }), ])) }) @@ -853,7 +892,7 @@ export function runGeneralSchemaTests( expect.objectContaining({ action: DiffAction.remove, beforeDeclarationPaths: [[...commonPath, 'maxProperties']], - type: expectedType(nonBreaking, breaking), + type: expectedType(nonBreaking, risky), }), ])) }) @@ -876,7 +915,7 @@ export function runGeneralSchemaTests( afterValue: 'string', beforeDeclarationPaths: TEST_DEFAULTS_DECLARATION_PATHS, afterDeclarationPaths: [[...commonPath, 'additionalProperties', 'type']], - type: expectedType(breaking, nonBreaking), + type: expectedType(breaking, risky), }), ]), ) @@ -1026,7 +1065,7 @@ export function runGeneralSchemaTests( expect.objectContaining({ action: DiffAction.remove, beforeDeclarationPaths: [[...commonPath, 'allOf', 2, 'properties', 'prop3']], - type: expectedType(breaking, nonBreaking), + type: nonBreaking, }), ])) }) @@ -1037,7 +1076,7 @@ export function runGeneralSchemaTests( expect.objectContaining({ action: DiffAction.remove, beforeDeclarationPaths: [[...commonPath, 'allOf', 1, 'properties', 'prop2']], - type: expectedType(breaking, nonBreaking), + type: nonBreaking, }), ])) }) diff --git a/test/compatibility-suites/schemas/schema-test-runner-openapi-only.ts b/test/compatibility-suites/schemas/schema-test-runner-openapi-only.ts index 71fc400..5f88097 100644 --- a/test/compatibility-suites/schemas/schema-test-runner-openapi-only.ts +++ b/test/compatibility-suites/schemas/schema-test-runner-openapi-only.ts @@ -333,13 +333,13 @@ export function runOpenApiOnlySchemaTests( action: DiffAction.replace, beforeDeclarationPaths: [[...commonPath, 'properties', 'option1', 'exclusiveMinimum']], afterDeclarationPaths: TEST_DEFAULTS_DECLARATION_PATHS, - type: expectedType(nonBreaking, breaking), + type: expectedType(nonBreaking, risky), }), expect.objectContaining({ action: DiffAction.replace, beforeDeclarationPaths: [[...commonPath, 'properties', 'option2', 'exclusiveMinimum']], afterDeclarationPaths: [[...commonPath, 'properties', 'option2', 'exclusiveMinimum']], - type: expectedType(nonBreaking, breaking), + type: expectedType(nonBreaking, risky), }), ])) }) @@ -367,13 +367,13 @@ export function runOpenApiOnlySchemaTests( action: DiffAction.replace, beforeDeclarationPaths: [[...commonPath, 'properties', 'option1', 'exclusiveMaximum']], afterDeclarationPaths: TEST_DEFAULTS_DECLARATION_PATHS, - type: expectedType(nonBreaking, breaking), + type: expectedType(nonBreaking, risky), }), expect.objectContaining({ action: DiffAction.replace, beforeDeclarationPaths: [[...commonPath, 'properties', 'option2', 'exclusiveMaximum']], afterDeclarationPaths: [[...commonPath, 'properties', 'option2', 'exclusiveMaximum']], - type: expectedType(nonBreaking, breaking), + type: expectedType(nonBreaking, risky), }), ])) }) @@ -455,7 +455,7 @@ export function runOpenApiOnlySchemaTests( action: DiffAction.replace, beforeDeclarationPaths: [[...commonPath, 'maxLength']], afterDeclarationPaths: [[...COMPONENTS_SCHEMAS, 'Color', 'maxLength']], - type: expectedType(nonBreaking, breaking), + type: expectedType(nonBreaking, risky), }), ])) },