Skip to content
Draft
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
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,4 @@
docs/build
site/
temp.*.json
vendor
6 changes: 4 additions & 2 deletions docs/examples/persistence/persistence.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,10 @@ func main() {

// export the whole engine state as bytes
// the export format is valid JSON and can be stored however you want
bytes := bpmnEngine.Marshal()

bytes, err := bpmnEngine.Marshal()
if err != nil {
panic(err)
}
// debug print ...
println(string(bytes))

Expand Down
214 changes: 195 additions & 19 deletions pkg/bpmn_engine/marshalling.go
Original file line number Diff line number Diff line change
Expand Up @@ -214,7 +214,7 @@
case *eventBasedGatewayActivity:
piia.ActivityAdapters = append(piia.ActivityAdapters, createEventBasedGatewayActivityAdapter(activity))
default:
panic(fmt.Sprintf("[invariant check] missing activity adapter for the type %T", a))
return nil, fmt.Errorf("[invariant check] missing activity adapter for the type %T", a)

Check warning on line 217 in pkg/bpmn_engine/marshalling.go

View check run for this annotation

Codecov / codecov/patch

pkg/bpmn_engine/marshalling.go#L217

Added line #L217 was not covered by tests
}
}
return json.Marshal(piia)
Expand All @@ -229,8 +229,7 @@
}
pii.ProcessInfo = &ProcessInfo{ProcessKey: adapter.ProcessKey}
pii.VariableHolder = adapter.VariableHolder
recoverProcessInstanceActivitiesPart1(pii, adapter)
return nil
return recoverProcessInstanceActivitiesPart1(pii, adapter)
}

