From 81c2c5dce46fcaa295a57f1cfcb87b24e0f08782 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20M=C3=BCller?= Date: Thu, 12 Jan 2023 15:09:44 +0100 Subject: [PATCH 1/5] feat: Add new tab with message binding to channel documentation and add header and message binding example to example tab --- .../channel-main/channel-main.component.html | 51 +++++--- .../channel-main/channel-main.component.ts | 56 +++++++-- src/app/shared/asyncapi-mapper.service.ts | 112 ++++++++++++------ .../shared/components/json/json.component.ts | 4 +- .../mock/mock.springwolf-kafka-example.json | 42 +++++++ src/app/shared/models/channel.model.ts | 7 ++ src/app/shared/models/example.model.ts | 11 +- 7 files changed, 214 insertions(+), 69 deletions(-) diff --git a/src/app/channels/channel-main/channel-main.component.html b/src/app/channels/channel-main/channel-main.component.html index f2a94bd..ac9398f 100644 --- a/src/app/channels/channel-main/channel-main.component.html +++ b/src/app/channels/channel-main/channel-main.component.html @@ -7,21 +7,42 @@

{{ operation.message.description }}

- +
+

Binding

+ +
+
+

Header

+ +
+
+

Message

+ +
- - +
@@ -43,16 +64,14 @@

