diff --git a/src/main/java/com/apiflows/model/Criterion.java b/src/main/java/com/apiflows/model/Criterion.java index 201f058..b2ca010 100644 --- a/src/main/java/com/apiflows/model/Criterion.java +++ b/src/main/java/com/apiflows/model/Criterion.java @@ -31,5 +31,20 @@ public String getType() { public void setType(String type) { this.type = type; } + + public Criterion condition(String condition) { + this.condition = condition; + return this; + } + + public Criterion context(String context) { + this.context = context; + return this; + } + + public Criterion type(String type) { + this.type = type; + return this; + } } diff --git a/src/main/java/com/apiflows/model/FailureAction.java b/src/main/java/com/apiflows/model/FailureAction.java index 67416e8..b045586 100644 --- a/src/main/java/com/apiflows/model/FailureAction.java +++ b/src/main/java/com/apiflows/model/FailureAction.java @@ -1,15 +1,27 @@ package com.apiflows.model; +import io.swagger.v3.oas.models.media.IntegerSchema; + +import java.util.ArrayList; import java.util.List; public class FailureAction { + private String type; private String workflowId; private String stepId; private Long retryAfter; private Integer retryLimit; private List criteria; + public String getType() { + return type; + } + + public void setType(String type) { + this.type = type; + } + public String getWorkflowId() { return workflowId; } @@ -49,4 +61,36 @@ public List getCriteria() { public void setCriteria(List criteria) { this.criteria = criteria; } + + public FailureAction type(String type) { + this.type = type; + return this; + } + + public FailureAction stepId(String stepId) { + this.stepId = stepId; + return this; + } + + public FailureAction workflowId(String workflowId) { + this.workflowId = workflowId; + return this; + } + + public FailureAction retryAfter(Long retryAfter) { + this.retryAfter = retryAfter; + return this; + } + + 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/Info.java b/src/main/java/com/apiflows/model/Info.java index 33af71c..7bcf2f2 100644 --- a/src/main/java/com/apiflows/model/Info.java +++ b/src/main/java/com/apiflows/model/Info.java @@ -34,4 +34,20 @@ public String getDescription() { public void setDescription(String description) { this.description = description; } + + public Info title(String title) { + this.title = title; + return this; + } + + public Info version(String version) { + this.version = version; + return this; + } + + public Info description(String description) { + this.description = description; + return this; + } + } diff --git a/src/main/java/com/apiflows/model/Parameter.java b/src/main/java/com/apiflows/model/Parameter.java index f73741e..3d4b39c 100644 --- a/src/main/java/com/apiflows/model/Parameter.java +++ b/src/main/java/com/apiflows/model/Parameter.java @@ -62,4 +62,35 @@ public void setStyle(String style) { public void set$ref(String $ref) { this.$ref = $ref; } + + public Parameter name(String name) { + this.name = name; + return this; + } + + public Parameter in(String in) { + this.in = in; + return this; + } + + public Parameter value(String value) { + this.value = value; + return this; + } + + public Parameter target(String target) { + this.target = target; + return this; + } + + public Parameter style(String style) { + this.style = style; + return this; + } + + public Parameter $ref(String $ref) { + this.$ref = $ref; + return this; + } + } diff --git a/src/main/java/com/apiflows/model/SourceDescription.java b/src/main/java/com/apiflows/model/SourceDescription.java index 23df13c..ad963e5 100644 --- a/src/main/java/com/apiflows/model/SourceDescription.java +++ b/src/main/java/com/apiflows/model/SourceDescription.java @@ -42,4 +42,20 @@ public boolean isOpenApi() { public boolean IsWorkflowsSpec() { return "workflowsSpec".equals(this.type); } + + public SourceDescription name(String name) { + this.name = name; + return this; + } + + public SourceDescription url(String url) { + this.url = url; + return this; + } + + public SourceDescription type(String type) { + this.type = type; + return this; + } + } diff --git a/src/main/java/com/apiflows/model/Step.java b/src/main/java/com/apiflows/model/Step.java index a80557d..7081e82 100644 --- a/src/main/java/com/apiflows/model/Step.java +++ b/src/main/java/com/apiflows/model/Step.java @@ -132,4 +132,69 @@ public List getOnFailure() { public void setOnFailure(List onFailure) { this.onFailure = onFailure; } + + public Step stepId(String stepId) { + this.setStepId(stepId); + return this; + } + + public Step operationId(String operationId) { + this.setOperationId(operationId); + return this; + } + + public Step operationRef(String operationRef) { + this.setOperationRef(operationRef); + return this; + } + + public Step operation(Operation operation) { + this.setOperation(operation); + return this; + } + + public Step workflowId(String workflowId) { + this.setWorkflowId(workflowId); + return this; + } + + public Step workflow(Workflow workflow) { + this.setWorkflow(workflow); + return this; + } + + public Step description(String description) { + this.setDescription(description); + return this; + } + + public Step dependsOn(String dependsOn) { + this.setDependsOn(dependsOn); + return this; + } + + public Step parameters(List parameters) { + this.setParameters(parameters); + return this; + } + + public Step successCriteria(List successCriteria) { + this.setSuccessCriteria(successCriteria); + return this; + } + + public Step outputs(Map outputs) { + this.setOutputs(outputs); + return this; + } + + public Step onSuccess(List onSuccess) { + this.setOnSuccess(onSuccess); + return this; + } + + public Step onFailure(List onFailure) { + this.setOnFailure(onFailure); + return this; + } } diff --git a/src/main/java/com/apiflows/model/SuccessAction.java b/src/main/java/com/apiflows/model/SuccessAction.java index 152bd4f..92a6792 100644 --- a/src/main/java/com/apiflows/model/SuccessAction.java +++ b/src/main/java/com/apiflows/model/SuccessAction.java @@ -1,5 +1,6 @@ package com.apiflows.model; +import java.util.ArrayList; import java.util.List; public class SuccessAction { @@ -40,4 +41,26 @@ public List getCriteria() { 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 SuccessAction type(String type) { + this.type = type; + return this; + } + + public SuccessAction workflowId(String workflowId) { + this.workflowId = workflowId; + return this; + } + + public SuccessAction stepId(String stepId) { + this.stepId = stepId; + return this; + } } diff --git a/src/main/java/com/apiflows/model/Workflow.java b/src/main/java/com/apiflows/model/Workflow.java index 9dbd3ab..e7f2e0b 100644 --- a/src/main/java/com/apiflows/model/Workflow.java +++ b/src/main/java/com/apiflows/model/Workflow.java @@ -62,6 +62,11 @@ public void setSteps(List steps) { this.steps = steps; } + public Workflow addStep(Step step) { + this.steps.add(step); + return this; + } + @JsonProperty("outputs") public Map getOutputs() { return outputs; @@ -70,4 +75,24 @@ public Map getOutputs() { public void setOutputs(Map outputs) { this.outputs = outputs; } + + public Workflow workflowId(String workflowId) { + this.workflowId = workflowId; + return this; + } + + public Workflow summary(String summary) { + this.summary = summary; + return this; + } + public Workflow description(String description) { + this.description = description; + return this; + } + + public Workflow inputs(Schema inputs) { + this.inputs = inputs; + return this; + } + } diff --git a/src/main/java/com/apiflows/parser/OpenAPIWorkflowParser.java b/src/main/java/com/apiflows/parser/OpenAPIWorkflowParser.java index 35cd4a0..ed793a2 100644 --- a/src/main/java/com/apiflows/parser/OpenAPIWorkflowParser.java +++ b/src/main/java/com/apiflows/parser/OpenAPIWorkflowParser.java @@ -67,7 +67,7 @@ public OpenAPIWorkflowParserResult parse(String input, ParseOptions options) { result.setOpenAPIWorkflow(openAPIWorkflow); if(options != null && options.isApplyValidation()) { - OpenAPIWorkflowValidatorResult validatorResult = new OpenAPIWorkflowValidator().validate(openAPIWorkflow); + OpenAPIWorkflowValidatorResult validatorResult = new OpenAPIWorkflowValidator(openAPIWorkflow).validate(); result.setValid(validatorResult.isValid()); result.setErrors(validatorResult.getErrors()); } diff --git a/src/main/java/com/apiflows/parser/OpenAPIWorkflowValidator.java b/src/main/java/com/apiflows/parser/OpenAPIWorkflowValidator.java index 1bdce72..cc9cc7e 100644 --- a/src/main/java/com/apiflows/parser/OpenAPIWorkflowValidator.java +++ b/src/main/java/com/apiflows/parser/OpenAPIWorkflowValidator.java @@ -1,172 +1,320 @@ package com.apiflows.parser; import com.apiflows.model.*; +import io.swagger.models.auth.In; -import java.util.Arrays; -import java.util.HashSet; -import java.util.List; +import java.util.*; import java.util.regex.Pattern; public class OpenAPIWorkflowValidator { - public OpenAPIWorkflowValidatorResult validate(OpenAPIWorkflow openAPIWorkflow) { + private OpenAPIWorkflow openAPIWorkflow = null; + private Set workflowIds = new HashSet<>(); + private Map> stepIds = new HashMap<>(); + + OpenAPIWorkflowValidator() { + } + + public OpenAPIWorkflowValidator(OpenAPIWorkflow openAPIWorkflow) { + this.openAPIWorkflow = openAPIWorkflow; + } + + public OpenAPIWorkflowValidatorResult validate() { + + if(this.openAPIWorkflow == null) { + throw new RuntimeException("OpenAPIWorkflow is not provided"); + } + + loadWorkflowIds(this.openAPIWorkflow.getWorkflows()); + loadStepIds(this.openAPIWorkflow.getWorkflows()); + OpenAPIWorkflowValidatorResult result = new OpenAPIWorkflowValidatorResult(); if (openAPIWorkflow.getWorkflowsSpec() == null || openAPIWorkflow.getWorkflowsSpec().isEmpty()) { result.addError("'workflowsSpec' is undefined"); } - if (openAPIWorkflow.getInfo() == null) { - result.addError("'Info' is undefined"); + result.addErrors(validateInfo(openAPIWorkflow.getInfo())); + + result.addErrors(validateSourceDescriptions(openAPIWorkflow.getSourceDescriptions())); + + if (openAPIWorkflow.getWorkflows() == null || openAPIWorkflow.getWorkflows().isEmpty()) { + result.addError("'Workflows' is undefined"); } - if (openAPIWorkflow.getInfo() != null && (openAPIWorkflow.getInfo().getTitle() == null || openAPIWorkflow.getInfo().getTitle().isEmpty())) { - result.addError("'Info title' is undefined"); + + if (openAPIWorkflow.getWorkflows() != null) { + for (Workflow workflow : openAPIWorkflow.getWorkflows()) { + int i = 0; + + result.addErrors(validateWorkflow(workflow, i)); + + for (Step step : workflow.getSteps()) { + result.addErrors(validateStep(step, workflow.getWorkflowId())); + } + } } - if (openAPIWorkflow.getInfo() != null && (openAPIWorkflow.getInfo().getVersion() == null || openAPIWorkflow.getInfo().getVersion().isEmpty())) { - result.addError("'Info version' is undefined"); + + if(!result.getErrors().isEmpty()) { + result.setValid(false); } - if (openAPIWorkflow.getSourceDescriptions() == null || openAPIWorkflow.getSourceDescriptions().isEmpty()) { - result.addError("'SourceDescriptions' is undefined"); + return result; + } + + List validateInfo(Info info) { + List errors = new ArrayList<>(); + + if (info == null) { + errors.add("'Info' is undefined"); + } + if (info != null && (info.getTitle() == null || info.getTitle().isEmpty())) { + errors.add("'Info title' is undefined"); + } + if (info != null && (info.getVersion() == null || info.getVersion().isEmpty())) { + errors.add("'Info version' is undefined"); } - if (openAPIWorkflow.getSourceDescriptions() != null) { + return errors; + } + + List validateSourceDescriptions(List sourceDescriptions) { + List SUPPORTED_TYPES = Arrays.asList("openapi", "workflowsSpec"); + + List errors = new ArrayList<>(); + + if (sourceDescriptions == null) { + errors.add("'SourceDescriptions' is undefined"); + } + + if (sourceDescriptions != null) { int i = 0; - for (SourceDescription sourceDescription : openAPIWorkflow.getSourceDescriptions()) { + for (SourceDescription sourceDescription : sourceDescriptions) { if (sourceDescription.getName() == null || sourceDescription.getName().isEmpty()) { - result.addError("'SourceDescription[" + i + "] name' is undefined"); + errors.add("'SourceDescription[" + i + "] name' is undefined"); } if (sourceDescription.getUrl() == null || sourceDescription.getUrl().isEmpty()) { - result.addError("'SourceDescription[" + i + "] url' is undefined"); + errors.add("'SourceDescription[" + i + "] url' is undefined"); } if (sourceDescription.getType() != null) { - List supportedValues = Arrays.asList("openapi", "workflowsSpec"); - if(!supportedValues.contains(sourceDescription.getType())) { - result.addError("'SourceDescription[" + i + "] type' is invalid"); + if(!SUPPORTED_TYPES.contains(sourceDescription.getType())) { + errors.add("'SourceDescription[" + i + "] type' is invalid"); } } i++; } if(i == 0) { - result.addError("'SourceDescriptions' is empty"); + errors.add("'SourceDescriptions' is empty"); } } - if (openAPIWorkflow.getWorkflows() == null || openAPIWorkflow.getWorkflows().isEmpty()) { - result.addError("'Workflows' is undefined"); + + return errors; + } + + List validateWorkflow(Workflow workflow, int index ){ + List errors = new ArrayList<>(); + + if (workflow.getWorkflowId() == null || workflow.getWorkflowId().isEmpty()) { + errors.add("'Workflow[" + index + "] workflowId' is undefined"); } - if (openAPIWorkflow.getWorkflows() != null) { - for (Workflow workflow : openAPIWorkflow.getWorkflows()) { - int i = 0; + if (workflow.getWorkflowId() != null && !isValidWorkflowId(workflow.getWorkflowId())) { + errors.add("WorkflowId " + workflow.getWorkflowId() + " format is invalid (should match regex " + getWorkflowIdRegularExpression() + ")"); + } - if (workflow.getWorkflowId() == null || workflow.getWorkflowId().isEmpty()) { - result.addError("'Workflow[" + i + "] workflowId' is undefined"); - } + if (workflow.getSteps() == null || workflow.getSteps().isEmpty()) { + errors.add("'Workflow " + workflow.getWorkflowId() + "' no Steps are undefined"); + } - if (!isValidWorkflowId(workflow.getWorkflowId())) { - result.addError("'Workflow[" + i + "] workflowId' format is invalid (should match regex " + getWorkflowIdRegularExpression() + ")"); - } + for (String key : workflow.getOutputs().keySet()) { + if(!isValidOutputsKey(key)) { + errors.add("Workflow[" + workflow.getWorkflowId() + "] Outputs key is invalid (should match regex " + getOutputsKeyRegularExpression() + ")"); + } + } - if (workflow.getSteps() == null) { - result.addError("'Workflow " + workflow.getWorkflowId() + "' no Steps are undefined"); - } - int j = 0; - HashSet stepIds = new HashSet<>(); - for (Step step : workflow.getSteps()) { - if (step.getStepId() == null || step.getStepId().isEmpty()) { - result.addError("'Workflow[" + workflow.getWorkflowId() + "] stepId' is undefined"); - } else { - if(stepIds.contains(step.getStepId())) { - result.addError("'Workflow[" + workflow.getWorkflowId() + "] stepId' " + step.getStepId() + " is not unique"); - } else { - stepIds.add(step.getStepId()); - } - } - if (!isValidStepId(step.getStepId())) { - result.addError("'Workflow[" + workflow.getWorkflowId() + "] stepId' is invalid (should match regex " + getStepIdRegularExpression() + ")"); - } - if (step.getOperationId() == null && step.getWorkflowId() == null && step.getOperationRef() == null) { - result.addError("'Workflow[" + workflow.getWorkflowId() + "]' should provide at least one of the following: [operationId, operationRef, workflowId]"); - } + return errors; + } - int numAssignedValues = (step.getOperationId() != null ? 1 : 0) + - (step.getWorkflowId() != null ? 1 : 0) + - (step.getOperationRef() != null ? 1 : 0); + List validateStep(Step step, String workflowId ) { + List errors = new ArrayList<>(); - if (numAssignedValues != 1) { - result.addError("'Workflow[" + workflow.getWorkflowId() + "]' should provide only one of the following: [operationId, operationRef, workflowId]"); - } + String stepId = step.getStepId(); - if(step.getParameters() != null) { - for(Parameter parameter : step.getParameters()) { - if(parameter.get$ref() != null) { - // Reference object - // check is URI - } else { - // Parameter object - if(parameter.getName() == null) { - result.addError("'Workflow[" + workflow.getWorkflowId() + "]' parameter has no name"); - } - if(parameter.getIn() == null) { - result.addError("'Workflow[" + workflow.getWorkflowId() + "]' parameter has no type"); - } - if(parameter.getIn() != null) { - List supportedValues = Arrays.asList("path", "query", "header", "cookie", "body", "workflow"); - if(!supportedValues.contains(parameter.getIn())) { - result.addError("'Workflow[" + workflow.getWorkflowId() + "]' parameter type (" + parameter.getIn() + ") is invalid"); - } - } - if(parameter.getValue() == null) { - result.addError("'Workflow[" + workflow.getWorkflowId() + "]' parameter has no value"); - } - } - } - } - j++; - - if(step.getDependsOn() != null) { - boolean matchingStep = false; - for (Step s : workflow.getSteps()) { - if(s.getStepId().equals(step.getDependsOn())) { - matchingStep = true; - break; - } - } - if(!matchingStep) { - result.addError("'Workflow[" + workflow.getWorkflowId() + "] stepId' " + step.getStepId() + " 'dependsOn' is invalid (no such a step exists)"); - } - } + if (stepId == null || stepId.isEmpty()) { + errors.add("'Workflow[" + workflowId + "] stepId' is undefined"); + } + + if (stepId != null && !isValidStepId(stepId)) { + errors.add("'Step " + stepId + " is invalid (should match regex " + getStepIdRegularExpression() + ")"); + } + + int numAssignedValues = (step.getOperationId() != null ? 1 : 0) + + (step.getWorkflowId() != null ? 1 : 0) + + (step.getOperationRef() != null ? 1 : 0); - for(SuccessAction successAction : step.getOnSuccess()) { - if(successAction.getType() == null) { - result.addError("'Workflow[" + workflow.getWorkflowId() + "]' step SuccessAction has no type"); - } - if(successAction.getType() != null) { - List supportedValues = Arrays.asList("end", "goto"); - if(!supportedValues.contains(successAction.getType())) { - result.addError("'Workflow[" + workflow.getWorkflowId() + "]' step SuccessAction type (" + successAction.getType() + ") is invalid"); - } - } + if (numAssignedValues != 1) { + if(stepId != null) { + errors.add("'Step " + stepId + " should provide only one of the following: [operationId, operationRef, workflowId]"); + } else { + errors.add("'Workflow[" + workflowId + "]' should provide only one of the following: [operationId, operationRef, workflowId]"); + } + } + + if(step.getParameters() != null) { + for(Parameter parameter : step.getParameters()) { + errors.addAll(validateParameter(parameter, workflowId)); + } + } + + if(step.getDependsOn() != null) { + if(this.stepIds.get(workflowId) == null || !this.stepIds.get(workflowId).contains(step.getDependsOn())) { + errors.add("'Step " + stepId + " 'dependsOn' is invalid (no such a step exists)"); + } + + if(step.getDependsOn().equals(stepId)) { + errors.add("'Step " + stepId + " 'dependsOn' is invalid (same value as stepId)"); + } + } + + for(Criterion criterion : step.getSuccessCriteria()) { + errors.addAll(validateCriterion(criterion, stepId)); + } + + for(SuccessAction successAction: step.getOnSuccess()) { + errors.addAll(validateSuccessAction(successAction, stepId)); + } + + for(FailureAction failureAction : step.getOnFailure()) { + errors.addAll(validateFailureAction(failureAction, stepId)); + + } + + return errors; + } + + List validateParameter(Parameter parameter, String workflowId ) { + List SUPPORTED_VALUES = Arrays.asList("path", "query", "header", "cookie", "body", "workflow"); + + List errors = new ArrayList<>(); + + if(parameter.get$ref() != null) { + // Reference object + // check is URI + } else { + // Parameter object + String name = parameter.getName(); + + if(name == null) { + errors.add("'Workflow[" + workflowId + "]' parameter has no name"); + } + if(parameter.getIn() == null) { + if(name != null) { + errors.add("Parameter '" + name + "' has no type"); + } else { + errors.add("'Workflow[" + workflowId + "]' parameter has no type"); + } + } + if(parameter.getIn() != null) { + if(!SUPPORTED_VALUES.contains(parameter.getIn())) { + if(name != null) { + errors.add("Parameter '" + name + "' type (" + parameter.getIn() + ") is invalid"); + } else { + errors.add("'Workflow[" + workflowId + "]' parameter type (" + parameter.getIn() + ") is invalid"); } } - if(j == 0) { - result.addError("'Workflow " + workflow.getWorkflowId() + "' no Steps are undefined"); + } + if(parameter.getValue() == null) { + if(name != null) { + errors.add("Parameter '" + name + "' has no value"); + } else { + errors.add("'Workflow[" + workflowId + "]' parameter has no value"); } + } + } + return errors; + } - for (String key : workflow.getOutputs().keySet()) { - if(!isValidOutputsKey(key)) { - result.addError("Workflow[" + workflow.getWorkflowId() + "] Outputs key is invalid (should match regex " + getOutputsKeyRegularExpression() + ")"); - } - } + List validateSuccessAction(SuccessAction successAction, String stepId) { + List SUPPORTED_VALUES = Arrays.asList("end", "goto"); + + List errors = new ArrayList<>(); + + if (successAction.getType() == null) { + errors.add("Step " + 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"); } } - - return result; + + if(successAction.getWorkflowId() == null && successAction.getStepId() == null) { + errors.add("Step " + stepId + " SuccessAction must define either workflowId or stepId"); + } + + if(successAction.getWorkflowId() != null && successAction.getStepId() != null) { + errors.add("Step " + stepId + " SuccessAction cannot define both workflowId and stepId"); + } + + return errors; } + List validateFailureAction(FailureAction failureAction, String stepId) { + List SUPPORTED_VALUES = Arrays.asList("end", "retry", "goto"); + + List errors = new ArrayList<>(); + + if (failureAction.getType() == null) { + errors.add("Step " + 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"); + } + } + + if(failureAction.getWorkflowId() == null && failureAction.getStepId() == null) { + errors.add("Step " + stepId + " FailureAction must define either workflowId or stepId"); + } + + if(failureAction.getWorkflowId() != null && failureAction.getStepId() != null) { + errors.add("Step " + stepId + " FailureAction cannot define both workflowId and stepId"); + } + + if(failureAction.getRetryAfter() != null && failureAction.getRetryAfter() < 0) { + errors.add("Step " + stepId + " FailureAction retryAfter must be non-negative"); + + } + + if(failureAction.getRetryLimit() != null && failureAction.getRetryLimit() < 0) { + errors.add("Step " + stepId + " FailureAction retryLimit must be non-negative"); + + } + + return errors; + } + + List validateCriterion(Criterion criterion, String stepId) { + List SUPPORTED_VALUES = Arrays.asList("simple", "regex", "JSONPath"); + + List errors = new ArrayList<>(); + + if(criterion.getCondition() == null) { + 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"); + } + + return errors; + } + + boolean isValidWorkflowId(String workflowId) { return Pattern.matches(getWorkflowIdRegularExpression(), workflowId); } @@ -190,4 +338,44 @@ String getOutputsKeyRegularExpression() { return "^[a-zA-Z0-9\\.\\-_]+$"; } + List loadWorkflowIds(List workflows) { + List errors = new ArrayList<>(); + + if(workflows != null) { + for(Workflow workflow : workflows) { + if(!this.workflowIds.add(workflow.getWorkflowId())) { + // id already exists + errors.add("WorkflowId is not unique: " + workflow.getWorkflowId()); + } + } + } + + return errors; + } + + List loadStepIds(List workflows) { + List errors = new ArrayList<>(); + + if(workflows != null) { + for(Workflow workflow : workflows) { + Set steps = this.stepIds.get(workflow.getWorkflowId()); + + if(steps == null) { + steps = new HashSet<>(); + } + + for(Step step : workflow.getSteps()) { + if(!steps.add(step.getStepId())) { + // id already exists + errors.add("StepId is not unique: " + step.getStepId()); + } + } + + this.stepIds.put(workflow.getWorkflowId(), steps); + } + } + + return errors; + } + } diff --git a/src/main/java/com/apiflows/parser/OpenAPIWorkflowValidatorResult.java b/src/main/java/com/apiflows/parser/OpenAPIWorkflowValidatorResult.java index 90251e9..aae451f 100644 --- a/src/main/java/com/apiflows/parser/OpenAPIWorkflowValidatorResult.java +++ b/src/main/java/com/apiflows/parser/OpenAPIWorkflowValidatorResult.java @@ -27,6 +27,9 @@ public void setErrors(List errors) { public void addError(String error) { this.errors.add(error); - this.valid = false; + } + + public void addErrors(List errors) { + this.errors.addAll(errors); } } diff --git a/src/test/java/com/apiflows/parser/OpenAPIWorkflowValidatorTest.java b/src/test/java/com/apiflows/parser/OpenAPIWorkflowValidatorTest.java index 03850ed..b3c4b15 100644 --- a/src/test/java/com/apiflows/parser/OpenAPIWorkflowValidatorTest.java +++ b/src/test/java/com/apiflows/parser/OpenAPIWorkflowValidatorTest.java @@ -1,22 +1,419 @@ package com.apiflows.parser; -import com.apiflows.model.OpenAPIWorkflow; +import com.apiflows.model.*; import org.junit.jupiter.api.Test; +import java.util.ArrayList; +import java.util.List; + import static org.junit.jupiter.api.Assertions.*; class OpenAPIWorkflowValidatorTest { + OpenAPIWorkflowValidator validator = new OpenAPIWorkflowValidator(); + @Test void validate() { OpenAPIWorkflow openAPIWorkflow = new OpenAPIWorkflow(); - OpenAPIWorkflowValidatorResult result = new OpenAPIWorkflowValidator().validate(openAPIWorkflow); + OpenAPIWorkflowValidatorResult result = new OpenAPIWorkflowValidator(openAPIWorkflow).validate(); assertFalse(result.isValid()); assertFalse(result.getErrors().isEmpty()); assertEquals("'workflowsSpec' is undefined", result.getErrors().get(0)); } + @Test + void validateInfoVersion() { + Info info = new Info(); + info.setTitle("title"); + + assertEquals(1, validator.validateInfo(info).size()); + } + + @Test + void validateSourceDescriptions() { + List sourceDescriptions = null; + + assertEquals(1, validator.validateSourceDescriptions(sourceDescriptions).size()); + } + + @Test + void validateSourceDescriptionsWithoutUrl() { + List sourceDescriptions = new ArrayList<>(); + sourceDescriptions.add(new SourceDescription() + .name("Source one") + .type("openapi") + .url(null)); + assertEquals(1, validator.validateSourceDescriptions(sourceDescriptions).size()); + } + + @Test + void validateSourceDescriptionsInvalidType() { + List sourceDescriptions = new ArrayList<>(); + sourceDescriptions.add(new SourceDescription() + .name("Source one") + .type("unkwown") + .url("https://example.com/spec.json")); + assertEquals(1, validator.validateSourceDescriptions(sourceDescriptions).size()); + } + + @Test + void validateWorkflowWithoutWorkflowId() { + Workflow workflow = new Workflow() + .workflowId(null) + .addStep(new Step() + .stepId("step-one")); + int index = 0; + + assertEquals(1, validator.validateWorkflow(workflow, index).size()); + } + + @Test + void validateWorkflowWithoutSteps() { + Workflow workflow = new Workflow() + .workflowId("workflow-id-1"); + int index = 0; + + assertEquals(1, validator.validateWorkflow(workflow, index).size()); + } + + @Test + void validateStep() { + Step step = new Step() + .stepId("step-one") + .description("First step in the workflow") + .operationId("openapi-endpoint"); + String worklowId = "q1"; + + assertEquals(0, validator.validateStep(step, worklowId).size()); + } + + @Test + void validateStepMissingStepId() { + Step step = new Step() + .stepId(null) + .description("First step in the workflow") + .operationId("openapi-endpoint"); + String worklowId = "q1"; + + assertEquals(1, validator.validateStep(step, worklowId).size()); + } + + @Test + void validateStepMissingEntity() { + Step step = new Step() + .stepId("step-one") + .description("First step in the workflow") + .operationId(null) + .workflowId(null) + .operationRef(null); + String worklowId = "q1"; + + assertEquals(1, validator.validateStep(step, worklowId).size()); + } + + @Test + void validateStepDependsOn() { + final String WORKFLOW_ID = "q1"; + + List workflows = List.of( + new Workflow() + .workflowId(WORKFLOW_ID) + .addStep(new Step() + .stepId("step-one")) + .addStep(new Step() + .stepId("step-two")) + ); + + validator.loadStepIds(workflows); + + Step step = new Step() + .stepId("step-two") + .description("Second step in the workflow") + .operationId("openapi-endpoint") + .dependsOn("step-one"); + + assertEquals(0, validator.validateStep(step, WORKFLOW_ID).size()); + } + + @Test + void validateStepDependsOnMissingStep() { + final String WORKFLOW_ID = "q1"; + + List workflows = List.of( + new Workflow() + .workflowId(WORKFLOW_ID) + .addStep(new Step() + .stepId("step-one")) + .addStep(new Step() + .stepId("step-two")) + ); + + validator.loadStepIds(workflows); + + Step step = new Step() + .stepId("step-two") + .description("Second step in the workflow") + .operationId("openapi-endpoint") + .dependsOn("step-three"); + + assertEquals(1, validator.validateStep(step, WORKFLOW_ID).size()); + } + + @Test + void validateStepDependsOnSelf() { + final String WORKFLOW_ID = "q1"; + + List workflows = List.of( + new Workflow() + .workflowId(WORKFLOW_ID) + .addStep(new Step() + .stepId("step-one")) + .addStep(new Step() + .stepId("step-two")) + ); + + validator.loadStepIds(workflows); + + Step step = new Step() + .stepId("step-one") + .description("Second step in the workflow") + .operationId("openapi-endpoint") + .dependsOn("step-one"); + + assertEquals(1, validator.validateStep(step, WORKFLOW_ID).size()); + } + + @Test + void validateParameter() { + Parameter parameter = new Parameter() + .name("param") + .value("1") + .in("path"); + String worklowId = "q1"; + + assertEquals(0, validator.validateParameter(parameter, worklowId).size()); + } + + @Test + void validateParameterInvalidIn() { + Parameter parameter = new Parameter() + .name("param") + .value("1") + .in("dummy"); + String worklowId = "q1"; + + assertEquals(1, validator.validateParameter(parameter, worklowId).size()); + } + + @Test + void validateParameterWithoutValue() { + Parameter parameter = new Parameter() + .name("param") + .value(null) + .in("query"); + String worklowId = "q1"; + + assertEquals(1, validator.validateParameter(parameter, worklowId).size()); + } + + @Test + void validateSuccessAction() { + String stepId = "step-one"; + SuccessAction successAction = new SuccessAction() + .type("end") + .stepId("step-one"); + + successAction.addCriteria( + new Criterion() + .context("$statusCode == 200")); + + assertEquals(0, validator.validateSuccessAction(successAction, stepId).size()); + } + + @Test + void validateSuccessActionInvalidType() { + String stepId = "step-one"; + SuccessAction successAction = new SuccessAction() + .type("invalid-type") + .stepId("step-one"); + + successAction.addCriteria( + new Criterion() + .context("$statusCode == 200")); + + assertEquals(1, validator.validateSuccessAction(successAction, stepId).size()); + } + + @Test + void validateSuccessActionMissingEntity() { + String stepId = "step-one"; + SuccessAction successAction = new SuccessAction() + .type("end") + .stepId(null) + .workflowId(null); + + successAction.addCriteria( + new Criterion() + .context("$statusCode == 200")); + + assertEquals(1, validator.validateSuccessAction(successAction, stepId).size()); + } + + @Test + void validateSuccessActionInvalidEntity() { + String stepId = "step-one"; + SuccessAction successAction = new SuccessAction() + .type("end") + .stepId("step-one") + .workflowId("workflow-id"); + + successAction.addCriteria( + new Criterion() + .condition("$statusCode == 200")); + + assertEquals(1, validator.validateSuccessAction(successAction, stepId).size()); + } + + @Test + void validateCriterion() { + String stepId = "step-one"; + + Criterion criterion = new Criterion() + .condition("$statusCode == 200") + .type("simple"); + + assertEquals(0, validator.validateCriterion(criterion, stepId).size()); + } + + @Test + void validateCriterionWithoutType() { + String stepId = "step-one"; + + Criterion criterion = new Criterion() + .condition("$statusCode == 200"); + + assertEquals(0, validator.validateCriterion(criterion, stepId).size()); + } + @Test + void validateCriterionInvalidType() { + String stepId = "step-one"; + + Criterion criterion = new Criterion() + .condition("$statusCode == 200") + .type("dummy"); + + assertEquals(1, validator.validateCriterion(criterion, stepId).size()); + } + + @Test + void validateFailureAction() { + String stepId = "step-one"; + FailureAction failureAction = new FailureAction() + .type("retry") + .stepId("step-one") + .retryAfter(1000L) + .retryLimit(3); + + failureAction.addCriteria( + new Criterion() + .context("$statusCode == 200")); + + assertEquals(0, validator.validateFailureAction(failureAction, stepId).size()); + } + + @Test + void validateFailureActionInvalidType() { + String stepId = "step-one"; + FailureAction failureAction = new FailureAction() + .type("dummy") + .stepId("step-one") + .retryAfter(1000L) + .retryLimit(3); + + failureAction.addCriteria( + new Criterion() + .context("$statusCode == 200")); + + assertEquals(1, validator.validateFailureAction(failureAction, stepId).size()); + } + + @Test + void validateFailureActionInvalidRetrySettings() { + String stepId = "step-one"; + FailureAction failureAction = new FailureAction() + .type("retry") + .stepId("step-one") + .retryAfter(-1000L) + .retryLimit(-3); + + failureAction.addCriteria( + new Criterion() + .context("$statusCode == 200")); + + assertEquals(2, validator.validateFailureAction(failureAction, stepId).size()); + } + + @Test + void validateFailureActionMissingEntity() { + String stepId = "step-one"; + FailureAction failureAction = new FailureAction() + .type("retry") + .stepId(null) + .workflowId(null) + .retryAfter(1000L) + .retryLimit(3); + + failureAction.addCriteria( + new Criterion() + .context("$statusCode == 200")); + + assertEquals(1, validator.validateFailureAction(failureAction, stepId).size()); + } + + @Test + void validateFailureActionInvalidEntity() { + String stepId = "step-one"; + FailureAction failureAction = new FailureAction() + .type("retry") + .stepId("step-one") + .workflowId("workflow-test") + .retryAfter(1000L) + .retryLimit(3); + + failureAction.addCriteria( + new Criterion() + .context("$statusCode == 200")); + + assertEquals(1, validator.validateFailureAction(failureAction, stepId).size()); + } + + @Test + void loadWorkflowIWithDuplicateIds() { + List list = List.of( + new Workflow() + .workflowId("one"), + new Workflow() + .workflowId("one")); + + assertEquals(1, validator.loadWorkflowIds(list).size()); + } + + @Test + void loadStepsWithDuplicateIds() { + List list = List.of( + new Workflow() + .workflowId("one") + .addStep(new Step() + .stepId("step-ABC")) + .addStep(new Step() + .stepId("step-ABC")) + ); + + assertEquals(1, validator.loadStepIds(list).size()); + } + + @Test void validWorkflowId() { assertTrue(new OpenAPIWorkflowValidator().isValidWorkflowId("idOfTheWorkflow_1")); @@ -26,4 +423,20 @@ void validWorkflowId() { void invalidWorkflowId() { assertFalse(new OpenAPIWorkflowValidator().isValidWorkflowId("workflow id")); } + + @Test + void validOutputsKey() { + assertTrue(new OpenAPIWorkflowValidator().isValidOutputsKey("tokenExpires")); + } + + @Test + void inalidOutputsKey() { + assertFalse(new OpenAPIWorkflowValidator().isValidOutputsKey("$tokenExpires")); + } + + @Test + void invalidOutputsKeyWithSpace() { + assertFalse(new OpenAPIWorkflowValidator().isValidOutputsKey("$token Expires")); + } + } \ No newline at end of file