diff --git a/README.md b/README.md index 7afdbb7..1624c2c 100644 --- a/README.md +++ b/README.md @@ -16,50 +16,48 @@ The `Step` type contains the requirments to execute a step function and move to ```go type Step struct { - Name StepName - Function interface{} - AdditionalArgs []interface{} - NextSteps []Step - NextStepResolver interface{} - ErrorsToRetry []error - StrictErrorCheck bool - SkipRetry bool - MaxAttempts int - RetrySleep time.Duration + Name StepName + Function interface{} + AdditionalArgs []interface{} + NextStep *Step + PossibleNextSteps PossibleNextSteps + NextStepResolver interface{} + ErrorsToRetry []error + StrictErrorCheck bool + SkipRetry bool + MaxAttempts int + RetrySleep time.Duration } ``` -| Field | Description | -|------------------|--------------------------------------------------------------------------------------------------------------------------| -| Name | Name of step | -| Function | The function to execute | -| AdditionalArgs | any additional arguments need to pass to te step | -| NextSteps | Candidate functions for next step (multiple next steps in-case of condition based execution) | -| NextStepResolver | A function that returns the step name, based on conditions, that is used to pick the nextStep from NextSteps | -| ErrorsToRetry | A list of error to retry step for | -| StrictErrorCheck | If set to `true` exact error is matched, else only presence of error is checked | -| SkipRetry | If set to `true` step is not retried for any error | -| MaxAttempts | Max attempts are the number of times the step is tried (first try + subsequent retries). If not set, it'll run 100 times | -| RetrySleep | Sleep duration (type time.Duration) between each re-attempts | +| Field | Description | +|-------------------|------------------------------------------------------------------------------------------------------------------------------| +| Name | Name of step | +| Function | The function to execute | +| AdditionalArgs | any additional arguments need to pass to te step | +| NextStep | Next Step for the current step. If next step needs to be conditional dont set this and use `PossibleNextSteps` field instead | +| PossibleNextSteps | Candidate functions for next step (pick from multiple possible next steps based on condition) | +| NextStepResolver | A function that returns the step name, based on conditions, that is used to pick the NextStep from PossibleNextSteps | +| ErrorsToRetry | A list of error to retry step for | +| StrictErrorCheck | If set to `true` exact error is matched, else only presence of error is checked | +| SkipRetry | If set to `true` step is not retried for any error | +| MaxAttempts | Max attempts are the number of times the step is tried (first try + subsequent retries). If not set, it'll run 100 times | +| RetrySleep | Sleep duration (type time.Duration) between each re-attempts | ### Defining Steps -To define steps, use the `gosteps.Steps` type and link the next steps in the `NextSteps` field as follows +To define steps, use the `gosteps.Steps` type and link the next steps in the `NextStep` field as follows ```go -var steps = gosteps.Steps{ - { - Name: "add", - Function: funcs.Add, - AdditionalArgs: []interface{}{2, 3}, - NextSteps: gosteps.Steps{ - { - Name: "sub", - Function: funcs.Sub, - AdditionalArgs: []interface{}{4}, - }, - }, +var steps = gosteps.Step{ + Name: "add", + Function: funcs.Add, + AdditionalArgs: []interface{}{2, 3}, + NextStep: gosteps.Steps{ + Name: "sub", + Function: funcs.Sub, + AdditionalArgs: []interface{}{4}, }, } ``` @@ -68,7 +66,31 @@ Here the first step is `Add` and next step (and final) is `Sub`, so the output o ### Conditional Steps -Some steps might have multiple candidates for next step and the executable next step is to be picked based on the output of the current step. To do so, steps with multiple next step candidates must use the `NextStepResolver` field passing a resolver function that returns the Name of the function to use as next step. +Some steps might have multiple candidates for next step and the executable next step is to be picked based on the output of the current step. Define the possible next steps in the `PossibleNextSteps` field, as an array of Steps. + +```go +PossibleNextSteps: gosteps.Step{ + Function: funcs.Add, + AdditionalArgs: []interface{}{2}, + NextStep: &gosteps.Step{ + Function: funcs.Sub, + AdditionalArgs: []interface{}{4}, + NextStepResolver: nextStepResolver, + PossibleNextSteps: gosteps.PossibleNextSteps{ + { + Name: "multiply", + Function: funcs.Multiply, + }, + { + Name: "divide", + Function: funcs.Divide, + }, + } + } +} +``` + +To pick the required next step based on conditions, we must use the `NextStepResolver` field passing a resolver function that returns the Name of the function to use as next step. The resolver function should be of type `func(args ...any) string`, where `args` are the output of current step and returned string is the name of the step to use. @@ -147,9 +169,9 @@ If you want to help fix the above constraint or other bugs/issues, feel free to ## Example -In [this example](./example/main.go), we've used a set of complex steps with conditional step and retry. The flow of the same is +In [this example](./example/multistep-example/main.go), we've used a set of complex steps with conditional step and retry. The flow of the same is -![flow](./example/diag.png) +![flow](./example/multistep-example/diag.png) Execute the example steps diff --git a/example/diag.png b/example/dynamic-steps-example/diag.png similarity index 100% rename from example/diag.png rename to example/dynamic-steps-example/diag.png diff --git a/example/dynamic-steps-example/main.go b/example/dynamic-steps-example/main.go new file mode 100644 index 0000000..2007296 --- /dev/null +++ b/example/dynamic-steps-example/main.go @@ -0,0 +1,46 @@ +package main + +import ( + "fmt" + + gosteps "github.com/TanmoySG/go-steps" + "github.com/TanmoySG/go-steps/example/funcs" +) + +func main() { + + intsToAdd := []int{1, 4, 7, 10} + + var step *gosteps.Step + for _, val := range intsToAdd { + step = addStepToChain(step, funcs.Add, []interface{}{val}) + } + + finalOutput, err := step.Execute(1) + if err != nil { + fmt.Printf("error executing steps: %s, final output: [%s]\n", err, finalOutput) + } + + fmt.Printf("Final Output: [%v]\n", finalOutput) +} + +// step to add new next step to step-chain; basically a linked-list insertion +func addStepToChain(step *gosteps.Step, stepFunc interface{}, additionalArgs []interface{}) *gosteps.Step { + temp := gosteps.Step{ + Function: stepFunc, + AdditionalArgs: additionalArgs, + } + + if step == nil { + step = &temp + return step + } + + curr := step + for curr.NextStep != nil { + curr = curr.NextStep + } + + curr.NextStep = &temp + return step +} diff --git a/example/main.go b/example/main.go deleted file mode 100644 index 39e6e57..0000000 --- a/example/main.go +++ /dev/null @@ -1,93 +0,0 @@ -package main - -import ( - "fmt" - "time" - - gosteps "github.com/TanmoySG/go-steps" - "github.com/TanmoySG/go-steps/example/funcs" -) - -const ( - stepMultiply = "Multiply" - stepDivide = "Divide" -) - -// reading/maintaining this is a bit tricky will add -// a functional way to create this in the next version -var steps = gosteps.Steps{ - { - Function: funcs.Add, - NextSteps: gosteps.Steps{ - { - Function: funcs.Sub, - AdditionalArgs: []interface{}{4}, - NextStepResolver: nextStepResolver, - NextSteps: gosteps.Steps{ - { - Name: stepMultiply, - Function: funcs.Multiply, - AdditionalArgs: []interface{}{-5}, - NextSteps: gosteps.Steps{ - { - Function: funcs.Add, - AdditionalArgs: []interface{}{100}, - NextSteps: gosteps.Steps{ - { - Function: funcs.StepWillError3Times, - ErrorsToRetry: []error{ - fmt.Errorf("error"), - }, - NextSteps: gosteps.Steps{ - { - Function: funcs.StepWillErrorInfinitely, - ErrorsToRetry: []error{ - fmt.Errorf("error"), - }, - NextSteps: gosteps.Steps{ - { - Function: funcs.Multiply, - }, - }, - StrictErrorCheck: true, - MaxAttempts: 5, // use gosteps.MaxMaxAttempts for Maximum Possible reattempts - }, - }, - MaxAttempts: 5, - RetrySleep: 1 * time.Second, - }, - }, - }, - }, - }, - { - Name: stepDivide, - Function: funcs.Divide, - AdditionalArgs: []interface{}{-2}, - }, - }, - }, - }, - }, -} - -func main() { - initArgs := []interface{}{1, 2} - finalOutput, err := steps.Execute(initArgs...) - if err != nil { - fmt.Printf("error executing steps: %s, final output: [%s]\n", err, finalOutput) - } - - fmt.Printf("Final Output: [%v]\n", finalOutput) -} - -// step resolver -func nextStepResolver(args ...any) string { - if args[0].(int) < 0 { - fmt.Printf("StepResolver [%v]: Arguments is Negative, going with Multiply\n", args) - return stepMultiply - } - - fmt.Printf("StepResolver [%v]: Arguments is Positive, going with Divide\n", args) - return stepDivide -} diff --git a/example/multistep-example/diag.png b/example/multistep-example/diag.png new file mode 100644 index 0000000..af39f13 Binary files /dev/null and b/example/multistep-example/diag.png differ diff --git a/example/multistep-example/main.go b/example/multistep-example/main.go new file mode 100644 index 0000000..f26efd3 --- /dev/null +++ b/example/multistep-example/main.go @@ -0,0 +1,82 @@ +package main + +import ( + "fmt" + "time" + + gosteps "github.com/TanmoySG/go-steps" + "github.com/TanmoySG/go-steps/example/funcs" +) + +const ( + stepMultiply = "Multiply" + stepDivide = "Divide" +) + +// reading/maintaining this is a bit tricky will add +// a functional way to create this in the next version +var steps = gosteps.Step{ + Function: funcs.Add, + AdditionalArgs: []interface{}{2}, + NextStep: &gosteps.Step{ + Function: funcs.Sub, + AdditionalArgs: []interface{}{4}, + NextStepResolver: nextStepResolver, + PossibleNextSteps: gosteps.PossibleNextSteps{ + { + Name: stepMultiply, + Function: funcs.Multiply, + AdditionalArgs: []interface{}{-5}, + NextStep: &gosteps.Step{ + Function: funcs.Add, + AdditionalArgs: []interface{}{100}, + NextStep: &gosteps.Step{ + Function: funcs.StepWillError3Times, + ErrorsToRetry: []error{ + fmt.Errorf("error"), + }, + NextStep: &gosteps.Step{ + Function: funcs.StepWillErrorInfinitely, + ErrorsToRetry: []error{ + fmt.Errorf("error"), + }, + NextStep: &gosteps.Step{ + Function: funcs.Multiply, + }, + StrictErrorCheck: false, + MaxAttempts: 5, // use gosteps.MaxMaxAttempts for Maximum Possible reattempts + }, + MaxAttempts: 5, + RetrySleep: 1 * time.Second, + }, + }, + }, + { + Name: stepDivide, + Function: funcs.Divide, + AdditionalArgs: []interface{}{-2}, + }, + }, + }, +} + +func main() { + initArgs := []interface{}{1} + finalOutput, err := steps.Execute(initArgs...) + if err != nil { + fmt.Printf("error executing steps: %s, final output: [%s]\n", err, finalOutput) + } + + fmt.Printf("Final Output: [%v]\n", finalOutput) +} + +// step resolver +func nextStepResolver(args ...any) string { + if args[0].(int) < 0 { + fmt.Printf("StepResolver [%v]: Arguments is Negative, going with Multiply\n", args) + return stepMultiply + } + + fmt.Printf("StepResolver [%v]: Arguments is Positive, going with Divide\n", args) + return stepDivide +} diff --git a/go-steps.go b/go-steps.go index 0021e3e..68dee81 100644 --- a/go-steps.go +++ b/go-steps.go @@ -23,36 +23,36 @@ var ( type StepName string type Step struct { - Name StepName - Function interface{} - AdditionalArgs []interface{} - NextSteps []Step - NextStepResolver interface{} - ErrorsToRetry []error - StrictErrorCheck bool - SkipRetry bool - MaxAttempts int - RetrySleep time.Duration + Name StepName + Function interface{} + AdditionalArgs []interface{} + NextStep *Step + PossibleNextSteps PossibleNextSteps + NextStepResolver interface{} + ErrorsToRetry []error + StrictErrorCheck bool + SkipRetry bool + MaxAttempts int + RetrySleep time.Duration } -type Steps []Step +type PossibleNextSteps []Step -func (steps Steps) Execute(initArgs ...any) ([]interface{}, error) { - // final output for steps execution +func (step *Step) Execute(initArgs ...any) ([]interface{}, error) { + // final output for step execution var finalOutput []interface{} // initialize step output and step error var stepOutput []interface{} var stepError error - // no entry or initial step - if len(steps) == 0 { + // no initial step or function + if step == nil || step.Function == nil { return nil, nil } // entry step var isEntryStep bool = true - step := steps[0] // step reattepts var stepReAttemptsLeft int = step.MaxAttempts @@ -62,7 +62,7 @@ func (steps Steps) Execute(initArgs ...any) ([]interface{}, error) { stepArgs := []interface{}{} stepArgs = append(stepArgs, stepOutput...) - // only runs for first step in steps + // only runs for first step in step if isEntryStep { stepArgs = initArgs isEntryStep = false @@ -95,28 +95,23 @@ func (steps Steps) Execute(initArgs ...any) ([]interface{}, error) { } // no next step, this is the final step - if step.NextSteps == nil { + if step.NextStep == nil && step.PossibleNextSteps == nil { finalOutput = stepOutput return finalOutput, nil } - // if there are multiple next steps but no resolver, - // first one in the list is considered as next step - var nextStep Step = step.NextSteps[0] - // next step is dependant on conditions - if step.NextSteps != nil && step.NextStepResolver != nil { + if step.PossibleNextSteps != nil && step.NextStepResolver != nil { nextStepName := step.NextStepResolver.(func(...interface{}) string)(stepOutput...) resolvedStep := step.resolveNextStep(StepName(nextStepName)) if resolvedStep == nil { return stepOutput, fmt.Errorf(unresolvedStepError, step.Name) } - - nextStep = *resolvedStep + step.NextStep = resolvedStep } // set step as resolved or default nextStep - step = nextStep + step = step.NextStep // if step.MaxAttempts is not set, set default max value if step.MaxAttempts < 1 { @@ -143,7 +138,7 @@ func (step Step) shouldRetry(err error) bool { // resolve next step by step name func (step Step) resolveNextStep(stepName StepName) *Step { - for _, nextStep := range step.NextSteps { + for _, nextStep := range step.PossibleNextSteps { if nextStep.Name == stepName { return &nextStep }