- +
- + + + +
diff --git a/src/app/channels/channel-main/channel-main.component.ts b/src/app/channels/channel-main/channel-main.component.ts index 02189d1..42259e6 100644 --- a/src/app/channels/channel-main/channel-main.component.ts +++ b/src/app/channels/channel-main/channel-main.component.ts @@ -4,7 +4,7 @@ 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 { MatSnackBar } from '@angular/material/snack-bar'; -import { Operation } from 'src/app/shared/models/channel.model'; +import {MessageBinding, Operation} from 'src/app/shared/models/channel.model'; import { STATUS } from 'angular-in-memory-web-api'; @Component({ @@ -27,6 +27,9 @@ export class ChannelMainComponent implements OnInit { headersExample: Example; headersTextAreaLineCount: number; protocolName: string; + messageBindingExample?: Example; + messageBindingExampleTextAreaLineCount: number; + headersTextArea?: HTMLTextAreaElement; constructor( private asyncApiService: AsyncApiService, @@ -38,47 +41,82 @@ export class ChannelMainComponent implements OnInit { ngOnInit(): void { this.asyncApiService.getAsyncApi().subscribe( asyncapi => { - let schemas: Map = asyncapi.components.schemas; - this.schemaName = this.operation.message.payload.name.slice(this.operation.message.payload.name.lastIndexOf('/') + 1) + const schemas: Map = asyncapi.components.schemas; + this.schemaName = this.operation.message.payload.name.slice(this.operation.message.payload.name.lastIndexOf('/') + 1); this.schema = schemas.get(this.schemaName); this.defaultExample = this.schema.example; this.exampleTextAreaLineCount = this.defaultExample?.lineCount || 0; - this.headersSchemaName = this.operation.message.headers.name.slice(this.operation.message.headers.name.lastIndexOf('/') + 1) + this.headersSchemaName = this.operation.message.headers.name.slice(this.operation.message.headers.name.lastIndexOf('/') + 1); this.headers = schemas.get(this.headersSchemaName); this.headersExample = this.headers.example; this.headersTextAreaLineCount = this.headersExample?.lineCount || 0; + this.messageBindingExampleTextAreaLineCount = this.messageBindingExample?.lineCount || 0; } ); this.protocolName = Object.keys(this.operation.bindings)[0]; } + isEmptyObject(object?: any): boolean { + return (object === undefined || object === null) || Object.keys(object).length > 0; + } + + createMessageBindingExample(messageBinding?: MessageBinding): Example | undefined { + if (messageBinding === undefined || messageBinding === null) { + return undefined; + } + + const bindingExampleObject = {}; + Object.keys(messageBinding).forEach((bindingKey) => { + if (bindingKey !== 'bindingVersion') { + bindingExampleObject[bindingKey] = this.getExampleValue(messageBinding[bindingKey]); + } + }); + + const bindingExample = new Example(bindingExampleObject); + + this.messageBindingExampleTextAreaLineCount = bindingExample.lineCount; + + return bindingExample; + } + + getExampleValue(bindingValue: string | Schema): any { + if (typeof bindingValue === 'string') { + return bindingValue; + } else { + return bindingValue.example.value; + } + } + recalculateLineCount(field: string, text: string): void { switch (field) { case 'example': this.exampleTextAreaLineCount = text.split('\n').length; break; case 'headers': - this.headersTextAreaLineCount = text.split('\n').length + this.headersTextAreaLineCount = text.split('\n').length; + break; + case 'massageBindingExample': + this.messageBindingExampleTextAreaLineCount = text.split('\n').length; break; } } - publish(example: string, headers: string): void { + publish(example: string, headers?: string): void { try { const payloadJson = JSON.parse(example); - const headersJson = JSON.parse(headers) + const headersJson = JSON.parse(headers); this.publisherService.publish(this.protocolName, this.channelName, payloadJson, headersJson).subscribe( _ => this.handlePublishSuccess(), err => this.handlePublishError(err) ); - } catch(error) { + } catch (error) { this.snackBar.open('Example payload is not valid', 'ERROR', { duration: 3000 - }) + }); } } diff --git a/src/app/shared/asyncapi-mapper.service.ts b/src/app/shared/asyncapi-mapper.service.ts index 6a6aa8c..5e70ff6 100644 --- a/src/app/shared/asyncapi-mapper.service.ts +++ b/src/app/shared/asyncapi-mapper.service.ts @@ -1,10 +1,10 @@ -import { AsyncApi } from './models/asyncapi.model'; -import { Server } from './models/server.model'; -import {Channel, 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 {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; @@ -26,6 +26,10 @@ interface ServerAsyncApiMessage { description?: string; payload: { $ref: string }; headers: { $ref: string }; + bindings: {[key: string]: ServerAsyncApiMessageBinding}; +} +interface ServerAsyncApiMessageBinding { + [key: string]: ServerAsyncApiSchema | string; } interface ServerAsyncApiInfo { @@ -64,7 +68,7 @@ export interface ServerAsyncApi { @Injectable() export class AsyncApiMapperService { - static BASE_URL = window.location.pathname + window.location.search + "#"; + static BASE_URL = window.location.pathname + window.location.search + '#'; constructor() { } @@ -89,49 +93,52 @@ export class AsyncApiMapperService { }; } - private mapServers(servers: ServerAsyncApi["servers"]): Map { + 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[] { + 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 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)) + 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[] + description: ServerAsyncApi['channels']['']['description'], + serverOperation: ServerAsyncApi['channels']['']['subscribe'] | ServerAsyncApi['channels']['']['publish'], + operationType: OperationType + ): Channel[] { - if(serverOperation !== undefined) { - let messages: Message[] = this.mapMessages(serverOperation.message) + if (serverOperation !== undefined) { + const messages: Message[] = this.mapMessages(serverOperation.message); return messages.map(message => { - const operation = this.mapOperation(operationType, message, serverOperation.bindings) + 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: description, - operation: operation, - } - }) + 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) + if ('oneOf' in message) { + return this.mapServerAsyncApiMessages(message.oneOf); } return this.mapServerAsyncApiMessages([message]); } @@ -144,23 +151,50 @@ export class AsyncApiMapperService { description: v.description, payload: { name: v.payload.$ref, - anchorUrl: AsyncApiMapperService.BASE_URL +v.payload.$ref?.split('/')?.pop() + anchorUrl: AsyncApiMapperService.BASE_URL + v.payload.$ref?.split('/')?.pop() }, headers: { name: v.headers.$ref, anchorUrl: AsyncApiMapperService.BASE_URL + v.headers.$ref?.split('/')?.pop() - } - } - }) + }, + bindings: this.mapServerAsyncApiMessageBindings(v.bindings) + }; + }); } - private mapOperation(operationType: OperationType, message: Message, bindings?: any): Operation { + private mapServerAsyncApiMessageBindings( + serverMessageBindings: { [type: string]: ServerAsyncApiMessageBinding } + ): Map { + const messageBindings = new Map(); + 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?: any): Operation { return { protocol: this.getProtocol(bindings), operation: operationType, - message: message, - bindings: bindings - } + message, + bindings + }; } private getProtocol(bindings?: any): string { @@ -184,12 +218,12 @@ export class AsyncApiMapperService { anchorIdentifier: '#' + schemaName, anchorUrl: anchorUrl, type: schema.type, - items: items, + items, format: schema.format, enum: schema.enum, - properties: properties, + properties, required: schema.required, - example: example, - } + example, + }; } } diff --git a/src/app/shared/components/json/json.component.ts b/src/app/shared/components/json/json.component.ts index e587d00..3fd595d 100644 --- a/src/app/shared/components/json/json.component.ts +++ b/src/app/shared/components/json/json.component.ts @@ -12,10 +12,10 @@ import { Component, OnInit, Input } from '@angular/core'; export class JsonComponent implements OnInit { @Input() data: any; - json: string; + @Input() json: string; ngOnInit(): void { - this.json = JSON.stringify(this.data, null, 2); + this.json = this.json === undefined ? JSON.stringify(this.data, null, 2) : this.json; } } diff --git a/src/app/shared/mock/mock.springwolf-kafka-example.json b/src/app/shared/mock/mock.springwolf-kafka-example.json index 24df8d7..03d6347 100644 --- a/src/app/shared/mock/mock.springwolf-kafka-example.json +++ b/src/app/shared/mock/mock.springwolf-kafka-example.json @@ -40,6 +40,9 @@ }, "headers" : { "$ref" : "#/components/schemas/HeadersNotDocumented" + }, + "bindings" : { + "kafka" : { } } } }, @@ -63,6 +66,9 @@ }, "headers" : { "$ref" : "#/components/schemas/HeadersNotDocumented" + }, + "bindings" : { + "kafka" : { } } } }, @@ -87,6 +93,18 @@ }, "headers" : { "$ref" : "#/components/schemas/CloudEventHeadersForAnotherPayloadDtoEndpoint" + }, + "bindings" : { + "kafka" : { + "key" : { + "type" : "string", + "description" : "Kafka Producer Message Key", + "example" : "example-key", + "exampleSetFlag" : true, + "types" : [ "string" ] + }, + "bindingVersion" : "1" + } } }, { "name" : "io.github.stavshamir.springwolf.example.dtos.ExamplePayloadDto", @@ -96,6 +114,18 @@ }, "headers" : { "$ref" : "#/components/schemas/SpringDefaultHeaders" + }, + "bindings" : { + "kafka" : { + "key" : { + "type" : "string", + "description" : "Kafka Producer Message Key", + "example" : "example-key", + "exampleSetFlag" : true, + "types" : [ "string" ] + }, + "bindingVersion" : "1" + } } } ] } @@ -119,6 +149,9 @@ }, "headers" : { "$ref" : "#/components/schemas/HeadersNotDocumented" + }, + "bindings" : { + "kafka" : { } } } }, @@ -142,6 +175,9 @@ }, "headers" : { "$ref" : "#/components/schemas/SpringDefaultHeaders-AnotherPayloadDto" + }, + "bindings" : { + "kafka" : { } } }, { "name" : "io.github.stavshamir.springwolf.example.dtos.ExamplePayloadDto", @@ -151,6 +187,9 @@ }, "headers" : { "$ref" : "#/components/schemas/SpringDefaultHeaders-ExamplePayloadDto" + }, + "bindings" : { + "kafka" : { } } }, { "name" : "javax.money.MonetaryAmount", @@ -160,6 +199,9 @@ }, "headers" : { "$ref" : "#/components/schemas/SpringDefaultHeaders-MonetaryAmount" + }, + "bindings" : { + "kafka" : { } } } ] } diff --git a/src/app/shared/models/channel.model.ts b/src/app/shared/models/channel.model.ts index f4d6aea..61ef230 100644 --- a/src/app/shared/models/channel.model.ts +++ b/src/app/shared/models/channel.model.ts @@ -1,3 +1,5 @@ +import {Schema} from './schema.model'; + export const CHANNEL_ANCHOR_PREFIX = "#channel-" export interface Channel { name: string; @@ -26,4 +28,9 @@ export interface Message { name: string anchorUrl: string; }; + bindings?: Map; +} + +export interface MessageBinding { + [type: string]: string | Schema; } diff --git a/src/app/shared/models/example.model.ts b/src/app/shared/models/example.model.ts index f60b3e1..76c2f3f 100644 --- a/src/app/shared/models/example.model.ts +++ b/src/app/shared/models/example.model.ts @@ -3,9 +3,14 @@ export class Example { public value: string; public lineCount: number; - constructor(exampleObject: object) { - this.value = JSON.stringify(exampleObject, null, 2); + constructor(exampleObject: object | string) { + if (typeof exampleObject === 'string') { + this.value = exampleObject; + } else { + this.value = JSON.stringify(exampleObject, null, 2); + } + this.lineCount = this.value.split('\n').length; } -} \ No newline at end of file +} From 30c1e7b3dbfc9b44f7c196ab35ace29fabaa35e2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20M=C3=BCller?= Date: Fri, 13 Jan 2023 09:44:52 +0100 Subject: [PATCH 2/5] Add message binding to the request made by PublisherService so that the backend can use the message binding to construct a message --- .../channel-main/channel-main.component.css | 4 +++ .../channel-main/channel-main.component.html | 31 ++++++++++++------- .../channel-main/channel-main.component.ts | 8 ++--- src/app/shared/publisher.service.ts | 10 +++--- 4 files changed, 33 insertions(+), 20 deletions(-) diff --git a/src/app/channels/channel-main/channel-main.component.css b/src/app/channels/channel-main/channel-main.component.css index 4d904c5..a0874fe 100644 --- a/src/app/channels/channel-main/channel-main.component.css +++ b/src/app/channels/channel-main/channel-main.component.css @@ -33,3 +33,7 @@ button { padding: 6px; font-weight: normal; } + +[hidden] { + display: none !important; +} diff --git a/src/app/channels/channel-main/channel-main.component.html b/src/app/channels/channel-main/channel-main.component.html index ac9398f..0d29a32 100644 --- a/src/app/channels/channel-main/channel-main.component.html +++ b/src/app/channels/channel-main/channel-main.component.html @@ -7,7 +7,7 @@

