Skip to content

Commit e2ebbc2

Browse files
sam0r040timonback
andauthored
feat(springwolf-ui): validate AsyncAPI specification against springwolf-ui schema (springwolf#1219)
Co-authored-by: Timon Back <[email protected]>
1 parent ee447fb commit e2ebbc2

File tree

9 files changed

+657
-10
lines changed

9 files changed

+657
-10
lines changed

springwolf-ui/package-lock.json

Lines changed: 96 additions & 8 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

springwolf-ui/package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
"@angular/platform-browser": "^19.2.0",
2323
"@angular/platform-browser-dynamic": "^19.2.0",
2424
"@angular/router": "^19.2.0",
25+
"ajv": "^8.17.1",
2526
"ngx-markdown": "^19.1.0",
2627
"prism-code-editor": "^3.4.0",
2728
"rxjs": "^7.8.2",
@@ -41,6 +42,7 @@
4142
"jest": "^29.7.0",
4243
"jest-junit": "^16.0.0",
4344
"jest-preset-angular": "^14.5.3",
45+
"ts-json-schema-generator": "^2.3.0",
4446
"typescript": "^5.8.2"
4547
},
4648
"overrides": {

springwolf-ui/src/app/app.config.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import { provideMarkdown } from "ngx-markdown";
1212
import { AssetService, IAssetService } from "./service/asset.service";
1313
import { AsyncApiService } from "./service/asyncapi/asyncapi.service";
1414
import { AsyncApiMapperService } from "./service/asyncapi/asyncapi-mapper.service";
15+
import { AsyncApiValidatorService } from "./service/asyncapi/validator/asyncapi-validator.service";
1516
import {
1617
INotificationService,
1718
NotificationService,
@@ -34,6 +35,7 @@ export const appConfig: ApplicationConfig = {
3435
{ provide: IAssetService, useClass: AssetService },
3536
AsyncApiService,
3637
AsyncApiMapperService,
38+
AsyncApiValidatorService,
3739
{ provide: INotificationService, useClass: NotificationService },
3840
PublisherService,
3941
{ provide: IUiService, useClass: UiService },

springwolf-ui/src/app/service/asyncapi/asyncapi.service.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,12 @@ import { AsyncApi } from "../../models/asyncapi.model";
33
import { HttpClient } from "@angular/common/http";
44
import { Injectable } from "@angular/core";
55
import { Observable, shareReplay, switchMap } from "rxjs";
6-
import { filter, map } from "rxjs/operators";
6+
import { filter, map, tap } from "rxjs/operators";
77
import { EndpointService } from "../endpoint.service";
88
import { AsyncApiMapperService } from "./asyncapi-mapper.service";
99
import { ServerAsyncApi } from "./models/asyncapi.model";
1010
import { IUiService } from "../ui.service";
11+
import { AsyncApiValidatorService } from "./validator/asyncapi-validator.service";
1112

1213
@Injectable()
1314
export class AsyncApiService {
@@ -16,7 +17,8 @@ export class AsyncApiService {
1617
constructor(
1718
private http: HttpClient,
1819
private asyncApiMapperService: AsyncApiMapperService,
19-
private uiService: IUiService
20+
private uiService: IUiService,
21+
private asyncApiValidatorService: AsyncApiValidatorService
2022
) {
2123
this.docs = this.uiService.isGroup$.pipe(
2224
switchMap((group) => {
@@ -26,6 +28,7 @@ export class AsyncApiService {
2628
: EndpointService.getDocsForGroupEndpoint(group);
2729
return this.http.get<ServerAsyncApi>(url);
2830
}),
31+
tap((item) => this.asyncApiValidatorService.validate(item)),
2932
map((item) => this.asyncApiMapperService.toAsyncApi(item)),
3033
filter((item): item is AsyncApi => item !== undefined),
3134
shareReplay()

springwolf-ui/src/app/service/asyncapi/models/info.models.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,19 @@ export interface ServerAsyncApiInfo {
77
name?: string;
88
url?: string;
99
email?: string;
10+
11+
// allow any additional fields
12+
[key: string]: unknown;
1013
};
1114
license?: {
1215
name?: string;
1316
url?: string;
17+
18+
// allow any additional fields
19+
[key: string]: unknown;
1420
};
21+
termsOfService?: string;
22+
23+
// allow any additional fields
24+
[key: string]: unknown;
1525
}

springwolf-ui/src/app/service/asyncapi/models/operations.model.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ export interface ServerOperations {
77

88
export interface ServerOperation {
99
action: string;
10+
title?: string;
1011
description?: string;
1112
channel: {
1213
$ref: string;
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
/* SPDX-License-Identifier: Apache-2.0 */
2+
import Ajv from "ajv";
3+
import { createGenerator } from "ts-json-schema-generator";
4+
import path from "node:path";
5+
import expectedSchema from "./server-async-api.schema.json";
6+
import { exampleSchemas } from "../../mock/example-data";
7+
import * as fs from "node:fs";
8+
9+
const config = {
10+
path: path.join(__dirname, "../models/asyncapi.model.ts"),
11+
tsconfig: path.join(__dirname, "../../../../../tsconfig.json"),
12+
type: "ServerAsyncApi",
13+
};
14+
const serverJsonSchema = createGenerator(config).createSchema("ServerAsyncApi");
15+
16+
describe("AsyncApiValidatorService", () => {
17+
const ajv = new Ajv({
18+
allErrors: true,
19+
removeAdditional: true, // allow additional properties (no strict mode) in test
20+
});
21+
22+
it("should compile to the checked-in schema", () => {
23+
// when
24+
const validate = ajv.compile(serverJsonSchema);
25+
26+
// then
27+
fs.writeFile(
28+
path.join(__dirname, "server-async-api.schema.actual.json"),
29+
JSON.stringify(validate.schema, null, 2),
30+
(err) => {
31+
if (err) throw err;
32+
}
33+
);
34+
35+
expect(validate.schema).toStrictEqual(expectedSchema);
36+
});
37+
38+
for (const [plugin, pluginSchema] of Object.entries(exampleSchemas)) {
39+
const pluginSchemaGroups = {
40+
...pluginSchema.groups,
41+
default: pluginSchema.value,
42+
};
43+
44+
for (const [group, schema] of Object.entries(pluginSchemaGroups)) {
45+
it(
46+
"should verify AsyncApi schema is valid - " +
47+
plugin +
48+
" example and group " +
49+
group,
50+
() => {
51+
// when
52+
const validate = ajv.compile(serverJsonSchema);
53+
const isValid = validate(schema);
54+
55+
// then
56+
expect(validate.errors).toBeNull();
57+
expect(isValid).toBeTruthy();
58+
}
59+
);
60+
}
61+
}
62+
});
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
/* SPDX-License-Identifier: Apache-2.0 */
2+
import { Injectable } from "@angular/core";
3+
import Ajv from "ajv";
4+
import expectedSchema from "./server-async-api.schema.json";
5+
import { ServerAsyncApi } from "../models/asyncapi.model";
6+
7+
@Injectable()
8+
export class AsyncApiValidatorService {
9+
// private readonly ajv = new Ajv({allErrors: true, removeAdditional: true});
10+
private readonly ajvInStrictMode = new Ajv({
11+
allErrors: true,
12+
removeAdditional: false,
13+
});
14+
15+
public validate(item: ServerAsyncApi) {
16+
// const validate = this.ajv.compile(expectedSchema);
17+
// const isValid = validate(item)
18+
// if(!isValid) {
19+
// console.warn("Validation error while parsing asyncapi file in Springwolf format", validate.errors)
20+
// }
21+
22+
const validateStrict = this.ajvInStrictMode.compile(expectedSchema);
23+
const isValidInStrictMode = validateStrict(item);
24+
if (!isValidInStrictMode) {
25+
console.info(
26+
"Validation error while parsing asyncapi file in Springwolf format (strict mode)",
27+
validateStrict.errors
28+
);
29+
}
30+
}
31+
}

0 commit comments

Comments
 (0)