Repository: serverlessworkflow/sdk-java
Pull request #234 (2023-07-04 16:24:27)

[4.0.x] Key property is missing in the Workflow
Cherry-pick for #228


Patch:
diff --git a/README.md b/README.md
index d7e6ac9..de0f3fb 100644
--- a/README.md
+++ b/README.md
@@ -20,6 +20,8 @@ to parse and validate workflow definitions as well as generate the workflow diag
 
 | Latest Releases | Conformance to spec version |
 | :---: | :---: |
+| [4.0.4.Final](https://github.com/serverlessworkflow/sdk-java/releases/) | [v0.8](https://github.com/serverlessworkflow/specification/tree/0.8.x) |
+| [4.0.3.Final](https://github.com/serverlessworkflow/sdk-java/releases/) | [v0.8](https://github.com/serverlessworkflow/specification/tree/0.8.x) |
 | [4.0.2.Final](https://github.com/serverlessworkflow/sdk-java/releases/) | [v0.8](https://github.com/serverlessworkflow/specification/tree/0.8.x) |
 | [3.0.0.Final](https://github.com/serverlessworkflow/sdk-java/releases/) | [v0.7](https://github.com/serverlessworkflow/specification/tree/0.7.x) |
 | [2.0.0.Final](https://github.com/serverlessworkflow/sdk-java/releases/) | [v0.6](https://github.com/serverlessworkflow/specification/tree/0.6.x) |
@@ -64,31 +66,31 @@ b) Add the following dependencies to your pom.xml `dependencies` section:
 <dependency>
     <groupId>io.serverlessworkflow</groupId>
     <artifactId>serverlessworkflow-api</artifactId>
-    <version>4.0.2.Final</version>
+    <version>4.0.4.Final</version>
 </dependency>
 
 <dependency>
     <groupId>io.serverlessworkflow</groupId>
     <artifactId>serverlessworkflow-spi</artifactId>
-    <version>4.0.2.Final</version>
+    <version>4.0.4.Final</version>
 </dependency>
 
 <dependency>
     <groupId>io.serverlessworkflow</groupId>
     <artifactId>serverlessworkflow-validation</artifactId>
-    <version>4.0.2.Final</version>
+    <version>4.0.4.Final</version>
 </dependency>
 
 <dependency>
     <groupId>io.serverlessworkflow</groupId>
     <artifactId>serverlessworkflow-diagram</artifactId>
-    <version>4.0.2.Final</version>
+    <version>4.0.4.Final</version>
 </dependency>
 
 <dependency>
     <groupId>io.serverlessworkflow</groupId>
     <artifactId>serverlessworkflow-util</artifactId>
-    <version>4.0.2.Final</version>
+    <version>4.0.4.Final</version>
 </dependency>
 ```
 
@@ -103,11 +105,11 @@ maven { url "https://oss.sonatype.org/content/repositories/snapshots" }
 b) Add the following dependencies to your build.gradle `dependencies` section:
 
 ```text
-implementation("io.serverlessworkflow:serverlessworkflow-api:4.0.2.Final")
-implementation("io.serverlessworkflow:serverlessworkflow-spi:4.0.2.Final")
-implementation("io.serverlessworkflow:serverlessworkflow-validation:4.0.2.Final")
-implementation("io.serverlessworkflow:serverlessworkflow-diagram:4.0.2.Final")
-implementation("io.serverlessworkflow:serverlessworkflow-util:4.0.2.Final")
+implementation("io.serverlessworkflow:serverlessworkflow-api:4.0.4.Final")
+implementation("io.serverlessworkflow:serverlessworkflow-spi:4.0.4.Final")
+implementation("io.serverlessworkflow:serverlessworkflow-validation:4.0.4.Final")
+implementation("io.serverlessworkflow:serverlessworkflow-diagram:4.0.4.Final")
+implementation("io.serverlessworkflow:serverlessworkflow-util:4.0.4.Final")
 ```
 
 ### How to Use 
diff --git a/api/src/main/java/io/serverlessworkflow/api/serializers/WorkflowSerializer.java b/api/src/main/java/io/serverlessworkflow/api/serializers/WorkflowSerializer.java
index 7fd83c7..4bbaf9c 100644
--- a/api/src/main/java/io/serverlessworkflow/api/serializers/WorkflowSerializer.java
+++ b/api/src/main/java/io/serverlessworkflow/api/serializers/WorkflowSerializer.java
@@ -53,6 +53,9 @@ public class WorkflowSerializer extends StdSerializer<Workflow> {
       gen.writeStringField("id", generateUniqueId());
     }
 
+    if (workflow.getKey() != null) {
+      gen.writeStringField("key", workflow.getKey());
+    }
     gen.writeStringField("name", workflow.getName());
 
     if (workflow.getDescription() != null && !workflow.getDescription().isEmpty()) {
diff --git a/api/src/main/resources/schema/workflow.json b/api/src/main/resources/schema/workflow.json
index 8fafeee..3ed9ce5 100644
--- a/api/src/main/resources/schema/workflow.json
+++ b/api/src/main/resources/schema/workflow.json
@@ -11,8 +11,11 @@
   "properties": {
     "id": {
       "type": "string",
-      "description": "Workflow unique identifier",
-      "minLength": 1
+      "description": "Workflow unique identifier"
+    },
+    "key": {
+      "type": "string",
+      "description": "Workflow Domain-specific identifier"
     },
     "name": {
       "type": "string",
@@ -155,10 +158,14 @@
       }
     }
   },
-  "required": [
-    "id",
-    "name",
-    "version",
-    "states"
-  ]
-}
\ No newline at end of file
+ "required": [
+        "name",
+        "version",
+        "states"
+ ],
+ "dependencies":
+  {
+    "id": { "not": { "required": ["key"] } },
+    "key": { "not": { "required": ["id"] } }
+  }
+}
diff --git a/validation/src/main/java/io/serverlessworkflow/validation/WorkflowValidatorImpl.java b/validation/src/main/java/io/serverlessworkflow/validation/WorkflowValidatorImpl.java
index 1e0999c..abef046 100644
--- a/validation/src/main/java/io/serverlessworkflow/validation/WorkflowValidatorImpl.java
+++ b/validation/src/main/java/io/serverlessworkflow/validation/WorkflowValidatorImpl.java
@@ -112,8 +112,10 @@ public class WorkflowValidatorImpl implements WorkflowValidator {
       List<EventDefinition> events =
           workflow.getEvents() != null ? workflow.getEvents().getEventDefs() : null;
 
-      if (workflow.getId() == null || workflow.getId().trim().isEmpty()) {
-        addValidationError("Workflow id should not be empty", ValidationError.WORKFLOW_VALIDATION);
+      if ((workflow.getId() == null || workflow.getId().trim().isEmpty())
+          && (workflow.getKey() == null || workflow.getKey().trim().isEmpty())) {
+        addValidationError(
+            "Workflow id or key should not be empty", ValidationError.WORKFLOW_VALIDATION);
       }
 
       if (workflow.getVersion() == null || workflow.getVersion().trim().isEmpty()) {


Test patch:
diff --git a/api/src/test/java/io/serverlessworkflow/api/test/MarkupToWorkflowTest.java b/api/src/test/java/io/serverlessworkflow/api/test/MarkupToWorkflowTest.java
index 768085e..a1854d4 100644
--- a/api/src/test/java/io/serverlessworkflow/api/test/MarkupToWorkflowTest.java
+++ b/api/src/test/java/io/serverlessworkflow/api/test/MarkupToWorkflowTest.java
@@ -105,10 +105,11 @@ public class MarkupToWorkflowTest {
 
   @ParameterizedTest
   @ValueSource(strings = {"/features/applicantrequest.json", "/features/applicantrequest.yml"})
-  public void testSpecFreatureFunctionRef(String workflowLocation) {
+  public void testSpecFeatureFunctionRef(String workflowLocation) {
     Workflow workflow = Workflow.fromSource(WorkflowTestUtils.readWorkflowFile(workflowLocation));
 
     assertNotNull(workflow);
+    assertNull(workflow.getKey());
     assertNotNull(workflow.getId());
     assertNotNull(workflow.getName());
     assertNotNull(workflow.getStates());
@@ -118,6 +119,46 @@ public class MarkupToWorkflowTest {
     assertEquals(1, workflow.getFunctions().getFunctionDefs().size());
   }
 
+  @ParameterizedTest
+  @ValueSource(
+      strings = {
+        "/features/applicantrequest-with-key.json",
+        "/features/applicantrequest-with-key.yml"
+      })
+  public void testSpecFeatureFunctionRefWithKey(String workflowLocation) {
+    Workflow workflow = Workflow.fromSource(WorkflowTestUtils.readWorkflowFile(workflowLocation));
+
+    assertNotNull(workflow);
+    assertEquals("applicant-key-request", workflow.getKey());
+    assertNull(workflow.getId());
+    assertNotNull(workflow.getName());
+    assertNotNull(workflow.getStates());
+    assertTrue(workflow.getStates().size() > 0);
+
+    assertNotNull(workflow.getFunctions());
+    assertEquals(1, workflow.getFunctions().getFunctionDefs().size());
+  }
+
+  @ParameterizedTest
+  @ValueSource(
+      strings = {
+        "/features/applicantrequest-with-id-and-key.json",
+        "/features/applicantrequest-with-id-and-key.yml"
+      })
+  public void testSpecFeatureFunctionRefWithIdAndKey(String workflowLocation) {
+    Workflow workflow = Workflow.fromSource(WorkflowTestUtils.readWorkflowFile(workflowLocation));
+
+    assertNotNull(workflow);
+    assertEquals("applicant-key-request", workflow.getKey());
+    assertEquals("applicant-with-key-and-id", workflow.getId());
+    assertNotNull(workflow.getName());
+    assertNotNull(workflow.getStates());
+    assertTrue(workflow.getStates().size() > 0);
+
+    assertNotNull(workflow.getFunctions());
+    assertEquals(1, workflow.getFunctions().getFunctionDefs().size());
+  }
+
   @ParameterizedTest
   @ValueSource(strings = {"/features/vetappointment.json", "/features/vetappointment.yml"})
   public void testSpecFreatureEventRef(String workflowLocation) {
diff --git a/api/src/test/resources/examples/applicantrequest.json b/api/src/test/resources/examples/applicantrequest.json
index 1621c2b..652e361 100644
--- a/api/src/test/resources/examples/applicantrequest.json
+++ b/api/src/test/resources/examples/applicantrequest.json
@@ -1,5 +1,6 @@
 {
   "id": "applicantrequest",
+  "key": "applicant-key-request",
   "version": "1.0",
   "specVersion": "0.8",
   "name": "Applicant Request Decision Workflow",
diff --git a/api/src/test/resources/features/applicantrequest-with-id-and-key.json b/api/src/test/resources/features/applicantrequest-with-id-and-key.json
new file mode 100644
index 0000000..405d7c3
--- /dev/null
+++ b/api/src/test/resources/features/applicantrequest-with-id-and-key.json
@@ -0,0 +1,60 @@
+{
+  "id": "applicant-with-key-and-id",
+  "key": "applicant-key-request",
+  "version": "1.0",
+  "specVersion": "0.8",
+  "name": "Applicant Request Decision Workflow",
+  "description": "Determine if applicant request is valid",
+  "start": "CheckApplication",
+  "functions": [
+    {
+      "name": "sendRejectionEmailFunction",
+      "operation": "http://myapis.org/applicationapi.json#emailRejection"
+    }
+  ],
+  "states":[
+    {
+      "name":"CheckApplication",
+      "type":"switch",
+      "dataConditions": [
+        {
+          "condition": "${ .applicants | .age >= 18 }",
+          "transition": "StartApplication"
+        },
+        {
+          "condition": "${ .applicants | .age < 18 }",
+          "transition": "RejectApplication"
+        }
+      ],
+      "defaultCondition": {
+        "transition": "RejectApplication"
+      }
+    },
+    {
+      "name": "StartApplication",
+      "type": "operation",
+      "actions": [
+        {
+          "subFlowRef": "startApplicationWorkflowId"
+        }
+      ],
+      "end": true
+    },
+    {
+      "name":"RejectApplication",
+      "type":"operation",
+      "actionMode":"sequential",
+      "actions":[
+        {
+          "functionRef": {
+            "refName": "sendRejectionEmailFunction",
+            "arguments": {
+              "applicant": "${ .applicant }"
+            }
+          }
+        }
+      ],
+      "end": true
+    }
+  ]
+}
\ No newline at end of file
diff --git a/api/src/test/resources/features/applicantrequest-with-id-and-key.yml b/api/src/test/resources/features/applicantrequest-with-id-and-key.yml
new file mode 100644
index 0000000..8a12366
--- /dev/null
+++ b/api/src/test/resources/features/applicantrequest-with-id-and-key.yml
@@ -0,0 +1,34 @@
+id: applicant-with-key-and-id
+key: applicant-key-request
+version: '1.0'
+specVersion: '0.8'
+name: Applicant Request Decision Workflow
+description: Determine if applicant request is valid
+start: CheckApplication
+functions:
+  - name: sendRejectionEmailFunction
+    operation: http://myapis.org/applicationapi.json#emailRejection
+states:
+  - name: CheckApplication
+    type: switch
+    dataConditions:
+      - condition: "${ .applicants | .age >= 18 }"
+        transition: StartApplication
+      - condition: "${ .applicants | .age < 18 }"
+        transition: RejectApplication
+    defaultCondition:
+      transition: RejectApplication
+  - name: StartApplication
+    type: operation
+    actions:
+      - subFlowRef: startApplicationWorkflowId
+    end: true
+  - name: RejectApplication
+    type: operation
+    actionMode: sequential
+    actions:
+      - functionRef:
+          refName: sendRejectionEmailFunction
+          arguments:
+            applicant: "${ .applicant }"
+    end: true
diff --git a/api/src/test/resources/features/applicantrequest-with-key.json b/api/src/test/resources/features/applicantrequest-with-key.json
new file mode 100644
index 0000000..f0481b0
--- /dev/null
+++ b/api/src/test/resources/features/applicantrequest-with-key.json
@@ -0,0 +1,59 @@
+{
+  "key": "applicant-key-request",
+  "version": "1.0",
+  "specVersion": "0.8",
+  "name": "Applicant Request Decision Workflow",
+  "description": "Determine if applicant request is valid",
+  "start": "CheckApplication",
+  "functions": [
+    {
+      "name": "sendRejectionEmailFunction",
+      "operation": "http://myapis.org/applicationapi.json#emailRejection"
+    }
+  ],
+  "states":[
+    {
+      "name":"CheckApplication",
+      "type":"switch",
+      "dataConditions": [
+        {
+          "condition": "${ .applicants | .age >= 18 }",
+          "transition": "StartApplication"
+        },
+        {
+          "condition": "${ .applicants | .age < 18 }",
+          "transition": "RejectApplication"
+        }
+      ],
+      "defaultCondition": {
+        "transition": "RejectApplication"
+      }
+    },
+    {
+      "name": "StartApplication",
+      "type": "operation",
+      "actions": [
+        {
+          "subFlowRef": "startApplicationWorkflowId"
+        }
+      ],
+      "end": true
+    },
+    {
+      "name":"RejectApplication",
+      "type":"operation",
+      "actionMode":"sequential",
+      "actions":[
+        {
+          "functionRef": {
+            "refName": "sendRejectionEmailFunction",
+            "arguments": {
+              "applicant": "${ .applicant }"
+            }
+          }
+        }
+      ],
+      "end": true
+    }
+  ]
+}
\ No newline at end of file
diff --git a/api/src/test/resources/features/applicantrequest-with-key.yml b/api/src/test/resources/features/applicantrequest-with-key.yml
new file mode 100644
index 0000000..85beed7
--- /dev/null
+++ b/api/src/test/resources/features/applicantrequest-with-key.yml
@@ -0,0 +1,33 @@
+key: applicant-key-request
+version: '1.0'
+specVersion: '0.8'
+name: Applicant Request Decision Workflow
+description: Determine if applicant request is valid
+start: CheckApplication
+functions:
+  - name: sendRejectionEmailFunction
+    operation: http://myapis.org/applicationapi.json#emailRejection
+states:
+  - name: CheckApplication
+    type: switch
+    dataConditions:
+      - condition: "${ .applicants | .age >= 18 }"
+        transition: StartApplication
+      - condition: "${ .applicants | .age < 18 }"
+        transition: RejectApplication
+    defaultCondition:
+      transition: RejectApplication
+  - name: StartApplication
+    type: operation
+    actions:
+      - subFlowRef: startApplicationWorkflowId
+    end: true
+  - name: RejectApplication
+    type: operation
+    actionMode: sequential
+    actions:
+      - functionRef:
+          refName: sendRejectionEmailFunction
+          arguments:
+            applicant: "${ .applicant }"
+    end: true
diff --git a/validation/src/test/java/io/serverlessworkflow/validation/test/WorkflowValidationTest.java b/validation/src/test/java/io/serverlessworkflow/validation/test/WorkflowValidationTest.java
index 38dbe2d..a81e14f 100644
--- a/validation/src/test/java/io/serverlessworkflow/validation/test/WorkflowValidationTest.java
+++ b/validation/src/test/java/io/serverlessworkflow/validation/test/WorkflowValidationTest.java
@@ -44,8 +44,9 @@ public class WorkflowValidationTest {
   public void testIncompleteYamlWithSchemaValidation() {
     WorkflowValidator workflowValidator = new WorkflowValidatorImpl();
     List<ValidationError> validationErrors =
-        workflowValidator.setSource("---\n" + "id: abc\n").validate();
+        workflowValidator.setSource("---\n" + "key: abc\n").validate();
     Assertions.assertNotNull(validationErrors);
+    System.out.println(validationErrors);
     Assertions.assertEquals(3, validationErrors.size());
   }
 
@@ -93,6 +94,27 @@ public class WorkflowValidationTest {
     Assertions.assertEquals("No states found", validationErrors.get(0).getMessage());
   }
 
+  @Test
+  public void testWorkflowMissingStatesIdAndKey() {
+    WorkflowValidator workflowValidator = new WorkflowValidatorImpl();
+    List<ValidationError> validationErrors =
+        workflowValidator
+            .setSource(
+                "{\n"
+                    + "\t\"name\": \"test workflow\",\n"
+                    + "  \"version\": \"1.0\",\n"
+                    + "  \"start\": \"SomeState\",\n"
+                    + "  \"states\": []\n"
+                    + "}")
+            .validate();
+    Assertions.assertNotNull(validationErrors);
+    Assertions.assertEquals(2, validationErrors.size());
+
+    Assertions.assertEquals(
+        "Workflow id or key should not be empty", validationErrors.get(0).getMessage());
+    Assertions.assertEquals("No states found", validationErrors.get(1).getMessage());
+  }
+
   @Test
   public void testOperationStateNoFunctionRef() {
     WorkflowValidator workflowValidator = new WorkflowValidatorImpl();
@@ -101,7 +123,7 @@ public class WorkflowValidationTest {
             .setSource(
                 "{\n"
                     + "\"id\": \"checkInbox\",\n"
-                    + "  \"name\": \"Check Inbox Workflow\",\n"
+                    + "\"name\": \"Check Inbox Workflow\",\n"
                     + "\"description\": \"Periodically Check Inbox\",\n"
                     + "\"version\": \"1.0\",\n"
                     + "\"start\": \"CheckInbox\",\n"
@@ -140,6 +162,7 @@ public class WorkflowValidationTest {
     Assertions.assertNotNull(validationErrors);
     Assertions.assertEquals(1, validationErrors.size());
 
+    // validationErrors.stream().forEach(v -> System.out.println(v.toString()));
     Assertions.assertEquals(
         "Operation State action functionRef does not reference an existing workflow function definition",
         validationErrors.get(0).getMessage());