func createEventBasedGatewayActivityAdapter(ebga *eventBasedGatewayActivity) *activityAdapter {
Expand Down Expand Up @@ -276,31 +275,182 @@

// ----------------------------------------------------------------------------

func (state *BpmnEngineState) Marshal() []byte {
// VariableWrapFunc function to wrap variables before marshalling
//
// Parameters:
// - key: Variables key
// - value: Wrapped value of the variable
type VariableWrapFunc func(string, any) (any, error)

// marshalOptions Options that will be used while marshalling the engine
type marshalOptions struct {
marshalVariablesFunc VariableWrapFunc
}

// MarshalOption is a function that modifies the marshalOptions
type MarshalOption func(*marshalOptions) error

// WithMarshalVariableFunc sets a function that will be called for each variable in the engine's VarHolder
// This allows you to customize variables before they are marshalled, e.g. to convert them to a different type
func WithMarshalVariableFunc(fun VariableWrapFunc) MarshalOption {
return func(opts *marshalOptions) error {
opts.marshalVariablesFunc = fun
return nil
}
}

// applyMarshalOptions Applies the given options and returns the applied marshalOptions
func applyMarshalOptions(options ...MarshalOption) (*marshalOptions, error) {
opts := &marshalOptions{}
for _, o := range options {
err := o(opts)
if err != nil {
return nil, fmt.Errorf("could not apply option: %w", err)
}

Check warning on line 309 in pkg/bpmn_engine/marshalling.go

View check run for this annotation

Codecov / codecov/patch

pkg/bpmn_engine/marshalling.go#L308-L309

Added lines #L308 - L309 were not covered by tests
}

return opts, nil
}

// Marshal marshals the engine into a byte array.
// Options may be provided to configure the marshalling process.
// It returns a byte array containing the marshalled engine state.
// If there is an error applying the options, it will panic.
//
// Example:
//
// ```go
// // Marshal with default options
// data, err := bpmn_engine.Marshal()
//
// // Marshal with type information for complex variables
// data, err := bpmn_engine.Marshal(WithMarshalComplexTypes())
// ```
func (state *BpmnEngineState) Marshal(options ...MarshalOption) ([]byte, error) {
opts, err := applyMarshalOptions(options...)
if err != nil {
return nil, err
}

Check warning on line 333 in pkg/bpmn_engine/marshalling.go

View check run for this annotation

Codecov / codecov/patch

pkg/bpmn_engine/marshalling.go#L332-L333

Added lines #L332 - L333 were not covered by tests
pis, err := createProcessInstances(state.processInstances, opts)
if err != nil {
return nil, err
}

Check warning on line 337 in pkg/bpmn_engine/marshalling.go

View check run for this annotation

Codecov / codecov/patch

pkg/bpmn_engine/marshalling.go#L336-L337

Added lines #L336 - L337 were not covered by tests

m := serializedBpmnEngine{
Version: CurrentSerializerVersion,
Name: state.name,
MessageSubscriptions: state.messageSubscriptions,
ProcessReferences: createReferences(state.processes),
ProcessInstances: state.processInstances,
ProcessInstances: pis,
Timers: state.timers,
Jobs: state.jobs,
}
bytes, err := json.Marshal(m)
if err != nil {
panic(err)
return nil, err
}

Check warning on line 351 in pkg/bpmn_engine/marshalling.go

View check run for this annotation

Codecov / codecov/patch

pkg/bpmn_engine/marshalling.go#L350-L351

Added lines #L350 - L351 were not covered by tests
return bytes, nil
}

// wrapVariables takes a variable holder and wraps each variable with a complexVariable if the variable is a
// struct or pointer
func wrapVariables(vh VariableHolder, f VariableWrapFunc) (VariableHolder, error) {
for k, v := range vh.variables {
val, err := f(k, v)
if err != nil {
return vh, err
}

Check warning on line 362 in pkg/bpmn_engine/marshalling.go

View check run for this annotation

Codecov / codecov/patch

pkg/bpmn_engine/marshalling.go#L361-L362

Added lines #L361 - L362 were not covered by tests
vh.variables[k] = val
}
// If there is a parent, create complex variables for it as well
if vh.parent != nil {
parent, err := wrapVariables(*vh.parent, f)
if err != nil {
return VariableHolder{}, err
}
vh.parent = &parent

Check warning on line 371 in pkg/bpmn_engine/marshalling.go

View check run for this annotation

Codecov / codecov/patch

pkg/bpmn_engine/marshalling.go#L367-L371

Added lines #L367 - L371 were not covered by tests
}
return vh, nil
}

// createProcessInstances Creates process instances that can be marshalled to JSON
func createProcessInstances(pii []*processInstanceInfo, opts *marshalOptions) ([]*processInstanceInfo, error) {

// If exporting types is not enable, there is nothing extra to do
if opts.marshalVariablesFunc == nil {
return pii, nil
}

// Create complex variables for each process instance
for _, pi := range pii {
cvs, err := wrapVariables(pi.VariableHolder, opts.marshalVariablesFunc)
if err != nil {
return nil, err
}

Check warning on line 389 in pkg/bpmn_engine/marshalling.go

View check run for this annotation

Codecov / codecov/patch

pkg/bpmn_engine/marshalling.go#L388-L389

Added lines #L388 - L389 were not covered by tests
pi.VariableHolder = cvs
}
return bytes
return pii, nil
}

// VariableUnwrapFunc function to unwrap variables from marshalled state
//
// Parameters:
// - key: Variables key
// - value: Wrapped value of the variable
type VariableUnwrapFunc func(key string, value any) (any, error)

// unmarshalOptions Options that will be used while unmarshalling the engine
type unmarshalOptions struct {
// variableUnwrapFunc function that can be called to restore a variable from a marshalled state
variableUnwrapFunc VariableUnwrapFunc
}

// UnmarshalOption is a function that modifies the unmarshalOptions
type UnmarshalOption func(*unmarshalOptions) error

// WithUnmarshalVariableFunc sets a function that will be called for each variable in the engine's VarHolder
// This allows you to customize variables after they are unmarshalled, e.g. to convert them to a different type
func WithUnmarshalVariableFunc(fun VariableUnwrapFunc) UnmarshalOption {
return func(opts *unmarshalOptions) error {
opts.variableUnwrapFunc = fun
return nil
}
}

// applyUnmarshalOptions Applies the given options and returns the applied unmarshalOptions
func applyUnmarshalOptions(options ...UnmarshalOption) (*unmarshalOptions, error) {
opts := &unmarshalOptions{}
for _, o := range options {
err := o(opts)
if err != nil {
return nil, fmt.Errorf("could not apply option: %w", err)
}

Check warning on line 427 in pkg/bpmn_engine/marshalling.go

View check run for this annotation

Codecov / codecov/patch

pkg/bpmn_engine/marshalling.go#L426-L427

Added lines #L426 - L427 were not covered by tests
}

return opts, nil
}

// Unmarshal loads the data byte array and creates a new instance of the BPMN Engine
// Will return an BpmnEngineUnmarshallingError, if there was an issue AND in case of error,
// the engine return object is only partially initialized and likely not usable
func Unmarshal(data []byte) (BpmnEngineState, error) {
func Unmarshal(data []byte, opts ...UnmarshalOption) (BpmnEngineState, error) {

// Build an unmarshalOptions object from the provided options
options, err := applyUnmarshalOptions(opts...)
if err != nil {
return BpmnEngineState{}, &BpmnEngineUnmarshallingError{
Msg: "Failed to apply unmarshalling options",
Err: err,
}
}

Check warning on line 445 in pkg/bpmn_engine/marshalling.go

View check run for this annotation

Codecov / codecov/patch

pkg/bpmn_engine/marshalling.go#L441-L445

Added lines #L441 - L445 were not covered by tests

eng := serializedBpmnEngine{}
err := json.Unmarshal(data, &eng)
err = json.Unmarshal(data, &eng)
if err != nil {
panic(err)
return BpmnEngineState{}, &BpmnEngineUnmarshallingError{
Msg: "Failed to unmarshall engine data",
Err: err,
}

Check warning on line 453 in pkg/bpmn_engine/marshalling.go

View check run for this annotation

Codecov / codecov/patch

pkg/bpmn_engine/marshalling.go#L450-L453

Added lines #L450 - L453 were not covered by tests
}
state := New()
state.name = eng.Name
Expand All @@ -327,12 +477,15 @@
}
if eng.ProcessInstances != nil {
state.processInstances = eng.ProcessInstances
err := recoverProcessInstances(&state)
err = recoverProcessInstances(&state, options)
if err != nil {
return state, err
}
}
recoverProcessInstanceActivitiesPart2(&state)
err = recoverProcessInstanceActivitiesPart2(&state)
if err != nil {
return BpmnEngineState{}, err
}

Check warning on line 488 in pkg/bpmn_engine/marshalling.go

View check run for this annotation

Codecov / codecov/patch

pkg/bpmn_engine/marshalling.go#L487-L488

Added lines #L487 - L488 were not covered by tests
if eng.MessageSubscriptions != nil {
state.messageSubscriptions = eng.MessageSubscriptions
err = recoverMessageSubscriptions(&state)
Expand All @@ -357,7 +510,7 @@
return state, nil
}

func recoverProcessInstanceActivitiesPart1(pii *processInstanceInfo, adapter *processInstanceInfoAdapter) {
func recoverProcessInstanceActivitiesPart1(pii *processInstanceInfo, adapter *processInstanceInfoAdapter) error {
for _, aa := range adapter.ActivityAdapters {
switch aa.Type {
case gatewayActivityAdapterType:
Expand All @@ -378,12 +531,13 @@
OutboundActivityCompleted: aa.OutboundActivityCompleted,
})
default:
panic(fmt.Sprintf("[invariant check] missing recovery code for actictyAdapter.Type=%d", aa.Type))
return fmt.Errorf("[invariant check] missing recovery code for actictyAdapter.Type=%d", aa.Type)

Check warning on line 534 in pkg/bpmn_engine/marshalling.go

View check run for this annotation

Codecov / codecov/patch

pkg/bpmn_engine/marshalling.go#L534

Added line #L534 was not covered by tests
}
}
return nil
}

func recoverProcessInstanceActivitiesPart2(state *BpmnEngineState) {
func recoverProcessInstanceActivitiesPart2(state *BpmnEngineState) error {
for _, pi := range state.processInstances {
for _, a := range pi.activities {
switch activity := a.(type) {
Expand All @@ -392,15 +546,33 @@
case *gatewayActivity:
activity.element = BPMN20.FindBaseElementsById(pi.ProcessInfo.definitions.Process, (*a.Element()).GetId())[0]
default:
panic(fmt.Sprintf("[invariant check] missing case for activity type=%T", a))
return fmt.Errorf("[invariant check] missing case for activity type=%T", a)

Check warning on line 549 in pkg/bpmn_engine/marshalling.go

View check run for this annotation

Codecov / codecov/patch

pkg/bpmn_engine/marshalling.go#L549

Added line #L549 was not covered by tests
}
}
}
return nil
}

// ----------------------------------------------------------------------------
// recoverVariableInstances recovers the variable instances from the given VariableHolder
func recoverVariableInstances(vh VariableHolder, opts *unmarshalOptions) (VariableHolder, error) {
if opts.variableUnwrapFunc == nil {
// Nothing additional to do
return vh, nil
}

func recoverProcessInstances(state *BpmnEngineState) error {
for k, v := range vh.variables {
val, err := opts.variableUnwrapFunc(k, v)
if err != nil {
return vh, err
}

Check warning on line 567 in pkg/bpmn_engine/marshalling.go

View check run for this annotation

Codecov / codecov/patch

pkg/bpmn_engine/marshalling.go#L566-L567

Added lines #L566 - L567 were not covered by tests

// Replace the variable with the proper instance
vh.variables[k] = val
}
return vh, nil
}

func recoverProcessInstances(state *BpmnEngineState, opts *unmarshalOptions) error {
for i, pi := range state.processInstances {
process := state.findProcess(pi.ProcessInfo.ProcessKey)
if process == nil {
Expand All @@ -410,7 +582,11 @@
}
}
state.processInstances[i].ProcessInfo = process
state.processInstances[i].VariableHolder = pi.VariableHolder
vars, err := recoverVariableInstances(pi.VariableHolder, opts)
if err != nil {
return err
}

Check warning on line 588 in pkg/bpmn_engine/marshalling.go

View check run for this annotation

Codecov / codecov/patch

pkg/bpmn_engine/marshalling.go#L587-L588

Added lines #L587 - L588 were not covered by tests
state.processInstances[i].VariableHolder = vars
}
return nil
}
Expand Down
60 changes: 59 additions & 1 deletion pkg/bpmn_engine/marshalling_test.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package bpmn_engine

import (
"encoding/json"
"fmt"
"github.com/corbym/gocrest/is"
"testing"
Expand All @@ -22,7 +23,8 @@ func Test_MarshallEngine(t *testing.T) {

_, _ = bpmnEngine.CreateAndRunInstance(process.ProcessKey, variableContext)

data := bpmnEngine.Marshal()
data, err := bpmnEngine.Marshal()
then.AssertThat(t, err, is.Empty())

fmt.Println(string(data))

Expand All @@ -32,3 +34,59 @@ func Test_MarshallEngine(t *testing.T) {
then.AssertThat(t, vars.GetVariable("john"), is.EqualTo("doe"))
then.AssertThat(t, vars.GetVariable("valueFromHandler"), is.EqualTo(true))
}

func Test_MarshallEngineWithWrapping(t *testing.T) {
bpmnEngine := New()
process, _ := bpmnEngine.LoadFromFile("../../test-cases/simple_task-with_output_mapping.bpmn")
bpmnEngine.NewTaskHandler().Id("id").Handler(func(job ActivatedJob) {
job.SetVariable("valueFromHandler", true)
job.SetVariable("otherVariable", "value")
job.Complete()
})
variableContext := make(map[string]interface{})
variableContext["hello"] = "world"
variableContext["john"] = "doe"

_, _ = bpmnEngine.CreateAndRunInstance(process.ProcessKey, variableContext)

type Wrapper struct {
Type string
Value any
}

// Simple wrapper function encapsulate a variable into a string
wrapVariableFunc := func(_ string, variable any) (any, error) {
w := Wrapper{
Type: fmt.Sprintf("%T", variable),
Value: variable,
}

v, err := json.Marshal(w)
if err != nil {
return nil, err
}

return string(v), nil
}

// Simple unwrapper function unwrap a variable from a string
unwrapVariableFunc := func(_ string, variable any) (any, error) {
w := &Wrapper{}
err := json.Unmarshal([]byte(variable.(string)), w)
if err != nil {
return nil, err
}
return w.Value, nil
}

data, err := bpmnEngine.Marshal(WithMarshalVariableFunc(wrapVariableFunc))
then.AssertThat(t, err, is.Empty())

fmt.Println(string(data))

bpmnEngine, _ = Unmarshal(data, WithUnmarshalVariableFunc(unwrapVariableFunc))
vars := bpmnEngine.ProcessInstances()[0].VariableHolder
then.AssertThat(t, vars.GetVariable("hello"), is.EqualTo("world"))
then.AssertThat(t, vars.GetVariable("john"), is.EqualTo("doe"))
then.AssertThat(t, vars.GetVariable("valueFromHandler"), is.EqualTo(true))
}
Loading