From d21596838205bb1836077319b8b5e1de6553c9c8 Mon Sep 17 00:00:00 2001 From: Jeroen Rinzema Date: Fri, 4 Sep 2020 09:51:08 +0200 Subject: [PATCH 1/3] feat: introducing template strings and strconcat function --- cmd/semaphore/daemon/config/options.go | 2 + cmd/semaphore/functions/errors.go | 43 ++++++++++++++++++++ cmd/semaphore/functions/functions.go | 8 ++++ cmd/semaphore/functions/strconcat.go | 55 ++++++++++++++++++++++++++ pkg/lookup/lookup.go | 17 +++++--- pkg/lookup/lookup_test.go | 43 ++++++++++++++++++++ pkg/specs/template/template.go | 16 ++++++++ 7 files changed, 179 insertions(+), 5 deletions(-) create mode 100644 cmd/semaphore/functions/errors.go create mode 100644 cmd/semaphore/functions/functions.go create mode 100644 cmd/semaphore/functions/strconcat.go diff --git a/cmd/semaphore/daemon/config/options.go b/cmd/semaphore/daemon/config/options.go index 4a5d0de3..f81e44ee 100644 --- a/cmd/semaphore/daemon/config/options.go +++ b/cmd/semaphore/daemon/config/options.go @@ -3,6 +3,7 @@ package config import ( "github.com/jexia/semaphore" "github.com/jexia/semaphore/cmd/semaphore/daemon/providers" + "github.com/jexia/semaphore/cmd/semaphore/functions" "github.com/jexia/semaphore/cmd/semaphore/middleware" "github.com/jexia/semaphore/pkg/broker" "github.com/jexia/semaphore/pkg/broker/logger" @@ -119,6 +120,7 @@ func NewCore(ctx *broker.Context, flags *Daemon) (semaphore.Options, error) { semaphore.WithCaller(micro.NewCaller("micro-grpc", microGRPC.NewService())), semaphore.WithCaller(grpc.NewCaller()), semaphore.WithCaller(http.NewCaller()), + semaphore.WithFunctions(functions.Default), } for _, path := range flags.Files { diff --git a/cmd/semaphore/functions/errors.go b/cmd/semaphore/functions/errors.go new file mode 100644 index 00000000..c3d3cc53 --- /dev/null +++ b/cmd/semaphore/functions/errors.go @@ -0,0 +1,43 @@ +package functions + +import ( + "fmt" + + "github.com/jexia/semaphore/pkg/prettyerr" + "github.com/jexia/semaphore/pkg/specs" + "github.com/jexia/semaphore/pkg/specs/types" +) + +type wrapErr struct { + Inner error +} + +func (i wrapErr) Unwrap() error { + return i.Inner +} + +// ErrInvalidArgument is thrown when a given argument is invalid +type ErrInvalidArgument struct { + wrapErr + Function string + Property *specs.Property + Expected types.Type +} + +// Error returns a description of the given error as a string +func (e ErrInvalidArgument) Error() string { + return fmt.Sprintf("invalid argument %s in %s expected %s", e.Property.Type, e.Function, e.Expected) +} + +// Prettify returns the prettified version of the given error +func (e ErrInvalidArgument) Prettify() prettyerr.Error { + return prettyerr.Error{ + Code: "InvalidArgument", + Message: e.Error(), + Details: map[string]interface{}{ + "Function": e.Function, + "Type": e.Property.Type, + "Expected": e.Expected, + }, + } +} diff --git a/cmd/semaphore/functions/functions.go b/cmd/semaphore/functions/functions.go new file mode 100644 index 00000000..d266cf03 --- /dev/null +++ b/cmd/semaphore/functions/functions.go @@ -0,0 +1,8 @@ +package functions + +import "github.com/jexia/semaphore/pkg/functions" + +// Default represents the default functions collection +var Default = functions.Custom{ + "strconcat": Strconcat, +} diff --git a/cmd/semaphore/functions/strconcat.go b/cmd/semaphore/functions/strconcat.go new file mode 100644 index 00000000..a739c3ee --- /dev/null +++ b/cmd/semaphore/functions/strconcat.go @@ -0,0 +1,55 @@ +package functions + +import ( + "github.com/jexia/semaphore/pkg/functions" + "github.com/jexia/semaphore/pkg/references" + "github.com/jexia/semaphore/pkg/specs" + "github.com/jexia/semaphore/pkg/specs/labels" + "github.com/jexia/semaphore/pkg/specs/types" +) + +// Strconcat compiles the given arguments and constructs a new executable +// function for the given arguments. +func Strconcat(args ...*specs.Property) (*specs.Property, functions.Exec, error) { + result := &specs.Property{ + Name: "concat", + Type: types.String, + Label: labels.Optional, + } + + for _, arg := range args { + if arg.Type != types.String { + return nil, nil, ErrInvalidArgument{ + Property: arg, + Expected: types.String, + Function: "strconcat", + } + } + } + + handle := func(store references.Store) error { + var result string + + for _, arg := range args { + var value string + + if arg.Default != nil { + value = arg.Default.(string) + } + + if arg.Reference != nil { + ref := store.Load(arg.Reference.Resource, arg.Reference.Path) + if ref != nil { + value = ref.Value.(string) + } + } + + result += value + } + + store.StoreValue("", ".", result) + return nil + } + + return result, handle, nil +} diff --git a/pkg/lookup/lookup.go b/pkg/lookup/lookup.go index ecbf5063..6212a9c2 100644 --- a/pkg/lookup/lookup.go +++ b/pkg/lookup/lookup.go @@ -77,12 +77,11 @@ func GetAvailableResources(flow specs.FlowInterface, breakpoint string) map[stri } if breakpoint == template.OutputResource { - references[template.ErrorResource] = ReferenceMap{ - template.ResponseResource: OnErrLookup(template.OutputResource, flow.GetOnError()), - } - if flow.GetOnError() != nil { - references[template.ErrorResource][template.ParamsResource] = ParamsLookup(flow.GetOnError().Params, flow, "") + references[template.ErrorResource] = ReferenceMap{ + template.ResponseResource: OnErrLookup(template.OutputResource, flow.GetOnError()), + template.ParamsResource: ParamsLookup(flow.GetOnError().Params, flow, ""), + } } } @@ -138,6 +137,14 @@ func GetAvailableResources(flow specs.FlowInterface, breakpoint string) map[stri } } + if flow.GetOutput() != nil { + if flow.GetOutput().Stack != nil { + for key, returns := range flow.GetOutput().Stack { + references[template.StackResource][key] = PropertyLookup(returns) + } + } + } + return references } diff --git a/pkg/lookup/lookup_test.go b/pkg/lookup/lookup_test.go index 2b73ac49..fb2aa2f4 100644 --- a/pkg/lookup/lookup_test.go +++ b/pkg/lookup/lookup_test.go @@ -474,6 +474,32 @@ func NewMockFlow(name string) *specs.Flow { Input: &specs.ParameterMap{ Property: NewInputMockProperty(), }, + OnError: &specs.OnError{ + Response: &specs.ParameterMap{ + Property: NewResultMockProperty(), + Params: map[string]*specs.Property{ + "message": { + Path: "message", + Default: "hello world", + Type: types.String, + Label: labels.Optional, + }, + "name": { + Path: "message", + Default: "hello world", + Type: types.String, + Label: labels.Optional, + }, + "reference": { + Path: "reference", + Reference: &specs.PropertyReference{ + Resource: name, + Path: "message", + }, + }, + }, + }, + }, Nodes: specs.NodeList{ NewMockCall("first"), NewMockCall("second"), @@ -508,6 +534,23 @@ func TestGetAvailableResources(t *testing.T) { result := GetAvailableResources(flow, "output") return expected, result }, + "output only": func() ([]string, map[string]ReferenceMap) { + flow := NewMockFlow("first") + + flow.OnError = nil + flow.Input = nil + flow.Nodes = nil + flow.Output = &specs.ParameterMap{ + Stack: map[string]*specs.Property{ + "hash": NewResultMockProperty(), + }, + } + + expected := []string{template.StackResource} + + result := GetAvailableResources(flow, "output") + return expected, result + }, "stack lookup request": func() ([]string, map[string]ReferenceMap) { flow := NewMockFlow("first") expected := []string{template.StackResource, template.ErrorResource, "input", "first", "second", "third"} diff --git a/pkg/specs/template/template.go b/pkg/specs/template/template.go index 1f243eb0..93dc4394 100644 --- a/pkg/specs/template/template.go +++ b/pkg/specs/template/template.go @@ -8,12 +8,16 @@ import ( "github.com/jexia/semaphore/pkg/broker/logger" "github.com/jexia/semaphore/pkg/broker/trace" "github.com/jexia/semaphore/pkg/specs" + "github.com/jexia/semaphore/pkg/specs/labels" + "github.com/jexia/semaphore/pkg/specs/types" "go.uber.org/zap" ) var ( // ReferencePattern is the matching pattern for references ReferencePattern = regexp.MustCompile(`^[a-zA-Z0-9_\-\.]*:[a-zA-Z0-9\^\&\%\$@_\-\.]*$`) + // StringPattern is the matching pattern for strings + StringPattern = regexp.MustCompile(`^\'(.+)\'$`) ) const ( @@ -117,6 +121,18 @@ func ParseContent(path string, name string, content string) (*specs.Property, er return ParseReference(path, name, content) } + if StringPattern.MatchString(content) { + matched := StringPattern.FindStringSubmatch(content) + + return &specs.Property{ + Name: name, + Path: path, + Type: types.String, + Label: labels.Optional, + Default: matched[1], + }, nil + } + return &specs.Property{ Name: name, Path: path, From 709bfb26d651695b6d56df33facd2d6231cc8a88 Mon Sep 17 00:00:00 2001 From: Jeroen Rinzema Date: Fri, 4 Sep 2020 10:00:33 +0200 Subject: [PATCH 2/3] feat: improved template test coverage --- pkg/specs/template/template_test.go | 79 ++++++++++++++++++++++++++++- 1 file changed, 78 insertions(+), 1 deletion(-) diff --git a/pkg/specs/template/template_test.go b/pkg/specs/template/template_test.go index 46c0cca9..87a91eea 100644 --- a/pkg/specs/template/template_test.go +++ b/pkg/specs/template/template_test.go @@ -6,6 +6,8 @@ import ( "github.com/jexia/semaphore/pkg/broker" "github.com/jexia/semaphore/pkg/broker/logger" "github.com/jexia/semaphore/pkg/specs" + "github.com/jexia/semaphore/pkg/specs/labels" + "github.com/jexia/semaphore/pkg/specs/types" ) func CompareProperties(t *testing.T, left specs.Property, right specs.Property) { @@ -55,6 +57,81 @@ func TestGetTemplateContent(t *testing.T) { } } +func TestParseTemplateContent(t *testing.T) { + name := "" + path := "message" + + tests := map[string]specs.Property{ + "'prefix'": { + Name: name, + Path: path, + Type: types.String, + Label: labels.Optional, + Default: "prefix", + }, + "'edge''": { + Name: name, + Path: path, + Type: types.String, + Label: labels.Optional, + Default: "edge'", + }, + "input:message": { + Name: name, + Path: path, + Reference: &specs.PropertyReference{ + Resource: "input", + Path: "message", + }, + }, + "input:user-id": { + Name: name, + Path: path, + Reference: &specs.PropertyReference{ + Resource: "input", + Path: "user-id", + }, + }, + "input.header:Authorization": { + Name: name, + Path: path, + Reference: &specs.PropertyReference{ + Resource: "input.header", + Path: "authorization", + }, + }, + "input.header:User-Id": { + Name: name, + Path: path, + Reference: &specs.PropertyReference{ + Resource: "input.header", + Path: "user-id", + }, + }, + "input.header:": { + Path: path, + Reference: &specs.PropertyReference{ + Resource: "input.header", + }, + }, + } + + for input, expected := range tests { + t.Run(input, func(t *testing.T) { + property, err := ParseContent(path, name, input) + if err != nil { + t.Fatal(err) + } + + if property.Path != expected.Path { + t.Errorf("unexpected path '%s', expected '%s'", property.Path, expected.Path) + } + + CompareProperties(t, *property, expected) + }) + } +} + func TestParseReference(t *testing.T) { name := "" path := "message" @@ -166,7 +243,7 @@ func TestUnknownReferencePattern(t *testing.T) { } } -func TestParseTemplate(t *testing.T) { +func TestParseReferenceTemplates(t *testing.T) { name := "" tests := map[string]specs.Property{ From 81e84fd4909134c5231bd6224eed05a25624022c Mon Sep 17 00:00:00 2001 From: Jeroen Rinzema Date: Fri, 4 Sep 2020 11:57:31 +0200 Subject: [PATCH 3/3] refactor: use strings.Builder --- cmd/semaphore/functions/strconcat.go | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/cmd/semaphore/functions/strconcat.go b/cmd/semaphore/functions/strconcat.go index a739c3ee..68deb837 100644 --- a/cmd/semaphore/functions/strconcat.go +++ b/cmd/semaphore/functions/strconcat.go @@ -1,6 +1,8 @@ package functions import ( + "strings" + "github.com/jexia/semaphore/pkg/functions" "github.com/jexia/semaphore/pkg/references" "github.com/jexia/semaphore/pkg/specs" @@ -28,7 +30,7 @@ func Strconcat(args ...*specs.Property) (*specs.Property, functions.Exec, error) } handle := func(store references.Store) error { - var result string + result := strings.Builder{} for _, arg := range args { var value string @@ -44,10 +46,13 @@ func Strconcat(args ...*specs.Property) (*specs.Property, functions.Exec, error) } } - result += value + _, err := result.WriteString(value) + if err != nil { + return err + } } - store.StoreValue("", ".", result) + store.StoreValue("", ".", result.String()) return nil }