Skip to content

Commit 589d327

Browse files
committed
Add support for adding a custom function to marshal/unmarshall variables
1 parent bf154b2 commit 589d327

File tree

6 files changed

+271
-32
lines changed

6 files changed

+271
-32
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,3 +4,4 @@
44
docs/build
55
site/
66
temp.*.json
7+
vendor

docs/examples/persistence/persistence.go

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,10 @@ func main() {
1111

1212
// export the whole engine state as bytes
1313
// the export format is valid JSON and can be stored however you want
14-
bytes := bpmnEngine.Marshal()
15-
14+
bytes, err := bpmnEngine.Marshal()
15+
if err != nil {
16+
panic(err)
17+
}
1618
// debug print ...
1719
println(string(bytes))
1820

pkg/bpmn_engine/marshalling.go

Lines changed: 185 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -214,7 +214,7 @@ func (pii *processInstanceInfo) MarshalJSON() ([]byte, error) {
214214
case *eventBasedGatewayActivity:
215215
piia.ActivityAdapters = append(piia.ActivityAdapters, createEventBasedGatewayActivityAdapter(activity))
216216
default:
217-
panic(fmt.Sprintf("[invariant check] missing activity adapter for the type %T", a))
217+
return nil, fmt.Errorf("[invariant check] missing activity adapter for the type %T", a)
218218
}
219219
}
220220
return json.Marshal(piia)
@@ -229,8 +229,7 @@ func (pii *processInstanceInfo) UnmarshalJSON(data []byte) error {
229229
}
230230
pii.ProcessInfo = &ProcessInfo{ProcessKey: adapter.ProcessKey}
231231
pii.VariableHolder = adapter.VariableHolder
232-
recoverProcessInstanceActivitiesPart1(pii, adapter)
233-
return nil
232+
return recoverProcessInstanceActivitiesPart1(pii, adapter)
234233
}
235234

