From 0b402f475e4a91b43fd94aac7d73db9145df676f Mon Sep 17 00:00:00 2001 From: Beppe Catanese Date: Sun, 29 Mar 2026 13:39:01 +0200 Subject: [PATCH 1/5] Implement missing Arazzo spec features Model additions: - Info.summary optional field - SuccessAction.name and FailureAction.name required fields - Parameter.value widened from String to Object (spec allows boolean/number/object/array/null) - Workflow.dependsOn changed from String to List (spec defines it as an array) - CriterionExpressionType model for jsonpath/xpath type+version objects - Criterion.type now deserialises both plain strings and CriterionExpressionType objects via CriterionTypeDeserializer Validator additions: - arazzo version pattern validated against ^1\.0\.\d+(-.+)?$ - SourceDescription.name pattern validated against ^[A-Za-z0-9_\-]+$ - SuccessAction and FailureAction name field validated as required - validateCriterion handles CriterionExpressionType (checks type is jsonpath/xpath, version is present, context is required) - validateComponents now validates successActions and failureActions keys and contents - validateWorkflow checks each dependsOn entry references an existing workflow Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com> --- .../java/com/apiflows/model/Criterion.java | 24 +- .../model/CriterionExpressionType.java | 37 ++++ .../com/apiflows/model/FailureAction.java | 14 ++ src/main/java/com/apiflows/model/Info.java | 15 ++ .../java/com/apiflows/model/Parameter.java | 8 +- .../com/apiflows/model/SuccessAction.java | 14 ++ .../java/com/apiflows/model/Workflow.java | 6 +- .../parser/OpenAPIWorkflowValidator.java | 71 +++++- .../util/CriterionTypeDeserializer.java | 35 +++ .../parser/OpenAPIWorkflowValidatorTest.java | 209 +++++++++++++++++- 10 files changed, 416 insertions(+), 17 deletions(-) create mode 100644 src/main/java/com/apiflows/model/CriterionExpressionType.java create mode 100644 src/main/java/com/apiflows/parser/util/CriterionTypeDeserializer.java diff --git a/src/main/java/com/apiflows/model/Criterion.java b/src/main/java/com/apiflows/model/Criterion.java index b2ca010..c8d5640 100644 --- a/src/main/java/com/apiflows/model/Criterion.java +++ b/src/main/java/com/apiflows/model/Criterion.java @@ -1,12 +1,16 @@ package com.apiflows.model; +import com.apiflows.parser.util.CriterionTypeDeserializer; import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; public class Criterion { private String condition; private String context; - private String type; + + @JsonDeserialize(using = CriterionTypeDeserializer.class) + private Object type; public String getCondition() { return condition; @@ -24,14 +28,25 @@ public void setContext(String context) { this.context = context; } + /** Returns the simple string type (e.g. "simple", "regex") or null if an expression type object is used. */ + @JsonProperty("type") public String getType() { - return type; + return type instanceof String ? (String) type : null; } public void setType(String type) { this.type = type; } + /** Returns the expression type object when type is "jsonpath" or "xpath" with a version, or null otherwise. */ + public CriterionExpressionType getExpressionType() { + return type instanceof CriterionExpressionType ? (CriterionExpressionType) type : null; + } + + public void setExpressionType(CriterionExpressionType expressionType) { + this.type = expressionType; + } + public Criterion condition(String condition) { this.condition = condition; return this; @@ -46,5 +61,10 @@ public Criterion type(String type) { this.type = type; return this; } + + public Criterion expressionType(CriterionExpressionType expressionType) { + this.type = expressionType; + return this; + } } diff --git a/src/main/java/com/apiflows/model/CriterionExpressionType.java b/src/main/java/com/apiflows/model/CriterionExpressionType.java new file mode 100644 index 0000000..565fbd6 --- /dev/null +++ b/src/main/java/com/apiflows/model/CriterionExpressionType.java @@ -0,0 +1,37 @@ +package com.apiflows.model; + +import com.fasterxml.jackson.annotation.JsonProperty; + +public class CriterionExpressionType { + + private String type; + private String version; + + @JsonProperty("type") + public String getType() { + return type; + } + + public void setType(String type) { + this.type = type; + } + + @JsonProperty("version") + public String getVersion() { + return version; + } + + public void setVersion(String version) { + this.version = version; + } + + public CriterionExpressionType type(String type) { + this.type = type; + return this; + } + + public CriterionExpressionType version(String version) { + this.version = version; + return this; + } +} diff --git a/src/main/java/com/apiflows/model/FailureAction.java b/src/main/java/com/apiflows/model/FailureAction.java index 838135e..44e3047 100644 --- a/src/main/java/com/apiflows/model/FailureAction.java +++ b/src/main/java/com/apiflows/model/FailureAction.java @@ -5,6 +5,7 @@ public class FailureAction { + private String name; private String type; private String workflowId; private String stepId; @@ -12,6 +13,14 @@ public class FailureAction { private Integer retryLimit; private List criteria; + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + public String getType() { return type; } @@ -60,6 +69,11 @@ public void setCriteria(List criteria) { this.criteria = criteria; } + public FailureAction name(String name) { + this.name = name; + return this; + } + public FailureAction type(String type) { this.type = type; return this; diff --git a/src/main/java/com/apiflows/model/Info.java b/src/main/java/com/apiflows/model/Info.java index 7bcf2f2..fa6be37 100644 --- a/src/main/java/com/apiflows/model/Info.java +++ b/src/main/java/com/apiflows/model/Info.java @@ -5,6 +5,7 @@ public class Info { private String title; + private String summary; private String version; private String description; @@ -17,6 +18,15 @@ public void setTitle(String title) { this.title = title; } + @JsonProperty("summary") + public String getSummary() { + return summary; + } + + public void setSummary(String summary) { + this.summary = summary; + } + @JsonProperty("version") public String getVersion() { return version; @@ -40,6 +50,11 @@ public Info title(String title) { return this; } + public Info summary(String summary) { + this.summary = summary; + return this; + } + public Info version(String version) { this.version = version; return this; diff --git a/src/main/java/com/apiflows/model/Parameter.java b/src/main/java/com/apiflows/model/Parameter.java index a5abfbc..0986473 100644 --- a/src/main/java/com/apiflows/model/Parameter.java +++ b/src/main/java/com/apiflows/model/Parameter.java @@ -6,7 +6,7 @@ public class Parameter { private String name; private String in; - private String value; + private Object value; private String reference; @JsonProperty("name") @@ -28,11 +28,11 @@ public void setIn(String in) { } @JsonProperty("value") - public String getValue() { + public Object getValue() { return value; } - public void setValue(String value) { + public void setValue(Object value) { this.value = value; } @@ -55,7 +55,7 @@ public Parameter in(String in) { return this; } - public Parameter value(String value) { + public Parameter value(Object value) { this.value = value; return this; } diff --git a/src/main/java/com/apiflows/model/SuccessAction.java b/src/main/java/com/apiflows/model/SuccessAction.java index 92a6792..89bf284 100644 --- a/src/main/java/com/apiflows/model/SuccessAction.java +++ b/src/main/java/com/apiflows/model/SuccessAction.java @@ -5,11 +5,20 @@ public class SuccessAction { + private String name; private String type; private String workflowId; private String stepId; private List criteria; + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + public String getType() { return type; } @@ -49,6 +58,11 @@ public void addCriteria(Criterion criterion) { this.criteria.add(criterion); } + public SuccessAction name(String name) { + this.name = name; + return this; + } + public SuccessAction type(String type) { this.type = type; return this; diff --git a/src/main/java/com/apiflows/model/Workflow.java b/src/main/java/com/apiflows/model/Workflow.java index eba5413..ac89492 100644 --- a/src/main/java/com/apiflows/model/Workflow.java +++ b/src/main/java/com/apiflows/model/Workflow.java @@ -15,7 +15,7 @@ public class Workflow { private String description; private Schema inputs; - private String dependsOn; + private List dependsOn; private List parameters = new ArrayList<>(); private List successActions = new ArrayList<>(); @@ -102,11 +102,11 @@ public Workflow inputs(Schema inputs) { return this; } - public String getDependsOn() { + public List getDependsOn() { return dependsOn; } - public void setDependsOn(String dependsOn) { + public void setDependsOn(List dependsOn) { this.dependsOn = dependsOn; } diff --git a/src/main/java/com/apiflows/parser/OpenAPIWorkflowValidator.java b/src/main/java/com/apiflows/parser/OpenAPIWorkflowValidator.java index 52a7b5b..15d8722 100644 --- a/src/main/java/com/apiflows/parser/OpenAPIWorkflowValidator.java +++ b/src/main/java/com/apiflows/parser/OpenAPIWorkflowValidator.java @@ -2,7 +2,6 @@ import com.apiflows.model.*; import com.fasterxml.jackson.core.JsonPointer; -import io.swagger.v3.oas.models.media.Schema; import java.util.*; import java.util.regex.Pattern; @@ -38,6 +37,8 @@ public OpenAPIWorkflowValidatorResult validate() { if (openAPIWorkflow.getArazzo() == null || openAPIWorkflow.getArazzo().isEmpty()) { result.addError("'arazzo' is undefined"); + } else if (!isValidArazzoVersion(openAPIWorkflow.getArazzo())) { + result.addError("'arazzo' version '" + openAPIWorkflow.getArazzo() + "' is invalid (must match 1.0.x)"); } // Info @@ -100,6 +101,8 @@ List validateSourceDescriptions(List sourceDescriptio for (SourceDescription sourceDescription : sourceDescriptions) { if (sourceDescription.getName() == null || sourceDescription.getName().isEmpty()) { errors.add("'SourceDescription[" + i + "] name' is undefined"); + } else if (!isValidSourceDescriptionName(sourceDescription.getName())) { + errors.add("'SourceDescription[" + i + "] name' is invalid (must match ^[A-Za-z0-9_\\-]+$)"); } if (sourceDescription.getUrl() == null || sourceDescription.getUrl().isEmpty()) { errors.add("'SourceDescription[" + i + "] url' is undefined"); @@ -142,6 +145,13 @@ List validateWorkflow(Workflow workflow, int index ){ } } + if (workflow.getDependsOn() != null) { + for (String dep : workflow.getDependsOn()) { + if (!workflowExists(dep)) { + errors.add("Workflow[" + workflow.getWorkflowId() + "] dependsOn '" + dep + "' is invalid (no such workflow exists)"); + } + } + } return errors; } @@ -289,6 +299,10 @@ List validateSuccessAction(String workflowId, String stepId, SuccessActi List errors = new ArrayList<>(); + if (successAction.getName() == null || successAction.getName().isEmpty()) { + errors.add("Step " + stepId + " SuccessAction has no name"); + } + if (successAction.getType() == null) { errors.add("Step " + stepId + " SuccessAction has no type"); } @@ -321,6 +335,10 @@ List validateFailureAction(String workflowId, String stepId, FailureActi List errors = new ArrayList<>(); + if (failureAction.getName() == null || failureAction.getName().isEmpty()) { + errors.add("Step " + stepId + " FailureAction has no name"); + } + if (failureAction.getType() == null) { errors.add("Step " + stepId + " FailureAction has no type"); } @@ -358,7 +376,8 @@ List validateFailureAction(String workflowId, String stepId, FailureActi } List validateCriterion(Criterion criterion, String stepId) { - List SUPPORTED_VALUES = Arrays.asList("simple", "regex", "jsonpath", "xpath"); + List SUPPORTED_SIMPLE_TYPES = Arrays.asList("simple", "regex", "jsonpath", "xpath"); + List SUPPORTED_EXPRESSION_TYPES = Arrays.asList("jsonpath", "xpath"); List errors = new ArrayList<>(); @@ -366,12 +385,26 @@ List validateCriterion(Criterion criterion, String stepId) { errors.add("Step " + stepId + " has no condition"); } - if (criterion.getType() != null && !SUPPORTED_VALUES.contains(criterion.getType())) { - errors.add("Step " + stepId + " SuccessCriteria type (" + criterion.getType() + ") is invalid"); + if (criterion.getType() != null) { + if (!SUPPORTED_SIMPLE_TYPES.contains(criterion.getType())) { + errors.add("Step " + stepId + " SuccessCriteria type (" + criterion.getType() + ") is invalid"); + } + if (criterion.getContext() == null) { + errors.add("Step " + stepId + " SuccessCriteria type is specified but context is not provided"); + } } - if (criterion.getType() != null && criterion.getContext() == null) { - errors.add("Step " + stepId + " SuccessCriteria type is specified but context is not provided"); + CriterionExpressionType expressionType = criterion.getExpressionType(); + if (expressionType != null) { + if (expressionType.getType() == null || !SUPPORTED_EXPRESSION_TYPES.contains(expressionType.getType())) { + errors.add("Step " + stepId + " SuccessCriteria expressionType '" + expressionType.getType() + "' is invalid (must be jsonpath or xpath)"); + } + if (expressionType.getVersion() == null || expressionType.getVersion().isEmpty()) { + errors.add("Step " + stepId + " SuccessCriteria expressionType has no version"); + } + if (criterion.getContext() == null) { + errors.add("Step " + stepId + " SuccessCriteria type is specified but context is not provided"); + } } return errors; @@ -400,6 +433,24 @@ List validateComponents(Components components) { } } } + if (components.getSuccessActions() != null) { + for (String key : components.getSuccessActions().keySet()) { + if (!isValidComponentKey(key)) { + errors.add("'Component successAction name " + key + " is invalid (should match regex " + getComponentKeyRegularExpression() + ")"); + } + } + components.getSuccessActions().forEach((key, action) -> + errors.addAll(validateSuccessAction(null, key, action))); + } + if (components.getFailureActions() != null) { + for (String key : components.getFailureActions().keySet()) { + if (!isValidComponentKey(key)) { + errors.add("'Component failureAction name " + key + " is invalid (should match regex " + getComponentKeyRegularExpression() + ")"); + } + } + components.getFailureActions().forEach((key, action) -> + errors.addAll(validateFailureAction(null, key, action))); + } } return errors; @@ -583,6 +634,14 @@ boolean isRuntimeExpression(String name) { return name != null && name.startsWith("$"); } + boolean isValidArazzoVersion(String version) { + return Pattern.matches("^1\\.0\\.\\d+(-.+)?$", version); + } + + boolean isValidSourceDescriptionName(String name) { + return Pattern.matches("^[A-Za-z0-9_\\-]+$", name); + } + private List validateGotoTarget(String stepId, String actionLabel, String targetStepId, String targetWorkflowId) { List errors = new ArrayList<>(); if (targetWorkflowId == null && targetStepId == null) { diff --git a/src/main/java/com/apiflows/parser/util/CriterionTypeDeserializer.java b/src/main/java/com/apiflows/parser/util/CriterionTypeDeserializer.java new file mode 100644 index 0000000..6c11f42 --- /dev/null +++ b/src/main/java/com/apiflows/parser/util/CriterionTypeDeserializer.java @@ -0,0 +1,35 @@ +package com.apiflows.parser.util; + +import com.apiflows.model.CriterionExpressionType; +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.databind.DeserializationContext; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.deser.std.StdDeserializer; + +import java.io.IOException; + +public class CriterionTypeDeserializer extends StdDeserializer { + + public CriterionTypeDeserializer() { + super(Object.class); + } + + @Override + public Object deserialize(JsonParser p, DeserializationContext ctx) throws IOException { + JsonNode node = p.getCodec().readTree(p); + if (node.isTextual()) { + return node.asText(); + } + if (node.isObject()) { + CriterionExpressionType expressionType = new CriterionExpressionType(); + if (node.has("type")) { + expressionType.setType(node.get("type").asText()); + } + if (node.has("version")) { + expressionType.setVersion(node.get("version").asText()); + } + return expressionType; + } + return null; + } +} diff --git a/src/test/java/com/apiflows/parser/OpenAPIWorkflowValidatorTest.java b/src/test/java/com/apiflows/parser/OpenAPIWorkflowValidatorTest.java index ba3078c..b230d43 100644 --- a/src/test/java/com/apiflows/parser/OpenAPIWorkflowValidatorTest.java +++ b/src/test/java/com/apiflows/parser/OpenAPIWorkflowValidatorTest.java @@ -2,6 +2,7 @@ import com.apiflows.model.*; import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.BeforeEach; import java.util.*; @@ -54,7 +55,7 @@ void validateSourceDescriptions() { void validateSourceDescriptionsWithoutUrl() { List sourceDescriptions = new ArrayList<>(); sourceDescriptions.add(new SourceDescription() - .name("Source one") + .name("source-one") .type("openapi") .url(null)); assertEquals(1, validator.validateSourceDescriptions(sourceDescriptions).size()); @@ -64,7 +65,7 @@ void validateSourceDescriptionsWithoutUrl() { void validateSourceDescriptionsInvalidType() { List sourceDescriptions = new ArrayList<>(); sourceDescriptions.add(new SourceDescription() - .name("Source one") + .name("source-one") .type("unkwown") .url("https://example.com/spec.json")); assertEquals(1, validator.validateSourceDescriptions(sourceDescriptions).size()); @@ -295,6 +296,7 @@ void validateSuccessAction() { String stepId = "step-one"; SuccessAction successAction = new SuccessAction() + .name("on-success") .type("end"); successAction.addCriteria( @@ -310,6 +312,7 @@ void validateSuccessActionEndTypeDoesNotRequireTarget() { String stepId = "step-one"; SuccessAction successAction = new SuccessAction() + .name("on-success") .type("end") .stepId(null) .workflowId(null); @@ -323,6 +326,7 @@ void validateSuccessActionInvalidType() { String stepId = "step-one"; SuccessAction successAction = new SuccessAction() + .name("on-success") .type("invalid-type") .stepId("step-one"); @@ -339,6 +343,7 @@ void validateSuccessActionGotoMissingTarget() { String stepId = "step-one"; SuccessAction successAction = new SuccessAction() + .name("on-success") .type("goto") .stepId(null) .workflowId(null); @@ -352,6 +357,7 @@ void validateSuccessActionGotoInvalidEntity() { v.workflowIds.add("target-workflow"); SuccessAction successAction = new SuccessAction() + .name("on-success") .type("goto") .stepId("step-one") .workflowId("target-workflow"); @@ -364,6 +370,7 @@ void validateSuccessActionGotoValidWorkflowId() { OpenAPIWorkflowValidator v = validatorWithWorkflowIds("target-workflow"); SuccessAction successAction = new SuccessAction() + .name("on-success") .type("goto") .workflowId("target-workflow"); @@ -375,6 +382,7 @@ void validateSuccessActionGotoInvalidWorkflowId() { OpenAPIWorkflowValidator v = validatorWithWorkflowIds("w1"); SuccessAction successAction = new SuccessAction() + .name("on-success") .type("goto") .workflowId("non-existent-workflow"); @@ -386,6 +394,7 @@ void validateSuccessActionInvalidStepId() { OpenAPIWorkflowValidator v = validatorWithStepIds("w1", "step-one", "step-two", "step-three"); SuccessAction successAction = new SuccessAction() + .name("on-success") .type("goto") .stepId("step-dummy"); @@ -444,6 +453,7 @@ void validateFailureAction() { String stepId = "step-one"; FailureAction failureAction = new FailureAction() + .name("on-failure") .type("end") .retryAfter(1.5) .retryLimit(3); @@ -461,6 +471,7 @@ void validateFailureActionInvalidType() { String stepId = "step-one"; FailureAction failureAction = new FailureAction() + .name("on-failure") .type("dummy") .retryAfter(1.5) .retryLimit(3); @@ -478,6 +489,7 @@ void validateFailureActionInvalidRetrySettings() { String stepId = "step-one"; FailureAction failureAction = new FailureAction() + .name("on-failure") .type("end") .retryAfter(-1.5) .retryLimit(-3); @@ -495,6 +507,7 @@ void validateFailureActionRetryTypeDoesNotRequireTarget() { String stepId = "step-one"; FailureAction failureAction = new FailureAction() + .name("on-failure") .type("retry") .stepId(null) .workflowId(null) @@ -510,6 +523,7 @@ void validateFailureActionGotoMissingTarget() { String stepId = "step-one"; FailureAction failureAction = new FailureAction() + .name("on-failure") .type("goto") .stepId(null) .workflowId(null); @@ -522,6 +536,7 @@ void validateFailureActionGotoInvalidEntity() { OpenAPIWorkflowValidator v = validatorWithStepIds("w1", "step-one", "step-two"); FailureAction failureAction = new FailureAction() + .name("on-failure") .type("goto") .stepId("step-one") .workflowId("workflow-test"); @@ -535,6 +550,7 @@ void validateFailureActionRetryAfterDecimalValue() { String stepId = "step-one"; FailureAction failureAction = new FailureAction() + .name("on-failure") .type("retry") .retryAfter(0.5) .retryLimit(3); @@ -547,6 +563,7 @@ void validateFailureActionInvalidStepId() { OpenAPIWorkflowValidator v = validatorWithStepIds("w1", "step-one", "step-two", "step-three"); FailureAction failureAction = new FailureAction() + .name("on-failure") .type("retry") .stepId("step-dummy") .retryAfter(1.5) @@ -756,6 +773,194 @@ void invalidComponentKey() { } + // --- New feature tests --- + + @Test + void validateArazzoVersionValid() { + assertTrue(new OpenAPIWorkflowValidator().isValidArazzoVersion("1.0.0")); + } + + @Test + void validateArazzoVersionValidWithPrerelease() { + assertTrue(new OpenAPIWorkflowValidator().isValidArazzoVersion("1.0.1-beta")); + } + + @Test + void validateArazzoVersionInvalid() { + assertFalse(new OpenAPIWorkflowValidator().isValidArazzoVersion("2.0.0")); + } + + @Test + void validateArazzoVersionInvalidFormat() { + assertFalse(new OpenAPIWorkflowValidator().isValidArazzoVersion("1.0")); + } + + @Test + void validateSourceDescriptionNameValid() { + assertTrue(new OpenAPIWorkflowValidator().isValidSourceDescriptionName("my-source_1")); + } + + @Test + void validateSourceDescriptionNameInvalidWithSpace() { + assertFalse(new OpenAPIWorkflowValidator().isValidSourceDescriptionName("my source")); + } + + @Test + void validateSourceDescriptionNameInvalidWithDot() { + assertFalse(new OpenAPIWorkflowValidator().isValidSourceDescriptionName("my.source")); + } + + @Test + void validateSourceDescriptionInvalidName() { + List sourceDescriptions = new ArrayList<>(); + sourceDescriptions.add(new SourceDescription() + .name("invalid name") + .type("openapi") + .url("https://example.com/spec.json")); + assertEquals(1, validator.validateSourceDescriptions(sourceDescriptions).size()); + } + + @Test + void validateSuccessActionMissingName() { + SuccessAction successAction = new SuccessAction() + .type("end"); + + assertEquals(1, validator.validateSuccessAction("w1", "step-one", successAction).size()); + } + + @Test + void validateFailureActionMissingName() { + FailureAction failureAction = new FailureAction() + .type("end"); + + assertEquals(1, validator.validateFailureAction("w1", "step-one", failureAction).size()); + } + + @Test + void validateWorkflowDependsOnValid() { + OpenAPIWorkflowValidator v = new OpenAPIWorkflowValidator(); + v.workflowIds.add("w1"); + v.workflowIds.add("w2"); + + Workflow workflow = new Workflow() + .workflowId("w2") + .addStep(new Step().stepId("step-one").operationId("op")); + workflow.setDependsOn(List.of("w1")); + + assertEquals(0, v.validateWorkflow(workflow, 0).size()); + } + + @Test + void validateWorkflowDependsOnInvalid() { + OpenAPIWorkflowValidator v = new OpenAPIWorkflowValidator(); + v.workflowIds.add("w1"); + + Workflow workflow = new Workflow() + .workflowId("w1") + .addStep(new Step().stepId("step-one").operationId("op")); + workflow.setDependsOn(List.of("non-existent-workflow")); + + assertEquals(1, v.validateWorkflow(workflow, 0).size()); + } + + @Test + void validateCriterionWithExpressionType() { + Criterion criterion = new Criterion() + .condition("$.petId") + .context("$response.body") + .expressionType(new CriterionExpressionType() + .type("jsonpath") + .version("draft-goessner-dispatch-jsonpath-00")); + + assertEquals(0, validator.validateCriterion(criterion, "step-one").size()); + } + + @Test + void validateCriterionExpressionTypeMissingVersion() { + Criterion criterion = new Criterion() + .condition("$.petId") + .context("$response.body") + .expressionType(new CriterionExpressionType() + .type("jsonpath")); + + assertEquals(1, validator.validateCriterion(criterion, "step-one").size()); + } + + @Test + void validateCriterionExpressionTypeInvalidType() { + Criterion criterion = new Criterion() + .condition("$.petId") + .context("$response.body") + .expressionType(new CriterionExpressionType() + .type("simple") + .version("draft-goessner-dispatch-jsonpath-00")); + + assertEquals(1, validator.validateCriterion(criterion, "step-one").size()); + } + + @Test + void validateCriterionExpressionTypeMissingContext() { + Criterion criterion = new Criterion() + .condition("$.petId") + .expressionType(new CriterionExpressionType() + .type("jsonpath") + .version("draft-goessner-dispatch-jsonpath-00")); + + assertEquals(1, validator.validateCriterion(criterion, "step-one").size()); + } + + @Test + void validateComponentsSuccessActions() { + Components components = new Components(); + components.getSuccessActions().put("onSuccess", new SuccessAction().name("onSuccess").type("end")); + + assertEquals(0, validator.validateComponents(components).size()); + } + + @Test + void validateComponentsSuccessActionsInvalidKey() { + Components components = new Components(); + components.getSuccessActions().put("on success", new SuccessAction().name("onSuccess").type("end")); + + assertEquals(1, validator.validateComponents(components).size()); + } + + @Test + void validateComponentsFailureActions() { + Components components = new Components(); + components.getFailureActions().put("onFailure", new FailureAction().name("onFailure").type("end")); + + assertEquals(0, validator.validateComponents(components).size()); + } + + @Test + void validateComponentsFailureActionsInvalidKey() { + Components components = new Components(); + components.getFailureActions().put("on failure", new FailureAction().name("onFailure").type("end")); + + assertEquals(1, validator.validateComponents(components).size()); + } + + @Test + void parameterValueBoolean() { + Parameter parameter = new Parameter() + .name("flag") + .value(true) + .in("query"); + + assertEquals(0, validator.validateParameter(parameter, "w1", null).size()); + } + + @Test + void parameterValueNumber() { + Parameter parameter = new Parameter() + .name("page") + .value(42) + .in("query"); + + assertEquals(0, validator.validateParameter(parameter, "w1", null).size()); + } + @Test void isValidJsonPointer() { assertTrue(new OpenAPIWorkflowValidator().isValidJsonPointer("/user/id")); From 9a498bdf15d49c87aad14046535db79bc5d685e1 Mon Sep 17 00:00:00 2001 From: Beppe Catanese Date: Sun, 29 Mar 2026 13:47:09 +0200 Subject: [PATCH 2/5] Extract Action base class to eliminate SuccessAction/FailureAction duplication Move shared fields name, type, workflowId, stepId, criteria and their getters/setters/addCriteria into a common Action base class. Both SuccessAction and FailureAction now extend it, retaining only their own fluent builder methods and FailureAction-specific retry fields. Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com> --- src/main/java/com/apiflows/model/Action.java | 60 +++++++++++++++++ .../com/apiflows/model/FailureAction.java | 65 ++---------------- .../com/apiflows/model/SuccessAction.java | 66 ++----------------- 3 files changed, 70 insertions(+), 121 deletions(-) create mode 100644 src/main/java/com/apiflows/model/Action.java diff --git a/src/main/java/com/apiflows/model/Action.java b/src/main/java/com/apiflows/model/Action.java new file mode 100644 index 0000000..e21cb30 --- /dev/null +++ b/src/main/java/com/apiflows/model/Action.java @@ -0,0 +1,60 @@ +package com.apiflows.model; + +import java.util.ArrayList; +import java.util.List; + +public abstract class Action { + + private String name; + private String type; + private String workflowId; + private String stepId; + private List criteria; + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public String getType() { + return type; + } + + public void setType(String type) { + this.type = type; + } + + public String getWorkflowId() { + return workflowId; + } + + public void setWorkflowId(String workflowId) { + this.workflowId = workflowId; + } + + public String getStepId() { + return stepId; + } + + public void setStepId(String stepId) { + this.stepId = stepId; + } + + public List getCriteria() { + return criteria; + } + + public void setCriteria(List criteria) { + this.criteria = criteria; + } + + public void addCriteria(Criterion criterion) { + if (this.criteria == null) { + this.criteria = new ArrayList<>(); + } + this.criteria.add(criterion); + } +} diff --git a/src/main/java/com/apiflows/model/FailureAction.java b/src/main/java/com/apiflows/model/FailureAction.java index 44e3047..29f31d5 100644 --- a/src/main/java/com/apiflows/model/FailureAction.java +++ b/src/main/java/com/apiflows/model/FailureAction.java @@ -1,49 +1,9 @@ package com.apiflows.model; -import java.util.ArrayList; -import java.util.List; +public class FailureAction extends Action { -public class FailureAction { - - private String name; - private String type; - private String workflowId; - private String stepId; private Double retryAfter; private Integer retryLimit; - private List criteria; - - public String getName() { - return name; - } - - public void setName(String name) { - this.name = name; - } - - public String getType() { - return type; - } - - public void setType(String type) { - this.type = type; - } - - public String getWorkflowId() { - return workflowId; - } - - public void setWorkflowId(String workflowId) { - this.workflowId = workflowId; - } - - public String getStepId() { - return stepId; - } - - public void setStepId(String stepId) { - this.stepId = stepId; - } public Double getRetryAfter() { return retryAfter; @@ -61,31 +21,23 @@ public void setRetryLimit(Integer retryLimit) { this.retryLimit = retryLimit; } - public List getCriteria() { - return criteria; - } - - public void setCriteria(List criteria) { - this.criteria = criteria; - } - public FailureAction name(String name) { - this.name = name; + setName(name); return this; } public FailureAction type(String type) { - this.type = type; + setType(type); return this; } public FailureAction stepId(String stepId) { - this.stepId = stepId; + setStepId(stepId); return this; } public FailureAction workflowId(String workflowId) { - this.workflowId = workflowId; + setWorkflowId(workflowId); return this; } @@ -98,11 +50,4 @@ public FailureAction retryLimit(Integer retryLimit) { this.retryLimit = retryLimit; return this; } - - public void addCriteria(Criterion criterion) { - if(this.criteria == null) { - this.criteria = new ArrayList<>(); - } - this.criteria.add(criterion); - } } diff --git a/src/main/java/com/apiflows/model/SuccessAction.java b/src/main/java/com/apiflows/model/SuccessAction.java index 89bf284..2671139 100644 --- a/src/main/java/com/apiflows/model/SuccessAction.java +++ b/src/main/java/com/apiflows/model/SuccessAction.java @@ -1,80 +1,24 @@ package com.apiflows.model; -import java.util.ArrayList; -import java.util.List; - -public class SuccessAction { - - private String name; - private String type; - private String workflowId; - private String stepId; - private List criteria; - - public String getName() { - return name; - } - - public void setName(String name) { - this.name = name; - } - - public String getType() { - return type; - } - - public void setType(String type) { - this.type = type; - } - - public String getWorkflowId() { - return workflowId; - } - - public void setWorkflowId(String workflowId) { - this.workflowId = workflowId; - } - - public String getStepId() { - return stepId; - } - - public void setStepId(String stepId) { - this.stepId = stepId; - } - - public List getCriteria() { - return criteria; - } - - public void setCriteria(List criteria) { - this.criteria = criteria; - } - - public void addCriteria(Criterion criterion) { - if(this.criteria == null) { - this.criteria = new ArrayList<>(); - } - this.criteria.add(criterion); - } +public class SuccessAction extends Action { public SuccessAction name(String name) { - this.name = name; + setName(name); return this; } public SuccessAction type(String type) { - this.type = type; + setType(type); return this; } public SuccessAction workflowId(String workflowId) { - this.workflowId = workflowId; + setWorkflowId(workflowId); return this; } public SuccessAction stepId(String stepId) { - this.stepId = stepId; + setStepId(stepId); return this; } } From 6deb973d45c7425e8aab3e2d943e9310ba0801c0 Mon Sep 17 00:00:00 2001 From: Beppe Catanese Date: Sun, 29 Mar 2026 13:49:18 +0200 Subject: [PATCH 3/5] Reduce cognitive complexity of validateStep below threshold Extract parameter-validation loop into validateStepParameters() to bring validateStep cognitive complexity from 17 down to 15 (S3776). Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com> --- .../parser/OpenAPIWorkflowValidator.java | 37 ++++++++++--------- 1 file changed, 19 insertions(+), 18 deletions(-) diff --git a/src/main/java/com/apiflows/parser/OpenAPIWorkflowValidator.java b/src/main/java/com/apiflows/parser/OpenAPIWorkflowValidator.java index 15d8722..256371b 100644 --- a/src/main/java/com/apiflows/parser/OpenAPIWorkflowValidator.java +++ b/src/main/java/com/apiflows/parser/OpenAPIWorkflowValidator.java @@ -181,24 +181,7 @@ List validateStep(Step step, String workflowId ) { } } - if(step.getParameters() != null) { - for(Parameter parameter : step.getParameters()) { - if(isRuntimeExpression(parameter.getReference())) { - // reference a reusable object - errors.addAll(validateReusableParameter(parameter, workflowId, null)); - } else { - // parameter - errors.addAll(validateParameter(parameter, workflowId, null)); - - if(step.getWorkflowId() == null) { - // when the step in context is NOT a workflowId the parameter IN must be defined - if(!isRuntimeExpression(parameter.getName()) && parameter.getIn() == null) { - errors.add("'Workflow[" + workflowId + "]' parameter IN must be defined"); - } - } - } - } - } + errors.addAll(validateStepParameters(step, workflowId)); if(step.getDependsOn() != null) { if(!stepExists(workflowId, step.getDependsOn())) { @@ -642,6 +625,24 @@ boolean isValidSourceDescriptionName(String name) { return Pattern.matches("^[A-Za-z0-9_\\-]+$", name); } + private List validateStepParameters(Step step, String workflowId) { + List errors = new ArrayList<>(); + if (step.getParameters() == null) { + return errors; + } + for (Parameter parameter : step.getParameters()) { + if (isRuntimeExpression(parameter.getReference())) { + errors.addAll(validateReusableParameter(parameter, workflowId, null)); + } else { + errors.addAll(validateParameter(parameter, workflowId, null)); + if (step.getWorkflowId() == null && !isRuntimeExpression(parameter.getName()) && parameter.getIn() == null) { + errors.add("'Workflow[" + workflowId + "]' parameter IN must be defined"); + } + } + } + return errors; + } + private List validateGotoTarget(String stepId, String actionLabel, String targetStepId, String targetWorkflowId) { List errors = new ArrayList<>(); if (targetWorkflowId == null && targetStepId == null) { From 6f9f7c5f53486de0c0ddd88137d74e5dfac2e1e3 Mon Sep 17 00:00:00 2001 From: Beppe Catanese Date: Sun, 29 Mar 2026 13:50:46 +0200 Subject: [PATCH 4/5] Replace duplicated 'Step ' string literal with STEP_PREFIX constant Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com> --- .../parser/OpenAPIWorkflowValidator.java | 40 ++++++++++--------- 1 file changed, 21 insertions(+), 19 deletions(-) diff --git a/src/main/java/com/apiflows/parser/OpenAPIWorkflowValidator.java b/src/main/java/com/apiflows/parser/OpenAPIWorkflowValidator.java index 256371b..13b94c3 100644 --- a/src/main/java/com/apiflows/parser/OpenAPIWorkflowValidator.java +++ b/src/main/java/com/apiflows/parser/OpenAPIWorkflowValidator.java @@ -8,6 +8,8 @@ public class OpenAPIWorkflowValidator { + private static final String STEP_PREFIX = "Step "; + private OpenAPIWorkflow openAPIWorkflow = null; Set workflowIds = new HashSet<>(); Map> stepIds = new HashMap<>(); @@ -283,16 +285,16 @@ List validateSuccessAction(String workflowId, String stepId, SuccessActi List errors = new ArrayList<>(); if (successAction.getName() == null || successAction.getName().isEmpty()) { - errors.add("Step " + stepId + " SuccessAction has no name"); + errors.add(STEP_PREFIX + stepId + " SuccessAction has no name"); } if (successAction.getType() == null) { - errors.add("Step " + stepId + " SuccessAction has no type"); + errors.add(STEP_PREFIX + stepId + " SuccessAction has no type"); } if (successAction.getType() != null) { if (!SUPPORTED_VALUES.contains(successAction.getType())) { - errors.add("Step " + stepId + " SuccessAction type (" + successAction.getType() + ") is invalid"); + errors.add(STEP_PREFIX + stepId + " SuccessAction type (" + successAction.getType() + ") is invalid"); } } @@ -300,12 +302,12 @@ List validateSuccessAction(String workflowId, String stepId, SuccessActi errors.addAll(validateGotoTarget(stepId, "SuccessAction", successAction.getStepId(), successAction.getWorkflowId())); if(successAction.getStepId() != null) { if (!stepExists(workflowId, successAction.getStepId())) { - errors.add("Step " + stepId + " SuccessAction stepId is invalid (no such a step exists)"); + errors.add(STEP_PREFIX + stepId + " SuccessAction stepId is invalid (no such a step exists)"); } } if(successAction.getWorkflowId() != null) { if(!workflowExists(successAction.getWorkflowId())) { - errors.add("Step " + stepId + " SuccessAction workflowId is invalid (no such a workflow exists)"); + errors.add(STEP_PREFIX + stepId + " SuccessAction workflowId is invalid (no such a workflow exists)"); } } } @@ -319,16 +321,16 @@ List validateFailureAction(String workflowId, String stepId, FailureActi List errors = new ArrayList<>(); if (failureAction.getName() == null || failureAction.getName().isEmpty()) { - errors.add("Step " + stepId + " FailureAction has no name"); + errors.add(STEP_PREFIX + stepId + " FailureAction has no name"); } if (failureAction.getType() == null) { - errors.add("Step " + stepId + " FailureAction has no type"); + errors.add(STEP_PREFIX + stepId + " FailureAction has no type"); } if (failureAction.getType() != null) { if (!SUPPORTED_VALUES.contains(failureAction.getType())) { - errors.add("Step " + stepId + " FailureAction type (" + failureAction.getType() + ") is invalid"); + errors.add(STEP_PREFIX + stepId + " FailureAction type (" + failureAction.getType() + ") is invalid"); } } @@ -337,12 +339,12 @@ List validateFailureAction(String workflowId, String stepId, FailureActi } if(failureAction.getRetryAfter() != null && failureAction.getRetryAfter() < 0) { - errors.add("Step " + stepId + " FailureAction retryAfter must be non-negative"); + errors.add(STEP_PREFIX + stepId + " FailureAction retryAfter must be non-negative"); } if(failureAction.getRetryLimit() != null && failureAction.getRetryLimit() < 0) { - errors.add("Step " + stepId + " FailureAction retryLimit must be non-negative"); + errors.add(STEP_PREFIX + stepId + " FailureAction retryLimit must be non-negative"); } @@ -350,7 +352,7 @@ List validateFailureAction(String workflowId, String stepId, FailureActi && (failureAction.getType().equals("goto") || failureAction.getType().equals("retry"))) { // when type `goto` or `retry` stepId must exist (if provided) if (!stepExists(workflowId, failureAction.getStepId())) { - errors.add("Step " + stepId + " FailureAction stepId is invalid (no such a step exists)"); + errors.add(STEP_PREFIX + stepId + " FailureAction stepId is invalid (no such a step exists)"); } } @@ -365,28 +367,28 @@ List validateCriterion(Criterion criterion, String stepId) { List errors = new ArrayList<>(); if(criterion.getCondition() == null) { - errors.add("Step " + stepId + " has no condition"); + errors.add(STEP_PREFIX + stepId + " has no condition"); } if (criterion.getType() != null) { if (!SUPPORTED_SIMPLE_TYPES.contains(criterion.getType())) { - errors.add("Step " + stepId + " SuccessCriteria type (" + criterion.getType() + ") is invalid"); + errors.add(STEP_PREFIX + stepId + " SuccessCriteria type (" + criterion.getType() + ") is invalid"); } if (criterion.getContext() == null) { - errors.add("Step " + stepId + " SuccessCriteria type is specified but context is not provided"); + errors.add(STEP_PREFIX + stepId + " SuccessCriteria type is specified but context is not provided"); } } CriterionExpressionType expressionType = criterion.getExpressionType(); if (expressionType != null) { if (expressionType.getType() == null || !SUPPORTED_EXPRESSION_TYPES.contains(expressionType.getType())) { - errors.add("Step " + stepId + " SuccessCriteria expressionType '" + expressionType.getType() + "' is invalid (must be jsonpath or xpath)"); + errors.add(STEP_PREFIX + stepId + " SuccessCriteria expressionType '" + expressionType.getType() + "' is invalid (must be jsonpath or xpath)"); } if (expressionType.getVersion() == null || expressionType.getVersion().isEmpty()) { - errors.add("Step " + stepId + " SuccessCriteria expressionType has no version"); + errors.add(STEP_PREFIX + stepId + " SuccessCriteria expressionType has no version"); } if (criterion.getContext() == null) { - errors.add("Step " + stepId + " SuccessCriteria type is specified but context is not provided"); + errors.add(STEP_PREFIX + stepId + " SuccessCriteria type is specified but context is not provided"); } } @@ -646,10 +648,10 @@ private List validateStepParameters(Step step, String workflowId) { private List validateGotoTarget(String stepId, String actionLabel, String targetStepId, String targetWorkflowId) { List errors = new ArrayList<>(); if (targetWorkflowId == null && targetStepId == null) { - errors.add("Step " + stepId + " " + actionLabel + " must define either workflowId or stepId"); + errors.add(STEP_PREFIX + stepId + " " + actionLabel + " must define either workflowId or stepId"); } if (targetWorkflowId != null && targetStepId != null) { - errors.add("Step " + stepId + " " + actionLabel + " cannot define both workflowId and stepId"); + errors.add(STEP_PREFIX + stepId + " " + actionLabel + " cannot define both workflowId and stepId"); } return errors; } From 9671faa6a05d20ebaeb0a20d371c354dd607ac34 Mon Sep 17 00:00:00 2001 From: Beppe Catanese Date: Sun, 29 Mar 2026 13:51:29 +0200 Subject: [PATCH 5/5] Remove unused BeforeEach import from OpenAPIWorkflowValidatorTest Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com> --- .../java/com/apiflows/parser/OpenAPIWorkflowValidatorTest.java | 1 - 1 file changed, 1 deletion(-) diff --git a/src/test/java/com/apiflows/parser/OpenAPIWorkflowValidatorTest.java b/src/test/java/com/apiflows/parser/OpenAPIWorkflowValidatorTest.java index b230d43..24f1af5 100644 --- a/src/test/java/com/apiflows/parser/OpenAPIWorkflowValidatorTest.java +++ b/src/test/java/com/apiflows/parser/OpenAPIWorkflowValidatorTest.java @@ -2,7 +2,6 @@ import com.apiflows.model.*; import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.BeforeEach; import java.util.*;