Skip to content

Commit

Permalink
Added UseArguments field to specify which arguments to use [#6]
Browse files Browse the repository at this point in the history
Merge pull request #6 from TanmoySG/runner-func-sig-change
---
Added `UseArguments` field to specify which arguments to use

In this PR, added the feature to pick between previous step returned values, or current step's arguments or use both as arguments to run the current step.

```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,
	},
}
```

Available configurations for `UseArguments`

```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"
```

Also refactored and added Unit Tests.
  • Loading branch information
TanmoySG authored Jul 27, 2023
2 parents 68ebd86 + 72dfb16 commit 6066592
Show file tree
Hide file tree
Showing 11 changed files with 325 additions and 64 deletions.
7 changes: 7 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
@@ -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
78 changes: 68 additions & 10 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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!).

<p align="center">
<img src="https://codetips.dev/wp-content/uploads/2022/08/gopher-go-run.jpg" alt="Sublime's custom image" width="300" height="300" />
<img src="https://upload.wikimedia.org/wikipedia/commons/b/b3/Go_gopher_pencil_running.jpg" alt="Sublime's custom image" width="300" height="300" />
</p>

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).
Expand All @@ -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{}
Expand All @@ -35,46 +36,103 @@ 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 |
| 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 |
| 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 |
| 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

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{
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.

```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{
{
Expand Down
6 changes: 3 additions & 3 deletions example/dynamic-steps-example/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Function: stepFunc,
StepArgs: additionalArgs,
}

if step == nil {
Expand Down
24 changes: 12 additions & 12 deletions example/multistep-example/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -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{
Expand All @@ -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)
Expand Down
8 changes: 8 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
@@ -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
)
10 changes: 10 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
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 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=
31 changes: 31 additions & 0 deletions go_step_types.go
Original file line number Diff line number Diff line change
@@ -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
64 changes: 25 additions & 39 deletions go-steps.go → go_steps.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,42 +2,10 @@ 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 StepName string

type Step struct {
Name StepName
Function interface{}
AdditionalArgs []interface{}
NextStep *Step
PossibleNextSteps PossibleNextSteps
NextStepResolver interface{}
ErrorsToRetry []error
StrictErrorCheck bool
SkipRetry bool
MaxAttempts int
RetrySleep time.Duration
}

type PossibleNextSteps []Step

func (step *Step) Execute(initArgs ...any) ([]interface{}, error) {
// final output for step execution
var finalOutput []interface{}
Expand All @@ -59,20 +27,19 @@ 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...)
// resolve step arguments based on step.UseArguments
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
Expand Down Expand Up @@ -128,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
}
}
Expand All @@ -146,3 +113,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
}
30 changes: 30 additions & 0 deletions go_steps_constants.go
Original file line number Diff line number Diff line change
@@ -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"
)
5 changes: 5 additions & 0 deletions go_steps_errors.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package gosteps

var (
unresolvedStepError = "error: step [%s] is unresolved, no step found with this name."
)
Loading

0 comments on commit 6066592

Please sign in to comment.