diff --git a/springwolf-ui/README.md b/springwolf-ui/README.md index 8545de722..95bbe6f14 100644 --- a/springwolf-ui/README.md +++ b/springwolf-ui/README.md @@ -16,6 +16,13 @@ dependencies { After starting the application, visit: `localhost:8080/springwolf/asyncapi-ui.html`. +## TODOs: + +- Migrate to AsyncApi 3 +- Review Angular compoents - adapt to latest angular guidelines +- Migrate tslint to eslint +- Validate existing documents using parser-js? + ## Development 1. Run `npm i` 2. Run `ng serve` @@ -25,8 +32,6 @@ After starting the application, visit: `localhost:8080/springwolf/asyncapi-ui.ht The application renders content based on mock data in `src/app/shared/mock`. It contains multiple mocks - including the ones from the springwolf-core examples projects. -To update the mock data, run `npm run update-mocks`. - ## Release Releasing is done by running the gradle task `publish`. For local development, use `publishToMavenLocal`. diff --git a/springwolf-ui/angular.json b/springwolf-ui/angular.json index 86aeea64b..74e216b8e 100644 --- a/springwolf-ui/angular.json +++ b/springwolf-ui/angular.json @@ -27,7 +27,7 @@ "styles": [ "./node_modules/@angular/material/prebuilt-themes/indigo-pink.css", "./node_modules/font-awesome/css/font-awesome.min.css", - "src/styles.css" + "src/main.css" ], "scripts": [] }, @@ -91,7 +91,7 @@ ], "styles": [ "./node_modules/@angular/material/prebuilt-themes/indigo-pink.css", - "src/styles.css" + "src/main.css" ], "scripts": [] } diff --git a/springwolf-ui/browserslist b/springwolf-ui/browserslist index 80848532e..1cf31eca5 100644 --- a/springwolf-ui/browserslist +++ b/springwolf-ui/browserslist @@ -9,4 +9,3 @@ last 2 versions Firefox ESR not dead -not IE 9-11 # For IE 9-11 support, remove 'not'. \ No newline at end of file diff --git a/springwolf-ui/src/app/app.module.ts b/springwolf-ui/src/app/app.module.ts index f2ca8886a..fa69c88cd 100644 --- a/springwolf-ui/src/app/app.module.ts +++ b/springwolf-ui/src/app/app.module.ts @@ -7,20 +7,21 @@ import { HttpClientInMemoryWebApiModule } from "angular-in-memory-web-api"; import { HighlightModule, HIGHLIGHT_OPTIONS } from "ngx-highlightjs"; import { environment } from "./../environments/environment"; import { AppComponent } from "./app.component"; -import { ChannelMainComponent } from "./channels/channel-main/channel-main.component"; -import { ChannelsComponent } from "./channels/channels.component"; -import { HeaderComponent } from "./header/header.component"; -import { InfoComponent } from "./info/info.component"; +import { ChannelMainComponent } from "./components/channels/channel-main/channel-main.component"; +import { ChannelsComponent } from "./components/channels/channels.component"; +import { HeaderComponent } from "./components/header/header.component"; +import { InfoComponent } from "./components/info/info.component"; import { MaterialModule } from "./material.module"; -import { SchemaComponent } from "./schemas/schema/schema.component"; -import { SchemasComponent } from "./schemas/schemas.component"; -import { ServersComponent } from "./servers/servers.component"; -import { AsyncApiService } from "./shared/asyncapi.service"; -import { MockServer } from "./shared/mock/mock-server"; -import { PublisherService } from "./shared/publisher.service"; +import { SchemaComponent } from "./components/schemas/schema/schema.component"; +import { SchemasComponent } from "./components/schemas/schemas.component"; +import { ServersComponent } from "./components/servers/servers.component"; +import { AsyncApiService } from "./service/asyncapi/asyncapi.service"; +import { MockServer } from "./service/mock/mock-server"; +import { PublisherService } from "./service/publisher.service"; +import { NotificationService } from "./service/notification.service"; import { FormsModule } from "@angular/forms"; -import { JsonComponent } from "./shared/components/json/json.component"; -import { AsyncApiMapperService } from "./shared/asyncapi-mapper.service"; +import { JsonComponent } from "./components/json/json.component"; +import { AsyncApiMapperService } from "./service/asyncapi/asyncapi-mapper.service"; @NgModule({ declarations: [ @@ -48,6 +49,7 @@ import { AsyncApiMapperService } from "./shared/asyncapi-mapper.service"; providers: [ AsyncApiService, AsyncApiMapperService, + NotificationService, PublisherService, { provide: HIGHLIGHT_OPTIONS, diff --git a/springwolf-ui/src/app/channels/channel-main/channel-main.component.css b/springwolf-ui/src/app/components/channels/channel-main/channel-main.component.css similarity index 100% rename from springwolf-ui/src/app/channels/channel-main/channel-main.component.css rename to springwolf-ui/src/app/components/channels/channel-main/channel-main.component.css diff --git a/springwolf-ui/src/app/channels/channel-main/channel-main.component.html b/springwolf-ui/src/app/components/channels/channel-main/channel-main.component.html similarity index 100% rename from springwolf-ui/src/app/channels/channel-main/channel-main.component.html rename to springwolf-ui/src/app/components/channels/channel-main/channel-main.component.html diff --git a/springwolf-ui/src/app/channels/channel-main/channel-main.component.ts b/springwolf-ui/src/app/components/channels/channel-main/channel-main.component.ts similarity index 88% rename from springwolf-ui/src/app/channels/channel-main/channel-main.component.ts rename to springwolf-ui/src/app/components/channels/channel-main/channel-main.component.ts index abb1e6134..76247f392 100644 --- a/springwolf-ui/src/app/channels/channel-main/channel-main.component.ts +++ b/springwolf-ui/src/app/components/channels/channel-main/channel-main.component.ts @@ -1,11 +1,12 @@ /* SPDX-License-Identifier: Apache-2.0 */ import { Component, Input, OnInit } from "@angular/core"; -import { AsyncApiService } from "src/app/shared/asyncapi.service"; -import { Example } from "src/app/shared/models/example.model"; -import { Schema } from "src/app/shared/models/schema.model"; -import { PublisherService } from "src/app/shared/publisher.service"; +import { AsyncApiService } from "src/app/service/asyncapi/asyncapi.service"; +import { Example } from "src/app/models/example.model"; +import { Schema } from "src/app/models/schema.model"; +import { PublisherService } from "src/app/service/publisher.service"; import { MatSnackBar } from "@angular/material/snack-bar"; -import { MessageBinding, Operation } from "src/app/shared/models/channel.model"; +import { Operation } from "src/app/models/channel.model"; +import { Binding } from "src/app/models/bindings.model"; import { STATUS } from "angular-in-memory-web-api"; @Component({ @@ -70,9 +71,7 @@ export class ChannelMainComponent implements OnInit { ); } - createMessageBindingExample( - messageBinding?: MessageBinding - ): Example | undefined { + createMessageBindingExample(messageBinding?: Binding): Example | undefined { if (messageBinding === undefined || messageBinding === null) { return undefined; } @@ -93,12 +92,13 @@ export class ChannelMainComponent implements OnInit { return bindingExample; } - getExampleValue(bindingValue: string | Schema): any { + getExampleValue(bindingValue: string | Binding): any { if (typeof bindingValue === "string") { return bindingValue; - } else { + } else if (typeof bindingValue.example === "object") { return bindingValue.example.value; } + return undefined; } recalculateLineCount(field: string, text: string): void { diff --git a/springwolf-ui/src/app/channels/channels.component.css b/springwolf-ui/src/app/components/channels/channels.component.css similarity index 100% rename from springwolf-ui/src/app/channels/channels.component.css rename to springwolf-ui/src/app/components/channels/channels.component.css diff --git a/springwolf-ui/src/app/channels/channels.component.html b/springwolf-ui/src/app/components/channels/channels.component.html similarity index 70% rename from springwolf-ui/src/app/channels/channels.component.html rename to springwolf-ui/src/app/components/channels/channels.component.html index c85f7f6fa..a5317bdfc 100644 --- a/springwolf-ui/src/app/channels/channels.component.html +++ b/springwolf-ui/src/app/components/channels/channels.component.html @@ -1,29 +1,6 @@

Channels

-Semantics of publish and subscribe: - - this.setChannelSelectionFromLocation()); this.asyncApiService.getAsyncApi().subscribe((asyncapi) => { - this.channels = this.sortChannels(asyncapi.channels); + this.channels = this.sortChannels(asyncapi.channelOperations); }); } - private sortChannels(channels: Array): Array { + private sortChannels( + channels: Array + ): Array { return channels.sort((a, b) => { if (a.operation.protocol === b.operation.protocol) { - if (a.operation.operation === b.operation.operation) { + if (a.operation.operationType === b.operation.operationType) { if (a.name === b.name) { return a.operation.message.name.localeCompare( b.operation.message.name @@ -41,7 +46,9 @@ export class ChannelsComponent implements OnInit { return a.name.localeCompare(b.name); } } else { - return a.operation.operation.localeCompare(b.operation.operation); + return a.operation.operationType.localeCompare( + b.operation.operationType + ); } } else { return a.operation.protocol.localeCompare(b.operation.protocol); @@ -49,7 +56,7 @@ export class ChannelsComponent implements OnInit { }); } - setChannelSelection(channel: Channel): void { + setChannelSelection(channel: ChannelOperation): void { window.location.hash = channel.anchorIdentifier; } setChannelSelectionFromLocation(): void { diff --git a/springwolf-ui/src/app/header/header.component.css b/springwolf-ui/src/app/components/header/header.component.css similarity index 100% rename from springwolf-ui/src/app/header/header.component.css rename to springwolf-ui/src/app/components/header/header.component.css diff --git a/springwolf-ui/src/app/header/header.component.html b/springwolf-ui/src/app/components/header/header.component.html similarity index 100% rename from springwolf-ui/src/app/header/header.component.html rename to springwolf-ui/src/app/components/header/header.component.html diff --git a/springwolf-ui/src/app/header/header.component.ts b/springwolf-ui/src/app/components/header/header.component.ts similarity index 100% rename from springwolf-ui/src/app/header/header.component.ts rename to springwolf-ui/src/app/components/header/header.component.ts diff --git a/springwolf-ui/src/app/info/info.component.css b/springwolf-ui/src/app/components/info/info.component.css similarity index 100% rename from springwolf-ui/src/app/info/info.component.css rename to springwolf-ui/src/app/components/info/info.component.css diff --git a/springwolf-ui/src/app/components/info/info.component.html b/springwolf-ui/src/app/components/info/info.component.html new file mode 100644 index 000000000..9c0816280 --- /dev/null +++ b/springwolf-ui/src/app/components/info/info.component.html @@ -0,0 +1,22 @@ + +

{{ info?.title }}

+
+ API VERSION {{ info?.version }} + - + Download AsyncAPI JSON file +
+

+ + + {{ info.license.name }} + + {{ info.license.name }} + +

+

{{ info.description }}

diff --git a/springwolf-ui/src/app/info/info.component.ts b/springwolf-ui/src/app/components/info/info.component.ts similarity index 82% rename from springwolf-ui/src/app/info/info.component.ts rename to springwolf-ui/src/app/components/info/info.component.ts index f8545c9aa..28a1f1f7b 100644 --- a/springwolf-ui/src/app/info/info.component.ts +++ b/springwolf-ui/src/app/components/info/info.component.ts @@ -1,8 +1,8 @@ /* SPDX-License-Identifier: Apache-2.0 */ import { Component, OnInit } from "@angular/core"; -import { AsyncApi } from "../shared/models/asyncapi.model"; -import { Info } from "../shared/models/info.model"; -import { AsyncApiService } from "../shared/asyncapi.service"; +import { AsyncApi } from "../../models/asyncapi.model"; +import { Info } from "../../models/info.model"; +import { AsyncApiService } from "../../service/asyncapi/asyncapi.service"; @Component({ selector: "app-info", diff --git a/springwolf-ui/src/app/shared/components/json/json.component.ts b/springwolf-ui/src/app/components/json/json.component.ts similarity index 100% rename from springwolf-ui/src/app/shared/components/json/json.component.ts rename to springwolf-ui/src/app/components/json/json.component.ts diff --git a/springwolf-ui/src/app/schemas/schema/schema.component.css b/springwolf-ui/src/app/components/schemas/schema/schema.component.css similarity index 100% rename from springwolf-ui/src/app/schemas/schema/schema.component.css rename to springwolf-ui/src/app/components/schemas/schema/schema.component.css diff --git a/springwolf-ui/src/app/schemas/schema/schema.component.html b/springwolf-ui/src/app/components/schemas/schema/schema.component.html similarity index 100% rename from springwolf-ui/src/app/schemas/schema/schema.component.html rename to springwolf-ui/src/app/components/schemas/schema/schema.component.html diff --git a/springwolf-ui/src/app/schemas/schema/schema.component.ts b/springwolf-ui/src/app/components/schemas/schema/schema.component.ts similarity index 82% rename from springwolf-ui/src/app/schemas/schema/schema.component.ts rename to springwolf-ui/src/app/components/schemas/schema/schema.component.ts index c59fb2a89..e2ed2d11b 100644 --- a/springwolf-ui/src/app/schemas/schema/schema.component.ts +++ b/springwolf-ui/src/app/components/schemas/schema/schema.component.ts @@ -1,6 +1,6 @@ /* SPDX-License-Identifier: Apache-2.0 */ import { Component, Input } from "@angular/core"; -import { Schema } from "src/app/shared/models/schema.model"; +import { Schema } from "src/app/models/schema.model"; @Component({ selector: "app-schema", diff --git a/springwolf-ui/src/app/schemas/schemas.component.css b/springwolf-ui/src/app/components/schemas/schemas.component.css similarity index 100% rename from springwolf-ui/src/app/schemas/schemas.component.css rename to springwolf-ui/src/app/components/schemas/schemas.component.css diff --git a/springwolf-ui/src/app/schemas/schemas.component.html b/springwolf-ui/src/app/components/schemas/schemas.component.html similarity index 100% rename from springwolf-ui/src/app/schemas/schemas.component.html rename to springwolf-ui/src/app/components/schemas/schemas.component.html diff --git a/springwolf-ui/src/app/schemas/schemas.component.ts b/springwolf-ui/src/app/components/schemas/schemas.component.ts similarity index 88% rename from springwolf-ui/src/app/schemas/schemas.component.ts rename to springwolf-ui/src/app/components/schemas/schemas.component.ts index 6b525dc15..68c7fa0a4 100644 --- a/springwolf-ui/src/app/schemas/schemas.component.ts +++ b/springwolf-ui/src/app/components/schemas/schemas.component.ts @@ -1,8 +1,8 @@ /* SPDX-License-Identifier: Apache-2.0 */ import { Component, OnInit } from "@angular/core"; import { Location } from "@angular/common"; -import { AsyncApiService } from "../shared/asyncapi.service"; -import { Schema } from "../shared/models/schema.model"; +import { AsyncApiService } from "../../service/asyncapi/asyncapi.service"; +import { Schema } from "../../models/schema.model"; @Component({ selector: "app-schemas", diff --git a/springwolf-ui/src/app/servers/servers.component.css b/springwolf-ui/src/app/components/servers/servers.component.css similarity index 100% rename from springwolf-ui/src/app/servers/servers.component.css rename to springwolf-ui/src/app/components/servers/servers.component.css diff --git a/springwolf-ui/src/app/servers/servers.component.html b/springwolf-ui/src/app/components/servers/servers.component.html similarity index 100% rename from springwolf-ui/src/app/servers/servers.component.html rename to springwolf-ui/src/app/components/servers/servers.component.html diff --git a/springwolf-ui/src/app/servers/servers.component.ts b/springwolf-ui/src/app/components/servers/servers.component.ts similarity index 79% rename from springwolf-ui/src/app/servers/servers.component.ts rename to springwolf-ui/src/app/components/servers/servers.component.ts index 049b09df6..63929561f 100644 --- a/springwolf-ui/src/app/servers/servers.component.ts +++ b/springwolf-ui/src/app/components/servers/servers.component.ts @@ -1,7 +1,7 @@ /* SPDX-License-Identifier: Apache-2.0 */ import { Component, OnInit } from "@angular/core"; -import { AsyncApiService } from "../shared/asyncapi.service"; -import { Server } from "../shared/models/server.model"; +import { AsyncApiService } from "../../service/asyncapi/asyncapi.service"; +import { Server } from "../../models/server.model"; @Component({ selector: "app-servers", diff --git a/springwolf-ui/src/app/info/info.component.html b/springwolf-ui/src/app/info/info.component.html deleted file mode 100644 index 2a09b2f75..000000000 --- a/springwolf-ui/src/app/info/info.component.html +++ /dev/null @@ -1,8 +0,0 @@ - -

{{ info?.title }}

-
- API VERSION {{ info?.version }} - - - AsyncAPI JSON file -
-

{{ info.description }}

diff --git a/springwolf-ui/src/app/material.module.ts b/springwolf-ui/src/app/material.module.ts index 6fa152909..868d2abec 100644 --- a/springwolf-ui/src/app/material.module.ts +++ b/springwolf-ui/src/app/material.module.ts @@ -12,6 +12,7 @@ import { ClipboardModule } from "@angular/cdk/clipboard"; import { MatSnackBarModule } from "@angular/material/snack-bar"; import { MatFormFieldModule } from "@angular/material/form-field"; import { MatSelectModule } from "@angular/material/select"; +import { MatChipsModule } from "@angular/material/chips"; const modules = [ MatButtonModule, @@ -25,6 +26,7 @@ const modules = [ MatSnackBarModule, MatFormFieldModule, MatSelectModule, + MatChipsModule, ]; @NgModule({ diff --git a/springwolf-ui/src/app/shared/models/asyncapi.model.ts b/springwolf-ui/src/app/models/asyncapi.model.ts similarity index 75% rename from springwolf-ui/src/app/shared/models/asyncapi.model.ts rename to springwolf-ui/src/app/models/asyncapi.model.ts index 322f76c4c..23f0971c8 100644 --- a/springwolf-ui/src/app/shared/models/asyncapi.model.ts +++ b/springwolf-ui/src/app/models/asyncapi.model.ts @@ -1,12 +1,12 @@ /* SPDX-License-Identifier: Apache-2.0 */ import { Info } from "./info.model"; import { Server } from "./server.model"; -import { Channel } from "./channel.model"; +import { ChannelOperation } from "./channel.model"; import { Schema } from "./schema.model"; export interface AsyncApi { info: Info; servers: Map; - channels: Channel[]; + channelOperations: ChannelOperation[]; components: { schemas: Map }; } diff --git a/springwolf-ui/src/app/models/bindings.model.ts b/springwolf-ui/src/app/models/bindings.model.ts new file mode 100644 index 000000000..405abaee4 --- /dev/null +++ b/springwolf-ui/src/app/models/bindings.model.ts @@ -0,0 +1,8 @@ +/* SPDX-License-Identifier: Apache-2.0 */ +export interface Bindings { + [protocol: string]: Binding; +} + +export interface Binding { + [protocol: string]: string | Binding; +} diff --git a/springwolf-ui/src/app/shared/models/channel.model.ts b/springwolf-ui/src/app/models/channel.model.ts similarity index 64% rename from springwolf-ui/src/app/shared/models/channel.model.ts rename to springwolf-ui/src/app/models/channel.model.ts index 246eca06e..b7d72d2a9 100644 --- a/springwolf-ui/src/app/shared/models/channel.model.ts +++ b/springwolf-ui/src/app/models/channel.model.ts @@ -1,20 +1,20 @@ /* SPDX-License-Identifier: Apache-2.0 */ -import { Schema } from "./schema.model"; +import { Binding, Bindings } from "./bindings.model"; export const CHANNEL_ANCHOR_PREFIX = "#channel-"; -export interface Channel { +export interface ChannelOperation { name: string; anchorIdentifier: string; description?: string; operation: Operation; } -export type OperationType = "publish" | "subscribe"; +export type OperationType = "receive" | "send"; export interface Operation { message: Message; - bindings?: { [protocol: string]: any }; + bindings?: Bindings; protocol: string; - operation: OperationType; + operationType: OperationType; } export interface Message { @@ -31,10 +31,6 @@ export interface Message { title: string; anchorUrl: string; }; - bindings?: Map; + bindings?: Map; rawBindings?: { [protocol: string]: object }; } - -export interface MessageBinding { - [protocol: string]: string | Schema; -} diff --git a/springwolf-ui/src/app/shared/models/example.model.ts b/springwolf-ui/src/app/models/example.model.ts similarity index 100% rename from springwolf-ui/src/app/shared/models/example.model.ts rename to springwolf-ui/src/app/models/example.model.ts diff --git a/springwolf-ui/src/app/shared/models/info.model.ts b/springwolf-ui/src/app/models/info.model.ts similarity index 73% rename from springwolf-ui/src/app/shared/models/info.model.ts rename to springwolf-ui/src/app/models/info.model.ts index 4d17cc288..ca08b5539 100644 --- a/springwolf-ui/src/app/shared/models/info.model.ts +++ b/springwolf-ui/src/app/models/info.model.ts @@ -3,5 +3,9 @@ export interface Info { title: string; version: string; description?: string; + license: { + name?: string; + url?: string; + }; asyncApiJson: object; } diff --git a/springwolf-ui/src/app/shared/models/schema.model.ts b/springwolf-ui/src/app/models/schema.model.ts similarity index 100% rename from springwolf-ui/src/app/shared/models/schema.model.ts rename to springwolf-ui/src/app/models/schema.model.ts diff --git a/springwolf-ui/src/app/shared/models/server.model.ts b/springwolf-ui/src/app/models/server.model.ts similarity index 84% rename from springwolf-ui/src/app/shared/models/server.model.ts rename to springwolf-ui/src/app/models/server.model.ts index 26556e76c..ff43bb1db 100644 --- a/springwolf-ui/src/app/shared/models/server.model.ts +++ b/springwolf-ui/src/app/models/server.model.ts @@ -1,5 +1,5 @@ /* SPDX-License-Identifier: Apache-2.0 */ export interface Server { - url: string; + host: string; protocol: string; } diff --git a/springwolf-ui/src/app/service/asyncapi/asyncapi-mapper.service.ts b/springwolf-ui/src/app/service/asyncapi/asyncapi-mapper.service.ts new file mode 100644 index 000000000..490aca6d4 --- /dev/null +++ b/springwolf-ui/src/app/service/asyncapi/asyncapi-mapper.service.ts @@ -0,0 +1,290 @@ +/* SPDX-License-Identifier: Apache-2.0 */ +import { AsyncApi } from "../../models/asyncapi.model"; +import { Server } from "../../models/server.model"; +import { + ChannelOperation, + CHANNEL_ANCHOR_PREFIX, + Message, + Operation, + OperationType, +} from "../../models/channel.model"; +import { Schema } from "../../models/schema.model"; +import { Injectable } from "@angular/core"; +import { Example } from "../../models/example.model"; +import { Info } from "../../models/info.model"; +import { + ServerAsyncApi, + ServerAsyncApiChannelMessage, +} from "./models/asyncapi.model"; +import { ServerAsyncApiMessage } from "./models/message.model"; +import { ServerAsyncApiSchema } from "./models/schema.model"; +import { ServerBinding, ServerBindings } from "./models/bindings.model"; +import { ServerOperations } from "./models/operations.model"; +import { ServerServers } from "./models/servers.model"; +import { ServerChannel, ServerChannels } from "./models/channels.model"; +import { Binding, Bindings } from "src/app/models/bindings.model"; +import { NotificationService } from "../notification.service"; + +@Injectable() +export class AsyncApiMapperService { + static BASE_URL = window.location.pathname + window.location.search + "#"; + + constructor(private notificationService: NotificationService) {} + + public toAsyncApi(item: ServerAsyncApi): AsyncApi { + try { + return { + info: this.mapInfo(item), + servers: this.mapServers(item.servers), + channelOperations: this.mapChannelOperations( + item.channels, + item.operations + ), + components: { + schemas: this.mapSchemas(item.components.schemas), + }, + }; + } catch (e) { + this.notificationService.showError( + "Error parsing AsyncAPI: " + e.message + ); + return undefined; + } + } + + private mapInfo(item: ServerAsyncApi): Info { + return { + title: item.info.title, + version: item.info.version, + description: item.info.description, + license: { + name: item.info.license?.name, + url: item.info.license?.url, + }, + asyncApiJson: item, + }; + } + + private mapServers(servers: ServerServers): Map { + const s = new Map(); + Object.entries(servers).forEach(([k, v]) => s.set(k, v)); + return s; + } + + private mapChannelOperations( + channels: ServerChannels, + operations: ServerOperations + ): ChannelOperation[] { + const s = new Array(); + for (let operationsKey in operations) { + const operation = operations[operationsKey]; + const channelName = this.resolveRef(operation.channel.$ref); + + const messages: Message[] = this.mapServerAsyncApiMessages( + operation.messages + ); + messages.forEach((message) => { + const channelOperation = this.parsingErrorBoundary( + "channel with name " + channelName, + () => + this.mapChannel( + channelName, + channels[channelName], + message, + operation.action + ) + ); + + if (channelOperation != undefined) { + s.push(channelOperation); + } + }); + } + return s; + } + + private mapChannel( + channelName: string, + channel: ServerChannel, + message: Message, + operationType: ServerOperations["action"] + ): ChannelOperation { + if ( + channel.bindings == undefined || + Object.keys(channel.bindings).length == 0 + ) { + this.notificationService.showWarning( + "No binding defined for channel " + channelName + ); + } + + const operation = this.mapOperation( + operationType, + message, + channel.bindings + ); + + return { + name: channelName, + anchorIdentifier: + CHANNEL_ANCHOR_PREFIX + + [ + operation.protocol, + channelName, + operation.operationType, + operation.message.title, + ].join("-"), + description: channel.description, + operation, + }; + } + + private mapServerAsyncApiMessages( + messages: ServerAsyncApiMessage[] + ): Message[] { + return messages + .map((v) => { + const message = this.parsingErrorBoundary( + "message with name " + v.name, + () => { + return { + name: v.name, + title: v.title, + description: v.description, + payload: { + name: v.payload.$ref, + title: this.resolveRef(v.payload.$ref), + anchorUrl: + AsyncApiMapperService.BASE_URL + + this.resolveRef(v.payload.$ref), + }, + headers: { + name: v.headers.$ref, + title: v.headers.$ref?.split("/")?.pop(), + anchorUrl: + AsyncApiMapperService.BASE_URL + + this.resolveRef(v.headers.$ref), + }, + bindings: this.mapServerAsyncApiMessageBindings(v.bindings), + rawBindings: v.bindings, + }; + } + ); + + return message; + }) + .filter((el) => el != undefined); + } + + private mapServerAsyncApiMessageBindings( + serverMessageBindings?: ServerBindings + ): Map { + const messageBindings = new Map(); + if (serverMessageBindings !== undefined) { + Object.keys(serverMessageBindings).forEach((protocol) => { + messageBindings.set( + protocol, + this.mapServerAsyncApiMessageBinding(serverMessageBindings[protocol]) + ); + }); + } + return messageBindings; + } + + private mapServerAsyncApiMessageBinding( + serverMessageBinding: ServerBinding + ): Binding { + const messageBinding: Binding = {}; + + Object.keys(serverMessageBinding).forEach((key) => { + const value = serverMessageBinding[key]; + if (typeof value === "object") { + messageBinding[key] = this.mapServerAsyncApiMessageBinding(value); + } else { + messageBinding[key] = value; + } + }); + + return messageBinding; + } + + private mapOperation( + operationType: ServerOperations["action"], + message: Message, + bindings?: Bindings + ): Operation { + return { + protocol: this.getProtocol(bindings), + operationType: operationType, + message, + bindings, + }; + } + + private getProtocol(bindings?: { [protocol: string]: object }): string { + return Object.keys(bindings)[0]; + } + + private mapSchemas( + schemas: Map + ): Map { + const s = new Map(); + Object.entries(schemas).forEach(([k, v]) => { + const schema = this.parsingErrorBoundary("schema with name " + k, () => + this.mapSchema(k, v) + ); + + if (schema != undefined) { + s.set(k, schema); + } + }); + return s; + } + + private mapSchema(schemaName: string, schema: ServerAsyncApiSchema): Schema { + const anchorUrl = schema.$ref + ? AsyncApiMapperService.BASE_URL + this.resolveRef(schema.$ref) + : undefined; + const properties = + schema.properties !== undefined + ? this.mapSchemas(schema.properties) + : undefined; + const items = + schema.items !== undefined + ? this.mapSchema(schema.$ref + "[]", schema.items) + : undefined; + const example = + schema.example !== undefined ? new Example(schema.example) : undefined; + return { + name: schemaName, + title: schemaName.split(".")?.pop(), + description: schema.description, + refName: schema.$ref, + refTitle: this.resolveRef(schema.$ref), + anchorIdentifier: "#" + schemaName, + anchorUrl: anchorUrl, + type: schema.type, + items, + format: schema.format, + enum: schema.enum, + properties, + required: schema.required, + example, + }; + } + + private resolveRef(ref: string) { + return ref?.split("/")?.pop(); + } + + private parsingErrorBoundary(path: string, f: () => T): T | undefined { + try { + return f(); + } catch (e) { + this.notificationService.showError( + "Error parsing AsyncAPI " + path + ": " + e.message + ); + return undefined; + } + } +} diff --git a/springwolf-ui/src/app/service/asyncapi/asyncapi.service.ts b/springwolf-ui/src/app/service/asyncapi/asyncapi.service.ts new file mode 100644 index 000000000..39070ea6e --- /dev/null +++ b/springwolf-ui/src/app/service/asyncapi/asyncapi.service.ts @@ -0,0 +1,30 @@ +/* SPDX-License-Identifier: Apache-2.0 */ +import { AsyncApi } from "../../models/asyncapi.model"; +import { HttpClient } from "@angular/common/http"; +import { Injectable } from "@angular/core"; +import { Observable, share } from "rxjs"; +import { map } from "rxjs/operators"; +import { EndpointService } from "../endpoint.service"; +import { AsyncApiMapperService } from "./asyncapi-mapper.service"; +import { ServerAsyncApi } from "./models/asyncapi.model"; + +@Injectable() +export class AsyncApiService { + private readonly docs: Observable; + + constructor( + private http: HttpClient, + private asyncApiMapperService: AsyncApiMapperService + ) { + this.docs = this.http.get(EndpointService.docs).pipe( + map((item) => { + return this.asyncApiMapperService.toAsyncApi(item); + }), + share() + ); + } + + public getAsyncApi(): Observable { + return this.docs; + } +} diff --git a/springwolf-ui/src/app/service/asyncapi/models/asyncapi.model.ts b/springwolf-ui/src/app/service/asyncapi/models/asyncapi.model.ts new file mode 100644 index 000000000..0508b3407 --- /dev/null +++ b/springwolf-ui/src/app/service/asyncapi/models/asyncapi.model.ts @@ -0,0 +1,20 @@ +/* SPDX-License-Identifier: Apache-2.0 */ +import { ServerAsyncApiMessage } from "./message.model"; +import { ServerAsyncApiInfo } from "./info.models"; +import { ServerServers } from "./servers.model"; +import { ServerChannels } from "./channels.model"; +import { ServerComponents } from "./components.model"; +import { ServerOperations } from "./operations.model"; + +export type ServerAsyncApiChannelMessage = + | ServerAsyncApiMessage + | { oneOf: ServerAsyncApiMessage[] }; + +export interface ServerAsyncApi { + asyncapi: string; + info: ServerAsyncApiInfo; + servers: ServerServers; + channels: ServerChannels; + operations: ServerOperations; + components: ServerComponents; +} diff --git a/springwolf-ui/src/app/service/asyncapi/models/bindings.model.ts b/springwolf-ui/src/app/service/asyncapi/models/bindings.model.ts new file mode 100644 index 000000000..d9506086e --- /dev/null +++ b/springwolf-ui/src/app/service/asyncapi/models/bindings.model.ts @@ -0,0 +1,8 @@ +/* SPDX-License-Identifier: Apache-2.0 */ +export interface ServerBindings { + [protocol: string]: ServerBinding; +} + +export interface ServerBinding { + [bindingProperty: string]: string | ServerBinding; +} diff --git a/springwolf-ui/src/app/service/asyncapi/models/channels.model.ts b/springwolf-ui/src/app/service/asyncapi/models/channels.model.ts new file mode 100644 index 000000000..813097c08 --- /dev/null +++ b/springwolf-ui/src/app/service/asyncapi/models/channels.model.ts @@ -0,0 +1,14 @@ +/* SPDX-License-Identifier: Apache-2.0 */ +import { ServerAsyncApiMessage } from "./message.model"; +import { ServerBindings } from "./bindings.model"; + +export interface ServerChannels { + [key: string]: ServerChannel; +} + +export interface ServerChannel { + address: string; + description?: string; + messages: Map; + bindings: ServerBindings; +} diff --git a/springwolf-ui/src/app/service/asyncapi/models/components.model.ts b/springwolf-ui/src/app/service/asyncapi/models/components.model.ts new file mode 100644 index 000000000..802eca4c5 --- /dev/null +++ b/springwolf-ui/src/app/service/asyncapi/models/components.model.ts @@ -0,0 +1,6 @@ +/* SPDX-License-Identifier: Apache-2.0 */ +import { ServerAsyncApiSchema } from "./schema.model"; + +export interface ServerComponents { + schemas: Map; +} diff --git a/springwolf-ui/src/app/service/asyncapi/models/info.models.ts b/springwolf-ui/src/app/service/asyncapi/models/info.models.ts new file mode 100644 index 000000000..a30a8af67 --- /dev/null +++ b/springwolf-ui/src/app/service/asyncapi/models/info.models.ts @@ -0,0 +1,11 @@ +/* SPDX-License-Identifier: Apache-2.0 */ +export interface ServerAsyncApiInfo { + title: string; + version: string; + description?: string; + // TODO: use license + license?: { + name?: string; + url?: string; + }; +} diff --git a/springwolf-ui/src/app/service/asyncapi/models/message.model.ts b/springwolf-ui/src/app/service/asyncapi/models/message.model.ts new file mode 100644 index 000000000..22a6e2694 --- /dev/null +++ b/springwolf-ui/src/app/service/asyncapi/models/message.model.ts @@ -0,0 +1,11 @@ +/* SPDX-License-Identifier: Apache-2.0 */ +import { ServerBindings } from "./bindings.model"; + +export interface ServerAsyncApiMessage { + name: string; + title: string; + description?: string; + payload: { $ref: string }; + headers: { $ref: string }; + bindings: ServerBindings; +} diff --git a/springwolf-ui/src/app/service/asyncapi/models/operations.model.ts b/springwolf-ui/src/app/service/asyncapi/models/operations.model.ts new file mode 100644 index 000000000..7ba5bf37c --- /dev/null +++ b/springwolf-ui/src/app/service/asyncapi/models/operations.model.ts @@ -0,0 +1,10 @@ +/* SPDX-License-Identifier: Apache-2.0 */ +import { ServerAsyncApiMessage } from "./message.model"; + +export interface ServerOperations { + action: "receive" | "send"; + channel: { + $ref: string; + }; + messages: ServerAsyncApiMessage[]; +} diff --git a/springwolf-ui/src/app/service/asyncapi/models/schema.model.ts b/springwolf-ui/src/app/service/asyncapi/models/schema.model.ts new file mode 100644 index 000000000..bd34aa40a --- /dev/null +++ b/springwolf-ui/src/app/service/asyncapi/models/schema.model.ts @@ -0,0 +1,16 @@ +/* SPDX-License-Identifier: Apache-2.0 */ +export interface ServerAsyncApiSchema { + description?: string; + type: string; + format: string; + enum: string[]; + properties?: Map; + items?: ServerAsyncApiSchema; + example?: + | { + [key: string]: object; + } + | string; + required?: string[]; + $ref?: string; +} diff --git a/springwolf-ui/src/app/service/asyncapi/models/servers.model.ts b/springwolf-ui/src/app/service/asyncapi/models/servers.model.ts new file mode 100644 index 000000000..a3aab9dc8 --- /dev/null +++ b/springwolf-ui/src/app/service/asyncapi/models/servers.model.ts @@ -0,0 +1,8 @@ +/* SPDX-License-Identifier: Apache-2.0 */ +export interface ServerServers { + [server: string]: { + host: string; + protocol: string; + description?: string; + }; +} diff --git a/springwolf-ui/src/app/shared/endpoints.ts b/springwolf-ui/src/app/service/endpoint.service.ts similarity index 52% rename from springwolf-ui/src/app/shared/endpoints.ts rename to springwolf-ui/src/app/service/endpoint.service.ts index de92fd4e7..8cc193610 100644 --- a/springwolf-ui/src/app/shared/endpoints.ts +++ b/springwolf-ui/src/app/service/endpoint.service.ts @@ -1,15 +1,15 @@ /* SPDX-License-Identifier: Apache-2.0 */ -export class Endpoints { - private static contextPath = Endpoints.getContextPath(); +export class EndpointService { + private static contextPath = EndpointService.getContextPath(); private static getContextPath(): string { let url = document.location.pathname; return url.split("/asyncapi-ui.html")[0]; } - public static docs = Endpoints.contextPath + "/docs"; + public static docs = EndpointService.contextPath + "/docs"; public static getPublishEndpoint(protocol: string): string { - return Endpoints.contextPath + `/${protocol}/publish`; + return EndpointService.contextPath + `/${protocol}/publish`; } } diff --git a/springwolf-ui/src/app/shared/mock/mock-server.ts b/springwolf-ui/src/app/service/mock/mock-server.ts similarity index 100% rename from springwolf-ui/src/app/shared/mock/mock-server.ts rename to springwolf-ui/src/app/service/mock/mock-server.ts diff --git a/springwolf-ui/src/app/service/notification.service.ts b/springwolf-ui/src/app/service/notification.service.ts new file mode 100644 index 000000000..fbe2cac5f --- /dev/null +++ b/springwolf-ui/src/app/service/notification.service.ts @@ -0,0 +1,16 @@ +/* SPDX-License-Identifier: Apache-2.0 */ +import { Injectable } from "@angular/core"; +import { MatSnackBar } from "@angular/material/snack-bar"; + +@Injectable() +export class NotificationService { + constructor(private snackBar: MatSnackBar) {} + + public showError(message: string) { + this.snackBar.open(message, "Close", { verticalPosition: "top" }); + } + + public showWarning(message: string) { + this.snackBar.open(message, "Close", { duration: 3000 }); + } +} diff --git a/springwolf-ui/src/app/shared/publisher.service.ts b/springwolf-ui/src/app/service/publisher.service.ts similarity index 87% rename from springwolf-ui/src/app/shared/publisher.service.ts rename to springwolf-ui/src/app/service/publisher.service.ts index 942edddd4..2273a97bd 100644 --- a/springwolf-ui/src/app/shared/publisher.service.ts +++ b/springwolf-ui/src/app/service/publisher.service.ts @@ -2,7 +2,7 @@ import { Injectable } from "@angular/core"; import { HttpClient, HttpParams } from "@angular/common/http"; import { Observable } from "rxjs"; -import { Endpoints } from "./endpoints"; +import { EndpointService } from "./endpoint.service"; @Injectable() export class PublisherService { @@ -16,7 +16,7 @@ export class PublisherService { headers: object, bindings: object ): Observable { - const url = Endpoints.getPublishEndpoint(protocol); + const url = EndpointService.getPublishEndpoint(protocol); const params = new HttpParams().set("topic", topic); const body = { payload, diff --git a/springwolf-ui/src/app/shared/asyncapi-mapper.service.ts b/springwolf-ui/src/app/shared/asyncapi-mapper.service.ts deleted file mode 100644 index 7f079fc29..000000000 --- a/springwolf-ui/src/app/shared/asyncapi-mapper.service.ts +++ /dev/null @@ -1,289 +0,0 @@ -/* SPDX-License-Identifier: Apache-2.0 */ -import { AsyncApi } from "./models/asyncapi.model"; -import { Server } from "./models/server.model"; -import { - Channel, - CHANNEL_ANCHOR_PREFIX, - Message, - MessageBinding, - Operation, - OperationType, -} from "./models/channel.model"; -import { Schema } from "./models/schema.model"; -import { Injectable } from "@angular/core"; -import { Example } from "./models/example.model"; -import { Info } from "./models/info.model"; - -interface ServerAsyncApiSchema { - description?: string; - type: string; - format: string; - enum: string[]; - properties?: Map; - items?: ServerAsyncApiSchema; - example?: { - [key: string]: object; - }; - required?: string[]; - $ref?: string; -} - -interface ServerAsyncApiMessage { - name: string; - title: string; - description?: string; - payload: { $ref: string }; - headers: { $ref: string }; - bindings: { [protocol: string]: ServerAsyncApiMessageBinding }; -} -interface ServerAsyncApiMessageBinding { - [protocol: string]: ServerAsyncApiSchema | string; -} - -interface ServerAsyncApiInfo { - title: string; - version: string; - description?: string; -} - -export type ServerAsyncApiChannelMessage = - | ServerAsyncApiMessage - | { oneOf: ServerAsyncApiMessage[] }; -export interface ServerAsyncApi { - asyncapi: string; - info: ServerAsyncApiInfo; - servers: { - [server: string]: { - url: string; - protocol: string; - }; - }; - channels: { - [key: string]: { - description?: string; - subscribe?: { - message: ServerAsyncApiChannelMessage; - bindings?: { [protocol: string]: object }; - }; - publish?: { - message: ServerAsyncApiChannelMessage; - bindings?: { [protocol: string]: object }; - }; - }; - }; - components: { - schemas: Map; - }; -} - -@Injectable() -export class AsyncApiMapperService { - static BASE_URL = window.location.pathname + window.location.search + "#"; - - constructor() {} - - public toAsyncApi(item: ServerAsyncApi): AsyncApi { - return { - info: this.mapInfo(item), - servers: this.mapServers(item.servers), - channels: this.mapChannels(item.channels), - components: { - schemas: this.mapSchemas(item.components.schemas), - }, - }; - } - - private mapInfo(item: ServerAsyncApi): Info { - return { - title: item.info.title, - version: item.info.version, - description: item.info.description, - asyncApiJson: item, - }; - } - - private mapServers(servers: ServerAsyncApi["servers"]): Map { - const s = new Map(); - Object.entries(servers).forEach(([k, v]) => s.set(k, v)); - return s; - } - - private mapChannels(channels: ServerAsyncApi["channels"]): Channel[] { - const s = new Array(); - Object.entries(channels).forEach(([k, v]) => { - const subscriberChannels = this.mapChannel( - k, - v.description, - v.subscribe, - "subscribe" - ); - subscriberChannels.forEach((channel) => s.push(channel)); - - const publisherChannels = this.mapChannel( - k, - v.description, - v.publish, - "publish" - ); - publisherChannels.forEach((channel) => s.push(channel)); - }); - return s; - } - - private mapChannel( - topicName: string, - description: ServerAsyncApi["channels"][""]["description"], - serverOperation: - | ServerAsyncApi["channels"][""]["subscribe"] - | ServerAsyncApi["channels"][""]["publish"], - operationType: OperationType - ): Channel[] { - if (serverOperation !== undefined) { - const messages: Message[] = this.mapMessages(serverOperation.message); - - return messages.map((message) => { - const operation = this.mapOperation( - operationType, - message, - serverOperation.bindings - ); - return { - name: topicName, - anchorIdentifier: - CHANNEL_ANCHOR_PREFIX + - [ - operation.protocol, - topicName, - operation.operation, - operation.message.title, - ].join("-"), - description, - operation, - }; - }); - } - return []; - } - - private mapMessages(message: ServerAsyncApiChannelMessage): Message[] { - if ("oneOf" in message) { - return this.mapServerAsyncApiMessages(message.oneOf); - } - return this.mapServerAsyncApiMessages([message]); - } - - private mapServerAsyncApiMessages( - messages: ServerAsyncApiMessage[] - ): Message[] { - return messages.map((v) => { - return { - name: v.name, - title: v.title, - description: v.description, - payload: { - name: v.payload.$ref, - title: v.payload.$ref?.split(".")?.pop(), - anchorUrl: - AsyncApiMapperService.BASE_URL + v.payload.$ref?.split("/")?.pop(), - }, - headers: { - name: v.headers.$ref, - title: v.headers.$ref?.split("/")?.pop(), - anchorUrl: - AsyncApiMapperService.BASE_URL + v.headers.$ref?.split("/")?.pop(), - }, - bindings: this.mapServerAsyncApiMessageBindings(v.bindings), - rawBindings: v.bindings, - }; - }); - } - - private mapServerAsyncApiMessageBindings(serverMessageBindings?: { - [protocol: string]: ServerAsyncApiMessageBinding; - }): Map { - const messageBindings = new Map(); - if (serverMessageBindings !== undefined) { - Object.keys(serverMessageBindings).forEach((protocol) => { - messageBindings.set( - protocol, - this.mapServerAsyncApiMessageBinding(serverMessageBindings[protocol]) - ); - }); - } - return messageBindings; - } - - private mapServerAsyncApiMessageBinding( - serverMessageBinding: ServerAsyncApiMessageBinding - ): MessageBinding { - const messageBinding: MessageBinding = {}; - - Object.keys(serverMessageBinding).forEach((key) => { - const value = serverMessageBinding[key]; - if (typeof value === "object") { - messageBinding[key] = this.mapSchema("MessageBinding", value); - } else { - messageBinding[key] = value; - } - }); - - return messageBinding; - } - - private mapOperation( - operationType: OperationType, - message: Message, - bindings?: { [protocol: string]: object } - ): Operation { - return { - protocol: this.getProtocol(bindings), - operation: operationType, - message, - bindings, - }; - } - - private getProtocol(bindings?: { [protocol: string]: object }): string { - return Object.keys(bindings)[0]; - } - - private mapSchemas( - schemas: Map - ): Map { - const s = new Map(); - Object.entries(schemas).forEach(([k, v]) => s.set(k, this.mapSchema(k, v))); - return s; - } - - private mapSchema(schemaName: string, schema: ServerAsyncApiSchema): Schema { - const anchorUrl = schema.$ref - ? AsyncApiMapperService.BASE_URL + schema.$ref?.split("/")?.pop() - : undefined; - const properties = - schema.properties !== undefined - ? this.mapSchemas(schema.properties) - : undefined; - const items = - schema.items !== undefined - ? this.mapSchema(schema.$ref + "[]", schema.items) - : undefined; - const example = - schema.example !== undefined ? new Example(schema.example) : undefined; - return { - name: schemaName, - title: schemaName.split(".")?.pop(), - description: schema.description, - refName: schema.$ref, - refTitle: schema.$ref?.split("/")?.pop(), - anchorIdentifier: "#" + schemaName, - anchorUrl: anchorUrl, - type: schema.type, - items, - format: schema.format, - enum: schema.enum, - properties, - required: schema.required, - example, - }; - } -} diff --git a/springwolf-ui/src/app/shared/asyncapi.service.ts b/springwolf-ui/src/app/shared/asyncapi.service.ts deleted file mode 100644 index 4ddefc161..000000000 --- a/springwolf-ui/src/app/shared/asyncapi.service.ts +++ /dev/null @@ -1,34 +0,0 @@ -/* SPDX-License-Identifier: Apache-2.0 */ -import { AsyncApi } from "./models/asyncapi.model"; -import { HttpClient } from "@angular/common/http"; -import { Injectable } from "@angular/core"; -import { Observable, of } from "rxjs"; -import { map } from "rxjs/operators"; -import { Endpoints } from "./endpoints"; -import { - AsyncApiMapperService, - ServerAsyncApi, -} from "./asyncapi-mapper.service"; - -@Injectable() -export class AsyncApiService { - private docs: AsyncApi; - - constructor( - private http: HttpClient, - private asyncApiMapperService: AsyncApiMapperService - ) {} - - public getAsyncApi(): Observable { - if (this.docs) { - return of(this.docs); - } - - return this.http.get(Endpoints.docs).pipe( - map((item) => { - this.docs = this.asyncApiMapperService.toAsyncApi(item); - return this.docs; - }) - ); - } -} diff --git a/springwolf-ui/src/favicon.ico b/springwolf-ui/src/assets/favicon.ico similarity index 100% rename from springwolf-ui/src/favicon.ico rename to springwolf-ui/src/assets/favicon.ico diff --git a/springwolf-ui/src/asyncapi-ui.html b/springwolf-ui/src/asyncapi-ui.html index ac5457907..e66223e0a 100644 --- a/springwolf-ui/src/asyncapi-ui.html +++ b/springwolf-ui/src/asyncapi-ui.html @@ -5,7 +5,7 @@ Springwolf - +