Skip to content
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -27,13 +27,13 @@
"update-lock-file": "update-lock-file @netcracker"
},
"dependencies": {
"@netcracker/qubership-apihub-api-unifier": "dev",
"@netcracker/qubership-apihub-api-unifier": "feature-exclusive-min-max",
"@netcracker/qubership-apihub-json-crawl": "dev",
"fast-equals": "6.0.0"
},
"devDependencies": {
"@asyncapi/parser": "3.4.0",
"@netcracker/qubership-apihub-compatibility-suites": "dev",
"@netcracker/qubership-apihub-compatibility-suites": "feature-exclusive-min-max",
"@netcracker/qubership-apihub-graphapi": "dev",
"@netcracker/qubership-apihub-npm-gitflow": "3.1.1",
"@types/jest": "30.0.0",
Expand Down
26 changes: 15 additions & 11 deletions src/jsonSchema/jsonSchema.classify.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {
nonBreaking,
PARENT_JUMP,
risky,
riskyIf,
strictResolveValueFromContext,
unclassified,
} from '../core'
Expand Down Expand Up @@ -42,27 +43,30 @@ export const minClassifier: ClassifyRule = [
]

export const minimumClassifier: ClassifyRule = [
({ before, after }) => {
const beforeExclusiveMinimum = getKeyValue(before.parent, 'exclusiveMinimum')
return breakingIf(!isNumber(beforeExclusiveMinimum) || !isNumber(after.value) || beforeExclusiveMinimum < after.value)
},
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 maximumClassifier: ClassifyRule = [
({ before, after }) => {
const beforeExclusiveMaximum = getKeyValue(before.parent, 'exclusiveMaximum')
return breakingIf(!isNumber(beforeExclusiveMaximum) || !isNumber(after.value) || beforeExclusiveMaximum > after.value)
},
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 exclusiveClassifier: ClassifyRule = [
export const exclusiveBooleanClassifier: 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 }) => (after.value === false ? risky : nonBreaking),
]

//todo think about replace multipleOf in inverse case
Expand All @@ -88,7 +92,7 @@ export const requiredItemClassifyRule: ClassifyRule = [
export const propertyClassifyRule: ClassifyRule = [
({ after }) => (
!isExist(getKeyValue(after.value, 'default')) &&
getArrayValue((strictResolveValueFromContext(after, PARENT_JUMP, PARENT_JUMP, 'required')))?.includes(after.key) ? breaking : nonBreaking
getArrayValue((strictResolveValueFromContext(after, PARENT_JUMP, PARENT_JUMP, 'required')))?.includes(after.key) ? breaking : nonBreaking
),
breaking,
unclassified,
Expand All @@ -102,7 +106,7 @@ export const enumClassifyRule: ClassifyRule = [
({ after }) => (isNotEmptyArray(after.parent) ? breaking : nonBreaking),
breaking,
({ before }) => (isNotEmptyArray(before.parent) ? risky : nonBreaking),
({ after }) => (isNotEmptyArray(after.parent) ? nonBreaking: risky ),
({ after }) => (isNotEmptyArray(after.parent) ? nonBreaking : risky),
nonBreaking
]

Expand Down
214 changes: 214 additions & 0 deletions src/jsonSchema/jsonSchema.numeric-bounds.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,214 @@
import {
Comment thread
JayLim2 marked this conversation as resolved.
cleanOrigins,
copyOrigins,
JSON_SCHEMA_PROPERTY_MINIMUM,
JSON_SCHEMA_PROPERTY_MAXIMUM,
JSON_SCHEMA_PROPERTY_EXCLUSIVE_MINIMUM,
JSON_SCHEMA_PROPERTY_EXCLUSIVE_MAXIMUM,
JsonSchemaNumericValidationKeywordsType,
} from '@netcracker/qubership-apihub-api-unifier'
import { isNumber, isObject } from '../utils'
import { AdapterResolver, ClassifyRule } from '../types'
import { CompareContext, DiffType } from '../types'
import { breaking, nonBreaking, risky } from '../core'

const hasBooleanExclusiveBounds = (value: Record<PropertyKey, unknown>): boolean =>
typeof value[JSON_SCHEMA_PROPERTY_EXCLUSIVE_MINIMUM] === 'boolean' ||
typeof value[JSON_SCHEMA_PROPERTY_EXCLUSIVE_MAXIMUM] === 'boolean'

const adaptBooleanExclusiveBound = (
value: Record<PropertyKey, unknown>,
boundaryKey: typeof JSON_SCHEMA_PROPERTY_MINIMUM | typeof JSON_SCHEMA_PROPERTY_MAXIMUM,
exclusiveKey: typeof JSON_SCHEMA_PROPERTY_EXCLUSIVE_MINIMUM | typeof JSON_SCHEMA_PROPERTY_EXCLUSIVE_MAXIMUM,
Comment thread
makeev-pavel marked this conversation as resolved.
originsFlag: symbol,
): void => {
const exclusive = value[exclusiveKey]
if (typeof exclusive !== 'boolean') {
return
}

const boundary = value[boundaryKey]
Comment thread
JayLim2 marked this conversation as resolved.
if (exclusive && isNumber(boundary)) {
value[exclusiveKey] = boundary
copyOrigins(value, value, boundaryKey, exclusiveKey, originsFlag)
delete value[boundaryKey]
cleanOrigins(value, boundaryKey, originsFlag)
return
}

delete value[exclusiveKey]
cleanOrigins(value, exclusiveKey, originsFlag)
}

export const booleanExclusiveBoundsOas30to31Adapter: AdapterResolver = (value, _reference, valueContext) => {
if (!isObject(value) || !hasBooleanExclusiveBounds(value)) {
return value
}

const { originsFlag } = valueContext.options

return valueContext.transformer(value, 'boolean-exclusive-bounds-to-numeric', (current) => {
if (!isObject(current)) {
return current
}

const result = { ...current }
adaptBooleanExclusiveBound(result, JSON_SCHEMA_PROPERTY_MINIMUM, JSON_SCHEMA_PROPERTY_EXCLUSIVE_MINIMUM, originsFlag)
adaptBooleanExclusiveBound(result, JSON_SCHEMA_PROPERTY_MAXIMUM, JSON_SCHEMA_PROPERTY_EXCLUSIVE_MAXIMUM, originsFlag)
return result
})
}

type EffectiveBound = {
propertyName: JsonSchemaNumericValidationKeywordsType
value: number
exclusive: boolean
}

const getNumericProperty = (schema: unknown, property: string): number | undefined => {
if (!isObject(schema)) {
return undefined
}
const value = schema[property]
return isNumber(value) ? value : undefined
}

const getEffectiveLowerBound = (schema: unknown): EffectiveBound | undefined => {
const minimumValue = getNumericProperty(schema, JSON_SCHEMA_PROPERTY_MINIMUM)
const exclusiveMinimumValue = getNumericProperty(schema, JSON_SCHEMA_PROPERTY_EXCLUSIVE_MINIMUM)

if (minimumValue === undefined && exclusiveMinimumValue === undefined) {
return undefined
}
if (minimumValue === undefined) {
return {
propertyName: JSON_SCHEMA_PROPERTY_EXCLUSIVE_MINIMUM,
value: exclusiveMinimumValue!,
exclusive: true,
}
}
if (exclusiveMinimumValue === undefined) {
return {
propertyName: JSON_SCHEMA_PROPERTY_MINIMUM,
value: minimumValue,
exclusive: false,
}
}

if (exclusiveMinimumValue > minimumValue) {
return {
propertyName: JSON_SCHEMA_PROPERTY_EXCLUSIVE_MINIMUM,
value: exclusiveMinimumValue,
exclusive: true,
}
}
if (exclusiveMinimumValue < minimumValue) {
return {
propertyName: JSON_SCHEMA_PROPERTY_MINIMUM,
value: minimumValue,
exclusive: false,
}
}
return {
propertyName: JSON_SCHEMA_PROPERTY_MINIMUM,
value: minimumValue,
exclusive: true,
}
}
Comment thread
JayLim2 marked this conversation as resolved.

const getEffectiveUpperBound = (schema: unknown): EffectiveBound | undefined => {
const maximumValue = getNumericProperty(schema, JSON_SCHEMA_PROPERTY_MAXIMUM)
const exclusiveMaximumValue = getNumericProperty(schema, JSON_SCHEMA_PROPERTY_EXCLUSIVE_MAXIMUM)

if (maximumValue === undefined && exclusiveMaximumValue === undefined) {
return undefined
}
if (maximumValue === undefined) {
return {
propertyName: JSON_SCHEMA_PROPERTY_EXCLUSIVE_MAXIMUM,
value: exclusiveMaximumValue!,
exclusive: true,
}
}
if (exclusiveMaximumValue === undefined) {
return {
propertyName: JSON_SCHEMA_PROPERTY_MAXIMUM,
value: maximumValue,
exclusive: false,
}
}

if (exclusiveMaximumValue < maximumValue) {
return {
propertyName: JSON_SCHEMA_PROPERTY_EXCLUSIVE_MAXIMUM,
value: exclusiveMaximumValue,
exclusive: true,
}
}
if (exclusiveMaximumValue > maximumValue) {
return {
propertyName: JSON_SCHEMA_PROPERTY_MAXIMUM,
value: maximumValue,
exclusive: false,
}
}
return {
propertyName: JSON_SCHEMA_PROPERTY_MAXIMUM,
value: maximumValue,
exclusive: true,
}
}
Comment thread
JayLim2 marked this conversation as resolved.

const classifyByEffectiveBound = (
ctx: CompareContext,
getEffectiveBound: (schema: unknown) => EffectiveBound | undefined,
isAfterStricter: (before: EffectiveBound, after: EffectiveBound) => boolean,
stricterType: DiffType,
looserType: DiffType,
): DiffType => {
const beforeBound = getEffectiveBound(ctx.before.parent)
const afterBound = getEffectiveBound(ctx.after.parent)

if (!beforeBound) {
return stricterType
}
if (!afterBound) {
return looserType
}
if (beforeBound.value === afterBound.value && beforeBound.exclusive === afterBound.exclusive) {
return nonBreaking
}

return isAfterStricter(beforeBound, afterBound) ? stricterType : looserType
}

const isAfterLowerStricter = (before: EffectiveBound, after: EffectiveBound) =>
after.value > before.value || (after.value === before.value && after.exclusive && !before.exclusive)

const isAfterUpperStricter = (before: EffectiveBound, after: EffectiveBound) =>
after.value < before.value || (after.value === before.value && after.exclusive && !before.exclusive)

const createPropertyBoundClassifier = (
propertyName: JsonSchemaNumericValidationKeywordsType,
getEffectiveBound: (schema: unknown) => EffectiveBound | undefined,
isAfterStricter: (before: EffectiveBound, after: EffectiveBound) => boolean,
stricterType: DiffType,
looserType: DiffType,
) => (ctx: CompareContext): DiffType => {
const definingBound = getEffectiveBound(ctx.after.parent) ?? getEffectiveBound(ctx.before.parent)
if (!definingBound || definingBound.propertyName !== propertyName) return nonBreaking
return classifyByEffectiveBound(ctx, getEffectiveBound, isAfterStricter, stricterType, looserType)
}

export const createEffectiveLowerBoundClassifier = (propertyName: JsonSchemaNumericValidationKeywordsType): ClassifyRule => {
const requestClassifier = createPropertyBoundClassifier(propertyName, getEffectiveLowerBound, isAfterLowerStricter, breaking, nonBreaking)
const responseClassifier = createPropertyBoundClassifier(propertyName, getEffectiveLowerBound, isAfterLowerStricter, nonBreaking, risky)
return [requestClassifier, requestClassifier, requestClassifier, responseClassifier, responseClassifier, responseClassifier]
}

export const createEffectiveUpperBoundClassifier = (propertyName: JsonSchemaNumericValidationKeywordsType): ClassifyRule => {
const requestClassifier = createPropertyBoundClassifier(propertyName, getEffectiveUpperBound, isAfterUpperStricter, breaking, nonBreaking)
const responseClassifier = createPropertyBoundClassifier(propertyName, getEffectiveUpperBound, isAfterUpperStricter, nonBreaking, risky)
return [requestClassifier, requestClassifier, requestClassifier, responseClassifier, responseClassifier, responseClassifier]
}

42 changes: 32 additions & 10 deletions src/jsonSchema/jsonSchema.rules.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ import {
} from '../core'
import {
enumClassifyRule,
exclusiveClassifier,
exclusiveBooleanClassifier,
maxClassifier,
maximumClassifier,
minClassifier,
Expand All @@ -39,8 +39,16 @@ import { jsonSchemaMappingResolver } from './jsonSchema.mapping'
import { combinersCompareResolver } from './jsonSchema.resolver'
import { ClassifyRule, CompareRules, DescriptionTemplates } from '../types'
import { JsonSchemaRulesOptions, NativeAnySchemaFactory } from './jsonSchema.types'
import { normalize, SPEC_TYPE_JSON_SCHEMA_04 } from '@netcracker/qubership-apihub-api-unifier'
import {
JSON_SCHEMA_PROPERTY_EXCLUSIVE_MAXIMUM,
JSON_SCHEMA_PROPERTY_EXCLUSIVE_MINIMUM,
JSON_SCHEMA_PROPERTY_MAXIMUM,
JSON_SCHEMA_PROPERTY_MINIMUM,
normalize,
SPEC_TYPE_JSON_SCHEMA_04
} from '@netcracker/qubership-apihub-api-unifier'
import { isBoolean, isNumber, isString } from '../utils'
import { createEffectiveLowerBoundClassifier, createEffectiveUpperBoundClassifier } from './jsonSchema.numeric-bounds'

const simpleRule = (classify: ClassifyRule, descriptionTemplate: DescriptionTemplates) => ({
$: classify,
Expand Down Expand Up @@ -85,15 +93,29 @@ export const jsonSchemaRules = ({
'/type': simpleRule(typeClassifier, resolveSchemaDescriptionTemplates('type')),

'/multipleOf': simpleRule(multipleOfClassifier, resolveSchemaDescriptionTemplates('multipleOf validator')),
'/maximum': simpleRule(maximumClassifier, resolveSchemaDescriptionTemplates('maximum validator')),
'/minimum': simpleRule(minimumClassifier, resolveSchemaDescriptionTemplates('minimum validator')),
...version === SPEC_TYPE_JSON_SCHEMA_04 ? {
'/exclusiveMaximum': simpleRule(exclusiveClassifier, resolveSchemaDescriptionTemplates('exclusiveMaximum validator')),
'/exclusiveMinimum': simpleRule(exclusiveClassifier, resolveSchemaDescriptionTemplates('exclusiveMinimum validator')),
...(version === SPEC_TYPE_JSON_SCHEMA_04 ? {
'/maximum': simpleRule(maximumClassifier, resolveSchemaDescriptionTemplates('maximum validator')),
'/minimum': simpleRule(minimumClassifier, resolveSchemaDescriptionTemplates('minimum validator')),
'/exclusiveMaximum': simpleRule(exclusiveBooleanClassifier, resolveSchemaDescriptionTemplates('exclusiveMaximum validator')),
'/exclusiveMinimum': simpleRule(exclusiveBooleanClassifier, resolveSchemaDescriptionTemplates('exclusiveMinimum validator')),
} : {
'/exclusiveMaximum': simpleRule(maxClassifier, resolveSchemaDescriptionTemplates('exclusiveMaximum validator')),
'/exclusiveMinimum': simpleRule(minClassifier, resolveSchemaDescriptionTemplates('exclusiveMinimum validator')),
},
'/maximum': {
$: createEffectiveUpperBoundClassifier(JSON_SCHEMA_PROPERTY_MAXIMUM),
description: diffDescription(resolveSchemaDescriptionTemplates('maximum validator')),
},
'/minimum': {
$: createEffectiveLowerBoundClassifier(JSON_SCHEMA_PROPERTY_MINIMUM),
description: diffDescription(resolveSchemaDescriptionTemplates('minimum validator')),
},
'/exclusiveMaximum': {
$: createEffectiveUpperBoundClassifier(JSON_SCHEMA_PROPERTY_EXCLUSIVE_MAXIMUM),
description: diffDescription(resolveSchemaDescriptionTemplates('exclusiveMaximum validator')),
},
'/exclusiveMinimum': {
$: createEffectiveLowerBoundClassifier(JSON_SCHEMA_PROPERTY_EXCLUSIVE_MINIMUM),
description: diffDescription(resolveSchemaDescriptionTemplates('exclusiveMinimum validator')),
},
}),
'/maxLength': simpleRule(maxClassifier, resolveSchemaDescriptionTemplates('maxLength validator')),
'/minLength': simpleRule(minClassifier, resolveSchemaDescriptionTemplates('minLength validator')),
'/pattern': simpleRule([breaking, nonBreaking, breaking, nonBreaking, breaking, breaking], resolveSchemaDescriptionTemplates('pattern validator')),
Expand Down
10 changes: 9 additions & 1 deletion src/openapi/openapi3.schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ import {
import { schemaParamsCalculator } from './openapi3.description.schema'
import { openApiSpecificationExtensionRulesFunction } from './openapi3.compare.rules'
import { isArray, isObject } from '../utils'
import { booleanExclusiveBoundsOas30to31Adapter } from '../jsonSchema/jsonSchema.numeric-bounds'

const NULL_TYPE_COMBINERS = [JSON_SCHEMA_PROPERTY_ANY_OF, JSON_SCHEMA_PROPERTY_ONE_OF] as const
const SPEC_TYPE_TO_VERSION: Record<OpenApiSpecVersion, string> = {
Expand Down Expand Up @@ -209,7 +210,14 @@ export const openApiSchemaRules = (options: OpenApi3SchemaRulesOptions): Compare
const schemaRules = jsonSchemaRules({
additionalRules: {
adapter: [
...(options.version === SPEC_TYPE_OPEN_API_31 ? [jsonSchemaOas30to31Adapter(openApiJsonSchemaAnyFactory(options.version))] : []),
...(
options.version === SPEC_TYPE_OPEN_API_31
? [
booleanExclusiveBoundsOas30to31Adapter,
jsonSchemaOas30to31Adapter(openApiJsonSchemaAnyFactory(options.version))
]
: []
),
jsonSchemaAdapter(openApiJsonSchemaAnyFactory(options.version)),
],
descriptionParamCalculator: schemaParamsCalculator,
Expand Down
Loading
Loading