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/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/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/core/src/main/java/io/serverlessworkflow/impl/WorkflowInstance.java b/impl/core/src/main/java/io/serverlessworkflow/impl/WorkflowInstance.java index 444d3fd5..0f3bd410 100644 --- a/impl/core/src/main/java/io/serverlessworkflow/impl/WorkflowInstance.java +++ b/impl/core/src/main/java/io/serverlessworkflow/impl/WorkflowInstance.java @@ -40,8 +40,7 @@ public class WorkflowInstance { .inputFilter() .ifPresent(f -> taskContext.input(f.apply(workflowContext, taskContext, input))); state = WorkflowState.STARTED; - taskContext.rawOutput( - WorkflowUtils.processTaskList(definition.workflow().getDo(), workflowContext, taskContext)); + WorkflowUtils.processTaskList(definition.workflow().getDo(), workflowContext, taskContext); definition .outputFilter() .ifPresent( diff --git a/impl/core/src/main/java/io/serverlessworkflow/impl/WorkflowPositionFactory.java b/impl/core/src/main/java/io/serverlessworkflow/impl/WorkflowPositionFactory.java index c2a3df7e..60fa5d6a 100644 --- a/impl/core/src/main/java/io/serverlessworkflow/impl/WorkflowPositionFactory.java +++ b/impl/core/src/main/java/io/serverlessworkflow/impl/WorkflowPositionFactory.java @@ -17,4 +17,5 @@ import java.util.function.Supplier; +@FunctionalInterface public interface WorkflowPositionFactory extends Supplier {} diff --git a/impl/core/src/main/java/io/serverlessworkflow/impl/WorkflowUtils.java b/impl/core/src/main/java/io/serverlessworkflow/impl/WorkflowUtils.java index 54abd7e7..01486d5f 100644 --- a/impl/core/src/main/java/io/serverlessworkflow/impl/WorkflowUtils.java +++ b/impl/core/src/main/java/io/serverlessworkflow/impl/WorkflowUtils.java @@ -92,6 +92,23 @@ public static Optional buildWorkflowFilter( : Optional.empty(); } + public static StringFilter buildStringFilter( + ExpressionFactory exprFactory, String expression, String literal) { + return expression != null ? from(buildWorkflowFilter(exprFactory, expression)) : from(literal); + } + + public static StringFilter buildStringFilter(ExpressionFactory exprFactory, String str) { + return ExpressionUtils.isExpr(str) ? from(buildWorkflowFilter(exprFactory, str)) : from(str); + } + + public static StringFilter from(WorkflowFilter filter) { + return (w, t) -> filter.apply(w, t, t.input()).asText(); + } + + private static StringFilter from(String literal) { + return (w, t) -> literal; + } + private static WorkflowFilter buildWorkflowFilter( ExpressionFactory exprFactory, String str, Object object) { if (str != null) { @@ -127,7 +144,7 @@ private static TaskItem findTaskByName(ListIterator iter, String taskN throw new IllegalArgumentException("Cannot find task with name " + taskName); } - public static JsonNode processTaskList( + public static void processTaskList( List tasks, WorkflowContext context, TaskContext parentTask) { parentTask.position().addProperty("do"); TaskContext currentContext = parentTask; @@ -136,7 +153,7 @@ public static JsonNode processTaskList( TaskItem nextTask = iter.next(); while (nextTask != null) { TaskItem task = nextTask; - parentTask.position().addIndex(iter.nextIndex()).addProperty(task.getName()); + parentTask.position().addIndex(iter.previousIndex()).addProperty(task.getName()); context .definition() .listeners() @@ -175,7 +192,7 @@ public static JsonNode processTaskList( } } parentTask.position().back(); - return currentContext.output(); + parentTask.rawOutput(currentContext.output()); } public static WorkflowFilter buildWorkflowFilter(ExpressionFactory exprFactory, String str) { 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 index 45a988a7..58bd18ac 100644 --- a/impl/core/src/main/java/io/serverlessworkflow/impl/executors/DefaultTaskExecutorFactory.java +++ b/impl/core/src/main/java/io/serverlessworkflow/impl/executors/DefaultTaskExecutorFactory.java @@ -67,6 +67,10 @@ public TaskExecutor getTaskExecutor( 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); } throw new UnsupportedOperationException(task.get().getClass().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 index df364c14..871a77da 100644 --- a/impl/core/src/main/java/io/serverlessworkflow/impl/executors/DoExecutor.java +++ b/impl/core/src/main/java/io/serverlessworkflow/impl/executors/DoExecutor.java @@ -29,6 +29,6 @@ protected DoExecutor(DoTask task, WorkflowDefinition definition) { @Override protected void internalExecute(WorkflowContext workflow, TaskContext taskContext) { - taskContext.rawOutput(WorkflowUtils.processTaskList(task.getDo(), workflow, taskContext)); + WorkflowUtils.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 index 511575f3..e74a18f9 100644 --- a/impl/core/src/main/java/io/serverlessworkflow/impl/executors/ForExecutor.java +++ b/impl/core/src/main/java/io/serverlessworkflow/impl/executors/ForExecutor.java @@ -52,7 +52,7 @@ protected void internalExecute(WorkflowContext workflow, TaskContext ta JsonNode item = iter.next(); taskContext.variables().put(task.getFor().getEach(), item); taskContext.variables().put(task.getFor().getAt(), i++); - taskContext.rawOutput(WorkflowUtils.processTaskList(task.getDo(), workflow, taskContext)); + WorkflowUtils.processTaskList(task.getDo(), workflow, taskContext); } } } 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/TryExecutor.java b/impl/core/src/main/java/io/serverlessworkflow/impl/executors/TryExecutor.java new file mode 100644 index 00000000..67af7995 --- /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 { + WorkflowUtils.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) { + WorkflowUtils.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/test/java/io/serverlessworkflow/impl/WorkflowDefinitionTest.java b/impl/core/src/test/java/io/serverlessworkflow/impl/WorkflowDefinitionTest.java index 27662797..6b29bac1 100644 --- a/impl/core/src/test/java/io/serverlessworkflow/impl/WorkflowDefinitionTest.java +++ b/impl/core/src/test/java/io/serverlessworkflow/impl/WorkflowDefinitionTest.java @@ -17,6 +17,7 @@ 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.time.Instant; @@ -42,10 +43,9 @@ static void init() { @ParameterizedTest @MethodSource("provideParameters") - void testWorkflowExecution(String fileName, Object input, Consumer assertions) + void testWorkflowExecution(String fileName, Consumer assertions) throws IOException { - assertions.accept( - appl.workflowDefinition(readWorkflowFromClasspath(fileName)).execute(input).output()); + assertions.accept(appl.workflowDefinition(readWorkflowFromClasspath(fileName))); } private static Stream provideParameters() { @@ -54,7 +54,7 @@ private static Stream provideParameters() { "switch-then-string.yaml", Map.of("orderType", "electronic"), o -> - assertThat(o) + assertThat(o.output()) .isEqualTo( Map.of( "orderType", "electronic", "validate", true, "status", "fulfilled"))), @@ -62,7 +62,7 @@ private static Stream provideParameters() { "switch-then-string.yaml", Map.of("orderType", "physical"), o -> - assertThat(o) + assertThat(o.output()) .isEqualTo( Map.of( "orderType", @@ -77,7 +77,7 @@ private static Stream provideParameters() { "switch-then-string.yaml", Map.of("orderType", "unknown"), o -> - assertThat(o) + assertThat(o.output()) .isEqualTo( Map.of( "orderType", @@ -89,27 +89,53 @@ private static Stream provideParameters() { args( "for-sum.yaml", Map.of("input", Arrays.asList(1, 2, 3)), - o -> assertThat(o).isEqualTo(6)), + o -> assertThat(o.output()).isEqualTo(6)), args( "for-collect.yaml", Map.of("input", Arrays.asList(1, 2, 3)), o -> - assertThat(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)); + WorkflowDefinitionTest::checkSpecialKeywords), + args( + "raise-inline copy.yaml", + WorkflowDefinitionTest::checkWorkflowException, + WorkflowException.class), + args( + "raise-reusable.yaml", + WorkflowDefinitionTest::checkWorkflowException, + WorkflowException.class)); } private static Arguments args( - String fileName, Map input, Consumer object) { - return Arguments.of(fileName, input, object); + 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 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(Object obj) { - Map result = (Map) obj; + private static void checkSpecialKeywords(WorkflowInstance obj) { + Map result = (Map) obj.output(); assertThat(Instant.ofEpochMilli((long) result.get("startedAt"))) .isAfterOrEqualTo(before) .isBeforeOrEqualTo(Instant.now()); 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/http/src/main/java/io/serverlessworkflow/impl/executors/HttpExecutor.java b/impl/http/src/main/java/io/serverlessworkflow/impl/executors/HttpExecutor.java index 5ecd27af..13e61d35 100644 --- a/impl/http/src/main/java/io/serverlessworkflow/impl/executors/HttpExecutor.java +++ b/impl/http/src/main/java/io/serverlessworkflow/impl/executors/HttpExecutor.java @@ -26,11 +26,14 @@ 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; @@ -101,7 +104,13 @@ public JsonNode apply( Builder request = target.request(); ExpressionUtils.evaluateExpressionMap(headersMap, workflow, taskContext, input) .forEach(request::header); - return requestFunction.apply(request, workflow, taskContext, input); + try { + return requestFunction.apply(request, workflow, taskContext, input); + } catch (WebApplicationException exception) { + throw new WorkflowException( + WorkflowError.communication(exception.getResponse().getStatus(), taskContext, exception) + .build()); + } } @Override diff --git a/impl/http/src/test/java/io/serverlessworkflow/impl/HTTPWorkflowDefinitionTest.java b/impl/http/src/test/java/io/serverlessworkflow/impl/HTTPWorkflowDefinitionTest.java index dacdfe2e..f3d77bdd 100644 --- a/impl/http/src/test/java/io/serverlessworkflow/impl/HTTPWorkflowDefinitionTest.java +++ b/impl/http/src/test/java/io/serverlessworkflow/impl/HTTPWorkflowDefinitionTest.java @@ -69,6 +69,11 @@ private static Stream provideParameters() { 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", diff --git a/impl/http/src/test/resources/callGetHttp.yaml b/impl/http/src/test/resources/callGetHttp.yaml index 6fc07807..192b0bcd 100644 --- a/impl/http/src/test/resources/callGetHttp.yaml +++ b/impl/http/src/test/resources/callGetHttp.yaml @@ -4,11 +4,18 @@ document: name: http-call-with-response 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 + - 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