Skip to content

Commit 4e42de4

Browse files
authored
feat: fail invalid ref publication
1 parent 932e60f commit 4e42de4

29 files changed

+418
-21
lines changed

src/apitypes/rest/rest.document.ts

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -20,10 +20,9 @@ import { convertObj } from 'swagger2openapi'
2020
import { REST_DOCUMENT_TYPE, REST_KIND_KEY } from './rest.consts'
2121
import type { RestDocumentInfo } from './rest.types'
2222

23-
import type { DocumentBuilder, DocumentDumper } from '../../types'
24-
import { YAML_EXPORT_GROUP_FORMAT } from '../../types'
23+
import { DocumentBuilder, DocumentDumper, YAML_EXPORT_GROUP_FORMAT } from '../../types'
2524
import { FILE_FORMAT } from '../../consts'
26-
import { getBundledFileDataWithDependencies, getDocumentTitle } from '../../utils'
25+
import { createBundlingErrorHandler, getBundledFileDataWithDependencies, getDocumentTitle } from '../../utils'
2726
import YAML from 'js-yaml'
2827

2928
const openApiDocumentMeta = (data: OpenAPIV3.Document): RestDocumentInfo => {
@@ -53,7 +52,10 @@ const openApiDocumentMeta = (data: OpenAPIV3.Document): RestDocumentInfo => {
5352
export const buildRestDocument: DocumentBuilder<OpenAPIV3.Document> = async (parsedFile, file, ctx) => {
5453
const { fileId, slug = '', publish = true, apiKind, ...fileMetadata } = file
5554

56-
const { data, dependencies } = await getBundledFileDataWithDependencies(fileId, ctx.parsedFileResolver)
55+
const {
56+
data,
57+
dependencies,
58+
} = await getBundledFileDataWithDependencies(fileId, ctx.parsedFileResolver, createBundlingErrorHandler(ctx, fileId))
5759

5860
let bundledFileData = data
5961

src/apitypes/rest/rest.operations.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,8 @@
1717
import { OpenAPIV3 } from 'openapi-types'
1818

1919
import { buildRestOperation } from './rest.operation'
20-
import type { OperationsBuilder } from '../../types'
21-
import { removeComponents, removeFirstSlash, slugify } from '../../utils'
20+
import { OperationsBuilder } from '../../types'
21+
import { createBundlingErrorHandler, removeComponents, removeFirstSlash, slugify } from '../../utils'
2222
import { getOperationBasePath } from './rest.utils'
2323
import type * as TYPE from './rest.types'
2424
import { HASH_FLAG, INLINE_REFS_FLAG, MESSAGE_SEVERITY, NORMALIZE_OPTIONS, ORIGINS_SYMBOL } from '../../consts'
@@ -28,6 +28,7 @@ import { normalize } from '@netcracker/qubership-apihub-api-unifier'
2828

2929
export const buildRestOperations: OperationsBuilder<OpenAPIV3.Document> = async (document, ctx, debugCtx) => {
3030
const documentWithoutComponents = removeComponents(document.data)
31+
const bundlingErrorHandler = createBundlingErrorHandler(ctx, document.fileId)
3132

3233
const { effectiveDocument, refsOnlyDocument } = syncDebugPerformance('[NormalizeDocument]', () => {
3334
const effectiveDocument = normalize(
@@ -37,6 +38,8 @@ export const buildRestOperations: OperationsBuilder<OpenAPIV3.Document> = async
3738
originsFlag: ORIGINS_SYMBOL,
3839
hashFlag: HASH_FLAG,
3940
source: document.data,
41+
onRefResolveError: (_: string, __: PropertyKey[], ref: string) =>
42+
bundlingErrorHandler([`The $ref "${ref}" references an invalid location in the document.`]),
4043
},
4144
) as OpenAPIV3.Document
4245
const refsOnlyDocument = normalize(

src/apitypes/unknown/unknown.document.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
*/
1616

1717
import { BuildConfigFile, DocumentBuilder, DocumentDumper, File, VersionDocument } from '../../types'
18-
import { getBundledFileDataWithDependencies, getDocumentTitle } from '../../utils'
18+
import { createBundlingErrorHandler, getBundledFileDataWithDependencies, getDocumentTitle } from '../../utils'
1919
import { FILE_FORMAT } from '../../consts'
2020

2121
export const buildUnknownDocument: DocumentBuilder<string> = async (parsedFile, file, ctx) => {
@@ -33,7 +33,7 @@ export const buildUnknownDocument: DocumentBuilder<string> = async (parsedFile,
3333
const {
3434
data,
3535
dependencies: fileDependencies,
36-
} = await getBundledFileDataWithDependencies(fileId, ctx.parsedFileResolver)
36+
} = await getBundledFileDataWithDependencies(fileId, ctx.parsedFileResolver, createBundlingErrorHandler(ctx, fileId))
3737
bundledFileData = data
3838
dependencies = fileDependencies
3939
}

src/builder.ts

Lines changed: 19 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
* limitations under the License.
1515
*/
1616

17-
import type {
17+
import {
1818
BuildConfig,
1919
BuildConfigFile,
2020
BuildConfigRef,
@@ -28,6 +28,7 @@ import type {
2828
ResolvedDocuments,
2929
ResolvedOperations,
3030
ResolvedVersionOperationsHashMap,
31+
VALIDATION_RULES_SEVERITY_LEVEL_WARNING,
3132
VersionId,
3233
} from './types'
3334
import {
@@ -50,7 +51,14 @@ import {
5051
import type { NotificationMessage, PackageConfig } from './types/package'
5152
import { graphqlApiBuilder, REST_API_TYPE, restApiBuilder, textApiBuilder, unknownApiBuilder } from './apitypes'
5253
import { filesDiff, findSharedPath, getCompositeKey, getFileExtension, getOperationsList } from './utils'
53-
import { BUILD_TYPE, DEFAULT_BATCH_SIZE, MESSAGE_SEVERITY, SUPPORTED_FILE_FORMATS, VERSION_STATUS } from './consts'
54+
import {
55+
BUILD_TYPE,
56+
DEFAULT_BATCH_SIZE,
57+
DEFAULT_VALIDATION_RULES_SEVERITY_CONFIG,
58+
MESSAGE_SEVERITY,
59+
SUPPORTED_FILE_FORMATS,
60+
VERSION_STATUS,
61+
} from './consts'
5462
import { unknownParsedFile } from './apitypes/unknown/unknown.parser'
5563
import { createVersionPackage } from './components/package'
5664
import { compareVersions } from './components/compare'
@@ -90,7 +98,15 @@ export class PackageVersionBuilder implements IPackageVersionBuilder {
9098

9199
constructor(config: BuildConfig, public params: BuilderParams, fileSources?: FileSourceMap) {
92100
this.apiBuilders.push(restApiBuilder, graphqlApiBuilder, textApiBuilder, unknownApiBuilder)
93-
this.config = { previousVersion: '', previousVersionPackageId: '', ...config }
101+
this.config = {
102+
previousVersion: '',
103+
previousVersionPackageId: '',
104+
...config,
105+
validationRulesSeverity: {
106+
...DEFAULT_VALIDATION_RULES_SEVERITY_CONFIG,
107+
...config.validationRulesSeverity,
108+
},
109+
}
94110

95111
this.params.configuration = {
96112
batchSize: DEFAULT_BATCH_SIZE,
@@ -232,11 +248,6 @@ export class PackageVersionBuilder implements IPackageVersionBuilder {
232248

233249
const source = await this.params.resolvers.fileResolver(fileId)
234250
if (!source) {
235-
this.notifications.push({
236-
severity: MESSAGE_SEVERITY.Error,
237-
message: 'Cannot resolve file',
238-
fileId: fileId,
239-
})
240251
return null
241252
}
242253

src/components/document.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@ export const buildDocument = async (parsedFile: File, file: BuildConfigFile, ctx
5555

5656
return await apiBuilder.buildDocument(parsedFile, file, ctx)
5757
} catch (error) {
58-
throw new Error(`Cannot build file ${file.fileId}. ${error instanceof Error ? error.message : 'Unknown error'}`)
58+
throw new Error(`Cannot process the "${file.fileId}" document. ${error instanceof Error ? error.message : 'Unknown error'}`)
5959
}
6060
}
6161

src/components/operations.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ export const buildOperations = async (document: VersionDocument, ctx: BuilderCon
2424
try {
2525
return await builder.buildOperations?.(document, ctx, debugCtx) ?? []
2626
} catch (error) {
27-
throw new Error(`Cannot build document "${document.slug}" operations. ${error instanceof Error ? error.message : 'Unknown error'}`)
27+
throw new Error(`Cannot process the "${document.fileId}" document. ${error instanceof Error ? error.message : 'Unknown error'}`)
2828
}
2929
}
3030

src/consts.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,10 +23,16 @@ import {
2323
NON_BREAKING_CHANGE_TYPE,
2424
SEMI_BREAKING_CHANGE_TYPE,
2525
UNCLASSIFIED_CHANGE_TYPE,
26+
VALIDATION_RULES_SEVERITY_LEVEL_WARNING,
27+
ValidationRulesSeverity,
2628
} from './types'
2729

2830
export const DEFAULT_BATCH_SIZE = 32
2931

32+
export const DEFAULT_VALIDATION_RULES_SEVERITY_CONFIG: ValidationRulesSeverity = {
33+
brokenRefs: VALIDATION_RULES_SEVERITY_LEVEL_WARNING,
34+
}
35+
3036
export const REVISION_DELIMITER = '@'
3137

3238
export const VERSION_DIFFERENCE_ACTION = {

src/types/external/config.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,17 @@ export type OperationsGroupExportFormat =
2929
| typeof JSON_EXPORT_GROUP_FORMAT
3030
| typeof HTML_EXPORT_GROUP_FORMAT
3131

32+
export const VALIDATION_RULES_SEVERITY_LEVEL_ERROR = 'error'
33+
export const VALIDATION_RULES_SEVERITY_LEVEL_WARNING = 'warning'
34+
35+
export type ValidationRulesSeverityLevel =
36+
| typeof VALIDATION_RULES_SEVERITY_LEVEL_ERROR
37+
| typeof VALIDATION_RULES_SEVERITY_LEVEL_WARNING
38+
39+
export interface ValidationRulesSeverity {
40+
brokenRefs: ValidationRulesSeverityLevel
41+
}
42+
3243
export interface BuildConfig {
3344
packageId: PackageId
3445
version: VersionId // @revision for rebuild
@@ -48,6 +59,8 @@ export interface BuildConfig {
4859

4960
metadata?: Record<string, unknown>
5061
format?: OperationsGroupExportFormat
62+
63+
validationRulesSeverity?: ValidationRulesSeverity
5164
}
5265

5366
export interface BuildConfigFile {

src/utils/document.ts

Lines changed: 32 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import createSlug from 'slug'
1818
import {
1919
_ParsedFileResolver,
2020
ApiOperation,
21+
BuilderContext,
2122
BuildResult,
2223
FILE_KIND,
2324
FileFormat,
@@ -27,11 +28,14 @@ import {
2728
OperationsGroupExportFormat,
2829
PackageDocument,
2930
ResolvedDocument,
31+
VALIDATION_RULES_SEVERITY_LEVEL_ERROR,
32+
VALIDATION_RULES_SEVERITY_LEVEL_WARNING,
3033
VersionDocument,
3134
YAML_EXPORT_GROUP_FORMAT,
3235
} from '../types'
3336
import { bundle, Resolver } from 'api-ref-bundler'
34-
import { FILE_FORMAT_JSON, FILE_FORMAT_YAML } from '../consts'
37+
import { FILE_FORMAT_JSON, FILE_FORMAT_YAML, MESSAGE_SEVERITY } from '../consts'
38+
import { isNotEmpty } from './arrays'
3539

3640
export const EXPORT_FORMAT_TO_FILE_FORMAT = new Map<OperationsGroupExportFormat, FileFormat>([
3741
[YAML_EXPORT_GROUP_FORMAT, FILE_FORMAT_YAML],
@@ -117,21 +121,43 @@ export const getDocumentTitle = (fileId: string): string => {
117121
return fileId.substring(cutDot).split('/').pop()!.replace(/\.[^/.]+$/, '')
118122
}
119123

124+
export const createBundlingErrorHandler = (ctx: BuilderContext, fileId: FileId) => (messages: string[]): void => {
125+
switch (ctx.config.validationRulesSeverity?.brokenRefs) {
126+
case VALIDATION_RULES_SEVERITY_LEVEL_ERROR:
127+
throw new Error(messages[0])
128+
case VALIDATION_RULES_SEVERITY_LEVEL_WARNING:
129+
default:
130+
for (const message of messages) {
131+
ctx.notifications.push({
132+
severity: MESSAGE_SEVERITY.Error,
133+
message: message,
134+
fileId: fileId,
135+
})
136+
}
137+
}
138+
}
139+
120140
export const getBundledFileDataWithDependencies = async (
121141
fileId: FileId,
122142
parsedFileResolver: _ParsedFileResolver,
143+
onError: (messages: string[]) => void,
123144
): Promise<{ data: any; dependencies: string[] }> => {
124145
const dependencies: string[] = []
146+
const errorMessages: string[] = []
125147

126148
const resolver: Resolver = async (filepath: string) => {
127149
const data = await parsedFileResolver(filepath)
128150

129151
if (data === null) {
152+
// can't throw the error here because it will be suppressed: https://github.com/udamir/api-ref-bundler/blob/0.4.0/src/resolver.ts#L33
153+
errorMessages.push(`Unable to resolve the file "${filepath}" because it does not exist.`)
130154
return {}
131155
}
132156

133157
if (data.kind !== FILE_KIND.TEXT) {
134-
throw new Error(`Dependency with path ${filepath} is not a text file`)
158+
// can't throw the error here because it will be suppressed: https://github.com/udamir/api-ref-bundler/blob/0.4.0/src/resolver.ts#L33
159+
errorMessages.push(`Unable to resolve the file "${filepath}" because it is not a valid text file.`)
160+
return {}
135161
}
136162

137163
if (filepath !== fileId) {
@@ -143,6 +169,10 @@ export const getBundledFileDataWithDependencies = async (
143169

144170
const bundledFileData = await bundle(fileId, resolver)
145171

172+
if (isNotEmpty(errorMessages)) {
173+
onError(errorMessages)
174+
}
175+
146176
return { data: bundledFileData, dependencies: dependencies }
147177
}
148178

test/helpers/matchers.ts

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,10 +15,13 @@
1515
*/
1616

1717
import {
18+
BuildResult,
1819
ChangeMessage,
1920
ChangeSummary,
2021
DeprecateItem,
2122
EMPTY_CHANGE_SUMMARY,
23+
MESSAGE_SEVERITY,
24+
NotificationMessage,
2225
OperationChanges,
2326
type OperationsApiType,
2427
OperationType,
@@ -32,6 +35,8 @@ import { ArrayContaining, ObjectContaining, RecursiveMatcher } from '../../.jest
3235
export type ApihubComparisonMatcher = ObjectContaining<VersionsComparison> & VersionsComparison
3336
export type ApihubOperationChangesMatcher = ObjectContaining<OperationChanges> & OperationChanges
3437
export type ApihubChangesSummaryMatcher = ObjectContaining<ChangeSummary> & ChangeSummary
38+
export type ApihubNotificationsMatcher = ObjectContaining<BuildResult> & BuildResult
39+
export type ApihubErrorNotificationMatcher = ObjectContaining<NotificationMessage> & NotificationMessage
3540
export type ApihubChangeMessagesMatcher = ArrayContaining<ChangeMessage> & ChangeMessage[]
3641

3742
export function apihubComparisonMatcher(
@@ -130,3 +135,21 @@ export function deprecatedItemDescriptionMatcher(
130135
}
131136

132137
type Matcher = ObjectContaining<DeprecateItem>
138+
139+
export function notificationsMatcher(
140+
expected: Array<RecursiveMatcher<NotificationMessage>>,
141+
): ApihubNotificationsMatcher {
142+
return expect.objectContaining({
143+
notifications: expect.toIncludeSameMembers(expected),
144+
},
145+
)
146+
}
147+
148+
export function errorNotificationMatcher(
149+
message: string | RegExp,
150+
): ApihubErrorNotificationMatcher {
151+
return expect.objectContaining({
152+
message: expect.stringMatching(message),
153+
severity: MESSAGE_SEVERITY.Error,
154+
})
155+
}

0 commit comments

Comments
 (0)