diff --git a/go.mod b/go.mod index 045bee4..0fb5b43 100644 --- a/go.mod +++ b/go.mod @@ -1,10 +1,9 @@ module github.com/rulego/streamsql -go 1.22.3 +go 1.18 require ( - github.com/expr-lang/expr v1.16.9 - github.com/spf13/cast v1.7.1 + github.com/expr-lang/expr v1.17.2 github.com/stretchr/testify v1.10.0 ) diff --git a/go.sum b/go.sum index 1dbf825..fc5187e 100644 --- a/go.sum +++ b/go.sum @@ -1,21 +1,9 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/expr-lang/expr v1.16.9 h1:WUAzmR0JNI9JCiF0/ewwHB1gmcGw5wW7nWt8gc6PpCI= -github.com/expr-lang/expr v1.16.9/go.mod h1:8/vRC7+7HBzESEqt5kKpYXxrxkr31SaO8r40VO/1IT4= -github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= -github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= -github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= -github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= -github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= -github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= -github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= -github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/expr-lang/expr v1.17.2 h1:o0A99O/Px+/DTjEnQiodAgOIK9PPxL8DtXhBRKC+Iso= +github.com/expr-lang/expr v1.17.2/go.mod h1:8/vRC7+7HBzESEqt5kKpYXxrxkr31SaO8r40VO/1IT4= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8= -github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= -github.com/spf13/cast v1.7.1 h1:cuNEagBQEHWN1FnbGEjCXL2szYEXqfJPbP2HNUaca9Y= -github.com/spf13/cast v1.7.1/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo= github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= diff --git a/utils/cast/cast.go b/utils/cast/cast.go new file mode 100644 index 0000000..2a7d1a3 --- /dev/null +++ b/utils/cast/cast.go @@ -0,0 +1,302 @@ +/* + * Copyright 2025 The RuleGo Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package cast + +import ( + "encoding/json" + "fmt" + "strconv" + "time" +) + +// ToInt converts an interface{} to int. +// It returns 0 if conversion fails. +func ToInt(value interface{}) int { + v, _ := ToIntE(value) + return v +} + +// ToIntE converts an interface{} to int with error handling. +// Returns the converted int value and nil error if successful. +// Returns 0 and an error if conversion fails. +func ToIntE(value interface{}) (int, error) { + switch v := value.(type) { + case int: + return v, nil + case int8: + return int(v), nil + case int16: + return int(v), nil + case int32: + return int(v), nil + case int64: + return int(v), nil + case uint: + return int(v), nil + case uint8: + return int(v), nil + case uint16: + return int(v), nil + case uint32: + return int(v), nil + case uint64: + return int(v), nil + case float64: + return int(v), nil + case float32: + return int(v), nil + case string: + if i, err := strconv.Atoi(v); err == nil { + return i, nil + } else { + return 0, err + } + default: + return 0, fmt.Errorf("unable to cast %v of type %T to int", value, value) + } +} + +// ToInt64 converts an interface{} to int64. +// It returns 0 if conversion fails. +func ToInt64(value interface{}) int64 { + v, _ := ToInt64E(value) + return v +} + +// ToInt64E converts an interface{} to int64 with error handling. +// Returns the converted int64 value and nil error if successful. +// Returns 0 and an error if conversion fails. +func ToInt64E(value interface{}) (int64, error) { + switch v := value.(type) { + case int64: + return v, nil + case int8: + return int64(v), nil + case int16: + return int64(v), nil + case int32: + return int64(v), nil + case int: + return int64(v), nil + case uint: + return int64(v), nil + case uint8: + return int64(v), nil + case uint16: + return int64(v), nil + case uint32: + return int64(v), nil + case uint64: + return int64(v), nil + case float64: + return int64(v), nil + case float32: + return int64(v), nil + case string: + if i, err := strconv.Atoi(v); err == nil { + return int64(i), nil + } else { + return 0, err + } + default: + return 0, fmt.Errorf("unable to cast %v of type %T to int", value, value) + } +} + +// ToDurationE converts an interface{} to time.Duration with error handling. +// Returns the converted duration value and nil error if successful. +// Returns 0 and an error if conversion fails. +func ToDurationE(value interface{}) (time.Duration, error) { + switch v := value.(type) { + case time.Duration: + return v, nil + case int: + return time.Duration(v), nil + case int8: + return time.Duration(v), nil + case int16: + return time.Duration(v), nil + case int32: + return time.Duration(v), nil + case int64: + return time.Duration(v), nil + case uint: + return time.Duration(v), nil + case uint8: + return time.Duration(v), nil + case uint16: + return time.Duration(v), nil + case uint32: + return time.Duration(v), nil + case uint64: + return time.Duration(v), nil + case string: + if dur, err := time.ParseDuration(v); err == nil { + return dur, nil + } else { + return 0, err + } + default: + return 0, fmt.Errorf("unable to cast %v of type %T to int", value, value) + } +} + +// ToBool converts an interface{} to bool. +// It returns false if conversion fails. +func ToBool(value interface{}) bool { + v, _ := ToBoolE(value) + return v +} + +// ToBoolE converts an interface{} to bool with error handling. +// Returns the converted bool value and nil error if successful. +// Returns false and an error if conversion fails. +func ToBoolE(value interface{}) (bool, error) { + switch v := value.(type) { + case bool: + return v, nil + case int, int8, int16, int32, int64, uint, uint8, uint16, uint32, uint64: + return v != 0, nil + case float32, float64: + return v != 0.0, nil + case string: + if b, err := strconv.ParseBool(v); err == nil { + return b, nil + } + return false, fmt.Errorf("unable to cast %v of type %T to bool", value, value) + default: + return false, fmt.Errorf("unable to cast %v of type %T to bool", value, value) + } +} + +// ToFloat64 converts an interface{} to float64. +// It returns 0 if conversion fails. +func ToFloat64(value interface{}) float64 { + v, _ := ToFloat64E(value) + return v +} + +// ToFloat64E converts an interface{} to float64 with error handling. +// Returns the converted float64 value and nil error if successful. +// Returns 0 and an error if conversion fails. +func ToFloat64E(value interface{}) (float64, error) { + switch v := value.(type) { + case float64: + return v, nil + case float32: + return float64(v), nil + case int64: + return float64(v), nil + case int8: + return float64(v), nil + case int16: + return float64(v), nil + case int32: + return float64(v), nil + case int: + return float64(v), nil + case uint: + return float64(v), nil + case uint8: + return float64(v), nil + case uint16: + return float64(v), nil + case uint32: + return float64(v), nil + case uint64: + return float64(v), nil + case string: + if f, err := strconv.ParseFloat(v, 64); err == nil { + return f, nil + } else { + return 0, err + } + default: + return 0, fmt.Errorf("unable to cast %v of type %T to float64", value, value) + } +} + +// ToString converts an interface{} to string. +// It returns empty string if conversion fails. +func ToString(input interface{}) string { + v, _ := ToStringE(input) + return v +} + +// ToStringE converts an interface{} to string with error handling. +// Returns the converted string value and nil error if successful. +// Returns empty string and an error if conversion fails. +func ToStringE(input interface{}) (string, error) { + if input == nil { + return "", nil + } + switch v := input.(type) { + case string: + return v, nil + case bool: + return strconv.FormatBool(v), nil + case float64: + ft := input.(float64) + return strconv.FormatFloat(ft, 'f', -1, 64), nil + case float32: + ft := input.(float32) + return strconv.FormatFloat(float64(ft), 'f', -1, 32), nil + case int: + return strconv.Itoa(v), nil + case uint: + return strconv.Itoa(int(v)), nil + case int8: + return strconv.Itoa(int(v)), nil + case uint8: + return strconv.Itoa(int(v)), nil + case int16: + return strconv.Itoa(int(v)), nil + case uint16: + return strconv.Itoa(int(v)), nil + case int32: + return strconv.Itoa(int(v)), nil + case uint32: + return strconv.Itoa(int(v)), nil + case int64: + return strconv.FormatInt(v, 10), nil + case uint64: + return strconv.FormatUint(v, 10), nil + case []byte: + return string(v), nil + case fmt.Stringer: + return v.String(), nil + case error: + return v.Error(), nil + case map[interface{}]interface{}: + // 转换为 map[string]interface{} + convertedInput := make(map[string]interface{}) + for k, value := range v { + convertedInput[fmt.Sprintf("%v", k)] = value + } + if newValue, err := json.Marshal(convertedInput); err == nil { + return string(newValue), nil + } else { + return "", err + } + default: + if newValue, err := json.Marshal(input); err == nil { + return string(newValue), nil + } else { + return "", err + } + } +} diff --git a/utils/cast/cast_test.go b/utils/cast/cast_test.go new file mode 100644 index 0000000..11c9e40 --- /dev/null +++ b/utils/cast/cast_test.go @@ -0,0 +1,239 @@ +/* + * Copyright 2025 The RuleGo Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package cast + +import ( + "fmt" + "testing" + "time" +) + +func TestToInt(t *testing.T) { + tests := []struct { + name string + input interface{} + expect int + hasErr bool + }{ + {"int", 123, 123, false}, + {"int8", int8(123), 123, false}, + {"int16", int16(123), 123, false}, + {"int32", int32(123), 123, false}, + {"int64", int64(123), 123, false}, + {"uint", uint(123), 123, false}, + {"uint8", uint8(123), 123, false}, + {"uint16", uint16(123), 123, false}, + {"uint32", uint32(123), 123, false}, + {"uint64", uint64(123), 123, false}, + {"float64", 1.1, 1, false}, + {"float64", float32(1.1), 1, false}, + {"string", "123", 123, false}, + {"invalid string", "abc", 0, true}, + {"invalid type", []int{1, 2, 3}, 0, true}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := ToInt(tt.input) + if got != tt.expect { + t.Errorf("ToInt() = %v, want %v", got, tt.expect) + } + + _, err := ToIntE(tt.input) + if (err != nil) != tt.hasErr { + t.Errorf("ToIntE() error = %v, wantErr %v", err, tt.hasErr) + } + }) + } +} + +func TestToInt64(t *testing.T) { + tests := []struct { + name string + input interface{} + expect int64 + hasErr bool + }{ + {"int", 123, 123, false}, + {"int8", int8(123), 123, false}, + {"int16", int16(123), 123, false}, + {"int32", int32(123), 123, false}, + {"int64", int64(123), 123, false}, + {"uint", uint(123), 123, false}, + {"uint8", uint8(123), 123, false}, + {"uint16", uint16(123), 123, false}, + {"uint32", uint32(123), 123, false}, + {"uint64", uint64(123), 123, false}, + {"string", "123", 123, false}, + {"invalid string", "abc", 0, true}, + {"invalid type", []int{1, 2, 3}, 0, true}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := ToInt64(tt.input) + if got != tt.expect { + t.Errorf("ToInt64() = %v, want %v", got, tt.expect) + } + + _, err := ToInt64E(tt.input) + if (err != nil) != tt.hasErr { + t.Errorf("ToInt64E() error = %v, wantErr %v", err, tt.hasErr) + } + }) + } +} + +func TestToDurationE(t *testing.T) { + tests := []struct { + name string + input interface{} + expect time.Duration + hasErr bool + }{ + {"duration", time.Second, time.Second, false}, + {"int", 1000, 1000, false}, + {"int64", int64(1000), 1000, false}, + {"string", "1s", time.Second, false}, + {"invalid string", "abc", 0, true}, + {"invalid type", []int{1, 2, 3}, 0, true}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + dur, err := ToDurationE(tt.input) + if (err != nil) != tt.hasErr { + t.Errorf("ToDurationE() error = %v, wantErr %v", err, tt.hasErr) + } + if !tt.hasErr && dur != tt.expect { + t.Errorf("ToDurationE() = %v, want %v", dur, tt.expect) + } + }) + } +} + +func TestToBool(t *testing.T) { + tests := []struct { + name string + input interface{} + expect bool + hasErr bool + }{ + {"bool true", true, true, false}, + {"bool false", false, false, false}, + {"int 1", 1, true, false}, + {"int 0", 0, false, false}, + {"float64 1.0", 1.0, true, false}, + {"float64 0.0", 0.0, false, false}, + {"string true", "true", true, false}, + {"string false", "false", false, false}, + {"string 1", "1", true, false}, + {"string 0", "0", false, false}, + {"invalid string", "abc", false, true}, + {"invalid type", []int{1, 2, 3}, false, true}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := ToBool(tt.input) + if got != tt.expect { + t.Errorf("ToBool() = %v, want %v", got, tt.expect) + } + + _, err := ToBoolE(tt.input) + if (err != nil) != tt.hasErr { + t.Errorf("ToBoolE() error = %v, wantErr %v", err, tt.hasErr) + } + }) + } +} + +func TestToFloat64(t *testing.T) { + tests := []struct { + name string + input interface{} + expect float64 + hasErr bool + }{ + {"float64", 3.14, 3.14, false}, + {"float32", float32(3.14), float64(float32(3.14)), false}, + {"int", 123, 123.0, false}, + {"int64", int64(123), 123.0, false}, + {"uint64", uint64(123), 123.0, false}, + {"string", "3.14", 3.14, false}, + {"invalid string", "abc", 0, true}, + {"invalid type", []int{1, 2, 3}, 0, true}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := ToFloat64(tt.input) + if got != tt.expect { + t.Errorf("ToFloat64() = %v, want %v", got, tt.expect) + } + + _, err := ToFloat64E(tt.input) + if (err != nil) != tt.hasErr { + t.Errorf("ToFloat64E() error = %v, wantErr %v", err, tt.hasErr) + } + }) + } +} + +func TestToString(t *testing.T) { + tests := []struct { + name string + input interface{} + expect string + hasErr bool + }{ + {"nil", nil, "", false}, + {"string", "test", "test", false}, + {"bool true", true, "true", false}, + {"bool false", false, "false", false}, + {"int", 123, "123", false}, + {"int8", int8(123), "123", false}, + {"int16", int16(123), "123", false}, + {"int32", int32(123), "123", false}, + {"int64", int64(123), "123", false}, + {"uint", uint(123), "123", false}, + {"uint8", uint8(123), "123", false}, + {"uint16", uint16(123), "123", false}, + {"uint32", uint32(123), "123", false}, + {"uint64", uint64(123), "123", false}, + {"float64", 3.14, "3.14", false}, + {"float32", float32(3.14), "3.14", false}, + {"[]byte", []byte("test"), "test", false}, + {"error", fmt.Errorf("test error"), "test error", false}, + {"map", map[string]int{"a": 1}, "{\"a\":1}", false}, + {"invalid type", make(chan int), "", true}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := ToString(tt.input) + if got != tt.expect { + t.Errorf("ToString() = %v, want %v", got, tt.expect) + } + + _, err := ToStringE(tt.input) + if (err != nil) != tt.hasErr { + t.Errorf("ToStringE() error = %v, wantErr %v", err, tt.hasErr) + } + }) + } +} diff --git a/utils/time.go b/utils/timex/time.go similarity index 100% rename from utils/time.go rename to utils/timex/time.go diff --git a/utils/time_test.go b/utils/timex/time_test.go similarity index 100% rename from utils/time_test.go rename to utils/timex/time_test.go diff --git a/window/counting_window.go b/window/counting_window.go index 069f671..979b4f2 100644 --- a/window/counting_window.go +++ b/window/counting_window.go @@ -3,12 +3,12 @@ package window import ( "context" "fmt" + "github.com/rulego/streamsql/utils/cast" + "github.com/rulego/streamsql/utils/timex" "sync" "time" "github.com/rulego/streamsql/model" - timex "github.com/rulego/streamsql/utils" - "github.com/spf13/cast" ) var _ Window = (*CountingWindow)(nil) diff --git a/window/sliding_window.go b/window/sliding_window.go index 88664ce..afc0c51 100644 --- a/window/sliding_window.go +++ b/window/sliding_window.go @@ -3,12 +3,12 @@ package window import ( "context" "fmt" + "github.com/rulego/streamsql/utils/cast" + "github.com/rulego/streamsql/utils/timex" "sync" "time" "github.com/rulego/streamsql/model" - timex "github.com/rulego/streamsql/utils" - "github.com/spf13/cast" ) // 确保 SlidingWindow 结构体实现了 Window 接口 diff --git a/window/sliding_window_test.go b/window/sliding_window_test.go index 5463ead..0eeedfc 100644 --- a/window/sliding_window_test.go +++ b/window/sliding_window_test.go @@ -2,11 +2,11 @@ package window import ( "context" + "github.com/rulego/streamsql/utils/timex" "testing" "time" "github.com/rulego/streamsql/model" - timex "github.com/rulego/streamsql/utils" "github.com/stretchr/testify/assert" ) diff --git a/window/tumbling_window.go b/window/tumbling_window.go index ad7fe19..baf1ab0 100644 --- a/window/tumbling_window.go +++ b/window/tumbling_window.go @@ -4,12 +4,12 @@ package window import ( "context" "fmt" + "github.com/rulego/streamsql/utils/cast" + "github.com/rulego/streamsql/utils/timex" "sync" "time" "github.com/rulego/streamsql/model" - timex "github.com/rulego/streamsql/utils" - "github.com/spf13/cast" ) // 确保 TumblingWindow 结构体实现了 Window 接口。