From 8b921aaa824d510d0ae06c79df7aa96345f1cb29 Mon Sep 17 00:00:00 2001 From: 42Atomys Date: Thu, 9 May 2024 19:08:19 +0200 Subject: [PATCH] =?UTF-8?q?refactor:=20complete=20conversion=20category=20?= =?UTF-8?q?migration=20=F0=9F=8C=B1=E2=9C=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- alias.go | 5 +- benchmarks/compare.tmpl | 3 +- conversion_functions.go | 149 +++++++++++++++++++++++++++++++++++ conversion_functions_test.go | 92 +++++++++++++++++++++ maps_functions.go | 2 +- migrated_functions.go | 47 ----------- slices_functions.go | 8 +- sprout.go | 10 +-- to_migrate_test.go | 141 --------------------------------- 9 files changed, 257 insertions(+), 200 deletions(-) create mode 100644 conversion_functions.go create mode 100644 conversion_functions_test.go diff --git a/alias.go b/alias.go index 853c15d..3de8ac4 100644 --- a/alias.go +++ b/alias.go @@ -13,7 +13,6 @@ var bc_registerSprigFuncs = FunctionAliasMap{ "ellipsis": {"abbrev"}, //! Deprecated: Should use ellipsis instead "ellipsisBoth": {"abbrevboth"}, //! Deprecated: Should use ellipsisBoth instead "trimAll": {"trimall"}, //! Deprecated: Should use trimAll instead - "int": {"atoi"}, //! Deprecated: Should use toInt instead "append": {"push"}, //! Deprecated: Should use append instead "mustAppend": {"mustPush"}, //! Deprecated: Should use mustAppend instead "list": {"tuple"}, // FIXME: with the addition of append/prepend these are no longer immutable. @@ -40,6 +39,10 @@ var bc_registerSprigFuncs = FunctionAliasMap{ "expandEnv": {"expandenv"}, //! Deprecated: Should use expandEnv instead "dateAgo": {"ago"}, //! Deprecated: Should use dateAgo instead "strSlice": {"toStrings"}, //! Deprecated: Should use strSlice instead + "toInt": {"int", "atoi"}, //! Deprecated: Should use toInt instead + "toInt64": {"int64"}, //! Deprecated: Should use toInt64 instead + "toFloat64": {"float64"}, //! Deprecated: Should use toFloat64 instead + "toOctal": {"toDecimal"}, //! Deprecated: Should use toOctal instead } //\ BACKWARDS COMPATIBILITY diff --git a/benchmarks/compare.tmpl b/benchmarks/compare.tmpl index ba68b36..b5f8988 100644 --- a/benchmarks/compare.tmpl +++ b/benchmarks/compare.tmpl @@ -179,7 +179,8 @@ Without: {{without (list 1 2 3) 2}} | {{without (list 1 2 3) 2 3}} HasKey: {{hasKey $dict "key"}} Slice: {{slice (list 1 2 3) 1 2}} | {{slice (list 1 2 3) 1}} Concat: {{concat (list 1 2) (list 3 4)}} -Dig: {{dict "a" 1 | dig "a" ""}} +# Incompatible with sprig +# Dig: {{/* dig "b" "a" (dict "a" 1 "b" (dict "a" 2)) */}} Chunk: {{list 1 2 3 4 5 | chunk 2}} {{/* Crypt Functions */}} diff --git a/conversion_functions.go b/conversion_functions.go new file mode 100644 index 0000000..7f3195a --- /dev/null +++ b/conversion_functions.go @@ -0,0 +1,149 @@ +package sprout + +import ( + "fmt" + "strconv" + "time" + + "github.com/spf13/cast" +) + +// ToInt converts a value to an int using robust type casting. +// +// Parameters: +// +// v any - the value to convert to an int. +// +// Returns: +// +// int - the integer representation of the value. +// +// Example: +// +// {{ "123" | toInt }} // Output: 123 +func (fh *FunctionHandler) ToInt(v any) int { + return cast.ToInt(v) +} + +// ToInt64 converts a value to an int64, accommodating larger integer values. +// +// Parameters: +// +// v any - the value to convert to an int64. +// +// Returns: +// +// int64 - the int64 representation of the value. +// +// Example: +// +// {{ "123456789012" | toInt64 }} // Output: 123456789012 +func (fh *FunctionHandler) ToInt64(v any) int64 { + return cast.ToInt64(v) +} + +// ToFloat64 converts a value to a float64. +// +// Parameters: +// +// v any - the value to convert to a float64. +// +// Returns: +// +// float64 - the float64 representation of the value. +// +// Example: +// +// {{ "123.456" | toFloat64 }} // Output: 123.456 +func (fh *FunctionHandler) ToFloat64(v any) float64 { + return cast.ToFloat64(v) +} + +// ToOctal parses a string value as an octal (base 8) integer. +// +// Parameters: +// +// v any - the string representing an octal number. +// +// Returns: +// +// int64 - the decimal (base 10) representation of the octal value. +// If parsing fails, returns 0. +// +// Example: +// +// {{ "123" | toOctal }} // Output: 83 (since "123" in octal is 83 in decimal) +func (fh *FunctionHandler) ToOctal(v any) int64 { + result, err := strconv.ParseInt(fmt.Sprint(v), 8, 64) + if err != nil { + return 0 + } + return result +} + +// ToString converts a value to a string, handling various types effectively. +// +// Parameters: +// +// v any - the value to convert to a string. +// +// Returns: +// +// string - the string representation of the value. +// +// Example: +// +// {{ 123 | toString }} // Output: "123" +func (fh *FunctionHandler) ToString(v any) string { + switch v := v.(type) { + case string: + return v + case []byte: + return string(v) + case error: + return v.Error() + case fmt.Stringer: + return v.String() + default: + return fmt.Sprintf("%v", v) + } +} + +// ToDate converts a string to a time.Time object based on a format specification. +// +// Parameters: +// +// fmt string - the date format string. +// str string - the date string to parse. +// +// Returns: +// +// time.Time - the parsed date. +// +// Example: +// +// {{ "2006-01-02", "2023-05-04" | toDate }} // Output: 2023-05-04 00:00:00 +0000 UTC +func (fh *FunctionHandler) ToDate(fmt, str string) time.Time { + result, _ := fh.MustToDate(fmt, str) + return result +} + +// MustToDate tries to parse a string into a time.Time object based on a format, +// returning an error if parsing fails. +// +// Parameters: +// +// fmt string - the date format string. +// str string - the date string to parse. +// +// Returns: +// +// time.Time - the parsed date. +// error - error if the date string does not conform to the format. +// +// Example: +// +// {{ "2006-01-02", "2023-05-04" | mustToDate }} // Output: 2023-05-04 00:00:00 +0000 UTC, nil +func (fh *FunctionHandler) MustToDate(fmt, str string) (time.Time, error) { + return time.ParseInLocation(fmt, str, time.Local) +} diff --git a/conversion_functions_test.go b/conversion_functions_test.go new file mode 100644 index 0000000..6069de8 --- /dev/null +++ b/conversion_functions_test.go @@ -0,0 +1,92 @@ +package sprout + +import ( + "fmt" + "testing" +) + +func TestToInt(t *testing.T) { + var tests = testCases{ + {"TestInt", `{{$v := toInt .V }}{{kindOf $v}}-{{$v}}`, "int-1", map[string]any{"V": 1}}, + {"TestInt32", `{{$v := toInt .V }}{{kindOf $v}}-{{$v}}`, "int-1", map[string]any{"V": int32(1)}}, + {"TestFloat64", `{{$v := toInt .V }}{{kindOf $v}}-{{$v}}`, "int-1", map[string]any{"V": float64(1.42)}}, + {"TestBool", `{{$v := toInt .V }}{{kindOf $v}}-{{$v}}`, "int-1", map[string]any{"V": true}}, + {"TestString", `{{$v := toInt .V }}{{kindOf $v}}-{{$v}}`, "int-1", map[string]any{"V": "1"}}, + } + + runTestCases(t, tests) +} + +func TestToInt64(t *testing.T) { + var tests = testCases{ + {"TestInt", `{{$v := toInt64 .V }}{{typeOf $v}}-{{$v}}`, "int64-1", map[string]any{"V": 1}}, + {"TestInt32", `{{$v := toInt64 .V }}{{typeOf $v}}-{{$v}}`, "int64-1", map[string]any{"V": int32(1)}}, + {"TestFloat64", `{{$v := toInt64 .V }}{{typeOf $v}}-{{$v}}`, "int64-1", map[string]any{"V": float64(1.42)}}, + {"TestBool", `{{$v := toInt64 .V }}{{typeOf $v}}-{{$v}}`, "int64-1", map[string]any{"V": true}}, + {"TestString", `{{$v := toInt64 .V }}{{typeOf $v}}-{{$v}}`, "int64-1", map[string]any{"V": "1"}}, + } + + runTestCases(t, tests) +} + +func TestToFloat64(t *testing.T) { + var tests = testCases{ + {"TestInt", `{{$v := toFloat64 .V }}{{typeOf $v}}-{{$v}}`, "float64-1", map[string]any{"V": 1}}, + {"TestInt32", `{{$v := toFloat64 .V }}{{typeOf $v}}-{{$v}}`, "float64-1", map[string]any{"V": int32(1)}}, + {"TestFloat64", `{{$v := toFloat64 .V }}{{typeOf $v}}-{{$v}}`, "float64-1.42", map[string]any{"V": float64(1.42)}}, + {"TestBool", `{{$v := toFloat64 .V }}{{typeOf $v}}-{{$v}}`, "float64-1", map[string]any{"V": true}}, + {"TestString", `{{$v := toFloat64 .V }}{{typeOf $v}}-{{$v}}`, "float64-1", map[string]any{"V": "1"}}, + } + + runTestCases(t, tests) +} + +func TestToDecimal(t *testing.T) { + var tests = testCases{ + {"TestInt", `{{$v := toDecimal .V }}{{typeOf $v}}-{{$v}}`, "int64-511", map[string]any{"V": 777}}, + {"TestInt32", `{{$v := toDecimal .V }}{{typeOf $v}}-{{$v}}`, "int64-504", map[string]any{"V": int32(770)}}, + {"TestString", `{{$v := toDecimal .V }}{{typeOf $v}}-{{$v}}`, "int64-1", map[string]any{"V": "1"}}, + } + + runTestCases(t, tests) +} + +type testStringer struct{} + +func (s testStringer) String() string { + return "stringer" +} + +func TestToString(t *testing.T) { + + var tests = testCases{ + {"TestInt", `{{$v := toString .V }}{{typeOf $v}}-{{$v}}`, "string-1", map[string]any{"V": 1}}, + {"TestInt32", `{{$v := toString .V }}{{typeOf $v}}-{{$v}}`, "string-1", map[string]any{"V": int32(1)}}, + {"TestFloat64", `{{$v := toString .V }}{{typeOf $v}}-{{$v}}`, "string-1.42", map[string]any{"V": float64(1.42)}}, + {"TestBool", `{{$v := toString .V }}{{typeOf $v}}-{{$v}}`, "string-true", map[string]any{"V": true}}, + {"TestString", `{{$v := toString .V }}{{typeOf $v}}-{{$v}}`, "string-1", map[string]any{"V": "1"}}, + {"TestError", `{{$v := toString .V }}{{typeOf $v}}-{{$v}}`, "string-error", map[string]any{"V": fmt.Errorf("error")}}, + {"TestStringer", `{{$v := toString .V }}{{typeOf $v}}-{{$v}}`, "string-stringer", map[string]any{"V": testStringer{}}}, + {"TestSliceOfBytes", `{{$v := toString .V }}{{typeOf $v}}-{{$v}}`, "string-abc", map[string]any{"V": []byte("abc")}}, + } + + runTestCases(t, tests) +} + +func TestToDate(t *testing.T) { + var tests = testCases{ + {"TestDate", `{{$v := toDate "2006-01-02" .V }}{{typeOf $v}}-{{$v}}`, "time.Time-2024-05-09 00:00:00 +0000 UTC", map[string]any{"V": "2024-05-09"}}, + } + + runTestCases(t, tests) +} + +func TestMustToDate(t *testing.T) { + var tests = mustTestCases{ + {testCase{"TestDate", `{{$v := mustToDate "2006-01-02" .V }}{{typeOf $v}}-{{$v}}`, "time.Time-2024-05-09 00:00:00 +0000 UTC", map[string]any{"V": "2024-05-09"}}, ""}, + {testCase{"TestInvalidValue", `{{$v := mustToDate "2006-01-02" .V }}{{typeOf $v}}-{{$v}}`, "", map[string]any{"V": ""}}, "cannot parse \"\" as \"2006\""}, + {testCase{"TestInvalidLayout", `{{$v := mustToDate "invalid" .V }}{{typeOf $v}}-{{$v}}`, "", map[string]any{"V": "2024-05-09"}}, "cannot parse \"2024-05-09\" as \"invalid\""}, + } + + runMustTestCases(t, tests) +} diff --git a/maps_functions.go b/maps_functions.go index 3c1b90e..fb6613a 100644 --- a/maps_functions.go +++ b/maps_functions.go @@ -30,7 +30,7 @@ func (fh *FunctionHandler) Dict(values ...any) map[string]any { dict := make(map[string]any, len(values)/2) for i := 0; i < len(values); i += 2 { - dict[fh.Strval(values[i])] = values[i+1] + dict[fh.ToString(values[i])] = values[i+1] } return dict diff --git a/migrated_functions.go b/migrated_functions.go index 41bd42e..ef7bbbd 100644 --- a/migrated_functions.go +++ b/migrated_functions.go @@ -30,33 +30,15 @@ import ( "net" "net/url" "reflect" - "strconv" "strings" "time" sv2 "github.com/Masterminds/semver/v3" "github.com/shopspring/decimal" - "github.com/spf13/cast" bcrypt_lib "golang.org/x/crypto/bcrypt" "golang.org/x/crypto/scrypt" ) -func (fh *FunctionHandler) Strval(v any) string { - switch v := v.(type) { - case string: - return v - case []byte: - return string(v) - case error: - return v.Error() - case fmt.Stringer: - return v.String() - default: - // Handles any other types by leveraging fmt.Sprintf for a string representation. - return fmt.Sprintf("%v", v) - } -} - func (fh *FunctionHandler) FillMapWithParts(parts []string) map[string]string { res := make(map[string]string, len(parts)) for i, v := range parts { @@ -122,26 +104,6 @@ func (fh *FunctionHandler) UrlJoin(d map[string]any) string { return resURL.String() } -func (fh *FunctionHandler) ToFloat64(v any) float64 { - return cast.ToFloat64(v) -} - -func (fh *FunctionHandler) ToInt(v any) int { - return cast.ToInt(v) -} - -func (fh *FunctionHandler) ToInt64(v any) int64 { - return cast.ToInt64(v) -} - -func (fh *FunctionHandler) ToDecimal(v any) int64 { - result, err := strconv.ParseInt(fmt.Sprint(v), 8, 64) - if err != nil { - return 0 - } - return result -} - func (fh *FunctionHandler) IntArrayToString(slice []int, delimeter string) string { return strings.Trim(strings.Join(strings.Fields(fmt.Sprint(slice)), delimeter), "[]") } @@ -171,15 +133,6 @@ func (fh *FunctionHandler) InList(haystack []any, needle any) bool { return false } -func (fh *FunctionHandler) ToDate(fmt, str string) time.Time { - t, _ := time.ParseInLocation(fmt, str, time.Local) - return t -} - -func (fh *FunctionHandler) MustToDate(fmt, str string) (time.Time, error) { - return time.ParseInLocation(fmt, str, time.Local) -} - func (fh *FunctionHandler) SemverCompare(constraint, version string) (bool, error) { c, err := sv2.NewConstraint(constraint) if err != nil { diff --git a/slices_functions.go b/slices_functions.go index 0c5b4b9..487e48b 100644 --- a/slices_functions.go +++ b/slices_functions.go @@ -383,7 +383,7 @@ func (fh *FunctionHandler) SortAlpha(list any) []string { sortedList.Sort() return sortedList } - return []string{fh.Strval(list)} + return []string{fh.ToString(list)} } // SplitList divides a string into a slice of substrings separated by the @@ -422,7 +422,7 @@ func (fh *FunctionHandler) StrSlice(value any) []string { var result []string for _, s := range interfaces { if s != nil { - result = append(result, fh.Strval(s)) + result = append(result, fh.ToString(s)) } } return result @@ -435,14 +435,14 @@ func (fh *FunctionHandler) StrSlice(value any) []string { for i := 0; i < reflectedValue.Len(); i++ { value := reflectedValue.Index(i).Interface() if value != nil { - result = append(result, fh.Strval(value)) + result = append(result, fh.ToString(value)) } } return result } // If it's not a slice, array, or nil, return a slice with the string representation of v. - return []string{fh.Strval(value)} + return []string{fh.ToString(value)} } // MustAppend appends an element to a slice or array, returning an error if the diff --git a/sprout.go b/sprout.go index 57f3e92..432e280 100644 --- a/sprout.go +++ b/sprout.go @@ -154,12 +154,12 @@ func FuncMap(opts ...FunctionHandlerOption) template.FuncMap { fnHandler.funcMap["sha1sum"] = fnHandler.Sha1sum fnHandler.funcMap["sha256sum"] = fnHandler.Sha256sum fnHandler.funcMap["adler32sum"] = fnHandler.Adler32sum - fnHandler.funcMap["toString"] = fnHandler.Strval - fnHandler.funcMap["int64"] = fnHandler.ToInt64 - fnHandler.funcMap["int"] = fnHandler.ToInt - fnHandler.funcMap["float64"] = fnHandler.ToFloat64 + fnHandler.funcMap["toString"] = fnHandler.ToString + fnHandler.funcMap["toInt64"] = fnHandler.ToInt64 + fnHandler.funcMap["toInt"] = fnHandler.ToInt + fnHandler.funcMap["toFloat64"] = fnHandler.ToFloat64 fnHandler.funcMap["seq"] = fnHandler.Seq - fnHandler.funcMap["toDecimal"] = fnHandler.ToDecimal + fnHandler.funcMap["toOctal"] = fnHandler.ToOctal fnHandler.funcMap["split"] = fnHandler.Split fnHandler.funcMap["splitList"] = fnHandler.SplitList fnHandler.funcMap["splitn"] = fnHandler.Splitn diff --git a/to_migrate_test.go b/to_migrate_test.go index e56e590..e2259e3 100644 --- a/to_migrate_test.go +++ b/to_migrate_test.go @@ -97,11 +97,6 @@ func TestUrlJoin(t *testing.T) { } -func TestToString(t *testing.T) { - tpl := `{{ toString 1 | kindOf }}` - assert.NoError(t, runt(tpl, "string")) -} - func TestSemverCompare(t *testing.T) { tests := map[string]string{ `{{ semverCompare "1.2.3" "1.2.3" }}`: `true`, @@ -126,135 +121,6 @@ func TestSemver(t *testing.T) { } } -func TestToFloat64(t *testing.T) { - fh := NewFunctionHandler() - target := float64(102) - if target != fh.ToFloat64(int8(102)) { - t.Errorf("Expected 102") - } - if target != fh.ToFloat64(int(102)) { - t.Errorf("Expected 102") - } - if target != fh.ToFloat64(int32(102)) { - t.Errorf("Expected 102") - } - if target != fh.ToFloat64(int16(102)) { - t.Errorf("Expected 102") - } - if target != fh.ToFloat64(int64(102)) { - t.Errorf("Expected 102") - } - if target != fh.ToFloat64("102") { - t.Errorf("Expected 102") - } - if 0 != fh.ToFloat64("frankie") { - t.Errorf("Expected 0") - } - if target != fh.ToFloat64(uint16(102)) { - t.Errorf("Expected 102") - } - if target != fh.ToFloat64(uint64(102)) { - t.Errorf("Expected 102") - } - if 102.1234 != fh.ToFloat64(float64(102.1234)) { - t.Errorf("Expected 102.1234") - } - if 1 != fh.ToFloat64(true) { - t.Errorf("Expected 102") - } -} -func TestToInt64(t *testing.T) { - fh := NewFunctionHandler() - target := int64(102) - if target != fh.ToInt64(int8(102)) { - t.Errorf("Expected 102") - } - if target != fh.ToInt64(int(102)) { - t.Errorf("Expected 102") - } - if target != fh.ToInt64(int32(102)) { - t.Errorf("Expected 102") - } - if target != fh.ToInt64(int16(102)) { - t.Errorf("Expected 102") - } - if target != fh.ToInt64(int64(102)) { - t.Errorf("Expected 102") - } - if target != fh.ToInt64("102") { - t.Errorf("Expected 102") - } - if 0 != fh.ToInt64("frankie") { - t.Errorf("Expected 0") - } - if target != fh.ToInt64(uint16(102)) { - t.Errorf("Expected 102") - } - if target != fh.ToInt64(uint64(102)) { - t.Errorf("Expected 102") - } - if target != fh.ToInt64(float64(102.1234)) { - t.Errorf("Expected 102") - } - if 1 != fh.ToInt64(true) { - t.Errorf("Expected 102") - } -} - -func TestToInt(t *testing.T) { - fh := NewFunctionHandler() - target := int(102) - if target != fh.ToInt(int8(102)) { - t.Errorf("Expected 102") - } - if target != fh.ToInt(int(102)) { - t.Errorf("Expected 102") - } - if target != fh.ToInt(int32(102)) { - t.Errorf("Expected 102") - } - if target != fh.ToInt(int16(102)) { - t.Errorf("Expected 102") - } - if target != fh.ToInt(int64(102)) { - t.Errorf("Expected 102") - } - if target != fh.ToInt("102") { - t.Errorf("Expected 102") - } - if 0 != fh.ToInt("frankie") { - t.Errorf("Expected 0") - } - if target != fh.ToInt(uint16(102)) { - t.Errorf("Expected 102") - } - if target != fh.ToInt(uint64(102)) { - t.Errorf("Expected 102") - } - if target != fh.ToInt(float64(102.1234)) { - t.Errorf("Expected 102") - } - if 1 != fh.ToInt(true) { - t.Errorf("Expected 102") - } -} - -func TestToDecimal(t *testing.T) { - tests := map[interface{}]int64{ - "777": 511, - 777: 511, - 770: 504, - 755: 493, - } - - for input, expectedResult := range tests { - result := NewFunctionHandler().ToDecimal(input) - if result != expectedResult { - t.Errorf("Expected %v but got %v", expectedResult, result) - } - } -} - func TestGetHostByName(t *testing.T) { tpl := `{{"www.google.com" | getHostByName}}` @@ -325,13 +191,6 @@ func runRaw(tpl string, vars interface{}) (string, error) { return b.String(), nil } -func TestToDate(t *testing.T) { - tpl := `{{toDate "2006-01-02" "2017-12-31" | date "02/01/2006"}}` - if err := runt(tpl, "31/12/2017"); err != nil { - t.Error(err) - } -} - const ( beginCertificate = "-----BEGIN CERTIFICATE-----" endCertificate = "-----END CERTIFICATE-----"