{{ operation.message.description }}

-
+

Binding

-
-

Header

- +
+ +

Header

+ +

Message

@@ -35,11 +37,18 @@

Message

>
- diff --git a/src/app/channels/channel-main/channel-main.component.ts b/src/app/channels/channel-main/channel-main.component.ts index 42259e6..d8facb5 100644 --- a/src/app/channels/channel-main/channel-main.component.ts +++ b/src/app/channels/channel-main/channel-main.component.ts @@ -29,7 +29,6 @@ export class ChannelMainComponent implements OnInit { protocolName: string; messageBindingExample?: Example; messageBindingExampleTextAreaLineCount: number; - headersTextArea?: HTMLTextAreaElement; constructor( private asyncApiService: AsyncApiService, @@ -60,7 +59,7 @@ export class ChannelMainComponent implements OnInit { } isEmptyObject(object?: any): boolean { - return (object === undefined || object === null) || Object.keys(object).length > 0; + return (object === undefined || object === null) || Object.keys(object).length === 0; } createMessageBindingExample(messageBinding?: MessageBinding): Example | undefined { @@ -104,12 +103,13 @@ export class ChannelMainComponent implements OnInit { } } - publish(example: string, headers?: string): void { + publish(example: string, headers?: string, bindings?: string): void { try { const payloadJson = JSON.parse(example); const headersJson = JSON.parse(headers); + const bindingsJson = JSON.parse(bindings); - this.publisherService.publish(this.protocolName, this.channelName, payloadJson, headersJson).subscribe( + this.publisherService.publish(this.protocolName, this.channelName, payloadJson, headersJson, bindingsJson).subscribe( _ => this.handlePublishSuccess(), err => this.handlePublishError(err) ); diff --git a/src/app/shared/publisher.service.ts b/src/app/shared/publisher.service.ts index 436dea9..86ddb39 100644 --- a/src/app/shared/publisher.service.ts +++ b/src/app/shared/publisher.service.ts @@ -1,17 +1,17 @@ -import { Injectable } from '@angular/core'; +import {Injectable} from '@angular/core'; import {HttpClient, HttpParams} from '@angular/common/http'; -import { Observable } from 'rxjs'; -import { Endpoints } from './endpoints'; +import {Observable} from 'rxjs'; +import {Endpoints} from './endpoints'; @Injectable() export class PublisherService { constructor(private http: HttpClient) { } - publish(protocol: string, topic: string, payload: object, headers: object): Observable { + publish(protocol: string, topic: string, payload: object, headers: object, bindings: object): Observable { const url = Endpoints.getPublishEndpoint(protocol); const params = new HttpParams().set('topic', topic); - const body = {"payload" : payload, "headers" : headers } + const body = {payload, headers, bindings}; console.log(`Publishing to ${url}`); return this.http.post(url, body, { params }); } From e6f46286e295e05d8731dcad04151b8bf2768738 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20M=C3=BCller?= Date: Fri, 13 Jan 2023 10:47:24 +0100 Subject: [PATCH 3/5] Improve mapping of message bindings to be more resilient if a backend creates a specification without message binding --- src/app/shared/asyncapi-mapper.service.ts | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/app/shared/asyncapi-mapper.service.ts b/src/app/shared/asyncapi-mapper.service.ts index 5e70ff6..2665dc5 100644 --- a/src/app/shared/asyncapi-mapper.service.ts +++ b/src/app/shared/asyncapi-mapper.service.ts @@ -163,12 +163,14 @@ export class AsyncApiMapperService { } private mapServerAsyncApiMessageBindings( - serverMessageBindings: { [type: string]: ServerAsyncApiMessageBinding } + serverMessageBindings?: { [type: string]: ServerAsyncApiMessageBinding } ): Map { const messageBindings = new Map(); - Object.keys(serverMessageBindings).forEach((protocol) => { - messageBindings.set(protocol, this.mapServerAsyncApiMessageBinding(serverMessageBindings[protocol])); - }); + if (serverMessageBindings !== undefined) { + Object.keys(serverMessageBindings).forEach((protocol) => { + messageBindings.set(protocol, this.mapServerAsyncApiMessageBinding(serverMessageBindings[protocol])); + }); + } return messageBindings; } From 92d8bf1a694d31399f65bb731b4c8d7d8fcd6f8f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20M=C3=BCller?= Date: Fri, 17 Feb 2023 15:15:07 +0100 Subject: [PATCH 4/5] Rename Binding in example tab to Message Binding and add full payload to log statement when producing messages --- src/app/channels/channel-main/channel-main.component.html | 2 +- src/app/shared/publisher.service.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/app/channels/channel-main/channel-main.component.html b/src/app/channels/channel-main/channel-main.component.html index 0d29a32..baf1a46 100644 --- a/src/app/channels/channel-main/channel-main.component.html +++ b/src/app/channels/channel-main/channel-main.component.html @@ -8,7 +8,7 @@

{{ operation.message.description }}

-

Binding

+

Message Binding