diff --git a/springwolf-core/src/main/java/io/github/stavshamir/springwolf/configuration/DefaultAsyncApiDocketService.java b/springwolf-core/src/main/java/io/github/stavshamir/springwolf/configuration/DefaultAsyncApiDocketService.java index d71710bd6..b5744e3af 100644 --- a/springwolf-core/src/main/java/io/github/stavshamir/springwolf/configuration/DefaultAsyncApiDocketService.java +++ b/springwolf-core/src/main/java/io/github/stavshamir/springwolf/configuration/DefaultAsyncApiDocketService.java @@ -9,6 +9,7 @@ import org.springframework.lang.Nullable; import org.springframework.util.StringUtils; +import java.util.Map; import java.util.Optional; @Slf4j @@ -75,19 +76,29 @@ private AsyncApiDocket parseApplicationConfigProperties() { return builder.build(); } - private static Info buildInfo(@Nullable SpringwolfConfigProperties.ConfigDocket.Info info) { - if (info == null || !StringUtils.hasText(info.getVersion()) || !StringUtils.hasText(info.getTitle())) { + private static Info buildInfo(@Nullable SpringwolfConfigProperties.ConfigDocket.Info configDocketInfo) { + if (configDocketInfo == null + || !StringUtils.hasText(configDocketInfo.getVersion()) + || !StringUtils.hasText(configDocketInfo.getTitle())) { throw new IllegalArgumentException("One or more required fields of the info object (title, version) " + "in application.properties with path prefix " + SpringwolfConfigConstants.SPRINGWOLF_CONFIG_PREFIX + " is not set."); } - return Info.builder() - .version(info.getVersion()) - .title(info.getTitle()) - .description(info.getDescription()) - .contact(info.getContact()) - .license(info.getLicense()) + Info asyncapiInfo = Info.builder() + .version(configDocketInfo.getVersion()) + .title(configDocketInfo.getTitle()) + .description(configDocketInfo.getDescription()) + .contact(configDocketInfo.getContact()) + .license(configDocketInfo.getLicense()) .build(); + + // copy extension fields from configDocketInfo to asyncapiInfo. + if (configDocketInfo.getExtensionFields() != null) { + Map extFieldsMap = Map.copyOf(configDocketInfo.getExtensionFields()); + asyncapiInfo.setExtensionFields(extFieldsMap); + } + + return asyncapiInfo; } } diff --git a/springwolf-core/src/main/java/io/github/stavshamir/springwolf/configuration/properties/SpringwolfConfigProperties.java b/springwolf-core/src/main/java/io/github/stavshamir/springwolf/configuration/properties/SpringwolfConfigProperties.java index 61c38d550..7ff503c81 100644 --- a/springwolf-core/src/main/java/io/github/stavshamir/springwolf/configuration/properties/SpringwolfConfigProperties.java +++ b/springwolf-core/src/main/java/io/github/stavshamir/springwolf/configuration/properties/SpringwolfConfigProperties.java @@ -132,6 +132,12 @@ public static class Info { @NestedConfigurationProperty @Nullable private License license; + + /** + * Extension properties for the Info block. + */ + @Nullable + private Map extensionFields; } } diff --git a/springwolf-core/src/test/java/io/github/stavshamir/springwolf/configuration/DefaultAsyncApiDocketServiceIntegrationTest.java b/springwolf-core/src/test/java/io/github/stavshamir/springwolf/configuration/DefaultAsyncApiDocketServiceIntegrationTest.java index 54aef3e84..5c452b752 100644 --- a/springwolf-core/src/test/java/io/github/stavshamir/springwolf/configuration/DefaultAsyncApiDocketServiceIntegrationTest.java +++ b/springwolf-core/src/test/java/io/github/stavshamir/springwolf/configuration/DefaultAsyncApiDocketServiceIntegrationTest.java @@ -32,6 +32,7 @@ public class DefaultAsyncApiDocketServiceIntegrationTest { "springwolf.enabled=true", "springwolf.docket.info.title=Info title was loaded from spring properties", "springwolf.docket.info.version=1.0.0", + "springwolf.docket.info.extension-fields.x-api-name=api-name", "springwolf.docket.base-package=io.github.stavshamir.springwolf.example", "springwolf.docket.servers.test-protocol.protocol=test", "springwolf.docket.servers.test-protocol.url=some-server:1234" @@ -46,6 +47,7 @@ void testDocketContentShouldBeLoadedFromProperties() { AsyncApiDocket docket = asyncApiDocketService.getAsyncApiDocket(); assertThat(docket).isNotNull(); assertThat(docket.getInfo().getTitle()).isEqualTo("Info title was loaded from spring properties"); + assertThat(docket.getInfo().getExtensionFields().get("x-api-name")).isEqualTo("api-name"); } } diff --git a/springwolf-core/src/test/java/io/github/stavshamir/springwolf/configuration/DefaultAsyncApiDocketServiceTest.java b/springwolf-core/src/test/java/io/github/stavshamir/springwolf/configuration/DefaultAsyncApiDocketServiceTest.java index 6e6e71869..4b9bfd96f 100644 --- a/springwolf-core/src/test/java/io/github/stavshamir/springwolf/configuration/DefaultAsyncApiDocketServiceTest.java +++ b/springwolf-core/src/test/java/io/github/stavshamir/springwolf/configuration/DefaultAsyncApiDocketServiceTest.java @@ -9,6 +9,7 @@ import io.github.stavshamir.springwolf.configuration.properties.SpringwolfConfigProperties.ConfigDocket.Info; import org.junit.jupiter.api.Test; +import java.util.Map; import java.util.Optional; import static org.assertj.core.api.Assertions.assertThat; @@ -34,6 +35,7 @@ void testServiceShouldMapAllPropertiesToTheDocket() { info.setDescription("some-description"); info.setLicense(new License("license-name", "license-url")); info.setContact(new Contact("contact-name", "contact-url", "contact-email")); + info.setExtensionFields(Map.of("x-api-name", "api-name")); configDocket.setInfo(info); SpringwolfConfigProperties properties = new SpringwolfConfigProperties(); @@ -52,6 +54,8 @@ void testServiceShouldMapAllPropertiesToTheDocket() { assertThat(asyncApiDocket.getInfo().getDescription()).isEqualTo(info.getDescription()); assertThat(asyncApiDocket.getInfo().getLicense()).isEqualTo(info.getLicense()); assertThat(asyncApiDocket.getInfo().getContact()).isEqualTo(info.getContact()); + assertThat(asyncApiDocket.getInfo().getExtensionFields().get("x-api-name")) + .isEqualTo("api-name"); } @Test diff --git a/springwolf-examples/springwolf-amqp-example/src/main/java/io/github/stavshamir/springwolf/example/amqp/configuration/AsyncApiConfiguration.java b/springwolf-examples/springwolf-amqp-example/src/main/java/io/github/stavshamir/springwolf/example/amqp/configuration/AsyncApiConfiguration.java index 2e1d08cbe..4f457250a 100644 --- a/springwolf-examples/springwolf-amqp-example/src/main/java/io/github/stavshamir/springwolf/example/amqp/configuration/AsyncApiConfiguration.java +++ b/springwolf-examples/springwolf-amqp-example/src/main/java/io/github/stavshamir/springwolf/example/amqp/configuration/AsyncApiConfiguration.java @@ -16,6 +16,8 @@ import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import java.util.Map; + @Configuration public class AsyncApiConfiguration { @@ -50,6 +52,12 @@ public AsyncApiDocket asyncApiDocket() { .license(License.builder().name("Apache License 2.0").build()) .build(); + // the builder for asyncapi info, contact and license doesn't support setting/adding extensions, so + // we add text extension explicitely + info.setExtensionFields(Map.of("x-api-audience", "company-internal")); + info.getContact().setExtensionFields(Map.of("x-phone", "+49 123 456789")); + info.getLicense().setExtensionFields(Map.of("x-desc", "some description")); + Server amqp = Server.builder() .protocol("amqp") .url(String.format("%s:%s", amqpHost, amqpPort)) diff --git a/springwolf-examples/springwolf-amqp-example/src/main/resources/application.properties b/springwolf-examples/springwolf-amqp-example/src/main/resources/application.properties index 64df12a73..c0716f620 100644 --- a/springwolf-examples/springwolf-amqp-example/src/main/resources/application.properties +++ b/springwolf-examples/springwolf-amqp-example/src/main/resources/application.properties @@ -1,6 +1,6 @@ ######### # Spring configuration -spring.application.name=Springwolf example project - Amqp +spring.application.name=Springwolf example project - AMQP ######### @@ -19,10 +19,13 @@ springwolf.docket.info.title=${spring.application.name} springwolf.docket.info.version=1.0.0 springwolf.docket.info.description=Springwolf example project to demonstrate springwolfs abilities springwolf.docket.info.terms-of-service=http://asyncapi.org/terms +springwolf.docket.info.extension-fields.x-api-audience=company-internal springwolf.docket.info.contact.name=springwolf springwolf.docket.info.contact.email=example@example.com springwolf.docket.info.contact.url=https://github.com/springwolf/springwolf-core +springwolf.docket.info.contact.extension-fields.x-phone=+49 123 456789 springwolf.docket.info.license.name=Apache License 2.0 +springwolf.docket.info.license.extension-fields.x-desc=some description springwolf.docket.servers.amqp.protocol=amqp springwolf.docket.servers.amqp.url=${spring.rabbitmq.host}:${spring.rabbitmq.port} diff --git a/springwolf-examples/springwolf-amqp-example/src/test/java/io/github/stavshamir/springwolf/example/amqp/ApiWithDocketBeanIntegrationTest.java b/springwolf-examples/springwolf-amqp-example/src/test/java/io/github/stavshamir/springwolf/example/amqp/ApiWithDocketBeanIntegrationTest.java new file mode 100644 index 000000000..ea556874a --- /dev/null +++ b/springwolf-examples/springwolf-amqp-example/src/test/java/io/github/stavshamir/springwolf/example/amqp/ApiWithDocketBeanIntegrationTest.java @@ -0,0 +1,17 @@ +// SPDX-License-Identifier: Apache-2.0 +package io.github.stavshamir.springwolf.example.amqp; + +import org.springframework.test.context.TestPropertySource; + +/** + * Api integrationtest based on a SpringBoot application that defines a custom Docket bean. This contains Info and + * Server Informations as well as some explicit Producer and Consumer definitions. + */ +@TestPropertySource(properties = {"customAsyncApiDocketBean=true"}) +public class ApiWithDocketBeanIntegrationTest extends BaseApiIntegrationTest { + + @Override + protected String getExpectedApiFileName() { + return "/asyncapi.json"; + } +} diff --git a/springwolf-examples/springwolf-amqp-example/src/test/java/io/github/stavshamir/springwolf/example/amqp/ApiWithDocketFromEnvironmentIntegrationTest.java b/springwolf-examples/springwolf-amqp-example/src/test/java/io/github/stavshamir/springwolf/example/amqp/ApiWithDocketFromEnvironmentIntegrationTest.java new file mode 100644 index 000000000..a562f65ed --- /dev/null +++ b/springwolf-examples/springwolf-amqp-example/src/test/java/io/github/stavshamir/springwolf/example/amqp/ApiWithDocketFromEnvironmentIntegrationTest.java @@ -0,0 +1,17 @@ +// SPDX-License-Identifier: Apache-2.0 +package io.github.stavshamir.springwolf.example.amqp; + +import org.springframework.test.context.TestPropertySource; + +/** + * Api integrationtest based on a SpringBoot application that defines all info and server properties via + * spring environment (from application.properties). + */ +@TestPropertySource(properties = {"customAsyncApiDocketBean=false"}) +public class ApiWithDocketFromEnvironmentIntegrationTest extends BaseApiIntegrationTest { + + @Override + protected String getExpectedApiFileName() { + return "/asyncapi_withdocketfromenvironment.json"; + } +} diff --git a/springwolf-examples/springwolf-amqp-example/src/test/java/io/github/stavshamir/springwolf/example/amqp/ApiIntegrationTest.java b/springwolf-examples/springwolf-amqp-example/src/test/java/io/github/stavshamir/springwolf/example/amqp/BaseApiIntegrationTest.java similarity index 68% rename from springwolf-examples/springwolf-amqp-example/src/test/java/io/github/stavshamir/springwolf/example/amqp/ApiIntegrationTest.java rename to springwolf-examples/springwolf-amqp-example/src/test/java/io/github/stavshamir/springwolf/example/amqp/BaseApiIntegrationTest.java index aef9233ec..1545884cc 100644 --- a/springwolf-examples/springwolf-amqp-example/src/test/java/io/github/stavshamir/springwolf/example/amqp/ApiIntegrationTest.java +++ b/springwolf-examples/springwolf-amqp-example/src/test/java/io/github/stavshamir/springwolf/example/amqp/BaseApiIntegrationTest.java @@ -14,10 +14,16 @@ import static org.junit.jupiter.api.Assertions.assertEquals; +/** + * Api integrationtest base class defining a SpringBootTest and a test method which asserts the resulting asyncapi. + * Subclasses can customize this test with @TestPropertySources and custom expectation file names. + * @see ApiWithDocketBeanIntegrationTest + * @see ApiWithDocketFromEnvironmentIntegrationTest + */ @SpringBootTest( classes = {SpringwolfAmqpExampleApplication.class}, webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) -public class ApiIntegrationTest { +public abstract class BaseApiIntegrationTest { @Autowired private TestRestTemplate restTemplate; @@ -31,9 +37,12 @@ void asyncApiResourceArtifactTest() throws JSONException, IOException { String actual = restTemplate.getForObject(url, String.class); System.out.println("Got: " + actual); - InputStream s = this.getClass().getResourceAsStream("/asyncapi.json"); + String expectedApiFileName = getExpectedApiFileName(); + InputStream s = this.getClass().getResourceAsStream(expectedApiFileName); String expected = new String(s.readAllBytes(), StandardCharsets.UTF_8); assertEquals(expected, actual); } + + protected abstract String getExpectedApiFileName(); } diff --git a/springwolf-examples/springwolf-amqp-example/src/test/resources/asyncapi.json b/springwolf-examples/springwolf-amqp-example/src/test/resources/asyncapi.json index 93fd9c34f..b607d33f4 100644 --- a/springwolf-examples/springwolf-amqp-example/src/test/resources/asyncapi.json +++ b/springwolf-examples/springwolf-amqp-example/src/test/resources/asyncapi.json @@ -7,11 +7,14 @@ "contact": { "name": "springwolf", "url": "https://github.com/springwolf/springwolf-core", - "email": "example@example.com" + "email": "example@example.com", + "x-phone": "+49 123 456789" }, "license": { - "name": "Apache License 2.0" - } + "name": "Apache License 2.0", + "x-desc": "some description" + }, + "x-api-audience": "company-internal" }, "defaultContentType": "application/json", "servers": { diff --git a/springwolf-examples/springwolf-amqp-example/src/test/resources/asyncapi_withdocketfromenvironment.json b/springwolf-examples/springwolf-amqp-example/src/test/resources/asyncapi_withdocketfromenvironment.json new file mode 100644 index 000000000..57422225d --- /dev/null +++ b/springwolf-examples/springwolf-amqp-example/src/test/resources/asyncapi_withdocketfromenvironment.json @@ -0,0 +1,334 @@ +{ + "asyncapi": "2.6.0", + "info": { + "title": "Springwolf example project - AMQP", + "version": "1.0.0", + "description": "Springwolf example project to demonstrate springwolfs abilities", + "contact": { + "name": "springwolf", + "url": "https://github.com/springwolf/springwolf-core", + "email": "example@example.com", + "x-phone": "+49 123 456789" + }, + "license": { + "name": "Apache License 2.0", + "x-desc": "some description" + }, + "x-api-audience": "company-internal" + }, + "defaultContentType": "application/json", + "servers": { + "amqp": { + "url": "amqp:5672", + "protocol": "amqp" + } + }, + "channels": { + "another-queue": { + "publish": { + "operationId": "another-queue_publish_receiveAnotherPayload", + "description": "Auto-generated description", + "bindings": { + "amqp": { + "cc": [ + "another-queue" + ], + "bindingVersion": "0.2.0" + } + }, + "message": { + "schemaFormat": "application/vnd.oai.openapi+json;version=3.0.0", + "name": "io.github.stavshamir.springwolf.example.amqp.dtos.AnotherPayloadDto", + "title": "AnotherPayloadDto", + "payload": { + "$ref": "#/components/schemas/AnotherPayloadDto" + }, + "headers": { + "$ref": "#/components/schemas/HeadersNotDocumented" + }, + "bindings": { + "amqp": { + "bindingVersion": "0.2.0" + } + } + } + }, + "bindings": { + "amqp": { + "is": "queue", + "exchange": { + "name": "", + "type": "direct", + "durable": true, + "autoDelete": false, + "vhost": "/" + }, + "queue": { + "name": "another-queue", + "durable": false, + "exclusive": false, + "autoDelete": false, + "vhost": "/" + }, + "bindingVersion": "0.2.0" + } + } + }, + "example-producer-channel-publisher": { + "subscribe": { + "operationId": "example-producer-channel-publisher_subscribe", + "description": "Custom, optional description defined in the AsyncPublisher annotation", + "bindings": { + "amqp": { + "expiration": 0, + "cc": [ ], + "priority": 0, + "deliveryMode": 0, + "mandatory": false, + "timestamp": false, + "ack": false, + "bindingVersion": "0.2.0" + } + }, + "message": { + "schemaFormat": "application/vnd.oai.openapi+json;version=3.0.0", + "name": "io.github.stavshamir.springwolf.example.amqp.dtos.ExamplePayloadDto", + "title": "ExamplePayloadDto", + "description": "Example payload model", + "payload": { + "$ref": "#/components/schemas/ExamplePayloadDto" + }, + "headers": { + "$ref": "#/components/schemas/HeadersNotDocumented" + }, + "bindings": { + "amqp": { + "bindingVersion": "0.2.0" + } + } + } + } + }, + "example-queue": { + "publish": { + "operationId": "example-queue_publish_receiveExamplePayload", + "description": "Auto-generated description", + "bindings": { + "amqp": { + "cc": [ + "example-queue" + ], + "bindingVersion": "0.2.0" + } + }, + "message": { + "schemaFormat": "application/vnd.oai.openapi+json;version=3.0.0", + "name": "io.github.stavshamir.springwolf.example.amqp.dtos.ExamplePayloadDto", + "title": "ExamplePayloadDto", + "payload": { + "$ref": "#/components/schemas/ExamplePayloadDto" + }, + "headers": { + "$ref": "#/components/schemas/HeadersNotDocumented" + }, + "bindings": { + "amqp": { + "bindingVersion": "0.2.0" + } + } + } + }, + "bindings": { + "amqp": { + "is": "queue", + "exchange": { + "name": "", + "type": "direct", + "durable": true, + "autoDelete": false, + "vhost": "/" + }, + "queue": { + "name": "example-queue", + "durable": false, + "exclusive": false, + "autoDelete": false, + "vhost": "/" + }, + "bindingVersion": "0.2.0" + } + } + }, + "example-topic-queue": { + "publish": { + "operationId": "example-topic-queue_publish_bindingsBeanExample", + "description": "Auto-generated description", + "bindings": { + "amqp": { + "cc": [ + "example-topic-routing-key" + ], + "bindingVersion": "0.2.0" + } + }, + "message": { + "schemaFormat": "application/vnd.oai.openapi+json;version=3.0.0", + "name": "io.github.stavshamir.springwolf.example.amqp.dtos.AnotherPayloadDto", + "title": "AnotherPayloadDto", + "payload": { + "$ref": "#/components/schemas/AnotherPayloadDto" + }, + "headers": { + "$ref": "#/components/schemas/HeadersNotDocumented" + }, + "bindings": { + "amqp": { + "bindingVersion": "0.2.0" + } + } + } + }, + "bindings": { + "amqp": { + "is": "routingKey", + "exchange": { + "name": "example-topic-exchange", + "type": "topic", + "durable": true, + "autoDelete": false, + "vhost": "/" + }, + "queue": { + "name": "example-topic-queue", + "durable": true, + "exclusive": false, + "autoDelete": false, + "vhost": "/" + }, + "bindingVersion": "0.2.0" + } + } + }, + "example-topic-routing-key": { + "publish": { + "operationId": "example-topic-routing-key_publish_bindingsExample", + "description": "Auto-generated description", + "bindings": { + "amqp": { + "cc": [ + "example-topic-routing-key" + ], + "bindingVersion": "0.2.0" + } + }, + "message": { + "schemaFormat": "application/vnd.oai.openapi+json;version=3.0.0", + "name": "io.github.stavshamir.springwolf.example.amqp.dtos.AnotherPayloadDto", + "title": "AnotherPayloadDto", + "payload": { + "$ref": "#/components/schemas/AnotherPayloadDto" + }, + "headers": { + "$ref": "#/components/schemas/HeadersNotDocumented" + }, + "bindings": { + "amqp": { + "bindingVersion": "0.2.0" + } + } + } + }, + "bindings": { + "amqp": { + "is": "routingKey", + "exchange": { + "name": "example-bindings-exchange-name", + "type": "topic", + "durable": true, + "autoDelete": false, + "vhost": "/" + }, + "queue": { + "name": "example-bindings-queue", + "durable": false, + "exclusive": true, + "autoDelete": true, + "vhost": "/" + }, + "bindingVersion": "0.2.0" + } + } + } + }, + "components": { + "schemas": { + "AnotherPayloadDto": { + "required": [ + "example" + ], + "type": "object", + "properties": { + "example": { + "$ref": "#/components/schemas/ExamplePayloadDto" + }, + "foo": { + "type": "string", + "description": "Foo field", + "example": "bar" + } + }, + "description": "Another payload model", + "example": { + "example": { + "someEnum": "FOO2", + "someLong": 5, + "someString": "some string value" + }, + "foo": "bar" + } + }, + "ExamplePayloadDto": { + "required": [ + "someEnum", + "someString" + ], + "type": "object", + "properties": { + "someEnum": { + "type": "string", + "description": "Some enum field", + "example": "FOO2", + "enum": [ + "FOO1", + "FOO2", + "FOO3" + ] + }, + "someLong": { + "type": "integer", + "description": "Some long field", + "format": "int64", + "example": 5 + }, + "someString": { + "type": "string", + "description": "Some string field", + "example": "some string value" + } + }, + "description": "Example payload model", + "example": { + "someEnum": "FOO2", + "someLong": 5, + "someString": "some string value" + } + }, + "HeadersNotDocumented": { + "type": "object", + "properties": { }, + "example": { } + } + } + }, + "tags": [ ] +} \ No newline at end of file