From 69da635a73651f8e3c02942c54434af3b8f0c02e Mon Sep 17 00:00:00 2001 From: Tanmoy Sen Gupta Date: Thu, 27 Jul 2023 01:52:00 +0530 Subject: [PATCH 01/12] updated Step model --- example/dynamic-steps-example/main.go | 4 +- example/multistep-example/main.go | 24 ++++++------ go-steps.go | 56 ++++++++++++++++++++++----- 3 files changed, 60 insertions(+), 24 deletions(-) diff --git a/example/dynamic-steps-example/main.go b/example/dynamic-steps-example/main.go index 2007296..a537f19 100644 --- a/example/dynamic-steps-example/main.go +++ b/example/dynamic-steps-example/main.go @@ -25,10 +25,10 @@ func main() { } // 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 { +func addStepToChain(step *gosteps.Step, stepFunc gosteps.StepFn, additionalArgs []interface{}) *gosteps.Step { temp := gosteps.Step{ Function: stepFunc, - AdditionalArgs: additionalArgs, + StepArgs: additionalArgs, } if step == nil { diff --git a/example/multistep-example/main.go b/example/multistep-example/main.go index f26efd3..db33dcb 100644 --- a/example/multistep-example/main.go +++ b/example/multistep-example/main.go @@ -16,20 +16,20 @@ const ( // 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}, + Function: funcs.Add, + StepArgs: []interface{}{2}, NextStep: &gosteps.Step{ Function: funcs.Sub, - AdditionalArgs: []interface{}{4}, + StepArgs: []interface{}{4}, NextStepResolver: nextStepResolver, PossibleNextSteps: gosteps.PossibleNextSteps{ { - Name: stepMultiply, - Function: funcs.Multiply, - AdditionalArgs: []interface{}{-5}, + Name: stepMultiply, + Function: funcs.Multiply, + StepArgs: []interface{}{-5}, NextStep: &gosteps.Step{ - Function: funcs.Add, - AdditionalArgs: []interface{}{100}, + Function: funcs.Add, + StepArgs: []interface{}{100}, NextStep: &gosteps.Step{ Function: funcs.StepWillError3Times, ErrorsToRetry: []error{ @@ -52,16 +52,16 @@ var steps = gosteps.Step{ }, }, { - Name: stepDivide, - Function: funcs.Divide, - AdditionalArgs: []interface{}{-2}, + Name: stepDivide, + Function: funcs.Divide, + StepArgs: []interface{}{-2}, }, }, }, } func main() { - initArgs := []interface{}{1} + initArgs := []interface{}{5} finalOutput, err := steps.Execute(initArgs...) if err != nil { fmt.Printf("error executing steps: %s, final output: [%s]\n", err, finalOutput) diff --git a/go-steps.go b/go-steps.go index 68dee81..23d41ab 100644 --- a/go-steps.go +++ b/go-steps.go @@ -20,12 +20,33 @@ var ( unresolvedStepError = "error: step [%s] is unresolved, no step found with this name." ) +type stepArgChainingType string + +var ( + // only previous step return will be passed to current step as arguments + PreviousStepReturns stepArgChainingType = "PreviousStepReturns" + + // only current step arguments (StepArgs) will be passed to current step as arguments + CurrentStepArgs stepArgChainingType = "CurrentStepArgs" + + // both previous step returns and current step arguments (StepArgs) will be passed + // to current step as arguments - previous step returns, followed by current step args, + PreviousReturnsWithCurrentStepArgs stepArgChainingType = "PreviousReturnsWithCurrentStepArgs" + + // both previous step returns and current step arguments (StepArgs) will be passed + // to current step as arguments - current step args, followed by previous step returns + CurrentStepArgsWithPreviousReturns stepArgChainingType = "CurrentStepArgsWithPreviousReturns" +) + type StepName string +type StepFn func(...interface{}) ([]interface{}, error) +type PossibleNextSteps []Step type Step struct { Name StepName - Function interface{} - AdditionalArgs []interface{} + Function StepFn + UseArguments stepArgChainingType + StepArgs []interface{} NextStep *Step PossibleNextSteps PossibleNextSteps NextStepResolver interface{} @@ -36,8 +57,6 @@ type Step struct { RetrySleep time.Duration } -type PossibleNextSteps []Step - func (step *Step) Execute(initArgs ...any) ([]interface{}, error) { // final output for step execution var finalOutput []interface{} @@ -59,20 +78,18 @@ func (step *Step) Execute(initArgs ...any) ([]interface{}, error) { for { // piping output from previous step as arguments for current step - stepArgs := []interface{}{} - stepArgs = append(stepArgs, stepOutput...) + var stepArgs []interface{} // only runs for first step in step if isEntryStep { - stepArgs = initArgs + step.StepArgs = append(step.StepArgs, initArgs...) isEntryStep = false } - // piping additional arguments as arguments for current step - stepArgs = append(stepArgs, step.AdditionalArgs...) + stepArgs = step.resolveStepArguments(stepOutput) // execute current step passing step arguments - stepOutput, stepError = step.Function.(func(...interface{}) ([]interface{}, error))(stepArgs...) + stepOutput, stepError = step.Function(stepArgs...) if stepError != nil { if !step.SkipRetry && step.shouldRetry(stepError) && stepReAttemptsLeft > 0 { // piping args as output for re-running same step @@ -146,3 +163,22 @@ func (step Step) resolveNextStep(stepName StepName) *Step { return nil } + +func (step Step) resolveStepArguments(previousStepReturns []interface{}) []interface{} { + var resolvedStepArgs []interface{} + + switch step.UseArguments { + case PreviousStepReturns: + resolvedStepArgs = previousStepReturns + case CurrentStepArgs: + resolvedStepArgs = step.StepArgs + case PreviousReturnsWithCurrentStepArgs: + resolvedStepArgs = append(resolvedStepArgs, previousStepReturns...) + resolvedStepArgs = append(resolvedStepArgs, step.StepArgs...) + default: // covers UseCurrentStepArgsWithPreviousReturns too + resolvedStepArgs = append(resolvedStepArgs, step.StepArgs...) + resolvedStepArgs = append(resolvedStepArgs, previousStepReturns...) + } + + return resolvedStepArgs +} From a7fe486dd3c3b20f0da10ab7ec8f850f93dae421 Mon Sep 17 00:00:00 2001 From: Tanmoy Sen Gupta Date: Thu, 27 Jul 2023 02:01:57 +0530 Subject: [PATCH 02/12] updated documentation --- README.md | 52 +++++++++++++++++++++++++++++++++++++++++++++------- 1 file changed, 45 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index 1624c2c..3afbeb7 100644 --- a/README.md +++ b/README.md @@ -17,8 +17,9 @@ The `Step` type contains the requirments to execute a step function and move to ```go type Step struct { Name StepName - Function interface{} - AdditionalArgs []interface{} + Function StepFn + UseArguments stepArgChainingType + StepArgs []interface{} NextStep *Step PossibleNextSteps PossibleNextSteps NextStepResolver interface{} @@ -35,7 +36,7 @@ type Step struct { |-------------------|------------------------------------------------------------------------------------------------------------------------------| | Name | Name of step | | Function | The function to execute | -| AdditionalArgs | any additional arguments need to pass to te step | +| StepArgs | 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 | @@ -53,17 +54,54 @@ To define steps, use the `gosteps.Steps` type and link the next steps in the `Ne var steps = gosteps.Step{ Name: "add", Function: funcs.Add, - AdditionalArgs: []interface{}{2, 3}, + StepArgs: []interface{}{2, 3}, NextStep: gosteps.Steps{ Name: "sub", Function: funcs.Sub, - AdditionalArgs: []interface{}{4}, + StepArgs: []interface{}{4}, }, } ``` Here the first step is `Add` and next step (and final) is `Sub`, so the output of Add is piped to Sub and that gives the final output. +### Choosing Arguments to Use + +Arguments to use in the current step function's execution is determined using the previous step returned values and the current step arguments specified. You can choose if you want to use previous step returned values as arguments to current step, or you want to use only current step arguments or both. You can also choose the order in which the returns from previous step, and step arguments are passed as arguments. This can be done so by passing the "strategy" to the `UseArguments` field. + +There are four directives defined in the library + +```go + // only previous step return will be passed to current step as arguments + PreviousStepReturns stepArgChainingType = "PreviousStepReturns" + + // only current step arguments (StepArgs) will be passed to current step as arguments + CurrentStepArgs stepArgChainingType = "CurrentStepArgs" + + // both previous step returns and current step arguments (StepArgs) will be passed + // to current step as arguments - previous step returns, followed by current step args, + PreviousReturnsWithCurrentStepArgs stepArgChainingType = "PreviousReturnsWithCurrentStepArgs" + + // both previous step returns and current step arguments (StepArgs) will be passed + // to current step as arguments - current step args, followed by previous step returns + CurrentStepArgsWithPreviousReturns stepArgChainingType = "CurrentStepArgsWithPreviousReturns" +``` + +These values can be used in the step chain as +```go +var steps = gosteps.Step{ + Name: "add", + Function: funcs.Add, + StepArgs: []interface{}{2, 3}, + NextStep: gosteps.Steps{ + Name: "sub", + Function: funcs.Sub, + StepArgs: []interface{}{4, 6}, + UseArguments: gosteps.CurrentStepArgs, + }, +} +``` + ### 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. Define the possible next steps in the `PossibleNextSteps` field, as an array of Steps. @@ -71,10 +109,10 @@ Some steps might have multiple candidates for next step and the executable next ```go PossibleNextSteps: gosteps.Step{ Function: funcs.Add, - AdditionalArgs: []interface{}{2}, + StepArgs: []interface{}{2}, NextStep: &gosteps.Step{ Function: funcs.Sub, - AdditionalArgs: []interface{}{4}, + StepArgs: []interface{}{4}, NextStepResolver: nextStepResolver, PossibleNextSteps: gosteps.PossibleNextSteps{ { From fb1ad3be873ae3068204fc272078662fe4313562 Mon Sep 17 00:00:00 2001 From: Tanmoy Sen Gupta Date: Thu, 27 Jul 2023 02:11:36 +0530 Subject: [PATCH 03/12] refactored code into multiple files --- README.md | 2 + go-steps.go | 184 ---------------------------------------------------- 2 files changed, 2 insertions(+), 184 deletions(-) delete mode 100644 go-steps.go diff --git a/README.md b/README.md index 3afbeb7..054f4a8 100644 --- a/README.md +++ b/README.md @@ -46,6 +46,8 @@ type Step struct { | 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 | +To see all the `types` available in go-steps see [`types.go`](./go_step_types.go) + ### Defining Steps To define steps, use the `gosteps.Steps` type and link the next steps in the `NextStep` field as follows diff --git a/go-steps.go b/go-steps.go deleted file mode 100644 index 23d41ab..0000000 --- a/go-steps.go +++ /dev/null @@ -1,184 +0,0 @@ -package gosteps - -import ( - "fmt" - "math" - "strings" - "time" -) - -const ( - // to avoid infinite runs due to the MaxAttempts not being set, we're keeping the default attempts to 100 - // if required, import and use the MaxMaxAttempts in the step.MaxAttempts field - DefaultMaxAttempts = 100 - - // the Max value is 9223372036854775807, which is not infinite but a huge number of attempts - MaxMaxAttempts = math.MaxInt -) - -var ( - unresolvedStepError = "error: step [%s] is unresolved, no step found with this name." -) - -type stepArgChainingType string - -var ( - // only previous step return will be passed to current step as arguments - PreviousStepReturns stepArgChainingType = "PreviousStepReturns" - - // only current step arguments (StepArgs) will be passed to current step as arguments - CurrentStepArgs stepArgChainingType = "CurrentStepArgs" - - // both previous step returns and current step arguments (StepArgs) will be passed - // to current step as arguments - previous step returns, followed by current step args, - PreviousReturnsWithCurrentStepArgs stepArgChainingType = "PreviousReturnsWithCurrentStepArgs" - - // both previous step returns and current step arguments (StepArgs) will be passed - // to current step as arguments - current step args, followed by previous step returns - CurrentStepArgsWithPreviousReturns stepArgChainingType = "CurrentStepArgsWithPreviousReturns" -) - -type StepName string -type StepFn func(...interface{}) ([]interface{}, error) -type PossibleNextSteps []Step - -type Step struct { - Name StepName - Function StepFn - UseArguments stepArgChainingType - StepArgs []interface{} - NextStep *Step - PossibleNextSteps PossibleNextSteps - NextStepResolver interface{} - ErrorsToRetry []error - StrictErrorCheck bool - SkipRetry bool - MaxAttempts int - RetrySleep time.Duration -} - -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 initial step or function - if step == nil || step.Function == nil { - return nil, nil - } - - // entry step - var isEntryStep bool = true - - // step reattepts - var stepReAttemptsLeft int = step.MaxAttempts - - for { - // piping output from previous step as arguments for current step - var stepArgs []interface{} - - // only runs for first step in step - if isEntryStep { - step.StepArgs = append(step.StepArgs, initArgs...) - isEntryStep = false - } - - stepArgs = step.resolveStepArguments(stepOutput) - - // execute current step passing step arguments - stepOutput, stepError = step.Function(stepArgs...) - if stepError != nil { - if !step.SkipRetry && step.shouldRetry(stepError) && stepReAttemptsLeft > 0 { - // piping args as output for re-running same step - stepOutput = stepArgs - - // decrementing re-attempts left for current run - stepReAttemptsLeft -= 1 - - // sleep step.RetrySleep duration if set - if step.RetrySleep > 0 { - time.Sleep(step.RetrySleep) - } - - continue - } - - // skip retry as step error not retryable - // return output of previous step and error - return stepArgs, stepError - } - - // no next step, this is the final step - if step.NextStep == nil && step.PossibleNextSteps == nil { - finalOutput = stepOutput - return finalOutput, nil - } - - // next step is dependant on conditions - 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) - } - step.NextStep = resolvedStep - } - - // set step as resolved or default nextStep - step = step.NextStep - - // if step.MaxAttempts is not set, set default max value - if step.MaxAttempts < 1 { - step.MaxAttempts = DefaultMaxAttempts - } - - // reset step re-attempts - stepReAttemptsLeft = step.MaxAttempts - 1 - } -} - -// should retry for error -func (step Step) shouldRetry(err error) bool { - for _, errorToRetry := range step.ErrorsToRetry { - if step.StrictErrorCheck && err.Error() == errorToRetry.Error() { - return true - } else if !step.StrictErrorCheck && strings.Contains(err.Error(), errorToRetry.Error()) { - return true - } - } - - return false -} - -// resolve next step by step name -func (step Step) resolveNextStep(stepName StepName) *Step { - for _, nextStep := range step.PossibleNextSteps { - if nextStep.Name == stepName { - return &nextStep - } - } - - return nil -} - -func (step Step) resolveStepArguments(previousStepReturns []interface{}) []interface{} { - var resolvedStepArgs []interface{} - - switch step.UseArguments { - case PreviousStepReturns: - resolvedStepArgs = previousStepReturns - case CurrentStepArgs: - resolvedStepArgs = step.StepArgs - case PreviousReturnsWithCurrentStepArgs: - resolvedStepArgs = append(resolvedStepArgs, previousStepReturns...) - resolvedStepArgs = append(resolvedStepArgs, step.StepArgs...) - default: // covers UseCurrentStepArgsWithPreviousReturns too - resolvedStepArgs = append(resolvedStepArgs, step.StepArgs...) - resolvedStepArgs = append(resolvedStepArgs, previousStepReturns...) - } - - return resolvedStepArgs -} From 1137fae6f82cfc3a8d11d532bab6d2c7194d5e5c Mon Sep 17 00:00:00 2001 From: Tanmoy Sen Gupta Date: Thu, 27 Jul 2023 02:12:52 +0530 Subject: [PATCH 04/12] refactored code into multiple files --- go_step_types.go | 31 ++++++++++ go_steps.go | 133 ++++++++++++++++++++++++++++++++++++++++++ go_steps_constants.go | 30 ++++++++++ go_steps_errors.go | 5 ++ 4 files changed, 199 insertions(+) create mode 100644 go_step_types.go create mode 100644 go_steps.go create mode 100644 go_steps_constants.go create mode 100644 go_steps_errors.go diff --git a/go_step_types.go b/go_step_types.go new file mode 100644 index 0000000..0c02d50 --- /dev/null +++ b/go_step_types.go @@ -0,0 +1,31 @@ +package gosteps + +import "time" + +// StepName type defined the name of the step +type StepName string + +// StepFn type defines the Step's Function +type StepFn func(...interface{}) ([]interface{}, error) + +// PossibleNextSteps type is a list/array of Step objects +type PossibleNextSteps []Step + +// Step type defines a step with all configurations for the step +type Step struct { + Name StepName + Function StepFn + UseArguments stepArgChainingType + StepArgs []interface{} + NextStep *Step + PossibleNextSteps PossibleNextSteps + NextStepResolver interface{} + ErrorsToRetry []error + StrictErrorCheck bool + SkipRetry bool + MaxAttempts int + RetrySleep time.Duration +} + +// enum type for step arguments chaining +type stepArgChainingType string diff --git a/go_steps.go b/go_steps.go new file mode 100644 index 0000000..9cc7424 --- /dev/null +++ b/go_steps.go @@ -0,0 +1,133 @@ +package gosteps + +import ( + "fmt" + "strings" + "time" +) + +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 initial step or function + if step == nil || step.Function == nil { + return nil, nil + } + + // entry step + var isEntryStep bool = true + + // step reattepts + var stepReAttemptsLeft int = step.MaxAttempts + + for { + // piping output from previous step as arguments for current step + var stepArgs []interface{} + + // only runs for first step in step + if isEntryStep { + step.StepArgs = append(step.StepArgs, initArgs...) + isEntryStep = false + } + + stepArgs = step.resolveStepArguments(stepOutput) + + // execute current step passing step arguments + stepOutput, stepError = step.Function(stepArgs...) + if stepError != nil { + if !step.SkipRetry && step.shouldRetry(stepError) && stepReAttemptsLeft > 0 { + // piping args as output for re-running same step + stepOutput = stepArgs + + // decrementing re-attempts left for current run + stepReAttemptsLeft -= 1 + + // sleep step.RetrySleep duration if set + if step.RetrySleep > 0 { + time.Sleep(step.RetrySleep) + } + + continue + } + + // skip retry as step error not retryable + // return output of previous step and error + return stepArgs, stepError + } + + // no next step, this is the final step + if step.NextStep == nil && step.PossibleNextSteps == nil { + finalOutput = stepOutput + return finalOutput, nil + } + + // next step is dependant on conditions + 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) + } + step.NextStep = resolvedStep + } + + // set step as resolved or default nextStep + step = step.NextStep + + // if step.MaxAttempts is not set, set default max value + if step.MaxAttempts < 1 { + step.MaxAttempts = DefaultMaxAttempts + } + + // reset step re-attempts + stepReAttemptsLeft = step.MaxAttempts - 1 + } +} + +// should retry for error +func (step Step) shouldRetry(err error) bool { + for _, errorToRetry := range step.ErrorsToRetry { + if step.StrictErrorCheck && err.Error() == errorToRetry.Error() { + return true + } else if !step.StrictErrorCheck && strings.Contains(err.Error(), errorToRetry.Error()) { + return true + } + } + + return false +} + +// resolve next step by step name +func (step Step) resolveNextStep(stepName StepName) *Step { + for _, nextStep := range step.PossibleNextSteps { + if nextStep.Name == stepName { + return &nextStep + } + } + + return nil +} + +func (step Step) resolveStepArguments(previousStepReturns []interface{}) []interface{} { + var resolvedStepArgs []interface{} + + switch step.UseArguments { + case PreviousStepReturns: + resolvedStepArgs = previousStepReturns + case CurrentStepArgs: + resolvedStepArgs = step.StepArgs + case PreviousReturnsWithCurrentStepArgs: + resolvedStepArgs = append(resolvedStepArgs, previousStepReturns...) + resolvedStepArgs = append(resolvedStepArgs, step.StepArgs...) + default: // covers UseCurrentStepArgsWithPreviousReturns too + resolvedStepArgs = append(resolvedStepArgs, step.StepArgs...) + resolvedStepArgs = append(resolvedStepArgs, previousStepReturns...) + } + + return resolvedStepArgs +} diff --git a/go_steps_constants.go b/go_steps_constants.go new file mode 100644 index 0000000..2781f4e --- /dev/null +++ b/go_steps_constants.go @@ -0,0 +1,30 @@ +package gosteps + +import ( + "math" +) + +const ( + // to avoid infinite runs due to the MaxAttempts not being set, we're keeping the default attempts to 100 + // if required, import and use the MaxMaxAttempts in the step.MaxAttempts field + DefaultMaxAttempts = 100 + + // the Max value is 9223372036854775807, which is not infinite but a huge number of attempts + MaxMaxAttempts = math.MaxInt +) + +var ( + // only previous step return will be passed to current step as arguments + PreviousStepReturns stepArgChainingType = "PreviousStepReturns" + + // only current step arguments (StepArgs) will be passed to current step as arguments + CurrentStepArgs stepArgChainingType = "CurrentStepArgs" + + // both previous step returns and current step arguments (StepArgs) will be passed + // to current step as arguments - previous step returns, followed by current step args, + PreviousReturnsWithCurrentStepArgs stepArgChainingType = "PreviousReturnsWithCurrentStepArgs" + + // both previous step returns and current step arguments (StepArgs) will be passed + // to current step as arguments - current step args, followed by previous step returns + CurrentStepArgsWithPreviousReturns stepArgChainingType = "CurrentStepArgsWithPreviousReturns" +) diff --git a/go_steps_errors.go b/go_steps_errors.go new file mode 100644 index 0000000..5ee3824 --- /dev/null +++ b/go_steps_errors.go @@ -0,0 +1,5 @@ +package gosteps + +var ( + unresolvedStepError = "error: step [%s] is unresolved, no step found with this name." +) From 86653ebe86d937a6e28ad803211bebcfa863e934 Mon Sep 17 00:00:00 2001 From: Tanmoy Sen Gupta Date: Thu, 27 Jul 2023 02:14:33 +0530 Subject: [PATCH 05/12] updated code --- go_steps.go | 1 + 1 file changed, 1 insertion(+) diff --git a/go_steps.go b/go_steps.go index 9cc7424..975e350 100644 --- a/go_steps.go +++ b/go_steps.go @@ -35,6 +35,7 @@ func (step *Step) Execute(initArgs ...any) ([]interface{}, error) { isEntryStep = false } + // resolve step arguments based on step.UseArguments stepArgs = step.resolveStepArguments(stepOutput) // execute current step passing step arguments From 2963f05e9047d85a5b364c459beeb33bca4f2af3 Mon Sep 17 00:00:00 2001 From: Tanmoy Sen Gupta Date: Thu, 27 Jul 2023 02:18:31 +0530 Subject: [PATCH 06/12] updated docs --- README.md | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/README.md b/README.md index 054f4a8..01dd9f2 100644 --- a/README.md +++ b/README.md @@ -46,6 +46,21 @@ type Step struct { | 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 | +### Step Function + +Define a step function using the following function signature. You can also use the `StepFn` type. + +```go +// StepFn Type +type StepFn func(...interface{}) ([]interface{}, error) + +// sample Step function +func Add(args ...any) ([]interface{}, error) { + fmt.Printf("Adding %v\n", args) + return []interface{}{args[0].(int) + args[1].(int)}, nil +} +``` + To see all the `types` available in go-steps see [`types.go`](./go_step_types.go) ### Defining Steps From 987b93d1d9da415051ed655daa6c76b52e6d7fc9 Mon Sep 17 00:00:00 2001 From: Tanmoy Sen Gupta Date: Thu, 27 Jul 2023 02:19:35 +0530 Subject: [PATCH 07/12] updated docs --- README.md | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 01dd9f2..b6c86e7 100644 --- a/README.md +++ b/README.md @@ -84,7 +84,11 @@ Here the first step is `Add` and next step (and final) is `Sub`, so the output o ### Choosing Arguments to Use -Arguments to use in the current step function's execution is determined using the previous step returned values and the current step arguments specified. You can choose if you want to use previous step returned values as arguments to current step, or you want to use only current step arguments or both. You can also choose the order in which the returns from previous step, and step arguments are passed as arguments. This can be done so by passing the "strategy" to the `UseArguments` field. +Arguments to use in the current step function's execution is determined using the previous step returned values and the current step arguments specified. + +You can choose if you want to use previous step returned values as arguments to current step, or you want to use only current step arguments or both. You can also choose the order in which the returns from previous step, and step arguments are passed as arguments. + +This can be done so by passing the "strategy" to the `UseArguments` field. There are four directives defined in the library From b511b0c4c4de6a72924b5488cbdc81a5990450eb Mon Sep 17 00:00:00 2001 From: Tanmoy Sen Gupta Date: Thu, 27 Jul 2023 02:19:54 +0530 Subject: [PATCH 08/12] updated docs --- README.md | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/README.md b/README.md index b6c86e7..9d12c42 100644 --- a/README.md +++ b/README.md @@ -88,9 +88,7 @@ Arguments to use in the current step function's execution is determined using th You can choose if you want to use previous step returned values as arguments to current step, or you want to use only current step arguments or both. You can also choose the order in which the returns from previous step, and step arguments are passed as arguments. -This can be done so by passing the "strategy" to the `UseArguments` field. - -There are four directives defined in the library +This can be done so by passing the "strategy" to the `UseArguments` field. There are four directives defined in the library ```go // only previous step return will be passed to current step as arguments From 4801513a86d88a3f705f736788be994c37a3cc20 Mon Sep 17 00:00:00 2001 From: Tanmoy Sen Gupta Date: Thu, 27 Jul 2023 02:21:50 +0530 Subject: [PATCH 09/12] updated docs --- README.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 9d12c42..6b4aa79 100644 --- a/README.md +++ b/README.md @@ -36,7 +36,8 @@ type Step struct { |-------------------|------------------------------------------------------------------------------------------------------------------------------| | Name | Name of step | | Function | The function to execute | -| StepArgs | any additional arguments need to pass to te step | +| StepArgs | any additional arguments need to pass to te step | +| UseArguments | Choosing Arguments to Use from previous step's return or current step's arguments or both. | | 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 | @@ -44,7 +45,7 @@ type Step struct { | 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 | +| RetrySleep | Sleep duration (type time.Duration) between each re-attempts | | ### Step Function From b29fbe38616842f6088e375f0416bd311bc95cca Mon Sep 17 00:00:00 2001 From: Tanmoy Sen Gupta Date: Thu, 27 Jul 2023 02:24:48 +0530 Subject: [PATCH 10/12] updated docs --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 6b4aa79..2fd8359 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,7 @@ GoSteps is a go library that helps in running functions as steps and reminds you to step out and get active (kidding!).

