diff --git a/.github/project.yml b/.github/project.yml index 274b48af..633f883e 100644 --- a/.github/project.yml +++ b/.github/project.yml @@ -1,3 +1,3 @@ release: - current-version: 7.0.0-alpha5 + current-version: 7.0.0-alpha5.1 next-version: 7.0.0-SNAPSHOT diff --git a/.github/workflows/maven-verify.yml b/.github/workflows/maven-verify.yml index 1b7d432c..a9f5077c 100644 --- a/.github/workflows/maven-verify.yml +++ b/.github/workflows/maven-verify.yml @@ -14,13 +14,13 @@ jobs: build: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v4 - - name: Set up JDK 11 - uses: actions/setup-java@v3 + - name: Set up JDK 17 + uses: actions/setup-java@v4 with: distribution: temurin - java-version: 11 + java-version: 17 cache: 'maven' - name: Verify with Maven diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index ef4ee698..2d002124 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -20,7 +20,7 @@ jobs: github-token: ${{secrets.GITHUB_TOKEN}} metadata-file-path: '.github/project.yml' - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Import GPG key id: import_gpg @@ -29,11 +29,11 @@ jobs: gpg_private_key: ${{ secrets.GPG_PRIVATE_KEY }} passphrase: ${{ secrets.GPG_PASSPHRASE }} - - name: Set up JDK 11 - uses: actions/setup-java@v3 + - name: Set up JDK 17 + uses: actions/setup-java@v4 with: distribution: temurin - java-version: 11 + java-version: 17 cache: 'maven' server-id: ossrh server-username: MAVEN_USERNAME @@ -57,4 +57,4 @@ jobs: MAVEN_PASSWORD: ${{ secrets.OSSRH_PASSWORD }} - name: Push tags - run: git push && git push --tags \ No newline at end of file + run: git push && git push --tags diff --git a/api/pom.xml b/api/pom.xml index 0b0a95d5..466a2754 100644 --- a/api/pom.xml +++ b/api/pom.xml @@ -4,7 +4,7 @@ io.serverlessworkflow serverlessworkflow-parent - 7.0.0-alpha5 + 7.0.0-alpha5.1 serverlessworkflow-api @@ -121,77 +121,6 @@ - - org.apache.maven.plugins - maven-checkstyle-plugin - - - - - - - - - - - - - - - - ${project.build.directory}/checkstyle.log - true - true - true - false - false - ${checkstyle.logViolationsToConsole} - ${checkstyle.failOnViolation} - - ${project.build.sourceDirectory} - ${project.build.testSourceDirectory} - - - - - compile - - check - - - - - - com.spotify.fmt - fmt-maven-plugin - - src/main/java - src/test/java - false - .*\.java - false - false - - - - - - format - - - - - - org.apache.maven.plugins - maven-jar-plugin - - - - test-jar - - - - 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/main/resources/schema/workflow.yaml b/api/src/main/resources/schema/workflow.yaml index 42456ec6..aecbeacb 100644 --- a/api/src/main/resources/schema/workflow.yaml +++ b/api/src/main/resources/schema/workflow.yaml @@ -777,7 +777,10 @@ $defs: errors: type: object title: CatchErrors - description: The configuration of a concept used to catch errors. + properties: + with: + $ref: '#/$defs/errorFilter' + description: static error filter as: type: string title: CatchAs @@ -785,11 +788,11 @@ $defs: when: type: string title: CatchWhen - description: A runtime expression used to determine whether or not to catch the filtered error. + description: A runtime expression used to determine whether to catch the filtered error. exceptWhen: type: string title: CatchExceptWhen - description: A runtime expression used to determine whether or not to catch the filtered error. + description: A runtime expression used to determine whether not to catch the filtered error. retry: oneOf: - $ref: '#/$defs/retryPolicy' @@ -1152,6 +1155,27 @@ $defs: title: ErrorDetails description: A human-readable explanation specific to this occurrence of the error. required: [ type, status ] + errorFilter: + type: object + title: ErrorFilter + description: Error filtering base on static values. For error filtering on dynamic values, use catch.when property + minProperties: 1 + properties: + type: + type: string + description: if present, means this value should be used for filtering + status: + type: integer + description: if present, means this value should be used for filtering + instance: + type: string + description: if present, means this value should be used for filtering + title: + type: string + description: if present, means this value should be used for filtering + details: + type: string + description: if present, means this value should be used for filtering uriTemplate: title: UriTemplate anyOf: 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/pom.xml b/custom-generator/pom.xml index 5cbea0a3..8444bb2a 100644 --- a/custom-generator/pom.xml +++ b/custom-generator/pom.xml @@ -3,7 +3,7 @@ io.serverlessworkflow serverlessworkflow-parent - 7.0.0-alpha5 + 7.0.0-alpha5.1 custom-generator 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..d14ba357 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); } - schema.setJavaTypeIfEmpty(javaType); + 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.setJavaType(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)); diff --git a/impl/bom/pom.xml b/impl/bom/pom.xml new file mode 100644 index 00000000..604a8300 --- /dev/null +++ b/impl/bom/pom.xml @@ -0,0 +1,20 @@ + + 4.0.0 + + io.serverlessworkflow + serverlessworkflow-impl + 7.0.0-alpha5.1 + + serverlessworkflow-impl-bom + pom + + + io.serverlessworkflow + serverlessworkflow-impl-core + + + io.serverlessworkflow + serverlessworkflow-impl-http + + + \ No newline at end of file diff --git a/impl/core/.checkstyle b/impl/core/.checkstyle new file mode 100644 index 00000000..cdd4188c --- /dev/null +++ b/impl/core/.checkstyle @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/impl/core/pom.xml b/impl/core/pom.xml new file mode 100644 index 00000000..9fac9df6 --- /dev/null +++ b/impl/core/pom.xml @@ -0,0 +1,59 @@ + + 4.0.0 + + io.serverlessworkflow + serverlessworkflow-impl + 7.0.0-alpha5.1 + + serverlessworkflow-impl-core + + 1.1.0 + 5.2.3 + + + + io.serverlessworkflow + serverlessworkflow-api + 7.0.0-alpha5.1 + + + com.github.f4b6a3 + ulid-creator + ${version.com.github.f4b6a3} + + + com.networknt + json-schema-validator + + + net.thisptr + jackson-jq + ${version.net.thisptr} + + + org.junit.jupiter + junit-jupiter-api + test + + + org.junit.jupiter + junit-jupiter-engine + test + + + org.junit.jupiter + junit-jupiter-params + test + + + org.assertj + assertj-core + test + + + ch.qos.logback + logback-classic + test + + + diff --git a/impl/core/src/main/java/io/serverlessworkflow/impl/ExecutorServiceFactory.java b/impl/core/src/main/java/io/serverlessworkflow/impl/ExecutorServiceFactory.java new file mode 100644 index 00000000..7c211149 --- /dev/null +++ b/impl/core/src/main/java/io/serverlessworkflow/impl/ExecutorServiceFactory.java @@ -0,0 +1,22 @@ +/* + * 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.impl; + +import java.util.concurrent.ExecutorService; +import java.util.function.Supplier; + +@FunctionalInterface +public interface ExecutorServiceFactory extends Supplier {} diff --git a/impl/core/src/main/java/io/serverlessworkflow/impl/LongFilter.java b/impl/core/src/main/java/io/serverlessworkflow/impl/LongFilter.java new file mode 100644 index 00000000..91b1b6c5 --- /dev/null +++ b/impl/core/src/main/java/io/serverlessworkflow/impl/LongFilter.java @@ -0,0 +1,21 @@ +/* + * 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.impl; + +import java.util.function.BiFunction; + +@FunctionalInterface +public interface LongFilter extends BiFunction, Long> {} diff --git a/impl/core/src/main/java/io/serverlessworkflow/impl/QueueWorkflowPosition.java b/impl/core/src/main/java/io/serverlessworkflow/impl/QueueWorkflowPosition.java new file mode 100644 index 00000000..5ad4934f --- /dev/null +++ b/impl/core/src/main/java/io/serverlessworkflow/impl/QueueWorkflowPosition.java @@ -0,0 +1,70 @@ +/* + * 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.impl; + +import java.util.ArrayDeque; +import java.util.Deque; +import java.util.stream.Collectors; + +public class QueueWorkflowPosition implements WorkflowPosition { + + private Deque queue; + + QueueWorkflowPosition() { + this(new ArrayDeque<>()); + } + + private QueueWorkflowPosition(Deque list) { + this.queue = list; + } + + public QueueWorkflowPosition copy() { + return new QueueWorkflowPosition(new ArrayDeque<>(this.queue)); + } + + @Override + public WorkflowPosition addIndex(int index) { + queue.add(index); + return this; + } + + @Override + public WorkflowPosition addProperty(String prop) { + queue.add(prop); + return this; + } + + @Override + public String jsonPointer() { + return queue.stream().map(Object::toString).collect(Collectors.joining("/")); + } + + @Override + public String toString() { + return "QueueWorkflowPosition [queue=" + queue + "]"; + } + + @Override + public WorkflowPosition back() { + queue.removeLast(); + return this; + } + + @Override + public Object last() { + return queue.getLast(); + } +} diff --git a/impl/core/src/main/java/io/serverlessworkflow/impl/RuntimeDescriptorFactory.java b/impl/core/src/main/java/io/serverlessworkflow/impl/RuntimeDescriptorFactory.java new file mode 100644 index 00000000..2d0601fb --- /dev/null +++ b/impl/core/src/main/java/io/serverlessworkflow/impl/RuntimeDescriptorFactory.java @@ -0,0 +1,22 @@ +/* + * 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.impl; + +import io.serverlessworkflow.impl.expressions.RuntimeDescriptor; +import java.util.function.Supplier; + +@FunctionalInterface +public interface RuntimeDescriptorFactory extends Supplier {} diff --git a/impl/core/src/main/java/io/serverlessworkflow/impl/StringBufferWorkflowPosition.java b/impl/core/src/main/java/io/serverlessworkflow/impl/StringBufferWorkflowPosition.java new file mode 100644 index 00000000..18aaf8e4 --- /dev/null +++ b/impl/core/src/main/java/io/serverlessworkflow/impl/StringBufferWorkflowPosition.java @@ -0,0 +1,70 @@ +/* + * 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.impl; + +public class StringBufferWorkflowPosition implements WorkflowPosition { + + private StringBuilder sb; + + StringBufferWorkflowPosition() { + this(""); + } + + private StringBufferWorkflowPosition(String str) { + this.sb = new StringBuilder(str); + } + + public StringBufferWorkflowPosition copy() { + return new StringBufferWorkflowPosition(this.jsonPointer()); + } + + @Override + public WorkflowPosition addIndex(int index) { + sb.append('/').append(index); + return this; + } + + @Override + public WorkflowPosition addProperty(String prop) { + sb.append('/').append(prop); + return this; + } + + @Override + public String jsonPointer() { + return sb.toString(); + } + + @Override + public String toString() { + return "StringBufferWorkflowPosition [sb=" + sb + "]"; + } + + @Override + public WorkflowPosition back() { + int indexOf = sb.lastIndexOf("/"); + if (indexOf != -1) { + sb.substring(0, indexOf); + } + return this; + } + + @Override + public Object last() { + int indexOf = sb.lastIndexOf("/"); + return indexOf != -1 ? jsonPointer().substring(indexOf + 1) : ""; + } +} diff --git a/impl/core/src/main/java/io/serverlessworkflow/impl/StringFilter.java b/impl/core/src/main/java/io/serverlessworkflow/impl/StringFilter.java new file mode 100644 index 00000000..5d0a648e --- /dev/null +++ b/impl/core/src/main/java/io/serverlessworkflow/impl/StringFilter.java @@ -0,0 +1,21 @@ +/* + * 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.impl; + +import java.util.function.BiFunction; + +@FunctionalInterface +public interface StringFilter extends BiFunction, String> {} diff --git a/impl/core/src/main/java/io/serverlessworkflow/impl/TaskContext.java b/impl/core/src/main/java/io/serverlessworkflow/impl/TaskContext.java new file mode 100644 index 00000000..dde5a315 --- /dev/null +++ b/impl/core/src/main/java/io/serverlessworkflow/impl/TaskContext.java @@ -0,0 +1,155 @@ +/* + * 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.impl; + +import com.fasterxml.jackson.databind.JsonNode; +import io.serverlessworkflow.api.types.FlowDirective; +import io.serverlessworkflow.api.types.FlowDirectiveEnum; +import io.serverlessworkflow.api.types.TaskBase; +import java.time.Instant; +import java.util.HashMap; +import java.util.Map; + +public class TaskContext { + + private final JsonNode rawInput; + private final T task; + private final WorkflowPosition position; + private final Instant startedAt; + + private JsonNode input; + private JsonNode output; + private JsonNode rawOutput; + private FlowDirective flowDirective; + private Map contextVariables; + private Instant completedAt; + + public TaskContext(JsonNode input, WorkflowPosition position) { + this(input, null, position, Instant.now(), input, input, input, null, new HashMap<>()); + } + + public TaskContext(JsonNode input, TaskContext taskContext, T task) { + this( + input, + task, + taskContext.position, + Instant.now(), + input, + input, + input, + task.getThen(), + new HashMap<>(taskContext.variables())); + } + + private TaskContext( + JsonNode rawInput, + T task, + WorkflowPosition position, + Instant startedAt, + JsonNode input, + JsonNode output, + JsonNode rawOutput, + FlowDirective flowDirective, + Map contextVariables) { + this.rawInput = rawInput; + this.task = task; + this.position = position; + this.startedAt = startedAt; + this.input = input; + this.output = output; + this.rawOutput = rawOutput; + this.flowDirective = flowDirective; + this.contextVariables = contextVariables; + } + + public TaskContext copy() { + return new TaskContext( + rawInput, + task, + position.copy(), + startedAt, + input, + output, + rawOutput, + flowDirective, + new HashMap<>(contextVariables)); + } + + public void input(JsonNode input) { + this.input = input; + this.rawOutput = input; + this.output = input; + } + + public JsonNode input() { + return input; + } + + public JsonNode rawInput() { + return rawInput; + } + + public T task() { + return task; + } + + public void rawOutput(JsonNode output) { + this.rawOutput = output; + this.output = output; + } + + public JsonNode rawOutput() { + return rawOutput; + } + + public void output(JsonNode output) { + this.output = output; + } + + public JsonNode output() { + return output; + } + + public void flowDirective(FlowDirective flowDirective) { + this.flowDirective = flowDirective; + } + + public FlowDirective flowDirective() { + return flowDirective == null + ? new FlowDirective().withFlowDirectiveEnum(FlowDirectiveEnum.CONTINUE) + : flowDirective; + } + + public Map variables() { + return contextVariables; + } + + public WorkflowPosition position() { + return position; + } + + public Instant startedAt() { + return startedAt; + } + + public void completedAt(Instant instant) { + this.completedAt = instant; + } + + public Instant completedAt() { + return completedAt; + } +} diff --git a/impl/core/src/main/java/io/serverlessworkflow/impl/WorkflowApplication.java b/impl/core/src/main/java/io/serverlessworkflow/impl/WorkflowApplication.java new file mode 100644 index 00000000..f36c23f6 --- /dev/null +++ b/impl/core/src/main/java/io/serverlessworkflow/impl/WorkflowApplication.java @@ -0,0 +1,216 @@ +/* + * 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.impl; + +import com.github.f4b6a3.ulid.UlidCreator; +import io.serverlessworkflow.api.types.Document; +import io.serverlessworkflow.api.types.Workflow; +import io.serverlessworkflow.impl.executors.DefaultTaskExecutorFactory; +import io.serverlessworkflow.impl.executors.TaskExecutorFactory; +import io.serverlessworkflow.impl.expressions.ExpressionFactory; +import io.serverlessworkflow.impl.expressions.JQExpressionFactory; +import io.serverlessworkflow.impl.expressions.RuntimeDescriptor; +import io.serverlessworkflow.impl.jsonschema.DefaultSchemaValidatorFactory; +import io.serverlessworkflow.impl.jsonschema.SchemaValidatorFactory; +import io.serverlessworkflow.impl.resources.DefaultResourceLoaderFactory; +import io.serverlessworkflow.impl.resources.ResourceLoaderFactory; +import java.util.Collection; +import java.util.Collections; +import java.util.HashSet; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; + +public class WorkflowApplication implements AutoCloseable { + + private final TaskExecutorFactory taskFactory; + private final ExpressionFactory exprFactory; + private final ResourceLoaderFactory resourceLoaderFactory; + private final SchemaValidatorFactory schemaValidatorFactory; + private final WorkflowIdFactory idFactory; + private final Collection listeners; + private final Map definitions; + private final WorkflowPositionFactory positionFactory; + private final ExecutorServiceFactory executorFactory; + private final RuntimeDescriptorFactory runtimeDescriptorFactory; + + private ExecutorService executorService; + + public WorkflowApplication( + TaskExecutorFactory taskFactory, + ExpressionFactory exprFactory, + ResourceLoaderFactory resourceLoaderFactory, + SchemaValidatorFactory schemaValidatorFactory, + WorkflowPositionFactory positionFactory, + WorkflowIdFactory idFactory, + RuntimeDescriptorFactory runtimeDescriptorFactory, + ExecutorServiceFactory executorFactory, + Collection listeners) { + this.taskFactory = taskFactory; + this.exprFactory = exprFactory; + this.resourceLoaderFactory = resourceLoaderFactory; + this.schemaValidatorFactory = schemaValidatorFactory; + this.positionFactory = positionFactory; + this.idFactory = idFactory; + this.runtimeDescriptorFactory = runtimeDescriptorFactory; + this.executorFactory = executorFactory; + this.listeners = listeners; + this.definitions = new ConcurrentHashMap<>(); + } + + public TaskExecutorFactory taskFactory() { + return taskFactory; + } + + public static Builder builder() { + return new Builder(); + } + + public ExpressionFactory expressionFactory() { + return exprFactory; + } + + public SchemaValidatorFactory validatorFactory() { + return schemaValidatorFactory; + } + + public ResourceLoaderFactory resourceLoaderFactory() { + return resourceLoaderFactory; + } + + public Collection listeners() { + return listeners; + } + + public WorkflowIdFactory idFactory() { + return idFactory; + } + + public static class Builder { + private TaskExecutorFactory taskFactory = DefaultTaskExecutorFactory.get(); + private ExpressionFactory exprFactory = JQExpressionFactory.get(); + private Collection listeners; + private ResourceLoaderFactory resourceLoaderFactory = DefaultResourceLoaderFactory.get(); + private SchemaValidatorFactory schemaValidatorFactory = DefaultSchemaValidatorFactory.get(); + private WorkflowPositionFactory positionFactory = () -> new QueueWorkflowPosition(); + private WorkflowIdFactory idFactory = () -> UlidCreator.getMonotonicUlid().toString(); + private ExecutorServiceFactory executorFactory = () -> Executors.newCachedThreadPool(); + private RuntimeDescriptorFactory descriptorFactory = + () -> new RuntimeDescriptor("reference impl", "1.0.0_alpha", Collections.emptyMap()); + + private Builder() {} + + public Builder withListener(WorkflowExecutionListener listener) { + if (listeners == null) { + listeners = new HashSet<>(); + } + listeners.add(listener); + return this; + } + + public Builder withTaskExecutorFactory(TaskExecutorFactory factory) { + this.taskFactory = factory; + return this; + } + + public Builder withExpressionFactory(ExpressionFactory factory) { + this.exprFactory = factory; + return this; + } + + public Builder withResourceLoaderFactory(ResourceLoaderFactory resourceLoader) { + this.resourceLoaderFactory = resourceLoader; + return this; + } + + public Builder withExecutorFactory(ExecutorServiceFactory executorFactory) { + this.executorFactory = executorFactory; + return this; + } + + public Builder withPositionFactory(WorkflowPositionFactory positionFactory) { + this.positionFactory = positionFactory; + return this; + } + + public Builder withSchemaValidatorFactory(SchemaValidatorFactory factory) { + this.schemaValidatorFactory = factory; + return this; + } + + public Builder withIdFactory(WorkflowIdFactory factory) { + this.idFactory = factory; + return this; + } + + public Builder withDescriptorFactory(RuntimeDescriptorFactory factory) { + this.descriptorFactory = factory; + return this; + } + + public WorkflowApplication build() { + return new WorkflowApplication( + taskFactory, + exprFactory, + resourceLoaderFactory, + schemaValidatorFactory, + positionFactory, + idFactory, + descriptorFactory, + executorFactory, + listeners == null + ? Collections.emptySet() + : Collections.unmodifiableCollection(listeners)); + } + } + + private static record WorkflowId(String namespace, String name, String version) { + static WorkflowId of(Document document) { + return new WorkflowId(document.getNamespace(), document.getName(), document.getVersion()); + } + } + + public WorkflowDefinition workflowDefinition(Workflow workflow) { + return definitions.computeIfAbsent( + WorkflowId.of(workflow.getDocument()), k -> WorkflowDefinition.of(this, workflow)); + } + + @Override + public void close() throws Exception { + for (WorkflowDefinition definition : definitions.values()) { + definition.close(); + } + definitions.clear(); + } + + public WorkflowPositionFactory positionFactory() { + return positionFactory; + } + + public RuntimeDescriptorFactory runtimeDescriptorFactory() { + return runtimeDescriptorFactory; + } + + public ExecutorService executorService() { + synchronized (executorFactory) { + if (executorService == null) { + executorService = executorFactory.get(); + } + } + return executorService; + } +} diff --git a/impl/src/main/java/io/serverlessworkflow/impl/ExpressionUtils.java b/impl/core/src/main/java/io/serverlessworkflow/impl/WorkflowContext.java similarity index 53% rename from impl/src/main/java/io/serverlessworkflow/impl/ExpressionUtils.java rename to impl/core/src/main/java/io/serverlessworkflow/impl/WorkflowContext.java index 45000931..f45f1b84 100644 --- a/impl/src/main/java/io/serverlessworkflow/impl/ExpressionUtils.java +++ b/impl/core/src/main/java/io/serverlessworkflow/impl/WorkflowContext.java @@ -15,26 +15,30 @@ */ package io.serverlessworkflow.impl; -public class ExpressionUtils { +import com.fasterxml.jackson.databind.JsonNode; - private static final String EXPR_PREFIX = "${"; - private static final String EXPR_SUFFIX = "}"; +public class WorkflowContext { + private final WorkflowDefinition definition; + private final WorkflowInstance instance; - private ExpressionUtils() {} + WorkflowContext(WorkflowDefinition definition, WorkflowInstance instance) { + this.definition = definition; + this.instance = instance; + } + + public WorkflowInstance instance() { + return instance; + } + + public JsonNode context() { + return instance.context(); + } - public static String trimExpr(String expr) { - expr = expr.trim(); - if (expr.startsWith(EXPR_PREFIX)) { - expr = trimExpr(expr, EXPR_PREFIX, EXPR_SUFFIX); - } - return expr.trim(); + public void context(JsonNode context) { + this.instance.context(context); } - private static String trimExpr(String expr, String prefix, String suffix) { - expr = expr.substring(prefix.length()); - if (expr.endsWith(suffix)) { - expr = expr.substring(0, expr.length() - suffix.length()); - } - return expr; + public WorkflowDefinition definition() { + return definition; } } diff --git a/impl/core/src/main/java/io/serverlessworkflow/impl/WorkflowDefinition.java b/impl/core/src/main/java/io/serverlessworkflow/impl/WorkflowDefinition.java new file mode 100644 index 00000000..df5b70e1 --- /dev/null +++ b/impl/core/src/main/java/io/serverlessworkflow/impl/WorkflowDefinition.java @@ -0,0 +1,147 @@ +/* + * 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.impl; + +import static io.serverlessworkflow.impl.WorkflowUtils.*; + +import io.serverlessworkflow.api.types.Input; +import io.serverlessworkflow.api.types.Output; +import io.serverlessworkflow.api.types.TaskBase; +import io.serverlessworkflow.api.types.Workflow; +import io.serverlessworkflow.impl.executors.TaskExecutor; +import io.serverlessworkflow.impl.executors.TaskExecutorFactory; +import io.serverlessworkflow.impl.expressions.ExpressionFactory; +import io.serverlessworkflow.impl.json.JsonUtils; +import io.serverlessworkflow.impl.jsonschema.SchemaValidator; +import io.serverlessworkflow.impl.jsonschema.SchemaValidatorFactory; +import io.serverlessworkflow.impl.resources.ResourceLoader; +import java.nio.file.Path; +import java.util.Collection; +import java.util.Map; +import java.util.Optional; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ExecutorService; + +public class WorkflowDefinition implements AutoCloseable { + + private final Workflow workflow; + private Optional inputSchemaValidator = Optional.empty(); + private Optional outputSchemaValidator = Optional.empty(); + private Optional inputFilter = Optional.empty(); + private Optional outputFilter = Optional.empty(); + private final Map> taskExecutors = + new ConcurrentHashMap<>(); + private final ResourceLoader resourceLoader; + private final WorkflowApplication application; + + private WorkflowDefinition( + WorkflowApplication application, Workflow workflow, ResourceLoader resourceLoader) { + this.workflow = workflow; + this.application = application; + this.resourceLoader = resourceLoader; + if (workflow.getInput() != null) { + Input input = workflow.getInput(); + this.inputSchemaValidator = + getSchemaValidator(application.validatorFactory(), resourceLoader, input.getSchema()); + this.inputFilter = buildWorkflowFilter(application.expressionFactory(), input.getFrom()); + } + if (workflow.getOutput() != null) { + Output output = workflow.getOutput(); + this.outputSchemaValidator = + getSchemaValidator(application.validatorFactory(), resourceLoader, output.getSchema()); + this.outputFilter = buildWorkflowFilter(application.expressionFactory(), output.getAs()); + } + } + + static WorkflowDefinition of(WorkflowApplication application, Workflow workflow) { + return of(application, workflow, null); + } + + static WorkflowDefinition of(WorkflowApplication application, Workflow workflow, Path path) { + return new WorkflowDefinition( + application, workflow, application.resourceLoaderFactory().getResourceLoader(path)); + } + + public WorkflowInstance execute(Object input) { + return new WorkflowInstance(this, JsonUtils.fromValue(input)); + } + + public Optional inputSchemaValidator() { + return inputSchemaValidator; + } + + public Optional inputFilter() { + return inputFilter; + } + + public Workflow workflow() { + return workflow; + } + + public Collection listeners() { + return application.listeners(); + } + + public Map> taskExecutors() { + return taskExecutors; + } + + public TaskExecutorFactory taskFactory() { + return application.taskFactory(); + } + + public Optional outputFilter() { + return outputFilter; + } + + public WorkflowIdFactory idFactory() { + return application.idFactory(); + } + + public Optional outputSchemaValidator() { + return outputSchemaValidator; + } + + public ExpressionFactory expressionFactory() { + return application.expressionFactory(); + } + + public SchemaValidatorFactory validatorFactory() { + return application.validatorFactory(); + } + + public ResourceLoader resourceLoader() { + + return resourceLoader; + } + + public WorkflowPositionFactory positionFactory() { + return application.positionFactory(); + } + + public ExecutorService executorService() { + return application.executorService(); + } + + public RuntimeDescriptorFactory runtimeDescriptorFactory() { + return application.runtimeDescriptorFactory(); + } + + @Override + public void close() { + // TODO close resourcers hold for uncompleted process instances, if any + } +} diff --git a/impl/core/src/main/java/io/serverlessworkflow/impl/WorkflowError.java b/impl/core/src/main/java/io/serverlessworkflow/impl/WorkflowError.java new file mode 100644 index 00000000..1823be94 --- /dev/null +++ b/impl/core/src/main/java/io/serverlessworkflow/impl/WorkflowError.java @@ -0,0 +1,73 @@ +/* + * 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.impl; + +public record WorkflowError( + String type, int status, String instance, String title, String details) { + + private static final String ERROR_FORMAT = "https://serverlessworkflow.io/spec/1.0.0/errors/%s"; + public static final String RUNTIME_TYPE = String.format(ERROR_FORMAT, "runtime"); + public static final String COMM_TYPE = String.format(ERROR_FORMAT, "communication"); + + public static Builder error(String type, int status) { + return new Builder(type, status); + } + + public static Builder communication(int status, TaskContext context, Exception ex) { + return new Builder(COMM_TYPE, status) + .instance(context.position().jsonPointer()) + .title(ex.getMessage()); + } + + public static Builder runtime(int status, TaskContext context, Exception ex) { + return new Builder(RUNTIME_TYPE, status) + .instance(context.position().jsonPointer()) + .title(ex.getMessage()); + } + + public static class Builder { + + private final String type; + private int status; + private String instance; + private String title; + private String details; + + private Builder(String type, int status) { + this.type = type; + this.status = status; + } + + public Builder instance(String instance) { + this.instance = instance; + return this; + } + + public Builder title(String title) { + this.title = title; + return this; + } + + public Builder details(String details) { + this.details = details; + return this; + } + + public WorkflowError build() { + return new WorkflowError(type, status, instance, title, details); + } + } +} diff --git a/impl/core/src/main/java/io/serverlessworkflow/impl/WorkflowException.java b/impl/core/src/main/java/io/serverlessworkflow/impl/WorkflowException.java new file mode 100644 index 00000000..685fc077 --- /dev/null +++ b/impl/core/src/main/java/io/serverlessworkflow/impl/WorkflowException.java @@ -0,0 +1,36 @@ +/* + * 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.impl; + +public class WorkflowException extends RuntimeException { + + private static final long serialVersionUID = 1L; + + private final WorkflowError worflowError; + + public WorkflowException(WorkflowError error) { + this(error, null); + } + + public WorkflowException(WorkflowError error, Throwable cause) { + super(error.toString(), cause); + this.worflowError = error; + } + + public WorkflowError getWorflowError() { + return worflowError; + } +} diff --git a/impl/src/main/java/io/serverlessworkflow/impl/WorkflowExecutionListener.java b/impl/core/src/main/java/io/serverlessworkflow/impl/WorkflowExecutionListener.java similarity index 77% rename from impl/src/main/java/io/serverlessworkflow/impl/WorkflowExecutionListener.java rename to impl/core/src/main/java/io/serverlessworkflow/impl/WorkflowExecutionListener.java index 700c6aa9..c121bb41 100644 --- a/impl/src/main/java/io/serverlessworkflow/impl/WorkflowExecutionListener.java +++ b/impl/core/src/main/java/io/serverlessworkflow/impl/WorkflowExecutionListener.java @@ -15,12 +15,11 @@ */ package io.serverlessworkflow.impl; -import com.fasterxml.jackson.core.JsonPointer; -import io.serverlessworkflow.api.types.Task; +import io.serverlessworkflow.api.types.TaskBase; public interface WorkflowExecutionListener { - void onTaskStarted(JsonPointer currentPos, Task task); + void onTaskStarted(WorkflowPosition currentPos, TaskBase task); - void onTaskEnded(JsonPointer currentPos, Task task); + void onTaskEnded(WorkflowPosition currentPos, TaskBase task); } diff --git a/impl/src/main/java/io/serverlessworkflow/impl/TaskExecutor.java b/impl/core/src/main/java/io/serverlessworkflow/impl/WorkflowFilter.java similarity index 80% rename from impl/src/main/java/io/serverlessworkflow/impl/TaskExecutor.java rename to impl/core/src/main/java/io/serverlessworkflow/impl/WorkflowFilter.java index 83c4bd18..7d25df48 100644 --- a/impl/src/main/java/io/serverlessworkflow/impl/TaskExecutor.java +++ b/impl/core/src/main/java/io/serverlessworkflow/impl/WorkflowFilter.java @@ -16,7 +16,8 @@ package io.serverlessworkflow.impl; import com.fasterxml.jackson.databind.JsonNode; -import io.serverlessworkflow.api.types.TaskBase; -import java.util.function.UnaryOperator; -public interface TaskExecutor extends UnaryOperator {} +@FunctionalInterface +public interface WorkflowFilter { + JsonNode apply(WorkflowContext workflow, TaskContext task, JsonNode node); +} diff --git a/impl/core/src/main/java/io/serverlessworkflow/impl/WorkflowIdFactory.java b/impl/core/src/main/java/io/serverlessworkflow/impl/WorkflowIdFactory.java new file mode 100644 index 00000000..12b0f7c6 --- /dev/null +++ b/impl/core/src/main/java/io/serverlessworkflow/impl/WorkflowIdFactory.java @@ -0,0 +1,21 @@ +/* + * 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.impl; + +import java.util.function.Supplier; + +@FunctionalInterface +public interface WorkflowIdFactory extends Supplier {} diff --git a/impl/core/src/main/java/io/serverlessworkflow/impl/WorkflowInstance.java b/impl/core/src/main/java/io/serverlessworkflow/impl/WorkflowInstance.java new file mode 100644 index 00000000..f81a6f24 --- /dev/null +++ b/impl/core/src/main/java/io/serverlessworkflow/impl/WorkflowInstance.java @@ -0,0 +1,91 @@ +/* + * 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.impl; + +import static io.serverlessworkflow.impl.json.JsonUtils.toJavaValue; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.node.NullNode; +import io.serverlessworkflow.impl.executors.TaskExecutorHelper; +import java.time.Instant; +import java.util.concurrent.atomic.AtomicReference; + +public class WorkflowInstance { + private final AtomicReference status; + private final TaskContext taskContext; + private final String id; + private final JsonNode input; + private final Instant startedAt; + private final AtomicReference context; + + WorkflowInstance(WorkflowDefinition definition, JsonNode input) { + this.id = definition.idFactory().get(); + this.input = input; + definition.inputSchemaValidator().ifPresent(v -> v.validate(input)); + this.startedAt = Instant.now(); + WorkflowContext workflowContext = new WorkflowContext(definition, this); + taskContext = new TaskContext<>(input, definition.positionFactory().get()); + definition + .inputFilter() + .ifPresent(f -> taskContext.input(f.apply(workflowContext, taskContext, input))); + status = new AtomicReference<>(WorkflowStatus.RUNNING); + context = new AtomicReference<>(NullNode.getInstance()); + TaskExecutorHelper.processTaskList(definition.workflow().getDo(), workflowContext, taskContext); + definition + .outputFilter() + .ifPresent( + f -> + taskContext.output(f.apply(workflowContext, taskContext, taskContext.rawOutput()))); + definition.outputSchemaValidator().ifPresent(v -> v.validate(taskContext.output())); + status.compareAndSet(WorkflowStatus.RUNNING, WorkflowStatus.COMPLETED); + } + + public String id() { + return id; + } + + public Instant startedAt() { + return startedAt; + } + + public JsonNode input() { + return input; + } + + public JsonNode context() { + return context.get(); + } + + public WorkflowStatus status() { + return status.get(); + } + + public void status(WorkflowStatus state) { + this.status.set(state); + } + + public Object output() { + return toJavaValue(taskContext.output()); + } + + public JsonNode outputAsJsonNode() { + return taskContext.output(); + } + + void context(JsonNode context) { + this.context.set(context); + } +} diff --git a/impl/core/src/main/java/io/serverlessworkflow/impl/WorkflowPosition.java b/impl/core/src/main/java/io/serverlessworkflow/impl/WorkflowPosition.java new file mode 100644 index 00000000..1c416100 --- /dev/null +++ b/impl/core/src/main/java/io/serverlessworkflow/impl/WorkflowPosition.java @@ -0,0 +1,31 @@ +/* + * 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.impl; + +public interface WorkflowPosition { + + String jsonPointer(); + + WorkflowPosition addProperty(String prop); + + WorkflowPosition addIndex(int index); + + WorkflowPosition back(); + + WorkflowPosition copy(); + + Object last(); +} diff --git a/impl/core/src/main/java/io/serverlessworkflow/impl/WorkflowPositionFactory.java b/impl/core/src/main/java/io/serverlessworkflow/impl/WorkflowPositionFactory.java new file mode 100644 index 00000000..60fa5d6a --- /dev/null +++ b/impl/core/src/main/java/io/serverlessworkflow/impl/WorkflowPositionFactory.java @@ -0,0 +1,21 @@ +/* + * 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.impl; + +import java.util.function.Supplier; + +@FunctionalInterface +public interface WorkflowPositionFactory extends Supplier {} diff --git a/impl/core/src/main/java/io/serverlessworkflow/impl/WorkflowStatus.java b/impl/core/src/main/java/io/serverlessworkflow/impl/WorkflowStatus.java new file mode 100644 index 00000000..bc657839 --- /dev/null +++ b/impl/core/src/main/java/io/serverlessworkflow/impl/WorkflowStatus.java @@ -0,0 +1,25 @@ +/* + * 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.impl; + +public enum WorkflowStatus { + PENDING, + RUNNING, + WAITING, + COMPLETED, + FAULTED, + CANCELLED +} diff --git a/impl/core/src/main/java/io/serverlessworkflow/impl/WorkflowUtils.java b/impl/core/src/main/java/io/serverlessworkflow/impl/WorkflowUtils.java new file mode 100644 index 00000000..0866ba05 --- /dev/null +++ b/impl/core/src/main/java/io/serverlessworkflow/impl/WorkflowUtils.java @@ -0,0 +1,151 @@ +/* + * 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.impl; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import io.serverlessworkflow.api.WorkflowFormat; +import io.serverlessworkflow.api.types.ExportAs; +import io.serverlessworkflow.api.types.InputFrom; +import io.serverlessworkflow.api.types.OutputAs; +import io.serverlessworkflow.api.types.SchemaExternal; +import io.serverlessworkflow.api.types.SchemaInline; +import io.serverlessworkflow.api.types.SchemaUnion; +import io.serverlessworkflow.impl.expressions.Expression; +import io.serverlessworkflow.impl.expressions.ExpressionFactory; +import io.serverlessworkflow.impl.expressions.ExpressionUtils; +import io.serverlessworkflow.impl.json.JsonUtils; +import io.serverlessworkflow.impl.jsonschema.SchemaValidator; +import io.serverlessworkflow.impl.jsonschema.SchemaValidatorFactory; +import io.serverlessworkflow.impl.resources.ResourceLoader; +import io.serverlessworkflow.impl.resources.StaticResource; +import java.io.IOException; +import java.io.InputStream; +import java.io.UncheckedIOException; +import java.util.Map; +import java.util.Optional; + +public class WorkflowUtils { + + private WorkflowUtils() {} + + public static Optional getSchemaValidator( + SchemaValidatorFactory validatorFactory, ResourceLoader resourceLoader, SchemaUnion schema) { + return schemaToNode(resourceLoader, schema).map(n -> validatorFactory.getValidator(n)); + } + + private static Optional schemaToNode( + ResourceLoader resourceLoader, SchemaUnion schema) { + if (schema != null) { + if (schema.getSchemaInline() != null) { + SchemaInline inline = schema.getSchemaInline(); + return Optional.of(JsonUtils.mapper().convertValue(inline.getDocument(), JsonNode.class)); + } else if (schema.getSchemaExternal() != null) { + SchemaExternal external = schema.getSchemaExternal(); + StaticResource resource = resourceLoader.loadStatic(external.getResource()); + ObjectMapper mapper = WorkflowFormat.fromFileName(resource.name()).mapper(); + try (InputStream in = resource.open()) { + return Optional.of(mapper.readTree(in)); + } catch (IOException io) { + throw new UncheckedIOException(io); + } + } + } + return Optional.empty(); + } + + public static Optional buildWorkflowFilter( + ExpressionFactory exprFactory, InputFrom from) { + return from != null + ? Optional.of(buildWorkflowFilter(exprFactory, from.getString(), from.getObject())) + : Optional.empty(); + } + + public static Optional buildWorkflowFilter( + ExpressionFactory exprFactory, OutputAs as) { + return as != null + ? Optional.of(buildWorkflowFilter(exprFactory, as.getString(), as.getObject())) + : Optional.empty(); + } + + public static Optional buildWorkflowFilter( + ExpressionFactory exprFactory, ExportAs as) { + return as != null + ? Optional.of(buildWorkflowFilter(exprFactory, as.getString(), as.getObject())) + : Optional.empty(); + } + + public static StringFilter buildStringFilter( + ExpressionFactory exprFactory, String expression, String literal) { + return expression != null + ? toString(buildWorkflowFilter(exprFactory, expression)) + : toString(literal); + } + + public static StringFilter buildStringFilter(ExpressionFactory exprFactory, String str) { + return ExpressionUtils.isExpr(str) + ? toString(buildWorkflowFilter(exprFactory, str)) + : toString(str); + } + + private static StringFilter toString(WorkflowFilter filter) { + return (w, t) -> filter.apply(w, t, t.input()).asText(); + } + + private static StringFilter toString(String literal) { + return (w, t) -> literal; + } + + private static WorkflowFilter buildWorkflowFilter( + ExpressionFactory exprFactory, String str, Object object) { + if (str != null) { + return buildWorkflowFilter(exprFactory, str); + } else if (object != null) { + Object exprObj = ExpressionUtils.buildExpressionObject(object, exprFactory); + return exprObj instanceof Map + ? (w, t, n) -> + JsonUtils.fromValue( + ExpressionUtils.evaluateExpressionMap((Map) exprObj, w, t, n)) + : (w, t, n) -> JsonUtils.fromValue(object); + } + throw new IllegalStateException("Both object and str are null"); + } + + public static LongFilter buildLongFilter( + ExpressionFactory exprFactory, String expression, Long literal) { + return expression != null + ? toLong(buildWorkflowFilter(exprFactory, expression)) + : toLong(literal); + } + + private static LongFilter toLong(WorkflowFilter filter) { + return (w, t) -> filter.apply(w, t, t.input()).asLong(); + } + + private static LongFilter toLong(Long literal) { + return (w, t) -> literal; + } + + public static WorkflowFilter buildWorkflowFilter(ExpressionFactory exprFactory, String str) { + assert str != null; + Expression expression = exprFactory.getExpression(str); + return expression::eval; + } + + public static Optional optionalFilter(ExpressionFactory exprFactory, String str) { + return str != null ? Optional.of(buildWorkflowFilter(exprFactory, str)) : Optional.empty(); + } +} diff --git a/impl/core/src/main/java/io/serverlessworkflow/impl/executors/AbstractTaskExecutor.java b/impl/core/src/main/java/io/serverlessworkflow/impl/executors/AbstractTaskExecutor.java new file mode 100644 index 00000000..f5ee1136 --- /dev/null +++ b/impl/core/src/main/java/io/serverlessworkflow/impl/executors/AbstractTaskExecutor.java @@ -0,0 +1,116 @@ +/* + * 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.impl.executors; + +import static io.serverlessworkflow.impl.WorkflowUtils.*; + +import com.fasterxml.jackson.databind.JsonNode; +import io.serverlessworkflow.api.types.Export; +import io.serverlessworkflow.api.types.Input; +import io.serverlessworkflow.api.types.Output; +import io.serverlessworkflow.api.types.TaskBase; +import io.serverlessworkflow.impl.TaskContext; +import io.serverlessworkflow.impl.WorkflowContext; +import io.serverlessworkflow.impl.WorkflowDefinition; +import io.serverlessworkflow.impl.WorkflowFilter; +import io.serverlessworkflow.impl.jsonschema.SchemaValidator; +import java.time.Instant; +import java.util.Optional; + +public abstract class AbstractTaskExecutor implements TaskExecutor { + + protected final T task; + + private Optional inputProcessor = Optional.empty(); + private Optional outputProcessor = Optional.empty(); + private Optional contextProcessor = Optional.empty(); + private Optional inputSchemaValidator = Optional.empty(); + private Optional outputSchemaValidator = Optional.empty(); + private Optional contextSchemaValidator = Optional.empty(); + + protected AbstractTaskExecutor(T task, WorkflowDefinition definition) { + this.task = task; + buildInputProcessors(definition); + buildOutputProcessors(definition); + buildContextProcessors(definition); + } + + private void buildInputProcessors(WorkflowDefinition definition) { + if (task.getInput() != null) { + Input input = task.getInput(); + this.inputProcessor = buildWorkflowFilter(definition.expressionFactory(), input.getFrom()); + this.inputSchemaValidator = + getSchemaValidator( + definition.validatorFactory(), definition.resourceLoader(), input.getSchema()); + } + } + + private void buildOutputProcessors(WorkflowDefinition definition) { + if (task.getOutput() != null) { + Output output = task.getOutput(); + this.outputProcessor = buildWorkflowFilter(definition.expressionFactory(), output.getAs()); + this.outputSchemaValidator = + getSchemaValidator( + definition.validatorFactory(), definition.resourceLoader(), output.getSchema()); + } + } + + private void buildContextProcessors(WorkflowDefinition definition) { + if (task.getExport() != null) { + Export export = task.getExport(); + if (export.getAs() != null) { + this.contextProcessor = buildWorkflowFilter(definition.expressionFactory(), export.getAs()); + } + this.contextSchemaValidator = + getSchemaValidator( + definition.validatorFactory(), definition.resourceLoader(), export.getSchema()); + } + } + + @Override + public TaskContext apply( + WorkflowContext workflowContext, TaskContext parentContext, JsonNode input) { + TaskContext taskContext = new TaskContext<>(input, parentContext, task); + if (TaskExecutorHelper.isActive(workflowContext)) { + + workflowContext + .definition() + .listeners() + .forEach(l -> l.onTaskStarted(parentContext.position(), task)); + + inputSchemaValidator.ifPresent(s -> s.validate(taskContext.rawInput())); + inputProcessor.ifPresent( + p -> taskContext.input(p.apply(workflowContext, taskContext, taskContext.rawInput()))); + internalExecute(workflowContext, taskContext); + outputProcessor.ifPresent( + p -> taskContext.output(p.apply(workflowContext, taskContext, taskContext.rawOutput()))); + outputSchemaValidator.ifPresent(s -> s.validate(taskContext.output())); + contextProcessor.ifPresent( + p -> + workflowContext.context( + p.apply(workflowContext, taskContext, workflowContext.context()))); + contextSchemaValidator.ifPresent(s -> s.validate(workflowContext.context())); + taskContext.completedAt(Instant.now()); + workflowContext + .definition() + .listeners() + .forEach(l -> l.onTaskEnded(parentContext.position(), task)); + } + return taskContext; + } + + protected abstract void internalExecute(WorkflowContext workflow, TaskContext taskContext); +} diff --git a/impl/core/src/main/java/io/serverlessworkflow/impl/executors/CallTaskExecutor.java b/impl/core/src/main/java/io/serverlessworkflow/impl/executors/CallTaskExecutor.java new file mode 100644 index 00000000..535057fa --- /dev/null +++ b/impl/core/src/main/java/io/serverlessworkflow/impl/executors/CallTaskExecutor.java @@ -0,0 +1,37 @@ +/* + * 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.impl.executors; + +import io.serverlessworkflow.api.types.TaskBase; +import io.serverlessworkflow.impl.TaskContext; +import io.serverlessworkflow.impl.WorkflowContext; +import io.serverlessworkflow.impl.WorkflowDefinition; + +public class CallTaskExecutor extends AbstractTaskExecutor { + + private final CallableTask callable; + + protected CallTaskExecutor(T task, WorkflowDefinition definition, CallableTask callable) { + super(task, definition); + this.callable = callable; + callable.init(task, definition); + } + + @Override + protected void internalExecute(WorkflowContext workflow, TaskContext taskContext) { + taskContext.rawOutput(callable.apply(workflow, taskContext, taskContext.input())); + } +} diff --git a/impl/src/main/java/io/serverlessworkflow/impl/AbstractTaskExecutor.java b/impl/core/src/main/java/io/serverlessworkflow/impl/executors/CallableTask.java similarity index 57% rename from impl/src/main/java/io/serverlessworkflow/impl/AbstractTaskExecutor.java rename to impl/core/src/main/java/io/serverlessworkflow/impl/executors/CallableTask.java index 13181603..ffb94912 100644 --- a/impl/src/main/java/io/serverlessworkflow/impl/AbstractTaskExecutor.java +++ b/impl/core/src/main/java/io/serverlessworkflow/impl/executors/CallableTask.java @@ -13,29 +13,18 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package io.serverlessworkflow.impl; +package io.serverlessworkflow.impl.executors; import com.fasterxml.jackson.databind.JsonNode; import io.serverlessworkflow.api.types.TaskBase; +import io.serverlessworkflow.impl.TaskContext; +import io.serverlessworkflow.impl.WorkflowContext; +import io.serverlessworkflow.impl.WorkflowDefinition; -public abstract class AbstractTaskExecutor implements TaskExecutor { +public interface CallableTask { + void init(T task, WorkflowDefinition definition); - protected final T task; - protected final ExpressionFactory exprFactory; + JsonNode apply(WorkflowContext workflowContext, TaskContext taskContext, JsonNode input); - protected AbstractTaskExecutor(T task, ExpressionFactory exprFactory) { - this.task = task; - this.exprFactory = exprFactory; - } - - @Override - public JsonNode apply(JsonNode node) { - - // do input filtering - return internalExecute(node); - // do output filtering - - } - - protected abstract JsonNode internalExecute(JsonNode node); + boolean accept(Class clazz); } diff --git a/impl/core/src/main/java/io/serverlessworkflow/impl/executors/DefaultTaskExecutorFactory.java b/impl/core/src/main/java/io/serverlessworkflow/impl/executors/DefaultTaskExecutorFactory.java new file mode 100644 index 00000000..e7dd07db --- /dev/null +++ b/impl/core/src/main/java/io/serverlessworkflow/impl/executors/DefaultTaskExecutorFactory.java @@ -0,0 +1,92 @@ +/* + * 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.impl.executors; + +import io.serverlessworkflow.api.types.CallAsyncAPI; +import io.serverlessworkflow.api.types.CallFunction; +import io.serverlessworkflow.api.types.CallGRPC; +import io.serverlessworkflow.api.types.CallHTTP; +import io.serverlessworkflow.api.types.CallOpenAPI; +import io.serverlessworkflow.api.types.CallTask; +import io.serverlessworkflow.api.types.Task; +import io.serverlessworkflow.api.types.TaskBase; +import io.serverlessworkflow.impl.WorkflowDefinition; +import java.util.ServiceLoader; +import java.util.ServiceLoader.Provider; + +public class DefaultTaskExecutorFactory implements TaskExecutorFactory { + + private static TaskExecutorFactory instance = new DefaultTaskExecutorFactory(); + + public static TaskExecutorFactory get() { + return instance; + } + + protected DefaultTaskExecutorFactory() {} + + private ServiceLoader callTasks = ServiceLoader.load(CallableTask.class); + + public TaskExecutor getTaskExecutor( + Task task, WorkflowDefinition definition) { + if (task.getCallTask() != null) { + CallTask callTask = task.getCallTask(); + if (callTask.getCallHTTP() != null) { + return new CallTaskExecutor<>( + callTask.getCallHTTP(), definition, findCallTask(CallHTTP.class)); + } else if (callTask.getCallAsyncAPI() != null) { + return new CallTaskExecutor<>( + callTask.getCallAsyncAPI(), definition, findCallTask(CallAsyncAPI.class)); + } else if (callTask.getCallGRPC() != null) { + return new CallTaskExecutor<>( + callTask.getCallGRPC(), definition, findCallTask(CallGRPC.class)); + } else if (callTask.getCallOpenAPI() != null) { + return new CallTaskExecutor<>( + callTask.getCallOpenAPI(), definition, findCallTask(CallOpenAPI.class)); + } else if (callTask.getCallFunction() != null) { + return new CallTaskExecutor<>( + callTask.getCallFunction(), definition, findCallTask(CallFunction.class)); + } + } else if (task.getSwitchTask() != null) { + return new SwitchExecutor(task.getSwitchTask(), definition); + } else if (task.getDoTask() != null) { + return new DoExecutor(task.getDoTask(), definition); + } else if (task.getSetTask() != null) { + return new SetExecutor(task.getSetTask(), definition); + } else if (task.getForTask() != null) { + return new ForExecutor(task.getForTask(), definition); + } else if (task.getRaiseTask() != null) { + return new RaiseExecutor(task.getRaiseTask(), definition); + } else if (task.getTryTask() != null) { + return new TryExecutor(task.getTryTask(), definition); + } else if (task.getForkTask() != null) { + return new ForkExecutor(task.getForkTask(), definition); + } else if (task.getWaitTask() != null) { + return new WaitExecutor(task.getWaitTask(), definition); + } + throw new UnsupportedOperationException(task.get().getClass().getName() + " not supported yet"); + } + + @SuppressWarnings("unchecked") + private CallableTask findCallTask(Class clazz) { + return (CallableTask) + callTasks.stream() + .map(Provider::get) + .filter(s -> s.accept(clazz)) + .findAny() + .orElseThrow( + () -> new UnsupportedOperationException(clazz.getName() + " not supported yet")); + } +} diff --git a/impl/core/src/main/java/io/serverlessworkflow/impl/executors/DoExecutor.java b/impl/core/src/main/java/io/serverlessworkflow/impl/executors/DoExecutor.java new file mode 100644 index 00000000..c5dbc4fd --- /dev/null +++ b/impl/core/src/main/java/io/serverlessworkflow/impl/executors/DoExecutor.java @@ -0,0 +1,33 @@ +/* + * 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.impl.executors; + +import io.serverlessworkflow.api.types.DoTask; +import io.serverlessworkflow.impl.TaskContext; +import io.serverlessworkflow.impl.WorkflowContext; +import io.serverlessworkflow.impl.WorkflowDefinition; + +public class DoExecutor extends AbstractTaskExecutor { + + protected DoExecutor(DoTask task, WorkflowDefinition definition) { + super(task, definition); + } + + @Override + protected void internalExecute(WorkflowContext workflow, TaskContext taskContext) { + TaskExecutorHelper.processTaskList(task.getDo(), workflow, taskContext); + } +} diff --git a/impl/core/src/main/java/io/serverlessworkflow/impl/executors/ForExecutor.java b/impl/core/src/main/java/io/serverlessworkflow/impl/executors/ForExecutor.java new file mode 100644 index 00000000..cb4ecec0 --- /dev/null +++ b/impl/core/src/main/java/io/serverlessworkflow/impl/executors/ForExecutor.java @@ -0,0 +1,58 @@ +/* + * 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.impl.executors; + +import com.fasterxml.jackson.databind.JsonNode; +import io.serverlessworkflow.api.types.ForTask; +import io.serverlessworkflow.api.types.ForTaskConfiguration; +import io.serverlessworkflow.impl.TaskContext; +import io.serverlessworkflow.impl.WorkflowContext; +import io.serverlessworkflow.impl.WorkflowDefinition; +import io.serverlessworkflow.impl.WorkflowFilter; +import io.serverlessworkflow.impl.WorkflowUtils; +import java.util.Iterator; +import java.util.Optional; + +public class ForExecutor extends AbstractTaskExecutor { + + private final WorkflowFilter collectionExpr; + private final Optional whileExpr; + + protected ForExecutor(ForTask task, WorkflowDefinition definition) { + super(task, definition); + ForTaskConfiguration forConfig = task.getFor(); + this.collectionExpr = + WorkflowUtils.buildWorkflowFilter(definition.expressionFactory(), forConfig.getIn()); + this.whileExpr = WorkflowUtils.optionalFilter(definition.expressionFactory(), task.getWhile()); + } + + @Override + protected void internalExecute(WorkflowContext workflow, TaskContext taskContext) { + Iterator iter = + collectionExpr.apply(workflow, taskContext, taskContext.input()).iterator(); + int i = 0; + while (iter.hasNext() + && whileExpr + .map(w -> w.apply(workflow, taskContext, taskContext.rawOutput())) + .map(n -> n.asBoolean(true)) + .orElse(true)) { + JsonNode item = iter.next(); + taskContext.variables().put(task.getFor().getEach(), item); + taskContext.variables().put(task.getFor().getAt(), i++); + TaskExecutorHelper.processTaskList(task.getDo(), workflow, taskContext); + } + } +} diff --git a/impl/core/src/main/java/io/serverlessworkflow/impl/executors/ForkExecutor.java b/impl/core/src/main/java/io/serverlessworkflow/impl/executors/ForkExecutor.java new file mode 100644 index 00000000..e0ce3b02 --- /dev/null +++ b/impl/core/src/main/java/io/serverlessworkflow/impl/executors/ForkExecutor.java @@ -0,0 +1,110 @@ +/* + * 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.impl.executors; + +import com.fasterxml.jackson.databind.JsonNode; +import io.serverlessworkflow.api.types.FlowDirectiveEnum; +import io.serverlessworkflow.api.types.ForkTask; +import io.serverlessworkflow.api.types.ForkTaskConfiguration; +import io.serverlessworkflow.api.types.TaskItem; +import io.serverlessworkflow.impl.TaskContext; +import io.serverlessworkflow.impl.WorkflowContext; +import io.serverlessworkflow.impl.WorkflowDefinition; +import io.serverlessworkflow.impl.WorkflowStatus; +import io.serverlessworkflow.impl.json.JsonUtils; +import java.lang.reflect.UndeclaredThrowableException; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Future; +import java.util.stream.Stream; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class ForkExecutor extends AbstractTaskExecutor { + + private static final Logger logger = LoggerFactory.getLogger(ForkExecutor.class); + private final ExecutorService service; + + protected ForkExecutor(ForkTask task, WorkflowDefinition definition) { + super(task, definition); + service = definition.executorService(); + } + + @Override + protected void internalExecute(WorkflowContext workflow, TaskContext taskContext) { + ForkTaskConfiguration forkConfig = task.getFork(); + + if (!forkConfig.getBranches().isEmpty()) { + Map>> futures = new HashMap<>(); + int index = 0; + for (TaskItem item : forkConfig.getBranches()) { + final int i = index++; + futures.put( + item.getName(), + service.submit(() -> executeBranch(workflow, taskContext.copy(), item, i))); + } + List>> results = new ArrayList<>(); + for (Map.Entry>> entry : futures.entrySet()) { + try { + results.add(Map.entry(entry.getKey(), entry.getValue().get())); + } catch (ExecutionException ex) { + Throwable cause = ex.getCause(); + if (cause instanceof RuntimeException) { + throw (RuntimeException) cause; + } else { + throw new UndeclaredThrowableException(ex); + } + } catch (InterruptedException ex) { + logger.warn("Branch {} was interrupted, no result will be recorded", entry.getKey(), ex); + } + } + if (!results.isEmpty()) { + Stream>> sortedStream = + results.stream() + .sorted( + (arg1, arg2) -> + arg1.getValue().completedAt().compareTo(arg2.getValue().completedAt())); + taskContext.rawOutput( + forkConfig.isCompete() + ? sortedStream.map(e -> e.getValue().output()).findFirst().orElseThrow() + : sortedStream + .map( + e -> + JsonUtils.mapper() + .createObjectNode() + .set(e.getKey(), e.getValue().output())) + .collect(JsonUtils.arrayNodeCollector())); + } + } + } + + private TaskContext executeBranch( + WorkflowContext workflow, TaskContext taskContext, TaskItem taskItem, int index) { + taskContext.position().addIndex(index); + TaskContext result = + TaskExecutorHelper.executeTask(workflow, taskContext, taskItem, taskContext.input()); + if (result.flowDirective() != null + && result.flowDirective().getFlowDirectiveEnum() == FlowDirectiveEnum.END) { + workflow.instance().status(WorkflowStatus.COMPLETED); + } + taskContext.position().back(); + return result; + } +} diff --git a/impl/core/src/main/java/io/serverlessworkflow/impl/executors/RaiseExecutor.java b/impl/core/src/main/java/io/serverlessworkflow/impl/executors/RaiseExecutor.java new file mode 100644 index 00000000..1ddc315f --- /dev/null +++ b/impl/core/src/main/java/io/serverlessworkflow/impl/executors/RaiseExecutor.java @@ -0,0 +1,104 @@ +/* + * 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.impl.executors; + +import io.serverlessworkflow.api.types.Error; +import io.serverlessworkflow.api.types.ErrorInstance; +import io.serverlessworkflow.api.types.ErrorType; +import io.serverlessworkflow.api.types.RaiseTask; +import io.serverlessworkflow.api.types.RaiseTaskError; +import io.serverlessworkflow.impl.StringFilter; +import io.serverlessworkflow.impl.TaskContext; +import io.serverlessworkflow.impl.WorkflowContext; +import io.serverlessworkflow.impl.WorkflowDefinition; +import io.serverlessworkflow.impl.WorkflowError; +import io.serverlessworkflow.impl.WorkflowException; +import io.serverlessworkflow.impl.WorkflowUtils; +import io.serverlessworkflow.impl.expressions.ExpressionFactory; +import java.util.Map; +import java.util.Optional; +import java.util.function.BiFunction; + +public class RaiseExecutor extends AbstractTaskExecutor { + + private final BiFunction, WorkflowError> errorBuilder; + + private final StringFilter typeFilter; + private final Optional instanceFilter; + private final StringFilter titleFilter; + private final StringFilter detailFilter; + + protected RaiseExecutor(RaiseTask task, WorkflowDefinition definition) { + super(task, definition); + RaiseTaskError raiseError = task.getRaise().getError(); + Error error = + raiseError.getRaiseErrorDefinition() != null + ? raiseError.getRaiseErrorDefinition() + : findError(definition, raiseError.getRaiseErrorReference()); + this.typeFilter = getTypeFunction(definition.expressionFactory(), error.getType()); + this.instanceFilter = getInstanceFunction(definition.expressionFactory(), error.getInstance()); + this.titleFilter = + WorkflowUtils.buildStringFilter(definition.expressionFactory(), error.getTitle()); + this.detailFilter = + WorkflowUtils.buildStringFilter(definition.expressionFactory(), error.getDetail()); + this.errorBuilder = (w, t) -> buildError(error, w, t); + } + + private static Error findError(WorkflowDefinition definition, String raiseErrorReference) { + Map errorsMap = + definition.workflow().getUse().getErrors().getAdditionalProperties(); + Error error = errorsMap.get(raiseErrorReference); + if (error == null) { + throw new IllegalArgumentException("Error " + error + "is not defined in " + errorsMap); + } + return error; + } + + private WorkflowError buildError( + Error error, WorkflowContext context, TaskContext taskContext) { + return WorkflowError.error(typeFilter.apply(context, taskContext), error.getStatus()) + .instance( + instanceFilter + .map(f -> f.apply(context, taskContext)) + .orElseGet(() -> taskContext.position().jsonPointer())) + .title(titleFilter.apply(context, taskContext)) + .details(detailFilter.apply(context, taskContext)) + .build(); + } + + private Optional getInstanceFunction( + ExpressionFactory expressionFactory, ErrorInstance errorInstance) { + return errorInstance != null + ? Optional.of( + WorkflowUtils.buildStringFilter( + expressionFactory, + errorInstance.getExpressionErrorInstance(), + errorInstance.getLiteralErrorInstance())) + : Optional.empty(); + } + + private StringFilter getTypeFunction(ExpressionFactory expressionFactory, ErrorType type) { + return WorkflowUtils.buildStringFilter( + expressionFactory, + type.getExpressionErrorType(), + type.getLiteralErrorType().get().toString()); + } + + @Override + protected void internalExecute(WorkflowContext workflow, TaskContext taskContext) { + throw new WorkflowException(errorBuilder.apply(workflow, taskContext)); + } +} diff --git a/impl/core/src/main/java/io/serverlessworkflow/impl/executors/SetExecutor.java b/impl/core/src/main/java/io/serverlessworkflow/impl/executors/SetExecutor.java new file mode 100644 index 00000000..0f0d999e --- /dev/null +++ b/impl/core/src/main/java/io/serverlessworkflow/impl/executors/SetExecutor.java @@ -0,0 +1,47 @@ +/* + * 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.impl.executors; + +import io.serverlessworkflow.api.types.SetTask; +import io.serverlessworkflow.impl.TaskContext; +import io.serverlessworkflow.impl.WorkflowContext; +import io.serverlessworkflow.impl.WorkflowDefinition; +import io.serverlessworkflow.impl.expressions.ExpressionUtils; +import io.serverlessworkflow.impl.json.JsonUtils; +import io.serverlessworkflow.impl.json.MergeUtils; +import java.util.Map; + +public class SetExecutor extends AbstractTaskExecutor { + + private Map toBeSet; + + protected SetExecutor(SetTask task, WorkflowDefinition definition) { + super(task, definition); + this.toBeSet = + ExpressionUtils.buildExpressionMap( + task.getSet().getAdditionalProperties(), definition.expressionFactory()); + } + + @Override + protected void internalExecute(WorkflowContext workflow, TaskContext taskContext) { + taskContext.rawOutput( + MergeUtils.merge( + JsonUtils.fromValue( + ExpressionUtils.evaluateExpressionMap( + toBeSet, workflow, taskContext, taskContext.input())), + taskContext.input())); + } +} diff --git a/impl/core/src/main/java/io/serverlessworkflow/impl/executors/SwitchExecutor.java b/impl/core/src/main/java/io/serverlessworkflow/impl/executors/SwitchExecutor.java new file mode 100644 index 00000000..dee0cee7 --- /dev/null +++ b/impl/core/src/main/java/io/serverlessworkflow/impl/executors/SwitchExecutor.java @@ -0,0 +1,61 @@ +/* + * 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.impl.executors; + +import io.serverlessworkflow.api.types.FlowDirective; +import io.serverlessworkflow.api.types.SwitchCase; +import io.serverlessworkflow.api.types.SwitchItem; +import io.serverlessworkflow.api.types.SwitchTask; +import io.serverlessworkflow.impl.TaskContext; +import io.serverlessworkflow.impl.WorkflowContext; +import io.serverlessworkflow.impl.WorkflowDefinition; +import io.serverlessworkflow.impl.WorkflowFilter; +import io.serverlessworkflow.impl.WorkflowUtils; +import java.util.Map; +import java.util.Map.Entry; +import java.util.concurrent.ConcurrentHashMap; + +public class SwitchExecutor extends AbstractTaskExecutor { + + private Map workflowFilters = new ConcurrentHashMap<>(); + private FlowDirective defaultDirective; + + protected SwitchExecutor(SwitchTask task, WorkflowDefinition definition) { + super(task, definition); + for (SwitchItem item : task.getSwitch()) { + SwitchCase switchCase = item.getSwitchCase(); + if (switchCase.getWhen() != null) { + workflowFilters.put( + switchCase, + WorkflowUtils.buildWorkflowFilter( + definition.expressionFactory(), switchCase.getWhen())); + } else { + defaultDirective = switchCase.getThen(); + } + } + } + + @Override + protected void internalExecute(WorkflowContext workflow, TaskContext taskContext) { + for (Entry entry : workflowFilters.entrySet()) { + if (entry.getValue().apply(workflow, taskContext, taskContext.input()).asBoolean()) { + taskContext.flowDirective(entry.getKey().getThen()); + return; + } + } + taskContext.flowDirective(defaultDirective); + } +} diff --git a/impl/core/src/main/java/io/serverlessworkflow/impl/executors/TaskExecutor.java b/impl/core/src/main/java/io/serverlessworkflow/impl/executors/TaskExecutor.java new file mode 100644 index 00000000..b4b66a9a --- /dev/null +++ b/impl/core/src/main/java/io/serverlessworkflow/impl/executors/TaskExecutor.java @@ -0,0 +1,27 @@ +/* + * 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.impl.executors; + +import com.fasterxml.jackson.databind.JsonNode; +import io.serverlessworkflow.api.types.TaskBase; +import io.serverlessworkflow.impl.TaskContext; +import io.serverlessworkflow.impl.WorkflowContext; + +@FunctionalInterface +public interface TaskExecutor { + TaskContext apply( + WorkflowContext workflowContext, TaskContext parentContext, JsonNode input); +} diff --git a/impl/src/main/java/io/serverlessworkflow/impl/TaskExecutorFactory.java b/impl/core/src/main/java/io/serverlessworkflow/impl/executors/TaskExecutorFactory.java similarity index 79% rename from impl/src/main/java/io/serverlessworkflow/impl/TaskExecutorFactory.java rename to impl/core/src/main/java/io/serverlessworkflow/impl/executors/TaskExecutorFactory.java index 69eaa0a0..8c399cf6 100644 --- a/impl/src/main/java/io/serverlessworkflow/impl/TaskExecutorFactory.java +++ b/impl/core/src/main/java/io/serverlessworkflow/impl/executors/TaskExecutorFactory.java @@ -13,11 +13,12 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package io.serverlessworkflow.impl; +package io.serverlessworkflow.impl.executors; import io.serverlessworkflow.api.types.Task; import io.serverlessworkflow.api.types.TaskBase; +import io.serverlessworkflow.impl.WorkflowDefinition; public interface TaskExecutorFactory { - TaskExecutor getTaskExecutor(Task task); + TaskExecutor getTaskExecutor(Task task, WorkflowDefinition definition); } diff --git a/impl/core/src/main/java/io/serverlessworkflow/impl/executors/TaskExecutorHelper.java b/impl/core/src/main/java/io/serverlessworkflow/impl/executors/TaskExecutorHelper.java new file mode 100644 index 00000000..e16cb085 --- /dev/null +++ b/impl/core/src/main/java/io/serverlessworkflow/impl/executors/TaskExecutorHelper.java @@ -0,0 +1,111 @@ +/* + * 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.impl.executors; + +import com.fasterxml.jackson.databind.JsonNode; +import io.serverlessworkflow.api.types.FlowDirective; +import io.serverlessworkflow.api.types.TaskBase; +import io.serverlessworkflow.api.types.TaskItem; +import io.serverlessworkflow.impl.TaskContext; +import io.serverlessworkflow.impl.WorkflowContext; +import io.serverlessworkflow.impl.WorkflowStatus; +import java.util.List; +import java.util.ListIterator; + +public class TaskExecutorHelper { + private TaskExecutorHelper() {} + + public static void processTaskList( + List tasks, WorkflowContext context, TaskContext parentTask) { + parentTask.position().addProperty("do"); + TaskContext currentContext = parentTask; + if (!tasks.isEmpty()) { + ListIterator iter = tasks.listIterator(); + TaskItem nextTask = iter.next(); + while (nextTask != null && isActive(context)) { + TaskItem task = nextTask; + parentTask.position().addIndex(iter.previousIndex()); + currentContext = executeTask(context, parentTask, task, currentContext.output()); + FlowDirective flowDirective = currentContext.flowDirective(); + if (flowDirective.getFlowDirectiveEnum() != null) { + switch (flowDirective.getFlowDirectiveEnum()) { + case CONTINUE: + nextTask = iter.hasNext() ? iter.next() : null; + break; + case END: + context.instance().status(WorkflowStatus.COMPLETED); + break; + case EXIT: + nextTask = null; + break; + } + } else { + nextTask = findTaskByName(iter, flowDirective.getString()); + } + parentTask.position().back(); + } + } + parentTask.position().back(); + parentTask.rawOutput(currentContext.output()); + } + + public static boolean isActive(WorkflowContext context) { + return isActive(context.instance().status()); + } + + public static boolean isActive(WorkflowStatus status) { + return status == WorkflowStatus.RUNNING; + } + + public static TaskContext executeTask( + WorkflowContext context, TaskContext parentTask, TaskItem task, JsonNode input) { + parentTask.position().addProperty(task.getName()); + TaskContext result = + context + .definition() + .taskExecutors() + .computeIfAbsent( + parentTask.position().jsonPointer(), + k -> + context + .definition() + .taskFactory() + .getTaskExecutor(task.getTask(), context.definition())) + .apply(context, parentTask, input); + parentTask.position().back(); + return result; + } + + private static TaskItem findTaskByName(ListIterator iter, String taskName) { + int currentIndex = iter.nextIndex(); + while (iter.hasPrevious()) { + TaskItem item = iter.previous(); + if (item.getName().equals(taskName)) { + return item; + } + } + while (iter.nextIndex() < currentIndex) { + iter.next(); + } + while (iter.hasNext()) { + TaskItem item = iter.next(); + if (item.getName().equals(taskName)) { + return item; + } + } + throw new IllegalArgumentException("Cannot find task with name " + taskName); + } +} diff --git a/impl/core/src/main/java/io/serverlessworkflow/impl/executors/TryExecutor.java b/impl/core/src/main/java/io/serverlessworkflow/impl/executors/TryExecutor.java new file mode 100644 index 00000000..eed2801b --- /dev/null +++ b/impl/core/src/main/java/io/serverlessworkflow/impl/executors/TryExecutor.java @@ -0,0 +1,86 @@ +/* + * 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.impl.executors; + +import io.serverlessworkflow.api.types.CatchErrors; +import io.serverlessworkflow.api.types.ErrorFilter; +import io.serverlessworkflow.api.types.TryTask; +import io.serverlessworkflow.api.types.TryTaskCatch; +import io.serverlessworkflow.impl.TaskContext; +import io.serverlessworkflow.impl.WorkflowContext; +import io.serverlessworkflow.impl.WorkflowDefinition; +import io.serverlessworkflow.impl.WorkflowError; +import io.serverlessworkflow.impl.WorkflowException; +import io.serverlessworkflow.impl.WorkflowFilter; +import io.serverlessworkflow.impl.WorkflowUtils; +import java.util.Optional; +import java.util.function.Predicate; + +public class TryExecutor extends AbstractTaskExecutor { + + private final Optional whenFilter; + private final Optional exceptFilter; + private final Optional> errorFilter; + + protected TryExecutor(TryTask task, WorkflowDefinition definition) { + super(task, definition); + TryTaskCatch catchInfo = task.getCatch(); + this.errorFilter = buildErrorFilter(catchInfo.getErrors()); + this.whenFilter = + WorkflowUtils.optionalFilter(definition.expressionFactory(), catchInfo.getWhen()); + this.exceptFilter = + WorkflowUtils.optionalFilter(definition.expressionFactory(), catchInfo.getExceptWhen()); + } + + @Override + protected void internalExecute(WorkflowContext workflow, TaskContext taskContext) { + try { + TaskExecutorHelper.processTaskList(task.getTry(), workflow, taskContext); + } catch (WorkflowException exception) { + if (errorFilter.map(f -> f.test(exception.getWorflowError())).orElse(true) + && whenFilter + .map(w -> w.apply(workflow, taskContext, taskContext.input()).asBoolean()) + .orElse(true) + && exceptFilter + .map(w -> !w.apply(workflow, taskContext, taskContext.input()).asBoolean()) + .orElse(true)) { + if (task.getCatch().getDo() != null) { + TaskExecutorHelper.processTaskList(task.getCatch().getDo(), workflow, taskContext); + } + } else { + throw exception; + } + } + } + + private static Optional> buildErrorFilter(CatchErrors errors) { + return errors != null + ? Optional.of(error -> filterError(error, errors.getWith())) + : Optional.empty(); + } + + private static boolean filterError(WorkflowError error, ErrorFilter errorFilter) { + return compareString(errorFilter.getType(), error.type()) + && (errorFilter.getStatus() <= 0 || error.status() == errorFilter.getStatus()) + && compareString(errorFilter.getInstance(), error.instance()) + && compareString(errorFilter.getTitle(), error.title()) + && compareString(errorFilter.getDetails(), errorFilter.getDetails()); + } + + private static boolean compareString(String one, String other) { + return one == null || one.equals(other); + } +} diff --git a/impl/core/src/main/java/io/serverlessworkflow/impl/executors/WaitExecutor.java b/impl/core/src/main/java/io/serverlessworkflow/impl/executors/WaitExecutor.java new file mode 100644 index 00000000..a1fb31c5 --- /dev/null +++ b/impl/core/src/main/java/io/serverlessworkflow/impl/executors/WaitExecutor.java @@ -0,0 +1,58 @@ +/* + * 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.impl.executors; + +import io.serverlessworkflow.api.types.DurationInline; +import io.serverlessworkflow.api.types.WaitTask; +import io.serverlessworkflow.impl.TaskContext; +import io.serverlessworkflow.impl.WorkflowContext; +import io.serverlessworkflow.impl.WorkflowDefinition; +import java.time.Duration; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class WaitExecutor extends AbstractTaskExecutor { + + private static Logger logger = LoggerFactory.getLogger(WaitExecutor.class); + private final Duration millisToWait; + + protected WaitExecutor(WaitTask task, WorkflowDefinition definition) { + super(task, definition); + this.millisToWait = + task.getWait().getDurationInline() != null + ? toLong(task.getWait().getDurationInline()) + : Duration.parse(task.getWait().getDurationExpression()); + } + + private Duration toLong(DurationInline durationInline) { + Duration duration = Duration.ofMillis(durationInline.getMilliseconds()); + duration.plus(Duration.ofSeconds(durationInline.getSeconds())); + duration.plus(Duration.ofMinutes(durationInline.getMinutes())); + duration.plus(Duration.ofHours(durationInline.getHours())); + duration.plus(Duration.ofDays(durationInline.getDays())); + return duration; + } + + @Override + protected void internalExecute(WorkflowContext workflow, TaskContext taskContext) { + try { + Thread.sleep(millisToWait.toMillis()); + } catch (InterruptedException e) { + logger.warn("Waiting thread was interrupted", e); + Thread.currentThread().interrupt(); + } + } +} diff --git a/impl/core/src/main/java/io/serverlessworkflow/impl/expressions/DateTimeDescriptor.java b/impl/core/src/main/java/io/serverlessworkflow/impl/expressions/DateTimeDescriptor.java new file mode 100644 index 00000000..7936763f --- /dev/null +++ b/impl/core/src/main/java/io/serverlessworkflow/impl/expressions/DateTimeDescriptor.java @@ -0,0 +1,48 @@ +/* + * 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.impl.expressions; + +import com.fasterxml.jackson.annotation.JsonProperty; +import java.time.Instant; + +public class DateTimeDescriptor { + + private final Instant instant; + + public static DateTimeDescriptor from(Instant instant) { + return new DateTimeDescriptor(instant); + } + + private DateTimeDescriptor(Instant instant) { + this.instant = instant; + } + + @JsonProperty("iso8601") + public String iso8601() { + return instant.toString(); + } + + @JsonProperty("epoch") + public Epoch epoch() { + return Epoch.of(instant); + } + + public static record Epoch(long seconds, long milliseconds) { + public static Epoch of(Instant instant) { + return new Epoch(instant.getEpochSecond(), instant.toEpochMilli()); + } + } +} diff --git a/impl/src/main/java/io/serverlessworkflow/impl/Expression.java b/impl/core/src/main/java/io/serverlessworkflow/impl/expressions/Expression.java similarity index 75% rename from impl/src/main/java/io/serverlessworkflow/impl/Expression.java rename to impl/core/src/main/java/io/serverlessworkflow/impl/expressions/Expression.java index b5bbfc0b..42566c77 100644 --- a/impl/src/main/java/io/serverlessworkflow/impl/Expression.java +++ b/impl/core/src/main/java/io/serverlessworkflow/impl/expressions/Expression.java @@ -13,10 +13,12 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package io.serverlessworkflow.impl; +package io.serverlessworkflow.impl.expressions; import com.fasterxml.jackson.databind.JsonNode; +import io.serverlessworkflow.impl.TaskContext; +import io.serverlessworkflow.impl.WorkflowContext; public interface Expression { - JsonNode eval(JsonNode input); + JsonNode eval(WorkflowContext workflowContext, TaskContext context, JsonNode node); } diff --git a/impl/src/main/java/io/serverlessworkflow/impl/ExpressionFactory.java b/impl/core/src/main/java/io/serverlessworkflow/impl/expressions/ExpressionFactory.java similarity index 83% rename from impl/src/main/java/io/serverlessworkflow/impl/ExpressionFactory.java rename to impl/core/src/main/java/io/serverlessworkflow/impl/expressions/ExpressionFactory.java index 8f9c1dd1..4d07d5af 100644 --- a/impl/src/main/java/io/serverlessworkflow/impl/ExpressionFactory.java +++ b/impl/core/src/main/java/io/serverlessworkflow/impl/expressions/ExpressionFactory.java @@ -13,9 +13,13 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package io.serverlessworkflow.impl; +package io.serverlessworkflow.impl.expressions; public interface ExpressionFactory { - + /** + * @throws ExpressionValidationException + * @param expression + * @return + */ Expression getExpression(String expression); } diff --git a/impl/core/src/main/java/io/serverlessworkflow/impl/expressions/ExpressionUtils.java b/impl/core/src/main/java/io/serverlessworkflow/impl/expressions/ExpressionUtils.java new file mode 100644 index 00000000..7f776322 --- /dev/null +++ b/impl/core/src/main/java/io/serverlessworkflow/impl/expressions/ExpressionUtils.java @@ -0,0 +1,78 @@ +/* + * 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.impl.expressions; + +import com.fasterxml.jackson.databind.JsonNode; +import io.serverlessworkflow.impl.TaskContext; +import io.serverlessworkflow.impl.WorkflowContext; +import io.serverlessworkflow.impl.json.JsonUtils; +import java.util.Map; + +public class ExpressionUtils { + + private static final String EXPR_PREFIX = "${"; + private static final String EXPR_SUFFIX = "}"; + + private ExpressionUtils() {} + + public static Map buildExpressionMap( + Map origMap, ExpressionFactory factory) { + return new ProxyMap(origMap, o -> isExpr(o) ? factory.getExpression(o.toString()) : o); + } + + public static Map evaluateExpressionMap( + Map origMap, WorkflowContext workflow, TaskContext task, JsonNode n) { + return new ProxyMap( + origMap, + o -> + o instanceof Expression + ? JsonUtils.toJavaValue(((Expression) o).eval(workflow, task, n)) + : o); + } + + public static Object buildExpressionObject(Object obj, ExpressionFactory factory) { + return obj instanceof Map + ? ExpressionUtils.buildExpressionMap((Map) obj, factory) + : obj; + } + + public static Object evaluateExpressionObject( + Object obj, WorkflowContext workflow, TaskContext task, JsonNode node) { + return obj instanceof Map + ? ExpressionUtils.evaluateExpressionMap((Map) obj, workflow, task, node) + : obj; + } + + public static boolean isExpr(Object expr) { + return expr instanceof String && ((String) expr).startsWith(EXPR_PREFIX); + } + + public static String trimExpr(String expr) { + expr = expr.trim(); + if (expr.startsWith(EXPR_PREFIX)) { + expr = trimExpr(expr, EXPR_PREFIX, EXPR_SUFFIX); + } + return expr.trim(); + } + + private static String trimExpr(String expr, String prefix, String suffix) { + expr = expr.substring(prefix.length()); + if (expr.endsWith(suffix)) { + expr = expr.substring(0, expr.length() - suffix.length()); + } + return expr; + } +} diff --git a/impl/core/src/main/java/io/serverlessworkflow/impl/expressions/ExpressionValidationException.java b/impl/core/src/main/java/io/serverlessworkflow/impl/expressions/ExpressionValidationException.java new file mode 100644 index 00000000..8dc1b4f2 --- /dev/null +++ b/impl/core/src/main/java/io/serverlessworkflow/impl/expressions/ExpressionValidationException.java @@ -0,0 +1,29 @@ +/* + * 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.impl.expressions; + +public class ExpressionValidationException extends RuntimeException { + + private static final long serialVersionUID = 1L; + + public ExpressionValidationException(String message) { + super(message); + } + + public ExpressionValidationException(String message, Throwable ex) { + super(message, ex); + } +} diff --git a/impl/core/src/main/java/io/serverlessworkflow/impl/expressions/JQExpression.java b/impl/core/src/main/java/io/serverlessworkflow/impl/expressions/JQExpression.java new file mode 100644 index 00000000..0207d3b5 --- /dev/null +++ b/impl/core/src/main/java/io/serverlessworkflow/impl/expressions/JQExpression.java @@ -0,0 +1,91 @@ +/* + * 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.impl.expressions; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.node.ArrayNode; +import io.serverlessworkflow.impl.TaskContext; +import io.serverlessworkflow.impl.WorkflowContext; +import io.serverlessworkflow.impl.json.JsonUtils; +import java.util.function.Supplier; +import net.thisptr.jackson.jq.Output; +import net.thisptr.jackson.jq.Scope; +import net.thisptr.jackson.jq.Version; +import net.thisptr.jackson.jq.exception.JsonQueryException; +import net.thisptr.jackson.jq.internal.javacc.ExpressionParser; + +public class JQExpression implements Expression { + + private final Supplier scope; + private final String expr; + private final net.thisptr.jackson.jq.Expression internalExpr; + + public JQExpression(Supplier scope, String expr, Version version) + throws JsonQueryException { + this.expr = expr; + this.scope = scope; + this.internalExpr = ExpressionParser.compile(expr, version); + } + + @Override + public JsonNode eval(WorkflowContext workflow, TaskContext task, JsonNode node) { + JsonNodeOutput output = new JsonNodeOutput(); + try { + internalExpr.apply(createScope(workflow, task), node, output); + return output.getResult(); + } catch (JsonQueryException e) { + throw new IllegalArgumentException( + "Unable to evaluate content " + node + " using expr " + expr, e); + } + } + + private static class JsonNodeOutput implements Output { + private JsonNode result; + private boolean arrayCreated; + + @Override + public void emit(JsonNode out) throws JsonQueryException { + if (this.result == null) { + this.result = out; + } else if (!arrayCreated) { + ArrayNode newNode = JsonUtils.mapper().createArrayNode(); + newNode.add(this.result).add(out); + this.result = newNode; + arrayCreated = true; + } else { + ((ArrayNode) this.result).add(out); + } + } + + public JsonNode getResult() { + return result; + } + } + + private Scope createScope(WorkflowContext workflow, TaskContext task) { + Scope childScope = Scope.newChildScope(scope.get()); + childScope.setValue("input", task.input()); + childScope.setValue("output", task.output()); + childScope.setValue("context", workflow.context()); + childScope.setValue( + "runtime", + () -> JsonUtils.fromValue(workflow.definition().runtimeDescriptorFactory().get())); + childScope.setValue("workflow", () -> JsonUtils.fromValue(WorkflowDescriptor.of(workflow))); + childScope.setValue("task", () -> JsonUtils.fromValue(TaskDescriptor.of(task))); + task.variables().forEach((k, v) -> childScope.setValue(k, JsonUtils.fromValue(v))); + return childScope; + } +} diff --git a/impl/src/main/java/io/serverlessworkflow/impl/jq/JQExpressionFactory.java b/impl/core/src/main/java/io/serverlessworkflow/impl/expressions/JQExpressionFactory.java similarity index 90% rename from impl/src/main/java/io/serverlessworkflow/impl/jq/JQExpressionFactory.java rename to impl/core/src/main/java/io/serverlessworkflow/impl/expressions/JQExpressionFactory.java index 787842d6..0375224a 100644 --- a/impl/src/main/java/io/serverlessworkflow/impl/jq/JQExpressionFactory.java +++ b/impl/core/src/main/java/io/serverlessworkflow/impl/expressions/JQExpressionFactory.java @@ -13,11 +13,8 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package io.serverlessworkflow.impl.jq; +package io.serverlessworkflow.impl.expressions; -import io.serverlessworkflow.impl.Expression; -import io.serverlessworkflow.impl.ExpressionFactory; -import io.serverlessworkflow.impl.ExpressionUtils; import java.util.function.Supplier; import net.thisptr.jackson.jq.BuiltinFunctionLoader; import net.thisptr.jackson.jq.Scope; diff --git a/impl/core/src/main/java/io/serverlessworkflow/impl/expressions/ProxyMap.java b/impl/core/src/main/java/io/serverlessworkflow/impl/expressions/ProxyMap.java new file mode 100644 index 00000000..bf4464b2 --- /dev/null +++ b/impl/core/src/main/java/io/serverlessworkflow/impl/expressions/ProxyMap.java @@ -0,0 +1,278 @@ +/* + * 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.impl.expressions; + +import java.util.Collection; +import java.util.Iterator; +import java.util.Map; +import java.util.Set; +import java.util.function.UnaryOperator; + +public class ProxyMap implements Map { + + private final Map map; + private final UnaryOperator function; + + public ProxyMap(Map map, UnaryOperator function) { + this.map = map; + this.function = function; + } + + @Override + public int size() { + return map.size(); + } + + @Override + public boolean isEmpty() { + return map.isEmpty(); + } + + @Override + public boolean containsKey(Object key) { + return map.containsKey(key); + } + + @Override + public boolean containsValue(Object value) { + return map.containsValue(value); + } + + @Override + public Object get(Object key) { + return processValue(map.get(key)); + } + + @Override + public Object put(String key, Object value) { + return map.put(key, processValue(value)); + } + + @Override + public Object remove(Object key) { + return map.remove(key); + } + + @Override + public void putAll(Map m) { + map.putAll(m); + } + + @Override + public void clear() { + map.clear(); + } + + @Override + public Set keySet() { + return map.keySet(); + } + + @Override + public Collection values() { + return new ProxyCollection(map.values()); + } + + @Override + public Set> entrySet() { + return new ProxyEntrySet(map.entrySet()); + } + + private abstract class AbstractProxyCollection { + + protected Collection values; + + protected AbstractProxyCollection(Collection values) { + this.values = values; + } + + public int size() { + return values.size(); + } + + public boolean isEmpty() { + return values.isEmpty(); + } + + public boolean contains(Object o) { + return values.contains(o); + } + + public boolean remove(Object o) { + return values.remove(o); + } + + public boolean containsAll(Collection c) { + return values.containsAll(c); + } + + public boolean retainAll(Collection c) { + return values.retainAll(c); + } + + public boolean removeAll(Collection c) { + return values.removeAll(c); + } + + public void clear() { + values.clear(); + } + + public boolean addAll(Collection c) { + return values.addAll(c); + } + + public boolean add(T e) { + return values.add(e); + } + } + + private class ProxyEntrySet extends AbstractProxyCollection> + implements Set> { + + public ProxyEntrySet(Set> entrySet) { + super(entrySet); + } + + @Override + public Iterator> iterator() { + return new ProxyEntryIterator(values.iterator()); + } + + @Override + public Object[] toArray() { + return processEntries(values.toArray()); + } + + @Override + public T[] toArray(T[] a) { + return processEntries(values.toArray(a)); + } + + private T[] processEntries(T[] array) { + for (int i = 0; i < array.length; i++) { + array[i] = (T) new ProxyEntry((Entry) array[i]); + } + return array; + } + } + + private class ProxyCollection extends AbstractProxyCollection + implements Collection { + + public ProxyCollection(Collection values) { + super(values); + } + + @Override + public Iterator iterator() { + return new ProxyIterator(values.iterator()); + } + + @Override + public Object[] toArray() { + return processArray(values.toArray()); + } + + @Override + public T[] toArray(T[] a) { + return processArray(values.toArray(a)); + } + + private S[] processArray(S[] array) { + for (int i = 0; i < array.length; i++) { + array[i] = (S) processValue(array[i]); + } + return array; + } + } + + private class ProxyEntry implements Entry { + + private Entry entry; + + private ProxyEntry(Entry entry) { + this.entry = entry; + } + + @Override + public String getKey() { + return entry.getKey(); + } + + @Override + public Object getValue() { + return processValue(entry.getValue()); + } + + @Override + public Object setValue(Object value) { + return entry.setValue(value); + } + } + + private class ProxyIterator implements Iterator { + + private Iterator iter; + + public ProxyIterator(Iterator iter) { + this.iter = iter; + } + + @Override + public boolean hasNext() { + return iter.hasNext(); + } + + @Override + public Object next() { + return processValue(iter.next()); + } + + @Override + public void remove() { + iter.remove(); + } + } + + private class ProxyEntryIterator implements Iterator> { + + private Iterator> iter; + + public ProxyEntryIterator(Iterator> iter) { + this.iter = iter; + } + + @Override + public boolean hasNext() { + return iter.hasNext(); + } + + @Override + public Entry next() { + return new ProxyEntry(iter.next()); + } + + @Override + public void remove() { + iter.remove(); + } + } + + private Object processValue(T obj) { + return function.apply(obj); + } +} diff --git a/impl/core/src/main/java/io/serverlessworkflow/impl/expressions/RuntimeDescriptor.java b/impl/core/src/main/java/io/serverlessworkflow/impl/expressions/RuntimeDescriptor.java new file mode 100644 index 00000000..66286632 --- /dev/null +++ b/impl/core/src/main/java/io/serverlessworkflow/impl/expressions/RuntimeDescriptor.java @@ -0,0 +1,21 @@ +/* + * 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.impl.expressions; + +import java.util.Map; + +public record RuntimeDescriptor(String name, String version, Map metadata) {} diff --git a/impl/core/src/main/java/io/serverlessworkflow/impl/expressions/TaskDescriptor.java b/impl/core/src/main/java/io/serverlessworkflow/impl/expressions/TaskDescriptor.java new file mode 100644 index 00000000..a78bffa7 --- /dev/null +++ b/impl/core/src/main/java/io/serverlessworkflow/impl/expressions/TaskDescriptor.java @@ -0,0 +1,39 @@ +/* + * 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.impl.expressions; + +import com.fasterxml.jackson.databind.JsonNode; +import io.serverlessworkflow.api.types.TaskBase; +import io.serverlessworkflow.impl.TaskContext; + +public record TaskDescriptor( + String name, + String reference, + T definition, + JsonNode rawInput, + JsonNode rawOutput, + DateTimeDescriptor startedAt) { + + public static TaskDescriptor of(TaskContext context) { + return new TaskDescriptor( + context.position().last().toString(), + context.position().jsonPointer(), + context.task(), + context.rawInput(), + context.rawOutput(), + DateTimeDescriptor.from(context.startedAt())); + } +} diff --git a/impl/core/src/main/java/io/serverlessworkflow/impl/expressions/WorkflowDescriptor.java b/impl/core/src/main/java/io/serverlessworkflow/impl/expressions/WorkflowDescriptor.java new file mode 100644 index 00000000..f6b906fb --- /dev/null +++ b/impl/core/src/main/java/io/serverlessworkflow/impl/expressions/WorkflowDescriptor.java @@ -0,0 +1,33 @@ +/* + * 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.impl.expressions; + +import com.fasterxml.jackson.databind.JsonNode; +import io.serverlessworkflow.api.types.TaskBase; +import io.serverlessworkflow.api.types.Workflow; +import io.serverlessworkflow.impl.WorkflowContext; + +public record WorkflowDescriptor( + String id, Workflow definition, JsonNode input, DateTimeDescriptor startedAt) { + + public static WorkflowDescriptor of(WorkflowContext context) { + return new WorkflowDescriptor( + context.instance().id(), + context.definition().workflow(), + context.instance().input(), + DateTimeDescriptor.from(context.instance().startedAt())); + } +} diff --git a/impl/src/main/java/io/serverlessworkflow/impl/JsonUtils.java b/impl/core/src/main/java/io/serverlessworkflow/impl/json/JsonUtils.java similarity index 84% rename from impl/src/main/java/io/serverlessworkflow/impl/JsonUtils.java rename to impl/core/src/main/java/io/serverlessworkflow/impl/json/JsonUtils.java index b00b14f1..0726c2be 100644 --- a/impl/src/main/java/io/serverlessworkflow/impl/JsonUtils.java +++ b/impl/core/src/main/java/io/serverlessworkflow/impl/json/JsonUtils.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package io.serverlessworkflow.impl; +package io.serverlessworkflow.impl.json; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.JsonNode; @@ -36,9 +36,16 @@ import java.math.BigInteger; import java.util.ArrayList; import java.util.Collection; +import java.util.Collections; +import java.util.EnumSet; import java.util.HashMap; import java.util.Map; +import java.util.Set; +import java.util.function.BiConsumer; +import java.util.function.BinaryOperator; import java.util.function.Function; +import java.util.function.Supplier; +import java.util.stream.Collector; public class JsonUtils { @@ -48,6 +55,38 @@ public static ObjectMapper mapper() { return mapper; } + public static Collector arrayNodeCollector() { + return new Collector() { + @Override + public BiConsumer accumulator() { + return (arrayNode, item) -> arrayNode.add(item); + } + + @Override + public Set characteristics() { + return Collections.unmodifiableSet(EnumSet.of(Collector.Characteristics.IDENTITY_FINISH)); + } + + @Override + public BinaryOperator combiner() { + return (r1, r2) -> { + r1.addAll(r2); + return r1; + }; + } + + @Override + public Function finisher() { + return arrayNode -> arrayNode; + } + + @Override + public Supplier supplier() { + return () -> mapper.createArrayNode(); + } + }; + } + /* * Implementation note: * Although we can use directly ObjectMapper.convertValue for implementing fromValue and toJavaValue methods, @@ -87,6 +126,10 @@ public static JsonNode fromValue(Object value) { } } + public static Object toJavaValue(Object object) { + return object instanceof JsonNode ? toJavaValue((JsonNode) object) : object; + } + public static JsonNode fromString(String value) { String trimmedValue = value.trim(); if (trimmedValue.startsWith("{") && trimmedValue.endsWith("}")) { @@ -201,7 +244,7 @@ private static ArrayNode mapToArray(Collection collection, ArrayNode arrayNod return arrayNode; } - static ObjectNode object() { + public static ObjectNode object() { return mapper.createObjectNode(); } diff --git a/impl/src/main/java/io/serverlessworkflow/impl/MergeUtils.java b/impl/core/src/main/java/io/serverlessworkflow/impl/json/MergeUtils.java similarity index 98% rename from impl/src/main/java/io/serverlessworkflow/impl/MergeUtils.java rename to impl/core/src/main/java/io/serverlessworkflow/impl/json/MergeUtils.java index 8c1ec1de..a3615d35 100644 --- a/impl/src/main/java/io/serverlessworkflow/impl/MergeUtils.java +++ b/impl/core/src/main/java/io/serverlessworkflow/impl/json/MergeUtils.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package io.serverlessworkflow.impl; +package io.serverlessworkflow.impl.json; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.node.ArrayNode; diff --git a/impl/core/src/main/java/io/serverlessworkflow/impl/jsonschema/DefaultSchemaValidator.java b/impl/core/src/main/java/io/serverlessworkflow/impl/jsonschema/DefaultSchemaValidator.java new file mode 100644 index 00000000..8982908f --- /dev/null +++ b/impl/core/src/main/java/io/serverlessworkflow/impl/jsonschema/DefaultSchemaValidator.java @@ -0,0 +1,42 @@ +/* + * 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.impl.jsonschema; + +import com.fasterxml.jackson.databind.JsonNode; +import com.networknt.schema.JsonSchema; +import com.networknt.schema.JsonSchemaFactory; +import com.networknt.schema.SpecVersion.VersionFlag; +import com.networknt.schema.ValidationMessage; +import java.util.Set; + +public class DefaultSchemaValidator implements SchemaValidator { + + private final JsonSchema schemaObject; + + public DefaultSchemaValidator(JsonNode jsonNode) { + this.schemaObject = JsonSchemaFactory.getInstance(VersionFlag.V7).getSchema(jsonNode); + } + + @Override + public void validate(JsonNode node) { + Set report = schemaObject.validate(node); + if (!report.isEmpty()) { + StringBuilder sb = new StringBuilder("There are JsonSchema validation errors:"); + report.forEach(m -> sb.append(System.lineSeparator()).append(m.getMessage())); + throw new IllegalArgumentException(sb.toString()); + } + } +} diff --git a/impl/core/src/main/java/io/serverlessworkflow/impl/jsonschema/DefaultSchemaValidatorFactory.java b/impl/core/src/main/java/io/serverlessworkflow/impl/jsonschema/DefaultSchemaValidatorFactory.java new file mode 100644 index 00000000..0f74e433 --- /dev/null +++ b/impl/core/src/main/java/io/serverlessworkflow/impl/jsonschema/DefaultSchemaValidatorFactory.java @@ -0,0 +1,34 @@ +/* + * 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.impl.jsonschema; + +import com.fasterxml.jackson.databind.JsonNode; + +public class DefaultSchemaValidatorFactory implements SchemaValidatorFactory { + + private DefaultSchemaValidatorFactory() {} + + private static final DefaultSchemaValidatorFactory instance = new DefaultSchemaValidatorFactory(); + + public static DefaultSchemaValidatorFactory get() { + return instance; + } + + @Override + public SchemaValidator getValidator(JsonNode node) { + return new DefaultSchemaValidator(node); + } +} diff --git a/impl/core/src/main/java/io/serverlessworkflow/impl/jsonschema/SchemaValidator.java b/impl/core/src/main/java/io/serverlessworkflow/impl/jsonschema/SchemaValidator.java new file mode 100644 index 00000000..d86a582f --- /dev/null +++ b/impl/core/src/main/java/io/serverlessworkflow/impl/jsonschema/SchemaValidator.java @@ -0,0 +1,22 @@ +/* + * 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.impl.jsonschema; + +import com.fasterxml.jackson.databind.JsonNode; + +public interface SchemaValidator { + void validate(JsonNode node); +} diff --git a/impl/core/src/main/java/io/serverlessworkflow/impl/jsonschema/SchemaValidatorFactory.java b/impl/core/src/main/java/io/serverlessworkflow/impl/jsonschema/SchemaValidatorFactory.java new file mode 100644 index 00000000..52c29584 --- /dev/null +++ b/impl/core/src/main/java/io/serverlessworkflow/impl/jsonschema/SchemaValidatorFactory.java @@ -0,0 +1,22 @@ +/* + * 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.impl.jsonschema; + +import com.fasterxml.jackson.databind.JsonNode; + +public interface SchemaValidatorFactory { + SchemaValidator getValidator(JsonNode node); +} diff --git a/impl/core/src/main/java/io/serverlessworkflow/impl/resources/ClasspathResource.java b/impl/core/src/main/java/io/serverlessworkflow/impl/resources/ClasspathResource.java new file mode 100644 index 00000000..dad39237 --- /dev/null +++ b/impl/core/src/main/java/io/serverlessworkflow/impl/resources/ClasspathResource.java @@ -0,0 +1,37 @@ +/* + * 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.impl.resources; + +import java.io.InputStream; + +public class ClasspathResource implements StaticResource { + + private String path; + + public ClasspathResource(String path) { + this.path = path; + } + + @Override + public InputStream open() { + return Thread.currentThread().getContextClassLoader().getResourceAsStream(path); + } + + @Override + public String name() { + return path; + } +} diff --git a/impl/core/src/main/java/io/serverlessworkflow/impl/resources/DefaultResourceLoader.java b/impl/core/src/main/java/io/serverlessworkflow/impl/resources/DefaultResourceLoader.java new file mode 100644 index 00000000..a23a9224 --- /dev/null +++ b/impl/core/src/main/java/io/serverlessworkflow/impl/resources/DefaultResourceLoader.java @@ -0,0 +1,105 @@ +/* + * 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.impl.resources; + +import io.serverlessworkflow.api.types.Endpoint; +import io.serverlessworkflow.api.types.EndpointUri; +import io.serverlessworkflow.api.types.ExternalResource; +import io.serverlessworkflow.api.types.UriTemplate; +import io.serverlessworkflow.impl.WorkflowContext; +import io.serverlessworkflow.impl.expressions.ExpressionFactory; +import java.net.MalformedURLException; +import java.net.URI; +import java.nio.file.Path; +import java.util.Optional; + +public class DefaultResourceLoader implements ResourceLoader { + + private final Optional workflowPath; + + protected DefaultResourceLoader(Path workflowPath) { + this.workflowPath = Optional.ofNullable(workflowPath); + } + + @Override + public StaticResource loadStatic(ExternalResource resource) { + return processEndpoint(resource.getEndpoint()); + } + + @Override + public DynamicResource loadDynamic( + WorkflowContext workflow, ExternalResource resource, ExpressionFactory factory) { + throw new UnsupportedOperationException("Dynamic loading of resources is not suppported"); + } + + private StaticResource buildFromString(String uri) { + return fileResource(uri); + } + + private StaticResource fileResource(String pathStr) { + Path path = Path.of(pathStr); + if (path.isAbsolute()) { + return new FileResource(path); + } else { + return workflowPath + .map(p -> new FileResource(p.resolve(path))) + .orElseGet(() -> new ClasspathResource(pathStr)); + } + } + + private StaticResource buildFromURI(URI uri) { + String scheme = uri.getScheme(); + if (scheme == null || scheme.equalsIgnoreCase("file")) { + return fileResource(uri.getPath()); + } else if (scheme.equalsIgnoreCase("http") || scheme.equalsIgnoreCase("https")) { + try { + return new HttpResource(uri.toURL()); + } catch (MalformedURLException e) { + throw new IllegalArgumentException(e); + } + } else { + throw new UnsupportedOperationException("Unsupported scheme " + scheme); + } + } + + private StaticResource processEndpoint(Endpoint endpoint) { + if (endpoint.getEndpointConfiguration() != null) { + EndpointUri uri = endpoint.getEndpointConfiguration().getUri(); + if (uri.getLiteralEndpointURI() != null) { + return getURI(uri.getLiteralEndpointURI()); + } else if (uri.getExpressionEndpointURI() != null) { + throw new UnsupportedOperationException( + "Expression not supported for loading a static resource"); + } + } else if (endpoint.getRuntimeExpression() != null) { + throw new UnsupportedOperationException( + "Expression not supported for loading a static resource"); + } else if (endpoint.getUriTemplate() != null) { + return getURI(endpoint.getUriTemplate()); + } + throw new IllegalArgumentException("Invalid endpoint definition " + endpoint); + } + + private StaticResource getURI(UriTemplate template) { + if (template.getLiteralUri() != null) { + return buildFromURI(template.getLiteralUri()); + } else if (template.getLiteralUriTemplate() != null) { + return buildFromString(template.getLiteralUriTemplate()); + } else { + throw new IllegalStateException("Invalid endpoint definition" + template); + } + } +} diff --git a/impl/core/src/main/java/io/serverlessworkflow/impl/resources/DefaultResourceLoaderFactory.java b/impl/core/src/main/java/io/serverlessworkflow/impl/resources/DefaultResourceLoaderFactory.java new file mode 100644 index 00000000..60717e1d --- /dev/null +++ b/impl/core/src/main/java/io/serverlessworkflow/impl/resources/DefaultResourceLoaderFactory.java @@ -0,0 +1,34 @@ +/* + * 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.impl.resources; + +import java.nio.file.Path; + +public class DefaultResourceLoaderFactory implements ResourceLoaderFactory { + + public static final ResourceLoaderFactory get() { + return factory; + } + + private static final ResourceLoaderFactory factory = new DefaultResourceLoaderFactory(); + + private DefaultResourceLoaderFactory() {} + + @Override + public ResourceLoader getResourceLoader(Path path) { + return new DefaultResourceLoader(path); + } +} diff --git a/impl/core/src/main/java/io/serverlessworkflow/impl/resources/DynamicResource.java b/impl/core/src/main/java/io/serverlessworkflow/impl/resources/DynamicResource.java new file mode 100644 index 00000000..ee9432c0 --- /dev/null +++ b/impl/core/src/main/java/io/serverlessworkflow/impl/resources/DynamicResource.java @@ -0,0 +1,26 @@ +/* + * 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.impl.resources; + +import com.fasterxml.jackson.databind.JsonNode; +import io.serverlessworkflow.impl.TaskContext; +import io.serverlessworkflow.impl.WorkflowContext; +import java.io.InputStream; +import java.util.Optional; + +public interface DynamicResource { + InputStream open(WorkflowContext workflow, Optional> task, JsonNode input); +} diff --git a/impl/core/src/main/java/io/serverlessworkflow/impl/resources/FileResource.java b/impl/core/src/main/java/io/serverlessworkflow/impl/resources/FileResource.java new file mode 100644 index 00000000..6358b6ce --- /dev/null +++ b/impl/core/src/main/java/io/serverlessworkflow/impl/resources/FileResource.java @@ -0,0 +1,45 @@ +/* + * 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.impl.resources; + +import java.io.IOException; +import java.io.InputStream; +import java.io.UncheckedIOException; +import java.nio.file.Files; +import java.nio.file.Path; + +class FileResource implements StaticResource { + + private Path path; + + public FileResource(Path path) { + this.path = path; + } + + @Override + public InputStream open() { + try { + return Files.newInputStream(path); + } catch (IOException io) { + throw new UncheckedIOException(io); + } + } + + @Override + public String name() { + return path.getFileName().toString(); + } +} diff --git a/impl/core/src/main/java/io/serverlessworkflow/impl/resources/HttpResource.java b/impl/core/src/main/java/io/serverlessworkflow/impl/resources/HttpResource.java new file mode 100644 index 00000000..906e5c83 --- /dev/null +++ b/impl/core/src/main/java/io/serverlessworkflow/impl/resources/HttpResource.java @@ -0,0 +1,43 @@ +/* + * 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.impl.resources; + +import java.io.IOException; +import java.io.InputStream; +import java.io.UncheckedIOException; +import java.net.URL; + +public class HttpResource implements StaticResource { + + private URL url; + + public HttpResource(URL url) { + this.url = url; + } + + @Override + public InputStream open() { + try { + return url.openStream(); + } catch (IOException e) { + throw new UncheckedIOException(e); + } + } + + public String name() { + return url.getFile(); + } +} diff --git a/impl/core/src/main/java/io/serverlessworkflow/impl/resources/ResourceLoader.java b/impl/core/src/main/java/io/serverlessworkflow/impl/resources/ResourceLoader.java new file mode 100644 index 00000000..346856bd --- /dev/null +++ b/impl/core/src/main/java/io/serverlessworkflow/impl/resources/ResourceLoader.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.impl.resources; + +import io.serverlessworkflow.api.types.ExternalResource; +import io.serverlessworkflow.impl.WorkflowContext; +import io.serverlessworkflow.impl.expressions.ExpressionFactory; + +public interface ResourceLoader { + + StaticResource loadStatic(ExternalResource resource); + + DynamicResource loadDynamic( + WorkflowContext context, ExternalResource resource, ExpressionFactory factory); +} diff --git a/impl/core/src/main/java/io/serverlessworkflow/impl/resources/ResourceLoaderFactory.java b/impl/core/src/main/java/io/serverlessworkflow/impl/resources/ResourceLoaderFactory.java new file mode 100644 index 00000000..c064672b --- /dev/null +++ b/impl/core/src/main/java/io/serverlessworkflow/impl/resources/ResourceLoaderFactory.java @@ -0,0 +1,22 @@ +/* + * 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.impl.resources; + +import java.nio.file.Path; + +public interface ResourceLoaderFactory { + ResourceLoader getResourceLoader(Path path); +} diff --git a/impl/core/src/main/java/io/serverlessworkflow/impl/resources/StaticResource.java b/impl/core/src/main/java/io/serverlessworkflow/impl/resources/StaticResource.java new file mode 100644 index 00000000..32443dfc --- /dev/null +++ b/impl/core/src/main/java/io/serverlessworkflow/impl/resources/StaticResource.java @@ -0,0 +1,24 @@ +/* + * 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.impl.resources; + +import java.io.InputStream; + +public interface StaticResource { + InputStream open(); + + String name(); +} diff --git a/impl/core/src/test/java/io/serverlessworkflow/impl/WorkflowDefinitionTest.java b/impl/core/src/test/java/io/serverlessworkflow/impl/WorkflowDefinitionTest.java new file mode 100644 index 00000000..e2a1dbf2 --- /dev/null +++ b/impl/core/src/test/java/io/serverlessworkflow/impl/WorkflowDefinitionTest.java @@ -0,0 +1,178 @@ +/* + * 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.impl; + +import static io.serverlessworkflow.api.WorkflowReader.readWorkflowFromClasspath; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.catchThrowableOfType; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.node.ArrayNode; +import com.fasterxml.jackson.databind.node.ObjectNode; +import io.serverlessworkflow.impl.json.JsonUtils; +import java.io.IOException; +import java.time.Instant; +import java.util.Arrays; +import java.util.Map; +import java.util.function.Consumer; +import java.util.stream.Stream; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class WorkflowDefinitionTest { + + private static WorkflowApplication appl; + private static Logger logger = LoggerFactory.getLogger(WorkflowDefinitionTest.class); + private static Instant before; + + @BeforeAll + static void init() { + appl = WorkflowApplication.builder().build(); + before = Instant.now(); + } + + @ParameterizedTest + @MethodSource("provideParameters") + void testWorkflowExecution(String fileName, Consumer assertions) + throws IOException { + assertions.accept(appl.workflowDefinition(readWorkflowFromClasspath(fileName))); + } + + private static Stream provideParameters() { + return Stream.of( + args( + "switch-then-string.yaml", + Map.of("orderType", "electronic"), + o -> + assertThat(o.output()) + .isEqualTo( + Map.of( + "orderType", "electronic", "validate", true, "status", "fulfilled"))), + args( + "switch-then-string.yaml", + Map.of("orderType", "physical"), + o -> + assertThat(o.output()) + .isEqualTo( + Map.of( + "orderType", + "physical", + "inventory", + "clear", + "items", + 1, + "address", + "Elmer St"))), + args( + "switch-then-string.yaml", + Map.of("orderType", "unknown"), + o -> + assertThat(o.output()) + .isEqualTo( + Map.of( + "orderType", + "unknown", + "log", + "warn", + "message", + "something's wrong"))), + args( + "for-sum.yaml", + Map.of("input", Arrays.asList(1, 2, 3)), + o -> assertThat(o.output()).isEqualTo(6)), + args( + "for-collect.yaml", + Map.of("input", Arrays.asList(1, 2, 3)), + o -> + assertThat(o.output()) + .isEqualTo( + Map.of("input", Arrays.asList(1, 2, 3), "output", Arrays.asList(2, 4, 6)))), + args( + "simple-expression.yaml", + Map.of("input", Arrays.asList(1, 2, 3)), + WorkflowDefinitionTest::checkSpecialKeywords), + args( + "raise-inline copy.yaml", + WorkflowDefinitionTest::checkWorkflowException, + WorkflowException.class), + args( + "raise-reusable.yaml", + WorkflowDefinitionTest::checkWorkflowException, + WorkflowException.class), + args( + "fork.yaml", + Map.of(), + o -> + assertThat(((ObjectNode) o.outputAsJsonNode()).get("patientId").asText()) + .isIn("John", "Smith")), + args("fork-no-compete.yaml", Map.of(), WorkflowDefinitionTest::checkNotCompeteOuput)); + } + + private static Arguments args( + String fileName, Map input, Consumer instance) { + return Arguments.of( + fileName, (Consumer) d -> instance.accept(d.execute(input))); + } + + private static Arguments args( + String fileName, Consumer consumer, Class clazz) { + return Arguments.of( + fileName, + (Consumer) + d -> consumer.accept(catchThrowableOfType(clazz, () -> d.execute(Map.of())))); + } + + private static void checkNotCompeteOuput(WorkflowInstance instance) { + JsonNode out = instance.outputAsJsonNode(); + logger.debug("Output is {}", out); + assertThat(out).isInstanceOf(ArrayNode.class); + assertThat(out).hasSize(2); + ArrayNode array = (ArrayNode) out; + assertThat(array) + .containsExactlyInAnyOrder( + createObjectNode("callNurse", "patientId", "John", "room", 1), + createObjectNode("callDoctor", "patientId", "Smith", "room", 2)); + } + + private static JsonNode createObjectNode( + String parent, String key1, String value1, String key2, int value2) { + return JsonUtils.mapper() + .createObjectNode() + .set(parent, JsonUtils.mapper().createObjectNode().put(key1, value1).put(key2, value2)); + } + + private static void checkWorkflowException(WorkflowException ex) { + assertThat(ex.getWorflowError().type()) + .isEqualTo("https://serverlessworkflow.io/errors/not-implemented"); + assertThat(ex.getWorflowError().status()).isEqualTo(500); + assertThat(ex.getWorflowError().title()).isEqualTo("Not Implemented"); + assertThat(ex.getWorflowError().details()).contains("raise-not-implemented"); + assertThat(ex.getWorflowError().instance()).isEqualTo("do/0/notImplemented"); + } + + private static void checkSpecialKeywords(WorkflowInstance obj) { + Map result = (Map) obj.output(); + assertThat(Instant.ofEpochMilli((long) result.get("startedAt"))) + .isAfterOrEqualTo(before) + .isBeforeOrEqualTo(Instant.now()); + assertThat(result.get("id").toString()).hasSize(26); + assertThat(result.get("version").toString()).contains("alpha"); + } +} diff --git a/impl/core/src/test/resources/for-collect.yaml b/impl/core/src/test/resources/for-collect.yaml new file mode 100644 index 00000000..7bcc48c2 --- /dev/null +++ b/impl/core/src/test/resources/for-collect.yaml @@ -0,0 +1,17 @@ +document: + dsl: '1.0.0-alpha5' + namespace: test + name: for-collect-example + version: '0.1.0' +do: + - sumAll: + for: + each: number + in: .input + at: index + input: + from: '{input: .input, output: []}' + do: + - sumIndex: + output: + as: .output+=[$number+$index+1] \ No newline at end of file diff --git a/impl/core/src/test/resources/for-sum.yaml b/impl/core/src/test/resources/for-sum.yaml new file mode 100644 index 00000000..e0fe106b --- /dev/null +++ b/impl/core/src/test/resources/for-sum.yaml @@ -0,0 +1,19 @@ +document: + dsl: '1.0.0-alpha5' + namespace: test + name: for-sum-example + version: '0.1.0' +do: + - initCounter: + set: + counter: 0 + - sumAll: + for: + each: number + in: .input + do: + - accumulate: + output: + as: .counter+=$number + output: + as: .counter \ No newline at end of file diff --git a/impl/core/src/test/resources/fork-no-compete.yaml b/impl/core/src/test/resources/fork-no-compete.yaml new file mode 100644 index 00000000..ee882f13 --- /dev/null +++ b/impl/core/src/test/resources/fork-no-compete.yaml @@ -0,0 +1,28 @@ +document: + dsl: '1.0.0-alpha5' + namespace: test + name: fork-example + version: '0.1.0' +do: + - callSomeone: + fork: + compete: false + branches: + - callNurse: + do: + - waitForNurse: + wait: + milliseconds: 500 + - nurseArrived: + set: + patientId: John + room: 1 + - callDoctor: + do: + - waitForDoctor: + wait: + milliseconds: 499 + - doctorArrived: + set: + patientId: Smith + room: 2 \ No newline at end of file diff --git a/impl/core/src/test/resources/fork.yaml b/impl/core/src/test/resources/fork.yaml new file mode 100644 index 00000000..e9f3e7a3 --- /dev/null +++ b/impl/core/src/test/resources/fork.yaml @@ -0,0 +1,18 @@ +document: + dsl: '1.0.0-alpha5' + namespace: test + name: fork-no-compete + version: '0.1.0' +do: + - callSomeone: + fork: + compete: true + branches: + - callNurse: + set: + patientId: John + room: 1 + - callDoctor: + set: + patientId: Smith + room: 2 \ No newline at end of file diff --git a/impl/core/src/test/resources/raise-inline copy.yaml b/impl/core/src/test/resources/raise-inline copy.yaml new file mode 100644 index 00000000..b4bcac88 --- /dev/null +++ b/impl/core/src/test/resources/raise-inline copy.yaml @@ -0,0 +1,13 @@ +document: + dsl: '1.0.0-alpha5' + namespace: test + name: raise-not-implemented + version: '0.1.0' +do: + - notImplemented: + raise: + error: + type: https://serverlessworkflow.io/errors/not-implemented + status: 500 + title: Not Implemented + detail: ${ "The workflow '\( $workflow.definition.document.name ):\( $workflow.definition.document.version )' is a work in progress and cannot be run yet" } \ No newline at end of file diff --git a/impl/core/src/test/resources/raise-reusable.yaml b/impl/core/src/test/resources/raise-reusable.yaml new file mode 100644 index 00000000..6955bd05 --- /dev/null +++ b/impl/core/src/test/resources/raise-reusable.yaml @@ -0,0 +1,16 @@ +document: + dsl: '1.0.0-alpha5' + namespace: test + name: raise-not-implemented-reusable + version: '0.1.0' +use: + errors: + notImplemented: + type: https://serverlessworkflow.io/errors/not-implemented + status: 500 + title: Not Implemented + detail: ${ "The workflow '\( $workflow.definition.document.name ):\( $workflow.definition.document.version )' is a work in progress and cannot be run yet" } +do: + - notImplemented: + raise: + error: notImplemented \ No newline at end of file diff --git a/impl/core/src/test/resources/simple-expression.yaml b/impl/core/src/test/resources/simple-expression.yaml new file mode 100644 index 00000000..4e240d6b --- /dev/null +++ b/impl/core/src/test/resources/simple-expression.yaml @@ -0,0 +1,11 @@ +document: + dsl: '1.0.0-alpha5' + namespace: test + name: simple-expression + version: '0.1.0' +do: + - useExpression: + set: + startedAt: ${$task.startedAt.epoch.milliseconds} + id : ${$workflow.id} + version: ${$runtime.version} \ No newline at end of file diff --git a/impl/core/src/test/resources/switch-then-string.yaml b/impl/core/src/test/resources/switch-then-string.yaml new file mode 100644 index 00000000..a35ebd45 --- /dev/null +++ b/impl/core/src/test/resources/switch-then-string.yaml @@ -0,0 +1,45 @@ +document: + dsl: 1.0.0-alpha5 + namespace: test + name: switch + version: 0.1.0 +do: + - processOrder: + switch: + - case1: + when: .orderType == "electronic" + then: processElectronicOrder + - case2: + when: .orderType == "physical" + then: processPhysicalOrder + - default: + then: handleUnknownOrderType + - processElectronicOrder: + do: + - validatePayment: + set: + validate: true + - fulfillOrder: + set: + status: fulfilled + then: exit + - processPhysicalOrder: + do: + - checkInventory: + set: + inventory: clear + - packItems: + set: + items: 1 + - scheduleShipping: + set: + address: Elmer St + then: exit + - handleUnknownOrderType: + do: + - logWarning: + set: + log: warn + - notifyAdmin: + set: + message: something's wrong diff --git a/impl/http/.checkstyle b/impl/http/.checkstyle new file mode 100644 index 00000000..a33f7d91 --- /dev/null +++ b/impl/http/.checkstyle @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/impl/http/pom.xml b/impl/http/pom.xml new file mode 100644 index 00000000..b67f2cd5 --- /dev/null +++ b/impl/http/pom.xml @@ -0,0 +1,43 @@ + + 4.0.0 + + io.serverlessworkflow + serverlessworkflow-impl + 7.0.0-alpha5.1 + + serverlessworkflow-impl-http + + + org.glassfish.jersey.core + jersey-client + + + org.glassfish.jersey.media + jersey-media-json-jackson + + + io.serverlessworkflow + serverlessworkflow-impl-core + + + org.junit.jupiter + junit-jupiter-api + test + + + org.junit.jupiter + junit-jupiter-engine + test + + + org.junit.jupiter + junit-jupiter-params + test + + + org.assertj + assertj-core + test + + + \ No newline at end of file diff --git a/impl/http/src/main/java/io/serverlessworkflow/impl/executors/HttpExecutor.java b/impl/http/src/main/java/io/serverlessworkflow/impl/executors/HttpExecutor.java new file mode 100644 index 00000000..13e61d35 --- /dev/null +++ b/impl/http/src/main/java/io/serverlessworkflow/impl/executors/HttpExecutor.java @@ -0,0 +1,165 @@ +/* + * 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.impl.executors; + +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.JsonNode; +import io.serverlessworkflow.api.types.CallHTTP; +import io.serverlessworkflow.api.types.Endpoint; +import io.serverlessworkflow.api.types.EndpointUri; +import io.serverlessworkflow.api.types.HTTPArguments; +import io.serverlessworkflow.api.types.TaskBase; +import io.serverlessworkflow.api.types.UriTemplate; +import io.serverlessworkflow.impl.TaskContext; +import io.serverlessworkflow.impl.WorkflowContext; +import io.serverlessworkflow.impl.WorkflowDefinition; +import io.serverlessworkflow.impl.WorkflowError; +import io.serverlessworkflow.impl.WorkflowException; +import io.serverlessworkflow.impl.expressions.Expression; +import io.serverlessworkflow.impl.expressions.ExpressionFactory; +import io.serverlessworkflow.impl.expressions.ExpressionUtils; +import io.serverlessworkflow.impl.json.JsonUtils; +import jakarta.ws.rs.HttpMethod; +import jakarta.ws.rs.WebApplicationException; +import jakarta.ws.rs.client.Client; +import jakarta.ws.rs.client.ClientBuilder; +import jakarta.ws.rs.client.Entity; +import jakarta.ws.rs.client.Invocation.Builder; +import jakarta.ws.rs.client.WebTarget; +import java.util.Map; +import java.util.Map.Entry; + +public class HttpExecutor implements CallableTask { + + private static final Client client = ClientBuilder.newClient(); + + private TargetSupplier targetSupplier; + private Map headersMap; + private Map queryMap; + private RequestSupplier requestFunction; + + @FunctionalInterface + private interface TargetSupplier { + WebTarget apply(WorkflowContext workflow, TaskContext task, JsonNode node); + } + + @FunctionalInterface + private interface RequestSupplier { + JsonNode apply(Builder request, WorkflowContext workflow, TaskContext task, JsonNode node); + } + + @Override + public void init(CallHTTP task, WorkflowDefinition definition) { + HTTPArguments httpArgs = task.getWith(); + this.targetSupplier = getTargetSupplier(httpArgs.getEndpoint(), definition.expressionFactory()); + this.headersMap = + httpArgs.getHeaders() != null + ? ExpressionUtils.buildExpressionMap( + httpArgs.getHeaders().getAdditionalProperties(), definition.expressionFactory()) + : Map.of(); + this.queryMap = + httpArgs.getQuery() != null + ? ExpressionUtils.buildExpressionMap( + httpArgs.getQuery().getAdditionalProperties(), definition.expressionFactory()) + : Map.of(); + switch (httpArgs.getMethod().toUpperCase()) { + case HttpMethod.POST: + Object body = + ExpressionUtils.buildExpressionObject( + httpArgs.getBody(), definition.expressionFactory()); + this.requestFunction = + (request, workflow, context, node) -> + request.post( + Entity.json( + ExpressionUtils.evaluateExpressionObject(body, workflow, context, node)), + JsonNode.class); + break; + case HttpMethod.GET: + default: + this.requestFunction = (request, w, t, n) -> request.get(JsonNode.class); + } + } + + @Override + public JsonNode apply( + WorkflowContext workflow, TaskContext taskContext, JsonNode input) { + WebTarget target = targetSupplier.apply(workflow, taskContext, input); + for (Entry entry : + ExpressionUtils.evaluateExpressionMap(queryMap, workflow, taskContext, input).entrySet()) { + target = target.queryParam(entry.getKey(), entry.getValue()); + } + Builder request = target.request(); + ExpressionUtils.evaluateExpressionMap(headersMap, workflow, taskContext, input) + .forEach(request::header); + try { + return requestFunction.apply(request, workflow, taskContext, input); + } catch (WebApplicationException exception) { + throw new WorkflowException( + WorkflowError.communication(exception.getResponse().getStatus(), taskContext, exception) + .build()); + } + } + + @Override + public boolean accept(Class clazz) { + return clazz.equals(CallHTTP.class); + } + + private static TargetSupplier getTargetSupplier( + Endpoint endpoint, ExpressionFactory expressionFactory) { + if (endpoint.getEndpointConfiguration() != null) { + EndpointUri uri = endpoint.getEndpointConfiguration().getUri(); + if (uri.getLiteralEndpointURI() != null) { + return getURISupplier(uri.getLiteralEndpointURI()); + } else if (uri.getExpressionEndpointURI() != null) { + return new ExpressionURISupplier( + expressionFactory.getExpression(uri.getExpressionEndpointURI())); + } + } else if (endpoint.getRuntimeExpression() != null) { + return new ExpressionURISupplier( + expressionFactory.getExpression(endpoint.getRuntimeExpression())); + } else if (endpoint.getUriTemplate() != null) { + return getURISupplier(endpoint.getUriTemplate()); + } + throw new IllegalArgumentException("Invalid endpoint definition " + endpoint); + } + + private static TargetSupplier getURISupplier(UriTemplate template) { + if (template.getLiteralUri() != null) { + return (w, t, n) -> client.target(template.getLiteralUri()); + } else if (template.getLiteralUriTemplate() != null) { + return (w, t, n) -> + client + .target(template.getLiteralUriTemplate()) + .resolveTemplates( + JsonUtils.mapper().convertValue(n, new TypeReference>() {})); + } + throw new IllegalArgumentException("Invalid uritemplate definition " + template); + } + + private static class ExpressionURISupplier implements TargetSupplier { + private Expression expr; + + public ExpressionURISupplier(Expression expr) { + this.expr = expr; + } + + @Override + public WebTarget apply(WorkflowContext workflow, TaskContext task, JsonNode node) { + return client.target(expr.eval(workflow, task, node).asText()); + } + } +} diff --git a/impl/http/src/main/resources/META-INF/services/io.serverlessworkflow.impl.executors.CallableTask b/impl/http/src/main/resources/META-INF/services/io.serverlessworkflow.impl.executors.CallableTask new file mode 100644 index 00000000..7d5e6bf9 --- /dev/null +++ b/impl/http/src/main/resources/META-INF/services/io.serverlessworkflow.impl.executors.CallableTask @@ -0,0 +1 @@ +io.serverlessworkflow.impl.executors.HttpExecutor \ No newline at end of file diff --git a/impl/http/src/test/java/io/serverlessworkflow/impl/HTTPWorkflowDefinitionTest.java b/impl/http/src/test/java/io/serverlessworkflow/impl/HTTPWorkflowDefinitionTest.java new file mode 100644 index 00000000..f3d77bdd --- /dev/null +++ b/impl/http/src/test/java/io/serverlessworkflow/impl/HTTPWorkflowDefinitionTest.java @@ -0,0 +1,93 @@ +/* + * 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.impl; + +import static io.serverlessworkflow.api.WorkflowReader.readWorkflowFromClasspath; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.catchThrowableOfType; + +import java.io.IOException; +import java.util.Map; +import java.util.stream.Stream; +import org.assertj.core.api.Condition; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; +import org.junit.jupiter.params.provider.ValueSource; + +public class HTTPWorkflowDefinitionTest { + + private static WorkflowApplication appl; + + @BeforeAll + static void init() { + appl = WorkflowApplication.builder().build(); + } + + @ParameterizedTest + @MethodSource("provideParameters") + void testWorkflowExecution(String fileName, Object input, Condition condition) + throws IOException { + assertThat(appl.workflowDefinition(readWorkflowFromClasspath(fileName)).execute(input).output()) + .is(condition); + } + + @ParameterizedTest + @ValueSource( + strings = { + "call-http-query-parameters.yaml", + "call-http-query-parameters-external-schema.yaml" + }) + void testWrongSchema(String fileName) { + IllegalArgumentException exception = + catchThrowableOfType( + IllegalArgumentException.class, + () -> appl.workflowDefinition(readWorkflowFromClasspath(fileName)).execute(Map.of())); + assertThat(exception) + .isNotNull() + .hasMessageContaining("There are JsonSchema validation errors"); + } + + private static Stream provideParameters() { + Map petInput = Map.of("petId", 10); + Condition petCondition = + new Condition<>( + o -> ((Map) o).containsKey("photoUrls"), "callHttpCondition"); + return Stream.of( + Arguments.of("callGetHttp.yaml", petInput, petCondition), + Arguments.of( + "callGetHttp.yaml", + Map.of("petId", "-1"), + new Condition<>( + o -> ((Map) o).containsKey("petId"), "notFoundCondition")), + Arguments.of("call-http-endpoint-interpolation.yaml", petInput, petCondition), + Arguments.of( + "call-http-query-parameters.yaml", + Map.of("searchQuery", "R2-D2"), + new Condition<>( + o -> ((Map) o).get("count").equals(1), "R2D2Condition")), + Arguments.of( + "call-http-query-parameters-external-schema.yaml", + Map.of("searchQuery", "Luke Skywalker"), + new Condition<>( + o -> ((Map) o).get("count").equals(1), "TheRealJediCondition")), + Arguments.of( + "callPostHttp.yaml", + Map.of("name", "Javierito", "status", "available"), + new Condition<>(o -> o.equals("Javierito"), "CallHttpPostCondition"))); + } +} diff --git a/impl/src/test/resources/call-http-endpoint-interpolation.yaml b/impl/http/src/test/resources/call-http-endpoint-interpolation.yaml similarity index 85% rename from impl/src/test/resources/call-http-endpoint-interpolation.yaml rename to impl/http/src/test/resources/call-http-endpoint-interpolation.yaml index 8380a9aa..5c1239f0 100644 --- a/impl/src/test/resources/call-http-endpoint-interpolation.yaml +++ b/impl/http/src/test/resources/call-http-endpoint-interpolation.yaml @@ -1,6 +1,6 @@ document: - dsl: '1.0.0-alpha5' - namespace: examples + dsl: 1.0.0-alpha5 + namespace: test name: call-http-shorthand-endpoint version: '0.1.0' do: diff --git a/impl/http/src/test/resources/call-http-query-parameters-external-schema.yaml b/impl/http/src/test/resources/call-http-query-parameters-external-schema.yaml new file mode 100644 index 00000000..9488592e --- /dev/null +++ b/impl/http/src/test/resources/call-http-query-parameters-external-schema.yaml @@ -0,0 +1,18 @@ +document: + dsl: 1.0.0-alpha2 + namespace: test + name: http-query-params-schema + version: 1.0.0-alpha2 +input: + schema: + resource: + endpoint: schema/searchquery.yaml +do: + - searchStarWarsCharacters: + call: http + with: + method: get + endpoint: https://swapi.dev/api/people/ + query: + search: ${.searchQuery} + diff --git a/impl/http/src/test/resources/call-http-query-parameters.yaml b/impl/http/src/test/resources/call-http-query-parameters.yaml new file mode 100644 index 00000000..d209bf07 --- /dev/null +++ b/impl/http/src/test/resources/call-http-query-parameters.yaml @@ -0,0 +1,23 @@ +document: + dsl: 1.0.0-alpha2 + namespace: test + name: http-query-params + version: 1.0.0-alpha2 +input: + schema: + 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/impl/http/src/test/resources/callGetHttp.yaml b/impl/http/src/test/resources/callGetHttp.yaml new file mode 100644 index 00000000..192b0bcd --- /dev/null +++ b/impl/http/src/test/resources/callGetHttp.yaml @@ -0,0 +1,21 @@ +document: + dsl: 1.0.0-alpha1 + namespace: test + name: http-call-with-response + version: 1.0.0 +do: + - tryGetPet: + try: + - getPet: + call: http + with: + headers: + content-type: application/json + method: get + endpoint: + uri: https://petstore.swagger.io/v2/pet/{petId} + catch: + errors: + with: + type: https://serverlessworkflow.io/spec/1.0.0/errors/communication + status: 404 \ No newline at end of file diff --git a/impl/http/src/test/resources/callPostHttp.yaml b/impl/http/src/test/resources/callPostHttp.yaml new file mode 100644 index 00000000..d66dcfaa --- /dev/null +++ b/impl/http/src/test/resources/callPostHttp.yaml @@ -0,0 +1,28 @@ +document: + dsl: 1.0.0-alpha1 + namespace: test + name: http-call-with-response-output + version: 1.0.0 +do: + - postPet: + call: http + with: + method: post + endpoint: + uri: https://petstore.swagger.io/v2/pet + body: + name: ${.name} + status: ${.status} + output: + as: .id + - getPet: + call: http + with: + method: get + endpoint: + uri: https://petstore.swagger.io/v2/pet/{petId} + input: + from: + petId: ${.} + output: + as: .name \ No newline at end of file diff --git a/impl/http/src/test/resources/schema/searchquery.yaml b/impl/http/src/test/resources/schema/searchquery.yaml new file mode 100644 index 00000000..f6dde131 --- /dev/null +++ b/impl/http/src/test/resources/schema/searchquery.yaml @@ -0,0 +1,6 @@ +type: object +required: + - searchQuery +properties: + searchQuery: + type: string \ No newline at end of file diff --git a/impl/pom.xml b/impl/pom.xml index 4c21cc18..6a380017 100644 --- a/impl/pom.xml +++ b/impl/pom.xml @@ -3,77 +3,40 @@ io.serverlessworkflow serverlessworkflow-parent - 7.0.0-alpha5 + 7.0.0-alpha5.1 serverlessworkflow-impl + pom - 3.1.9 - 1.0.1 + 3.1.9 - - - io.serverlessworkflow - serverlessworkflow-api - 7.0.0-alpha5 - - - org.glassfish.jersey.core - jersey-client - ${version.org.glassfish.jersey} - - - org.glassfish.jersey.media - jersey-media-json-jackson - ${version.org.glassfish.jersey} - - - net.thisptr - jackson-jq - ${version.net.thisptr} - - - org.junit.jupiter - junit-jupiter-api - test - - - org.junit.jupiter - junit-jupiter-engine - test - - - org.junit.jupiter - junit-jupiter-params - test - - - org.assertj - assertj-core - test - - - - - - com.spotify.fmt - fmt-maven-plugin - - src/main/java - src/test/java - false - .*\.java - false - false - - - - - - format - - - - - - + + + + io.serverlessworkflow + serverlessworkflow-impl-core + ${project.version} + + + io.serverlessworkflow + serverlessworkflow-impl-http + ${project.version} + + + org.glassfish.jersey.core + jersey-client + ${version.org.glassfish.jersey} + + + org.glassfish.jersey.media + jersey-media-json-jackson + ${version.org.glassfish.jersey} + + + + + http + core + bom + \ No newline at end of file diff --git a/impl/src/main/java/io/serverlessworkflow/impl/DefaultTaskExecutorFactory.java b/impl/src/main/java/io/serverlessworkflow/impl/DefaultTaskExecutorFactory.java deleted file mode 100644 index fab07d8c..00000000 --- a/impl/src/main/java/io/serverlessworkflow/impl/DefaultTaskExecutorFactory.java +++ /dev/null @@ -1,51 +0,0 @@ -/* - * 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.impl; - -import io.serverlessworkflow.api.types.CallTask; -import io.serverlessworkflow.api.types.Task; -import io.serverlessworkflow.api.types.TaskBase; -import io.serverlessworkflow.impl.jq.JQExpressionFactory; - -public class DefaultTaskExecutorFactory implements TaskExecutorFactory { - - private final ExpressionFactory exprFactory; - - private static TaskExecutorFactory instance = - new DefaultTaskExecutorFactory(JQExpressionFactory.get()); - - public static TaskExecutorFactory get() { - return instance; - } - - public static TaskExecutorFactory get(ExpressionFactory factory) { - return new DefaultTaskExecutorFactory(factory); - } - - protected DefaultTaskExecutorFactory(ExpressionFactory exprFactory) { - this.exprFactory = exprFactory; - } - - public TaskExecutor getTaskExecutor(Task task) { - if (task.getCallTask() != null) { - CallTask callTask = task.getCallTask(); - if (callTask.getCallHTTP() != null) { - return new HttpExecutor(callTask.getCallHTTP(), exprFactory); - } - } - throw new UnsupportedOperationException(task.get().getClass().getName() + " not supported yet"); - } -} diff --git a/impl/src/main/java/io/serverlessworkflow/impl/HttpExecutor.java b/impl/src/main/java/io/serverlessworkflow/impl/HttpExecutor.java deleted file mode 100644 index e2c2c42f..00000000 --- a/impl/src/main/java/io/serverlessworkflow/impl/HttpExecutor.java +++ /dev/null @@ -1,139 +0,0 @@ -/* - * 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.impl; - -import com.fasterxml.jackson.core.type.TypeReference; -import com.fasterxml.jackson.databind.JsonNode; -import io.serverlessworkflow.api.types.CallHTTP; -import io.serverlessworkflow.api.types.Endpoint; -import io.serverlessworkflow.api.types.EndpointUri; -import io.serverlessworkflow.api.types.HTTPArguments; -import io.serverlessworkflow.api.types.UriTemplate; -import io.serverlessworkflow.api.types.WithHTTPHeaders; -import io.serverlessworkflow.api.types.WithHTTPQuery; -import jakarta.ws.rs.HttpMethod; -import jakarta.ws.rs.client.Client; -import jakarta.ws.rs.client.ClientBuilder; -import jakarta.ws.rs.client.Entity; -import jakarta.ws.rs.client.Invocation.Builder; -import jakarta.ws.rs.client.WebTarget; -import java.net.URI; -import java.util.Map; -import java.util.Map.Entry; -import java.util.function.Function; - -public class HttpExecutor extends AbstractTaskExecutor { - - private static final Client client = ClientBuilder.newClient(); - - private final Function targetSupplier; - - public HttpExecutor(CallHTTP task, ExpressionFactory factory) { - super(task, factory); - this.targetSupplier = getTargetSupplier(task.getWith().getEndpoint()); - } - - @Override - protected JsonNode internalExecute(JsonNode node) { - HTTPArguments httpArgs = task.getWith(); - WithHTTPQuery query = httpArgs.getQuery(); - WebTarget target = targetSupplier.apply(node); - if (query != null) { - for (Entry entry : query.getAdditionalProperties().entrySet()) { - target = target.queryParam(entry.getKey(), entry.getValue()); - } - } - Builder request = target.request(); - WithHTTPHeaders headers = httpArgs.getHeaders(); - if (headers != null) { - headers.getAdditionalProperties().forEach(request::header); - } - switch (httpArgs.getMethod().toUpperCase()) { - case HttpMethod.GET: - default: - return request.get(JsonNode.class); - case HttpMethod.POST: - return request.post(Entity.json(httpArgs.getBody()), JsonNode.class); - } - } - - private Function getTargetSupplier(Endpoint endpoint) { - if (endpoint.getEndpointConfiguration() != null) { - EndpointUri uri = endpoint.getEndpointConfiguration().getUri(); - if (uri.getLiteralEndpointURI() != null) { - return getURISupplier(uri.getLiteralEndpointURI()); - } else if (uri.getExpressionEndpointURI() != null) { - return new ExpressionURISupplier(uri.getExpressionEndpointURI()); - } - } else if (endpoint.getRuntimeExpression() != null) { - return new ExpressionURISupplier(endpoint.getRuntimeExpression()); - } else if (endpoint.getUriTemplate() != null) { - return getURISupplier(endpoint.getUriTemplate()); - } - throw new IllegalArgumentException("Invalid endpoint definition " + endpoint); - } - - private Function getURISupplier(UriTemplate template) { - if (template.getLiteralUri() != null) { - return new URISupplier(template.getLiteralUri()); - } else if (template.getLiteralUriTemplate() != null) { - return new URITemplateSupplier(template.getLiteralUriTemplate()); - } - throw new IllegalArgumentException("Invalid uritemplate definition " + template); - } - - private class URISupplier implements Function { - private final URI uri; - - public URISupplier(URI uri) { - this.uri = uri; - } - - @Override - public WebTarget apply(JsonNode input) { - return client.target(uri); - } - } - - private class URITemplateSupplier implements Function { - private final String uri; - - public URITemplateSupplier(String uri) { - this.uri = uri; - } - - @Override - public WebTarget apply(JsonNode input) { - return client - .target(uri) - .resolveTemplates( - JsonUtils.mapper().convertValue(input, new TypeReference>() {})); - } - } - - private class ExpressionURISupplier implements Function { - private Expression expr; - - public ExpressionURISupplier(String expr) { - this.expr = exprFactory.getExpression(expr); - } - - @Override - public WebTarget apply(JsonNode input) { - return client.target(expr.eval(input).asText()); - } - } -} diff --git a/impl/src/main/java/io/serverlessworkflow/impl/WorkflowDefinition.java b/impl/src/main/java/io/serverlessworkflow/impl/WorkflowDefinition.java deleted file mode 100644 index f926a755..00000000 --- a/impl/src/main/java/io/serverlessworkflow/impl/WorkflowDefinition.java +++ /dev/null @@ -1,145 +0,0 @@ -/* - * 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.impl; - -import static io.serverlessworkflow.impl.JsonUtils.*; - -import com.fasterxml.jackson.core.JsonPointer; -import com.fasterxml.jackson.databind.JsonNode; -import io.serverlessworkflow.api.types.TaskBase; -import io.serverlessworkflow.api.types.TaskItem; -import io.serverlessworkflow.api.types.Workflow; -import java.util.Collection; -import java.util.Collections; -import java.util.HashSet; -import java.util.List; -import java.util.Map; -import java.util.concurrent.ConcurrentHashMap; - -public class WorkflowDefinition { - - private WorkflowDefinition( - Workflow workflow, - TaskExecutorFactory taskFactory, - Collection listeners) { - this.workflow = workflow; - this.taskFactory = taskFactory; - this.listeners = listeners; - } - - private final Workflow workflow; - private final Collection listeners; - private final TaskExecutorFactory taskFactory; - private final Map> taskExecutors = - new ConcurrentHashMap<>(); - - public static class Builder { - private final Workflow workflow; - private TaskExecutorFactory taskFactory = DefaultTaskExecutorFactory.get(); - private Collection listeners; - - private Builder(Workflow workflow) { - this.workflow = workflow; - } - - public Builder withListener(WorkflowExecutionListener listener) { - if (listeners == null) { - listeners = new HashSet<>(); - } - listeners.add(listener); - return this; - } - - public Builder withTaskExecutorFactory(TaskExecutorFactory factory) { - this.taskFactory = factory; - return this; - } - - public WorkflowDefinition build() { - return new WorkflowDefinition( - workflow, - taskFactory, - listeners == null - ? Collections.emptySet() - : Collections.unmodifiableCollection(listeners)); - } - } - - public static Builder builder(Workflow workflow) { - return new Builder(workflow); - } - - public WorkflowInstance execute(Object input) { - return new WorkflowInstance(taskFactory, JsonUtils.fromValue(input)); - } - - enum State { - STARTED, - WAITING, - FINISHED - }; - - public class WorkflowInstance { - - private final JsonNode input; - private JsonNode output; - private State state; - - private JsonPointer currentPos; - - private WorkflowInstance(TaskExecutorFactory factory, JsonNode input) { - this.input = input; - this.output = object(); - this.state = State.STARTED; - this.currentPos = JsonPointer.compile("/"); - processDo(workflow.getDo()); - } - - private void processDo(List tasks) { - currentPos = currentPos.appendProperty("do"); - int index = 0; - for (TaskItem task : tasks) { - currentPos = currentPos.appendIndex(index).appendProperty(task.getName()); - listeners.forEach(l -> l.onTaskStarted(currentPos, task.getTask())); - this.output = - MergeUtils.merge( - taskExecutors - .computeIfAbsent(currentPos, k -> taskFactory.getTaskExecutor(task.getTask())) - .apply(input), - output); - listeners.forEach(l -> l.onTaskEnded(currentPos, task.getTask())); - currentPos = currentPos.head().head(); - } - currentPos = currentPos.head(); - } - - public String currentPos() { - return currentPos.toString(); - } - - public State state() { - return state; - } - - public Object output() { - return toJavaValue(output); - } - - public Object outputAsJsonNode() { - return output; - } - } -} diff --git a/impl/src/main/java/io/serverlessworkflow/impl/jq/JQExpression.java b/impl/src/main/java/io/serverlessworkflow/impl/jq/JQExpression.java deleted file mode 100644 index b77f34a2..00000000 --- a/impl/src/main/java/io/serverlessworkflow/impl/jq/JQExpression.java +++ /dev/null @@ -1,251 +0,0 @@ -/* - * 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.impl.jq; - -import com.fasterxml.jackson.databind.JsonNode; -import com.fasterxml.jackson.databind.node.ArrayNode; -import io.serverlessworkflow.impl.Expression; -import io.serverlessworkflow.impl.JsonUtils; -import java.lang.reflect.Field; -import java.util.ArrayList; -import java.util.Collection; -import java.util.HashSet; -import java.util.Map; -import java.util.Optional; -import java.util.concurrent.ConcurrentHashMap; -import java.util.function.Supplier; -import net.thisptr.jackson.jq.Output; -import net.thisptr.jackson.jq.Scope; -import net.thisptr.jackson.jq.Version; -import net.thisptr.jackson.jq.exception.JsonQueryException; -import net.thisptr.jackson.jq.internal.javacc.ExpressionParser; -import net.thisptr.jackson.jq.internal.tree.FunctionCall; -import net.thisptr.jackson.jq.internal.tree.StringInterpolation; -import net.thisptr.jackson.jq.internal.tree.binaryop.BinaryOperatorExpression; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -public class JQExpression implements Expression { - - private static final Logger logger = LoggerFactory.getLogger(JQExpression.class); - private final Map, Collection> - declaredFieldsMap = new ConcurrentHashMap<>(); - private final Map, Collection> - allFieldsMap = new ConcurrentHashMap<>(); - - private final Supplier scope; - private final String expr; - - private net.thisptr.jackson.jq.Expression internalExpr; - private static Field rhsField; - - static { - try { - rhsField = BinaryOperatorExpression.class.getDeclaredField("rhs"); - rhsField.setAccessible(true); - } catch (ReflectiveOperationException e) { - logger.warn("Unexpected exception while resolving rhs field", e); - } - } - - public JQExpression(Supplier scope, String expr, Version version) - throws JsonQueryException { - this.expr = expr; - this.scope = scope; - this.internalExpr = compile(version); - checkFunctionCall(internalExpr); - } - - private net.thisptr.jackson.jq.Expression compile(Version version) throws JsonQueryException { - net.thisptr.jackson.jq.Expression expression; - try { - expression = ExpressionParser.compile(expr, version); - } catch (JsonQueryException ex) { - expression = handleStringInterpolation(version).orElseThrow(() -> ex); - } - checkFunctionCall(expression); - return expression; - } - - private Optional handleStringInterpolation(Version version) { - if (!expr.startsWith("\"")) { - try { - net.thisptr.jackson.jq.Expression expression = - ExpressionParser.compile("\"" + expr + "\"", version); - if (expression instanceof StringInterpolation) { - return Optional.of(expression); - } - } catch (JsonQueryException ex) { - // ignoring it - } - } - return Optional.empty(); - } - - private interface TypedOutput extends Output { - T getResult(); - } - - @SuppressWarnings("unchecked") - private TypedOutput output(Class returnClass) { - TypedOutput out; - if (String.class.isAssignableFrom(returnClass)) { - out = (TypedOutput) new StringOutput(); - } else if (Collection.class.isAssignableFrom(returnClass)) { - out = (TypedOutput) new CollectionOutput(); - } else { - out = (TypedOutput) new JsonNodeOutput(); - } - return out; - } - - private static class StringOutput implements TypedOutput { - StringBuilder sb = new StringBuilder(); - - @Override - public void emit(JsonNode out) throws JsonQueryException { - if (sb.length() > 0) { - sb.append(' '); - } - if (!out.isNull() && out.asText() != null) { - sb.append(out.asText()); - } - } - - @Override - public String getResult() { - return sb.toString(); - } - } - - private static class CollectionOutput implements TypedOutput> { - Collection result = new ArrayList<>(); - - @SuppressWarnings("unchecked") - @Override - public void emit(JsonNode out) throws JsonQueryException { - Object obj = JsonUtils.toJavaValue(out); - if (obj instanceof Collection) result.addAll((Collection) obj); - else { - result.add(obj); - } - } - - @Override - public Collection getResult() { - return result; - } - } - - private static class JsonNodeOutput implements TypedOutput { - - private JsonNode result; - private boolean arrayCreated; - - @Override - public void emit(JsonNode out) throws JsonQueryException { - if (this.result == null) { - this.result = out; - } else if (!arrayCreated) { - ArrayNode newNode = JsonUtils.mapper().createArrayNode(); - newNode.add(this.result).add(out); - this.result = newNode; - arrayCreated = true; - } else { - ((ArrayNode) this.result).add(out); - } - } - - @Override - public JsonNode getResult() { - return result; - } - } - - @Override - public JsonNode eval(JsonNode context) { - TypedOutput output = output(JsonNode.class); - try { - internalExpr.apply(this.scope.get(), context, output); - return output.getResult(); - } catch (JsonQueryException e) { - throw new IllegalArgumentException( - "Unable to evaluate content " + context + " using expr " + expr, e); - } - } - - private void checkFunctionCall(net.thisptr.jackson.jq.Expression toCheck) - throws JsonQueryException { - if (toCheck instanceof FunctionCall) { - toCheck.apply(scope.get(), JsonUtils.mapper().createObjectNode(), out -> {}); - } else if (toCheck instanceof BinaryOperatorExpression) { - if (rhsField != null) { - try { - checkFunctionCall((net.thisptr.jackson.jq.Expression) rhsField.get(toCheck)); - } catch (ReflectiveOperationException e) { - logger.warn( - "Ignoring unexpected error {} while accesing field {} for class{} and expression {}", - e.getMessage(), - rhsField.getName(), - toCheck.getClass(), - expr); - } - } - } else if (toCheck != null) { - for (Field f : getAllExprFields(toCheck)) - try { - checkFunctionCall((net.thisptr.jackson.jq.Expression) f.get(toCheck)); - } catch (ReflectiveOperationException e) { - logger.warn( - "Ignoring unexpected error {} while accesing field {} for class{} and expression {}", - e.getMessage(), - f.getName(), - toCheck.getClass(), - expr); - } - } - } - - private Collection getAllExprFields(net.thisptr.jackson.jq.Expression toCheck) { - return allFieldsMap.computeIfAbsent(toCheck.getClass(), this::getAllExprFields); - } - - private Collection getAllExprFields( - Class clazz) { - Collection fields = new HashSet<>(); - Class currentClass = clazz; - do { - fields.addAll( - declaredFieldsMap.computeIfAbsent( - currentClass.asSubclass(net.thisptr.jackson.jq.Expression.class), - this::getDeclaredExprFields)); - currentClass = currentClass.getSuperclass(); - } while (net.thisptr.jackson.jq.Expression.class.isAssignableFrom(currentClass)); - return fields; - } - - private Collection getDeclaredExprFields( - Class clazz) { - Collection fields = new HashSet<>(); - for (Field f : clazz.getDeclaredFields()) { - if (net.thisptr.jackson.jq.Expression.class.isAssignableFrom(f.getType())) { - f.setAccessible(true); - fields.add(f); - } - } - return fields; - } -} diff --git a/impl/src/test/java/io/serverlessworkflow/impl/WorkflowDefinitionTest.java b/impl/src/test/java/io/serverlessworkflow/impl/WorkflowDefinitionTest.java deleted file mode 100644 index 66ef5d86..00000000 --- a/impl/src/test/java/io/serverlessworkflow/impl/WorkflowDefinitionTest.java +++ /dev/null @@ -1,52 +0,0 @@ -package io.serverlessworkflow.impl; - -/* - * 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. - */ -import static io.serverlessworkflow.api.WorkflowReader.readWorkflowFromClasspath; -import static org.assertj.core.api.Assertions.assertThat; - -import java.io.IOException; -import java.util.Map; -import java.util.stream.Stream; -import org.assertj.core.api.Condition; -import org.junit.jupiter.params.ParameterizedTest; -import org.junit.jupiter.params.provider.Arguments; -import org.junit.jupiter.params.provider.MethodSource; - -public class WorkflowDefinitionTest { - - @ParameterizedTest - @MethodSource("provideParameters") - void testWorkflowExecution(String fileName, Object input, Condition condition) - throws IOException { - assertThat( - WorkflowDefinition.builder(readWorkflowFromClasspath(fileName)) - .build() - .execute(input) - .output()) - .is(condition); - } - - private static Stream provideParameters() { - Map petInput = Map.of("petId", 10); - Condition petCondition = - new Condition<>( - o -> ((Map) o).containsKey("photoUrls"), "callHttpCondition"); - return Stream.of( - Arguments.of("callHttp.yaml", petInput, petCondition), - Arguments.of("call-http-endpoint-interpolation.yaml", petInput, petCondition)); - } -} diff --git a/impl/src/test/resources/callHttp.yaml b/impl/src/test/resources/callHttp.yaml deleted file mode 100644 index 0fdeb10a..00000000 --- a/impl/src/test/resources/callHttp.yaml +++ /dev/null @@ -1,14 +0,0 @@ -document: - dsl: 1.0.0-alpha1 - namespace: default - name: http-call-with-response-output - version: 1.0.0 -do: - - getPet: - call: http - with: - headers: - content-type: application/json - method: get - endpoint: - uri: https://petstore.swagger.io/v2/pet/{petId} \ No newline at end of file diff --git a/pom.xml b/pom.xml index e0108a57..501bf66d 100644 --- a/pom.xml +++ b/pom.xml @@ -4,7 +4,7 @@ io.serverlessworkflow serverlessworkflow-parent - 7.0.0-alpha5 + 7.0.0-alpha5.1 pom Serverless Workflow :: Parent @@ -33,7 +33,7 @@ scm:git:git@github.com:serverlessworkflow/sdk-java.git scm:git:git@github.com:serverlessworkflow/sdk-java.git https://github.com/serverlessworkflow/sdk-java - 7.0.0-alpha5 + 7.0.0-alpha5.1 @@ -43,7 +43,7 @@ - 11 + 17 ${java.version} ${java.version} UTF-8 @@ -69,8 +69,8 @@ 1.5.12 - 2.18.1 - 1.5.3 + 2.18.2 + 1.5.4 3.1.0 1.5.2 3.26.3 @@ -232,6 +232,67 @@ + + org.apache.maven.plugins + maven-checkstyle-plugin + + + + + + + + + + + + + + + + ${project.build.directory}/checkstyle.log + true + true + true + false + false + ${checkstyle.logViolationsToConsole} + ${checkstyle.failOnViolation} + + ${project.build.sourceDirectory} + ${project.build.testSourceDirectory} + + + + + compile + + check + + + + + + com.spotify.fmt + fmt-maven-plugin + + src/main/java + src/test/java + false + .*\.java + false + false + + + + + + format + + + + + pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy