From b86c02cf984937d332ef423600bf1e1901664662 Mon Sep 17 00:00:00 2001 From: Michele Santoro <10807610+michelu89@users.noreply.github.com> Date: Fri, 19 Apr 2024 08:57:03 +0200 Subject: [PATCH] Feature/add generation of async api (#71) * Add generate async api functionality * Add e2e test collection * Update sdk version to latest one * Fix unit test for jsonschema * Fix graal native image reflection und resource --- .../.graalvm/reflect-config.json | 36 +- .../.graalvm/resource-config.json | 3 + .../postman/ame.postman_collection.json | 315 +++++++++++++++++- aspect-model-editor-service/pom.xml | 7 +- .../esmf/ame/services/GenerateService.java | 121 ++++++- .../esmf/ame/services/utils/ZipUtils.java | 35 +- .../ame/services/GenerateServiceTest.java | 68 +++- .../esmf/ame/api/GenerateController.java | 45 ++- pom.xml | 10 +- 9 files changed, 586 insertions(+), 54 deletions(-) diff --git a/aspect-model-editor-runtime/.graalvm/reflect-config.json b/aspect-model-editor-runtime/.graalvm/reflect-config.json index 96bb96cb..bc2ba296 100644 --- a/aspect-model-editor-runtime/.graalvm/reflect-config.json +++ b/aspect-model-editor-runtime/.graalvm/reflect-config.json @@ -5634,6 +5634,18 @@ "boolean", "java.util.Optional" ] + }, + { + "name": "asyncApiSpec", + "parameterTypes": [ + "java.lang.String", + "java.lang.String", + "java.lang.String", + "java.lang.String", + "java.lang.String", + "boolean", + "boolean" + ] } ] }, @@ -13411,21 +13423,21 @@ }, { "name": "sun.awt.X11FontManager", - "allDeclaredConstructors" : true, - "allPublicConstructors" : true, - "allDeclaredMethods" : true, - "allPublicMethods" : true, - "allDeclaredFields" : true, - "allPublicFields" : true + "allDeclaredConstructors": true, + "allPublicConstructors": true, + "allDeclaredMethods": true, + "allPublicMethods": true, + "allDeclaredFields": true, + "allPublicFields": true }, { "name": "sun.font.FontConfigManager", - "allDeclaredConstructors" : true, - "allPublicConstructors" : true, - "allDeclaredMethods" : true, - "allPublicMethods" : true, - "allDeclaredFields" : true, - "allPublicFields" : true + "allDeclaredConstructors": true, + "allPublicConstructors": true, + "allDeclaredMethods": true, + "allPublicMethods": true, + "allDeclaredFields": true, + "allPublicFields": true }, { "name": "sun.misc.Unsafe", diff --git a/aspect-model-editor-runtime/.graalvm/resource-config.json b/aspect-model-editor-runtime/.graalvm/resource-config.json index 0417ffec..aa9e5ced 100644 --- a/aspect-model-editor-runtime/.graalvm/resource-config.json +++ b/aspect-model-editor-runtime/.graalvm/resource-config.json @@ -73,6 +73,9 @@ { "pattern": "\\Qopenapi/OffsetBasedPaging.json\\E" }, + { + "pattern": "\\Qasyncapi/AsyncApiRootJson.json\\E" + }, { "pattern": "\\Qcom/eclipsesource/v8/V8.class\\E" }, diff --git a/aspect-model-editor-runtime/postman/ame.postman_collection.json b/aspect-model-editor-runtime/postman/ame.postman_collection.json index 509ab512..95cd6c2e 100644 --- a/aspect-model-editor-runtime/postman/ame.postman_collection.json +++ b/aspect-model-editor-runtime/postman/ame.postman_collection.json @@ -2,7 +2,8 @@ "info": { "_postman_id": "26a275be-9ff5-4002-a3c8-9cdd4bcf4f29", "name": "AME.POSTMAN.RESOURCES", - "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json" + "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json", + "_exporter_id": "30151852" }, "item": [ { @@ -601,18 +602,19 @@ " pm.response.to.have.status(200);\r", "});\r", "pm.test(\"Response body is valid\", function () {\r", - " pm.expect(pm.response.json()[\"components\"][\"schemas\"][\"urn_samm_org.eclipse.esmf.samm_characteristic_2.1.0_Boolean\"][\"type\"]).to.equal(\"boolean\");\r", - " pm.expect(pm.response.json()[\"components\"][\"schemas\"][\"urn_samm_org.eclipse.examples_1.0.0_Coordinate\"][\"type\"]).to.equal(\"number\");\r", - " pm.expect(pm.response.json()[\"components\"][\"schemas\"][\"urn_samm_org.eclipse.examples_1.0.0_MetresAboveMeanSeaLevel\"][\"type\"]).to.equal(\"number\");\r", - " pm.expect(pm.response.json()[\"components\"][\"schemas\"][\"urn_samm_org.eclipse.examples_1.0.0_SpatialPosition\"][\"type\"]).to.equal(\"object\");\r", + " pm.expect(pm.response.json()[\"components\"][\"schemas\"][\"Boolean\"][\"type\"]).to.equal(\"boolean\");\r", + " pm.expect(pm.response.json()[\"components\"][\"schemas\"][\"Coordinate\"][\"type\"]).to.equal(\"number\");\r", + " pm.expect(pm.response.json()[\"components\"][\"schemas\"][\"MetresAboveMeanSeaLevel\"][\"type\"]).to.equal(\"number\");\r", + " pm.expect(pm.response.json()[\"components\"][\"schemas\"][\"SpatialPosition\"][\"type\"]).to.equal(\"object\");\r", "\r", - " pm.expect(pm.response.json()[\"properties\"][\"isMoving\"][\"$ref\"]).to.equal(\"#/components/schemas/urn_samm_org.eclipse.esmf.samm_characteristic_2.1.0_Boolean\");\r", - " pm.expect(pm.response.json()[\"properties\"][\"position\"][\"$ref\"]).to.equal(\"#/components/schemas/urn_samm_org.eclipse.examples_1.0.0_SpatialPosition\");\r", - " pm.expect(pm.response.json()[\"properties\"][\"speed\"][\"$ref\"]).to.equal(\"#/components/schemas/urn_samm_org.eclipse.examples_1.0.0_Speed\");\r", - " pm.expect(pm.response.json()[\"properties\"][\"speedLimitWarning\"][\"$ref\"]).to.equal(\"#/components/schemas/urn_samm_org.eclipse.examples_1.0.0_TrafficLight\");\r", + " pm.expect(pm.response.json()[\"properties\"][\"isMoving\"][\"$ref\"]).to.equal(\"#/components/schemas/Boolean\");\r", + " pm.expect(pm.response.json()[\"properties\"][\"position\"][\"$ref\"]).to.equal(\"#/components/schemas/SpatialPositionCharacteristic\");\r", + " pm.expect(pm.response.json()[\"properties\"][\"speed\"][\"$ref\"]).to.equal(\"#/components/schemas/Speed\");\r", + " pm.expect(pm.response.json()[\"properties\"][\"speedLimitWarning\"][\"$ref\"]).to.equal(\"#/components/schemas/TrafficLight\");\r", "});" ], - "type": "text/javascript" + "type": "text/javascript", + "packages": {} } } ], @@ -1408,6 +1410,299 @@ } }, "response": [] + }, + { + "name": "GenerateJsonAsyncApiSpec", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Status code is 200\", function () {\r", + " pm.response.to.have.status(200);\r", + "});\r", + "pm.test(\"Response body is valid\", function () {\r", + " const jsonData = pm.response.json();\r", + " pm.expect(jsonData.asyncapi).to.equal(\"3.0.0\");\r", + " pm.expect(jsonData.id).to.equal(\"application:id\");\r", + "\r", + " pm.expect(jsonData.info.title).to.equal(\"movement MQTT API\");\r", + " pm.expect(jsonData.info.version).to.equal(\"v1\");\r", + "\r", + " pm.expect(jsonData.defaultContentType).to.equal(\"application/json\");\r", + " pm.expect(jsonData.channels.Movement.address).to.equal(\"foo/bar\");\r", + "});" + ], + "type": "text/javascript", + "packages": {} + } + } + ], + "request": { + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "@prefix samm: .\r\n@prefix samm-c: .\r\n@prefix samm-e: .\r\n@prefix unit: .\r\n@prefix rdf: .\r\n@prefix rdfs: .\r\n@prefix xsd: .\r\n@prefix : .\r\n\r\n:Movement a samm:Aspect ;\r\n samm:preferredName \"movement\"@en ;\r\n samm:description \"Aspect for movement information\"@en ;\r\n samm:properties ( :isMoving :position :speed :speedLimitWarning ) ;\r\n samm:operations ( ) ;\r\n samm:events ( ) .\r\n\r\n:isMoving a samm:Property ;\r\n samm:preferredName \"is moving\"@en ;\r\n samm:description \"Flag indicating whether the asset is currently moving\"@en ;\r\n samm:characteristic samm-c:Boolean .\r\n\r\n:position a samm:Property ;\r\n samm:preferredName \"position\"@en ;\r\n samm:description \"Indicates a position\"@en ;\r\n samm:characteristic :SpatialPositionCharacteristic .\r\n\r\n:speed a samm:Property ;\r\n samm:preferredName \"speed\"@en ;\r\n samm:description \"speed of vehicle\"@en ;\r\n samm:characteristic :Speed .\r\n\r\n:speedLimitWarning a samm:Property ;\r\n samm:preferredName \"speed limit warning\"@en ;\r\n samm:description \"Indicates if the speed limit is adhered to.\"@en ;\r\n samm:characteristic :TrafficLight .\r\n\r\n:SpatialPositionCharacteristic a samm-c:SingleEntity ;\r\n samm:preferredName \"spatial position characteristic\"@en ;\r\n samm:description \"Represents a single position in space with optional z coordinate.\"@en ;\r\n samm:dataType :SpatialPosition .\r\n\r\n:Speed a samm-c:Measurement ;\r\n samm:preferredName \"speed\"@en ;\r\n samm:description \"Scalar representation of speed of an object in kilometers per hour.\"@en ;\r\n samm:dataType xsd:float ;\r\n samm-c:unit unit:kilometrePerHour .\r\n\r\n:TrafficLight a samm-c:Enumeration ;\r\n samm:preferredName \"warning level\"@en ;\r\n samm:description \"Represents if speed of position change is within specification (green), within tolerance (yellow), or outside specification (red).\"@en ;\r\n samm:dataType xsd:string ;\r\n samm-c:values ( \"green\" \"yellow\" \"red\" ) .\r\n\r\n:SpatialPosition a samm:Entity ;\r\n samm:preferredName \"spatial position\"@en ;\r\n samm:description \"Represents latitude, longitude and altitude information in the WGS84 geodetic reference datum\"@en ;\r\n samm:see ;\r\n samm:properties ( :latitude :longitude [ samm:property :altitude; samm:optional true ] ) .\r\n\r\n:latitude a samm:Property ;\r\n samm:preferredName \"latitude\"@en ;\r\n samm:description \"latitude coordinate in space (WGS84)\"@en ;\r\n samm:see ;\r\n samm:characteristic :Coordinate ;\r\n samm:exampleValue \"9.1781\"^^xsd:decimal .\r\n\r\n:longitude a samm:Property ;\r\n samm:preferredName \"longitude\"@en ;\r\n samm:description \"longitude coordinate in space (WGS84)\"@en ;\r\n samm:see ;\r\n samm:characteristic :Coordinate ;\r\n samm:exampleValue \"48.80835\"^^xsd:decimal .\r\n\r\n:altitude a samm:Property ;\r\n samm:preferredName \"altitude\"@en ;\r\n samm:description \"Elevation above sea level zero\"@en ;\r\n samm:see ;\r\n samm:characteristic :MetresAboveMeanSeaLevel ;\r\n samm:exampleValue \"153\"^^xsd:float .\r\n\r\n:Coordinate a samm-c:Measurement ;\r\n samm:preferredName \"coordinate\"@en ;\r\n samm:description \"Representing the geographical coordinate\"@en ;\r\n samm:dataType xsd:decimal ;\r\n samm-c:unit unit:degreeUnitOfAngle .\r\n\r\n:MetresAboveMeanSeaLevel a samm-c:Measurement ;\r\n samm:preferredName \"metres above mean sea level\"@en ;\r\n samm:description \"Signifies the vertical distance in reference to a historic mean sea level as a vertical datum\"@en ;\r\n samm:see ;\r\n samm:dataType xsd:float ;\r\n samm-c:unit unit:metre .\r\n" + }, + "url": { + "raw": "http://localhost:{{port}}/ame/api/generate/async-api-spec?language=en&output=json&applicationId=application:id&channelAddress=foo/bar&useSemanticVersion=false&writeSeparateFiles=false", + "protocol": "http", + "host": [ + "localhost" + ], + "port": "{{port}}", + "path": [ + "ame", + "api", + "generate", + "async-api-spec" + ], + "query": [ + { + "key": "language", + "value": "en" + }, + { + "key": "output", + "value": "json" + }, + { + "key": "applicationId", + "value": "application:id" + }, + { + "key": "channelAddress", + "value": "foo/bar" + }, + { + "key": "useSemanticVersion", + "value": "false" + }, + { + "key": "writeSeparateFiles", + "value": "false" + } + ] + } + }, + "response": [] + }, + { + "name": "GenerateJsonAsyncApiSpecPackage", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Status code is 200\", function () {\r", + " pm.response.to.have.status(200);\r", + "});\r", + "pm.test(\"Response body is valid\", function () {\r", + " const jsonData = pm.response.text();\r", + " pm.expect(jsonData).to.include(\"json\");\r", + "});" + ], + "type": "text/javascript", + "packages": {} + } + } + ], + "request": { + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "@prefix samm: .\r\n@prefix samm-c: .\r\n@prefix samm-e: .\r\n@prefix unit: .\r\n@prefix rdf: .\r\n@prefix rdfs: .\r\n@prefix xsd: .\r\n@prefix : .\r\n\r\n:Movement a samm:Aspect ;\r\n samm:preferredName \"movement\"@en ;\r\n samm:description \"Aspect for movement information\"@en ;\r\n samm:properties ( :isMoving :position :speed :speedLimitWarning ) ;\r\n samm:operations ( ) ;\r\n samm:events ( ) .\r\n\r\n:isMoving a samm:Property ;\r\n samm:preferredName \"is moving\"@en ;\r\n samm:description \"Flag indicating whether the asset is currently moving\"@en ;\r\n samm:characteristic samm-c:Boolean .\r\n\r\n:position a samm:Property ;\r\n samm:preferredName \"position\"@en ;\r\n samm:description \"Indicates a position\"@en ;\r\n samm:characteristic :SpatialPositionCharacteristic .\r\n\r\n:speed a samm:Property ;\r\n samm:preferredName \"speed\"@en ;\r\n samm:description \"speed of vehicle\"@en ;\r\n samm:characteristic :Speed .\r\n\r\n:speedLimitWarning a samm:Property ;\r\n samm:preferredName \"speed limit warning\"@en ;\r\n samm:description \"Indicates if the speed limit is adhered to.\"@en ;\r\n samm:characteristic :TrafficLight .\r\n\r\n:SpatialPositionCharacteristic a samm-c:SingleEntity ;\r\n samm:preferredName \"spatial position characteristic\"@en ;\r\n samm:description \"Represents a single position in space with optional z coordinate.\"@en ;\r\n samm:dataType :SpatialPosition .\r\n\r\n:Speed a samm-c:Measurement ;\r\n samm:preferredName \"speed\"@en ;\r\n samm:description \"Scalar representation of speed of an object in kilometers per hour.\"@en ;\r\n samm:dataType xsd:float ;\r\n samm-c:unit unit:kilometrePerHour .\r\n\r\n:TrafficLight a samm-c:Enumeration ;\r\n samm:preferredName \"warning level\"@en ;\r\n samm:description \"Represents if speed of position change is within specification (green), within tolerance (yellow), or outside specification (red).\"@en ;\r\n samm:dataType xsd:string ;\r\n samm-c:values ( \"green\" \"yellow\" \"red\" ) .\r\n\r\n:SpatialPosition a samm:Entity ;\r\n samm:preferredName \"spatial position\"@en ;\r\n samm:description \"Represents latitude, longitude and altitude information in the WGS84 geodetic reference datum\"@en ;\r\n samm:see ;\r\n samm:properties ( :latitude :longitude [ samm:property :altitude; samm:optional true ] ) .\r\n\r\n:latitude a samm:Property ;\r\n samm:preferredName \"latitude\"@en ;\r\n samm:description \"latitude coordinate in space (WGS84)\"@en ;\r\n samm:see ;\r\n samm:characteristic :Coordinate ;\r\n samm:exampleValue \"9.1781\"^^xsd:decimal .\r\n\r\n:longitude a samm:Property ;\r\n samm:preferredName \"longitude\"@en ;\r\n samm:description \"longitude coordinate in space (WGS84)\"@en ;\r\n samm:see ;\r\n samm:characteristic :Coordinate ;\r\n samm:exampleValue \"48.80835\"^^xsd:decimal .\r\n\r\n:altitude a samm:Property ;\r\n samm:preferredName \"altitude\"@en ;\r\n samm:description \"Elevation above sea level zero\"@en ;\r\n samm:see ;\r\n samm:characteristic :MetresAboveMeanSeaLevel ;\r\n samm:exampleValue \"153\"^^xsd:float .\r\n\r\n:Coordinate a samm-c:Measurement ;\r\n samm:preferredName \"coordinate\"@en ;\r\n samm:description \"Representing the geographical coordinate\"@en ;\r\n samm:dataType xsd:decimal ;\r\n samm-c:unit unit:degreeUnitOfAngle .\r\n\r\n:MetresAboveMeanSeaLevel a samm-c:Measurement ;\r\n samm:preferredName \"metres above mean sea level\"@en ;\r\n samm:description \"Signifies the vertical distance in reference to a historic mean sea level as a vertical datum\"@en ;\r\n samm:see ;\r\n samm:dataType xsd:float ;\r\n samm-c:unit unit:metre .\r\n" + }, + "url": { + "raw": "http://localhost:{{port}}/ame/api/generate/async-api-spec?language=en&output=json&applicationId=application:id&channelAddress=foo/bar&useSemanticVersion=false&writeSeparateFiles=true", + "protocol": "http", + "host": [ + "localhost" + ], + "port": "{{port}}", + "path": [ + "ame", + "api", + "generate", + "async-api-spec" + ], + "query": [ + { + "key": "language", + "value": "en" + }, + { + "key": "output", + "value": "json" + }, + { + "key": "applicationId", + "value": "application:id" + }, + { + "key": "channelAddress", + "value": "foo/bar" + }, + { + "key": "useSemanticVersion", + "value": "false" + }, + { + "key": "writeSeparateFiles", + "value": "true" + } + ] + } + }, + "response": [] + }, + { + "name": "GenerateYamlAsyncApiSpec", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Status code is 200\", function () {\r", + " pm.response.to.have.status(200);\r", + "});\r", + "pm.test(\"Response body is valid\", function () {\r", + " const jsonData = pm.response.text();\r", + " pm.expect(jsonData).to.include(\"asyncapi: 3.0.0\");\r", + " pm.expect(jsonData).to.include(\"id: application:id\");\r", + "\r", + " pm.expect(jsonData).to.include(\"title: movement MQTT API\");\r", + " pm.expect(jsonData).to.include(\"version: v1\");\r", + "\r", + " pm.expect(jsonData).to.include(\"defaultContentType: application/json\");\r", + " pm.expect(jsonData).to.include(\"address: foo/bar\");\r", + "});" + ], + "type": "text/javascript", + "packages": {} + } + } + ], + "request": { + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "@prefix samm: .\r\n@prefix samm-c: .\r\n@prefix samm-e: .\r\n@prefix unit: .\r\n@prefix rdf: .\r\n@prefix rdfs: .\r\n@prefix xsd: .\r\n@prefix : .\r\n\r\n:Movement a samm:Aspect ;\r\n samm:preferredName \"movement\"@en ;\r\n samm:description \"Aspect for movement information\"@en ;\r\n samm:properties ( :isMoving :position :speed :speedLimitWarning ) ;\r\n samm:operations ( ) ;\r\n samm:events ( ) .\r\n\r\n:isMoving a samm:Property ;\r\n samm:preferredName \"is moving\"@en ;\r\n samm:description \"Flag indicating whether the asset is currently moving\"@en ;\r\n samm:characteristic samm-c:Boolean .\r\n\r\n:position a samm:Property ;\r\n samm:preferredName \"position\"@en ;\r\n samm:description \"Indicates a position\"@en ;\r\n samm:characteristic :SpatialPositionCharacteristic .\r\n\r\n:speed a samm:Property ;\r\n samm:preferredName \"speed\"@en ;\r\n samm:description \"speed of vehicle\"@en ;\r\n samm:characteristic :Speed .\r\n\r\n:speedLimitWarning a samm:Property ;\r\n samm:preferredName \"speed limit warning\"@en ;\r\n samm:description \"Indicates if the speed limit is adhered to.\"@en ;\r\n samm:characteristic :TrafficLight .\r\n\r\n:SpatialPositionCharacteristic a samm-c:SingleEntity ;\r\n samm:preferredName \"spatial position characteristic\"@en ;\r\n samm:description \"Represents a single position in space with optional z coordinate.\"@en ;\r\n samm:dataType :SpatialPosition .\r\n\r\n:Speed a samm-c:Measurement ;\r\n samm:preferredName \"speed\"@en ;\r\n samm:description \"Scalar representation of speed of an object in kilometers per hour.\"@en ;\r\n samm:dataType xsd:float ;\r\n samm-c:unit unit:kilometrePerHour .\r\n\r\n:TrafficLight a samm-c:Enumeration ;\r\n samm:preferredName \"warning level\"@en ;\r\n samm:description \"Represents if speed of position change is within specification (green), within tolerance (yellow), or outside specification (red).\"@en ;\r\n samm:dataType xsd:string ;\r\n samm-c:values ( \"green\" \"yellow\" \"red\" ) .\r\n\r\n:SpatialPosition a samm:Entity ;\r\n samm:preferredName \"spatial position\"@en ;\r\n samm:description \"Represents latitude, longitude and altitude information in the WGS84 geodetic reference datum\"@en ;\r\n samm:see ;\r\n samm:properties ( :latitude :longitude [ samm:property :altitude; samm:optional true ] ) .\r\n\r\n:latitude a samm:Property ;\r\n samm:preferredName \"latitude\"@en ;\r\n samm:description \"latitude coordinate in space (WGS84)\"@en ;\r\n samm:see ;\r\n samm:characteristic :Coordinate ;\r\n samm:exampleValue \"9.1781\"^^xsd:decimal .\r\n\r\n:longitude a samm:Property ;\r\n samm:preferredName \"longitude\"@en ;\r\n samm:description \"longitude coordinate in space (WGS84)\"@en ;\r\n samm:see ;\r\n samm:characteristic :Coordinate ;\r\n samm:exampleValue \"48.80835\"^^xsd:decimal .\r\n\r\n:altitude a samm:Property ;\r\n samm:preferredName \"altitude\"@en ;\r\n samm:description \"Elevation above sea level zero\"@en ;\r\n samm:see ;\r\n samm:characteristic :MetresAboveMeanSeaLevel ;\r\n samm:exampleValue \"153\"^^xsd:float .\r\n\r\n:Coordinate a samm-c:Measurement ;\r\n samm:preferredName \"coordinate\"@en ;\r\n samm:description \"Representing the geographical coordinate\"@en ;\r\n samm:dataType xsd:decimal ;\r\n samm-c:unit unit:degreeUnitOfAngle .\r\n\r\n:MetresAboveMeanSeaLevel a samm-c:Measurement ;\r\n samm:preferredName \"metres above mean sea level\"@en ;\r\n samm:description \"Signifies the vertical distance in reference to a historic mean sea level as a vertical datum\"@en ;\r\n samm:see ;\r\n samm:dataType xsd:float ;\r\n samm-c:unit unit:metre .\r\n" + }, + "url": { + "raw": "http://localhost:{{port}}/ame/api/generate/async-api-spec?language=en&output=yaml&applicationId=application:id&channelAddress=foo/bar&useSemanticVersion=false&writeSeparateFiles=false", + "protocol": "http", + "host": [ + "localhost" + ], + "port": "{{port}}", + "path": [ + "ame", + "api", + "generate", + "async-api-spec" + ], + "query": [ + { + "key": "language", + "value": "en" + }, + { + "key": "output", + "value": "yaml" + }, + { + "key": "applicationId", + "value": "application:id" + }, + { + "key": "channelAddress", + "value": "foo/bar" + }, + { + "key": "useSemanticVersion", + "value": "false" + }, + { + "key": "writeSeparateFiles", + "value": "false" + } + ] + } + }, + "response": [] + }, + { + "name": "GenerateYamlAsyncApiSpecPackage", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Status code is 200\", function () {\r", + " pm.response.to.have.status(200);\r", + "});\r", + "pm.test(\"Response body is valid\", function () {\r", + " const jsonData = pm.response.text();\r", + "});" + ], + "type": "text/javascript", + "packages": {} + } + } + ], + "request": { + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "@prefix samm: .\r\n@prefix samm-c: .\r\n@prefix samm-e: .\r\n@prefix unit: .\r\n@prefix rdf: .\r\n@prefix rdfs: .\r\n@prefix xsd: .\r\n@prefix : .\r\n\r\n:Movement a samm:Aspect ;\r\n samm:preferredName \"movement\"@en ;\r\n samm:description \"Aspect for movement information\"@en ;\r\n samm:properties ( :isMoving :position :speed :speedLimitWarning ) ;\r\n samm:operations ( ) ;\r\n samm:events ( ) .\r\n\r\n:isMoving a samm:Property ;\r\n samm:preferredName \"is moving\"@en ;\r\n samm:description \"Flag indicating whether the asset is currently moving\"@en ;\r\n samm:characteristic samm-c:Boolean .\r\n\r\n:position a samm:Property ;\r\n samm:preferredName \"position\"@en ;\r\n samm:description \"Indicates a position\"@en ;\r\n samm:characteristic :SpatialPositionCharacteristic .\r\n\r\n:speed a samm:Property ;\r\n samm:preferredName \"speed\"@en ;\r\n samm:description \"speed of vehicle\"@en ;\r\n samm:characteristic :Speed .\r\n\r\n:speedLimitWarning a samm:Property ;\r\n samm:preferredName \"speed limit warning\"@en ;\r\n samm:description \"Indicates if the speed limit is adhered to.\"@en ;\r\n samm:characteristic :TrafficLight .\r\n\r\n:SpatialPositionCharacteristic a samm-c:SingleEntity ;\r\n samm:preferredName \"spatial position characteristic\"@en ;\r\n samm:description \"Represents a single position in space with optional z coordinate.\"@en ;\r\n samm:dataType :SpatialPosition .\r\n\r\n:Speed a samm-c:Measurement ;\r\n samm:preferredName \"speed\"@en ;\r\n samm:description \"Scalar representation of speed of an object in kilometers per hour.\"@en ;\r\n samm:dataType xsd:float ;\r\n samm-c:unit unit:kilometrePerHour .\r\n\r\n:TrafficLight a samm-c:Enumeration ;\r\n samm:preferredName \"warning level\"@en ;\r\n samm:description \"Represents if speed of position change is within specification (green), within tolerance (yellow), or outside specification (red).\"@en ;\r\n samm:dataType xsd:string ;\r\n samm-c:values ( \"green\" \"yellow\" \"red\" ) .\r\n\r\n:SpatialPosition a samm:Entity ;\r\n samm:preferredName \"spatial position\"@en ;\r\n samm:description \"Represents latitude, longitude and altitude information in the WGS84 geodetic reference datum\"@en ;\r\n samm:see ;\r\n samm:properties ( :latitude :longitude [ samm:property :altitude; samm:optional true ] ) .\r\n\r\n:latitude a samm:Property ;\r\n samm:preferredName \"latitude\"@en ;\r\n samm:description \"latitude coordinate in space (WGS84)\"@en ;\r\n samm:see ;\r\n samm:characteristic :Coordinate ;\r\n samm:exampleValue \"9.1781\"^^xsd:decimal .\r\n\r\n:longitude a samm:Property ;\r\n samm:preferredName \"longitude\"@en ;\r\n samm:description \"longitude coordinate in space (WGS84)\"@en ;\r\n samm:see ;\r\n samm:characteristic :Coordinate ;\r\n samm:exampleValue \"48.80835\"^^xsd:decimal .\r\n\r\n:altitude a samm:Property ;\r\n samm:preferredName \"altitude\"@en ;\r\n samm:description \"Elevation above sea level zero\"@en ;\r\n samm:see ;\r\n samm:characteristic :MetresAboveMeanSeaLevel ;\r\n samm:exampleValue \"153\"^^xsd:float .\r\n\r\n:Coordinate a samm-c:Measurement ;\r\n samm:preferredName \"coordinate\"@en ;\r\n samm:description \"Representing the geographical coordinate\"@en ;\r\n samm:dataType xsd:decimal ;\r\n samm-c:unit unit:degreeUnitOfAngle .\r\n\r\n:MetresAboveMeanSeaLevel a samm-c:Measurement ;\r\n samm:preferredName \"metres above mean sea level\"@en ;\r\n samm:description \"Signifies the vertical distance in reference to a historic mean sea level as a vertical datum\"@en ;\r\n samm:see ;\r\n samm:dataType xsd:float ;\r\n samm-c:unit unit:metre .\r\n" + }, + "url": { + "raw": "http://localhost:{{port}}/ame/api/generate/async-api-spec?language=en&output=yaml&applicationId=application:id&channelAddress=foo/bar&useSemanticVersion=false&writeSeparateFiles=true", + "protocol": "http", + "host": [ + "localhost" + ], + "port": "{{port}}", + "path": [ + "ame", + "api", + "generate", + "async-api-spec" + ], + "query": [ + { + "key": "language", + "value": "en" + }, + { + "key": "output", + "value": "yaml" + }, + { + "key": "applicationId", + "value": "application:id" + }, + { + "key": "channelAddress", + "value": "foo/bar" + }, + { + "key": "useSemanticVersion", + "value": "false" + }, + { + "key": "writeSeparateFiles", + "value": "true" + } + ] + } + }, + "response": [] } ], "event": [ diff --git a/aspect-model-editor-service/pom.xml b/aspect-model-editor-service/pom.xml index 57a14d15..8a746e81 100644 --- a/aspect-model-editor-service/pom.xml +++ b/aspect-model-editor-service/pom.xml @@ -3,8 +3,7 @@ ~ Copyright (c) 2023 Robert Bosch Manufacturing Solutions GmbH, Germany. All rights reserved. --> - 4.0.0 @@ -71,6 +70,10 @@ com.fasterxml.jackson.dataformat jackson-dataformat-xml + + com.fasterxml.jackson.core + jackson-core + diff --git a/aspect-model-editor-service/src/main/java/org/eclipse/esmf/ame/services/GenerateService.java b/aspect-model-editor-service/src/main/java/org/eclipse/esmf/ame/services/GenerateService.java index 9f85cabe..10f92812 100644 --- a/aspect-model-editor-service/src/main/java/org/eclipse/esmf/ame/services/GenerateService.java +++ b/aspect-model-editor-service/src/main/java/org/eclipse/esmf/ame/services/GenerateService.java @@ -16,17 +16,26 @@ import java.io.ByteArrayOutputStream; import java.io.IOException; import java.nio.charset.StandardCharsets; +import java.nio.file.Path; +import java.util.HashMap; import java.util.Locale; +import java.util.Map; import java.util.Optional; import org.apache.commons.lang3.LocaleUtils; +import org.eclipse.esmf.ame.exceptions.FileHandlingException; import org.eclipse.esmf.ame.exceptions.GenerationException; import org.eclipse.esmf.ame.exceptions.InvalidAspectModelException; import org.eclipse.esmf.ame.resolver.strategy.FileSystemStrategy; import org.eclipse.esmf.ame.resolver.strategy.utils.ResolverUtils; +import org.eclipse.esmf.ame.services.utils.ZipUtils; import org.eclipse.esmf.ame.utils.ModelUtils; import org.eclipse.esmf.aspectmodel.aas.AasFileFormat; import org.eclipse.esmf.aspectmodel.aas.AspectModelAasGenerator; +import org.eclipse.esmf.aspectmodel.generator.asyncapi.AspectModelAsyncApiGenerator; +import org.eclipse.esmf.aspectmodel.generator.asyncapi.AsyncApiSchemaArtifact; +import org.eclipse.esmf.aspectmodel.generator.asyncapi.AsyncApiSchemaGenerationConfig; +import org.eclipse.esmf.aspectmodel.generator.asyncapi.AsyncApiSchemaGenerationConfigBuilder; import org.eclipse.esmf.aspectmodel.generator.docu.AspectModelDocumentationGenerator; import org.eclipse.esmf.aspectmodel.generator.json.AspectModelJsonPayloadGenerator; import org.eclipse.esmf.aspectmodel.generator.jsonschema.AspectModelJsonSchemaGenerator; @@ -34,13 +43,17 @@ import org.eclipse.esmf.aspectmodel.generator.openapi.PagingOption; import org.eclipse.esmf.aspectmodel.resolver.services.DataType; import org.eclipse.esmf.aspectmodel.resolver.services.VersionedModel; +import org.eclipse.esmf.metamodel.Aspect; import org.eclipse.esmf.metamodel.AspectContext; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.stereotype.Service; +import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.dataformat.yaml.YAMLGenerator; +import com.fasterxml.jackson.dataformat.yaml.YAMLMapper; import com.google.common.collect.ImmutableMap; import io.vavr.control.Try; @@ -48,7 +61,7 @@ @Service public class GenerateService { private static final Logger LOG = LoggerFactory.getLogger( GenerateService.class ); - + private static final ObjectMapper YAML_MAPPER = new YAMLMapper().enable( YAMLGenerator.Feature.MINIMIZE_QUOTES ); private static final String COULD_NOT_LOAD_ASPECT = "Could not load Aspect"; private static final String COULD_NOT_LOAD_ASPECT_MODEL = "Could not load Aspect model, please make sure the model is valid."; public static final String WRONG_RESOURCE_PATH_ID = "The resource path ID and properties ID do not match. Please verify and correct them."; @@ -90,8 +103,7 @@ public String jsonSchema( final String aspectModel, final String language ) { public String sampleJSONPayload( final String aspectModel ) { try { - return new AspectModelJsonPayloadGenerator( - generateAspectContext( aspectModel ) ).generateJson(); + return new AspectModelJsonPayloadGenerator( generateAspectContext( aspectModel ) ).generateJson(); } catch ( final IOException e ) { LOG.error( COULD_NOT_LOAD_ASPECT_MODEL ); throw new InvalidAspectModelException( COULD_NOT_LOAD_ASPECT, e ); @@ -144,21 +156,15 @@ private AspectContext generateAspectContext( final String aspectModel ) { public String generateYamlOpenApiSpec( final String language, final String aspectModel, final String baseUrl, final boolean includeQueryApi, final boolean useSemanticVersion, final Optional pagingOption, final Optional resourcePath, final Optional yamlProperties ) { - try { - final String ymlOutput = new AspectModelOpenApiGenerator().applyForYaml( - ResolverUtils.resolveAspectFromModel( aspectModel ), - useSemanticVersion, baseUrl, resourcePath, yamlProperties, includeQueryApi, pagingOption, - Locale.forLanguageTag( language ) ); - - if ( ymlOutput.equals( "--- {}\n" ) ) { - throw new GenerationException( WRONG_RESOURCE_PATH_ID ); - } + final String ymlOutput = new AspectModelOpenApiGenerator().applyForYaml( + ResolverUtils.resolveAspectFromModel( aspectModel ), useSemanticVersion, baseUrl, resourcePath, + yamlProperties, includeQueryApi, pagingOption, Locale.forLanguageTag( language ) ); - return ymlOutput; - } catch ( final IOException e ) { - LOG.error( "YAML OpenAPI specification could not be generated." ); - throw new InvalidAspectModelException( "Error generating YAML OpenAPI specification", e ); + if ( ymlOutput.equals( "--- {}\n" ) ) { + throw new GenerationException( WRONG_RESOURCE_PATH_ID ); } + + return ymlOutput; } public String generateJsonOpenApiSpec( final String language, final String aspectModel, final String baseUrl, @@ -166,8 +172,8 @@ public String generateJsonOpenApiSpec( final String language, final String aspec final Optional resourcePath, final Optional jsonProperties ) { try { final JsonNode json = new AspectModelOpenApiGenerator().applyForJson( - ResolverUtils.resolveAspectFromModel( aspectModel ), useSemanticVersion, baseUrl, - resourcePath, jsonProperties, includeQueryApi, pagingOption, LocaleUtils.toLocale( language ) ); + ResolverUtils.resolveAspectFromModel( aspectModel ), useSemanticVersion, baseUrl, resourcePath, + jsonProperties, includeQueryApi, pagingOption, LocaleUtils.toLocale( language ) ); final ByteArrayOutputStream out = new ByteArrayOutputStream(); final ObjectMapper objectMapper = new ObjectMapper(); @@ -186,4 +192,83 @@ public String generateJsonOpenApiSpec( final String language, final String aspec throw new InvalidAspectModelException( "Error generating JSON OpenAPI specification", e ); } } + + public byte[] generateAsyncApiSpec( final String aspectModel, final String language, final String output, + final String applicationId, final String channelAddress, final boolean useSemanticVersion, + final boolean writeSeparateFiles ) { + final AspectModelAsyncApiGenerator generator = new AspectModelAsyncApiGenerator(); + final AsyncApiSchemaGenerationConfig config = buildAsyncApiSchemaGenerationConfig( applicationId, channelAddress, + useSemanticVersion, language ); + + final Aspect aspect = ResolverUtils.resolveAspectFromModel( aspectModel ); + final AsyncApiSchemaArtifact asyncApiSpec = generator.apply( aspect, config ); + + if ( writeSeparateFiles ) { + return generateZipFile( asyncApiSpec, output ); + } + + return generateSingleFile( asyncApiSpec, output ); + } + + private AsyncApiSchemaGenerationConfig buildAsyncApiSchemaGenerationConfig( final String applicationId, + final String channelAddress, final boolean useSemanticVersion, final String language ) { + return AsyncApiSchemaGenerationConfigBuilder.builder().useSemanticVersion( useSemanticVersion ) + .applicationId( applicationId ).channelAddress( channelAddress ) + .locale( LocaleUtils.toLocale( language ) ).build(); + } + + private byte[] generateZipFile( final AsyncApiSchemaArtifact asyncApiSpec, final String output ) { + if ( output.equals( "json" ) ) { + return jsonZip( asyncApiSpec.getContentWithSeparateSchemasAsJson() ); + } + + return yamlZip( asyncApiSpec.getContentWithSeparateSchemasAsYaml() ); + } + + private byte[] jsonZip( final Map separateFilesContent ) { + final ObjectMapper objectMapper = new ObjectMapper(); + final Map content = new HashMap<>(); + + for ( final Map.Entry entry : separateFilesContent.entrySet() ) { + try { + final byte[] bytes = objectMapper.writerWithDefaultPrettyPrinter().writeValueAsBytes( entry.getValue() ); + content.put( entry.getKey(), bytes ); + } catch ( final JsonProcessingException e ) { + LOG.error( "Failed to convert JSON to bytes.", e ); + throw new FileHandlingException( "Failed to get JSON async api.", e ); + } + } + + return ZipUtils.createAsyncApiPackage( content ); + } + + private byte[] yamlZip( final Map separateFilesContent ) { + final Map content = new HashMap<>(); + + for ( final Map.Entry entry : separateFilesContent.entrySet() ) { + final byte[] bytes = entry.getValue().getBytes( StandardCharsets.UTF_8 ); + content.put( entry.getKey(), bytes ); + } + + return ZipUtils.createAsyncApiPackage( content ); + } + + private byte[] generateSingleFile( final AsyncApiSchemaArtifact asyncApiSpec, final String output ) { + final JsonNode json = asyncApiSpec.getContent(); + + if ( output.equals( "yaml" ) ) { + return jsonToYaml( json ).getBytes( StandardCharsets.UTF_8 ); + } + + return json.toString().getBytes( StandardCharsets.UTF_8 ); + } + + private String jsonToYaml( final JsonNode json ) { + try { + return YAML_MAPPER.writeValueAsString( json ); + } catch ( final JsonProcessingException e ) { + LOG.error( "JSON could not be converted to YAML", e ); + throw new FileHandlingException( "Failed to get YAML async api.", e ); + } + } } diff --git a/aspect-model-editor-service/src/main/java/org/eclipse/esmf/ame/services/utils/ZipUtils.java b/aspect-model-editor-service/src/main/java/org/eclipse/esmf/ame/services/utils/ZipUtils.java index 8d642367..75a741ef 100644 --- a/aspect-model-editor-service/src/main/java/org/eclipse/esmf/ame/services/utils/ZipUtils.java +++ b/aspect-model-editor-service/src/main/java/org/eclipse/esmf/ame/services/utils/ZipUtils.java @@ -20,6 +20,7 @@ import java.io.FileOutputStream; import java.io.IOException; import java.nio.file.Files; +import java.nio.file.Path; import java.nio.file.attribute.BasicFileAttributes; import java.util.ArrayList; import java.util.HashSet; @@ -43,14 +44,35 @@ private ZipUtils() { static final int BUFFER = 1024; + public static byte[] createAsyncApiPackage( final Map content ) { + final ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + + try ( final ZipOutputStream zos = new ZipOutputStream( outputStream ) ) { + for ( final Map.Entry entry : content.entrySet() ) { + final ZipEntry zipEntry = new ZipEntry( entry.getKey().toString() ); + zos.putNextEntry( zipEntry ); + + final byte[] bytes = entry.getValue(); + zos.write( bytes, 0, bytes.length ); + + zos.closeEntry(); + } + } catch ( final IOException e ) { + LOG.error( "Failed to create the asynchronous API ZIP file." ); + throw new CreateFileException( "An error occurred while creating the ZIP file for the async API.", e ); + } + + return outputStream.toByteArray(); + } + public static byte[] createPackageFromCache( final Map exportCache ) throws IOException { - ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + final ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); - try ( ZipOutputStream zos = new ZipOutputStream( outputStream ) ) { - Set zipFolderSet = new HashSet<>(); - Set zipVersionedNamespaceSet = new HashSet<>(); + try ( final ZipOutputStream zos = new ZipOutputStream( outputStream ) ) { + final Set zipFolderSet = new HashSet<>(); + final Set zipVersionedNamespaceSet = new HashSet<>(); - for ( Map.Entry entry : exportCache.entrySet() ) { + for ( final Map.Entry entry : exportCache.entrySet() ) { final String[] fileStructure = entry.getKey().split( ":" ); final String aspectModel = entry.getValue(); @@ -85,7 +107,8 @@ public static void createPackageFromWorkspace( final String zipFileName, final S final String storagePath ) throws IOException { final String zipFile = aspectModelPath + File.separator + zipFileName; - try ( FileOutputStream fos = new FileOutputStream( zipFile ); ZipOutputStream zos = new ZipOutputStream( fos ) ) { + try ( final FileOutputStream fos = new FileOutputStream( zipFile ); + final ZipOutputStream zos = new ZipOutputStream( fos ) ) { final List fileList = getFileList( new File( storagePath ), new ArrayList<>(), storagePath ); diff --git a/aspect-model-editor-service/src/test/java/org/eclipse/esmf/ame/services/GenerateServiceTest.java b/aspect-model-editor-service/src/test/java/org/eclipse/esmf/ame/services/GenerateServiceTest.java index 5345b046..e6c1b501 100644 --- a/aspect-model-editor-service/src/test/java/org/eclipse/esmf/ame/services/GenerateServiceTest.java +++ b/aspect-model-editor-service/src/test/java/org/eclipse/esmf/ame/services/GenerateServiceTest.java @@ -15,11 +15,13 @@ import static org.junit.jupiter.api.Assertions.*; +import java.io.ByteArrayInputStream; import java.io.IOException; import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.Path; import java.util.Optional; +import java.util.zip.ZipInputStream; import org.eclipse.esmf.ame.config.TestConfig; import org.eclipse.esmf.ame.exceptions.GenerationException; @@ -68,7 +70,7 @@ void testAspectModelJsonSchema() throws IOException { final String payload = generateService.jsonSchema( testModel, "en-EN" ); - assertTrue( payload.contains( "#/components/schemas/urn_samm_org.eclipse.esmf.example_1.0.0_Characteristic" ) ); + assertTrue( payload.contains( "#/components/schemas/Characteristic" ) ); } @Test @@ -238,4 +240,68 @@ void testAspectModelAASJson() throws IOException { assertTrue( payload.contains( "assetAdministrationShells" ) ); assertTrue( payload.contains( "submodels" ) ); } + + @Test + void testAspectModelJsonAsyncApiSpec() throws IOException { + final Path storagePath = Path.of( eclipseTestPath.toString(), model ); + final String testModel = Files.readString( storagePath, StandardCharsets.UTF_8 ); + + final String payload = new String( + generateService.generateAsyncApiSpec( testModel, "en", "json", "application:id", "foo/bar", + false, false ), StandardCharsets.UTF_8 ); + + assertTrue( payload.contains( "\"asyncapi\":\"3.0.0\"" ) ); + assertTrue( payload.contains( "\"id\":\"application:id\"" ) ); + assertTrue( payload.contains( "\"title\":\"AspectModelForService MQTT API\"" ) ); + assertTrue( payload.contains( "\"version\":\"v1\"" ) ); + assertTrue( payload.contains( "\"defaultContentType\":\"application/json\"" ) ); + assertTrue( payload.contains( "\"address\":\"foo/bar\"" ) ); + } + + @Test + void testAspectModelJsonAsyncApiSpecWithSeparateFiles() throws IOException { + final Path storagePath = Path.of( eclipseTestPath.toString(), model ); + final String testModel = Files.readString( storagePath, StandardCharsets.UTF_8 ); + + final byte[] payload = generateService.generateAsyncApiSpec( testModel, "en", "json", "application:id", "foo/bar", + false, true ); + + assertTrue( isValidZipFile( payload ) ); + } + + @Test + void testAspectModelYAMLAsyncApiSpec() throws IOException { + final Path storagePath = Path.of( eclipseTestPath.toString(), model ); + final String testModel = Files.readString( storagePath, StandardCharsets.UTF_8 ); + + final String payload = new String( + generateService.generateAsyncApiSpec( testModel, "en", "yaml", "application:id", "foo/bar", + false, false ), StandardCharsets.UTF_8 ); + + assertTrue( payload.contains( "asyncapi: 3.0.0" ) ); + assertTrue( payload.contains( "id: application:id" ) ); + assertTrue( payload.contains( "title: AspectModelForService MQTT API" ) ); + assertTrue( payload.contains( "version: v1" ) ); + assertTrue( payload.contains( "defaultContentType: application/json" ) ); + assertTrue( payload.contains( "address: foo/bar" ) ); + } + + @Test + void testAspectModelYAMLAsyncApiSpecWithSeparateFiles() throws IOException { + final Path storagePath = Path.of( eclipseTestPath.toString(), model ); + final String testModel = Files.readString( storagePath, StandardCharsets.UTF_8 ); + + final byte[] payload = generateService.generateAsyncApiSpec( testModel, "en", "yaml", "application:id", "foo/bar", + false, true ); + + assertTrue( isValidZipFile( payload ) ); + } + + private boolean isValidZipFile( final byte[] payload ) { + try ( final ZipInputStream zis = new ZipInputStream( new ByteArrayInputStream( payload ) ) ) { + return zis.getNextEntry() != null; + } catch ( final IOException e ) { + return false; + } + } } diff --git a/aspect-model-editor-web/src/main/java/org/eclipse/esmf/ame/api/GenerateController.java b/aspect-model-editor-web/src/main/java/org/eclipse/esmf/ame/api/GenerateController.java index 6f8466d2..d7c05f60 100644 --- a/aspect-model-editor-web/src/main/java/org/eclipse/esmf/ame/api/GenerateController.java +++ b/aspect-model-editor-web/src/main/java/org/eclipse/esmf/ame/api/GenerateController.java @@ -18,6 +18,7 @@ import org.eclipse.esmf.ame.services.GenerateService; import org.eclipse.esmf.aspectmodel.generator.openapi.PagingOption; +import org.springframework.http.HttpHeaders; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; @@ -140,9 +141,9 @@ public ResponseEntity openApiSpec( @RequestBody final String aspectModel throws JsonProcessingException { final Optional properties = - !resourcePath.isEmpty() && (!ymlProperties.isEmpty() || !jsonProperties.isEmpty()) - ? Optional.of( !ymlProperties.isEmpty() ? ymlProperties : jsonProperties ) - : Optional.empty(); + !resourcePath.isEmpty() && (!ymlProperties.isEmpty() || !jsonProperties.isEmpty()) ? + Optional.of( !ymlProperties.isEmpty() ? ymlProperties : jsonProperties ) : + Optional.empty(); final String openApiOutput = generateOpenApiSpec( language, aspectModel, baseUrl, includeQueryApi, useSemanticVersion, pagingOption, resourcePath, properties, output ); @@ -165,4 +166,42 @@ private String generateOpenApiSpec( final String language, final String aspectMo return generateService.generateYamlOpenApiSpec( language, aspectModel, baseUrl, includeQueryApi, useSemanticVersion, pagingOption, Optional.of( resourcePath ), properties ); } + + /** + * This method is used to generate an AsyncApi specification of the Aspect Model + * + * @param aspectModel the Aspect Model Data + * @param language of the generated AsyncApi specification + * @param output of the AsyncApi specification + * @param applicationId Sets the application id, e.g. an identifying URL + * @param channelAddress Sets the channel address (i.e., for MQTT, the topic's name) + * @param useSemanticVersion if set to true, the complete semantic version of the Aspect Model will be used as + * the version of the API, otherwise only the major part of the Aspect Version is used as the version of the + * API. + * @param writeSeparateFiles Create separate files for each schema + * @return The AsyncApi specification + */ + @PostMapping( "async-api-spec" ) + public ResponseEntity asyncApiSpec( @RequestBody final String aspectModel, + @RequestParam( name = "language", defaultValue = "en" ) final String language, + @RequestParam( name = "output", defaultValue = "yaml" ) final String output, + @RequestParam( name = "applicationId", defaultValue = "" ) final String applicationId, + @RequestParam( name = "channelAddress", defaultValue = "" ) final String channelAddress, + @RequestParam( name = "useSemanticVersion", defaultValue = "false" ) final boolean useSemanticVersion, + @RequestParam( name = "writeSeparateFiles", defaultValue = "false" ) final boolean writeSeparateFiles ) { + final byte[] asyncApiSpec = generateService.generateAsyncApiSpec( aspectModel, language, output, applicationId, + channelAddress, useSemanticVersion, writeSeparateFiles ); + + return buildResponse( asyncApiSpec, writeSeparateFiles ); + } + + private ResponseEntity buildResponse( final byte[] asyncApiSpec, final boolean writeSeparateFiles ) { + final ResponseEntity.BodyBuilder responseBuilder = ResponseEntity.ok(); + + if ( writeSeparateFiles ) { + responseBuilder.header( HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=\"async-api-package.zip\"" ); + } + + return responseBuilder.body( asyncApiSpec ); + } } diff --git a/pom.xml b/pom.xml index 14b64404..91552465 100644 --- a/pom.xml +++ b/pom.xml @@ -61,7 +61,7 @@ - 2.6.1 + 2.7.0 3.0.0 @@ -81,7 +81,8 @@ 0.9.19 1.3 2.7.0 - 2.15.2 + 2.17.0 + 2.17.0 1.26.0 @@ -373,6 +374,11 @@ jackson-dataformat-xml ${jackson-dataformat-xml-version} + + com.fasterxml.jackson.core + jackson-core + ${jackson-core-version} + org.apache.commons commons-compress