Skip to content

Commit 954ba52

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

File tree

6 files changed

+281
-32
lines changed

6 files changed

+281
-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: 195 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,182 @@ func (a activitySurrogate) Element() *BPMN20.BaseElement {
276275

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

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

296433
// Unmarshal loads the data byte array and creates a new instance of the BPMN Engine
297434
// Will return an BpmnEngineUnmarshallingError, if there was an issue AND in case of error,
298435
// the engine return object is only partially initialized and likely not usable
299-
func Unmarshal(data []byte) (BpmnEngineState, error) {
436+
func Unmarshal(data []byte, opts ...UnmarshalOption) (BpmnEngineState, error) {
437+
438+
// Build an unmarshalOptions object from the provided options
439+
options, err := applyUnmarshalOptions(opts...)
440+
if err != nil {
441+
return BpmnEngineState{}, &BpmnEngineUnmarshallingError{
442+
Msg: "Failed to apply unmarshalling options",
443+
Err: err,
444+
}
445+
}
446+
300447
eng := serializedBpmnEngine{}
301-
err := json.Unmarshal(data, &eng)
448+
err = json.Unmarshal(data, &eng)
302449
if err != nil {
303-
panic(err)
450+
return BpmnEngineState{}, &BpmnEngineUnmarshallingError{
451+
Msg: "Failed to unmarshall engine data",
452+
Err: err,
453+
}
304454
}
305455
state := New()
306456
state.name = eng.Name
@@ -327,12 +477,15 @@ func Unmarshal(data []byte) (BpmnEngineState, error) {
327477
}
328478
if eng.ProcessInstances != nil {
329479
state.processInstances = eng.ProcessInstances
330-
err := recoverProcessInstances(&state)
480+
err = recoverProcessInstances(&state, options)
331481
if err != nil {
332482
return state, err
333483
}
334484
}
335-
recoverProcessInstanceActivitiesPart2(&state)
485+
err = recoverProcessInstanceActivitiesPart2(&state)
486+
if err != nil {
487+
return BpmnEngineState{}, err
488+
}
336489
if eng.MessageSubscriptions != nil {
337490
state.messageSubscriptions = eng.MessageSubscriptions
338491
err = recoverMessageSubscriptions(&state)
@@ -357,7 +510,7 @@ func Unmarshal(data []byte) (BpmnEngineState, error) {
357510
return state, nil
358511
}
359512

360-
func recoverProcessInstanceActivitiesPart1(pii *processInstanceInfo, adapter *processInstanceInfoAdapter) {
513+
func recoverProcessInstanceActivitiesPart1(pii *processInstanceInfo, adapter *processInstanceInfoAdapter) error {
361514
for _, aa := range adapter.ActivityAdapters {
362515
switch aa.Type {
363516
case gatewayActivityAdapterType:
@@ -378,12 +531,13 @@ func recoverProcessInstanceActivitiesPart1(pii *processInstanceInfo, adapter *pr
378531
OutboundActivityCompleted: aa.OutboundActivityCompleted,
379532
})
380533
default:
381-
panic(fmt.Sprintf("[invariant check] missing recovery code for actictyAdapter.Type=%d", aa.Type))
534+
return fmt.Errorf("[invariant check] missing recovery code for actictyAdapter.Type=%d", aa.Type)
382535
}
383536
}
537+
return nil
384538
}
385539

386-
func recoverProcessInstanceActivitiesPart2(state *BpmnEngineState) {
540+
func recoverProcessInstanceActivitiesPart2(state *BpmnEngineState) error {
387541
for _, pi := range state.processInstances {
388542
for _, a := range pi.activities {
389543
switch activity := a.(type) {
@@ -392,15 +546,33 @@ func recoverProcessInstanceActivitiesPart2(state *BpmnEngineState) {
392546
case *gatewayActivity:
393547
activity.element = BPMN20.FindBaseElementsById(pi.ProcessInfo.definitions.Process, (*a.Element()).GetId())[0]
394548
default:
395-
panic(fmt.Sprintf("[invariant check] missing case for activity type=%T", a))
549+
return fmt.Errorf("[invariant check] missing case for activity type=%T", a)
396550
}
397551
}
398552
}
553+
return nil
399554
}
400555

401-
// ----------------------------------------------------------------------------
556+
// recoverVariableInstances recovers the variable instances from the given VariableHolder
557+
func recoverVariableInstances(vh VariableHolder, opts *unmarshalOptions) (VariableHolder, error) {
558+
if opts.variableUnwrapFunc == nil {
559+
// Nothing additional to do
560+
return vh, nil
561+
}
402562

403-
func recoverProcessInstances(state *BpmnEngineState) error {
563+
for k, v := range vh.variables {
564+
val, err := opts.variableUnwrapFunc(k, v)
565+
if err != nil {
566+
return vh, err
567+
}
568+
569+
// Replace the variable with the proper instance
570+
vh.variables[k] = val
571+
}
572+
return vh, nil
573+
}
574+
575+
func recoverProcessInstances(state *BpmnEngineState, opts *unmarshalOptions) error {
404576
for i, pi := range state.processInstances {
405577
process := state.findProcess(pi.ProcessInfo.ProcessKey)
406578
if process == nil {
@@ -410,7 +582,11 @@ func recoverProcessInstances(state *BpmnEngineState) error {
410582
}
411583
}
412584
state.processInstances[i].ProcessInfo = process
413-
state.processInstances[i].VariableHolder = pi.VariableHolder
585+
vars, err := recoverVariableInstances(pi.VariableHolder, opts)
586+
if err != nil {
587+
return err
588+
}
589+
state.processInstances[i].VariableHolder = vars
414590
}
415591
return nil
416592
}

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(_ string, 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(_ string, 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+
}

0 commit comments

Comments
 (0)