diff --git a/README.md b/README.md index e8f6ef07e5a6f31c3018c88c6bde7552f133e474..33be81ebe50bca76ab3c015475c0fa1c7c9e9abd 100644 --- a/README.md +++ b/README.md @@ -37,7 +37,7 @@ make install 2. After your changes have been merged to the `main` branch of `go-utils`, run the following command which will automatically create a new tag: ``` -bob release full +bob release tag ``` and select project `bob-public-utils/bobgroup-go-utils` diff --git a/api_responses/api_responses.go b/api_responses/api_responses.go index 2138361dbda238d18db0b503e0aa228e71812b50..0f6325532844af3cc285853b644dfbbb07d85afd 100644 --- a/api_responses/api_responses.go +++ b/api_responses/api_responses.go @@ -274,8 +274,13 @@ func GenericJSONResponseWithMessage(code int, err error) events.APIGatewayProxyR var body map[string]string if err != nil { - customErr := err.(*errors.CustomError) - message = customErr.Formatted(errors.FormattingOptions{NewLines: false, Causes: true}) + var message string + if customErr, ok := err.(*errors.CustomError); ok { + message = customErr.Formatted(errors.FormattingOptions{NewLines: false, Causes: true}) + } else { + message = err.Error() + } + body = map[string]string{ "message": string_utils.Capitalize(message), } diff --git a/date_utils/date_utils.go b/date_utils/date_utils.go index ccca80d5014f376762ba0d395f13640e78e59b97..c88b39a22077602ead42c62458808428a45db174 100644 --- a/date_utils/date_utils.go +++ b/date_utils/date_utils.go @@ -1,10 +1,15 @@ package date_utils import ( - "github.com/araddon/dateparse" + "fmt" "reflect" "strconv" + "strings" "time" + + "github.com/araddon/dateparse" + "gitlab.bob.co.za/bob-public-utils/bobgroup-go-utils/errors" + "gitlab.bob.co.za/bob-public-utils/bobgroup-go-utils/utils" ) const TimeZoneString = "Africa/Johannesburg" @@ -271,3 +276,147 @@ func formatTimestampsWithTimeZoneInSlice(fieldValue reflect.Value, location *tim } return nil } + +// TradingHours represents an array of (StartTime,EndTime) pairs, one for each day of the week. +// The array is 0 indexed, with 0 being Sunday and 6 being Saturday and 7 being public holidays. +type TradingHours []struct { + StartTime string `json:"start_time"` + EndTime string `json:"end_time"` +} + +func (t TradingHours) Validate() error { + if len(t) != 8 { + return errors.Error("Trading hours must have 8 days, 7 for every day of the week and 1 for public holidays") + } + + for _, day := range t { + if day.StartTime == "" || day.EndTime == "" { + // Allow empty trading hours for a day to represent closed + continue + } + + if !TimeBefore(day.StartTime, day.EndTime) { + return errors.Error("Start time must be before end time") + } + + if len(day.StartTime) != 5 || len(day.EndTime) != 5 { + return errors.Error("Time must be in the format HH:MM") + } + + startHourMinSlice := strings.Split(day.StartTime, ":") + if len(startHourMinSlice) != 2 { + return errors.Error("Time must be in the format HH:MM") + } + startHour, startMin := startHourMinSlice[0], startHourMinSlice[1] + startHourInt, err := strconv.Atoi(startHour) + if err != nil || startHourInt < 0 || startHourInt > 23 { + return errors.Error("Start hour must be between 0 and 23") + } + startMinInt, err := strconv.Atoi(startMin) + if err != nil || !(startMinInt == 0 || startMinInt == 30) { + return errors.Error("Start minute must be 0 or 30") + } + + endHourMinSlice := strings.Split(day.EndTime, ":") + if len(endHourMinSlice) != 2 { + return errors.Error("Time must be in the format HH:MM") + } + endHour, endMin := endHourMinSlice[0], endHourMinSlice[1] + endHourInt, err := strconv.Atoi(endHour) + if err != nil || endHourInt < 0 || endHourInt > 23 { + return errors.Error("End hour must be between 0 and 23") + } + endMinInt, err := strconv.Atoi(endMin) + if err != nil || !(endMinInt == 0 || endMinInt == 30 || endMinInt == 59) { + return errors.Error("End minute must be 0, 30 or 59") + } + } + + return nil +} + +func (t TradingHours) String() string { + var result strings.Builder + const numberOfDaysInWeek = 7 + copyOfT := utils.DeepCopy(t).(TradingHours) + weekdays, publicHolidays := copyOfT[:numberOfDaysInWeek], copyOfT[numberOfDaysInWeek] + weekdays = append(weekdays, weekdays[0]) // Add the first day (Sunday) to the end because we want Monday to be first in the string + + rangeStartIndex := 1 + for i := 1; i < len(weekdays); i++ { + currentDay := weekdays[i] + nextDay := currentDay + if i+1 < len(weekdays) { + nextDay = weekdays[i+1] + } + + // Determine times + var times string + if currentDay.StartTime != "" && currentDay.EndTime != "" { + startTime, err := time.Parse("15:04", currentDay.StartTime) + if err != nil { + return "" + } + + endTime, err := time.Parse("15:04", currentDay.EndTime) + if err != nil { + return "" + } + + times = startTime.Format("3:04pm") + " – " + endTime.Format("3:04pm") + if currentDay.StartTime == "00:00" && currentDay.EndTime == "23:59" { + times = "All day" + } + } else { + times = "Closed" + } + + // If we're at the last element or the next day doesn't have the same times, we end the current range + if i == len(weekdays)-1 || currentDay.StartTime != nextDay.StartTime || currentDay.EndTime != nextDay.EndTime { + if rangeStartIndex == i { + day := time.Weekday(rangeStartIndex).String()[:3] + if rangeStartIndex == numberOfDaysInWeek { + day = time.Sunday.String()[:3] + } + result.WriteString(fmt.Sprintf("%s: %s", day, times)) + } else { + rangeStartDay := time.Weekday(rangeStartIndex).String()[:3] + rangeEndDay := time.Weekday(i).String()[:3] + if i == numberOfDaysInWeek { + rangeEndDay = time.Sunday.String()[:3] + } + result.WriteString(fmt.Sprintf("%s – %s: %s", rangeStartDay, rangeEndDay, times)) + } + + if i < len(weekdays)-1 { + result.WriteString(", ") + } + + rangeStartIndex = i + 1 + } + } + + // Public holidays + var times string + if publicHolidays.StartTime != "" && publicHolidays.EndTime != "" { + startTime, err := time.Parse("15:04", publicHolidays.StartTime) + if err != nil { + return "" + } + + endTime, err := time.Parse("15:04", publicHolidays.EndTime) + if err != nil { + return "" + } + + times = startTime.Format("3:04pm") + " – " + endTime.Format("3:04pm") + if publicHolidays.StartTime == "00:00" && publicHolidays.EndTime == "23:59" { + times = "All day" + } + } else { + times = "Closed" + } + result.WriteString(fmt.Sprintf(", Public holidays: %s", times)) + + return result.String() +} diff --git a/go.mod b/go.mod index 341c3f2f045fec1bfe9668fcd12a1df65b2116a3..494108d1bdd90b73ee14aa6a310468ac09e3cc55 100644 --- a/go.mod +++ b/go.mod @@ -3,6 +3,7 @@ module gitlab.bob.co.za/bob-public-utils/bobgroup-go-utils go 1.21 require ( + github.com/AfterShip/email-verifier v1.4.0 github.com/MindscapeHQ/raygun4go v1.1.1 github.com/araddon/dateparse v0.0.0-20210429162001-6b43995a97de github.com/aws/aws-lambda-go v1.26.0 @@ -15,6 +16,7 @@ require ( github.com/aws/aws-sdk-go-v2/service/sqs v1.31.4 github.com/aws/aws-secretsmanager-caching-go v1.1.0 github.com/aws/smithy-go v1.20.2 + github.com/dimuska139/go-email-normalizer/v2 v2.0.0 github.com/dlsniper/debugger v0.6.0 github.com/go-pg/pg/v10 v10.10.6 github.com/go-redis/redis/v8 v8.11.4 @@ -22,6 +24,8 @@ require ( github.com/go-resty/resty/v2 v2.7.0 github.com/golang-jwt/jwt/v4 v4.4.3 github.com/google/uuid v1.3.0 + github.com/jaytaylor/html2text v0.0.0-20230321000545-74c2419ad056 + github.com/jinzhu/now v1.1.5 github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 github.com/opensearch-project/opensearch-go/v2 v2.2.0 github.com/pkg/errors v0.9.1 @@ -29,10 +33,10 @@ require ( github.com/samber/lo v1.38.1 github.com/sirupsen/logrus v1.8.1 github.com/thoas/go-funk v0.9.1 - github.com/uptrace/bun v1.1.14 + github.com/uptrace/bun v1.1.17 golang.ngrok.com/ngrok v1.4.1 - golang.org/x/crypto v0.9.0 - golang.org/x/text v0.9.0 + golang.org/x/crypto v0.22.0 + golang.org/x/text v0.14.0 ) require ( @@ -56,27 +60,32 @@ require ( github.com/go-pg/zerochecker v0.2.0 // indirect github.com/go-stack/stack v1.8.1 // indirect github.com/golang/protobuf v1.5.2 // indirect + github.com/hbollon/go-edlib v1.6.0 // indirect github.com/inconshreveable/log15 v3.0.0-testing.3+incompatible // indirect github.com/inconshreveable/log15/v3 v3.0.0-testing.5 // indirect github.com/jinzhu/inflection v1.0.0 // indirect github.com/jmespath/go-jmespath v0.4.0 // indirect github.com/jpillora/backoff v1.0.0 // indirect github.com/mattn/go-colorable v0.1.13 // indirect - github.com/mattn/go-isatty v0.0.19 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect + github.com/mattn/go-runewidth v0.0.10 // indirect + github.com/olekukonko/tablewriter v0.0.5 // indirect github.com/pborman/uuid v1.2.1 // indirect + github.com/rivo/uniseg v0.1.0 // indirect github.com/smartystreets/goconvey v1.7.2 // indirect + github.com/ssor/bom v0.0.0-20170718123548-6386211fdfcf // indirect github.com/tmthrgd/go-hex v0.0.0-20190904060850-447a3041c3bc // indirect github.com/vmihailenco/bufpool v0.1.11 // indirect github.com/vmihailenco/msgpack v4.0.4+incompatible // indirect - github.com/vmihailenco/msgpack/v5 v5.3.5 // indirect + github.com/vmihailenco/msgpack/v5 v5.4.1 // indirect github.com/vmihailenco/tagparser v0.1.2 // indirect github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect go.uber.org/multierr v1.10.0 // indirect golang.ngrok.com/muxado/v2 v2.0.0 // indirect golang.org/x/exp v0.0.0-20220303212507-bbda1eaf7a17 // indirect - golang.org/x/net v0.10.0 // indirect - golang.org/x/sys v0.8.0 // indirect - golang.org/x/term v0.8.0 // indirect + golang.org/x/net v0.24.0 // indirect + golang.org/x/sys v0.19.0 // indirect + golang.org/x/term v0.19.0 // indirect google.golang.org/appengine v1.6.6 // indirect google.golang.org/protobuf v1.28.1 // indirect mellium.im/sasl v0.2.1 // indirect diff --git a/go.sum b/go.sum index 671dcd1e917c3dba3b02e17ffe20a991f7a99005..9946076a47c7a649cc1002cee8c657f4461cea2f 100644 --- a/go.sum +++ b/go.sum @@ -1,4 +1,6 @@ cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +github.com/AfterShip/email-verifier v1.4.0 h1:DoQplvVFVhZUfS5fPiVnmCQDr5i1tv+ivUV0TFd2AZo= +github.com/AfterShip/email-verifier v1.4.0/go.mod h1:JNPV1KZpTq4TArmss1NAOJsTD8JRa/ZElbCAJCEgikg= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/MindscapeHQ/raygun4go v1.1.1 h1:fk3Uknv9kQxUIwL3mywwHQRyfq3PaR9lE/e40K+OcY0= github.com/MindscapeHQ/raygun4go v1.1.1/go.mod h1:NW0eWi2Qs00ZcctO6owrVMY+h2HxzJVgQGDrTj2ysw4= @@ -76,6 +78,8 @@ 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/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78= github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc= +github.com/dimuska139/go-email-normalizer/v2 v2.0.0 h1:LH41ypO4BFast9bc8hNu6YEkRvLloHFYihSjfwiARSg= +github.com/dimuska139/go-email-normalizer/v2 v2.0.0/go.mod h1:2Gil1j/rfUKJ5BHc/uxxyRiuk3YTg6/C3D7dz7jVQfw= github.com/dlsniper/debugger v0.6.0 h1:AyPoOtJviCmig9AKNRAPPw5B5UyB+cI72zY3Jb+6LlA= github.com/dlsniper/debugger v0.6.0/go.mod h1:FFdRcPU2Yo4P411bp5U97DHJUSUMKcqw1QMGUu0uVb8= github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= @@ -130,15 +134,24 @@ github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 h1:EGx4pi6eqNxGaHF6qqu48+N2wcFQ5qg5FXgOdqsJ5d8= github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= +github.com/h2non/parth v0.0.0-20190131123155-b4df798d6542 h1:2VTzZjLZBgl62/EtslCrtky5vbi9dd7HrQPQIx6wqiw= +github.com/h2non/parth v0.0.0-20190131123155-b4df798d6542/go.mod h1:Ow0tF8D4Kplbc8s8sSb3V2oUCygFHVp8gC3Dn6U4MNI= github.com/hashicorp/yamux v0.1.1 h1:yrQxtgseBDrq9Y652vSRDvsKCJKOUD+GzTS4Y0Y8pvE= github.com/hashicorp/yamux v0.1.1/go.mod h1:CtWFDAQgb7dxtzFs4tWbplKIe2jSi3+5vKbgIO0SLnQ= +github.com/hashicorp/yamux v0.1.1/go.mod h1:CtWFDAQgb7dxtzFs4tWbplKIe2jSi3+5vKbgIO0SLnQ= +github.com/hbollon/go-edlib v1.6.0 h1:ga7AwwVIvP8mHm9GsPueC0d71cfRU/52hmPJ7Tprv4E= +github.com/hbollon/go-edlib v1.6.0/go.mod h1:wnt6o6EIVEzUfgbUZY7BerzQ2uvzp354qmS2xaLkrhM= github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= github.com/inconshreveable/log15 v3.0.0-testing.3+incompatible h1:zaX5fYT98jX5j4UhO/WbfY8T1HkgVrydiDMC9PWqGCo= github.com/inconshreveable/log15 v3.0.0-testing.3+incompatible/go.mod h1:cOaXtrgN4ScfRrD9Bre7U1thNq5RtJ8ZoP4iXVGRj6o= github.com/inconshreveable/log15/v3 v3.0.0-testing.5 h1:h4e0f3kjgg+RJBlKOabrohjHe47D3bbAB9BgMrc3DYA= github.com/inconshreveable/log15/v3 v3.0.0-testing.5/go.mod h1:3GQg1SVrLoWGfRv/kAZMsdyU5cp8eFc1P3cw+Wwku94= +github.com/jaytaylor/html2text v0.0.0-20230321000545-74c2419ad056 h1:iCHtR9CQyktQ5+f3dMVZfwD2KWJUgm7M0gdL9NGr8KA= +github.com/jaytaylor/html2text v0.0.0-20230321000545-74c2419ad056/go.mod h1:CVKlgaMiht+LXvHG173ujK6JUhZXKb2u/BQtjPDIvyk= github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E= github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc= +github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ= +github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k= github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg= github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo= @@ -148,8 +161,10 @@ github.com/jpillora/backoff v1.0.0 h1:uvFg412JmmHBHw7iwprIxkPMI+sGQ4kzOWsMeHnm2E github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4= github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo= github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= -github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= +github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pretty v0.2.1 h1:Fmg33tUaq4/8ym9TJN1x7sLJnHVwhP33CNkpYV/7rwI= +github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= @@ -157,15 +172,20 @@ github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= -github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA= -github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= +github.com/mattn/go-runewidth v0.0.10 h1:CoZ3S2P7pvtP45xOtBw+/mDL2z0RKI576gSkzRRpdGg= github.com/mattn/go-runewidth v0.0.10/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk= github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 h1:RWengNIwukTxcDr9M+97sNutRR1RKhG96O6jWumTTnw= github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826/go.mod h1:TaXosZuwdSHYgviHp1DAtfrULt5eUgsSMsZf+YrPgl8= +github.com/nbio/st v0.0.0-20140626010706-e9e8d9816f32/go.mod h1:9wM+0iRr9ahx58uYLpLIr5fm8diHn0JbqRycJi6w0Ms= github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE= github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU= +github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec= +github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY= github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk= github.com/onsi/ginkgo v1.14.2/go.mod h1:iSB4RoI2tjJc9BBv4NKIKWKya62Rps+oPG/Lv9klQyY= @@ -187,6 +207,7 @@ github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZN github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= github.com/r3labs/diff/v2 v2.14.2 h1:1HVhQKwg1YnoCWzCYlOWYLG4C3yfTudZo5AcrTSgCTc= github.com/r3labs/diff/v2 v2.14.2/go.mod h1:I8noH9Fc2fjSaMxqF3G2lhDdC0b+JXCfyx85tWFM9kc= +github.com/rivo/uniseg v0.1.0 h1:+2KBaVoUmb9XzDsrx/Ct0W/EYOSFf/nWTauy++DprtY= github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/samber/lo v1.38.1 h1:j2XEAqXKb09Am4ebOg31SpvzUTTs6EN3VfgeLUhPdXM= @@ -199,6 +220,8 @@ github.com/smartystreets/assertions v1.2.0 h1:42S6lae5dvLc7BrLu/0ugRtcFVjoJNMC/N github.com/smartystreets/assertions v1.2.0/go.mod h1:tcbTF8ujkAEcZ8TElKY+i30BzYlVhC/LOxJk7iOWnoo= github.com/smartystreets/goconvey v1.7.2 h1:9RBaZCeXEQ3UselpuwUQHltGVXvdwm6cv1hgR6gDIPg= github.com/smartystreets/goconvey v1.7.2/go.mod h1:Vw0tHAZW6lzCRk3xgdin6fKYcG+G3Pg9vgXWeJpQFMM= +github.com/ssor/bom v0.0.0-20170718123548-6386211fdfcf h1:pvbZ0lM0XWPBqUKqFU8cmavspvIl9nulOYwdy6IFRRo= +github.com/ssor/bom v0.0.0-20170718123548-6386211fdfcf/go.mod h1:RJID2RhlZKId02nZ62WenDCkgHFerpIOmW0iT7GKmXM= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= @@ -209,22 +232,23 @@ github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/ github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= -github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk= github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= github.com/thoas/go-funk v0.9.1 h1:O549iLZqPpTUQ10ykd26sZhzD+rmR5pWhuElrhbC20M= github.com/thoas/go-funk v0.9.1/go.mod h1:+IWnUfUmFO1+WVYQWQtIJHeRRdaIyyYglZN7xzUPe4Q= github.com/tmthrgd/go-hex v0.0.0-20190904060850-447a3041c3bc h1:9lRDQMhESg+zvGYmW5DyG0UqvY96Bu5QYsTLvCHdrgo= github.com/tmthrgd/go-hex v0.0.0-20190904060850-447a3041c3bc/go.mod h1:bciPuU6GHm1iF1pBvUfxfsH0Wmnc2VbpgvbI9ZWuIRs= -github.com/uptrace/bun v1.1.14 h1:S5vvNnjEynJ0CvnrBOD7MIRW7q/WbtvFXrdfy0lddAM= -github.com/uptrace/bun v1.1.14/go.mod h1:RHk6DrIisO62dv10pUOJCz5MphXThuOTpVNYEYv7NI8= +github.com/uptrace/bun v1.1.17 h1:qxBaEIo0hC/8O3O6GrMDKxqyT+mw5/s0Pn/n6xjyGIk= +github.com/uptrace/bun v1.1.17/go.mod h1:hATAzivtTIRsSJR4B8AXR+uABqnQxr3myKDKEf5iQ9U= github.com/urfave/cli/v2 v2.2.0/go.mod h1:SE9GqnLQmjVa0iPEY0f1w3ygNIYcIJ0OKPMoW2caLfQ= github.com/vmihailenco/bufpool v0.1.11 h1:gOq2WmBrq0i2yW5QJ16ykccQ4wH9UyEsgLm6czKAd94= github.com/vmihailenco/bufpool v0.1.11/go.mod h1:AFf/MOy3l2CFTKbxwt0mp2MwnqjNEs5H/UxrkA5jxTQ= github.com/vmihailenco/msgpack v4.0.4+incompatible h1:dSLoQfGFAo3F6OoNhwUmLwVgaUXK79GlxNBwueZn0xI= github.com/vmihailenco/msgpack v4.0.4+incompatible/go.mod h1:fy3FlTQTDXWkZ7Bh6AcGMlsjHatGryHQYUTf1ShIgkk= github.com/vmihailenco/msgpack/v5 v5.3.4/go.mod h1:7xyJ9e+0+9SaZT0Wt1RGleJXzli6Q/V5KbhBonMG9jc= -github.com/vmihailenco/msgpack/v5 v5.3.5 h1:5gO0H1iULLWGhs2H5tbAHIZTV8/cYafcFOr9znI5mJU= -github.com/vmihailenco/msgpack/v5 v5.3.5/go.mod h1:7xyJ9e+0+9SaZT0Wt1RGleJXzli6Q/V5KbhBonMG9jc= +github.com/vmihailenco/msgpack/v5 v5.4.1 h1:cQriyiUvjTwOHg8QZaPihLWeRAAVoCpE00IUPn0Bjt8= +github.com/vmihailenco/msgpack/v5 v5.4.1/go.mod h1:GaZTsDaehaPpQVyxrf5mtQlH+pc21PIudVV/E3rRQok= github.com/vmihailenco/tagparser v0.1.2 h1:gnjoVuB/kljJ5wICEEOpx98oXMWPLj22G67Vbd1qPqc= github.com/vmihailenco/tagparser v0.1.2/go.mod h1:OeAg3pn3UbLjkWt+rN9oFYB6u/cQgqMEUPoW2WPyhdI= github.com/vmihailenco/tagparser/v2 v2.0.0 h1:y09buUbR+b5aycVFQs/g70pqKVZNBmxwAhO7/IwNM9g= @@ -242,8 +266,9 @@ golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACk golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= -golang.org/x/crypto v0.9.0 h1:LF6fAI+IutBocDJ2OT0Q1g8plpYljMZ4+lty+dsqw3g= -golang.org/x/crypto v0.9.0/go.mod h1:yrmDGqONDYtNj3tH8X9dzUun2m2lzPa9ngI6/RUPGR0= +golang.org/x/crypto v0.16.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4= +golang.org/x/crypto v0.22.0 h1:g1v0xeRhjcugydODzvb3mEM9SQ0HGp9s/nh3COQ/C30= +golang.org/x/crypto v0.22.0/go.mod h1:vr6Su+7cTlO45qkww3VDJlzDn0ctJvRgYbC2NvXHt+M= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20220303212507-bbda1eaf7a17 h1:3MTrJm4PyNL9NBqvYDSj3DHl46qQakyfqfWo4jgfaEM= golang.org/x/exp v0.0.0-20220303212507-bbda1eaf7a17/go.mod h1:lgLbSvA5ygNOMpwM/9anMpWVlVJ7Z+cHWq/eFuinpGE= @@ -252,6 +277,7 @@ golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvx golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= +golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= @@ -268,14 +294,18 @@ golang.org/x/net v0.0.0-20210428140749-89ef3d95e781/go.mod h1:OJAsFXCWl8Ukc7SiCT golang.org/x/net v0.0.0-20211029224645-99673261e6eb/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/net v0.1.0/go.mod h1:Cx3nUiGt4eDBEyega/BKRp+/AlGL8hYe7U9odMt2Cco= -golang.org/x/net v0.10.0 h1:X2//UzNDwYmtCLn7To6G58Wr6f5ahEAQgKNzv9Y951M= +golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= +golang.org/x/net v0.19.0/go.mod h1:CfAk/cbD4CthTvqiEl8NpboMuiuOYsAr/7NOjZJtv1U= +golang.org/x/net v0.24.0 h1:1PcaxkF854Fu3+lvBIx5SYn9wRlBzzcnHZSiaFFAb0w= +golang.org/x/net v0.24.0/go.mod h1:2Q7sJY5mzlzWjKtYUEXSlBWCdyaioyXzRB2RtU8KVE8= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= @@ -296,22 +326,30 @@ golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.8.0 h1:EBmGv8NaZBZTWvrbjNoL6HVt+IVy3QDQpJs7VRIw3tU= golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.19.0 h1:q5f1RH2jigJ1MoAWp2KTp3gm5zAGFUTarQZ5U386+4o= +golang.org/x/sys v0.19.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.1.0/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= -golang.org/x/term v0.8.0 h1:n5xxQn2i3PC0yLAbjTpNT85q/Kgzcr2gIoX9OrJUols= +golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= +golang.org/x/term v0.15.0/go.mod h1:BDl952bC7+uMoWR75FIrCDx79TPU9oHkTZ9yRbYOrX0= +golang.org/x/term v0.19.0 h1:+ThwsDv+tYfnJFhF4L8jITxu1tdTWRTZpdsWgEgjL6Q= +golang.org/x/term v0.19.0/go.mod h1:2CuTdWZ7KHSQwUzKva0cbMg6q2DMI3Mmxp+gKJbskEk= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= -golang.org/x/text v0.9.0 h1:2sjJmO8cDvYveuX97RDLsxlyUxLl+GHoLxBiRdHllBE= +golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= +golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= +golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= @@ -321,6 +359,7 @@ golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBn golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= +golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= @@ -354,6 +393,8 @@ gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8 gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= +gopkg.in/h2non/gock.v1 v1.1.2 h1:jBbHXgGBK/AoPVfJh5x4r/WxIrElvbLel8TCZkkZJoY= +gopkg.in/h2non/gock.v1 v1.1.2/go.mod h1:n7UGz/ckNChHiK05rDoiC4MYSunEC/lyaUm2WWaDva0= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= diff --git a/handler_utils/api.go b/handler_utils/api.go index 0c14596501b845e90a2facd030f94ac596b7e2ac..41d45ca5ce061bd90837292450c2935d5fac1cca 100644 --- a/handler_utils/api.go +++ b/handler_utils/api.go @@ -43,6 +43,28 @@ func ValidateAPIEndpoints(endpoints map[string]map[string]interface{}) (map[stri return endpoints, nil } +// ValidateWebsocketEndpoints checks that all websocket endpoints are correctly defined using one of the supported +// handler types and returns updated endpoints with additional information +func ValidateWebsocketEndpoints(endpoints map[string]interface{}) (map[string]interface{}, error) { + for websocketAction, actionFunc := range endpoints { + if websocketAction == "" { + return nil, errors.Errorf("blank action") + } + if actionFunc == nil { + return nil, errors.Errorf("nil handler on %s %s", websocketAction, actionFunc) + } + + handler, err := NewHandler(actionFunc) + if err != nil { + return nil, errors.Wrapf(err, "%s has invalid handler %T", websocketAction, actionFunc) + } + // replace the endpoint value so that we can quickly call this handler + endpoints[websocketAction] = handler + + } + return endpoints, nil +} + func ValidateRequestParams(request *events.APIGatewayProxyRequest, paramsStructType reflect.Type) (reflect.Value, error) { paramValues := map[string]interface{}{} for n, v := range request.QueryStringParameters { diff --git a/logs/logs.go b/logs/logs.go index 6fc4ce57eb9c27ee5a5c22626a625f64b9a1c431..c901a85ca3a37dd569e0e53f385c8e166d5985b9 100644 --- a/logs/logs.go +++ b/logs/logs.go @@ -369,6 +369,24 @@ func LogResponseInfo(req events.APIGatewayProxyRequest, res events.APIGatewayPro InfoWithFields(fields, "Res") } +func LogFullResponseInfo(res events.APIGatewayProxyResponse, err error) { + if disableLogging { + return + } + + fields := map[string]interface{}{ + "status_code": res.StatusCode, + } + + if err != nil { + fields["error"] = err + } + + fields["body"] = res.Body + + InfoWithFields(fields, "Res") +} + func LogApiAudit(fields log.Fields) { if disableLogging { return @@ -409,6 +427,23 @@ func LogSQSEvent(event events.SQSEvent) { }, "") } +func LogWebsocketEvent(req events.APIGatewayWebsocketProxyRequest, shouldExcludeBody bool) { + if disableLogging { + return + } + + fields := map[string]interface{}{ + "route": req.RequestContext.RouteKey, + "connection_id": req.RequestContext.ConnectionID, + } + + if !shouldExcludeBody { + fields["body"] = req.Body + } + + InfoWithFields(fields, "Req") +} + func SetOutput(out io.Writer) { log.SetOutput(out) } diff --git a/redis/redis.go b/redis/redis.go index 86958ed2e67e6e3d5b41a0e9b71add25ae2a88d2..cb305b8801abe4b3df18ecf5d789cfb5ca58396c 100644 --- a/redis/redis.go +++ b/redis/redis.go @@ -27,6 +27,13 @@ type ClientWithHelpers struct { Available bool } +type SetLockKeyOptions struct { + Value *string + MaxRetries *int + RetryInterval *time.Duration + FallbackResult *bool +} + func GetRedisClient(isDebug bool) *ClientWithHelpers { if redisClient != nil && redisClient.IsConnected() { return redisClient @@ -177,14 +184,16 @@ func (r ClientWithHelpers) SetObjectByKeyIndefinitely(key string, object interfa } -func GetObjectByKey[T any](redisClient *ClientWithHelpers, key string, object T) *T { +// GetObjectByKey fetches an object from Redis by key. If the key does not exist or there is an error, it returns nil. +// If an expiry is provided, it will both fetch the key and update its expiry. +func GetObjectByKey[T any](redisClient *ClientWithHelpers, key string, object T, expiry ...time.Duration) *T { // Make sure we have a Redis client, and it is connected if redisClient == nil || !redisClient.IsConnected() { return nil } // Get the object from Redis - jsonString := redisClient.GetValueByKey(key) + jsonString := redisClient.GetValueByKey(key, expiry...) if jsonString == "" { return nil } @@ -198,12 +207,20 @@ func GetObjectByKey[T any](redisClient *ClientWithHelpers, key string, object T) return &object } -func (r ClientWithHelpers) GetValueByKey(key string) string { +// GetValueByKey fetches a value from Redis by key. If the key does not exist or there is an error, it returns an empty +// string. If an expiry is provided, it will both fetch the key and update its expiry. +func (r ClientWithHelpers) GetValueByKey(key string, expiry ...time.Duration) string { if !r.IsConnected() { return "" } - jsonString, err := r.Client.Get(ctx, key).Result() + var jsonString string + var err error + if len(expiry) > 0 { + jsonString, err = r.Client.GetEx(ctx, key, expiry[0]).Result() + } else { + jsonString, err = r.Client.Get(ctx, key).Result() + } if err == redis.Nil { /* Key does not exist */ return "" } else if err != nil { /* Actual error */ @@ -232,3 +249,62 @@ func (r ClientWithHelpers) RateLimit(key string, limitFn func(int) redis_rate.Li return res.Allowed == 1, nil } } + +// SetLockKey attempts to set a lock on the specified key. +func (r ClientWithHelpers) SetLockKey(key string, expiration time.Duration, lockOptions ...SetLockKeyOptions) (success bool, err error) { + fallbackResult := false + if len(lockOptions) > 0 && lockOptions[0].FallbackResult != nil { + fallbackResult = *lockOptions[0].FallbackResult + } + + if !r.IsConnected() { + return fallbackResult, errors.Error("Redis is not connected") + } + + value := "1" + retries := 0 + retryInterval := 100 * time.Millisecond + if len(lockOptions) > 0 { + // Only use the values that were set + if lockOptions[0].Value != nil { + value = *lockOptions[0].Value + } + if lockOptions[0].MaxRetries != nil { + retries = *lockOptions[0].MaxRetries + } + if lockOptions[0].RetryInterval != nil { + retryInterval = *lockOptions[0].RetryInterval + } + } + + for retries >= 0 { + success, err = r.Client.SetNX(ctx, key, value, expiration).Result() + if err != nil { + logs.ErrorWithMsg(fmt.Sprintf("Error setting lock key %s", key), err) + + // Prevent further calls in this execution from trying to connect and also timeout + if strings.HasSuffix(err.Error(), "i/o timeout") { + r.Available = false + } + + return fallbackResult, err + } + + if success || retries == 0 { + break + } + + retries-- + time.Sleep(retryInterval) + } + + return success, nil +} + +func (r ClientWithHelpers) KeepLockKeyAlive(key string, expiration time.Duration) { + if !r.IsConnected() { + return + } + + _ = r.Client.Expire(ctx, key, expiration) +} diff --git a/slice_utils/slice_utils.go b/slice_utils/slice_utils.go index f203d0dc8d0dd698f63a5e53a815a990fb005aff..86ce64e0ca0e8465da1140a3fa836ef1884a091d 100644 --- a/slice_utils/slice_utils.go +++ b/slice_utils/slice_utils.go @@ -2,6 +2,7 @@ package slice_utils import ( "github.com/thoas/go-funk" + "gitlab.bob.co.za/bob-public-utils/bobgroup-go-utils/utils" ) func MinimumFloat64(values []float64) (min float64) { @@ -64,3 +65,22 @@ func ArraySlice(s []any, offset, length int) []any { } return s[offset:] } + +func SliceToSlicePointers[V any](s []V) []*V { + result := make([]*V, len(s)) + for i, v := range s { + vCopy := v // Create a copy to avoid pointer to loop variable issue + result[i] = utils.ValueToPointer(vCopy) + } + return result +} + +func SlicePointersToSlice[V any](s []*V) []V { + result := make([]V, len(s)) + for i, vp := range s { + if vp != nil { + result[i] = utils.PointerToValue(vp) + } + } + return result +} diff --git a/string_utils/string_utils.go b/string_utils/string_utils.go index 9d591700c5dcc907f9f433929d95c68b782eb078..07dd05f75a460e4fc857c1ec80f50ca3cd0de2c3 100644 --- a/string_utils/string_utils.go +++ b/string_utils/string_utils.go @@ -4,6 +4,7 @@ import ( "bytes" "encoding/json" "fmt" + "github.com/jaytaylor/html2text" "github.com/samber/lo" "gitlab.bob.co.za/bob-public-utils/bobgroup-go-utils/number_utils" "regexp" @@ -26,6 +27,48 @@ const ( regexIndexSubmatchEnd = 3 ) +var WhitespaceChars = []string{ + // Standard whitespace characters + "\u0009", // Character tabulation + "\u000A", // Line feed + "\u000B", // Line tabulation + "\u000C", // Form feed + "\u000D", // Carriage return + "\u0020", // Space + "\u0085", // Next line + "\u00A0", // No-break space + "\u1680", // Ogham space mark + "\u2000", // En quad + "\u2001", // Em quad + "\u2002", // En space + "\u2003", // Em space + "\u2004", // Three-per-em space + "\u2005", // Four-per-em space + "\u2006", // Six-per-em space + "\u2007", // Figure space + "\u2008", // Punctuation space + "\u2009", // Thin space + "\u200A", // Hair space + "\u2028", // Line separator + "\u2029", // Paragraph separator + "\u202F", // Narrow no-break space + "\u205F", // Medium mathematical space + "\u3000", // Ideographic space +} + +var NonOfficialWhitespaceChars = []string{ + // Characters with property White_Space=no + "\u180E", // Mongolian vowel separator + "\u200B", // Zero width space + "\u200C", // Zero width non-joiner + "\u200D", // Zero width joiner + "\u200E", // Left-to-right mark + "\u2060", // Word joiner + "\u202C", // Pop directional formatting + "\uFEFF", // Zero width no-break space + "\u00AD", // Soft hyphen +} + var snakeCaseRegex = regexp.MustCompile("^" + snakeCasePattern + "$") func IsSnakeCase(name string) bool { @@ -49,7 +92,11 @@ func ReplaceNonSpacingMarks(str string) string { } func RemoveAllWhiteSpaces(s string) string { - return strings.ReplaceAll(strings.ReplaceAll(s, " ", ""), "\t", "") + cleanedString := strings.ReplaceAll(strings.ReplaceAll(s, " ", ""), "\t", "") + for _, whitespaceChar := range WhitespaceChars { + cleanedString = strings.ReplaceAll(cleanedString, whitespaceChar, "") + } + return cleanedString } func ReplaceCaseInsensitive(string, toReplace, replaceWith string) string { @@ -307,3 +354,32 @@ func TrimSpaceForPointer(s *string) *string { func TrimAndToLower(s string) string { return strings.ToLower(strings.TrimSpace(s)) } + +func StringContainsNumbers(s string) bool { + // Define a regular expression to match numbers + regex := regexp.MustCompile("[0-9]") + + // Use FindString to check if the string contains any numbers + return regex.MatchString(s) +} + +func StringContainsOnlyNumbers(s string) bool { + for _, char := range s { + if !unicode.IsDigit(char) { + return false + } + } + return true +} + +func HTMLStringToTextString(html string) (string, error) { + return html2text.FromString(html, html2text.Options{}) +} + +func HTMLStringToTextBytes(html string) ([]byte, error) { + text, err := HTMLStringToTextString(html) + if err != nil { + return []byte{}, err + } + return []byte(text), nil +} diff --git a/utils/utils.go b/utils/utils.go index f0a27b92ac5730717bdda9fd4fe914e415e2585b..296f43419957df0635a8cd100216fbf6a8e13bdc 100644 --- a/utils/utils.go +++ b/utils/utils.go @@ -2,15 +2,20 @@ package utils import ( "bytes" + emailverifier "github.com/AfterShip/email-verifier" + normalizer "github.com/dimuska139/go-email-normalizer/v2" + "github.com/jinzhu/now" "github.com/mohae/deepcopy" "gitlab.bob.co.za/bob-public-utils/bobgroup-go-utils/errors" "gitlab.bob.co.za/bob-public-utils/bobgroup-go-utils/string_utils" - "net/mail" + "math" + "net" "net/url" "os" "reflect" "regexp" "strings" + "time" ) // GetEnv is a helper function for getting environment variables with a default @@ -54,24 +59,85 @@ func PointerToValue[V any](value *V) V { return *new(V) // zero value of V } -func ValidateEmailAddress(email string) (string, error) { - if email == "" { +func ValidateEmailAddress(email string, options ...map[string]string) (string, error) { + // To lower + cleanedEmail := strings.ToLower(strings.TrimSpace(email)) + + // Remove all whitespaces + cleanedEmail = string_utils.RemoveAllWhiteSpaces(cleanedEmail) + + // Also remove unofficial whitespaces + for _, char := range string_utils.NonOfficialWhitespaceChars { + cleanedEmail = strings.ReplaceAll(cleanedEmail, char, "") + } + + // Strip invalid characters + cleanedEmail = stripInvalidCharacters(cleanedEmail) + + // Make sure the email is not empty + if cleanedEmail == "" { return "", errors.Error("email address is empty") } - cleanEmail := strings.ToLower(strings.TrimSpace(email)) - cleanEmail = string_utils.RemoveAllWhiteSpaces(cleanEmail) + doOnlyParse := false + if options != nil { + if value, exists := options[0]["do_only_parse"]; exists && value == "true" { + doOnlyParse = true + } + } + + // Parse and verify the email + verifier := emailverifier.NewVerifier() - // Remove ZWSP ("\u200B") characters with an empty string to remove it - cleanEmail = strings.ReplaceAll(cleanEmail, "\u200B", "") + if doOnlyParse { + result := verifier.ParseAddress(cleanedEmail) + if !result.Valid { + return cleanedEmail, errors.Error("could not parse email address") + } + } else { + result, err := verifier.Verify(cleanedEmail) + if err != nil || !result.Syntax.Valid { + return cleanedEmail, errors.Wrap(err, "could not parse email address") + } + } - // We validate it but still return it since in some cases we don't want to break everything if the email is bad - _, err := mail.ParseAddress(cleanEmail) - if err != nil { - return cleanEmail, errors.Wrap(err, "could not parse email address") + return cleanedEmail, nil +} + +func stripInvalidCharacters(email string) string { + cleanEmail := email + + // Replace quotes, asterisks, etc. + cleanEmail = strings.ReplaceAll(cleanEmail, "'", "") + cleanEmail = strings.ReplaceAll(cleanEmail, "*", "") + cleanEmail = strings.ReplaceAll(cleanEmail, "!", "") + cleanEmail = strings.ReplaceAll(cleanEmail, "`", "") + + // Trim invalid characters, like underscore, so that it still fails if it's inside the email + cleanEmail = strings.Trim(cleanEmail, "_") + cleanEmail = strings.Trim(cleanEmail, "+") + + return cleanEmail +} + +func SplitAndCleanEmailAddresses(emails string) []string { + var destinationEmails []string + + splitEmails := string_utils.SplitString(emails, []rune{',', ';'}) + if len(splitEmails) >= 1 { + // Success - return these emails + for _, email := range splitEmails { + cleanedEmail, err := ValidateEmailAddress(email) + if err == nil && cleanedEmail != "" { + destinationEmails = append(destinationEmails, cleanedEmail) + } + } + if len(destinationEmails) > 0 { + return destinationEmails + } } - return cleanEmail, nil + return destinationEmails } func StripEmail(email string) (strippedEmail string, strippedDomain string) { @@ -100,6 +166,11 @@ func StripEmail(email string) (strippedEmail string, strippedDomain string) { return strippedEmail, strippedDomain } +func NormalizeEmail(email string) string { + emailNormalizer := normalizer.NewNormalizer() + return emailNormalizer.Normalize(email) +} + // IsUrlStrict Returns whether a URL is valid in a strict way (Must have scheme and host) func IsUrlStrict(str string) bool { u, err := url.Parse(str) @@ -203,3 +274,18 @@ func IsEqual(expected interface{}, actual interface{}) bool { return reflect.DeepEqual(expected, actual) } + +func DetermineDaysLeft(fromDateLocal time.Time, toDateLocal time.Time) int { + fromDate := now.With(fromDateLocal).EndOfDay() + toDate := now.With(toDateLocal).EndOfDay() + return int(math.Floor(toDate.Sub(fromDate).Hours() / 24)) +} + +func ValidateIPAddress(ipAddress string) (cleanedIPAddress string, err error) { + ipAddress = strings.ToLower(strings.TrimSpace(ipAddress)) + ip := net.ParseIP(ipAddress) + if ip == nil { + return "", errors.Error("invalid IP address") + } + return ipAddress, nil +} diff --git a/utils/utils_test.go b/utils/utils_test.go new file mode 100644 index 0000000000000000000000000000000000000000..b1a07699ec800d5f43bea7aef7aef65d843a992d --- /dev/null +++ b/utils/utils_test.go @@ -0,0 +1,148 @@ +package utils + +import ( + "gitlab.bob.co.za/bob-public-utils/bobgroup-go-utils/errors" + "testing" +) + +func TestStripEmail(t *testing.T) { + tests := []struct { + name string + email string + wantStripped string + wantDomain string + }{ + { + name: "Test with + symbol", + email: "test+extra@gmail.com", + wantStripped: "test@gmail.com", + wantDomain: "gmail.com", + }, + { + name: "Test without + symbol", + email: "test@gmail.com", + wantStripped: "test@gmail.com", + wantDomain: "gmail.com", + }, + { + name: "Test with multiple + symbols", + email: "test+extra+more@gmail.com", + wantStripped: "test@gmail.com", + wantDomain: "gmail.com", + }, + { + name: "Test with different domain", + email: "test+extra@yahoo.com", + wantStripped: "test@yahoo.com", + wantDomain: "yahoo.com", + }, + { + name: "Test with subdomain", + email: "test+extra@mail.example.com", + wantStripped: "test@mail.example.com", + wantDomain: "mail.example.com", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + gotStripped, gotDomain := StripEmail(tt.email) + if gotStripped != tt.wantStripped { + t.Errorf("StripEmail() gotStripped = %v, want %v", gotStripped, tt.wantStripped) + } + if gotDomain != tt.wantDomain { + t.Errorf("StripEmail() gotDomain = %v, want %v", gotDomain, tt.wantDomain) + } + }) + } +} + +func TestNormalizeEmail(t *testing.T) { + tests := []struct { + name string + email string + wantNormalized string + }{ + { + name: "Test with + symbol", + email: "test+extra@gmail.com", + wantNormalized: "test@gmail.com", + }, + { + name: "Test without + symbol", + email: "test@gmail.com", + wantNormalized: "test@gmail.com", + }, + { + name: "Test with multiple + symbols", + email: "test+extra+more@gmail.com", + wantNormalized: "test@gmail.com", + }, + { + name: "Test with different domain", + email: "test-extra@yahoo.com", + wantNormalized: "testextra@yahoo.com", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + gotNormalized := NormalizeEmail(tt.email) + if gotNormalized != tt.wantNormalized { + t.Errorf("NormalizeEmail() gotNormalized = %v, want %v", gotNormalized, tt.wantNormalized) + } + }) + } +} + +func TestValidateIPAddress(t *testing.T) { + tests := []struct { + name string + ip string + want string + err error + }{ + { + name: "Test with valid IPv4", + ip: "192.168.1.1", + want: "192.168.1.1", + err: nil, + }, + { + name: "Test with valid IPv6", + ip: "2001:0db8:85a3:0000:0000:8a2e:0370:7334", + want: "2001:0db8:85a3:0000:0000:8a2e:0370:7334", + err: nil, + }, + { + name: "Test with invalid IP", + ip: "999.999.999.999", + want: "", + err: errors.Error("invalid IP address"), + }, + { + name: "Test with empty string", + ip: "", + want: "", + err: errors.Error("invalid IP address"), + }, + { + name: "Test with non-IP string", + ip: "not an ip address", + want: "", + err: errors.Error("invalid IP address"), + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := ValidateIPAddress(tt.ip) + if got != tt.want { + t.Errorf("ValidateIPAddress() got = %v, want %v", got, tt.want) + } + if err != nil && err.Error() != tt.err.Error() { + t.Errorf("ValidateIPAddress() err = %v, want %v", err, tt.err) + } + }) + } +} diff --git a/websocket_utils/websocket_utils.go b/websocket_utils/websocket_utils.go new file mode 100644 index 0000000000000000000000000000000000000000..3a77890ddbeb692af6d7008741ce98c55afa838f --- /dev/null +++ b/websocket_utils/websocket_utils.go @@ -0,0 +1,73 @@ +package websocket_utils + +import ( + "fmt" + "github.com/aws/aws-lambda-go/events" + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/aws/session" + "github.com/aws/aws-sdk-go/service/apigatewaymanagementapi" + "gitlab.bob.co.za/bob-public-utils/bobgroup-go-utils/errors" + "gitlab.bob.co.za/bob-public-utils/bobgroup-go-utils/utils" + "os" +) + +var ( + sessions = map[string]*APIGateWaySessionWithHelpers{} +) + +type APIGateWaySessionWithHelpers struct { + APIGatewaySession *apigatewaymanagementapi.ApiGatewayManagementApi +} + +func GetSession(region ...string) *APIGateWaySessionWithHelpers { + s3Region := os.Getenv("AWS_REGION") + + // Set custom region + if region != nil && len(region) > 0 { + s3Region = region[0] + } + + // Check if session exists for region, if it does return it + if apiGatewaySession, ok := sessions[s3Region]; ok { + return apiGatewaySession + } + + // Setup session + options := session.Options{ + Config: aws.Config{ + Region: utils.ValueToPointer(s3Region), + }, + } + sess, err := session.NewSessionWithOptions(options) + if err != nil { + return nil + } + apiGatewaySession := NewSession(sess) + sessions[s3Region] = apiGatewaySession + return apiGatewaySession +} + +func NewSession(session *session.Session) *APIGateWaySessionWithHelpers { + return &APIGateWaySessionWithHelpers{ + APIGatewaySession: apigatewaymanagementapi.New(session), + } +} + +func (s APIGateWaySessionWithHelpers) PostToConnectionIDs(data []byte, req *events.APIGatewayWebsocketProxyRequest, connectionIDs []string) error { + if req == nil { + return errors.Error("websocket request is nil") + } + + for _, connectionID := range connectionIDs { + s.APIGatewaySession.Endpoint = fmt.Sprintf("https://%s/%s", req.RequestContext.DomainName, req.RequestContext.Stage) + _, err := s.APIGatewaySession.PostToConnection(&apigatewaymanagementapi.PostToConnectionInput{ + ConnectionId: &connectionID, + Data: data, + }) + if err != nil { + return err + } + } + + return nil +} diff --git a/zip_utils/zip_utils.go b/zip_utils/zip_utils.go index 7edc3e6922aad8fa93fc481ebf0b491adcae525f..9cf328a7e79f2ec3125e2d6b5796a7f571a1d9ac 100644 --- a/zip_utils/zip_utils.go +++ b/zip_utils/zip_utils.go @@ -3,6 +3,7 @@ package zip_utils import ( "archive/zip" "bytes" + "gitlab.bob.co.za/bob-public-utils/bobgroup-go-utils/date_utils" "io" "time" ) @@ -71,7 +72,12 @@ func readZipFile(zipFile *zip.File) ([]byte, error) { } func AddFileToZip(fileName string, fileBytes []byte, zipWriter *zip.Writer) error { - file, err := zipWriter.Create(fileName) + header := &zip.FileHeader{ + Name: fileName, + Method: zip.Deflate, + Modified: date_utils.CurrentDate(), + } + file, err := zipWriter.CreateHeader(header) if err != nil { return err }