Skip to content

Commit

Permalink
Adds v0.3.0 files to v0 module (#21)
Browse files Browse the repository at this point in the history
This PR reinstates v0 of the GoStep library within the v0 sub module, to
ensure backwards compatibility.

- Closes #20 

---
This pull request introduces significant changes to the `go-steps`
library, including updates to the documentation and the addition of new
examples, functions, and types for better functionality and error
handling. The most important changes include updates to the `README.md`,
new examples demonstrating the use of the library, and the introduction
of new types and constants.

Documentation Updates:
*
[`README.md`](diffhunk://#diff-b335630551682c19a781afebcf4d07bf978fb1f8ac04c6bf87428ed5106870f5L12-R12):
Updated the note about breaking changes in `go-steps` v1 and provided
guidance on how to continue using v0. Removed the "Help" section.
[[1]](diffhunk://#diff-b335630551682c19a781afebcf4d07bf978fb1f8ac04c6bf87428ed5106870f5L12-R12)
[[2]](diffhunk://#diff-b335630551682c19a781afebcf4d07bf978fb1f8ac04c6bf87428ed5106870f5L388-L391)

New Examples:
*
[`example/v0/dynamic-steps-example/main.go`](diffhunk://#diff-9f65a6c7c462b9aafde89f8842cea0388dfa47c29d4cdda9ed31b3c3cc803527R1-R46):
Added a new example demonstrating dynamic step chaining and execution.
*
[`example/v0/multistep-example/main.go`](diffhunk://#diff-5998290fc2698be4434d3be135de85779aea097d9bd3167869e3ecc471273123R1-R82):
Added a new example demonstrating multi-step execution with conditional
next steps and error handling.

New Functions and Types:
*
[`v0/go_step_types.go`](diffhunk://#diff-3871d5570fcc4606f51bc2b07d1a4afff1263ed57775afaca7a97f6fef8072f0R1-R31):
Introduced new types (`StepName`, `StepFn`, `PossibleNextSteps`, `Step`,
`stepArgChainingType`) to define steps and their configurations.
*
[`v0/go_steps_constants.go`](diffhunk://#diff-00c62dd19839758501e3de1e50db946f3af186f53029f5d0469c386ff0de908aR1-R30):
Added constants and variables for default and maximum step attempts, and
argument chaining types.

Step Execution and Error Handling:
*
[`v0/go_steps.go`](diffhunk://#diff-d8d5992c8abe8b10e8a23990c37c33069dbec70650c4ef09eb5652b66459ebeaR1-R134):
Implemented the `Execute` method for the `Step` type, including logic
for step execution, argument resolution, and error handling with
retries.
*
[`v0/go_steps_errors.go`](diffhunk://#diff-ee4342af74061ed13816e9ebb9deadca3ab123968b5fa1fef42361531a127b1cR1-R5):
Added error messages for unresolved steps.

Testing:
*
[`v0/go_steps_test.go`](diffhunk://#diff-18430ccf7ca865ff636a6222e0ce9055d20bc9324dd923322d894b408c259187R1-R126):
Added tests for step argument resolution, next step resolution, and
retry logic.
  • Loading branch information
TanmoySG authored Nov 19, 2024
2 parents aaa4e62 + f332d7f commit 7d2440f
Show file tree
Hide file tree
Showing 12 changed files with 497 additions and 5 deletions.
6 changes: 1 addition & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ GoSteps is a go library that helps in running functions as steps and reminds you
The idea behind `gosteps` is to define set of functions as chain-of-steps and execute them in a sequential fashion.

> [!NOTE]
> go-steps v1 is a breaking change and older v0 models wont work with the new version. For v0 documentation, examples refer to [v0.3.0-beta documentation](https://github.com/TanmoySG/go-steps/tree/v0.3.0-beta) or v0 guide [here](./v0/README.md).
> go-steps v1 is a breaking change and older v0 models won't work with the new version. To continue to use the v0, you can either use the `v0` sub-package of the library or the `v0.3.0` tag. For usage documentation of `v0`, refer to the [v0 README](./v0/README.md).
## Usage

Expand Down Expand Up @@ -385,7 +385,3 @@ func(c gosteps.GoStepsCtx) gosteps.StepResult {
### Example

Sample code can be found in the [example](./example/) directory.

### Help

If you want to help fix the above constraint or other bugs/issues, feel free to raise an Issue or Pull Request with the changes. It'd be an immense help!
File renamed without changes.
File renamed without changes
46 changes: 46 additions & 0 deletions example/v0/dynamic-steps-example/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
package main

import (
"fmt"

gosteps "github.com/TanmoySG/go-steps/v0"
"github.com/TanmoySG/go-steps/example/v0/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 gosteps.StepFn, additionalArgs []interface{}) *gosteps.Step {
temp := gosteps.Step{
Function: stepFunc,
StepArgs: additionalArgs,
}

if step == nil {
step = &temp
return step
}

curr := step
for curr.NextStep != nil {
curr = curr.NextStep
}

curr.NextStep = &temp
return step
}
42 changes: 42 additions & 0 deletions example/v0/funcs/funcs.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
package funcs

import "fmt"

var itr int = 1

func Add(args ...any) ([]interface{}, error) {
fmt.Printf("Adding %v\n", args)
return []interface{}{args[0].(int) + args[1].(int)}, nil
}

func Sub(args ...any) ([]interface{}, error) {
fmt.Printf("Sub %v\n", args)
return []interface{}{args[0].(int) - args[1].(int)}, nil
}

func Multiply(args ...any) ([]interface{}, error) {
fmt.Printf("Multiply %v\n", args)
return []interface{}{args[0].(int) * args[1].(int)}, nil
}

func Divide(args ...any) ([]interface{}, error) {
fmt.Printf("Divide %v\n", args)
return []interface{}{args[0].(int) / args[1].(int)}, nil
}

// Step will error 3times and return arg*30 and arg*31 on the 4th try
func StepWillError3Times(args ...any) ([]interface{}, error) {
fmt.Printf("Running fake error function for arg [%v]\n", args)
if itr == 3 {
return []interface{}{args[0].(int) * 30, args[0].(int) * 50}, nil
}

itr += 1
return nil, fmt.Errorf("error to retry")
}

// Step will error infinitely
func StepWillErrorInfinitely(args ...any) ([]interface{}, error) {
fmt.Printf("Running infinite fake error function for arg [%v]\n", args)
return nil, fmt.Errorf("error to retry")
}
Binary file added example/v0/multistep-example/diag.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
82 changes: 82 additions & 0 deletions example/v0/multistep-example/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
package main

import (
"fmt"
"time"

gosteps "github.com/TanmoySG/go-steps/v0"
"github.com/TanmoySG/go-steps/example/v0/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,
StepArgs: []interface{}{2},
NextStep: &gosteps.Step{
Function: funcs.Sub,
StepArgs: []interface{}{4},
NextStepResolver: nextStepResolver,
PossibleNextSteps: gosteps.PossibleNextSteps{
{
Name: stepMultiply,
Function: funcs.Multiply,
StepArgs: []interface{}{-5},
NextStep: &gosteps.Step{
Function: funcs.Add,
StepArgs: []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,
StepArgs: []interface{}{-2},
},
},
},
}

func main() {
initArgs := []interface{}{5}
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
}
31 changes: 31 additions & 0 deletions v0/go_step_types.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package v0

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
134 changes: 134 additions & 0 deletions v0/go_steps.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
package v0

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
}

// resolve step arguments based on step.UseArguments
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(errorToRetry.Error(), err.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
}
30 changes: 30 additions & 0 deletions v0/go_steps_constants.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package v0

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 v0/go_steps_errors.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package v0

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

0 comments on commit 7d2440f

Please sign in to comment.