diff --git a/api/src/main/java/io/serverlessworkflow/api/OneOfValueProvider.java b/api/src/main/java/io/serverlessworkflow/api/OneOfValueProvider.java index f3d2ab26..9d17b872 100644 --- a/api/src/main/java/io/serverlessworkflow/api/OneOfValueProvider.java +++ b/api/src/main/java/io/serverlessworkflow/api/OneOfValueProvider.java @@ -15,6 +15,6 @@ */ package io.serverlessworkflow.api; -public interface OneOfValueProvider { - Object get(); +public interface OneOfValueProvider { + T get(); } diff --git a/api/src/main/java/io/serverlessworkflow/api/WorkflowReader.java b/api/src/main/java/io/serverlessworkflow/api/WorkflowReader.java index 01c6c8b0..4decc696 100644 --- a/api/src/main/java/io/serverlessworkflow/api/WorkflowReader.java +++ b/api/src/main/java/io/serverlessworkflow/api/WorkflowReader.java @@ -16,10 +16,12 @@ package io.serverlessworkflow.api; import io.serverlessworkflow.api.types.Workflow; +import java.io.ByteArrayInputStream; import java.io.FileNotFoundException; import java.io.IOException; import java.io.InputStream; import java.io.Reader; +import java.io.StringReader; import java.nio.file.Files; import java.nio.file.Path; @@ -37,6 +39,19 @@ public static Workflow readWorkflow(Path path, WorkflowFormat format) throws IOE return format.mapper().readValue(Files.readAllBytes(path), Workflow.class); } + public static Workflow readWorkflow(byte[] content, WorkflowFormat format) throws IOException { + try (InputStream input = new ByteArrayInputStream(content)) { + return readWorkflow(input, format); + } + } + + public static Workflow readWorkflowFromString(String content, WorkflowFormat format) + throws IOException { + try (Reader reader = new StringReader(content)) { + return readWorkflow(reader, format); + } + } + public static Workflow readWorkflowFromClasspath(String classpath) throws IOException { return readWorkflowFromClasspath( classpath, diff --git a/api/src/main/java/io/serverlessworkflow/api/WorkflowWriter.java b/api/src/main/java/io/serverlessworkflow/api/WorkflowWriter.java index f98e6402..29115396 100644 --- a/api/src/main/java/io/serverlessworkflow/api/WorkflowWriter.java +++ b/api/src/main/java/io/serverlessworkflow/api/WorkflowWriter.java @@ -16,8 +16,10 @@ package io.serverlessworkflow.api; import io.serverlessworkflow.api.types.Workflow; +import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.OutputStream; +import java.io.StringWriter; import java.io.Writer; import java.nio.file.Files; import java.nio.file.Path; @@ -45,5 +47,21 @@ public static void writeWorkflow(Path output, Workflow workflow, WorkflowFormat } } + public static String workflowAsString(Workflow workflow, WorkflowFormat format) + throws IOException { + try (Writer writer = new StringWriter()) { + writeWorkflow(writer, workflow, format); + return writer.toString(); + } + } + + public static byte[] workflowAsBytes(Workflow workflow, WorkflowFormat format) + throws IOException { + try (ByteArrayOutputStream out = new ByteArrayOutputStream()) { + writeWorkflow(out, workflow, format); + return out.toByteArray(); + } + } + private WorkflowWriter() {} } diff --git a/api/src/main/java/io/serverlessworkflow/serialization/DeserializeHelper.java b/api/src/main/java/io/serverlessworkflow/serialization/DeserializeHelper.java index 72b4cce0..cfbd54ca 100644 --- a/api/src/main/java/io/serverlessworkflow/serialization/DeserializeHelper.java +++ b/api/src/main/java/io/serverlessworkflow/serialization/DeserializeHelper.java @@ -21,24 +21,57 @@ import com.fasterxml.jackson.databind.JsonMappingException; import jakarta.validation.ConstraintViolationException; import java.io.IOException; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.util.ArrayList; import java.util.Collection; public class DeserializeHelper { public static T deserializeOneOf( - JsonParser p, Class targetClass, Collection> unionTypes) throws IOException { + JsonParser p, Class targetClass, Collection> oneOfTypes) throws IOException { TreeNode node = p.readValueAsTree(); - JsonProcessingException ex = - new JsonMappingException(p, "Problem deserializing " + targetClass); - for (Class unionType : unionTypes) { - try { - Object object = p.getCodec().treeToValue(node, unionType); - return targetClass.getConstructor(unionType).newInstance(object); - } catch (IOException | ReflectiveOperationException | ConstraintViolationException io) { - ex.addSuppressed(io); + try { + T result = targetClass.getDeclaredConstructor().newInstance(); + Collection exceptions = new ArrayList<>(); + for (Class oneOfType : oneOfTypes) { + try { + assingIt(p, result, node, targetClass, oneOfType); + break; + } catch (IOException | ConstraintViolationException | InvocationTargetException ex) { + exceptions.add(ex); + } + } + if (exceptions.size() == oneOfTypes.size()) { + JsonMappingException ex = + new JsonMappingException( + p, + String.format( + "Error deserializing class %s, all oneOf alternatives %s has failed ", + targetClass, oneOfTypes)); + exceptions.forEach(ex::addSuppressed); + throw ex; + } + return result; + } catch (ReflectiveOperationException ex) { + throw new IllegalStateException(ex); + } + } + + private static void assingIt( + JsonParser p, T result, TreeNode node, Class targetClass, Class type) + throws JsonProcessingException, ReflectiveOperationException { + findSetMethod(targetClass, type).invoke(result, p.getCodec().treeToValue(node, type)); + } + + private static Method findSetMethod(Class targetClass, Class type) { + for (Method method : targetClass.getMethods()) { + OneOfSetter oneOfSetter = method.getAnnotation(OneOfSetter.class); + if (oneOfSetter != null && type.equals(oneOfSetter.value())) { + return method; } } - throw ex; + throw new IllegalStateException("Cannot find a setter for type " + type); } public static T deserializeItem(JsonParser p, Class targetClass, Class valueClass) diff --git a/api/src/main/java/io/serverlessworkflow/serialization/OneOfSetter.java b/api/src/main/java/io/serverlessworkflow/serialization/OneOfSetter.java new file mode 100644 index 00000000..098df425 --- /dev/null +++ b/api/src/main/java/io/serverlessworkflow/serialization/OneOfSetter.java @@ -0,0 +1,28 @@ +/* + * Copyright 2020-Present The Serverless Workflow Specification Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.serverlessworkflow.serialization; + +import static java.lang.annotation.ElementType.METHOD; +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +@Retention(RUNTIME) +@Target(METHOD) +public @interface OneOfSetter { + Class value(); +} diff --git a/api/src/test/java/io/serverlessworkflow/api/FeaturesTest.java b/api/src/test/java/io/serverlessworkflow/api/FeaturesTest.java index fd16b952..81d10ecf 100644 --- a/api/src/test/java/io/serverlessworkflow/api/FeaturesTest.java +++ b/api/src/test/java/io/serverlessworkflow/api/FeaturesTest.java @@ -17,7 +17,10 @@ import static io.serverlessworkflow.api.WorkflowReader.readWorkflow; import static io.serverlessworkflow.api.WorkflowReader.readWorkflowFromClasspath; +import static io.serverlessworkflow.api.WorkflowWriter.workflowAsBytes; +import static io.serverlessworkflow.api.WorkflowWriter.workflowAsString; import static io.serverlessworkflow.api.WorkflowWriter.writeWorkflow; +import static org.assertj.core.api.Assertions.assertThat; import static org.junit.jupiter.api.Assertions.assertNotNull; import io.serverlessworkflow.api.types.Workflow; @@ -32,6 +35,13 @@ public class FeaturesTest { @ParameterizedTest @ValueSource( strings = { + "features/authentication-bearer.yaml", + "features/authentication-bearer-uri-format.yaml", + "features/authentication-oauth2.yaml", + "features/authentication-oauth2-secret.yaml", + "features/authentication-oidc.yaml", + "features/authentication-oidc-secret.yaml", + "features/authentication-reusable.yaml", "features/callHttp.yaml", "features/callOpenAPI.yaml", "features/composite.yaml", @@ -45,12 +55,13 @@ public class FeaturesTest { "features/try.yaml", "features/listen.yaml", "features/callFunction.yaml", - "features/callCustomFunction.yaml" + "features/callCustomFunction.yaml", + "features/call-http-query-parameters.yaml" }) public void testSpecFeaturesParsing(String workflowLocation) throws IOException { Workflow workflow = readWorkflowFromClasspath(workflowLocation); assertWorkflow(workflow); - assertWorkflow(writeAndReadInMemory(workflow)); + assertWorkflowEquals(workflow, writeAndReadInMemory(workflow)); } private static Workflow writeAndReadInMemory(Workflow workflow) throws IOException { @@ -69,4 +80,11 @@ private static void assertWorkflow(Workflow workflow) { assertNotNull(workflow.getDocument()); assertNotNull(workflow.getDo()); } + + private static void assertWorkflowEquals(Workflow workflow, Workflow other) throws IOException { + assertThat(workflowAsString(workflow, WorkflowFormat.YAML)) + .isEqualTo(workflowAsString(other, WorkflowFormat.YAML)); + assertThat(workflowAsBytes(workflow, WorkflowFormat.JSON)) + .isEqualTo(workflowAsBytes(other, WorkflowFormat.JSON)); + } } diff --git a/api/src/test/resources/features/authentication-bearer-uri-format.yaml b/api/src/test/resources/features/authentication-bearer-uri-format.yaml new file mode 100644 index 00000000..b0019fbb --- /dev/null +++ b/api/src/test/resources/features/authentication-bearer-uri-format.yaml @@ -0,0 +1,15 @@ +document: + dsl: '1.0.0-alpha5' + namespace: examples + name: bearer-auth + version: '0.1.0' +do: + - getPet: + call: http + with: + method: get + endpoint: + uri: https://petstore.swagger.io/v2/pet/{petId} + authentication: + bearer: + token: ${ .token } diff --git a/api/src/test/resources/features/authentication-bearer.yaml b/api/src/test/resources/features/authentication-bearer.yaml new file mode 100644 index 00000000..f0c42741 --- /dev/null +++ b/api/src/test/resources/features/authentication-bearer.yaml @@ -0,0 +1,15 @@ +document: + dsl: '1.0.0-alpha5' + namespace: examples + name: bearer-auth-uri-format + version: '0.1.0' +do: + - getPet: + call: http + with: + method: get + endpoint: + uri: https://petstore.swagger.io/v2/pet/1 + authentication: + bearer: + token: ${ .token } \ No newline at end of file diff --git a/api/src/test/resources/features/authentication-oauth2-secret.yaml b/api/src/test/resources/features/authentication-oauth2-secret.yaml new file mode 100644 index 00000000..635076ab --- /dev/null +++ b/api/src/test/resources/features/authentication-oauth2-secret.yaml @@ -0,0 +1,18 @@ +document: + dsl: 1.0.0-alpha1 + namespace: examples + name: oauth2-authentication + version: 1.0.0-alpha1 +use: + secrets: + - mySecret +do: + - getPet: + call: http + with: + method: get + endpoint: + uri: https://petstore.swagger.io/v2/pet/{petId} + authentication: + oauth2: + use: mySecret \ No newline at end of file diff --git a/api/src/test/resources/features/authentication-oauth2.yaml b/api/src/test/resources/features/authentication-oauth2.yaml new file mode 100644 index 00000000..625a1e2c --- /dev/null +++ b/api/src/test/resources/features/authentication-oauth2.yaml @@ -0,0 +1,22 @@ +document: + dsl: '1.0.0-alpha5' + namespace: examples + name: oauth2-authentication + version: '0.1.0' +do: + - getPet: + call: http + with: + method: get + endpoint: + uri: https://petstore.swagger.io/v2/pet/{petId} + authentication: + oauth2: + authority: http://keycloak/realms/fake-authority + endpoints: #optional + token: /auth/token #defaults to /oauth2/token + introspection: /auth/introspect #defaults to /oauth2/introspect + grant: client_credentials + client: + id: workflow-runtime-id + secret: workflow-runtime-secret \ No newline at end of file diff --git a/api/src/test/resources/features/authentication-oidc-secret.yaml b/api/src/test/resources/features/authentication-oidc-secret.yaml new file mode 100644 index 00000000..19c387c1 --- /dev/null +++ b/api/src/test/resources/features/authentication-oidc-secret.yaml @@ -0,0 +1,18 @@ +document: + dsl: 1.0.0-alpha1 + namespace: examples + name: oidc-authentication + version: 1.0.0-alpha1 +use: + secrets: + - mySecret +do: + - getPet: + call: http + with: + method: get + endpoint: + uri: https://petstore.swagger.io/v2/pet/{petId} + authentication: + oidc: + use: mySecret \ No newline at end of file diff --git a/api/src/test/resources/features/authentication-oidc.yaml b/api/src/test/resources/features/authentication-oidc.yaml new file mode 100644 index 00000000..18aec74d --- /dev/null +++ b/api/src/test/resources/features/authentication-oidc.yaml @@ -0,0 +1,19 @@ +document: + dsl: '1.0.0-alpha5' + namespace: examples + name: oidc-authentication + version: '0.1.0' +do: + - getPet: + call: http + with: + method: get + endpoint: + uri: https://petstore.swagger.io/v2/pet/{petId} + authentication: + oidc: + authority: http://keycloak/realms/fake-authority #endpoints are resolved using the OIDC configuration located at '/.well-known/openid-configuration' + grant: client_credentials + client: + id: workflow-runtime-id + secret: workflow-runtime-secret \ No newline at end of file diff --git a/api/src/test/resources/features/authentication-reusable.yaml b/api/src/test/resources/features/authentication-reusable.yaml new file mode 100644 index 00000000..a5da803d --- /dev/null +++ b/api/src/test/resources/features/authentication-reusable.yaml @@ -0,0 +1,19 @@ +document: + dsl: '1.0.0-alpha5' + namespace: examples + name: bearer-auth + version: '0.1.0' +use: + authentications: + petStoreAuth: + bearer: + token: ${ .token } +do: + - getPet: + call: http + with: + method: get + endpoint: + uri: https://petstore.swagger.io/v2/pet/{petId} + authentication: + use: petStoreAuth diff --git a/api/src/test/resources/features/call-http-query-parameters.yaml b/api/src/test/resources/features/call-http-query-parameters.yaml new file mode 100644 index 00000000..95934315 --- /dev/null +++ b/api/src/test/resources/features/call-http-query-parameters.yaml @@ -0,0 +1,24 @@ +document: + dsl: 1.0.0-alpha2 + namespace: examples + name: http-query-params + version: 1.0.0-alpha2 +input: + schema: + format: json + document: + type: object + required: + - searchQuery + properties: + searchQuery: + type: string +do: + - searchStarWarsCharacters: + call: http + with: + method: get + endpoint: https://swapi.dev/api/people/ + query: + search: ${.searchQuery} + diff --git a/custom-generator/src/main/java/io/serverlessworkflow/generator/AllAnyOneOfSchemaRule.java b/custom-generator/src/main/java/io/serverlessworkflow/generator/AllAnyOneOfSchemaRule.java index ab0c1a23..df3ead34 100644 --- a/custom-generator/src/main/java/io/serverlessworkflow/generator/AllAnyOneOfSchemaRule.java +++ b/custom-generator/src/main/java/io/serverlessworkflow/generator/AllAnyOneOfSchemaRule.java @@ -136,96 +136,138 @@ public JType apply( Schema schema) { Optional refType = refType(nodeName, schemaNode, parent, generatableType, schema); - List unionTypes = new ArrayList<>(); + List oneOfTypes = new ArrayList<>(); + List allOfTypes = new ArrayList<>(); - unionType("oneOf", nodeName, schemaNode, parent, generatableType, schema, unionTypes); - unionType("anyOf", nodeName, schemaNode, parent, generatableType, schema, unionTypes); - unionType("allOf", nodeName, schemaNode, parent, generatableType, schema, unionTypes); + unionType("oneOf", nodeName, schemaNode, parent, generatableType, schema, oneOfTypes); + unionType("anyOf", nodeName, schemaNode, parent, generatableType, schema, oneOfTypes); + unionType("allOf", nodeName, schemaNode, parent, generatableType, schema, allOfTypes); - Collections.sort(unionTypes); + Collections.sort(oneOfTypes); JType javaType; if (schemaNode.has("enum")) { javaType = ruleFactory.getEnumRule().apply(nodeName, schemaNode, parent, generatableType, schema); - } else if (!schemaNode.has("properties") && unionTypes.isEmpty() && refType.isPresent()) { + } else if (!schemaNode.has("properties") + && oneOfTypes.isEmpty() + && allOfTypes.isEmpty() + && refType.isPresent()) { javaType = refType.get(); - } else { - javaType = - ruleFactory - .getTypeRule() - .apply(nodeName, schemaNode, parent, generatableType.getPackage(), schema); + JPackage container = generatableType.getPackage(); + javaType = ruleFactory.getTypeRule().apply(nodeName, schemaNode, parent, container, schema); if (javaType instanceof JDefinedClass) { - populateClass(schema, (JDefinedClass) javaType, refType, unionTypes); - } else if (!unionTypes.isEmpty()) { javaType = - createUnionClass( - schema, nodeName, schemaNode, generatableType.getPackage(), refType, unionTypes); + populateAllOf( + schema, populateRef((JDefinedClass) javaType, refType, schema), allOfTypes); + } + if (!oneOfTypes.isEmpty()) { + try { + JDefinedClass unionClass; + Optional commonType; + if (javaType instanceof JDefinedClass) { + JDefinedClass clazz = (JDefinedClass) javaType; + if (clazz.methods().isEmpty()) { + unionClass = clazz; + commonType = Optional.empty(); + } else { + unionClass = container._class(clazz.name() + "Union"); + commonType = Optional.of(clazz); + } + } else { + unionClass = + container._class( + ruleFactory + .getNameHelper() + .getUniqueClassName(nodeName, schemaNode, container)); + commonType = Optional.empty(); + } + javaType = populateOneOf(schema, unionClass, commonType, oneOfTypes); + } catch (JClassAlreadyExistsException ex) { + throw new IllegalStateException(ex); + } } schema.setJavaTypeIfEmpty(javaType); } return javaType; } - private JDefinedClass populateClass( + private JDefinedClass populateAllOf( + Schema parentSchema, JDefinedClass definedClass, Collection allOfTypes) { + return wrapAll(parentSchema, definedClass, Optional.empty(), allOfTypes, Optional.empty()); + } + + private JDefinedClass populateOneOf( Schema parentSchema, JDefinedClass definedClass, - Optional refType, - Collection unionTypes) { - JType clazzClass = definedClass.owner()._ref(Object.class); - - Optional valueField; - if (!unionTypes.isEmpty()) { - valueField = - Optional.of( - definedClass.field( - JMod.PRIVATE, - clazzClass, - ruleFactory.getNameHelper().getPropertyName("value", null), - null)); - - definedClass._implements( - definedClass.owner().ref(GeneratorUtils.ONE_OF_VALUE_PROVIDER_INTERFACE_NAME)); - - GeneratorUtils.implementInterface(definedClass, valueField.orElseThrow()); - - try { - JDefinedClass serializer = generateSerializer(definedClass); - definedClass.annotate(JsonSerialize.class).param("using", serializer); - } catch (JClassAlreadyExistsException ex) { - // already serialized aware - } + Optional commonType, + Collection oneOfTypes) { - try { - JDefinedClass deserializer = generateDeserializer(definedClass, unionTypes); - definedClass.annotate(JsonDeserialize.class).param("using", deserializer); - } catch (JClassAlreadyExistsException ex) { - // already deserialized aware - } + JFieldVar valueField = + definedClass.field( + JMod.PRIVATE, + commonType.orElse(definedClass.owner().ref(Object.class)), + ruleFactory.getNameHelper().getPropertyName("value", null), + null); + + definedClass._implements( + definedClass + .owner() + .ref(GeneratorUtils.ONE_OF_VALUE_PROVIDER_INTERFACE_NAME) + .narrow(valueField.type())); + GeneratorUtils.implementInterface(definedClass, valueField); + try { + JDefinedClass serializer = generateSerializer(definedClass); + definedClass.annotate(JsonSerialize.class).param("using", serializer); + } catch (JClassAlreadyExistsException ex) { + // already serialized aware + } - Collection stringTypes = new ArrayList<>(); - for (JTypeWrapper unionType : unionTypes) { - if (isStringType(unionType.getType())) { - stringTypes.add(unionType); - } else { - wrapIt(parentSchema, definedClass, valueField, unionType.getType(), unionType.getNode()); + try { + JDefinedClass deserializer = + generateDeserializer(definedClass, oneOfTypes, "deserializeOneOf"); + definedClass.annotate(JsonDeserialize.class).param("using", deserializer); + } catch (JClassAlreadyExistsException ex) { + // already deserialized aware + } + + return wrapAll(parentSchema, definedClass, commonType, oneOfTypes, Optional.of(valueField)); + } + + private JDefinedClass wrapAll( + Schema parentSchema, + JDefinedClass definedClass, + Optional commonType, + Collection types, + Optional valueField) { + Collection stringTypes = new ArrayList<>(); + for (JTypeWrapper unionType : types) { + if (isStringType(unionType.getType())) { + stringTypes.add(unionType); + } else { + if (unionType.getType() instanceof JDefinedClass) { + commonType.ifPresent( + c -> ((JDefinedClass) unionType.getType())._extends((JDefinedClass) c)); } - } - if (!stringTypes.isEmpty()) { - wrapStrings(parentSchema, definedClass, valueField, stringTypes); - } - } else { - valueField = Optional.empty(); + wrapIt(parentSchema, definedClass, valueField, unionType.getType(), unionType.getNode()); + } + } + if (!stringTypes.isEmpty()) { + wrapStrings(parentSchema, definedClass, valueField, stringTypes); } + return definedClass; + } + private JDefinedClass populateRef( + JDefinedClass definedClass, Optional refType, Schema parentSchema) { refType.ifPresent( type -> { if (type instanceof JClass) { definedClass._extends((JClass) type); } else { - wrapIt(parentSchema, definedClass, valueField, type, null); + wrapIt(parentSchema, definedClass, Optional.empty(), type, null); } }); @@ -258,7 +300,7 @@ private JDefinedClass generateSerializer(JDefinedClass relatedClass) } private JDefinedClass generateDeserializer( - JDefinedClass relatedClass, Collection unionTypes) + JDefinedClass relatedClass, Collection oneOfTypes, String methodName) throws JClassAlreadyExistsException { JDefinedClass definedClass = GeneratorUtils.deserializerClass(relatedClass); GeneratorUtils.fillDeserializer( @@ -266,37 +308,23 @@ private JDefinedClass generateDeserializer( relatedClass, (method, parserParam) -> { JBlock body = method.body(); - JInvocation list = definedClass.owner().ref(List.class).staticInvoke("of"); - unionTypes.forEach(c -> list.arg(((JClass) c.getType()).dotclass())); + body._return( definedClass .owner() .ref(GeneratorUtils.DESERIALIZE_HELPER_NAME) - .staticInvoke("deserializeOneOf") + .staticInvoke(methodName) .arg(parserParam) .arg(relatedClass.dotclass()) - .arg(list)); + .arg(list(definedClass, oneOfTypes))); }); return definedClass; } - private JDefinedClass createUnionClass( - Schema parentSchema, - String nodeName, - JsonNode schemaNode, - JPackage container, - Optional refType, - Collection unionTypes) { - try { - return populateClass( - parentSchema, - container._class( - ruleFactory.getNameHelper().getUniqueClassName(nodeName, schemaNode, container)), - refType, - unionTypes); - } catch (JClassAlreadyExistsException e) { - throw new IllegalArgumentException(e); - } + private JInvocation list(JDefinedClass definedClass, Collection list) { + JInvocation result = definedClass.owner().ref(List.class).staticInvoke("of"); + list.forEach(c -> result.arg(((JClass) c.getType()).dotclass())); + return result; } private void wrapIt( @@ -306,11 +334,39 @@ private void wrapIt( JType unionType, JsonNode node) { JFieldVar instanceField = getInstanceField(parentSchema, definedClass, unionType, node); - JMethod constructor = definedClass.constructor(JMod.PUBLIC); - JVar instanceParam = constructor.param(unionType, instanceField.name()); - JBlock body = constructor.body(); - valueField.ifPresent(v -> body.assign(JExpr._this().ref(v), instanceParam)); - body.assign(JExpr._this().ref(instanceField), instanceParam); + JMethod method = getSetterMethod(definedClass, instanceField, node); + method + .body() + .assign( + JExpr._this().ref(instanceField), + setupMethod(definedClass, method, valueField, instanceField)); + } + + private JVar setupMethod( + JDefinedClass definedClass, + JMethod method, + Optional valueField, + JFieldVar instanceField) { + JVar methodParam = method.param(instanceField.type(), instanceField.name()); + valueField.ifPresent( + v -> { + method.body().assign(JExpr._this().ref(v), methodParam); + method + .annotate(definedClass.owner().ref(GeneratorUtils.SETTER_ANNOTATION_NAME)) + .param("value", instanceField.type()); + }); + return methodParam; + } + + private JMethod getSetterMethod( + JDefinedClass definedClass, JFieldVar instanceField, JsonNode node) { + String setterName = ruleFactory.getNameHelper().getSetterName(instanceField.name(), node); + JMethod fluentMethod = + definedClass.method(JMod.PUBLIC, definedClass, setterName.replaceFirst("set", "with")); + JBlock body = fluentMethod.body(); + body.assign(instanceField, fluentMethod.param(instanceField.type(), "value")); + body._return(JExpr._this()); + return definedClass.method(JMod.PUBLIC, definedClass.owner().VOID, setterName); } private void wrapStrings( @@ -320,21 +376,19 @@ private void wrapStrings( Collection stringTypes) { Iterator iter = stringTypes.iterator(); JTypeWrapper first = iter.next(); - JMethod constructor = definedClass.constructor(JMod.PUBLIC); - - JBlock body = constructor.body(); String pattern = pattern(first.getNode(), parentSchema); if (pattern == null && iter.hasNext()) { pattern = ".*"; } JFieldVar instanceField = getInstanceField(parentSchema, definedClass, first.getType(), first.getNode()); - JVar instanceParam = constructor.param(first.type, instanceField.name()); - valueField.ifPresent(v -> body.assign(JExpr._this().ref(v), instanceParam)); + JMethod setterMethod = getSetterMethod(definedClass, instanceField, first.getNode()); + JVar methodParam = setupMethod(definedClass, setterMethod, valueField, instanceField); + JBlock body = setterMethod.body(); if (pattern != null) { JConditional condition = - body._if(getPatternCondition(pattern, body, instanceField, instanceParam, definedClass)); - condition._then().assign(JExpr._this().ref(instanceField), instanceParam); + body._if(getPatternCondition(pattern, body, instanceField, methodParam, definedClass)); + condition._then().assign(JExpr._this().ref(instanceField), methodParam); while (iter.hasNext()) { JTypeWrapper item = iter.next(); instanceField = @@ -345,8 +399,8 @@ private void wrapStrings( } condition = condition._elseif( - getPatternCondition(pattern, body, instanceField, instanceParam, definedClass)); - condition._then().assign(JExpr._this().ref(instanceField), instanceParam); + getPatternCondition(pattern, body, instanceField, methodParam, definedClass)); + condition._then().assign(JExpr._this().ref(instanceField), methodParam); } condition ._else() @@ -358,10 +412,10 @@ private void wrapStrings( .ref(String.class) .staticInvoke("format") .arg("%s does not match any pattern") - .arg(instanceParam)) + .arg(methodParam)) .arg(JExpr._null())); } else { - body.assign(JExpr._this().ref(instanceField), instanceParam); + body.assign(JExpr._this().ref(instanceField), methodParam); } } @@ -374,7 +428,7 @@ private JFieldVar getInstanceField( ruleFactory .getNameHelper() .getPropertyName(getTypeName(node, type, parentSchema), node)); - GeneratorUtils.buildMethod( + GeneratorUtils.getterMethod( definedClass, instanceField, ruleFactory.getNameHelper(), instanceField.name()); return instanceField; } diff --git a/custom-generator/src/main/java/io/serverlessworkflow/generator/GeneratorUtils.java b/custom-generator/src/main/java/io/serverlessworkflow/generator/GeneratorUtils.java index ce3badc2..e7af60d5 100644 --- a/custom-generator/src/main/java/io/serverlessworkflow/generator/GeneratorUtils.java +++ b/custom-generator/src/main/java/io/serverlessworkflow/generator/GeneratorUtils.java @@ -38,6 +38,8 @@ public class GeneratorUtils { "io.serverlessworkflow.serialization.DeserializeHelper"; public static final String ONE_OF_VALUE_PROVIDER_INTERFACE_NAME = "io.serverlessworkflow.api.OneOfValueProvider"; + public static final String SETTER_ANNOTATION_NAME = + "io.serverlessworkflow.serialization.OneOfSetter"; @FunctionalInterface public interface SerializerFiller { @@ -60,13 +62,13 @@ public static JDefinedClass deserializerClass(JDefinedClass relatedClass) } public static JMethod implementInterface(JDefinedClass definedClass, JFieldVar valueField) { - JMethod method = definedClass.method(JMod.PUBLIC, Object.class, "get"); + JMethod method = definedClass.method(JMod.PUBLIC, valueField.type(), "get"); method.annotate(Override.class); method.body()._return(valueField); return method; } - public static JMethod buildMethod( + public static JMethod getterMethod( JDefinedClass definedClass, JFieldVar instanceField, NameHelper nameHelper, String name) { JMethod method = definedClass.method( diff --git a/custom-generator/src/main/java/io/serverlessworkflow/generator/UnevaluatedPropertiesRule.java b/custom-generator/src/main/java/io/serverlessworkflow/generator/UnevaluatedPropertiesRule.java index 0e937658..18bdbba6 100644 --- a/custom-generator/src/main/java/io/serverlessworkflow/generator/UnevaluatedPropertiesRule.java +++ b/custom-generator/src/main/java/io/serverlessworkflow/generator/UnevaluatedPropertiesRule.java @@ -70,7 +70,7 @@ private JDefinedClass addKeyValueFields( JType stringClass = jclass.owner()._ref(String.class); JFieldVar nameField = jclass.field(JMod.PRIVATE, stringClass, nameHelper.getPropertyName("name", null)); - JMethod nameMethod = GeneratorUtils.buildMethod(jclass, nameField, nameHelper, "name"); + JMethod nameMethod = GeneratorUtils.getterMethod(jclass, nameField, nameHelper, "name"); JType propertyType; if (node != null && node.size() != 0) { String pathToAdditionalProperties; @@ -98,7 +98,7 @@ private JDefinedClass addKeyValueFields( jclass.field( JMod.PRIVATE, propertyType, nameHelper.getPropertyName(propertyType.name(), null)); JMethod valueMethod = - GeneratorUtils.buildMethod(jclass, valueField, nameHelper, propertyType.name()); + GeneratorUtils.getterMethod(jclass, valueField, nameHelper, propertyType.name()); jclass .annotate(JsonSerialize.class) .param("using", generateSerializer(jclass, nameMethod, valueMethod));