Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
100 commits
Select commit Hold shift + click to select a range
fd56d94
feat: add classify rule id rules to separate diff identification and …
b41ex Apr 3, 2026
43fed2a
feat: add classify rule id calculation for some of JSON schema rules
b41ex Apr 3, 2026
2016371
feat: add classify rule id calculation for some of OpenAPI schema rules
b41ex Apr 3, 2026
9b65ba9
docs: add Cursor instructions for writing classification ruleId rules…
b41ex Apr 3, 2026
f2c0388
feat: provide effectiveBwcScope in diff
b41ex Apr 3, 2026
5841dba
refactor: provide an ability to load rules specified in yaml file
b41ex Apr 3, 2026
61308f9
feat: introduce ability to specify diff classification via matching r…
b41ex Apr 3, 2026
4f09514
feat(classify): add ClassifyRuleIdRule for maxClassifier
b41ex Apr 3, 2026
22b8f9a
feat(classify): add ClassifyRuleIdRule for minClassifier
b41ex Apr 3, 2026
881ab7e
feat(classify): add ClassifyRuleIdRule for minimumClassifier
b41ex Apr 3, 2026
1f495fa
feat(classify): add ClassifyRuleIdRule for maximumClassifier
b41ex Apr 3, 2026
909c6cf
feat(classify): add ClassifyRuleIdRule for exclusiveClassifier
b41ex Apr 3, 2026
1af8c61
feat(classify): add ClassifyRuleIdRule for multipleOfClassifier
b41ex Apr 3, 2026
e3c8e6d
feat(classify): add ClassifyRuleIdRule for additionalPropertiesClassi…
b41ex Apr 3, 2026
76bcbde
feat(classify): add ClassifyRuleIdRule for paramClassifyRule
b41ex Apr 3, 2026
7ba5278
feat(classify): add ClassifyRuleIdRule for parameterExplodeClassifyRule
b41ex Apr 3, 2026
f410203
feat(classify): add ClassifyRuleIdRule for parameterAllowReservedClas…
b41ex Apr 3, 2026
51f6864
feat(classify): add ClassifyRuleIdRule for parameterNameClassifyRule
b41ex Apr 3, 2026
9a6b963
feat(classify): add ClassifyRuleIdRule for parameterRequiredClassifyRule
b41ex Apr 3, 2026
c9435ab
feat(classify): add ClassifyRuleIdRule for apihubAllowEmptyValueParam…
b41ex Apr 3, 2026
394e5b8
feat(classify): add ClassifyRuleIdRule for apihubParametersRemovalCla…
b41ex Apr 3, 2026
88a9239
feat(classify): add ClassifyRuleIdRule for globalSecurityClassifyRule
b41ex Apr 3, 2026
039dcb6
feat(classify): add ClassifyRuleIdRule for globalSecurityItemClassify…
b41ex Apr 3, 2026
57f894c
feat(classify): add ClassifyRuleIdRule for pathChangeClassifyRule
b41ex Apr 3, 2026
f118ef1
feat(classifyRuleId): add ClassifyRuleIdRule for JSON Schema pattern …
b41ex Apr 4, 2026
9118263
feat(classifyRuleId): add ClassifyRuleIdRule for JSON Schema format k…
b41ex Apr 4, 2026
b8217cc
feat(classifyRuleId): add ClassifyRuleIdRule for JSON Schema uniqueIt…
b41ex Apr 3, 2026
1b53038
feat(classifyRuleId): add ClassifyRuleIdRule for JSON Schema readOnly…
b41ex Apr 3, 2026
a3d53b0
feat(classifyRuleId): add ClassifyRuleIdRule for JSON Schema default …
b41ex Apr 4, 2026
642b134
feat(classifyRuleId): add ClassifyRuleIdRule for JSON Schema const ke…
b41ex Apr 4, 2026
2039534
feat(classifyRuleId): add ClassifyRuleIdRule for JSON Schema enum arr…
b41ex Apr 3, 2026
aca1695
feat(classifyRuleId): add ClassifyRuleIdRule for JSON Schema oneOf, a…
b41ex Apr 3, 2026
a0c6cb4
feat(classifyRuleId): add ClassifyRuleIdRule for JSON Schema addition…
b41ex Apr 4, 2026
2f6344d
feat(classifyRuleId): add ClassifyRuleIdRule for JSON Schema patternP…
b41ex Apr 4, 2026
4f3e683
feat(classifyRuleId): add ClassifyRuleIdRule for JSON Schema property…
b41ex Apr 4, 2026
2298336
feat(classifyRuleId): add ClassifyRuleIdRule for JSON Schema definiti…
b41ex Apr 4, 2026
167bbc5
feat(classifyRuleId): add ClassifyRuleIdRule for JSON Schema $defs items
b41ex Apr 4, 2026
70bb86c
feat(classifyRuleId): add ClassifyRuleId for JSON Schema /examples node
b41ex Apr 4, 2026
b0e41a0
feat(classifyRuleId): add ClassifyRuleId for JSON Schema examples arr…
b41ex Apr 4, 2026
c6e0a3c
feat(classifyRuleId): add ClassifyRuleId for JSON Schema items tuple …
b41ex Apr 4, 2026
5787257
feat(classifyRuleId): add ClassifyRuleId for JSON Schema items single…
b41ex Apr 4, 2026
21accc7
feat(classifyRuleId): OpenAPI parameter in property classifyRuleId
b41ex Apr 4, 2026
8bea942
feat(classifyRuleId): OpenAPI parameter schema node classifyRuleId
b41ex Apr 4, 2026
9fec07b
feat(classifyRuleId): OpenAPI parameter style property classifyRuleId
b41ex Apr 4, 2026
656d459
feat(classifyRuleId): OpenAPI headers container classifyRuleId
b41ex Apr 4, 2026
384b131
feat(classifyRuleId): OpenAPI individual header classifyRuleId
b41ex Apr 4, 2026
f2537b0
feat(classifyRuleId): OpenAPI header required property classifyRuleIds
b41ex Apr 4, 2026
ed23795
feat(classifyRuleId): OpenAPI header schema node classifyRuleId
b41ex Apr 4, 2026
27dfe8f
feat(classifyRuleId): OpenAPI encoding object and sub-properties clas…
b41ex Apr 4, 2026
c061502
feat(classifyRuleId): OpenAPI content object classifyRuleId
b41ex Apr 4, 2026
2e8cb04
feat(classifyRuleId): OpenAPI media type entry classifyRuleId
b41ex Apr 4, 2026
dbd185c
feat(classifyRuleId): OpenAPI media type schema classifyRuleId
b41ex Apr 4, 2026
505c4e9
feat(classifyRuleId): OpenAPI request body object classifyRuleId
b41ex Apr 4, 2026
af721d6
feat(classifyRuleId): OpenAPI request body required classifyRuleIds
b41ex Apr 4, 2026
cf3c933
feat(classifyRuleId): OpenAPI response entry classifyRuleIds and x-* …
b41ex Apr 4, 2026
511365b
feat(classifyRuleId): OpenAPI operation object classifyRuleIds and no…
b41ex Apr 4, 2026
4c31b7c
feat(classifyRuleId): OpenAPI operation responses map classifyRuleId
b41ex Apr 4, 2026
ba88b0f
feat(classifyRuleId): OpenAPI operation security scope classifyRuleIds
b41ex Apr 4, 2026
35c4ce7
feat(classifyRuleId): OpenAPI OAuth flow classifyRuleIds and non-flow…
b41ex Apr 4, 2026
8138786
feat(classifyRuleId): OpenAPI OAuth flows object classifyRuleId
b41ex Apr 4, 2026
a6689e9
feat(classifyRuleId): OpenAPI path item parameters array classifyRuleId
b41ex Apr 4, 2026
9550ea9
feat(classifyRuleId): OpenAPI components object classifyRuleId
b41ex Apr 4, 2026
9929d53
feat(classifyRuleId): OpenAPI components sub-collection maps classify…
b41ex Apr 4, 2026
bae9a50
feat(classifyRuleId): OpenAPI security schemes and scheme property cl…
b41ex Apr 4, 2026
e5a9c31
feat(classifyRuleId): OpenAPI paths map classifyRuleId
b41ex Apr 4, 2026
014c317
feat(classifyRuleId): OpenAPI tags array classifyRuleId
b41ex Apr 4, 2026
5f0c789
feat(classifyRuleId): OpenAPI parameter deprecated property
b41ex Apr 4, 2026
f0d1d67
feat(classifyRuleId): OpenAPI parameter description property
b41ex Apr 4, 2026
907b45f
feat(classifyRuleId): OpenAPI parameter example property
b41ex Apr 4, 2026
2530945
feat(classifyRuleId): OpenAPI parameter example nested item (/**)
b41ex Apr 4, 2026
5a38e02
feat(classifyRuleId): OpenAPI header deprecated property
b41ex Apr 4, 2026
c98b168
feat(classifyRuleId): OpenAPI header description property
b41ex Apr 4, 2026
1fb3eb7
feat(classifyRuleId): OpenAPI header allowEmptyValue property
b41ex Apr 4, 2026
c955158
feat(classifyRuleId): OpenAPI header allowReserved property
b41ex Apr 4, 2026
f022e88
feat(classifyRuleId): OpenAPI header example property
b41ex Apr 4, 2026
6ba37b0
feat(classifyRuleId): OpenAPI header example nested item (/**)
b41ex Apr 4, 2026
75320b6
feat(classifyRuleId): OpenAPI header explode property
b41ex Apr 4, 2026
37552a6
feat(classifyRuleId): OpenAPI header style property
b41ex Apr 4, 2026
0993a78
feat(classifyRuleId): OpenAPI operation deprecated property
b41ex Apr 4, 2026
19fce29
feat(classifyRuleId): OpenAPI media type example property
b41ex Apr 4, 2026
c0ff6d7
feat(classifyRuleId): OpenAPI media type example nested item (/**)
b41ex Apr 4, 2026
c8012d5
feat(classifyRuleId): OpenAPI request body description property
b41ex Apr 4, 2026
5d962c1
feat(classifyRuleId): JSON Schema deprecated keyword
b41ex Apr 4, 2026
389a240
feat(classifyRuleId): OpenAPI examples map root
b41ex Apr 4, 2026
2aff5e7
feat(classifyRuleId): OpenAPI named Example object in examples map
b41ex Apr 4, 2026
18f91e7
feat(classifyRuleId): OpenAPI Example object description property
b41ex Apr 4, 2026
e17e11b
feat(classifyRuleId): OpenAPI Example object externalValue property
b41ex Apr 4, 2026
0af4990
feat(classifyRuleId): OpenAPI Example object summary property
b41ex Apr 4, 2026
5accfe9
feat(classifyRuleId): OpenAPI Example object value property
b41ex Apr 4, 2026
c74335e
feat(classifyRuleId): OpenAPI Example value nested content (/**)
b41ex Apr 4, 2026
8e3ca69
feat(classifyRuleId): OpenAPI examples map deep paths (/**)
b41ex Apr 4, 2026
79e3ca6
feat(classifyRuleId): OpenAPI External Documentation object root
b41ex Apr 4, 2026
5e422c6
feat(classifyRuleId): OpenAPI External Documentation object fields
b41ex Apr 4, 2026
ce586f3
feat(classifyRuleId): OpenAPI response description property
b41ex Apr 4, 2026
9b3d8cb
feat(classifyRuleId): JSON Schema title keyword
b41ex Apr 4, 2026
07bce14
feat(classifyRuleId): JSON Schema description keyword
b41ex Apr 4, 2026
2abc588
feat(classifyRuleId): OpenAPI path item description property
b41ex Apr 4, 2026
9f95f0d
feat(classifyRuleId): OpenAPI path item summary property
b41ex Apr 4, 2026
c23417e
feat(classifyRuleId): remaining OpenAPI document, servers, operation …
b41ex Apr 4, 2026
a89c8cd
refactor: introduce separate ruleIds for separate max* JSON schema ke…
b41ex Apr 10, 2026
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
102 changes: 102 additions & 0 deletions .cursor/rules/classify-rule-id.mdc
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
---
description: Conventions for ClassifyRuleIdRule definitions and ruleId constant naming
globs: src/**/*.classify*.ts
alwaysApply: false
---

# ClassifyRuleIdRule Authoring Conventions

## Purpose

`ClassifyRuleIdRule` attaches a stable string identifier (`classifyRuleId`) to each diff so a `diffClassifier` can reclassify it via a simple table lookup, without re-implementing the original condition logic.

## Type Forms

`ClassifyRuleIdRule` is one of:

- **Single element** – same ruleId for all actions:
```ts
export const requiredItemClassifyRuleIdRule: ClassifyRuleIdRule =
({ after }) => (requiredItemHasPropertyDefault(after)
? JSON_SCHEMA_CLASSIFY_RULE_IDS.REQUIRED_ITEM_AFTER_PROPERTY_HAS_DEFAULT
: JSON_SCHEMA_CLASSIFY_RULE_IDS.REQUIRED_ITEM_AFTER_PROPERTY_HAS_NO_DEFAULT)
```

- **3-tuple `[add, remove, replace]`** – when add/remove/replace have different conditions or inspect different sides:
```ts
export const enumItemClassifyRuleIdRule: ClassifyRuleIdRule = [
({ before }) => (isNotEmptyArray(before.parent)
? JSON_SCHEMA_CLASSIFY_RULE_IDS.ENUM_ITEM_BEFORE_ENUM_NON_EMPTY
: JSON_SCHEMA_CLASSIFY_RULE_IDS.ENUM_ITEM_BEFORE_ENUM_EMPTY),
({ after }) => (isNotEmptyArray(after.parent)
? JSON_SCHEMA_CLASSIFY_RULE_IDS.ENUM_ITEM_AFTER_ENUM_NON_EMPTY
: JSON_SCHEMA_CLASSIFY_RULE_IDS.ENUM_ITEM_AFTER_ENUM_EMPTY),
JSON_SCHEMA_CLASSIFY_RULE_IDS.ENUM_ITEM, // replace: static fallback
]
```

## Side Selection in Resolvers

| Tuple position | Inspect side | Rationale |
|---|---|---|
| add (index 0) | `after` | Added state is in `after` |
| remove (index 1) | `before` | Removed state is in `before` |
| replace (index 2) | `after` (or both) | New state or comparison between sides |
| array-item add | `before.parent` | State of the sibling array before adding |
| array-item remove | `after.parent` | State of the sibling array after removing |

## Placement & Structure

- Place `ClassifyRuleIdRule` **directly before** its corresponding `ClassifyRule`.
- Extract shared boolean helpers between the two (e.g., `propertyHasDefault`, `propertyIsRequired`).
- Place helpers immediately before the `ClassifyRuleIdRule`.
- Both live in the same `*.classify.ts` file; constants go in the matching `*.classify.ruleIds.ts`.

## Do NOT Encode Action in the ruleId String

`diff.action` is already available on the diff object. Encode the **condition**, not the action.

```ts
// ❌ encodes action
REQUIRED_ITEM_ADD_NO_DEFAULT: 'json-schema.required.item.add-no-default'

// ✅ describes condition from relevant side
REQUIRED_ITEM_AFTER_PROPERTY_HAS_NO_DEFAULT: 'json-schema.required.item.after-property-has-no-default'
```

Exception: static constants for structurally unique cases are fine (`TYPE_ADD`, `TYPE_REMOVE`).

## Naming Conventions

**OpenAPI / REST** (`REST_CLASSIFY_RULE_IDS` in `openapi3.classify.ruleIds.ts`):
```
rest.{object}.{property}[.item].{details}
```
Example: `rest.operation.security.item.after-security-requirements-are-subset-of-before`

**JSON Schema** (`JSON_SCHEMA_CLASSIFY_RULE_IDS` in `jsonSchema.classify.ruleIds.ts`):
```
json-schema.{keyword}[.{sub-property}][.item].{details}
```
Example: `json-schema.required.item.after-property-has-no-default`

`details` rules:
- Use `before-{condition}` when describing the before-state (remove context).
- Use `after-{condition}` when describing the after-state (add/replace context).
- Use a plain condition phrase when action-independent (e.g., `incompatible`, `subset-of-before`).
- Keep it in kebab-case and human-readable.

## JSDoc for Each Constant

Every constant must have a JSDoc comment stating:
1. What structural condition it represents.
2. The default classification behaviour.

```ts
/**
* JSON Schema `required` array item was added and the corresponding property
* in the after object has no `default` value.
* By default, adding it to `required` is breaking.
*/
REQUIRED_ITEM_AFTER_PROPERTY_HAS_NO_DEFAULT: 'json-schema.required.item.after-property-has-no-default',
```
1 change: 1 addition & 0 deletions jest.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ module.exports = {
testTimeout: 100000,
transform: {
'^.+\\.tsx?$': 'ts-jest',
'^.+\\.ya?ml$': '<rootDir>/test/transforms/yaml-transform.cjs',
},
transformIgnorePatterns: [
'<rootDir>/node_modules/',
Expand Down
53 changes: 50 additions & 3 deletions src/api.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { COMPARE_MODE_DEFAULT, COMPARE_SCOPE_ROOT, CompareEngine, CompareOptions, CompareResult } from './types'
import { COMPARE_MODE_DEFAULT, COMPARE_SCOPE_ROOT, CompareEngine, CompareOptions, CompareResult, Diff, DiffClassifier, DiffType } from './types'
import { compareJsonSchema } from './jsonSchema'
import { compareGraphApi } from './graphapi'
import { compareAsyncApi } from './asyncapi'
Expand All @@ -17,6 +17,10 @@ import {
OpenApiSpecVersion,
} from '@netcracker/qubership-apihub-api-unifier'
import { DEFAULT_NORMALIZED_RESULT, DEFAULT_OPTION_DEFAULTS_META_KEY, DEFAULT_OPTION_ORIGINS_META_KEY, DIFF_META_KEY } from './core'
import { matchingDiffClassifier, MatchingRule } from './core'
import openapi3Rules from './openapi/openapi3.classify.rules.yaml'
import jsonSchemaRules from './jsonSchema/jsonSchema.classify.rules.yaml'
import generalRules from './core/general.classify.rules.yaml'

function isOpenApiSpecVersion(specType: SpecType): specType is OpenApiSpecVersion {
return specType === SPEC_TYPE_OPEN_API_30 || specType === SPEC_TYPE_OPEN_API_31
Expand Down Expand Up @@ -54,15 +58,27 @@ export const COMPARE_ENGINES_MAP: Record<SpecType, CompareEngine> = {
[SPEC_TYPE_GRAPH_API]: compareGraphApi,
}

function buildOobClassifier(specType: SpecType): DiffClassifier | undefined {
if (isOpenApiSpecVersion(specType)) {
return matchingDiffClassifier([
...(jsonSchemaRules as MatchingRule[]),
...(openapi3Rules as MatchingRule[]),
...(generalRules as MatchingRule[]),
])
}
return undefined
}

// Wrapper function. Use it!
export function apiDiff(before: unknown, after: unknown, options: CompareOptions = {}): CompareResult {
const beforeSpec = resolveSpec(before)
const afterSpec = resolveSpec(after)
if (!areSpecTypesCompatible(beforeSpec.type, afterSpec.type)) {
throw new Error(`Specification cannot be different. Got ${beforeSpec.type} and ${afterSpec.type}`)
}
const engine = COMPARE_ENGINES_MAP[selectEngineSpecType(beforeSpec.type, afterSpec.type)]
return engine(before, after, {
const engineSpecType = selectEngineSpecType(beforeSpec.type, afterSpec.type)
const engine = COMPARE_ENGINES_MAP[engineSpecType]
const result = engine(before, after, {
mode: COMPARE_MODE_DEFAULT,
normalizedResult: DEFAULT_NORMALIZED_RESULT,
metaKey: DIFF_META_KEY,
Expand All @@ -75,4 +91,35 @@ export function apiDiff(before: unknown, after: unknown, options: CompareOptions
createdMergedJso: new Set(),
...options,
})

const oobClassifier = buildOobClassifier(engineSpecType)

if (oobClassifier || options.diffClassifier) {
for (const diff of result.diffs) {
const preType = diff.type

if (oobClassifier) {
const oobResult = oobClassifier(diff)
if (oobResult?.type !== undefined) {
//TODO this a a part of validation harness for transition period
// from classify rules to separate diff identification and classification
if (oobResult.type !== preType) {
throw new Error(
`OOB classifier type mismatch for classifyRuleId '${diff.classifyRuleId}': engine='${preType}', oob='${oobResult.type}'`,
)
}
diff.type = oobResult.type
}
}

if (options.diffClassifier) {
const userResult = options.diffClassifier(diff)
if (userResult?.type !== undefined) {
diff.type = userResult.type
}
}
}
}

return result
}
8 changes: 6 additions & 2 deletions src/core/diff.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,12 +22,16 @@ export const NEVER_KEY = Symbol('never-key')

export const createDiff = <D extends Diff>(diff: Omit<D, 'type'>, ctx: CompareContext): D => {
const classifierRule = ctx.rules?.$ ?? {}//todo. rules should be evaluated, like in json-crawl
const mutableDiffCopy = { ...diff, type: unclassified } as D
const index = diff.action === DiffAction.rename ? 2 : [DiffAction.add, DiffAction.remove, DiffAction.replace].indexOf(diff.action)

const classifyRuleIdDef = ctx.rules?.classifyRuleId
const classifyRuleIdElement = Array.isArray(classifyRuleIdDef) ? classifyRuleIdDef[index] : classifyRuleIdDef
const classifyRuleId = isFunc(classifyRuleIdElement) ? classifyRuleIdElement(ctx) : classifyRuleIdElement
const mutableDiffCopy = { ...diff, type: unclassified, classifyRuleId, effectiveBwcScope: ctx.apiCompatibilityScope } as D

if (classifierRule) {
const classifier = Array.isArray(classifierRule) ? classifierRule : allUnclassified

const index = diff.action === DiffAction.rename ? 2 : [DiffAction.add, DiffAction.remove, DiffAction.replace].indexOf(diff.action)
const changeType = classifier[index]

try {
Expand Down
20 changes: 20 additions & 0 deletions src/core/general.classify.rules.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
# General matching rules that apply across all classifyRuleIds.
#
# Rules use classifyRuleId: '*' (WILDCARD_CLASSIFY_RULE_ID) to match any ruleId.
# Wildcard rules are collected into a separate list inside matchingDiffClassifier
# and applied after all specific (per-ruleId) rules have been processed.
#
# The `type` filter in a wildcard rule is checked against the accumulated result
# type from preceding specific rules, not the engine's final diff type. This
# allows the wildcard to react to the type established by specific rules.
#
# This rule replicates the engine's reclassifyBreakingToRisky logic:
# any diff whose path is marked as NOT_BACKWARD_COMPATIBLE and whose
# accumulated classification is 'breaking' is promoted to 'risky'.

- match:
classifyRuleId: "*"
effectiveBwcScope: NOT_BACKWARD_COMPATIBLE
type: breaking
set:
type: risky
1 change: 1 addition & 0 deletions src/core/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,4 @@ export * from './mapping'
export * from './diff'
export * from './rules'
export * from './description'
export * from './matchingDiffClassifier'
113 changes: 113 additions & 0 deletions src/core/matchingDiffClassifier.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
import { ActionType, ApiCompatibilityKind, CompareScope, Diff, DiffClassifier, DiffClassifierResult, DiffType } from '../types'

/** Use this as `classifyRuleId` in a matching rule to match any ruleId. */
export const WILDCARD_CLASSIFY_RULE_ID = '*'

export type MatchingRuleMatch = {
/**
* The classifyRuleId to match. Use WILDCARD_CLASSIFY_RULE_ID ('*') to match any
* classifyRuleId. Wildcard rules are collected into a separate list and
* applied after all specific rules have been processed.
*/
classifyRuleId: string
action?: ActionType | ActionType[]
scope?: CompareScope | CompareScope[]
/**
* Matches against the accumulated result type from rules applied so far,
* falling back to the diff's original type when no preceding rule has
* matched yet. This lets wildcard rules react to the type set by specific
* rules rather than only the engine's final diff type.
*/
type?: DiffType | DiffType[]
effectiveBwcScope?: ApiCompatibilityKind | ApiCompatibilityKind[]
}

export type MatchingRule = {
match: MatchingRuleMatch
set: { type: DiffType }
}

function matchesValue<T>(value: T | undefined, filter: T | T[]): boolean {
return Array.isArray(filter) ? filter.includes(value as T) : value === filter
}

function ruleApplies(diff: Diff, match: MatchingRuleMatch, currentType?: DiffType): boolean {
if (match.action !== undefined && !matchesValue(diff.action as ActionType, match.action)) {
return false
}
if (match.scope !== undefined && !matchesValue(diff.scope, match.scope)) {
return false
}
if (match.type !== undefined && !matchesValue(currentType ?? diff.type, match.type)) {
return false
}
if (
match.effectiveBwcScope !== undefined &&
!matchesValue(diff.effectiveBwcScope, match.effectiveBwcScope as ApiCompatibilityKind | ApiCompatibilityKind[])
) {
return false
}
return true
}

/**
* Builds a DiffClassifier from an ordered array of matching rules.
*
* Rules are grouped by classifyRuleId. Rules whose classifyRuleId is
* WILDCARD_CLASSIFY_RULE_ID ('*') are collected into a separate wildcard list and
* applied after all specific rules for the diff's classifyRuleId.
*
* Rules are applied in direct (array) order; the LAST matching rule wins.
* Place a more-general rule first and the more-specific (overriding) rule
* after it so the specific rule takes precedence.
*
* The `type` filter checks the accumulated result type from preceding rules,
* falling back to the diff's original type. This allows wildcard rules to
* react to the type established by specific rules.
*/
export function matchingDiffClassifier(rules: MatchingRule[]): DiffClassifier {
const ruleMap = new Map<string, MatchingRule[]>()
const wildcardRules: MatchingRule[] = []

for (const rule of rules) {
const id = rule.match.classifyRuleId
if (id === WILDCARD_CLASSIFY_RULE_ID) {
wildcardRules.push(rule)
} else {
let group = ruleMap.get(id)
if (!group) {
group = []
ruleMap.set(id, group)
}
group.push(rule)
}
}

return (diff: Diff): DiffClassifierResult | undefined => {
if (!diff.classifyRuleId) {
return undefined
}
const candidates = ruleMap.get(diff.classifyRuleId)
if (!candidates && wildcardRules.length === 0) {
return undefined
}

let result: DiffClassifierResult | undefined = undefined

// Specific rules applied in direct order; last match wins.
for (const candidate of candidates ?? []) {
if (ruleApplies(diff, candidate.match, result?.type)) {
result = { type: candidate.set.type }
}
}

// Wildcard rules applied in direct order after all specific rules.
for (const candidate of wildcardRules) {
if (ruleApplies(diff, candidate.match, result?.type)) {
result = { type: candidate.set.type }
}
}

return result
}
}
10 changes: 10 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,16 @@ export type {
DiffReplace,
DiffRename,
DiffMetaRecord,
DiffClassifier,
DiffClassifierResult,
ClassifyRuleIdResolver,
ClassifyRuleIdElement,
ClassifyRuleIdRule,
} from './types'

export { matchingDiffClassifier, WILDCARD_CLASSIFY_RULE_ID } from './core'
export type { MatchingRule, MatchingRuleMatch } from './core'

export {
isDiffAdd,
isDiffRemove,
Expand All @@ -42,3 +50,5 @@ export {
onlyExistedArrayIndexes
} from './utils'

export { REST_CLASSIFY_RULE_IDS } from './openapi'
export { JSON_SCHEMA_CLASSIFY_RULE_IDS } from './jsonSchema'
1 change: 1 addition & 0 deletions src/jsonSchema/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ export * from './jsonSchema.compare'
export * from './jsonSchema.resolver'
export * from './jsonSchema.classify'
export * from './jsonSchema.mapping'
export * from './jsonSchema.classify.ruleIds'
export * from './jsonSchema.types'
export * from './jsonSchema.adapter'
export * from './jsonSchema.utils'
Expand Down
Loading
Loading