diff --git a/plugins/asyncapi-spring-cloud-streams3/README.md b/plugins/asyncapi-spring-cloud-streams3/README.md index 038ec9ca..abb0b473 100644 --- a/plugins/asyncapi-spring-cloud-streams3/README.md +++ b/plugins/asyncapi-spring-cloud-streams3/README.md @@ -73,36 +73,37 @@ jbang zw -p io.zenwave360.sdk.plugins.SpringCloudStreams3Plugin --help ### Options -| **Option** | **Description** | **Type** | **Default** | **Values** | -|---------------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|-------------------------|----------------------|-----------------------------------| -| `specFile` | API Specification File | URI | | | -| `targetFolder` | Target folder to generate code to. If left empty, it will print to stdout. | File | | | -| `style` | Programming style | ProgrammingStyle | imperative | imperative, reactive | -| `role` | Project role: provider/client | AsyncapiRoleType | provider | provider, client | -| `exposeMessage` | Whether to expose underlying spring Message to consumers or not. | boolean | false | | -| `apiPackage` | Java API package name for producerApiPackage and consumerApiPackage if not specified. | String | | | -| `modelPackage` | Java Models package name | String | | | -| `producerApiPackage` | Java API package name for outbound (producer) services. It can override apiPackage for producers. | String | {{apiPackage}} | | -| `consumerApiPackage` | Java API package name for inbound (consumer) services. It can override apiPackage for consumer. | String | {{apiPackage}} | | -| `bindingTypes` | Binding names to include in code generation. Generates code for ALL bindings if left empty | List | | | -| `operationIds` | Operation ids to include in code generation. Generates code for ALL if left empty | List | [] | | -| `methodAndMessageSeparator` | To avoid method erasure conflicts, when exposeMessage or reactive style this character will be used as separator to append message payload type to method names in consumer interfaces. | String | $ | | -| `skipProducerImplementation` | Generate only the producer interface and skip the implementation. | boolean | false | | -| `transactionalOutbox` | Transactional outbox type for message producers. | TransactionalOutboxType | none | none, mongodb, jdbc | -| `useEnterpriseEnvelope` | Include support for enterprise envelop wrapping/unwrapping. | boolean | false | | -| `runtimeHeadersProperty` | AsyncAPI extension property name for runtime auto-configuration of headers. | String | x-runtime-expression | | -| `tracingIdSupplierQualifier` | Spring bean id for the tracing id supplier for runtime header with expression: '$tracingIdSupplier' | String | tracingIdSupplier | | -| `envelopeJavaTypeExtensionName` | AsyncAPI Message extension name for the envelop java type for wrapping/unwrapping. | String | x-envelope-java-type | | -| `includeKafkaCommonHeaders` | Include Kafka common headers 'kafka_messageKey' as x-runtime-header | boolean | false | | -| `consumerPrefix` | SC Streams Binder class prefix | String | | | -| `consumerSuffix` | SC Streams Binder class suffix | String | Consumer | | -| `bindingPrefix` | SC Streams Binding Name Prefix (used in @Component name) | String | | | -| `servicePrefix` | Business/Service interface prefix | String | I | | -| `serviceSuffix` | Business/Service interface suffix | String | ConsumerService | | -| `bindingSuffix` | Spring-Boot binding suffix. It will be appended to the operation name kebab-cased. E.g. -in-0 | String | -0 | | -| `formatter` | Code formatter implementation | Formatters | spring | google, palantir, spring, eclipse | -| `skipFormatting` | Skip java sources output formatting | boolean | false | | -| `haltOnFailFormatting` | Halt on formatting errors | boolean | true | | +| **Option** | **Description** | **Type** | **Default** | **Values** | +|-------------------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|-----------------------------------|----------------------|-----------------------------------| +| `specFile` | API Specification File | URI | | | +| `targetFolder` | Target folder to generate code to. If left empty, it will print to stdout. | File | | | +| `style` | Programming style | ProgrammingStyle | imperative | imperative, reactive | +| `role` | Project role: provider/client | AsyncapiRoleType | provider | provider, client | +| `exposeMessage` | Whether to expose underlying spring Message to consumers or not. | boolean | false | | +| `apiPackage` | Java API package name for producerApiPackage and consumerApiPackage if not specified. | String | | | +| `modelPackage` | Java Models package name | String | | | +| `producerApiPackage` | Java API package name for outbound (producer) services. It can override apiPackage for producers. | String | {{apiPackage}} | | +| `consumerApiPackage` | Java API package name for inbound (consumer) services. It can override apiPackage for consumer. | String | {{apiPackage}} | | +| `bindingTypes` | Binding names to include in code generation. Generates code for ALL bindings if left empty | List | | | +| `operationIds` | Operation ids to include in code generation. Generates code for ALL if left empty | List | [] | | +| `methodAndMessageSeparator` | To avoid method erasure conflicts, when exposeMessage or reactive style this character will be used as separator to append message payload type to method names in consumer interfaces. | String | $ | | +| `skipProducerImplementation` | Generate only the producer interface and skip the implementation. | boolean | false | | +| `transactionalOutbox` | Transactional outbox type for message producers. | TransactionalOutboxType | none | none, mongodb, jdbc | +| `useEnterpriseEnvelope` | Include support for enterprise envelop wrapping/unwrapping. | boolean | false | | +| `runtimeHeadersProperty` | AsyncAPI extension property name for runtime auto-configuration of headers. | String | x-runtime-expression | | +| `tracingIdSupplierQualifier` | Spring bean id for the tracing id supplier for runtime header with expression: '$tracingIdSupplier' | String | tracingIdSupplier | | +| `envelopeJavaTypeExtensionName` | AsyncAPI Message extension name for the envelop java type for wrapping/unwrapping. | String | x-envelope-java-type | | +| `includeKafkaCommonHeaders` | Include Kafka common headers 'kafka_messageKey' as x-runtime-header | boolean | false | | +| `consumerPrefix` | SC Streams Binder class prefix | String | | | +| `consumerSuffix` | SC Streams Binder class suffix | String | Consumer | | +| `bindingPrefix` | SC Streams Binding Name Prefix (used in @Component name) | String | | | +| `servicePrefix` | Business/Service interface prefix | String | I | | +| `serviceSuffix` | Business/Service interface suffix | String | ConsumerService | | +| `bindingSuffix` | Spring-Boot binding suffix. It will be appended to the operation name kebab-cased. E.g. -in-0 | String | -0 | | +| `functionalInterfaceImplementation` | Whether to use a 'java.util.function.Consumer' or 'java.util.function.Function' as the functional interface implementation. | FunctionalInterfaceImplementation | consumer | consumer, function | +| `formatter` | Code formatter implementation | Formatters | spring | google, palantir, spring, eclipse | +| `skipFormatting` | Skip java sources output formatting | boolean | false | | +| `haltOnFailFormatting` | Halt on formatting errors | boolean | true | | ### Populating Headers at Runtime Automatically diff --git a/plugins/asyncapi-spring-cloud-streams3/src/main/java/io/zenwave360/sdk/plugins/SpringCloudStreams3Generator.java b/plugins/asyncapi-spring-cloud-streams3/src/main/java/io/zenwave360/sdk/plugins/SpringCloudStreams3Generator.java index 24f8c618..2e528e04 100644 --- a/plugins/asyncapi-spring-cloud-streams3/src/main/java/io/zenwave360/sdk/plugins/SpringCloudStreams3Generator.java +++ b/plugins/asyncapi-spring-cloud-streams3/src/main/java/io/zenwave360/sdk/plugins/SpringCloudStreams3Generator.java @@ -27,6 +27,10 @@ public enum TransactionalOutboxType { none, mongodb, jdbc } + public enum FunctionalInterfaceImplementation { + consumer, function + } + public String sourceProperty = "api"; @DocumentedOption(description = "Programming style") @@ -68,6 +72,9 @@ public enum TransactionalOutboxType { @DocumentedOption(description = "Spring-Boot binding suffix. It will be appended to the operation name kebab-cased. E.g. -in-0") public String bindingSuffix = "-0"; + @DocumentedOption(description = "Whether to use a 'java.util.function.Consumer' or 'java.util.function.Function' as the functional interface implementation.") + public FunctionalInterfaceImplementation functionalInterfaceImplementation = FunctionalInterfaceImplementation.consumer; + @DocumentedOption(description = "AsyncAPI extension property name for runtime auto-configuration of headers.") public String runtimeHeadersProperty = "x-runtime-expression"; diff --git a/plugins/asyncapi-spring-cloud-streams3/src/main/resources/io/zenwave360/sdk/plugins/SpringCloudStream3Generator/consumer/imperative/Consumer.java.hbs b/plugins/asyncapi-spring-cloud-streams3/src/main/resources/io/zenwave360/sdk/plugins/SpringCloudStream3Generator/consumer/imperative/Consumer.java.hbs index d80daf2a..3b6abaa7 100644 --- a/plugins/asyncapi-spring-cloud-streams3/src/main/resources/io/zenwave360/sdk/plugins/SpringCloudStream3Generator/consumer/imperative/Consumer.java.hbs +++ b/plugins/asyncapi-spring-cloud-streams3/src/main/resources/io/zenwave360/sdk/plugins/SpringCloudStream3Generator/consumer/imperative/Consumer.java.hbs @@ -4,6 +4,7 @@ import java.io.PrintWriter; import java.io.StringWriter; import java.util.HashMap; import java.util.Map; +import java.util.function.Consumer; import java.util.function.Function; import org.slf4j.Logger; @@ -24,7 +25,13 @@ import {{modelPackage}}.*; @Component("{{bindingPrefix}}{{operation.x--operationIdKebabCase}}") @jakarta.annotation.Generated(value = "io.zenwave360.sdk.plugins.SpringCloudStreams3Plugin", date = "{{date}}") -public class {{consumerName operation.x--operationIdCamelCase}} implements Function, Void> { +public class {{consumerName operation.x--operationIdCamelCase}} implements + {{~#if (eq functionalInterfaceImplementation 'function')~}} + Function, Void> + {{~else~}} + Consumer> + {{~/if~}} +{ protected Logger log = LoggerFactory.getLogger(getClass()); @@ -51,7 +58,13 @@ public class {{consumerName operation.x--operationIdCamelCase}} implements Funct } {{~/if}} - @Override + // NOTE: implementing both accept and apply facilitates code generation. + // See what this class actually implements. + + public void accept(Message<{{messageType operation}}> message) { + apply(message); + } + public Void apply(Message<{{messageType operation}}> message) { log.debug("Received message: {}", message); try { @@ -86,12 +99,14 @@ public class {{consumerName operation.x--operationIdCamelCase}} implements Funct if (streamBridge != null && resolvedDLQ != null) { try { log.debug("Sending message to dead letter queue: {}", resolvedDLQ); + Object payload = message.getPayload(); var headers = new HashMap(message.getHeaders()); + headers.put("x-original-headers", message.getHeaders().toString()); headers.put("x-exception-type", e.getClass().getName()); headers.put("x-exception-message", e.getMessage()); headers.put("x-exception-stacktrace ", getStackTraceAsString(e)); - headers.put("x-exception-payload-type", message.getPayload().getClass().getName()); - streamBridge.send(resolvedDLQ, MessageBuilder.createMessage(message.getPayload(), new MessageHeaders(headers))); + headers.put("x-exception-payload-type", payload.getClass().getName()); + streamBridge.send(resolvedDLQ, MessageBuilder.createMessage(payload, new MessageHeaders(headers))); return null; } catch (Exception e1) { log.error("Error sending message to dead letter queue: {}", resolvedDLQ, e1); diff --git a/plugins/asyncapi-spring-cloud-streams3/src/main/resources/io/zenwave360/sdk/plugins/SpringCloudStream3Generator/consumer/imperative/IService.java.hbs b/plugins/asyncapi-spring-cloud-streams3/src/main/resources/io/zenwave360/sdk/plugins/SpringCloudStream3Generator/consumer/imperative/IService.java.hbs index 402f4b96..3b9a7b05 100644 --- a/plugins/asyncapi-spring-cloud-streams3/src/main/resources/io/zenwave360/sdk/plugins/SpringCloudStream3Generator/consumer/imperative/IService.java.hbs +++ b/plugins/asyncapi-spring-cloud-streams3/src/main/resources/io/zenwave360/sdk/plugins/SpringCloudStream3Generator/consumer/imperative/IService.java.hbs @@ -29,9 +29,14 @@ public interface {{serviceInterfaceName operation.x--operationIdCamelCase}} { * Default method for handling unknown messages or tombstone records (null record values). */ {{~#if exposeMessage}} - default void defaultHandler(Message msg) {}; + default void defaultHandler(Message msg) { + var payload = msg.getPayload(); + throw new UnsupportedOperationException("Payload type not supported: " + (payload != null? payload.getClass().getName() : null)); + }; {{~else}} - default void defaultHandler(Object payload, Map headers) {}; + default void defaultHandler(Object payload, Map headers) { + throw new UnsupportedOperationException("Payload type not supported: " + (payload != null? payload.getClass().getName() : null)); + }; {{~/if}} {{#each messages as |message|}} diff --git a/zenwave-sdk-cli/src/main/java/io/zenwave360/sdk/generators/AbstractAsyncapiGenerator.java b/zenwave-sdk-cli/src/main/java/io/zenwave360/sdk/generators/AbstractAsyncapiGenerator.java index 774233c4..c0dd904c 100644 --- a/zenwave-sdk-cli/src/main/java/io/zenwave360/sdk/generators/AbstractAsyncapiGenerator.java +++ b/zenwave-sdk-cli/src/main/java/io/zenwave360/sdk/generators/AbstractAsyncapiGenerator.java @@ -57,6 +57,9 @@ public boolean isProducer() { @DocumentedOption(description = "Operation ids to include in code generation. Generates code for ALL if left empty") public List operationIds = new ArrayList<>(); + @DocumentedOption(description = "Operation ids to include in code generation. Generates code for ALL if left empty") + public List excludeOperationIds = new ArrayList<>(); + public Map>> getPublishOperationsGroupedByTag(Model apiModel) { return getOperationsGroupedByTag(apiModel, AsyncapiOperationType.publish); } @@ -155,10 +158,15 @@ public boolean isProducer(Map operation) { } public boolean isSkipOperation(Map operation) { - if(operationIds == null || operationIds.isEmpty()) { - return false; + boolean isIncluded = true; + if(operationIds != null && !operationIds.isEmpty()) { + isIncluded = operationIds.contains((String) operation.get("operationId")); + } + boolean isExcluded = false; + if(excludeOperationIds != null && !excludeOperationIds.isEmpty()) { + isExcluded = excludeOperationIds.contains((String) operation.get("operationId")); } - return !operationIds.contains((String) operation.get("operationId")); + return !isIncluded || isExcluded; } diff --git a/zenwave-sdk-cli/src/main/java/io/zenwave360/sdk/generators/AbstractZDLProjectGenerator.java b/zenwave-sdk-cli/src/main/java/io/zenwave360/sdk/generators/AbstractZDLProjectGenerator.java index 58f0aa25..a3b89eab 100644 --- a/zenwave-sdk-cli/src/main/java/io/zenwave360/sdk/generators/AbstractZDLProjectGenerator.java +++ b/zenwave-sdk-cli/src/main/java/io/zenwave360/sdk/generators/AbstractZDLProjectGenerator.java @@ -118,7 +118,7 @@ public List generate(Map contextModel) { } for (TemplateInput template : templates.allServicesTemplates) { - templateOutputList.addAll(generateTemplateOutput(contextModel, template, Map.of("services", servicesList, "entities", entities.values()))); + templateOutputList.addAll(generateTemplateOutput(contextModel, template, Map.of("services", servicesList, "entities", new ArrayList(entities.values())))); } diff --git a/zenwave-sdk-cli/src/test/java/io/zenwave360/sdk/generators/AbstractAsyncapiGeneratorTest.java b/zenwave-sdk-cli/src/test/java/io/zenwave360/sdk/generators/AbstractAsyncapiGeneratorTest.java index b8e9ea8f..b19c62bf 100644 --- a/zenwave-sdk-cli/src/test/java/io/zenwave360/sdk/generators/AbstractAsyncapiGeneratorTest.java +++ b/zenwave-sdk-cli/src/test/java/io/zenwave360/sdk/generators/AbstractAsyncapiGeneratorTest.java @@ -58,6 +58,31 @@ public void test_filter_operations_for_provider_nobindings() throws Exception { Assertions.assertTrue(producerOperations.isEmpty()); } + @Test + public void test_filter_operations_for_provider_nobindings_includes() throws Exception { + Model model = loadAsyncapiModelFromResource("classpath:io/zenwave360/sdk/resources/asyncapi/v2/asyncapi-circular-refs.yml"); + AbstractAsyncapiGenerator asyncapiGenerator = newAbstractAsyncapiGenerator(); + asyncapiGenerator.role = AsyncapiRoleType.provider; + asyncapiGenerator.operationIds = Arrays.asList("doCreateProduct"); + Map>> consumerOperations = asyncapiGenerator.getSubscribeOperationsGroupedByTag(model); + Map>> producerOperations = asyncapiGenerator.getPublishOperationsGroupedByTag(model); + Assertions.assertEquals(1, consumerOperations.size()); + Assertions.assertEquals("doCreateProduct", consumerOperations.get("DefaultService").get(0).get("operationId")); + Assertions.assertTrue(producerOperations.isEmpty()); + } + + @Test + public void test_filter_operations_for_provider_nobindings_excludes() throws Exception { + Model model = loadAsyncapiModelFromResource("classpath:io/zenwave360/sdk/resources/asyncapi/v2/asyncapi-circular-refs.yml"); + AbstractAsyncapiGenerator asyncapiGenerator = newAbstractAsyncapiGenerator(); + asyncapiGenerator.role = AsyncapiRoleType.provider; + asyncapiGenerator.excludeOperationIds = Arrays.asList("doCreateProduct"); + Map>> consumerOperations = asyncapiGenerator.getSubscribeOperationsGroupedByTag(model); + Map>> producerOperations = asyncapiGenerator.getPublishOperationsGroupedByTag(model); + Assertions.assertEquals(0, consumerOperations.size()); + } + + @Test public void test_filter_operations_for_provider_nobindings_v3() throws Exception { Model model = loadAsyncapiModelFromResource("classpath:io/zenwave360/sdk/resources/asyncapi/v3/customer-order-asyncapi.yml");