Skip to content

Commit e6565fd

Browse files
authored
Feature/add generateion of async api spec (#198)
* Add open api improvement * Add new condition for open api generation * Update generate-open-api.component.ts * Add e2e tests * Update generate-open-api.cy.ts * Add error message when resource path clash with properteis * Add documentation link to open api generation with resource path * Add dialog for async api generation * Update generate-async-api.component.ts * Revert open api generation * Update generate-open-api.component.ts * Update generate-open-api.component.ts * Fix cypress tests * Add doc for async api * Add async api doc into navigation
1 parent 91362cf commit e6565fd

File tree

30 files changed

+522
-14
lines changed

30 files changed

+522
-14
lines changed
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
/* eslint-disable cypress/no-unnecessary-waiting */
2+
/*
3+
* Copyright (c) 2024 Robert Bosch Manufacturing Solutions GmbH
4+
*
5+
* See the AUTHORS file(s) distributed with this work for
6+
* additional information regarding authorship.
7+
*
8+
* This Source Code Form is subject to the terms of the Mozilla Public
9+
* License, v. 2.0. If a copy of the MPL was not distributed with this
10+
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
11+
*
12+
* SPDX-License-Identifier: MPL-2.0
13+
*/
14+
15+
/// <reference types="Cypress" />
16+
17+
import {
18+
GENERATION_tbApplicationIdInput,
19+
GENERATION_tbChannelAddressInput,
20+
GENERATION_tbGenerateAsyncApiButton,
21+
GENERATION_tbOutputButton,
22+
GENERATION_tbOutputButton_JSON,
23+
GENERATION_writeSeparateFilesCheckbox,
24+
} from '../../support/constants';
25+
26+
describe('Test generation and download of async api specification', () => {
27+
it('Can generate valid JSON Async Api Specification', () => {
28+
cy.visitDefault();
29+
cy.startModelling()
30+
.then(() => cy.openGenerationAsyncApiSpec().wait(500))
31+
.then(() => cy.get(GENERATION_tbOutputButton).click())
32+
.then(() => cy.get(GENERATION_tbOutputButton_JSON).click())
33+
.then(() => cy.get(GENERATION_tbApplicationIdInput).focus().clear().type('application:id').blur())
34+
.then(() => cy.get(GENERATION_tbChannelAddressInput).focus().clear().type('foo/bar').blur())
35+
.then(() => cy.get(GENERATION_tbGenerateAsyncApiButton).click().wait(5000))
36+
.then(() => cy.fixture('cypress/downloads/en-async-api.json'));
37+
});
38+
39+
it('Can generate valid YAML Async Api Specification', () => {
40+
cy.visitDefault();
41+
cy.startModelling()
42+
.then(() => cy.openGenerationAsyncApiSpec().wait(500))
43+
.wait(500)
44+
.then(() => cy.get(GENERATION_tbApplicationIdInput).focus().clear().type('application:id').blur())
45+
.then(() => cy.get(GENERATION_tbChannelAddressInput).focus().clear().type('foo/bar').blur())
46+
.then(() => cy.get(GENERATION_tbGenerateAsyncApiButton).click().wait(5000))
47+
.then(() => cy.fixture('cypress/downloads/en-async-api.yaml'));
48+
});
49+
50+
it('Can generate and download valid package for Async Api Specification', () => {
51+
cy.visitDefault();
52+
cy.startModelling()
53+
.then(() => cy.openGenerationAsyncApiSpec().wait(500))
54+
.then(() => cy.get(GENERATION_tbOutputButton).click())
55+
.then(() => cy.get(GENERATION_tbOutputButton_JSON).click())
56+
.then(() => cy.get(GENERATION_tbApplicationIdInput).focus().clear().type('application:id').blur())
57+
.then(() => cy.get(GENERATION_tbChannelAddressInput).focus().clear().type('foo/bar').blur())
58+
.then(() => cy.get(GENERATION_writeSeparateFilesCheckbox).click())
59+
.then(() => cy.get(GENERATION_tbGenerateAsyncApiButton).click().wait(5000))
60+
.then(() => cy.fixture('cypress/downloads/en-async-api.zip'));
61+
});
62+
});

core/apps/ame-e2e/src/support/commands.ts

Lines changed: 35 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,12 @@ declare global {
6262
*/
6363
openGenerationOpenApiSpec(): Chainable;
6464

65+
/**
66+
* Custom command to open the generation of AsyncAPI specification.
67+
* @returns {Cypress.Chainable} A chainable Cypress object.
68+
*/
69+
openGenerationAsyncApiSpec(): Chainable;
70+
6571
/**
6672
* Custom command to open the generation of documentation.
6773
* @returns {Cypress.Chainable} A chainable Cypress object.
@@ -529,25 +535,25 @@ Cypress.Commands.add('saveAspectModelToWorkspace', () => {
529535
Cypress.Commands.add('openGenerationOpenApiSpec', () => {
530536
cy.intercept(
531537
'POST',
532-
'http://localhost:9091/ame/api/generate/open-api-spec?language=en&output=json&baseUrl=https://example.com&includeQueryApi=false&pagingOption=NO_PAGING&resourcePath=null&ymlProperties=&jsonProperties=',
538+
'http://localhost:9091/ame/api/generate/open-api-spec?language=en&output=json&baseUrl=https://example.com&includeQueryApi=false&useSemanticVersion=false&pagingOption=NO_PAGING&resourcePath=null&ymlProperties=&jsonProperties=',
533539
{fixture: 'valid-open-api.json'},
534540
);
535541

536542
cy.intercept(
537543
'POST',
538-
'http://localhost:9091/ame/api/generate/open-api-spec?language=en&output=yaml&baseUrl=https://example.com&includeQueryApi=false&pagingOption=NO_PAGING&resourcePath=null&ymlProperties=&jsonProperties=',
544+
'http://localhost:9091/ame/api/generate/open-api-spec?language=en&output=yaml&baseUrl=https://example.com&includeQueryApi=false&useSemanticVersion=false&pagingOption=NO_PAGING&resourcePath=null&ymlProperties=&jsonProperties=',
539545
{fixture: 'valid-open-api.yaml'},
540546
);
541547

542548
cy.intercept(
543549
'POST',
544-
'http://localhost:9091/ame/api/generate/open-api-spec?language=en&output=json&baseUrl=https://example.com&includeQueryApi=false&pagingOption=NO_PAGING&resourcePath=/resource/%7BresourceId%7D&ymlProperties=&jsonProperties=%7B%0A%20%20%22key%22:%20%22value%22%0A%7D',
550+
'http://localhost:9091/ame/api/generate/open-api-spec?language=en&output=json&baseUrl=https://example.com&includeQueryApi=false&useSemanticVersion=false&pagingOption=NO_PAGING&resourcePath=/resource/%7BresourceId%7D&ymlProperties=&jsonProperties=%7B%0A%20%20%22key%22:%20%22value%22%0A%7D',
545551
{fixture: 'valid-open-api.json'},
546552
);
547553

548554
cy.intercept(
549555
'POST',
550-
'http://localhost:9091/ame/api/generate/open-api-spec?language=en&output=yaml&baseUrl=https://example.com&includeQueryApi=false&pagingOption=NO_PAGING&resourcePath=/resource/%7BresourceId%7D&ymlProperties=resourceId:%0A%20%20name:%20resourceId%0A%20%20in:%20path%0A%20%20description:%20An%20example%20resource%20Id.%0A%20%20required:%20true%0A%20%20schema:%0A%20%20%20%20type:%20string%0A&jsonProperties=',
556+
'http://localhost:9091/ame/api/generate/open-api-spec?language=en&output=yaml&baseUrl=https://example.com&includeQueryApi=false&useSemanticVersion=false&pagingOption=NO_PAGING&resourcePath=/resource/%7BresourceId%7D&ymlProperties=resourceId:%0A%20%20name:%20resourceId%0A%20%20in:%20path%0A%20%20description:%20An%20example%20resource%20Id.%0A%20%20required:%20true%0A%20%20schema:%0A%20%20%20%20type:%20string%0A&jsonProperties=',
551557
{fixture: 'valid-open-api.yaml'},
552558
);
553559

@@ -557,6 +563,31 @@ Cypress.Commands.add('openGenerationOpenApiSpec', () => {
557563
});
558564
});
559565

566+
Cypress.Commands.add('openGenerationAsyncApiSpec', () => {
567+
cy.intercept(
568+
'POST',
569+
'http://localhost:9091/ame/api/generate/async-api-spec?language=en&output=json&applicationId=application:id&channelAddress=foo/bar&useSemanticVersion=false&writeSeparateFiles=false',
570+
{fixture: 'valid-open-api.json'},
571+
);
572+
573+
cy.intercept(
574+
'POST',
575+
'http://localhost:9091/ame/api/generate/async-api-spec?language=en&output=yaml&applicationId=application:id&channelAddress=foo/bar&useSemanticVersion=false&writeSeparateFiles=false',
576+
{fixture: 'valid-open-api.json'},
577+
);
578+
579+
cy.intercept(
580+
'POST',
581+
'http://localhost:9091/ame/api/generate/async-api-spec?language=en&output=json&applicationId=application:id&channelAddress=foo/bar&useSemanticVersion=false&writeSeparateFiles=true',
582+
{fixture: 'valid-open-api.json'},
583+
);
584+
585+
return cy.window().then(win => {
586+
const generateHandlingService: GenerateHandlingService = win['angular.generateHandlingService'];
587+
return generateHandlingService.openGenerationAsyncApiSpec().afterClosed().subscribe();
588+
});
589+
});
590+
560591
Cypress.Commands.add('openGenerationDocumentation', () => {
561592
cy.intercept('POST', 'http://localhost:9091/ame/api/generate/documentation?language=en', {fixture: 'valid-documentation.html'});
562593

core/apps/ame-e2e/src/support/constants.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -138,12 +138,16 @@ export enum SettingsDialogSelectors {
138138

139139
// Generation
140140
export const GENERATION_tbGenerateOpenApiButton = '[data-cy="tbGenerateOpenApiButton"]';
141+
export const GENERATION_tbGenerateAsyncApiButton = '[data-cy="tbGenerateAsyncApiButton"]';
141142
export const GENERATION_tbOutputButton = '[data-cy="tbOutputButton"]';
142143
export const GENERATION_tbOutputButton_YAML = '[data-cy="tbOutputButton-yaml"]';
143144
export const GENERATION_tbOutputButton_JSON = '[data-cy="tbOutputButton-json"]';
144145
export const GENERATION_tbBaseUrlInput = '[data-cy="tbBaseUrlInput"]';
145146
export const GENERATION_tbBaseUrlInputError = '[data-cy="tbBaseUrlInputError"]';
147+
export const GENERATION_tbApplicationIdInput = '[data-cy="tbApplicationIdInput"]';
148+
export const GENERATION_tbChannelAddressInput = '[data-cy="tbChannelAddressInput"]';
146149
export const GENERATION_activateResourcePathCheckbox = '[data-cy="activateResourcePathCheckbox"]';
150+
export const GENERATION_writeSeparateFilesCheckbox = '[data-cy="writeSeparateFilesCheckbox"]';
147151
export const GENERATION_resourcePathTitle = '[data-cy="resourcePathTitle"]';
148152
export const GENERATION_resourcePathInput = '[data-cy="resourcePathInput"]';
149153
export const GENERATION_resourcePathRequiredError = '[data-cy="resourcePathRequiredError"]';

core/apps/ame/src/assets/i18n/en.json

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -254,10 +254,10 @@
254254
"OUTPUT_FILE_FORMAT": "Output File format",
255255
"BASEURL": "Base Url - the base URL for the Aspect API",
256256
"PLEASE_ADD_VALID_URL": "Please add a valid url",
257-
"INCLUDE_QUERY_API_TOOLTIP": "If enabled, a dedicated section for the Query API Endpoint of the Aspect API will be included in the specification.",
257+
"INCLUDE_QUERY_API_TOOLTIP": "Include the path for the Query Aspect API Endpoint in the OpenApi specification.",
258258
"INCLUDE_QUERY_API": "Include Query Api",
259259
"USE_SEMANTIC_VERSION": "Use Semantic Version",
260-
"USE_SEMANTIC_VERSION_TOOLTIP": "If enabled, the complete semantic version of the Aspect Model will be utilized as the API version. Otherwise, only the major part of the Aspect Version will be used as the API version.",
260+
"USE_SEMANTIC_VERSION_TOOLTIP": "Use the full semantic version from the Aspect Model as the version for the Aspect API.",
261261
"ACTIVATE_RESOURCE_PATH": "If enabled, the resource path will be activated in the OpenAPI specification.",
262262
"ACTIVATE_RESOURCE_PATH_TOOLTIP": "Activate Resource Path",
263263
"RESOURCE_PATH": "Resource Path - The resource path for the Aspect API endpoints",
@@ -282,6 +282,25 @@
282282
"GENERATE": "Generate"
283283
}
284284
},
285+
"GENERATE_ASYNCAPI_SPEC_DIALOG": {
286+
"TITLE": "Generate AsyncAPI specification",
287+
"GENERATING": "Generate",
288+
"CONFIGURATION": "Configuration",
289+
"LANGUAGE": "Language",
290+
"OUTPUT_FILE_FORMAT": "Output File format",
291+
"APPLICATION_ID": "Application ID - sets the application id, e.g. an identifying URL",
292+
"CHANNEL_ADDRESS": "Channel Address - sets the channel address (i.e., for MQTT, the topic's name)",
293+
"CHANNEL_ADDRESS_ERROR": "Channel address is pattern is not matching",
294+
"USE_SEMANTIC_VERSION": "Use Semantic Version",
295+
"USE_SEMANTIC_VERSION_TOOLTIP": "Use the full semantic version from the Aspect Model as the version for the Aspect API.",
296+
"WRITE_SEPARATE_FILES": "Write Separate Files",
297+
"WRITE_SEPARATE_FILES_TOOLTIP": "Package each schema into individual files.",
298+
"BUTTON": {
299+
"CANCEL": "Cancel",
300+
"GENERATE": "Generate"
301+
}
302+
},
303+
285304
"GENERATE_HANDLING": {
286305
"FAIL_GENERATE_OPENAPI_SPEC": "Failed to generate Open Api specification",
287306
"INVALID_MODEL": "Invalid Aspect Model",
@@ -632,6 +651,7 @@
632651
"LABEL": "Generate",
633652
"HTML_DOCUMENTATION": "HTML Documentation",
634653
"OPEN_API_SPECIFICATION": "OpenAPI Specification",
654+
"ASYNC_API_SPECIFICATION": "AsyncAPI Specification",
635655
"AASX_XML": "AASX / XML",
636656
"SAMPLE_JSON_PAYLOAD": "Sample JSON Payload",
637657
"JSON_SCHEMA": "JSON Schema"
519 Bytes
Loading
519 Bytes
Loading

core/electron-libs/consts.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -161,6 +161,10 @@ icons = {
161161
disabled: `${disabledIconsPath}${path.sep}api_FILL0_wght400_GRAD0_opsz24.png`,
162162
enabled: `${enabledIconsPath}${path.sep}api_FILL0_wght400_GRAD0_opsz24.png`,
163163
},
164+
GENERATE_ASYNC_API_SPECIFICATION: {
165+
disabled: `${disabledIconsPath}${path.sep}async_FILL0_wght400_GRAD0_opsz24.png`,
166+
enabled: `${enabledIconsPath}${path.sep}async_FILL0_wght400_GRAD0_opsz24.png`,
167+
},
164168
GENERATE_AASX_XML: {
165169
disabled: `${disabledIconsPath}${path.sep}data_array_FILL0_wght400_GRAD0_opsz24.png`,
166170
enabled: `${enabledIconsPath}${path.sep}data_array_FILL0_wght400_GRAD0_opsz24.png`,

core/electron-libs/events.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,7 @@ module.exports = {
9090
SIGNAL_VALIDATE_MODEL: 'SIGNAL_VALIDATE_MODEL',
9191
SIGNAL_GENERATE_HTML_DOCUMENTATION: 'SIGNAL_GENERATE_HTML_DOCUMENTATION',
9292
SIGNAL_GENERATE_OPEN_API_SPECIFICATION: 'SIGNAL_GENERATE_OPEN_API_SPECIFICATION',
93+
SIGNAL_GENERATE_ASYNC_API_SPECIFICATION: 'SIGNAL_GENERATE_ASYNC_API_SPECIFICATION',
9394
SIGNAL_GENERATE_AASX_XML: 'SIGNAL_GENERATE_AASX_XML',
9495
SIGNAL_GENERATE_JSON_PAYLOAD: 'SIGNAL_GENERATE_JSON_PAYLOAD',
9596
SIGNAL_GENERATE_JSON_SCHEMA: 'SIGNAL_GENERATE_JSON_SCHEMA',

core/electron-libs/menu/generate-sub-menu.js

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ const {
1818
SIGNAL_GENERATE_JSON_PAYLOAD,
1919
SIGNAL_GENERATE_JSON_SCHEMA,
2020
SIGNAL_GENERATE_OPEN_API_SPECIFICATION,
21+
SIGNAL_GENERATE_ASYNC_API_SPECIFICATION,
2122
} = require('../events');
2223
const {getIcon} = require('./utils');
2324

@@ -35,6 +36,12 @@ function generateSubmenu(translation) {
3536
icon: getIcon(icons.GENERATE_OPEN_API_SPECIFICATION.enabled),
3637
click: (menuItem, browserWindow, _) => browserWindow.webContents.send(SIGNAL_GENERATE_OPEN_API_SPECIFICATION),
3738
},
39+
{
40+
id: 'GENERATE_ASYNC_API_SPECIFICATION',
41+
label: translation.ASYNC_API_SPECIFICATION,
42+
icon: getIcon(icons.GENERATE_ASYNC_API_SPECIFICATION.enabled),
43+
click: (menuItem, browserWindow, _) => browserWindow.webContents.send(SIGNAL_GENERATE_ASYNC_API_SPECIFICATION),
44+
},
3845
{
3946
id: 'GENERATE_AASX_XML',
4047
label: translation.AASX_XML,

core/libs/api/src/lib/model-api.service.ts

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ import {catchError, map, mergeMap, retry, tap, timeout} from 'rxjs/operators';
1717
import {forkJoin, Observable, of, throwError} from 'rxjs';
1818
import {APP_CONFIG, AppConfig, BrowserService, FileContentModel, HttpHeaderBuilder, LogService} from '@ame/shared';
1919
import {ModelValidatorService} from './model-validator.service';
20-
import {OpenApi, ViolationError} from '@ame/editor';
20+
import {AsyncApi, OpenApi, ViolationError} from '@ame/editor';
2121
import {removeCommentsFromTTL} from '@ame/utils';
2222

2323
export enum PREDEFINED_MODELS {
@@ -278,6 +278,7 @@ export class ModelApiService {
278278
output: openApi.output,
279279
baseUrl: openApi.baseUrl,
280280
includeQueryApi: openApi.includeQueryApi,
281+
useSemanticVersion: openApi.useSemanticVersion,
281282
pagingOption: openApi.paging,
282283
resourcePath: openApi.resourcePath,
283284
ymlProperties: openApi.ymlProperties || '',
@@ -294,6 +295,29 @@ export class ModelApiService {
294295
);
295296
}
296297

298+
generateAsyncApiSpec(rdfContent: string, asyncApi: AsyncApi): Observable<any> {
299+
return this.http
300+
.post<string>(`${this.serviceUrl}${this.api.generate}/async-api-spec`, rdfContent, {
301+
headers: new HttpHeaderBuilder().withContentTypeRdfTurtle().build(),
302+
params: {
303+
language: asyncApi.language,
304+
output: asyncApi.output,
305+
applicationId: asyncApi.applicationId,
306+
channelAddress: asyncApi.channelAddress,
307+
useSemanticVersion: asyncApi.useSemanticVersion,
308+
writeSeparateFiles: asyncApi.writeSeparateFiles,
309+
},
310+
responseType: asyncApi.writeSeparateFiles ? ('blob' as 'json') : asyncApi.output === 'yaml' ? ('text' as 'json') : 'json',
311+
})
312+
.pipe(
313+
timeout(this.requestTimeout),
314+
catchError(res => {
315+
res.error = asyncApi.output === 'yaml' ? JSON.parse(res.error)?.error : res.error.error;
316+
return throwError(() => res);
317+
}),
318+
);
319+
}
320+
297321
downloadDocumentation(rdfContent: string, language: string): Observable<string> {
298322
return this.http
299323
.post(`${this.serviceUrl}${this.api.generate}/documentation`, rdfContent, {

0 commit comments

Comments
 (0)