diff --git a/pkg/expressions/errors.go b/pkg/expressions/errors.go new file mode 100644 index 0000000..74ef3af --- /dev/null +++ b/pkg/expressions/errors.go @@ -0,0 +1,67 @@ +package expressions + +import ( + "errors" + "fmt" + "strings" +) + +var ( + ErrorUnterminated = errors.New("non-terminated statement in expression") + ErrorEmptyStatement = errors.New("empty statement in expression") + ErrorMissingFunction = errors.New("missing function") +) + +type DetailedError struct { + Err error + Context string + Index int +} + +func (s *DetailedError) Error() string { + return fmt.Sprintf("At `%s` (%d): %v", s.Context, s.Index, s.Err) +} + +func (s *DetailedError) Unwrap() error { + return s.Err +} + +type CompilerErrors struct { + Errors []*DetailedError + Expression string +} + +func (s *CompilerErrors) Error() string { + if len(s.Errors) == 1 { + return s.Errors[0].Error() + } + var sb strings.Builder + sb.WriteString("Compiler Errors in: `") + sb.WriteString(s.Expression) + sb.WriteString("`\n") + for _, e := range s.Errors { + sb.WriteString(" ") + sb.WriteString(e.Error()) + sb.WriteString("\n") + } + return sb.String() +} + +func (s *CompilerErrors) Unwrap() error { + return s.Errors[0] +} + +func (s *CompilerErrors) add(underlying error, context string, offset int) { + s.Errors = append(s.Errors, &DetailedError{underlying, context, offset}) +} + +func (s *CompilerErrors) empty() bool { + return len(s.Errors) == 0 +} + +// Inherit all errors from another compiler error set, and offset the index eg. if nested compile +func (s *CompilerErrors) inherit(other *CompilerErrors, offset int) { + for _, oe := range other.Errors { + s.add(oe.Err, oe.Context, oe.Index+offset) + } +} diff --git a/pkg/expressions/keyBuilder.go b/pkg/expressions/keyBuilder.go index 20b6f5f..aa8b1bc 100644 --- a/pkg/expressions/keyBuilder.go +++ b/pkg/expressions/keyBuilder.go @@ -1,7 +1,6 @@ package expressions import ( - "errors" "fmt" "strings" ) @@ -38,12 +37,18 @@ func (s *KeyBuilder) Funcs(funcs map[string]KeyBuilderFunction) { } } -// Compile builds a new key-builder -func (s *KeyBuilder) Compile(template string) (*CompiledKeyBuilder, error) { +// Compile builds a new key-builder, returning error(s) on build issues +// if the CompiledKeyBuilder is not nil, then something is still useable (albeit may have problems) +func (s *KeyBuilder) Compile(template string) (*CompiledKeyBuilder, *CompilerErrors) { kb := &CompiledKeyBuilder{ stages: make([]KeyBuilderStage, 0), } + errs := CompilerErrors{ + Expression: template, + } + + startStatement := 0 inStatement := 0 var sb strings.Builder runes := []rune(template) @@ -59,6 +64,7 @@ func (s *KeyBuilder) Compile(template string) (*CompiledKeyBuilder, error) { kb.stages = append(kb.stages, stageLiteral(sb.String())) sb.Reset() } + startStatement = i } else { sb.WriteRune(r) } @@ -68,23 +74,32 @@ func (s *KeyBuilder) Compile(template string) (*CompiledKeyBuilder, error) { if inStatement == 0 { args := splitTokenizedArguments(sb.String()) if len(args) == 0 { - return nil, errors.New("empty statement in expression") + errs.add(ErrorEmptyStatement, string(runes[startStatement:i+1]), startStatement) } else if len(args) == 1 { // Simple variable keyword like "{1}" kb.stages = append(kb.stages, stageSimpleVariable(args[0])) } else { // Complex function like "{add 1 2}" f := s.functions[args[0]] if f != nil { - compiledArgs := make([]KeyBuilderStage, 0) + compiledArgs := make([]KeyBuilderStage, 0, len(args)-1) for _, arg := range args[1:] { compiled, err := s.Compile(arg) if err != nil { - return nil, err + errs.inherit(err, startStatement) + } + if compiled != nil { + compiledArgs = append(compiledArgs, compiled.joinStages()) } - compiledArgs = append(compiledArgs, compiled.joinStages()) } - kb.stages = append(kb.stages, f(compiledArgs)) + stage, err := f(compiledArgs) + if err != nil { + errs.add(err, sb.String(), startStatement) + } + if stage != nil { + kb.stages = append(kb.stages, stage) + } } else { - kb.stages = append(kb.stages, stageError(fmt.Sprintf("Err:%s", args[0]))) + kb.stages = append(kb.stages, stageLiteral(fmt.Sprintf("", args[0]))) + errs.add(ErrorMissingFunction, sb.String(), startStatement) } } @@ -98,7 +113,7 @@ func (s *KeyBuilder) Compile(template string) (*CompiledKeyBuilder, error) { } if inStatement != 0 { - return nil, errors.New("non-terminated statement in expression") + errs.add(ErrorUnterminated, string(runes[startStatement:]), startStatement) } if sb.Len() > 0 { @@ -109,6 +124,9 @@ func (s *KeyBuilder) Compile(template string) (*CompiledKeyBuilder, error) { kb = kb.optimize() } + if !errs.empty() { + return kb, &errs + } return kb, nil } diff --git a/pkg/expressions/keyBuilder_test.go b/pkg/expressions/keyBuilder_test.go index 70b1820..a6c974f 100644 --- a/pkg/expressions/keyBuilder_test.go +++ b/pkg/expressions/keyBuilder_test.go @@ -2,6 +2,7 @@ package expressions import ( "bytes" + "errors" "strconv" "testing" "text/template" @@ -9,26 +10,17 @@ import ( "github.com/stretchr/testify/assert" ) -var testData = []string{"ab", "cd", "123"} -var testKeyData = map[string]string{ - "test": "testval", -} - -type TestContext struct{} - -func (s *TestContext) GetMatch(idx int) string { - return testData[idx] -} - -func (s *TestContext) GetKey(key string) string { - return testKeyData[key] +var testContext = KeyBuilderContextArray{ + Elements: []string{"ab", "cd", "123"}, + Keys: map[string]string{ + "test": "testval", + }, } -var testContext = TestContext{} - func TestSimpleKey(t *testing.T) { - kb, _ := NewKeyBuilder().Compile("test 123") + kb, err := NewKeyBuilder().Compile("test 123") key := kb.BuildKey(&testContext) + assert.Nil(t, err) assert.Equal(t, "test 123", key) assert.Equal(t, 1, len(kb.stages)) } @@ -43,13 +35,26 @@ func TestSimpleReplacement(t *testing.T) { func TestUnterminatedReplacement(t *testing.T) { kb, err := NewKeyBuilder().Compile("{0} is {123") assert.Error(t, err) - assert.Nil(t, kb) + assert.Len(t, err.Errors, 1) + assert.NotEmpty(t, err.Error()) + assert.NotNil(t, kb) // Still returns workable expression, but with errors +} + +func TestManyErrors(t *testing.T) { + kb, err := NewKeyBuilder().Compile("{0} is {abc 1} and {unclosed") + assert.NotNil(t, kb) + assert.Error(t, err) + assert.Len(t, err.Errors, 2) + assert.ErrorIs(t, err.Errors[0], ErrorMissingFunction) + assert.ErrorIs(t, err.Errors[1], ErrorUnterminated) + assert.ErrorIs(t, err, ErrorMissingFunction) + assert.NotEmpty(t, err.Error()) } func TestEscapedString(t *testing.T) { - kb, _ := NewKeyBuilder().Compile("{0} is \\{1\\} cool\\n\\t\\a") + kb, _ := NewKeyBuilder().Compile("{0} is \\{1\\} cool\\n\\t\\a\\r") key := kb.BuildKey(&testContext) - assert.Equal(t, "ab is {1} cool\n\ta", key) + assert.Equal(t, "ab is {1} cool\n\ta\r", key) assert.Equal(t, 2, len(kb.stages)) } @@ -67,10 +72,11 @@ func TestStringKey(t *testing.T) { func TestEmptyStatement(t *testing.T) { kb, err := NewKeyBuilder().Compile("{} test") - assert.Nil(t, kb) + assert.NotNil(t, kb) assert.Error(t, err) } +// BenchmarkSimpleReplacement-4 7515498 141.4 ns/op 24 B/op 2 allocs/op func BenchmarkSimpleReplacement(b *testing.B) { kb, _ := NewKeyBuilder().Compile("{0} is awesome") for n := 0; n < b.N; n++ { @@ -78,6 +84,7 @@ func BenchmarkSimpleReplacement(b *testing.B) { } } +// BenchmarkGoTextTemplate-4 3139363 406.3 ns/op 160 B/op 3 allocs/op func BenchmarkGoTextTemplate(b *testing.B) { kb, _ := template.New("test").Parse("{a} is awesome") for n := 0; n < b.N; n++ { @@ -89,7 +96,10 @@ func BenchmarkGoTextTemplate(b *testing.B) { // func tests var simpleFuncs = map[string]KeyBuilderFunction{ - "addi": func(args []KeyBuilderStage) KeyBuilderStage { + "addi": func(args []KeyBuilderStage) (KeyBuilderStage, error) { + if len(args) < 2 { + return nil, errors.New("expected at least 2 args") + } return func(ctx KeyBuilderContext) string { val, _ := strconv.Atoi(args[0](ctx)) for i := 1; i < len(args); i++ { @@ -97,7 +107,7 @@ var simpleFuncs = map[string]KeyBuilderFunction{ val += aVal } return strconv.Itoa(val) - } + }, nil }, } @@ -109,6 +119,24 @@ func TestSimpleFuncs(t *testing.T) { assert.Equal(t, "value: 5", kb.BuildKey(&KeyBuilderContextArray{})) } +func TestSimpleFuncErrors(t *testing.T) { + k := NewKeyBuilder() + k.Funcs(simpleFuncs) + kb, err := k.Compile("value: {addi 1} {addi 1 2}") + assert.Error(t, err) + assert.NotNil(t, kb) + assert.Equal(t, "value: 3", kb.BuildKey(&KeyBuilderContextArray{})) +} + +func TestDeepFuncError(t *testing.T) { + k := NewKeyBuilder() + k.Funcs(simpleFuncs) + kb, err := k.Compile("value: {addi 1 {addi 1}} {addi 1 2}") + assert.Error(t, err) + assert.NotNil(t, kb) + assert.Equal(t, "value: 1 3", kb.BuildKey(&KeyBuilderContextArray{})) +} + func TestManyStages(t *testing.T) { k := NewKeyBuilderEx(false) k.Funcs(simpleFuncs) @@ -117,6 +145,8 @@ func TestManyStages(t *testing.T) { assert.Equal(t, "value: -1 8", kb.BuildKey(&KeyBuilderContextArray{})) } +// Optimization + func TestManyStagesOptimize(t *testing.T) { k := NewKeyBuilderEx(true) diff --git a/pkg/expressions/stage.go b/pkg/expressions/stage.go index 5b000f7..900b51a 100644 --- a/pkg/expressions/stage.go +++ b/pkg/expressions/stage.go @@ -1,7 +1,6 @@ package expressions import ( - "fmt" "strconv" "strings" ) @@ -12,7 +11,7 @@ const ( ) // KeyBuilderFunction defines a helper function at runtime -type KeyBuilderFunction func([]KeyBuilderStage) KeyBuilderStage +type KeyBuilderFunction func([]KeyBuilderStage) (KeyBuilderStage, error) // KeyBuilderStage is a stage within the compiled builder type KeyBuilderStage func(KeyBuilderContext) string @@ -35,13 +34,6 @@ func stageSimpleVariable(s string) KeyBuilderStage { }) } -func stageError(msg string) KeyBuilderStage { - errMessage := fmt.Sprintf("<%s>", msg) - return KeyBuilderStage(func(context KeyBuilderContext) string { - return errMessage - }) -} - // make a delim-separated array func MakeArray(args ...string) string { var sb strings.Builder diff --git a/pkg/expressions/stageAnalysis_test.go b/pkg/expressions/stageAnalysis_test.go index b465d0a..1772763 100644 --- a/pkg/expressions/stageAnalysis_test.go +++ b/pkg/expressions/stageAnalysis_test.go @@ -39,3 +39,9 @@ func TestEvaluateStageIndex(t *testing.T) { assert.Equal(t, "test2", EvalStageIndexOrDefault(stages, 1, "nope")) assert.Equal(t, "nope", EvalStageIndexOrDefault(stages, 2, "nope")) } + +func TestEvaluationStageInt(t *testing.T) { + assert.Equal(t, 5, EvalStageInt(testStageNoContext("5"), 1)) + assert.Equal(t, 1, EvalStageInt(testStageNoContext("5b"), 1)) + assert.Equal(t, 1, EvalStageInt(testStageUseContext("5"), 1)) +} diff --git a/pkg/expressions/stdlib/drawing.go b/pkg/expressions/stdlib/drawing.go index a4255ca..5c66e53 100644 --- a/pkg/expressions/stdlib/drawing.go +++ b/pkg/expressions/stdlib/drawing.go @@ -8,21 +8,21 @@ import ( "strings" ) -func kfColor(args []KeyBuilderStage) KeyBuilderStage { +func kfColor(args []KeyBuilderStage) (KeyBuilderStage, error) { if len(args) != 2 { - return stageLiteral(ErrorArgCount) + return stageErrArgCount(args, 2) } colorCode, _ := color.LookupColorByName(EvalStageOrDefault(args[0], "")) return KeyBuilderStage(func(context KeyBuilderContext) string { return color.Wrap(colorCode, args[1](context)) - }) + }), nil } // {repeat c {count}} -func kfRepeat(args []KeyBuilderStage) KeyBuilderStage { +func kfRepeat(args []KeyBuilderStage) (KeyBuilderStage, error) { if len(args) != 2 { - return stageLiteral(ErrorArgCount) + return stageErrArgCount(args, 2) } char := EvalStageOrDefault(args[0], "|") @@ -30,32 +30,32 @@ func kfRepeat(args []KeyBuilderStage) KeyBuilderStage { return KeyBuilderStage(func(context KeyBuilderContext) string { count, err := strconv.Atoi(args[1](context)) if err != nil { - return ErrorType + return ErrorNum } return strings.Repeat(char, count) - }) + }), nil } // {bar {val} "maxVal" "len"} -func kfBar(args []KeyBuilderStage) KeyBuilderStage { +func kfBar(args []KeyBuilderStage) (KeyBuilderStage, error) { if len(args) != 3 { - return stageLiteral(ErrorArgCount) + return stageErrArgCount(args, 3) } maxVal, err := strconv.ParseInt(EvalStageOrDefault(args[1], ""), 10, 64) if err != nil { - return stageLiteral(ErrorType) + return stageError(ErrNum) } maxLen, err := strconv.ParseInt(EvalStageOrDefault(args[2], ""), 10, 64) if err != nil { - return stageLiteral(ErrorType) + return stageError(ErrNum) } return KeyBuilderStage(func(context KeyBuilderContext) string { val, err := strconv.ParseInt(args[0](context), 10, 64) if err != nil { - return ErrorType + return ErrorNum } return termunicode.BarString(val, maxVal, maxLen) - }) + }), nil } diff --git a/pkg/expressions/stdlib/drawing_test.go b/pkg/expressions/stdlib/drawing_test.go index d3deacd..3c74a79 100644 --- a/pkg/expressions/stdlib/drawing_test.go +++ b/pkg/expressions/stdlib/drawing_test.go @@ -7,15 +7,23 @@ import ( func TestRepeatCharacter(t *testing.T) { testExpression(t, mockContext("4"), - "{repeat a 2} {repeat b {0}} {repeat a} {repeat a a}", - "aa bbbb ") + "{repeat a 2} {repeat b {0}}", + "aa bbbb") + testExpressionErr(t, + mockContext("4"), + "{repeat a} {repeat a a}", + " ", + ErrArgCount) } func TestAddingColor(t *testing.T) { testExpression(t, mockContext("what what"), - "{color red {0}} {color a}", - "what what ") + "{color red {0}}", + "what what") + testExpressionErr(t, + mockContext("what waht"), + "{color a}", "", ErrArgCount) } func TestBarGraph(t *testing.T) { diff --git a/pkg/expressions/stdlib/errors.go b/pkg/expressions/stdlib/errors.go index 921f7ff..de5ddd6 100644 --- a/pkg/expressions/stdlib/errors.go +++ b/pkg/expressions/stdlib/errors.go @@ -1,7 +1,23 @@ package stdlib +import ( + "errors" + "fmt" + . "rare/pkg/expressions" //lint:ignore ST1001 Legacy +) + +type funcError struct { + expr string + err error +} + +func newFuncErr(expr, message string) funcError { + return funcError{expr, errors.New(message)} +} + +// Realtime errors const ( - ErrorType = "" // Error parsing the principle value of the input because of unexpected type + ErrorNum = "" // Error parsing the principle value of the input because of unexpected type (non-numeric) ErrorParsing = "" // Error parsing the principle value of the input (non-numeric) ErrorArgCount = "" // Function to not support a variation with the given argument count ErrorConst = "" // Expected constant value @@ -9,3 +25,36 @@ const ( ErrorArgName = "" // A variable accessed by a given name does not exist ErrorEmpty = "" // A value was expected, but was empty ) + +// Compilation errors +var ( + ErrNum = newFuncErr(ErrorNum, "invalid arg type, expected int") // always numeric + ErrParsing = newFuncErr(ErrorParsing, "unable to parse") // always non-numeric + ErrConst = newFuncErr(ErrorConst, "expected const") + ErrEnum = newFuncErr(ErrorEnum, "unable to find value in set") + ErrEmpty = newFuncErr(ErrorEmpty, "invalid empty value") +) + +var ( + ErrArgCount = errors.New("invalid number of arguments") +) + +func stageError(err funcError) (KeyBuilderStage, error) { + return func(ctx KeyBuilderContext) string { + return err.expr + }, err.err +} + +func stageErrArgCount(got []KeyBuilderStage, expected int) (KeyBuilderStage, error) { + return stageError(funcError{ + ErrorArgCount, + fmt.Errorf("%w: got %d, expected %d", ErrArgCount, len(got), expected), + }) +} + +func stageErrArgRange(got []KeyBuilderStage, text string) (KeyBuilderStage, error) { + return stageError(funcError{ + ErrorArgCount, + fmt.Errorf("%w: got %d, expected %s", ErrArgCount, len(got), text), + }) +} diff --git a/pkg/expressions/stdlib/funcsArithmatic.go b/pkg/expressions/stdlib/funcsArithmatic.go index 2d61aef..7783dd2 100644 --- a/pkg/expressions/stdlib/funcsArithmatic.go +++ b/pkg/expressions/stdlib/funcsArithmatic.go @@ -7,50 +7,50 @@ import ( // Simple helper that will take 2 or more integers, and apply an operation func arithmaticHelperi(equation func(int, int) int) KeyBuilderFunction { - return KeyBuilderFunction(func(args []KeyBuilderStage) KeyBuilderStage { + return KeyBuilderFunction(func(args []KeyBuilderStage) (KeyBuilderStage, error) { if len(args) < 2 { - return stageLiteral(ErrorArgCount) + return stageErrArgRange(args, "2+") } return KeyBuilderStage(func(context KeyBuilderContext) string { final, err := strconv.Atoi(args[0](context)) if err != nil { - return ErrorType + return ErrorNum } for i := 1; i < len(args); i++ { val, err := strconv.Atoi(args[i](context)) if err != nil { - return ErrorType + return ErrorNum } final = equation(final, val) } return strconv.Itoa(final) - }) + }), nil }) } // Simple helper that will take 2 or more integers, and apply an operation func arithmaticHelperf(equation func(float64, float64) float64) KeyBuilderFunction { - return KeyBuilderFunction(func(args []KeyBuilderStage) KeyBuilderStage { + return KeyBuilderFunction(func(args []KeyBuilderStage) (KeyBuilderStage, error) { if len(args) < 2 { - return stageLiteral(ErrorArgCount) + return stageErrArgRange(args, "2+") } return KeyBuilderStage(func(context KeyBuilderContext) string { final, err := strconv.ParseFloat(args[0](context), 64) if err != nil { - return ErrorType + return ErrorNum } for i := 1; i < len(args); i++ { val, err := strconv.ParseFloat(args[i](context), 64) if err != nil { - return ErrorType + return ErrorNum } final = equation(final, val) } return strconv.FormatFloat(final, 'f', -1, 64) - }) + }), nil }) } diff --git a/pkg/expressions/stdlib/funcsCommon.go b/pkg/expressions/stdlib/funcsCommon.go index d8516e3..3e28612 100644 --- a/pkg/expressions/stdlib/funcsCommon.go +++ b/pkg/expressions/stdlib/funcsCommon.go @@ -7,7 +7,7 @@ import ( . "rare/pkg/expressions" //lint:ignore ST1001 Legacy ) -func kfCoalesce(args []KeyBuilderStage) KeyBuilderStage { +func kfCoalesce(args []KeyBuilderStage) (KeyBuilderStage, error) { return KeyBuilderStage(func(context KeyBuilderContext) string { for _, arg := range args { val := arg(context) @@ -16,46 +16,46 @@ func kfCoalesce(args []KeyBuilderStage) KeyBuilderStage { } } return "" - }) + }), nil } -func kfBucket(args []KeyBuilderStage) KeyBuilderStage { +func kfBucket(args []KeyBuilderStage) (KeyBuilderStage, error) { if len(args) != 2 { - return stageLiteral(ErrorArgCount) + return stageErrArgCount(args, 2) } bucketSize, err := strconv.Atoi(EvalStageOrDefault(args[1], "")) if err != nil { - return stageLiteral(ErrorType) + return stageError(ErrNum) } return KeyBuilderStage(func(context KeyBuilderContext) string { val, err := strconv.Atoi(args[0](context)) if err != nil { - return ErrorType + return ErrorNum } return strconv.Itoa((val / bucketSize) * bucketSize) - }) + }), nil } -func kfClamp(args []KeyBuilderStage) KeyBuilderStage { +func kfClamp(args []KeyBuilderStage) (KeyBuilderStage, error) { if len(args) != 3 { - return stageLiteral(ErrorArgCount) + return stageErrArgCount(args, 3) } min, minErr := strconv.Atoi(EvalStageOrDefault(args[1], "")) max, maxErr := strconv.Atoi(EvalStageOrDefault(args[2], "")) if minErr != nil || maxErr != nil { - return stageLiteral(ErrorType) + return stageError(ErrNum) } return KeyBuilderStage(func(context KeyBuilderContext) string { arg0 := args[0](context) val, err := strconv.Atoi(arg0) if err != nil { - return ErrorType + return ErrorNum } if val < min { @@ -65,20 +65,20 @@ func kfClamp(args []KeyBuilderStage) KeyBuilderStage { } else { return arg0 } - }) + }), nil } -func kfExpBucket(args []KeyBuilderStage) KeyBuilderStage { +func kfExpBucket(args []KeyBuilderStage) (KeyBuilderStage, error) { if len(args) != 1 { - return stageLiteral(ErrorArgCount) + return stageErrArgCount(args, 1) } return KeyBuilderStage(func(context KeyBuilderContext) string { val, err := strconv.Atoi(args[0](context)) if err != nil { - return ErrorType + return ErrorNum } logVal := int(math.Log10(float64(val))) return strconv.Itoa(int(math.Pow10(logVal))) - }) + }), nil } diff --git a/pkg/expressions/stdlib/funcsComparators.go b/pkg/expressions/stdlib/funcsComparators.go index 42fe2fb..51ac2cd 100644 --- a/pkg/expressions/stdlib/funcsComparators.go +++ b/pkg/expressions/stdlib/funcsComparators.go @@ -8,9 +8,9 @@ import ( ) func stringComparator(equation func(string, string) string) KeyBuilderFunction { - return KeyBuilderFunction(func(args []KeyBuilderStage) KeyBuilderStage { + return KeyBuilderFunction(func(args []KeyBuilderStage) (KeyBuilderStage, error) { if len(args) < 2 { - return stageLiteral(ErrorArgCount) + return stageErrArgRange(args, "2+") } return KeyBuilderStage(func(context KeyBuilderContext) string { val := args[0](context) @@ -19,48 +19,48 @@ func stringComparator(equation func(string, string) string) KeyBuilderFunction { } return val - }) + }), nil }) } // Checks equality, and returns truthy if equals, and empty if not func arithmaticEqualityHelper(test func(float64, float64) bool) KeyBuilderFunction { - return KeyBuilderFunction(func(args []KeyBuilderStage) KeyBuilderStage { + return KeyBuilderFunction(func(args []KeyBuilderStage) (KeyBuilderStage, error) { if len(args) != 2 { - return stageLiteral(ErrorArgCount) + return stageErrArgCount(args, 2) } return KeyBuilderStage(func(context KeyBuilderContext) string { left, err := strconv.ParseFloat(args[0](context), 64) if err != nil { - return ErrorType + return ErrorNum } right, err := strconv.ParseFloat(args[1](context), 64) if err != nil { - return ErrorType + return ErrorNum } if test(left, right) { return TruthyVal } return FalsyVal - }) + }), nil }) } -func kfNot(args []KeyBuilderStage) KeyBuilderStage { +func kfNot(args []KeyBuilderStage) (KeyBuilderStage, error) { if len(args) != 1 { - return stageLiteral(ErrorArgCount) + return stageErrArgCount(args, 1) } return KeyBuilderStage(func(context KeyBuilderContext) string { if Truthy(args[0](context)) { return FalsyVal } return TruthyVal - }) + }), nil } // {and a b c ...} -func kfAnd(args []KeyBuilderStage) KeyBuilderStage { +func kfAnd(args []KeyBuilderStage) (KeyBuilderStage, error) { return KeyBuilderStage(func(context KeyBuilderContext) string { for _, arg := range args { if arg(context) == FalsyVal { @@ -68,11 +68,11 @@ func kfAnd(args []KeyBuilderStage) KeyBuilderStage { } } return TruthyVal - }) + }), nil } // {or a b c ...} -func kfOr(args []KeyBuilderStage) KeyBuilderStage { +func kfOr(args []KeyBuilderStage) (KeyBuilderStage, error) { return KeyBuilderStage(func(context KeyBuilderContext) string { for _, arg := range args { if arg(context) != FalsyVal { @@ -80,13 +80,13 @@ func kfOr(args []KeyBuilderStage) KeyBuilderStage { } } return FalsyVal - }) + }), nil } // {like string contains} -func kfLike(args []KeyBuilderStage) KeyBuilderStage { +func kfLike(args []KeyBuilderStage) (KeyBuilderStage, error) { if len(args) != 2 { - return stageLiteral(ErrorArgCount) + return stageErrArgCount(args, 2) } return KeyBuilderStage(func(context KeyBuilderContext) string { val := args[0](context) @@ -96,13 +96,13 @@ func kfLike(args []KeyBuilderStage) KeyBuilderStage { return val } return FalsyVal - }) + }), nil } // {if truthy val elseVal} -func kfIf(args []KeyBuilderStage) KeyBuilderStage { +func kfIf(args []KeyBuilderStage) (KeyBuilderStage, error) { if len(args) < 2 || len(args) > 3 { - return stageLiteral(ErrorArgCount) + return stageErrArgRange(args, "2-3") } return KeyBuilderStage(func(context KeyBuilderContext) string { ifVal := args[0](context) @@ -112,12 +112,12 @@ func kfIf(args []KeyBuilderStage) KeyBuilderStage { return args[2](context) } return FalsyVal - }) + }), nil } -func kfUnless(args []KeyBuilderStage) KeyBuilderStage { +func kfUnless(args []KeyBuilderStage) (KeyBuilderStage, error) { if len(args) != 2 { - return stageLiteral(ErrorArgCount) + return stageErrArgCount(args, 2) } return func(context KeyBuilderContext) string { ifVal := args[0](context) @@ -125,5 +125,5 @@ func kfUnless(args []KeyBuilderStage) KeyBuilderStage { return args[1](context) } return "" - } + }, nil } diff --git a/pkg/expressions/stdlib/funcsCsv.go b/pkg/expressions/stdlib/funcsCsv.go index 613ee8e..7eb72ec 100644 --- a/pkg/expressions/stdlib/funcsCsv.go +++ b/pkg/expressions/stdlib/funcsCsv.go @@ -15,7 +15,7 @@ func csvItemEncode(s string) string { return s } -func kfCsv(args []KeyBuilderStage) KeyBuilderStage { +func kfCsv(args []KeyBuilderStage) (KeyBuilderStage, error) { if len(args) == 0 { return stageLiteral("") } @@ -28,5 +28,5 @@ func kfCsv(args []KeyBuilderStage) KeyBuilderStage { } } return sb.String() - }) + }), nil } diff --git a/pkg/expressions/stdlib/funcsJson.go b/pkg/expressions/stdlib/funcsJson.go index dc9d782..90fe914 100644 --- a/pkg/expressions/stdlib/funcsJson.go +++ b/pkg/expressions/stdlib/funcsJson.go @@ -6,22 +6,22 @@ import ( . "rare/pkg/expressions" //lint:ignore ST1001 Legacy ) -func kfJsonQuery(args []KeyBuilderStage) KeyBuilderStage { +func kfJsonQuery(args []KeyBuilderStage) (KeyBuilderStage, error) { if len(args) == 1 { // Assumes "{0}" is the json blob to extract, so arg[0] is the key return KeyBuilderStage(func(context KeyBuilderContext) string { json := context.GetMatch(0) expression := args[0](context) return gjson.Get(json, expression).String() - }) + }), nil } else if len(args) == 2 { // Json is arg[0], key is arg[1] return KeyBuilderStage(func(context KeyBuilderContext) string { json := args[0](context) expression := args[1](context) return gjson.Get(json, expression).String() - }) + }), nil } else { - return stageLiteral(ErrorArgCount) + return stageErrArgRange(args, "1-2") } } diff --git a/pkg/expressions/stdlib/funcsPath.go b/pkg/expressions/stdlib/funcsPath.go index 091322a..856b4c3 100644 --- a/pkg/expressions/stdlib/funcsPath.go +++ b/pkg/expressions/stdlib/funcsPath.go @@ -5,14 +5,14 @@ import ( . "rare/pkg/expressions" //lint:ignore ST1001 Legacy ) -func kfPathManip(manipulator func(string) string) func([]KeyBuilderStage) KeyBuilderStage { - return func(args []KeyBuilderStage) KeyBuilderStage { +func kfPathManip(manipulator func(string) string) KeyBuilderFunction { + return func(args []KeyBuilderStage) (KeyBuilderStage, error) { if len(args) != 1 { - return stageLiteral(ErrorArgCount) + return stageErrArgCount(args, 1) } return KeyBuilderStage(func(context KeyBuilderContext) string { return manipulator(args[0](context)) - }) + }), nil } } diff --git a/pkg/expressions/stdlib/funcsStrings.go b/pkg/expressions/stdlib/funcsStrings.go index c5071bc..b9e170a 100644 --- a/pkg/expressions/stdlib/funcsStrings.go +++ b/pkg/expressions/stdlib/funcsStrings.go @@ -10,9 +10,9 @@ import ( ) // {prefix string prefix} -func kfPrefix(args []KeyBuilderStage) KeyBuilderStage { +func kfPrefix(args []KeyBuilderStage) (KeyBuilderStage, error) { if len(args) != 2 { - return stageLiteral(ErrorArgCount) + return stageErrArgCount(args, 2) } return KeyBuilderStage(func(context KeyBuilderContext) string { val := args[0](context) @@ -22,13 +22,13 @@ func kfPrefix(args []KeyBuilderStage) KeyBuilderStage { return val } return "" - }) + }), nil } // {suffix string suffix} -func kfSuffix(args []KeyBuilderStage) KeyBuilderStage { +func kfSuffix(args []KeyBuilderStage) (KeyBuilderStage, error) { if len(args) != 2 { - return stageLiteral(ErrorArgCount) + return stageErrArgCount(args, 2) } return KeyBuilderStage(func(context KeyBuilderContext) string { val := args[0](context) @@ -38,31 +38,31 @@ func kfSuffix(args []KeyBuilderStage) KeyBuilderStage { return val } return "" - }) + }), nil } -func kfUpper(args []KeyBuilderStage) KeyBuilderStage { +func kfUpper(args []KeyBuilderStage) (KeyBuilderStage, error) { if len(args) != 1 { - return stageLiteral(ErrorArgCount) + return stageErrArgCount(args, 1) } return func(context KeyBuilderContext) string { return strings.ToUpper(args[0](context)) - } + }, nil } -func kfLower(args []KeyBuilderStage) KeyBuilderStage { +func kfLower(args []KeyBuilderStage) (KeyBuilderStage, error) { if len(args) != 1 { - return stageLiteral(ErrorArgCount) + return stageErrArgCount(args, 1) } return func(context KeyBuilderContext) string { return strings.ToLower(args[0](context)) - } + }, nil } // {substr {0} } -func kfSubstr(args []KeyBuilderStage) KeyBuilderStage { +func kfSubstr(args []KeyBuilderStage) (KeyBuilderStage, error) { if len(args) != 3 { - return stageLiteral(ErrorArgCount) + return stageErrArgCount(args, 3) } return KeyBuilderStage(func(context KeyBuilderContext) string { @@ -75,7 +75,7 @@ func kfSubstr(args []KeyBuilderStage) KeyBuilderStage { left, err1 := strconv.Atoi(args[1](context)) length, err2 := strconv.Atoi(args[2](context)) if err1 != nil || err2 != nil { - return ErrorType + return ErrorNum } if length < 0 { @@ -96,24 +96,24 @@ func kfSubstr(args []KeyBuilderStage) KeyBuilderStage { right = lenS } return s[left:right] - }) + }), nil } // {select {0} 1} -func kfSelect(args []KeyBuilderStage) KeyBuilderStage { +func kfSelect(args []KeyBuilderStage) (KeyBuilderStage, error) { if len(args) != 2 { - return stageLiteral(ErrorArgCount) + return stageErrArgCount(args, 2) } return KeyBuilderStage(func(context KeyBuilderContext) string { s := args[0](context) idx, err := strconv.Atoi(args[1](context)) if err != nil { - return ErrorType + return ErrorNum } return selectField(s, idx) - }) + }), nil } func selectField(s string, idx int) string { @@ -147,9 +147,9 @@ func selectField(s string, idx int) string { // {format str args...} // just like fmt.Sprintf -func kfFormat(args []KeyBuilderStage) KeyBuilderStage { +func kfFormat(args []KeyBuilderStage) (KeyBuilderStage, error) { if len(args) < 1 { - return stageLiteral(ErrorArgCount) + return stageErrArgRange(args, "1+") } return KeyBuilderStage(func(context KeyBuilderContext) string { format := args[0](context) @@ -160,61 +160,61 @@ func kfFormat(args []KeyBuilderStage) KeyBuilderStage { } return fmt.Sprintf(format, printArgs...) - }) + }), nil } -func kfHumanizeInt(args []KeyBuilderStage) KeyBuilderStage { +func kfHumanizeInt(args []KeyBuilderStage) (KeyBuilderStage, error) { if len(args) != 1 { - return stageLiteral(ErrorArgCount) + return stageErrArgCount(args, 1) } return KeyBuilderStage(func(context KeyBuilderContext) string { val, err := strconv.Atoi(args[0](context)) if err != nil { - return ErrorType + return ErrorNum } return humanize.Hi32(val) - }) + }), nil } -func kfHumanizeFloat(args []KeyBuilderStage) KeyBuilderStage { +func kfHumanizeFloat(args []KeyBuilderStage) (KeyBuilderStage, error) { if len(args) != 1 { - return stageLiteral(ErrorArgCount) + return stageErrArgCount(args, 1) } return KeyBuilderStage(func(context KeyBuilderContext) string { val, err := strconv.ParseFloat(args[0](context), 64) if err != nil { - return ErrorType + return ErrorNum } return humanize.Hf(val) - }) + }), nil } -func kfBytesize(args []KeyBuilderStage) KeyBuilderStage { +func kfBytesize(args []KeyBuilderStage) (KeyBuilderStage, error) { if len(args) < 1 { - return stageLiteral(ErrorArgCount) + return stageErrArgRange(args, "1+") } precision, err := strconv.Atoi(EvalStageIndexOrDefault(args, 1, "0")) if err != nil { - return stageLiteral(ErrorType) + return stageError(ErrNum) } return KeyBuilderStage(func(context KeyBuilderContext) string { val, err := strconv.ParseUint(args[0](context), 10, 64) if err != nil { - return ErrorType + return ErrorNum } return humanize.AlwaysByteSize(val, precision) - }) + }), nil } func kfJoin(delim rune) KeyBuilderFunction { - return func(args []KeyBuilderStage) KeyBuilderStage { + return func(args []KeyBuilderStage) (KeyBuilderStage, error) { if len(args) == 0 { return stageLiteral("") } if len(args) == 1 { - return args[0] + return args[0], nil } return KeyBuilderStage(func(context KeyBuilderContext) string { var sb strings.Builder @@ -224,6 +224,6 @@ func kfJoin(delim rune) KeyBuilderFunction { sb.WriteString(arg(context)) } return sb.String() - }) + }), nil } } diff --git a/pkg/expressions/stdlib/funcsStrings_test.go b/pkg/expressions/stdlib/funcsStrings_test.go index 0ac154e..3b68877 100644 --- a/pkg/expressions/stdlib/funcsStrings_test.go +++ b/pkg/expressions/stdlib/funcsStrings_test.go @@ -1,6 +1,7 @@ package stdlib import ( + "rare/pkg/expressions" "strings" "testing" @@ -34,6 +35,12 @@ func TestSelect(t *testing.T) { testExpression(t, mockContext(), `{select "ab cd ef" 1}`, "cd") } +func TestJoinEmpty(t *testing.T) { + stage, err := kfJoin('-')([]expressions.KeyBuilderStage{}) + assert.NoError(t, err) + assert.Equal(t, "", stage(mockContext())) +} + func TestSelectField(t *testing.T) { var s = "this is\ta\ntest\x00really" assert.Equal(t, "this", selectField(s, 0)) diff --git a/pkg/expressions/stdlib/funcsTime.go b/pkg/expressions/stdlib/funcsTime.go index ad15ad9..bc3eb75 100644 --- a/pkg/expressions/stdlib/funcsTime.go +++ b/pkg/expressions/stdlib/funcsTime.go @@ -52,7 +52,7 @@ func namedTimeFormatToFormat(f string) string { } // smartDateParseWrapper wraps different types of date parsing and manipulation into a stage -func smartDateParseWrapper(format string, tz *time.Location, dateStage KeyBuilderStage, f func(time time.Time) string) KeyBuilderStage { +func smartDateParseWrapper(format string, tz *time.Location, dateStage KeyBuilderStage, f func(time time.Time) string) (KeyBuilderStage, error) { switch strings.ToLower(format) { case "auto": // Auto will attempt to parse every time return KeyBuilderStage(func(context KeyBuilderContext) string { @@ -62,7 +62,7 @@ func smartDateParseWrapper(format string, tz *time.Location, dateStage KeyBuilde return ErrorParsing } return f(val) - }) + }), nil case "", "cache": // Empty format will auto-detect on first successful entry var atomicFormat atomic.Value @@ -91,7 +91,7 @@ func smartDateParseWrapper(format string, tz *time.Location, dateStage KeyBuilde return ErrorParsing } return f(val) - }) + }), nil default: // non-empty; Set format will resolve to a go date parseFormat := namedTimeFormatToFormat(format) @@ -102,16 +102,16 @@ func smartDateParseWrapper(format string, tz *time.Location, dateStage KeyBuilde return ErrorParsing } return f(val) - }) + }), nil } } // Parse time into standard unix epoch time (easier to use) // By default, will attempt to auto-detect and cache format // {func