From 340025002b3515c4d3137504181ebad94d0ec9ff Mon Sep 17 00:00:00 2001 From: Steven Date: Tue, 20 Aug 2024 23:45:55 +0800 Subject: [PATCH] chore: tweak utils --- go.mod | 1 + go.sum | 2 + internal/util/util.go | 106 ++++++++++++++++++++++-- internal/util/util_test.go | 162 ++++++++++++++++++++++++++++--------- 4 files changed, 224 insertions(+), 47 deletions(-) diff --git a/go.mod b/go.mod index 50240ec..c38a249 100644 --- a/go.mod +++ b/go.mod @@ -71,6 +71,7 @@ require ( github.com/joho/godotenv v1.5.1 github.com/lib/pq v1.10.9 github.com/mssola/useragent v1.0.0 + github.com/nyaruka/phonenumbers v1.4.0 github.com/pkg/errors v0.9.1 github.com/posthog/posthog-go v1.2.18 golang.org/x/exp v0.0.0-20240808152545-0cdaa3abc0fa diff --git a/go.sum b/go.sum index e39acf0..3ac9bc5 100644 --- a/go.sum +++ b/go.sum @@ -258,6 +258,8 @@ github.com/nats-io/nkeys v0.1.3/go.mod h1:xpnFELMwJABBLVhffcfd1MZx6VsNRFpEugbxzi github.com/nats-io/nuid v1.0.1/go.mod h1:19wcPz3Ph3q0Jbyiqsd0kePYG7A95tJPxeL+1OSON2c= github.com/ncruces/go-strftime v0.1.9 h1:bY0MQC28UADQmHmaF5dgpLmImcShSi2kHU9XLdhx/f4= github.com/ncruces/go-strftime v0.1.9/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls= +github.com/nyaruka/phonenumbers v1.4.0 h1:ddhWiHnHCIX3n6ETDA58Zq5dkxkjlvgrDWM2OHHPCzU= +github.com/nyaruka/phonenumbers v1.4.0/go.mod h1:gv+CtldaFz+G3vHHnasBSirAi3O2XLqZzVWz4V1pl2E= github.com/oklog/oklog v0.3.2/go.mod h1:FCV+B7mhrz4o+ueLpx+KqkyXRGMWOYEvfiXtdGtbWGs= github.com/oklog/run v1.0.0/go.mod h1:dlhp/R75TPv97u0XWUtDeV/lRKWPKSdTuV0TZvrmrQA= github.com/olekukonko/tablewriter v0.0.0-20170122224234-a0225b3f23b5/go.mod h1:vsDQFd/mU46D+Z4whnwzcISnGGzXWMclvtLoiIKAKIo= diff --git a/internal/util/util.go b/internal/util/util.go index 659fc09..8a61393 100644 --- a/internal/util/util.go +++ b/internal/util/util.go @@ -2,20 +2,25 @@ package util import ( "crypto/rand" + "fmt" "math/big" "net/mail" - "net/url" "strconv" "strings" + "unicode/utf8" + + "github.com/google/uuid" + "github.com/nyaruka/phonenumbers" + "github.com/pkg/errors" ) // ConvertStringToInt32 converts a string to int32. func ConvertStringToInt32(src string) (int32, error) { - i, err := strconv.Atoi(src) + parsed, err := strconv.ParseInt(src, 10, 32) if err != nil { return 0, err } - return int32(i), nil + return int32(parsed), nil } // HasPrefixes returns true if the string s has any of the given prefixes. @@ -36,6 +41,10 @@ func ValidateEmail(email string) bool { return true } +func GenUUID() string { + return uuid.New().String() +} + var letters = []rune("0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ") // RandomString returns a random string with length n. @@ -57,8 +66,91 @@ func RandomString(n int) (string, error) { return sb.String(), nil } -// ValidateURI validates the URI. -func ValidateURI(uri string) bool { - u, err := url.Parse(uri) - return err == nil && u.Scheme != "" && u.Host != "" +// ValidatePhone validates the phone number. +func ValidatePhone(phone string) error { + phoneNumber, err := phonenumbers.Parse(phone, "") + if err != nil { + return err + } + if !phonenumbers.IsValidNumber(phoneNumber) { + return errors.New("invalid phone number") + } + return nil +} + +// SanitizeUTF8String returns a copy of the string s with each run of invalid or unprintable UTF-8 byte sequences +// replaced by its hexadecimal representation string. +func SanitizeUTF8String(s string) string { + var b strings.Builder + + for i, c := range s { + if c != utf8.RuneError { + continue + } + + _, wid := utf8.DecodeRuneInString(s[i:]) + if wid == 1 { + b.Grow(len(s)) + _, _ = b.WriteString(s[:i]) + s = s[i:] + break + } + } + + // Fast path for unchanged input + if b.Cap() == 0 { // didn't call b.Grow above + return s + } + + for i := 0; i < len(s); { + c := s[i] + // U+0000-U+0019 are control characters + if 0x20 <= c && c < utf8.RuneSelf { + i++ + _ = b.WriteByte(c) + continue + } + _, wid := utf8.DecodeRuneInString(s[i:]) + if wid == 1 { + i++ + _, _ = b.WriteString(fmt.Sprintf("\\x%02x", c)) + continue + } + _, _ = b.WriteString(s[i : i+wid]) + i += wid + } + + return b.String() +} + +// ReplaceString replaces all occurrences of old in slice with new. +func ReplaceString(slice []string, old, new string) []string { + for i, s := range slice { + if s == old { + slice[i] = new + } + } + return slice +} + +// TruncateString truncates the string to have a maximum length of `limit` characters. +func TruncateString(str string, limit int) (string, bool) { + chars := 0 + // The string may contain unicode characters, so we iterate here. + for i := range str { + if chars >= limit { + return str[:i], true + } + chars++ + } + return str, false +} + +// TruncateStringWithDescription tries to truncate the string and append "... (view details in Bytebase)" if truncated. +func TruncateStringWithDescription(str string) string { + const limit = 450 + if truncatedStr, truncated := TruncateString(str, limit); truncated { + return fmt.Sprintf("%s... (view details in Bytebase)", truncatedStr) + } + return str } diff --git a/internal/util/util_test.go b/internal/util/util_test.go index 6071ed7..1cc08d7 100644 --- a/internal/util/util_test.go +++ b/internal/util/util_test.go @@ -2,60 +2,142 @@ package util import ( "testing" + + "github.com/stretchr/testify/assert" ) -func TestValidateEmail(t *testing.T) { - tests := []struct { - email string - want bool - }{ - { - email: "t@gmail.com", - want: true, - }, - { - email: "@yourselfhosted.com", - want: false, - }, - { - email: "1@gmail", - want: true, - }, +func TestHasPrefixes(t *testing.T) { + type args struct { + src string + prefixes []string } - for _, test := range tests { - result := ValidateEmail(test.email) - if result != test.want { - t.Errorf("Validate Email %s: got result %v, want %v.", test.email, result, test.want) - } - } -} - -func TestValidateURI(t *testing.T) { tests := []struct { - uri string + name string + args args want bool }{ { - uri: "https://localhsot:3000", + name: "has prefixes", + args: args{ + src: "abc", + prefixes: []string{"a", "b", "c"}, + }, want: true, }, { - uri: "https://yourselfhosted.com", - want: true, - }, - { - uri: "google.com", - want: false, - }, - { - uri: "i don't know", + name: "has no matching prefix", + args: args{ + src: "this is a sentence", + prefixes: []string{"that", "x", "y"}, + }, want: false, }, } + for i := range tests { + tt := tests[i] + t.Run(tt.name, func(t *testing.T) { + got := HasPrefixes(tt.args.src, tt.args.prefixes...) + assert.Equal(t, got, tt.want) + }) + } +} + +func TestTruncateString(t *testing.T) { + tests := []struct { + name string + str string + limit int + want string + truncated bool + }{ + { + name: "simple truncate 0", + str: "0123", + limit: 0, + want: "", + truncated: true, + }, + { + name: "simple truncate 2", + str: "0123", + limit: 2, + want: "01", + truncated: true, + }, + { + name: "simple truncate 3", + str: "0123", + limit: 3, + want: "012", + truncated: true, + }, + { + name: "simple truncate 4", + str: "0123", + limit: 4, + want: "0123", + truncated: false, + }, + { + name: "simple truncate 20", + str: "0123", + limit: 20, + want: "0123", + truncated: false, + }, + { + name: "unicode truncate 5", + str: "H㐀〾▓朗퐭텟şüöžåйкл¤", + limit: 5, + want: "H㐀〾▓朗", + truncated: true, + }, + { + name: "unicode truncate 10", + str: "H㐀〾▓朗퐭텟şüöžåйкл¤", + limit: 10, + want: "H㐀〾▓朗퐭텟şüö", + truncated: true, + }, + { + name: "unicode fit", + str: "H㐀〾▓朗퐭텟şüöžåйкл¤", + limit: 16, + want: "H㐀〾▓朗퐭텟şüöžåйкл¤", + truncated: false, + }, + } + a := assert.New(t) + for i := range tests { + test := tests[i] + t.Run(test.name, func(_ *testing.T) { + got, truncated := TruncateString(test.str, test.limit) + a.Equal(test.want, got) + a.Equal(test.truncated, truncated) + }) + } +} + +func TestValidatePhone(t *testing.T) { + tests := []struct { + phone string + want bool + }{ + { + phone: "1234567890", + want: false, + }, + { + phone: "+8615655556666", + want: true, + }, + } + for _, test := range tests { - result := ValidateURI(test.uri) - if result != test.want { - t.Errorf("Validate URI %s: got result %v, want %v.", test.uri, result, test.want) + got := ValidatePhone(test.phone) + isValid := got == nil + if isValid != test.want { + t.Errorf("validatePhone %s, err %v", test.phone, got) } } }