From 993d44b9b8a88a8af6857b2b85db79a42ece80fd Mon Sep 17 00:00:00 2001
From: Felix Kaspar <ich@felixkaspar.com>
Date: Thu, 21 Dec 2023 15:57:51 +0100
Subject: [PATCH] Workflow validation: Operator interop

---
 .../src/routes/api/workflow-controller.ts     |  4 +-
 shared-operations/src/functions/impose.ts     | 12 ++--
 shared-operations/src/functions/index.ts      | 12 ++--
 .../src/workflow/validateOperations.ts        | 60 +++++++++++++++----
 4 files changed, 63 insertions(+), 25 deletions(-)

diff --git a/server-node/src/routes/api/workflow-controller.ts b/server-node/src/routes/api/workflow-controller.ts
index 8b1951449..4ee0d6f8e 100644
--- a/server-node/src/routes/api/workflow-controller.ts
+++ b/server-node/src/routes/api/workflow-controller.ts
@@ -48,7 +48,7 @@ router.post("/:workflowUuid?", [
         if(req.body.async === "false") {
             console.log("Don't do async");
 
-            traverseOperations(workflow.operations, inputs, (state) => {
+            traverseOperations(workflow.actions, inputs, (state) => {
                 console.log("State: ", state);
             }).then(async (pdfResults) => {
                 console.log("Download");
@@ -88,7 +88,7 @@ router.post("/:workflowUuid?", [
                 }
             });
 
-            traverseOperations(workflow.operations, inputs, (state) => {
+            traverseOperations(workflow.actions, inputs, (state) => {
                 console.log("State: ", state);
                 if(activeWorkflow.eventStream)
                     activeWorkflow.eventStream.write(`data: ${state}\n\n`);
diff --git a/shared-operations/src/functions/impose.ts b/shared-operations/src/functions/impose.ts
index 19910776c..e6a254c3b 100644
--- a/shared-operations/src/functions/impose.ts
+++ b/shared-operations/src/functions/impose.ts
@@ -45,11 +45,11 @@ export class Impose extends Operator {
     static type: string = "impose";
 
     /**
-     * Validation
+     * Validation & Localisation
      */
 
-    static inputSchema = JoiPDFFileSchema.label(translationObject.inputs.pdfFile.name).description(translationObject.inputs.pdfFile.description);
-    static valueSchema = Joi.object({
+    protected static inputSchema = JoiPDFFileSchema.label(translationObject.inputs.pdfFile.name).description(translationObject.inputs.pdfFile.description);
+    protected static valueSchema = Joi.object({
         nup: Joi.number().integer().valid(2, 3, 4, 8, 9, 12, 16).required()
             .label(translationObject.operators.nup.values.nup.friendlyName).description(translationObject.operators.nup.values.nup.description)
             .example("3").example("4"),
@@ -92,12 +92,11 @@ export class Impose extends Operator {
             "JIS-B0", "JIS-B1", "JIS-B2", "JIS-B3", "JIS-B4", "JIS-B5", "JIS-B6",
             "JIS-B7", "JIS-B8", "JIS-B9", "JIS-B10", "JIS-B11", "JIS-B12",
             "Shirokuban4", "Shirokuban5", "Shirokuban6", "Kiku4", "Kiku5", "AB", "B40", "Shikisen"
-        ].flatMap(size => [size, size + "P", size + "L"]))
-        .required()
+        ].flatMap(size => [size, size + "P", size + "L"])).required()
             .label(translationObject.operators.nup.values.format.friendlyName).description(translationObject.operators.nup.values.format.description)
             .example("A4").example("A3L")
     });
-    static outputSchema = JoiPDFFileSchema.label(translationObject.outputs.pdfFile.name).description(translationObject.outputs.pdfFile.description);
+    protected static outputSchema = JoiPDFFileSchema.label(translationObject.outputs.pdfFile.name).description(translationObject.outputs.pdfFile.description);
 
     static schema = Joi.object({
         input: Impose.inputSchema.required(),
@@ -105,6 +104,7 @@ export class Impose extends Operator {
         output: Impose.outputSchema.optional()
     }).label(translationObject.operators.nup.friendlyName).description(translationObject.operators.nup.description);
 
+
     /**
      * Logic
      */
diff --git a/shared-operations/src/functions/index.ts b/shared-operations/src/functions/index.ts
index 07022e6c9..7d62cd086 100644
--- a/shared-operations/src/functions/index.ts
+++ b/shared-operations/src/functions/index.ts
@@ -18,10 +18,14 @@ export class Operator {
     static type: string;
 
     /** The Joi validators & decorators */
-    static inputSchema: Joi.Schema;
-    static valueSchema: Joi.Schema;
-    static outputSchema: Joi.Schema;
-    static schema: Joi.Schema;
+    protected static inputSchema: Joi.Schema;
+    protected static valueSchema: Joi.Schema;
+    protected static outputSchema: Joi.Schema;
+    static schema: Joi.ObjectSchema<{
+        input: Joi.Schema;
+        values: Joi.Schema;
+        output: Joi.Schema;
+    }>;
 
     actionValues: any;
 
diff --git a/shared-operations/src/workflow/validateOperations.ts b/shared-operations/src/workflow/validateOperations.ts
index 1cb94cd64..55039806a 100644
--- a/shared-operations/src/workflow/validateOperations.ts
+++ b/shared-operations/src/workflow/validateOperations.ts
@@ -1,26 +1,54 @@
+import { Operator } from "functions";
 import { Action } from "../../declarations/Action";
 import { getOperatorByName } from "./getOperatorByName";
 
 /** This function validates the "workflow-json" from the API */
 export function validateOperations(actions: Action[]): { valid: boolean, reason?: string} {
-    for (const action of actions) {
-        if (action.type === "wait" || action.type === "done") {
-            // TODO: Validate these too ):
-            return { valid: true };
-        }
-        else {
-            const operator = getOperatorByName(action.type);
-            if(!operator) {
-                return { valid: false, reason: `action.type ${action.type} does not exist` }
-            }
-            const validationResult = new operator(action).validate();
+    const done: Action[] = [];
 
-            if(!validationResult.valid) {
-                return validationResult;
+    for (const action of actions) {
+        if (action.type === "done") {
+            if(done[action.values.id] !== undefined) {
+                return { valid: false, reason: "There is a duplicate id in the done actions." };
             }
+            done[action.values.id] = action;
+            continue;
+        }
+        
+        const operator = getOperatorByName(action.type);
+        if(!operator) {
+            return { valid: false, reason: `action.type ${action.type} does not exist` }
+        }
+        const validationResult = new operator(action).validate();
+
+        if(!validationResult.valid) {
+            return validationResult;
         }
 
         if (action.actions) {
+            // Check io compatibility of the operators
+            for (const childAction of action.actions) {
+                if (childAction.type === "wait") {
+                    if(done[childAction.values.id] === undefined) {
+                        return { valid: false, reason: "There is a wait action that does not have an associated done action." };
+                    }
+
+                    for (const afterDoneChild of done[childAction.values.id].actions) {
+                        if(!ioCompatible(operator, getOperatorByName(afterDoneChild.type))) {
+                            return { valid: false, reason: `Ouput of action ${action.type} is not compatible with input of action ${afterDoneChild.type}` };
+                        }
+                    }
+                }
+                else if (action.type === "done") {
+                    return { valid: false, reason: `There shouldn't be a done action here.` };
+                }
+                else {
+                    if(!ioCompatible(operator, getOperatorByName(childAction.type))) {
+                        return { valid: false, reason: `Ouput of action ${action.type} is not compatible with input of action ${childAction.type}` };
+                    }
+                }
+            }
+
             const validationResult = validateOperations(action.actions);
 
             if(!validationResult.valid) {
@@ -29,4 +57,10 @@ export function validateOperations(actions: Action[]): { valid: boolean, reason?
         }
     }
     return { valid: true };
+}
+
+function ioCompatible(outputingOperator: typeof Operator, recievingOperator: typeof Operator): boolean {
+    const outputType = outputingOperator.schema.describe().keys.output.label;
+    const inputType = recievingOperator.schema.describe().keys.input.label;
+    return outputType == inputType;
 }
\ No newline at end of file