Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion mdl/executor/cmd_microflows_builder_enum_split_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,7 @@ func TestEnumSplitNestedEmptyThenBranchKeepsContinuationCase(t *testing.T) {
if flow.OriginID != nestedSplitID {
continue
}
if value, ok := enumCaseValue(flow); ok && value == "true" {
if flowCaseString(flow.CaseValue) == "true" {
if _, ok := objects[flow.DestinationID].(*microflows.ExclusiveMerge); ok {
return
}
Expand Down
28 changes: 21 additions & 7 deletions mdl/executor/cmd_microflows_builder_flows.go
Original file line number Diff line number Diff line change
Expand Up @@ -653,10 +653,7 @@ func newHorizontalFlow(originID, destinationID model.ID) *microflows.SequenceFlo
// newHorizontalFlowWithCase creates a horizontal SequenceFlow with a boolean case value (for splits)
func newHorizontalFlowWithCase(originID, destinationID model.ID, caseValue string) *microflows.SequenceFlow {
flow := newHorizontalFlow(originID, destinationID)
flow.CaseValue = microflows.EnumerationCase{
BaseElement: model.BaseElement{ID: model.ID(types.GenerateID())},
Value: caseValue, // "true" or "false" as string
}
flow.CaseValue = caseValueForFlow(caseValue)
return flow
}

Expand All @@ -678,10 +675,27 @@ func newDownwardFlowWithCase(originID, destinationID model.ID, caseValue string)
DestinationID: destinationID,
OriginConnectionIndex: AnchorBottom, // Connect from bottom of origin (going down)
DestinationConnectionIndex: AnchorLeft, // Connect to left side of destination
CaseValue: microflows.EnumerationCase{
CaseValue: caseValueForFlow(caseValue),
}
}

func caseValueForFlow(caseValue string) microflows.CaseValue {
switch caseValue {
case "true":
return &microflows.ExpressionCase{
BaseElement: model.BaseElement{ID: model.ID(types.GenerateID())},
Value: caseValue, // "true" or "false" as string
},
Expression: "true",
}
case "false":
return &microflows.ExpressionCase{
BaseElement: model.BaseElement{ID: model.ID(types.GenerateID())},
Expression: "false",
}
default:
return microflows.EnumerationCase{
BaseElement: model.BaseElement{ID: model.ID(types.GenerateID())},
Value: caseValue,
}
}
}

Expand Down
12 changes: 2 additions & 10 deletions mdl/executor/cmd_microflows_builder_terminal_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -892,17 +892,9 @@ func flowCaseString(caseValue microflows.CaseValue) string {
if c != nil {
return c.Value
}
case microflows.BooleanCase:
if c.Value {
return "true"
}
return "false"
case *microflows.BooleanCase:
if c != nil && c.Value {
return "true"
}
case *microflows.ExpressionCase:
if c != nil {
return "false"
return c.Expression
}
}
return ""
Expand Down
39 changes: 30 additions & 9 deletions mdl/executor/cmd_microflows_guard_pattern_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -47,17 +47,10 @@ func TestBuilder_GuardPatternPreservesFalseBranchAnchor(t *testing.T) {
oc := fb.buildFlowGraph(body, nil)

// Find the flow from the split to the tail log. It's the only one with
// an EnumerationCase Value=="false" that doesn't target an EndEvent.
// a false branch case that doesn't target an EndEvent.
var found *microflows.SequenceFlow
for _, f := range oc.Flows {
cv, ok := f.CaseValue.(microflows.EnumerationCase)
if !ok {
if p, okp := f.CaseValue.(*microflows.EnumerationCase); okp {
cv = *p
ok = true
}
}
if !ok || cv.Value != "false" {
if flowCaseString(f.CaseValue) != "false" {
continue
}
// Exclude flows pointing at an EndEvent.
Expand Down Expand Up @@ -85,3 +78,31 @@ func TestBuilder_GuardPatternPreservesFalseBranchAnchor(t *testing.T) {
t.Errorf("destination: got %d, want %d (Top)", found.DestinationConnectionIndex, AnchorTop)
}
}

func TestCaseValueForFlowUsesExpressionCaseForBooleanBranches(t *testing.T) {
for _, tc := range []struct {
value string
want string
}{
{value: "true", want: "true"},
{value: "false", want: "false"},
} {
got, ok := caseValueForFlow(tc.value).(*microflows.ExpressionCase)
if !ok {
t.Fatalf("caseValueForFlow(%q) = %T, want *ExpressionCase", tc.value, caseValueForFlow(tc.value))
}
if got.Expression != tc.want {
t.Fatalf("caseValueForFlow(%q).Expression = %q, want %q", tc.value, got.Expression, tc.want)
}
}
}

func TestCaseValueForFlowKeepsEnumValuesAsEnumerationCase(t *testing.T) {
got, ok := caseValueForFlow("Submitted").(microflows.EnumerationCase)
if !ok {
t.Fatalf("caseValueForFlow(enum) = %T, want EnumerationCase", caseValueForFlow("Submitted"))
}
if got.Value != "Submitted" {
t.Fatalf("enum case value = %q, want Submitted", got.Value)
}
}
21 changes: 21 additions & 0 deletions sdk/mpr/parser_microflow_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,27 @@ func TestParseSequenceFlow_NewCaseValueEnumerationCase(t *testing.T) {
}
}

func TestParseSequenceFlow_NewCaseValueExpressionCase(t *testing.T) {
flow := parseSequenceFlow(map[string]any{
"$ID": "flow-1",
"OriginPointer": "start-1",
"DestinationPointer": "dest-1",
"NewCaseValue": primitive.D{
{Key: "$ID", Value: "case-1"},
{Key: "$Type", Value: "Microflows$ExpressionCase"},
{Key: "Expression", Value: "false"},
},
})

got, ok := flow.CaseValue.(*microflows.ExpressionCase)
if !ok {
t.Fatalf("expected *ExpressionCase, got %T", flow.CaseValue)
}
if got.Expression != "false" {
t.Fatalf("expected false branch, got %q", got.Expression)
}
}

func TestParseSequenceFlow_NewCaseValueNoCase(t *testing.T) {
flow := parseSequenceFlow(map[string]any{
"$ID": "flow-1",
Expand Down
23 changes: 21 additions & 2 deletions sdk/mpr/writer_microflow.go
Original file line number Diff line number Diff line change
Expand Up @@ -170,7 +170,7 @@ func (w *Writer) serializeMicroflow(mf *microflows.Microflow) ([]byte, error) {
func serializeSequenceFlow(flow *microflows.SequenceFlow, majorVersion int) bson.D {
// Build the case document. Every sequence flow needs a case — NoCase is the
// default when no branch condition has been set.
caseDoc := buildSequenceFlowCase(flow.CaseValue)
caseDoc := buildSequenceFlowCase(flow.CaseValue, majorVersion)

originCV := flow.OriginControlVector
if originCV == "" {
Expand Down Expand Up @@ -221,13 +221,15 @@ func serializeSequenceFlow(flow *microflows.SequenceFlow, majorVersion int) bson
// buildSequenceFlowCase renders the case document for a sequence flow.
// When no case has been set on the flow, a NoCase document is synthesised —
// Studio Pro requires every SequenceFlow to carry an explicit case object.
func buildSequenceFlowCase(cv microflows.CaseValue) bson.D {
func buildSequenceFlowCase(cv microflows.CaseValue, majorVersion int) bson.D {
// Normalise value receivers to pointers so each case is handled once.
switch c := cv.(type) {
case microflows.EnumerationCase:
cv = &c
case microflows.NoCase:
cv = &c
case microflows.ExpressionCase:
cv = &c
}

switch c := cv.(type) {
Expand All @@ -250,6 +252,23 @@ func buildSequenceFlowCase(cv microflows.CaseValue) bson.D {
{Key: "$ID", Value: idToBsonBinary(id)},
{Key: "$Type", Value: "Microflows$NoCase"},
}
case *microflows.ExpressionCase:
id := string(c.ID)
if id == "" {
id = generateUUID()
}
if majorVersion <= 9 {
return bson.D{
{Key: "$ID", Value: idToBsonBinary(id)},
{Key: "$Type", Value: "Microflows$EnumerationCase"},
{Key: "Value", Value: c.Expression},
}
}
return bson.D{
{Key: "$ID", Value: idToBsonBinary(id)},
{Key: "$Type", Value: "Microflows$ExpressionCase"},
{Key: "Expression", Value: c.Expression},
}
}
// Default: synthesise a NoCase document with a fresh ID.
return bson.D{
Expand Down
32 changes: 30 additions & 2 deletions sdk/mpr/writer_microflow_version_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -161,10 +161,38 @@ func TestSerializeMicroflowParameter_Mx9_OmitsMx10OnlyKeys(t *testing.T) {

func TestBuildSequenceFlowCase_NormalisesValueReceiver(t *testing.T) {
// A value-receiver NoCase must produce the same shape as a pointer.
fromValue := buildSequenceFlowCase(microflows.NoCase{BaseElement: model.BaseElement{ID: "x"}})
fromPointer := buildSequenceFlowCase(&microflows.NoCase{BaseElement: model.BaseElement{ID: "x"}})
fromValue := buildSequenceFlowCase(microflows.NoCase{BaseElement: model.BaseElement{ID: "x"}}, 9)
fromPointer := buildSequenceFlowCase(&microflows.NoCase{BaseElement: model.BaseElement{ID: "x"}}, 9)

if bsonGetKey(fromValue, "$Type") != bsonGetKey(fromPointer, "$Type") {
t.Error("value and pointer NoCase must produce identical $Type")
}
}

func TestBuildSequenceFlowCase_ExpressionCase_Mx10(t *testing.T) {
doc := buildSequenceFlowCase(microflows.ExpressionCase{
BaseElement: model.BaseElement{ID: "case-false"},
Expression: "false",
}, 10)

if got := bsonGetKey(doc, "$Type"); got != "Microflows$ExpressionCase" {
t.Fatalf("$Type = %v, want Microflows$ExpressionCase", got)
}
if got := bsonGetKey(doc, "Expression"); got != "false" {
t.Fatalf("Expression = %v, want false", got)
}
}

func TestBuildSequenceFlowCase_ExpressionCase_Mx9UsesEnumerationCase(t *testing.T) {
doc := buildSequenceFlowCase(microflows.ExpressionCase{
BaseElement: model.BaseElement{ID: "case-false"},
Expression: "false",
}, 9)

if got := bsonGetKey(doc, "$Type"); got != "Microflows$EnumerationCase" {
t.Fatalf("$Type = %v, want Microflows$EnumerationCase", got)
}
if got := bsonGetKey(doc, "Value"); got != "false" {
t.Fatalf("Value = %v, want false", got)
}
}
Loading