- Sublime's custom image + Sublime's custom image

The idea behind `gosteps` is to define set of functions as steps-chain (kind of a linked list) and execute them in a sequential fashion by piping output (other than error) from previous step, as arguments, into the next steps (not necessarily using the args). From 9634198f643480bc51504eea05306bbbe0dedb45 Mon Sep 17 00:00:00 2001 From: Tanmoy Sen Gupta Date: Fri, 28 Jul 2023 00:56:57 +0530 Subject: [PATCH 11/12] added unit tests --- Makefile | 7 ++ README.md | 2 +- example/dynamic-steps-example/main.go | 2 +- go.mod | 8 ++ go.sum | 9 ++ go_steps.go | 2 +- go_steps_test.go | 126 ++++++++++++++++++++++++++ 7 files changed, 153 insertions(+), 3 deletions(-) create mode 100644 Makefile create mode 100644 go.sum create mode 100644 go_steps_test.go diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..f17263e --- /dev/null +++ b/Makefile @@ -0,0 +1,7 @@ +coverage: + go test ./... -coverpkg=./... -coverprofile ./coverage.out + go tool cover -func ./coverage.out + +run-example: + go run example/multistep-example/main.go + go run example/dynamic-steps-example/main.go \ No newline at end of file diff --git a/README.md b/README.md index 2fd8359..b013ddc 100644 --- a/README.md +++ b/README.md @@ -66,7 +66,7 @@ To see all the `types` available in go-steps see [`types.go`](./go_step_types.go ### Defining Steps -To define steps, use the `gosteps.Steps` type and link the next steps in the `NextStep` field as follows +To define steps, use the `gosteps.Step` type and link the next steps in the `NextStep` field like a Linked List. ```go var steps = gosteps.Step{ diff --git a/example/dynamic-steps-example/main.go b/example/dynamic-steps-example/main.go index a537f19..0e2a255 100644 --- a/example/dynamic-steps-example/main.go +++ b/example/dynamic-steps-example/main.go @@ -27,7 +27,7 @@ func main() { // step to add new next step to step-chain; basically a linked-list insertion func addStepToChain(step *gosteps.Step, stepFunc gosteps.StepFn, additionalArgs []interface{}) *gosteps.Step { temp := gosteps.Step{ - Function: stepFunc, + Function: stepFunc, StepArgs: additionalArgs, } diff --git a/go.mod b/go.mod index c55a2ce..5bb51ea 100644 --- a/go.mod +++ b/go.mod @@ -1,3 +1,11 @@ module github.com/TanmoySG/go-steps go 1.18 + +require github.com/stretchr/testify v1.8.4 + +require ( + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..8cf6655 --- /dev/null +++ b/go.sum @@ -0,0 +1,9 @@ +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/go_steps.go b/go_steps.go index 975e350..4d0f808 100644 --- a/go_steps.go +++ b/go_steps.go @@ -95,7 +95,7 @@ func (step Step) shouldRetry(err error) bool { for _, errorToRetry := range step.ErrorsToRetry { if step.StrictErrorCheck && err.Error() == errorToRetry.Error() { return true - } else if !step.StrictErrorCheck && strings.Contains(err.Error(), errorToRetry.Error()) { + } else if !step.StrictErrorCheck && strings.Contains(errorToRetry.Error(), err.Error()) { return true } } diff --git a/go_steps_test.go b/go_steps_test.go new file mode 100644 index 0000000..e9779f0 --- /dev/null +++ b/go_steps_test.go @@ -0,0 +1,126 @@ +package gosteps + +import ( + "fmt" + "testing" + + "github.com/stretchr/testify/assert" +) + +func Test_resolveStepArguments(t *testing.T) { + + samplePreviousStepOutput := []interface{}{4} + sampleStepArgs := []interface{}{5} + + testCases := []struct { + useArguments stepArgChainingType + stepArgs []interface{} + previousStepOutput []interface{} + expectedResolvedStepArguments []interface{} + }{ + { + useArguments: PreviousStepReturns, + stepArgs: sampleStepArgs, + previousStepOutput: samplePreviousStepOutput, + expectedResolvedStepArguments: samplePreviousStepOutput, + }, + { + useArguments: CurrentStepArgs, + stepArgs: sampleStepArgs, + previousStepOutput: samplePreviousStepOutput, + expectedResolvedStepArguments: sampleStepArgs, + }, + { + useArguments: PreviousReturnsWithCurrentStepArgs, + stepArgs: sampleStepArgs, + previousStepOutput: samplePreviousStepOutput, + expectedResolvedStepArguments: []interface{}{4, 5}, + }, + { + useArguments: CurrentStepArgsWithPreviousReturns, + stepArgs: sampleStepArgs, + previousStepOutput: samplePreviousStepOutput, + expectedResolvedStepArguments: []interface{}{5, 4}, + }, + } + + for _, tc := range testCases { + step := Step{ + StepArgs: tc.stepArgs, + UseArguments: tc.useArguments, + } + + resolvedStepArgs := step.resolveStepArguments(tc.previousStepOutput) + + assert.Equal(t, tc.expectedResolvedStepArguments, resolvedStepArgs) + } +} + +func Test_resolveNextStep(t *testing.T) { + step := Step{ + PossibleNextSteps: PossibleNextSteps{ + { + Name: StepName("stepA"), + }, + { + Name: StepName("stepB"), + }, + { + Name: StepName("stepC"), + }, + }, + } + + // happy path + resolvedStep := step.resolveNextStep("stepA") + assert.NotNil(t, resolvedStep) + + // step not found + resolvedStep = step.resolveNextStep("stepD") + assert.Nil(t, resolvedStep) +} + +func Test_shouldRetry(t *testing.T) { + + testCases := []struct { + StrictErrorCheck bool + ExpectedShouldRetry bool + ErrorToCheck error + }{ + { + StrictErrorCheck: false, + ErrorToCheck: fmt.Errorf("error"), + ExpectedShouldRetry: true, + }, + { + StrictErrorCheck: true, + ErrorToCheck: fmt.Errorf("error"), + ExpectedShouldRetry: false, + }, + { + StrictErrorCheck: true, + ErrorToCheck: fmt.Errorf("error1"), + ExpectedShouldRetry: true, + }, + { + StrictErrorCheck: false, + ErrorToCheck: fmt.Errorf("wont retry for this error"), + ExpectedShouldRetry: false, + }, + } + + for _, tc := range testCases { + step := Step{ + ErrorsToRetry: []error{ + fmt.Errorf("error1"), + fmt.Errorf("error2"), + fmt.Errorf("error3"), + }, + StrictErrorCheck: tc.StrictErrorCheck, + } + + shouldRetry := step.shouldRetry(tc.ErrorToCheck) + + assert.Equal(t, tc.ExpectedShouldRetry, shouldRetry) + } +} From 72dfb1685dc0e462ff7406ef68d4f3b6e2cd86b3 Mon Sep 17 00:00:00 2001 From: Tanmoy Sen Gupta Date: Fri, 28 Jul 2023 00:57:19 +0530 Subject: [PATCH 12/12] added unit tests --- go.sum | 1 + 1 file changed, 1 insertion(+) diff --git a/go.sum b/go.sum index 8cf6655..fa4b6e6 100644 --- a/go.sum +++ b/go.sum @@ -4,6 +4,7 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=