236235
func createEventBasedGatewayActivityAdapter(ebga *eventBasedGatewayActivity) *activityAdapter {
@@ -276,31 +275,172 @@ func (a activitySurrogate) Element() *BPMN20.BaseElement {
276275

277276
// ----------------------------------------------------------------------------
278277

279-
func (state *BpmnEngineState) Marshal() []byte {
278+
type VariableWrapFunc func(any) (any, error)
279+
280+
// marshalOptions Options that will be used while marshalling the engine
281+
type marshalOptions struct {
282+
marshalVariablesFunc VariableWrapFunc
283+
}
284+
285+
// MarshalOption is a function that modifies the marshalOptions
286+
type MarshalOption func(*marshalOptions) error
287+
288+
// WithMarshalVariableFunc sets a function that will be called for each variable in the engine's VarHolder
289+
// This allows you to customize variables before they are marshalled, e.g. to convert them to a different type
290+
func WithMarshalVariableFunc(fun VariableWrapFunc) MarshalOption {
291+
return func(opts *marshalOptions) error {
292+
opts.marshalVariablesFunc = fun
293+
return nil
294+
}
295+
}
296+
297+
// applyMarshalOptions Applies the given options and returns the applied marshalOptions
298+
func applyMarshalOptions(options ...MarshalOption) (*marshalOptions, error) {
299+
opts := &marshalOptions{}
300+
for _, o := range options {
301+
err := o(opts)
302+
if err != nil {
303+
return nil, fmt.Errorf("could not apply option: %w", err)
304+
}
305+
}
306+
307+
return opts, nil
308+
}
309+
310+
// Marshal marshals the engine into a byte array.
311+
// Options may be provided to configure the marshalling process.
312+
// It returns a byte array containing the marshalled engine state.
313+
// If there is an error applying the options, it will panic.
314+
//
315+
// Example:
316+
//
317+
// ```go
318+
// // Marshal with default options
319+
// data, err := bpmn_engine.Marshal()
320+
//
321+
// // Marshal with type information for complex variables
322+
// data, err := bpmn_engine.Marshal(WithMarshalComplexTypes())
323+
// ```
324+
func (state *BpmnEngineState) Marshal(options ...MarshalOption) ([]byte, error) {
325+
opts, err := applyMarshalOptions(options...)
326+
if err != nil {
327+
return nil, err
328+
}
329+
pis, err := createProcessInstances(state.processInstances, opts)
330+
if err != nil {
331+
return nil, err
332+
}
333+
280334
m := serializedBpmnEngine{
281335
Version: CurrentSerializerVersion,
282336
Name: state.name,
283337
MessageSubscriptions: state.messageSubscriptions,
284338
ProcessReferences: createReferences(state.processes),
285-
ProcessInstances: state.processInstances,
339+
ProcessInstances: pis,
286340
Timers: state.timers,
287341
Jobs: state.jobs,
288342
}
289343
bytes, err := json.Marshal(m)
290344
if err != nil {
291-
panic(err)
345+
return nil, err
346+
}
347+
return bytes, nil
348+
}
349+
350+
// wrapVariables takes a variable holder and wraps each variable with a complexVariable if the variable is a
351+
// struct or pointer
352+
func wrapVariables(vh VariableHolder, f VariableWrapFunc) (VariableHolder, error) {
353+
for k, v := range vh.variables {
354+
val, err := f(v)
355+
if err != nil {
356+
return vh, err
357+
}
358+
vh.variables[k] = val
359+
}
360+
// If there is a parent, create complex variables for it as well
361+
if vh.parent != nil {
362+
parent, err := wrapVariables(*vh.parent, f)
363+
if err != nil {
364+
return VariableHolder{}, err
365+
}
366+
vh.parent = &parent
367+
}
368+
return vh, nil
369+
}
370+
371+
// createProcessInstances Creates process instances that can be marshalled to JSON
372+
func createProcessInstances(pii []*processInstanceInfo, opts *marshalOptions) ([]*processInstanceInfo, error) {
373+
374+
// If exporting types is not enable, there is nothing extra to do
375+
if opts.marshalVariablesFunc == nil {
376+
return pii, nil
377+
}
378+
379+
// Create complex variables for each process instance
380+
for _, pi := range pii {
381+
cvs, err := wrapVariables(pi.VariableHolder, opts.marshalVariablesFunc)
382+
if err != nil {
383+
return nil, err
384+
}
385+
pi.VariableHolder = cvs
292386
}
293-
return bytes
387+
return pii, nil
388+
}
389+
390+
type VariableUnwrapFunc func(any) (any, error)
391+
392+
// unmarshalOptions Options that will be used while unmarshalling the engine
393+
type unmarshalOptions struct {
394+
// variableUnwrapFunc function that can be called to restore a variable from a marshalled state
395+
variableUnwrapFunc VariableUnwrapFunc
396+
}
397+
398+
// UnmarshalOption is a function that modifies the unmarshalOptions
399+
type UnmarshalOption func(*unmarshalOptions) error
400+
401+
// WithUnmarshalVariableFunc sets a function that will be called for each variable in the engine's VarHolder
402+
// This allows you to customize variables after they are unmarshalled, e.g. to convert them to a different type
403+
func WithUnmarshalVariableFunc(fun VariableUnwrapFunc) UnmarshalOption {
404+
return func(opts *unmarshalOptions) error {
405+
opts.variableUnwrapFunc = fun
406+
return nil
407+
}
408+
}
409+
410+
// applyUnmarshalOptions Applies the given options and returns the applied unmarshalOptions
411+
func applyUnmarshalOptions(options ...UnmarshalOption) (*unmarshalOptions, error) {
412+
opts := &unmarshalOptions{}
413+
for _, o := range options {
414+
err := o(opts)
415+
if err != nil {
416+
return nil, fmt.Errorf("could not apply option: %w", err)
417+
}
418+
}
419+
420+
return opts, nil
294421
}
295422

296423
// Unmarshal loads the data byte array and creates a new instance of the BPMN Engine
297424
// Will return an BpmnEngineUnmarshallingError, if there was an issue AND in case of error,
298425
// the engine return object is only partially initialized and likely not usable
299-
func Unmarshal(data []byte) (BpmnEngineState, error) {
426+
func Unmarshal(data []byte, opts ...UnmarshalOption) (BpmnEngineState, error) {
427+
428+
// Build an unmarshalOptions object from the provided options
429+
options, err := applyUnmarshalOptions(opts...)
430+
if err != nil {
431+
return BpmnEngineState{}, &BpmnEngineUnmarshallingError{
432+
Msg: "Failed to apply unmarshalling options",
433+
Err: err,
434+
}
435+
}
436+
300437
eng := serializedBpmnEngine{}
301-
err := json.Unmarshal(data, &eng)
438+
err = json.Unmarshal(data, &eng)
302439
if err != nil {
303-
panic(err)
440+
return BpmnEngineState{}, &BpmnEngineUnmarshallingError{
441+
Msg: "Failed to unmarshall engine data",
442+
Err: err,
443+
}
304444
}
305445
state := New()
306446
state.name = eng.Name
@@ -327,12 +467,15 @@ func Unmarshal(data []byte) (BpmnEngineState, error) {
327467
}
328468
if eng.ProcessInstances != nil {
329469
state.processInstances = eng.ProcessInstances
330-
err := recoverProcessInstances(&state)
470+
err = recoverProcessInstances(&state, options)
331471
if err != nil {
332472
return state, err
333473
}
334474
}
335-
recoverProcessInstanceActivitiesPart2(&state)
475+
err = recoverProcessInstanceActivitiesPart2(&state)
476+
if err != nil {
477+
return BpmnEngineState{}, err
478+
}
336479
if eng.MessageSubscriptions != nil {
337480
state.messageSubscriptions = eng.MessageSubscriptions
338481
err = recoverMessageSubscriptions(&state)
@@ -357,7 +500,7 @@ func Unmarshal(data []byte) (BpmnEngineState, error) {
357500
return state, nil
358501
}
359502

360-
func recoverProcessInstanceActivitiesPart1(pii *processInstanceInfo, adapter *processInstanceInfoAdapter) {
503+
func recoverProcessInstanceActivitiesPart1(pii *processInstanceInfo, adapter *processInstanceInfoAdapter) error {
361504
for _, aa := range adapter.ActivityAdapters {
362505
switch aa.Type {
363506
case gatewayActivityAdapterType:
@@ -378,12 +521,13 @@ func recoverProcessInstanceActivitiesPart1(pii *processInstanceInfo, adapter *pr
378521
OutboundActivityCompleted: aa.OutboundActivityCompleted,
379522
})
380523
default:
381-
panic(fmt.Sprintf("[invariant check] missing recovery code for actictyAdapter.Type=%d", aa.Type))
524+
return fmt.Errorf("[invariant check] missing recovery code for actictyAdapter.Type=%d", aa.Type)
382525
}
383526
}
527+
return nil
384528
}
385529

386-
func recoverProcessInstanceActivitiesPart2(state *BpmnEngineState) {
530+
func recoverProcessInstanceActivitiesPart2(state *BpmnEngineState) error {
387531
for _, pi := range state.processInstances {
388532
for _, a := range pi.activities {
389533
switch activity := a.(type) {
@@ -392,15 +536,33 @@ func recoverProcessInstanceActivitiesPart2(state *BpmnEngineState) {
392536
case *gatewayActivity:
393537
activity.element = BPMN20.FindBaseElementsById(pi.ProcessInfo.definitions.Process, (*a.Element()).GetId())[0]
394538
default:
395-
panic(fmt.Sprintf("[invariant check] missing case for activity type=%T", a))
539+
return fmt.Errorf("[invariant check] missing case for activity type=%T", a)
396540
}
397541
}
398542
}
543+
return nil
399544
}
400545

401-
// ----------------------------------------------------------------------------
546+
// recoverVariableInstances recovers the variable instances from the given VariableHolder
547+
func recoverVariableInstances(vh VariableHolder, opts *unmarshalOptions) (VariableHolder, error) {
548+
if opts.variableUnwrapFunc == nil {
549+
// Nothing additional to do
550+
return vh, nil
551+
}
402552

403-
func recoverProcessInstances(state *BpmnEngineState) error {
553+
for k, v := range vh.variables {
554+
val, err := opts.variableUnwrapFunc(v)
555+
if err != nil {
556+
return vh, err
557+
}
558+
559+
// Replace the variable with the proper instance
560+
vh.variables[k] = val
561+
}
562+
return vh, nil
563+
}
564+
565+
func recoverProcessInstances(state *BpmnEngineState, opts *unmarshalOptions) error {
404566
for i, pi := range state.processInstances {
405567
process := state.findProcess(pi.ProcessInfo.ProcessKey)
406568
if process == nil {
@@ -410,7 +572,11 @@ func recoverProcessInstances(state *BpmnEngineState) error {
410572
}
411573
}
412574
state.processInstances[i].ProcessInfo = process
413-
state.processInstances[i].VariableHolder = pi.VariableHolder
575+
vars, err := recoverVariableInstances(pi.VariableHolder, opts)
576+
if err != nil {
577+
return err
578+
}
579+
state.processInstances[i].VariableHolder = vars
414580
}
415581
return nil
416582
}

pkg/bpmn_engine/marshalling_test.go

Lines changed: 59 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package bpmn_engine
22

33
import (
4+
"encoding/json"
45
"fmt"
56
"github.com/corbym/gocrest/is"
67
"testing"
@@ -22,7 +23,8 @@ func Test_MarshallEngine(t *testing.T) {
2223

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

25-
data := bpmnEngine.Marshal()
26+
data, err := bpmnEngine.Marshal()
27+
then.AssertThat(t, err, is.Empty())
2628

2729
fmt.Println(string(data))
2830

@@ -32,3 +34,59 @@ func Test_MarshallEngine(t *testing.T) {
3234
then.AssertThat(t, vars.GetVariable("john"), is.EqualTo("doe"))
3335
then.AssertThat(t, vars.GetVariable("valueFromHandler"), is.EqualTo(true))
3436
}
37+
38+
func Test_MarshallEngineWithWrapping(t *testing.T) {
39+
bpmnEngine := New()
40+
process, _ := bpmnEngine.LoadFromFile("../../test-cases/simple_task-with_output_mapping.bpmn")
41+
bpmnEngine.NewTaskHandler().Id("id").Handler(func(job ActivatedJob) {
42+
job.SetVariable("valueFromHandler", true)
43+
job.SetVariable("otherVariable", "value")
44+
job.Complete()
45+
})
46+
variableContext := make(map[string]interface{})
47+
variableContext["hello"] = "world"
48+
variableContext["john"] = "doe"
49+
50+
_, _ = bpmnEngine.CreateAndRunInstance(process.ProcessKey, variableContext)
51+
52+
type Wrapper struct {
53+
Type string
54+
Value any
55+
}
56+
57+
// Simple wrapper function encapsulate a variable into a string
58+
wrapVariableFunc := func(variable any) (any, error) {
59+
w := Wrapper{
60+
Type: fmt.Sprintf("%T", variable),
61+
Value: variable,
62+
}
63+
64+
v, err := json.Marshal(w)
65+
if err != nil {
66+
return nil, err
67+
}
68+
69+
return string(v), nil
70+
}
71+
72+
// Simple unwrapper function unwrap a variable from a string
73+
unwrapVariableFunc := func(variable any) (any, error) {
74+
w := &Wrapper{}
75+
err := json.Unmarshal([]byte(variable.(string)), w)
76+
if err != nil {
77+
return nil, err
78+
}
79+
return w.Value, nil
80+
}
81+
82+
data, err := bpmnEngine.Marshal(WithMarshalVariableFunc(wrapVariableFunc))
83+
then.AssertThat(t, err, is.Empty())
84+
85+
fmt.Println(string(data))
86+
87+
bpmnEngine, _ = Unmarshal(data, WithUnmarshalVariableFunc(unwrapVariableFunc))
88+
vars := bpmnEngine.ProcessInstances()[0].VariableHolder
89+
then.AssertThat(t, vars.GetVariable("hello"), is.EqualTo("world"))
90+
then.AssertThat(t, vars.GetVariable("john"), is.EqualTo("doe"))
91+
then.AssertThat(t, vars.GetVariable("valueFromHandler"), is.EqualTo(true))
92+
}

tests/marshalling_integration_test.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,8 @@ func Test_unmarshalled_v1_contains_all_fields(t *testing.T) {
3232
then.AssertThat(t, err, is.Nil())
3333

3434
// when
35-
marshalledBytes := engine.Marshal()
35+
marshalledBytes, err := engine.Marshal()
36+
then.AssertThat(t, err, is.Nil())
3637

3738
equal, err := JSONBytesEqual(referenceBytes, marshalledBytes)
3839
then.AssertThat(t, err, is.Nil())

0 commit comments

Comments
 (0)