Skip to content

Commit

Permalink
feat: support context and out variables
Browse files Browse the repository at this point in the history
Support task definitions that optionally take a context and/or return a
map of out variables to set
  • Loading branch information
justenwalker committed May 12, 2021
1 parent 80352c6 commit 04aac34
Show file tree
Hide file tree
Showing 19 changed files with 677 additions and 222 deletions.
10 changes: 7 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,15 +19,19 @@ import (
"context"

"go.justen.tech/goodwill/gw"
"go.justen.tech/goodwill/gw/value"
)

// Each task is a function with the following signature
// The function name should be capitalized

// Both the ctx context.Context arg and the map[string]value.Value return are optional
// and may be omitted from the method signature

// Default Task, Says Hello
func Default(ts *gw.Task) error {
_ = ts.Log(context.TODO(), "Hello, Goodwill!")
return nil
func Default(ctx context.Context, ts *gw.Task) (map[string]value.Value, error) {
_ = ts.Log(ctx, "Hello, Goodwill!")
return nil, nil
}
```

Expand Down
20 changes: 18 additions & 2 deletions gw/run.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
package gw

import (
"context"
"crypto/tls"
"crypto/x509"
"encoding/pem"
Expand Down Expand Up @@ -31,8 +32,15 @@ const (
ErrNoGRPCAddr = Error("no grpc address provided")
)

var ranOnce bool

// Run a function
func Run(fn func(ts *Task) error) error {
func Run(ctx context.Context, tr TaskRunner) error {
// Guard to prevent task from executing this function
if ranOnce {
return Error("Run called more than once")
}
ranOnce = true
var err error
if os.Getenv(EnvMagicKey) != EnvMagicValue {
return ErrNoMagicKey
Expand All @@ -59,7 +67,15 @@ func Run(fn func(ts *Task) error) error {
return fmt.Errorf("could not create context: %w", err)
}
defer c.Close()
return fn(c)
vars, err := tr.Run(ctx, c)
if err != nil {
return fmt.Errorf("task failed: %w", err)
}
err = c.Context().SetVariables(ctx, vars)
if err != nil {
return fmt.Errorf("failed to set output variables: %w", err)
}
return nil
}

func transportSecurity(opts []grpc.DialOption) ([]grpc.DialOption, error) {
Expand Down
38 changes: 38 additions & 0 deletions gw/task.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (
"context"
"fmt"
"go.justen.tech/goodwill/gw/jsonstore"
"go.justen.tech/goodwill/internal/pb"

"go.justen.tech/goodwill/gw/config"
"go.justen.tech/goodwill/gw/docker"
Expand Down Expand Up @@ -46,6 +47,16 @@ type Task struct {
conn *grpc.ClientConn
}

type TaskRunner interface {
Run(ctx context.Context, ts *Task) (map[string]value.Value, error)
}

type TaskRunnerFunc func(ctx context.Context, ts *Task) (map[string]value.Value, error)

func (f TaskRunnerFunc) Run(ctx context.Context, ts *Task) (map[string]value.Value, error) {
return f(ctx, ts)
}

const (
logLineVar = "_goodwill_log_line"
logLineCallExpr = `${log.call(` + logLineVar + `)}`
Expand Down Expand Up @@ -97,6 +108,33 @@ func (c *Task) KV() *kv.Service {
return kv.NewService(c.conn)
}

func (c *Task) setResult(ctx context.Context, vars map[string]value.Value) error {
if len(vars) == 0 {
return nil
}
var variables pb.Variables
for key, val := range vars {
variable, err := newVariable(key, val)
if err != nil {
return fmt.Errorf("set result variable %q=%v failed: %w", key, val, err)
}
variables.Parameters = append(variables.Parameters, variable)
}
_, err := pb.NewContextServiceClient(c.conn).SetTaskResult(ctx, &variables)
return err
}

func (c *Task) Close() error {
return c.conn.Close()
}

func newVariable(key string, val value.Value) (*pb.Variable, error) {
v, err := value.Marshal(val)
if err != nil {
return nil, err
}
return &pb.Variable{
Name: key,
Value: v,
}, nil
}
18 changes: 17 additions & 1 deletion gw/taskcontext/taskcontext.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,22 @@ func (c *Service) SetVariable(ctx context.Context, name string, value value.Valu
return err
}

func (c *Service) SetVariables(ctx context.Context, vars map[string]value.Value) error {
if len(vars) == 0 {
return nil
}
var variables pb.Variables
for key, val := range vars {
variable, err := newVariable(key, val)
if err != nil {
return fmt.Errorf("make variable %q=%v failed: %w", key, val, err)
}
variables.Parameters = append(variables.Parameters, variable)
}
_, err := c.client.SetVariables(ctx, &variables)
return err
}

// GetVariable gets the tasks variable to the given value
func (c *Service) GetVariable(ctx context.Context, name string, out value.ValueOut) error {
v, err := c.client.GetVariable(ctx, &pb.VariableName{Name: name})
Expand Down Expand Up @@ -69,7 +85,7 @@ func (c *Service) GetVariables(ctx context.Context) (map[string]interface{}, err
// EvaluateParams evaluates the given expression, and returns the result into the output value.
// The given map of parameters are set as variables before the expression is evaluated,
// which approximates a parameterized query; allowing a safer expression evaluation compared to string concatenation.
func (c *Service) EvaluateParams(ctx context.Context, expr string, out value.ValueOut, params map[string]value.Value, ) error {
func (c *Service) EvaluateParams(ctx context.Context, expr string, out value.ValueOut, params map[string]value.Value) error {
var parameters []*pb.Variable
for key, val := range params {
mv, err := value.Marshal(val)
Expand Down
48 changes: 47 additions & 1 deletion internal/command/render.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,11 @@ import (
"strings"
)

const (
gwPackage = "go.justen.tech/goodwill/gw"
valuePackage = "go.justen.tech/goodwill/gw/value"
)

func renderMain(w io.Writer, data ParsedData) error {
file := NewFile("main")
file.HeaderComment("+build goodwill")
Expand Down Expand Up @@ -63,6 +68,23 @@ func renderMain(w io.Writer, data ParsedData) error {
jenRunTask(fn),
Return()))
}

mainCode = append(mainCode,
//ctx, cancel := context.WithCancel(context.Background())
List(Id("ctx"), Id("cancel")).Op(":=").Qual("context", "WithCancel").Call(Qual("context", "Background").Call()),
//defer cancel()
Defer().Id("cancel").Call(),
//sigCh := make(chan signal.Signal)
Id("sigCh").Op(":=").Make(Id("chan").Qual("os", "Signal")),
//signal.Notify(sigCh, os.Interrupt)
Qual("os/signal", "Notify").Call(Id("sigCh"), Qual("os", "Interrupt")),
//go func() {
Go().Func().Params().Block(
// <-sigCh
Op("<-").Id("sigCh"),
// cancel()
Id("cancel").Call(),
).Call()) //}()
// switch strings.ToLower(funcName) { ... cases ... }
mainCode = append(mainCode, Switch(Qual("strings", "ToLower").Call(Id("funcName"))).Block(funcSwitchCases...))
file.Func().Id("usage").Params().Block(usageCode...)
Expand All @@ -75,7 +97,31 @@ func renderMain(w io.Writer, data ParsedData) error {
}

func jenRunTask(fn parse.TaskFunction) Code {
return Id("dieOnError").Call(Qual(parse.GwPackage, "Run").Call(Id(fn.Name)))
var block Code
if fn.Context {
// Func(ctx,ts)
block = Id(fn.Name).Call(Id("ctx"), Id("ts"))
} else {
// Func(ts)
block = Id(fn.Name).Call(Id("ts"))
}
if fn.OutVars {
// return Func(...)
block = Return(block)
} else {
// return nil, Func(...)
block = Return(Nil(), block)
}
// dieOnError(gw.Run(gw.TaskRunnerFunc(func(ctx context.Context, ts *gw.Task) (map[string]value.Value,error) {<block>}))
return Id("dieOnError").Call(Qual(gwPackage, "Run").Call(
Id("ctx"),
Qual(gwPackage, "TaskRunnerFunc").Parens(Func().Params(
Id("ctx").Qual("context", "Context"),
Id("ts").Add(Op("*")).Qual(gwPackage, "Task"),
).Params(
Map(String()).Qual(valuePackage, "Value"),
Id("error"),
).Block(block))))
}

func printlnStdErr(code ...Code) Code {
Expand Down
23 changes: 20 additions & 3 deletions internal/command/render_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,26 @@ func TestRenderMain(t *testing.T) {
g := goldie.New(t)
err := renderMain(&buf, ParsedData{
Functions: []parse.TaskFunction{
{Name: "Default", Doc: "Default Doc"},
{Name: "Task1", Doc: "Task 1 Doc"},
{Name: "Task2", Doc: "Task 2 Doc"},
{
Name: "ContextFunc",
Doc: "ContextFunc takes a context and a task and returns an error",
Context: true,
},
{
Name: "ContextOutFunc",
Doc: "ContextOutFunc takes a context and a task and returns output variables and an error",
Context: true,
OutVars: true,
},
{
Name: "Func",
Doc: "Func only takes a task and returns an error",
},
{
Name: "OutFunc",
Doc: "OutFunc takes a task and returns output variables and an error",
OutVars: true,
},
},
})
if err != nil {
Expand Down
41 changes: 32 additions & 9 deletions internal/command/testdata/main.golden
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,12 @@
package main

import (
"context"
"fmt"
gw "go.justen.tech/goodwill/gw"
value "go.justen.tech/goodwill/gw/value"
"os"
"os/signal"
"strings"
)

Expand All @@ -18,9 +21,10 @@ func dieOnError(err error) {
func usage() {
fmt.Printf("Usage: %s FUNCTION\n", os.Args[0])
fmt.Println("Functions:")
fmt.Println("\tDefault\tDefault Doc")
fmt.Println("\tTask1\tTask 1 Doc")
fmt.Println("\tTask2\tTask 2 Doc")
fmt.Println("\tContextFunc\tContextFunc takes a context and a task and returns an error")
fmt.Println("\tContextOutFunc\tContextOutFunc takes a context and a task and returns output variables and an error")
fmt.Println("\tFunc\tFunc only takes a task and returns an error")
fmt.Println("\tOutFunc\tOutFunc takes a task and returns output variables and an error")
os.Exit(128)
}
func main() {
Expand All @@ -29,15 +33,34 @@ func main() {
usage()
}
funcName := os.Args[1]
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
sigCh := make(chan os.Signal)
signal.Notify(sigCh, os.Interrupt)
go func() {
<-sigCh
cancel()
}()
switch strings.ToLower(funcName) {
case "default":
dieOnError(gw.Run(Default))
case "contextfunc":
dieOnError(gw.Run(ctx, gw.TaskRunnerFunc(func(ctx context.Context, ts *gw.Task) (map[string]value.Value, error) {
return nil, ContextFunc(ctx, ts)
})))
return
case "task1":
dieOnError(gw.Run(Task1))
case "contextoutfunc":
dieOnError(gw.Run(ctx, gw.TaskRunnerFunc(func(ctx context.Context, ts *gw.Task) (map[string]value.Value, error) {
return ContextOutFunc(ctx, ts)
})))
return
case "task2":
dieOnError(gw.Run(Task2))
case "func":
dieOnError(gw.Run(ctx, gw.TaskRunnerFunc(func(ctx context.Context, ts *gw.Task) (map[string]value.Value, error) {
return nil, Func(ts)
})))
return
case "outfunc":
dieOnError(gw.Run(ctx, gw.TaskRunnerFunc(func(ctx context.Context, ts *gw.Task) (map[string]value.Value, error) {
return OutFunc(ts)
})))
return
}
fmt.Fprintln(os.Stderr, "Unknown function name:", funcName)
Expand Down
Loading

0 comments on commit 04aac34

Please sign in to comment.