|
15 | 15 | */ |
16 | 16 |
|
17 | 17 | import { RestOperationData, VersionRestOperation } from './rest.types' |
18 | | -import { areDeprecatedOriginsNotEmpty, isOperationRemove, removeComponents } from '../../utils' |
| 18 | +import { |
| 19 | + areDeprecatedOriginsNotEmpty, |
| 20 | + IGNORE_PATH_PARAM_UNIFIED_PLACEHOLDER, |
| 21 | + isEmpty, |
| 22 | + isOperationRemove, |
| 23 | + normalizePath, |
| 24 | + removeComponents, |
| 25 | + removeFirstSlash, |
| 26 | + slugify, |
| 27 | +} from '../../utils' |
19 | 28 | import { |
20 | 29 | apiDiff, |
21 | 30 | breaking, |
22 | 31 | COMPARE_MODE_OPERATION, |
| 32 | + DEFAULT_DIFFS_AGGREGATED_META_KEY, |
23 | 33 | Diff, |
| 34 | + DIFF_META_KEY, |
24 | 35 | DiffAction, |
25 | 36 | risky, |
26 | 37 | } from '@netcracker/qubership-apihub-api-diff' |
27 | 38 | import { MESSAGE_SEVERITY, NORMALIZE_OPTIONS, ORIGINS_SYMBOL } from '../../consts' |
28 | 39 | import { |
29 | 40 | BREAKING_CHANGE_TYPE, |
| 41 | + CompareContext, |
30 | 42 | CompareOperationsPairContext, |
| 43 | + NormalizedOperationId, |
| 44 | + OperationChanges, |
| 45 | + OperationsApiType, |
| 46 | + ResolvedOperation, |
| 47 | + ResolvedVersionDocument, |
31 | 48 | RISKY_CHANGE_TYPE, |
| 49 | + WithAggregatedDiffs, |
| 50 | + WithDiffMetaRecord, |
32 | 51 | } from '../../types' |
33 | 52 | import { isObject } from '@netcracker/qubership-apihub-json-crawl' |
34 | 53 | import { areDeclarationPathsEqual } from '../../utils/path' |
35 | | -import { JSON_SCHEMA_PROPERTY_DEPRECATED, pathItemToFullPath, resolveOrigins } from '@netcracker/qubership-apihub-api-unifier' |
| 54 | +import { |
| 55 | + JSON_SCHEMA_PROPERTY_DEPRECATED, |
| 56 | + pathItemToFullPath, |
| 57 | + resolveOrigins, |
| 58 | +} from '@netcracker/qubership-apihub-api-unifier' |
36 | 59 | import { findRequiredRemovedProperties } from './rest.required' |
37 | 60 | import { calculateObjectHash } from '../../utils/hashes' |
38 | 61 | import { REST_API_TYPE } from './rest.consts' |
| 62 | +import { OpenAPIV3 } from 'openapi-types' |
| 63 | +import { extractServersDiffs, getOperationBasePath } from './rest.utils' |
| 64 | +import { createOperationChange, getOperationTags, takeSubstringIf } from '../../components' |
| 65 | + |
| 66 | +export const compareDocuments = async (apiType: OperationsApiType, operationsMap: Record<NormalizedOperationId, { |
| 67 | + previous?: ResolvedOperation |
| 68 | + current?: ResolvedOperation |
| 69 | +}>, prevFile: File, currFile: File, currDoc: ResolvedVersionDocument, prevDoc: ResolvedVersionDocument, ctx: CompareContext): Promise<{ |
| 70 | + operationChanges: OperationChanges[] |
| 71 | + tags: string[] |
| 72 | +}> => { |
| 73 | + const prevDocData = JSON.parse(await prevFile.text()) |
| 74 | + const currDocData = JSON.parse(await currFile.text()) |
| 75 | + |
| 76 | + const { merged, diffs } = apiDiff( |
| 77 | + prevDocData, |
| 78 | + currDocData, |
| 79 | + { |
| 80 | + ...NORMALIZE_OPTIONS, |
| 81 | + metaKey: DIFF_META_KEY, |
| 82 | + originsFlag: ORIGINS_SYMBOL, |
| 83 | + diffsAggregatedFlag: DEFAULT_DIFFS_AGGREGATED_META_KEY, |
| 84 | + // mode: COMPARE_MODE_OPERATION, |
| 85 | + normalizedResult: true, |
| 86 | + }, |
| 87 | + ) as { merged: OpenAPIV3.Document; diffs: Diff[] } |
| 88 | + |
| 89 | + if (isEmpty(diffs)) { |
| 90 | + return { operationChanges: [], tags: [] } |
| 91 | + } |
| 92 | + |
| 93 | + // todo reclassify |
| 94 | + // const olnyBreaking = diffs.filter((diff) => diff.type === breaking) |
| 95 | + // if (olnyBreaking.length > 0 && previous?.operationId) { |
| 96 | + // await reclassifyBreakingChanges(previous.operationId, diffResult.merged, olnyBreaking, ctx) |
| 97 | + // } |
| 98 | + |
| 99 | + const { currentGroup, previousGroup } = ctx.config |
| 100 | + const currGroupSlug = slugify(removeFirstSlash(currentGroup || '')) |
| 101 | + const prevGroupSlug = slugify(removeFirstSlash(previousGroup || '')) |
| 102 | + |
| 103 | + const tags = new Set<string>() |
| 104 | + const changedOperations: OperationChanges[] = [] |
| 105 | + |
| 106 | + for (const path of Object.keys((merged as OpenAPIV3.Document).paths)) { |
| 107 | + const pathData = (merged as OpenAPIV3.Document).paths[path] |
| 108 | + if (typeof pathData !== 'object' || !pathData) { continue } |
| 109 | + |
| 110 | + for (const key of Object.keys(pathData)) { |
| 111 | + const inferredMethod = key as OpenAPIV3.HttpMethods |
| 112 | + |
| 113 | + // check if field is a valid openapi http method defined in OpenAPIV3.HttpMethods |
| 114 | + if (!Object.values(OpenAPIV3.HttpMethods).includes(inferredMethod)) { |
| 115 | + continue |
| 116 | + } |
| 117 | + |
| 118 | + const methodData = pathData[inferredMethod] |
| 119 | + const basePath = getOperationBasePath(methodData?.servers || pathData?.servers || merged.servers || []) |
| 120 | + const operationPath = basePath + path |
| 121 | + const operationId = slugify(`${removeFirstSlash(operationPath)}-${inferredMethod}`) |
| 122 | + const normalizedOperationId = slugify(`${normalizePath(basePath + path)}-${inferredMethod}`, [], IGNORE_PATH_PARAM_UNIFIED_PLACEHOLDER) |
| 123 | + // todo what's with prevslug? which tests affected? which slug to slice prev or curr? |
| 124 | + const qwe = takeSubstringIf(!!currGroupSlug, normalizedOperationId, currGroupSlug.length) |
| 125 | + |
| 126 | + const { current, previous } = operationsMap[qwe] ?? operationsMap[operationId] ?? {} |
| 127 | + |
| 128 | + let operationDiffs: Diff[] = [] |
| 129 | + if (current && previous) { |
| 130 | + operationDiffs = [ |
| 131 | + ...(methodData as WithAggregatedDiffs<OpenAPIV3.OperationObject>)[DEFAULT_DIFFS_AGGREGATED_META_KEY], |
| 132 | + // todo what about security? add test |
| 133 | + ...extractServersDiffs(merged), |
| 134 | + ] |
| 135 | + |
| 136 | + const pathParamRenameDiff = (merged.paths as WithDiffMetaRecord<OpenAPIV3.PathsObject>)[DIFF_META_KEY]?.[path] |
| 137 | + pathParamRenameDiff && operationDiffs.push(pathParamRenameDiff) |
| 138 | + } else if (current || previous) { |
| 139 | + const operationDiff = (merged.paths as WithDiffMetaRecord<OpenAPIV3.PathsObject>)[DIFF_META_KEY]?.[path] |
| 140 | + if (!operationDiff) { |
| 141 | + throw new Error('should not happen') |
| 142 | + } |
| 143 | + operationDiffs.push(operationDiff) |
| 144 | + } |
| 145 | + |
| 146 | + if (isEmpty(operationDiffs)) { |
| 147 | + continue |
| 148 | + } |
| 149 | + |
| 150 | + // todo operationDiffs can be [undefined] in 'type error must not appear during build' |
| 151 | + changedOperations.push(createOperationChange(apiType, operationDiffs, previous, current, currGroupSlug, prevGroupSlug, currentGroup, previousGroup)) |
| 152 | + getOperationTags(current ?? previous).forEach(tag => tags.add(tag)) |
| 153 | + } |
| 154 | + } |
| 155 | + |
| 156 | + return { operationChanges: changedOperations, tags: [...tags.values()] } |
| 157 | +} |
39 | 158 |
|
| 159 | +/** @deprecated */ |
40 | 160 | export const compareRestOperationsData = async (current: VersionRestOperation | undefined, previous: VersionRestOperation | undefined, ctx: CompareOperationsPairContext): Promise<Diff[]> => { |
41 | 161 |
|
42 | 162 | let previousOperation = removeComponents(previous?.data) |
|
0 commit comments