├── .gitignore ├── LICENSE ├── README.md ├── apperrors ├── base.go ├── invalid_params.go ├── not_found.go ├── permission.go └── server.go ├── auth ├── claims.go ├── controller.go ├── dao.go ├── entities.go ├── gin_middleware.go ├── locale.go ├── otp_service.go ├── otp_service_test.go └── service.go ├── cache ├── errors.go └── memory.go ├── config ├── init.go └── loader.go ├── crud ├── controller.go ├── dao.go ├── middleware.go ├── nested_controller.go └── types.go ├── examples ├── README.md ├── config.go ├── development.yml ├── main.go ├── models.go └── schema.go ├── go.mod ├── go.sum ├── integrations ├── fast2sms │ ├── client.go │ └── entities.go └── twilio │ ├── client.go │ └── entities.go ├── pgdb ├── db.go ├── model.go └── paginate.go └── request ├── binding.go ├── context.go └── crud.go /.gitignore: -------------------------------------------------------------------------------- 1 | # If you prefer the allow list template instead of the deny list, see community template: 2 | # https://github.com/github/gitignore/blob/main/community/Golang/Go.AllowList.gitignore 3 | # 4 | # Binaries for programs and plugins 5 | *.exe 6 | *.exe~ 7 | *.dll 8 | *.so 9 | *.dylib 10 | 11 | # Test binary, built with `go test -c` 12 | *.test 13 | 14 | # Output of the go coverage tool, specifically when used with LiteIDE 15 | *.out 16 | 17 | # Dependency directories (remove the comment below to include it) 18 | # vendor/ 19 | 20 | # Go workspace file 21 | go.work 22 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Nikhil Soni 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # go-rest-kit 2 | Ready made packages for quickly setting up REST APIs in Go. 3 | Specially avoiding to write handlers and request parsing for every new API you want. 4 | Limitation here is most packages are helful if you're using Gin for your APIs. 5 | 6 | ## Motivation 7 | Frameworks like FastAPI for Python are able to provide automatic request parsing and validation based on type hints 8 | and Python insn't even a typed language. I could not find any framework providing similar feature in Go, i.e. not 9 | having to repeat the same parsing and error handling in every API handler. 10 | 11 | I should be able to write following handler / controller similar to FastAPI by just defining `SpecificRequest` and `SpecificResponse` i.e. get request body type as argument and return response type along with error: 12 | ```go 13 | func myHandler(ctx context.Context, request SpecificRequest) (*SpecificResponse, error) {} 14 | ``` 15 | 16 | ## Setup 17 | This module is written with generic usecases in mind, which serves well in most usecase e.g. CRUD APIs. 18 | If you have a custom requirement, it's recommended to clone the repo in your development machine and use the local 19 | version of the module by replacing module path. This allows you extend and customize 20 | the types and methods provided as per your requirement. 21 | 22 | ```bash 23 | go get github.com/krsoninikhil/go-rest-kit 24 | git clone github.com/krsoninikhil/go-rest-kit ../ 25 | go mod edit -replace github.com/krsoninikhil/go-rest-kit=../go-rest-kit 26 | ``` 27 | 28 | ## Example 29 | 30 | ### 1. Exposing CRUD APIs 31 | Exposing simple CRUD APIs is as simple as following 32 | - Ensure your model implements `crud.Model`, you can embed `pgdb.BaseModel` to get default implementation for few methods. 33 | - Define your request and response types and have them implement `crud.Request` and `crud.Response` interfaces. 34 | - Add routes specifying your request, response and model types 35 | 36 | > This example also shows use of request binding methods, and how you can avoid writing repeated code for parsing request body. 37 | 38 | ```go 39 | // models.go 40 | type BusinessType struct { 41 | Name string 42 | Icon string 43 | pgdb.BaseModel 44 | } 45 | 46 | func (b BusinessType) ResourceName() string { return fmt.Sprintf("%T", b) } 47 | 48 | // entities.go 49 | type ( 50 | BusinessTypeRequest struct { 51 | Name string `json:"name" binding:"required"` 52 | Icon string `json:"icon"` 53 | } 54 | BusinessTypeResponse struct { 55 | BusinessTypeRequest 56 | ID int `json:"id"` 57 | } 58 | ) 59 | 60 | // implement `crud.Request` 61 | func (b BusinessTypeRequest) ToModel(_ *gin.Context) BusinessType { 62 | return BusinessType{Name: b.Name, Icon: b.Icon} 63 | } 64 | 65 | // implement `crud.Response` 66 | func (b BusinessTypeResponse) FillFromModel(m models.BusinessType) crud.Response[models.BusinessType] { 67 | return BusinessTypeResponse{ 68 | ID: m.ID, 69 | BusinessTypeRequest: BusinessTypeRequest{Name: m.Name, Icon: m.Icon}, 70 | } 71 | } 72 | func (b BusinessTypeResponse) ItemID() int { return b.ID } 73 | 74 | // setup routes 75 | func main() { 76 | businessTypeDao = crud.Dao[models.BusinessType]{PGDB: db} // use your own doa if you need custom implementation 77 | businessTypeCtlr = crud.Controller[models.BusinessType, types.BusinessTypeResponse, types.BusinessTypeRequest]{ 78 | Svc: &businessTypeDao, // using dao for service as no business logic is required here 79 | } // prewritten controller struct with CRUD methods 80 | 81 | r := gin.Default() 82 | r.GET("/business-types", request.BindGet(businessTypeCtlr.Retrieve)) 83 | r.GET("/business-types", request.BindGet(businessTypeCtlr.List)) 84 | r.POST("/business-types", request.BindCreate(businessTypeCtlr.Create)) 85 | r.PATCH("/business-types", request.BindUpdate(businessTypeCtlr.Update)) 86 | r.DELETE("/business-types", request.BindDelete(businessTypeCtlr.Delete)) 87 | // start your server 88 | } 89 | ``` 90 | 91 | Best part here is -- you can replace any component (controller, usecase or dao) with your own implementation. 92 | 93 | ### 2. Load Your Application Config 94 | Configs are loaded from yaml files where empty values are overriden from environment, which is set using `.env` file. 95 | e.g. if `redis.password` in your yaml is empty, it will be set by `REDIS_PASSWORD` env value. Neat, Hmm? 96 | 97 | ```go 98 | // define application config 99 | type Config struct { 100 | DB pgdb.Config 101 | Twilio twilio.Config 102 | Auth auth.Config 103 | Env string 104 | } 105 | // implement `config.AppConfig` 106 | func (c *Config) EnvPath() string { return "./.env" } // path to your .env file 107 | func (c *Config) SourcePath() string { return fmt.Sprintf("./api/%s.yml", c.Env) } // path to your yaml configuration file 108 | func (c *Config) SetEnv(env string) { c.Env = env } // embed `config.BaseConfig` to avoid defining SetEnv 109 | 110 | func main() { 111 | var ( 112 | ctx = context.Background() 113 | conf Config 114 | ) 115 | config.Load(ctx, &conf) 116 | // that's about it, you use the `conf` now, e.g. see below usage for creating postgres connection 117 | db := pgdb.NewPGConnection(ctx, conf.DB) 118 | } 119 | ``` 120 | 121 | ### 3. Exposing Auth APIs -- Signup, OTP Verification and Token Refresh 122 | > This also shows an example use of `pgdb` and `twillio` packages. 123 | 124 | Assuming you have a `models.User` already defined, exposing auth APIs are just about defining your routes. Easy peazy! 125 | 126 | ```go 127 | func main() { 128 | // reusing gin engine `r`, configuration `conf` and postgres connection `db` from above examples 129 | var ( 130 | cache = cache.NewInMemory() // im memory cache impelementation is provided for the purpose of examples 131 | // injecting dependencies, use your own implementation for any object if default isn't enough for your usecase 132 | userDao = auth.NewUserDao[models.User](db) 133 | authSvc = auth.NewService(conf.Auth, userDao) 134 | smsProvider = twilio.NewClient(conf.Auth.Twilio) 135 | otpSvc = auth.NewOTPSvc(conf.Auth.OTP, smsProvider, cache) // 136 | authController = auth.NewController(authSvc, otpSvc, cache) 137 | ) 138 | 139 | r.POST("/auth/otp/send", request.BindCreate(authController.SendOTP)) 140 | r.POST("/auth/otp/verify", request.BindCreate(authController.VerifyOTP)) 141 | r.POST("/auth/token/refresh", request.BindCreate(authController.RefreshToken)) 142 | r.GET("/countries/:alpha2Code", request.BindGet(authController.CountryInfo)) 143 | // start your server 144 | } 145 | ``` 146 | 147 | ### Getting Rid Of Repeated Code For Request Parsing In Every Handler 148 | If you usecase is not a typical CRUD, don't worry, you can still use the generic binding from `request` package. 149 | While `request.BindGet` would parse only the query string for GET request, `request.BindAll` would parse values 150 | from request uri, body and query based on the tags defined in request struct. 151 | 152 | ```go 153 | func main() { 154 | r := gin.Default() 155 | r.GET("/business-types/insights", request.BindGet(businessTypeInsights)) 156 | r.GET("/business-types/insights-2", request.BindAll(businessTypeInsights)) 157 | } 158 | 159 | type ( 160 | RequestType struct {} 161 | ResponseType struct {} 162 | ) 163 | 164 | func businessTypeInsights(c *gin.Context, req RequestType) (*ResponseType, error) { 165 | // your customer controller 166 | return nil, apperrors.NewServerError(errors.New("not implemented")) 167 | } 168 | 169 | ``` 170 | 171 | See full example for better understanding the implementation and usage [here](./examples/main.go). 172 | 173 | ## Packages Implementation Details 174 | 175 | - `request`: Provides parameter binding based on defined request type, this allows controllers to receive the request parameters and body as a argument and not have to parse and unmarshal the request in every controller. 176 | - `request.WithBinding` takes a fast-api like controller as argument and converts it to a `gin` controller. 177 | - `request.BindGet`, `request.BindCreate`, `request.BindUpdate` and `request.BindDelete` all takes a method and converts it go `gin` controller while providing parsed and validated request body to the function argument. Since these binding methods require the function signature to be defined, it assumes that the `Get` and `Delete` binding expects the argument struct to be parsed from URI and query params while `Update` and `Delete` exepects a struct for parsing URI and another for request body. See example to a better idea of usage. 178 | 179 | - `crud`: Provides controllers for any resource like which request typical CRUD apis. These controller methods follow the signature that can be used directly with above explained `request` package binding methods. CRUD apis for any new model become just about registering these controllers with router. See example. 180 | - `crud.Controller`: Controller for a resource like `GET /resource` 181 | - `crud.NestedController`: Controller for a nested resources like `GET /parent/:parentID/resource` 182 | 183 | - `apperrors`: Provides error that any typical API exposing application will require. Idea is to add more as per your requirement. 184 | 185 | - `config`: Provides quick method to load and parse your config files to the provide struct. See example. 186 | 187 | - `pgdb`: Provides config and constructor to create a new connection. See example. 188 | 189 | - `integrations`: Provides frequently used third party client like Twilio for sending OTPs. 190 | 191 | - `auth`: Almost all backend apps will require API to signup by a mobile no. and respond with JWT token on OTP verification. This also comes with controller for refreshing the tokens. 192 | 193 | -------------------------------------------------------------------------------- /apperrors/base.go: -------------------------------------------------------------------------------- 1 | package apperrors 2 | 3 | type AppError interface { 4 | error 5 | HTTPCode() int 6 | HTTPResponse() map[string]any 7 | } 8 | 9 | type baseError struct { 10 | Cause error 11 | Resource string 12 | } 13 | 14 | func (e baseError) Error() string { return e.Cause.Error() } 15 | 16 | func (e baseError) httpResponse(title string) map[string]any { 17 | return map[string]any{ 18 | "title": title, 19 | "detail": e.Error(), 20 | "entity": e.Resource, 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /apperrors/invalid_params.go: -------------------------------------------------------------------------------- 1 | package apperrors 2 | 3 | import ( 4 | "net/http" 5 | ) 6 | 7 | type InvalidParamsError struct { 8 | baseError 9 | } 10 | 11 | func NewInvalidParamsError(resource string, err error) InvalidParamsError { 12 | return InvalidParamsError{baseError{ 13 | Resource: resource, 14 | Cause: err, 15 | }} 16 | } 17 | 18 | func (e InvalidParamsError) HTTPCode() int { return http.StatusBadRequest } 19 | func (e InvalidParamsError) HTTPResponse() map[string]any { return e.httpResponse("INVALID_PARAM") } 20 | 21 | type ConflictError struct { 22 | baseError 23 | } 24 | 25 | func NewConflictError(resource string, err error) ConflictError { 26 | return ConflictError{baseError{ 27 | Resource: resource, 28 | Cause: err, 29 | }} 30 | } 31 | 32 | func (e ConflictError) HTTPCode() int { return http.StatusConflict } 33 | func (e ConflictError) HTTPResponse() map[string]any { return e.httpResponse("CONFLICT") } 34 | -------------------------------------------------------------------------------- /apperrors/not_found.go: -------------------------------------------------------------------------------- 1 | package apperrors 2 | 3 | import ( 4 | "errors" 5 | "net/http" 6 | ) 7 | 8 | // NotFoundError 9 | type NotFoundError struct { 10 | baseError 11 | } 12 | 13 | func NewNotFoundError(resource string) NotFoundError { 14 | return NotFoundError{baseError{ 15 | Resource: resource, 16 | Cause: errors.New("not found"), 17 | }} 18 | } 19 | 20 | func (e NotFoundError) HTTPCode() int { return http.StatusNotFound } 21 | func (e NotFoundError) HTTPResponse() map[string]any { return e.httpResponse("NOT_FOUND") } 22 | -------------------------------------------------------------------------------- /apperrors/permission.go: -------------------------------------------------------------------------------- 1 | package apperrors 2 | 3 | import ( 4 | "errors" 5 | "net/http" 6 | ) 7 | 8 | // PermissionError 9 | type PermissionError struct { 10 | baseError 11 | } 12 | 13 | func NewPermissionError(resource string) PermissionError { 14 | return PermissionError{baseError{ 15 | Resource: resource, 16 | Cause: errors.New("permission error"), 17 | }} 18 | } 19 | 20 | func (e PermissionError) HTTPCode() int { return http.StatusUnauthorized } 21 | func (e PermissionError) HTTPResponse() map[string]any { return e.httpResponse("PERMISSION_ERROR") } 22 | -------------------------------------------------------------------------------- /apperrors/server.go: -------------------------------------------------------------------------------- 1 | package apperrors 2 | 3 | import ( 4 | "net/http" 5 | ) 6 | 7 | // ServerError 8 | type ServerError struct { 9 | baseError 10 | } 11 | 12 | func NewServerError(err error) ServerError { 13 | return ServerError{baseError{Cause: err}} 14 | } 15 | 16 | func (e ServerError) HTTPCode() int { return http.StatusInternalServerError } 17 | func (e ServerError) HTTPResponse() map[string]any { 18 | res := e.httpResponse("SERVER_ERROR") 19 | res["detail"] = "internal server errror" 20 | return res 21 | } 22 | -------------------------------------------------------------------------------- /auth/claims.go: -------------------------------------------------------------------------------- 1 | package auth 2 | 3 | import ( 4 | "fmt" 5 | "time" 6 | 7 | "github.com/golang-jwt/jwt" 8 | ) 9 | 10 | type claimsSvc struct { 11 | accessTokenValidity time.Duration 12 | refreshTokenValidity time.Duration 13 | signingKey string 14 | } 15 | 16 | func NewStdClaimsSvc(accessTokenValidity, refreshTokenValidity time.Duration, signingKey string) *claimsSvc { 17 | return &claimsSvc{ 18 | accessTokenValidity: accessTokenValidity, 19 | refreshTokenValidity: refreshTokenValidity, 20 | signingKey: signingKey, 21 | } 22 | } 23 | 24 | func (s *claimsSvc) NewAccessTokenClaims(subject string) jwt.Claims { 25 | return jwt.StandardClaims{ 26 | Audience: audienceLogin, 27 | ExpiresAt: time.Now().Add(s.accessTokenValidity).Unix(), 28 | IssuedAt: time.Now().Unix(), 29 | Subject: subject, 30 | } 31 | } 32 | 33 | func (s *claimsSvc) NewRefreshTokenClaims(subject string) jwt.Claims { 34 | return jwt.StandardClaims{ 35 | Audience: audienceRefresh, 36 | ExpiresAt: time.Now().Add(s.refreshTokenValidity).Unix(), 37 | IssuedAt: time.Now().Unix(), 38 | Subject: subject, 39 | } 40 | } 41 | 42 | func (s *claimsSvc) ValidateAccessTokenClaims(claims jwt.Claims) (string, error) { 43 | if claims.Valid() != nil { 44 | return "", fmt.Errorf("expired token") 45 | } 46 | 47 | stdClaims, ok := claims.(*jwt.StandardClaims) 48 | if !ok { 49 | return "", fmt.Errorf("invalid token claims") 50 | } 51 | 52 | if stdClaims.Audience != audienceLogin { 53 | return "", fmt.Errorf("invalid token audience") 54 | } 55 | return stdClaims.Subject, nil 56 | } 57 | 58 | func (s *claimsSvc) ValiateRefreshTokenClaims(claims jwt.Claims) (string, error) { 59 | if claims.Valid() != nil { 60 | return "", fmt.Errorf("expired token") 61 | } 62 | stdClaims, ok := claims.(*jwt.StandardClaims) 63 | if !ok { 64 | return "", fmt.Errorf("invalid token claims") 65 | } 66 | 67 | if stdClaims.Audience != audienceRefresh { 68 | return "", fmt.Errorf("invalid token audience") 69 | } 70 | return stdClaims.Subject, nil 71 | } 72 | 73 | func (s *claimsSvc) VerifyToken(token string) (*jwt.Token, error) { 74 | parsedToken, err := jwt.ParseWithClaims(token, &jwt.StandardClaims{}, func(token *jwt.Token) (interface{}, error) { 75 | if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok { 76 | return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"]) 77 | } 78 | return []byte(s.signingKey), nil 79 | }) 80 | if err != nil { 81 | return nil, fmt.Errorf("failed to parse token: %v", err) 82 | } 83 | 84 | if !parsedToken.Valid { 85 | return nil, fmt.Errorf("expired token") 86 | } 87 | return parsedToken, nil 88 | } 89 | -------------------------------------------------------------------------------- /auth/controller.go: -------------------------------------------------------------------------------- 1 | package auth 2 | 3 | import ( 4 | "context" 5 | "log" 6 | 7 | "github.com/gin-gonic/gin" 8 | ) 9 | 10 | // dependencies 11 | type ( 12 | OAuthProvider interface { 13 | Initiate() 14 | Verify() 15 | } 16 | OTPSvcI interface { 17 | Send(ctx context.Context, phone string) (*OTPStatus, error) 18 | Verify(ctx context.Context, phone, otp string) error 19 | } 20 | AuthGSI interface { 21 | VerifyToken() 22 | } 23 | AuthService interface { 24 | UpsertUser(ctx context.Context, u SigupInfo) (*Token, error) 25 | RefreshToken(ctx context.Context, refreshToken string) (*Token, error) 26 | } 27 | LocalSvc interface { 28 | GetCountryInfo(ctx context.Context, locale string) (*CountryInfoSource, error) 29 | } 30 | ) 31 | 32 | type Controller struct { 33 | authSvc AuthService 34 | otpSvc OTPSvcI 35 | localeSvc LocalSvc 36 | // googleSvc OAuthProvider 37 | // appleSvc OAuthProvider 38 | // googleGSI AuthGSI 39 | } 40 | 41 | func NewController(authSvc AuthService, otpSvc OTPSvcI, cacheClient cacheClient) *Controller { 42 | return &Controller{ 43 | authSvc: authSvc, 44 | otpSvc: otpSvc, 45 | localeSvc: NewLocaleSvc(cacheClient), 46 | } 47 | } 48 | 49 | func (a *Controller) SendOTP(c *gin.Context, r SendOTPRequest) (*SendOTPResponse, error) { 50 | log.Printf("auth: sending otp request=%+v", r) 51 | res, err := a.otpSvc.Send(c, r.Phone) 52 | if err != nil { 53 | return nil, err 54 | } 55 | log.Printf("auth: otp sent successfully") 56 | 57 | return &SendOTPResponse{ 58 | RetryAfter: res.RetryAfter, 59 | AttemptLeft: res.AttemptLeft, 60 | }, nil 61 | } 62 | 63 | func (a *Controller) VerifyOTP(c *gin.Context, r VerifyOTPRequest) (*VerifyOTPResponse, error) { 64 | err := a.otpSvc.Verify(c, r.Phone, r.OTP) 65 | if err != nil { 66 | return nil, err 67 | } 68 | 69 | res, err := a.authSvc.UpsertUser(c, r.toSigupInfo()) 70 | if err != nil { 71 | return nil, err 72 | } 73 | 74 | return &VerifyOTPResponse{ 75 | AccessToken: res.AccessToken, 76 | RefreshToken: res.RefreshToken, 77 | ExpiresIn: res.ExpiresIn, 78 | RefreshExpiresIn: res.RefreshExpiresIn, 79 | }, nil 80 | } 81 | 82 | func (a *Controller) RefreshToken(c *gin.Context, r RefreshTokenRequest) (*VerifyOTPResponse, error) { 83 | res, err := a.authSvc.RefreshToken(c, r.RefreshToken) 84 | if err != nil { 85 | return nil, err 86 | } 87 | 88 | return &VerifyOTPResponse{ 89 | AccessToken: res.AccessToken, 90 | RefreshToken: res.RefreshToken, 91 | ExpiresIn: res.ExpiresIn, 92 | RefreshExpiresIn: res.RefreshExpiresIn, 93 | }, nil 94 | } 95 | 96 | func (a *Controller) CountryInfo(c *gin.Context, r CountryInfoRequest) (*CountryInfoResponse, error) { 97 | country, err := a.localeSvc.GetCountryInfo(c, r.Apha2Code) 98 | if err != nil { 99 | return nil, err 100 | } 101 | 102 | res := CountryInfoResponse(*country) 103 | return &res, nil 104 | } 105 | -------------------------------------------------------------------------------- /auth/dao.go: -------------------------------------------------------------------------------- 1 | package auth 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | 7 | "github.com/krsoninikhil/go-rest-kit/apperrors" 8 | "github.com/krsoninikhil/go-rest-kit/pgdb" 9 | "gorm.io/gorm" 10 | "gorm.io/gorm/clause" 11 | ) 12 | 13 | type UserModel interface { 14 | SetPhone(string) UserModel 15 | SetSignupInfo(SigupInfo) UserModel 16 | PK() int 17 | ResourceName() string 18 | } 19 | 20 | type userDao[U UserModel] struct { 21 | *pgdb.PGDB 22 | } 23 | 24 | func NewUserDao[U UserModel](db *pgdb.PGDB) *userDao[U] { 25 | return &userDao[U]{db} 26 | } 27 | 28 | func (d *userDao[U]) Create(ctx context.Context, u SigupInfo) (int, error) { 29 | var user U 30 | user = user.SetPhone(u.Phone).(U) 31 | user = user.SetSignupInfo(u).(U) 32 | if err := d.PGDB.DB(ctx).Create(&user).Error; err != nil { 33 | return 0, err 34 | } 35 | return user.PK(), nil 36 | } 37 | 38 | // Upsert doesn't return the id if the user already exists 39 | func (d *userDao[U]) Upsert(ctx context.Context, phone string) (int, error) { 40 | var user U 41 | user = user.SetPhone(phone).(U) 42 | err := d.PGDB.DB(ctx).Clauses( 43 | clause.Returning{Columns: []clause.Column{{Name: "id"}}}, 44 | clause.OnConflict{ 45 | Columns: []clause.Column{{Name: "phone"}}, 46 | DoNothing: true, 47 | }, 48 | ).Create(&user).Error 49 | if err != nil { 50 | return 0, err 51 | } 52 | return user.PK(), nil 53 | } 54 | 55 | func (d *userDao[U]) GetByPhone(ctx context.Context, phone string) (int, error) { 56 | var user U 57 | err := d.PGDB.DB(ctx).Where("phone = ?", phone).First(&user).Error 58 | if err != nil { 59 | if errors.Is(err, gorm.ErrRecordNotFound) { 60 | return 0, apperrors.NewNotFoundError(user.ResourceName()) 61 | } 62 | return 0, apperrors.NewServerError(err) 63 | } 64 | return user.PK(), nil 65 | } 66 | -------------------------------------------------------------------------------- /auth/entities.go: -------------------------------------------------------------------------------- 1 | package auth 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/krsoninikhil/go-rest-kit/integrations/fast2sms" 7 | "github.com/krsoninikhil/go-rest-kit/integrations/twilio" 8 | ) 9 | 10 | const ( 11 | CtxKeyTokenClaims string = "tokenClaims" 12 | CtxKeyUserID string = "userID" 13 | audienceLogin string = "login" 14 | audienceRefresh string = "refresh" 15 | 16 | // otp for config.TestPhone to allow app reviews 17 | testOTP = "000000" 18 | ) 19 | 20 | // Config holds the configuration for auth service 21 | type Config struct { 22 | SecretKey string `validate:"required" log:"-"` 23 | AccessTokenValiditySeconds int `validate:"required"` 24 | RefreshTokenValiditySeconds int `validate:"required"` 25 | OTP otpConfig `validate:"required"` 26 | Twilio twilio.Config `validate:"required"` 27 | Fast2SMS fast2sms.Config `validate:"required"` 28 | } 29 | 30 | func (c Config) accessTokenValidity() time.Duration { 31 | return time.Duration(c.AccessTokenValiditySeconds) * time.Second 32 | } 33 | 34 | func (c Config) refreshTokenValidity() time.Duration { 35 | return time.Duration(c.RefreshTokenValiditySeconds) * time.Second 36 | } 37 | 38 | // otpConfig holds the configuration for OTP service 39 | type otpConfig struct { 40 | ValiditySeconds int `validate:"required"` 41 | MaxAttempts int `validate:"required"` 42 | RetryAfterSeconds int `validate:"required"` 43 | Length int `validate:"required"` 44 | TestPhone string 45 | } 46 | 47 | func (c otpConfig) validity() time.Duration { 48 | return time.Duration(c.ValiditySeconds) * time.Second 49 | } 50 | func (c otpConfig) retryAfter() time.Duration { 51 | return time.Duration(c.RetryAfterSeconds) * time.Second 52 | } 53 | 54 | // schema 55 | type ( 56 | SendOTPRequest struct { 57 | Phone string `json:"phone" binding:"required"` 58 | DialCode string `json:"dial_code"` 59 | Country string `json:"country"` 60 | Locale string `json:"locale"` 61 | } 62 | SendOTPResponse struct { 63 | RetryAfter int `json:"retry_after"` 64 | AttemptLeft int `json:"attempt_left"` 65 | } 66 | 67 | VerifyOTPRequest struct { 68 | SendOTPRequest 69 | OTP string `json:"otp" binding:"required,numeric"` 70 | } 71 | VerifyOTPResponse struct { 72 | AccessToken string `json:"access_token"` 73 | RefreshToken string `json:"refresh_token"` 74 | ExpiresIn int64 `json:"expires_in"` 75 | RefreshExpiresIn int64 `json:"refresh_expires_in"` 76 | } 77 | 78 | RefreshTokenRequest struct { 79 | RefreshToken string `json:"refresh_token" binding:"required"` 80 | } 81 | 82 | CountryInfoRequest struct { 83 | Apha2Code string `uri:"alpha2Code" binding:"required"` 84 | } 85 | CountryInfoResponse struct { 86 | Name string `json:"name"` 87 | Nationality string `json:"nationality"` 88 | Code string `json:"code"` 89 | DialCode string `json:"dial_code"` 90 | } 91 | ) 92 | 93 | // dto 94 | type ( 95 | OTPStatus struct { 96 | RetryAfter int 97 | AttemptLeft int 98 | } 99 | Token struct { 100 | AccessToken string 101 | RefreshToken string 102 | ExpiresIn int64 103 | RefreshExpiresIn int64 104 | } 105 | SigupInfo struct { 106 | Phone string 107 | DialCode string 108 | Country string 109 | Locale string 110 | } 111 | CountryInfoSource struct { 112 | Name string `json:"en_short_name"` 113 | Nationality string `json:"nationality"` 114 | Code string `json:"alpha_2_code"` 115 | DialCode string `json:"dial_code"` 116 | } 117 | ) 118 | 119 | func (v *VerifyOTPRequest) toSigupInfo() SigupInfo { 120 | return SigupInfo{ 121 | Phone: v.Phone, 122 | DialCode: v.DialCode, 123 | Country: v.Country, 124 | Locale: v.Locale, 125 | } 126 | } 127 | -------------------------------------------------------------------------------- /auth/gin_middleware.go: -------------------------------------------------------------------------------- 1 | package auth 2 | 3 | import ( 4 | "net/http" 5 | "strconv" 6 | "strings" 7 | "time" 8 | 9 | "github.com/gin-gonic/gin" 10 | "github.com/golang-jwt/jwt" 11 | ) 12 | 13 | type tokenSvc interface { 14 | VerifyToken(token string) (*jwt.Token, error) 15 | ValidateAccessTokenClaims(claims jwt.Claims) (string, error) 16 | } 17 | 18 | func GinStdMiddleware(conf Config) gin.HandlerFunc { 19 | return GinMiddleware(NewStdClaimsSvc( 20 | time.Duration(conf.accessTokenValidity()), 21 | time.Duration(conf.refreshTokenValidity()), 22 | conf.SecretKey, 23 | )) 24 | } 25 | 26 | func GinMiddleware(tokenSvc tokenSvc) gin.HandlerFunc { 27 | return func(c *gin.Context) { 28 | authHeader := c.GetHeader("Authorization") 29 | if authHeader == "" { 30 | c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "Authorization header is missing"}) 31 | return 32 | } 33 | 34 | authHeaderParts := strings.Split(authHeader, " ") 35 | if len(authHeaderParts) != 2 || authHeaderParts[0] != "Bearer" { 36 | c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "Invalid Authorization header format"}) 37 | return 38 | } 39 | 40 | parsedToken, err := tokenSvc.VerifyToken(authHeaderParts[1]) 41 | if err != nil { 42 | c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "Invalid token"}) 43 | return 44 | } 45 | 46 | sub, err := tokenSvc.ValidateAccessTokenClaims(parsedToken.Claims) 47 | if err != nil { 48 | c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": err.Error()}) 49 | return 50 | } 51 | 52 | c.Set(CtxKeyTokenClaims, parsedToken.Claims) 53 | c.Set(CtxKeyUserID, sub) 54 | c.Next() 55 | } 56 | } 57 | 58 | func UserID(c *gin.Context) int { 59 | userID, _ := strconv.Atoi(c.GetString(CtxKeyUserID)) 60 | return userID 61 | } 62 | -------------------------------------------------------------------------------- /auth/locale.go: -------------------------------------------------------------------------------- 1 | package auth 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "strings" 7 | "time" 8 | 9 | "log" 10 | 11 | "github.com/dghubble/sling" 12 | "github.com/krsoninikhil/go-rest-kit/apperrors" 13 | "github.com/pkg/errors" 14 | ) 15 | 16 | const ( 17 | countriesFile = "https://cdn.faithlabs.io/assets/country_list.json" 18 | cacheKeyCountryList = "locale_country_list" 19 | ) 20 | 21 | type localeSvc struct { 22 | cache cacheClient 23 | sling *sling.Sling 24 | } 25 | 26 | func NewLocaleSvc(cache cacheClient) *localeSvc { 27 | return &localeSvc{ 28 | cache: cache, 29 | sling: sling.New(), 30 | } 31 | } 32 | 33 | func (s *localeSvc) GetCountryInfo(ctx context.Context, locale string) (*CountryInfoSource, error) { 34 | val, err := s.cache.Get(cacheKeyCountryList) 35 | countries, ok := val.(map[string]CountryInfoSource) 36 | if !ok || err != nil { 37 | log.Printf("countries not found in cache, downloading ok= %v, err= %v", ok, err) 38 | countries, err = downloadCountriesFile() 39 | if err != nil { 40 | return nil, errors.WithStack(err) 41 | } 42 | if err = s.cache.Set(cacheKeyCountryList, countries, 30*24*time.Hour); err != nil { 43 | log.Printf("error setting countries in cache: %v", err) 44 | } 45 | } 46 | 47 | // countryAlphaCode 48 | country, ok := countries[strings.ToLower(locale)] 49 | if !ok { 50 | return nil, apperrors.NewNotFoundError("locale") 51 | } 52 | return &country, nil 53 | } 54 | 55 | func downloadCountriesFile() (map[string]CountryInfoSource, error) { 56 | var countries []CountryInfoSource 57 | res, err := sling.New().Get(countriesFile).ReceiveSuccess(&countries) 58 | if err != nil { 59 | return nil, apperrors.NewServerError(fmt.Errorf("error connecting to countriesFile url: %v", err)) 60 | } else if res.StatusCode != 200 { 61 | return nil, apperrors.NewServerError(fmt.Errorf("error getting countries: %v", res.Status)) 62 | } 63 | 64 | countryMap := make(map[string]CountryInfoSource) 65 | for _, country := range countries { 66 | countryMap[strings.ToLower(country.Code)] = country 67 | } 68 | return countryMap, nil 69 | } 70 | -------------------------------------------------------------------------------- /auth/otp_service.go: -------------------------------------------------------------------------------- 1 | package auth 2 | 3 | import ( 4 | "context" 5 | "math/rand" 6 | "time" 7 | 8 | "github.com/krsoninikhil/go-rest-kit/apperrors" 9 | "github.com/krsoninikhil/go-rest-kit/cache" 10 | "github.com/pkg/errors" 11 | ) 12 | 13 | // dependencies 14 | type ( 15 | smsProvider interface { 16 | SendSMS(phone, message string) error 17 | } 18 | cacheClient interface { 19 | Set(key string, value any, ttl time.Duration) error 20 | Get(key string) (any, error) 21 | } 22 | ) 23 | 24 | type otpMetaData struct { 25 | OTP string 26 | Attempt int 27 | SentAt time.Time 28 | } 29 | 30 | type otpSvc struct { 31 | config otpConfig 32 | smsProvider smsProvider 33 | cache cacheClient 34 | } 35 | 36 | func NewOTPSvc(config otpConfig, smsProvider smsProvider, cache cacheClient) otpSvc { 37 | return otpSvc{ 38 | config: config, 39 | smsProvider: smsProvider, 40 | cache: cache, 41 | } 42 | } 43 | 44 | func (s otpSvc) Send(ctx context.Context, phone string) (*OTPStatus, error) { 45 | if err := validatePhone(phone); err != nil { 46 | return nil, apperrors.NewInvalidParamsError("phone", err) 47 | } 48 | 49 | attempt := 1 50 | lastOTPMeta, err := s.cache.Get(phone) 51 | if err != nil { 52 | if !errors.Is(err, cache.ErrKeyNotFound) { 53 | return nil, errors.Wrap(err, "unable to get last otp") 54 | } 55 | } else { 56 | lastOTP, ok := lastOTPMeta.(otpMetaData) 57 | if !ok { 58 | return nil, apperrors.NewServerError(errors.New("invalid last otp")) 59 | } 60 | 61 | if lastOTP.Attempt >= s.config.MaxAttempts { 62 | return nil, apperrors.NewInvalidParamsError("otp", errors.New("max attempt reached")) 63 | } 64 | 65 | if time.Since(lastOTP.SentAt) < s.config.retryAfter() { 66 | return nil, apperrors.NewInvalidParamsError("otp", errors.New("retry too soon")) 67 | } 68 | attempt = lastOTP.Attempt + 1 69 | } 70 | 71 | otp := generateOTP(s.config.Length) 72 | if s.config.TestPhone == phone { 73 | otp = testOTP 74 | } else { 75 | if err := s.smsProvider.SendSMS(phone, otp); err != nil { 76 | return nil, errors.Wrap(err, "unable to send otp") 77 | } 78 | } 79 | 80 | otpMeta := otpMetaData{ 81 | OTP: otp, 82 | Attempt: attempt, 83 | SentAt: time.Now(), 84 | } 85 | if err := s.cache.Set(phone, otpMeta, s.config.validity()); err != nil { 86 | return nil, errors.Wrap(err, "unable to set otp") 87 | } 88 | 89 | return &OTPStatus{ 90 | RetryAfter: s.config.RetryAfterSeconds, 91 | AttemptLeft: s.config.MaxAttempts - otpMeta.Attempt, 92 | }, nil 93 | } 94 | 95 | func (s otpSvc) Verify(ctx context.Context, phone, otp string) error { 96 | lastOTPMeta, err := s.cache.Get(phone) 97 | if err != nil { 98 | if errors.Is(err, cache.ErrKeyNotFound) { 99 | return apperrors.NewInvalidParamsError("otp", errors.New("otp not sent or expired")) 100 | } 101 | return errors.Wrap(err, "unable to get last otp") 102 | } 103 | 104 | lastOTP, ok := lastOTPMeta.(otpMetaData) 105 | if !ok { 106 | return apperrors.NewServerError(errors.New("invalid last otp")) 107 | } 108 | 109 | if time.Since(lastOTP.SentAt) >= s.config.validity() { 110 | return apperrors.NewInvalidParamsError("otp", errors.New("otp expired")) 111 | } 112 | 113 | if lastOTP.OTP != otp { 114 | return apperrors.NewInvalidParamsError("otp", errors.New("incorrect otp")) 115 | } 116 | 117 | return nil 118 | } 119 | 120 | var r = rand.New(rand.NewSource(time.Now().UnixNano())) 121 | 122 | func generateOTP(length int) string { 123 | digits := "0123456789" 124 | otp := make([]byte, length) 125 | for i := range otp { 126 | otp[i] = digits[r.Intn(len(digits))] 127 | } 128 | 129 | return string(otp) 130 | } 131 | 132 | func otpMessage(otp string) string { 133 | return "Your OTP is " + otp 134 | } 135 | 136 | func validatePhone(phone string) error { 137 | if len(phone) < 9 || len(phone) > 15 { 138 | return errors.New("invalid phone number") 139 | } 140 | if phone[0] != '+' { 141 | return errors.New("country code is required in phone") 142 | } 143 | return nil 144 | } 145 | -------------------------------------------------------------------------------- /auth/otp_service_test.go: -------------------------------------------------------------------------------- 1 | package auth 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | ) 7 | 8 | func Test_generateOTP(t *testing.T) { 9 | type args struct { 10 | length int 11 | } 12 | tests := []struct { 13 | name string 14 | args args 15 | }{ 16 | {"test for length 4", args{4}}, 17 | {"test for length 6", args{6}}, 18 | {"test for length 6", args{6}}, 19 | } 20 | for _, tt := range tests { 21 | t.Run(tt.name, func(t *testing.T) { 22 | if got := generateOTP(tt.args.length); len(got) != tt.args.length { 23 | t.Errorf("generateOTP() = %v, length want %v", got, tt.args.length) 24 | } else { 25 | fmt.Printf("got: %v\n", got) 26 | } 27 | }) 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /auth/service.go: -------------------------------------------------------------------------------- 1 | package auth 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | 7 | "github.com/golang-jwt/jwt" 8 | "github.com/krsoninikhil/go-rest-kit/apperrors" 9 | ) 10 | 11 | type UserDao interface { 12 | Create(ctx context.Context, u SigupInfo) (userID int, err error) 13 | GetByPhone(ctx context.Context, phone string) (userID int, err error) 14 | } 15 | 16 | type TokenSvc interface { 17 | NewAccessTokenClaims(subject string) jwt.Claims 18 | NewRefreshTokenClaims(subject string) jwt.Claims 19 | VerifyToken(token string) (*jwt.Token, error) 20 | ValidateAccessTokenClaims(claims jwt.Claims) (subject string, err error) 21 | ValiateRefreshTokenClaims(claims jwt.Claims) (subject string, err error) 22 | } 23 | 24 | type Service struct { 25 | config Config 26 | userDao UserDao 27 | tokenSvc TokenSvc 28 | } 29 | 30 | func NewService(config Config, userDao UserDao) *Service { 31 | tokenSvc := NewStdClaimsSvc( 32 | config.accessTokenValidity(), 33 | config.refreshTokenValidity(), 34 | config.SecretKey, 35 | ) 36 | return &Service{ 37 | config: config, 38 | userDao: userDao, 39 | tokenSvc: tokenSvc, 40 | } 41 | } 42 | 43 | func (s *Service) UpsertUser(ctx context.Context, u SigupInfo) (*Token, error) { 44 | userID, err := s.userDao.GetByPhone(ctx, u.Phone) 45 | if err != nil { 46 | if _, ok := err.(apperrors.NotFoundError); ok { 47 | userID, err = s.userDao.Create(ctx, u) 48 | if err != nil { 49 | return nil, apperrors.NewServerError(fmt.Errorf("error creating user: %v", err)) 50 | } 51 | } else { 52 | return nil, apperrors.NewServerError(fmt.Errorf("error getting user: %v", err)) 53 | } 54 | } 55 | 56 | return s.generateToken(fmt.Sprintf("%d", userID)) 57 | } 58 | 59 | func (s *Service) RefreshToken(ctx context.Context, refreshToken string) (*Token, error) { 60 | token, err := s.tokenSvc.VerifyToken(refreshToken) 61 | if err != nil { 62 | return nil, apperrors.NewInvalidParamsError("token", err) 63 | } 64 | 65 | subject, err := s.tokenSvc.ValiateRefreshTokenClaims(token.Claims) 66 | if err != nil { 67 | return nil, apperrors.NewInvalidParamsError("token", err) 68 | } 69 | 70 | return s.generateToken(subject) 71 | } 72 | 73 | func (s *Service) generateToken(subject string) (*Token, error) { 74 | accessClaims := s.tokenSvc.NewAccessTokenClaims(subject) 75 | accessToken, err := generateJWTToken(accessClaims, s.config.SecretKey) 76 | if err != nil { 77 | return nil, fmt.Errorf("unable to generate access token: %v", err) 78 | } 79 | 80 | refreshClaims := s.tokenSvc.NewRefreshTokenClaims(subject) 81 | refreshToken, err := generateJWTToken(refreshClaims, s.config.SecretKey) 82 | if err != nil { 83 | return nil, fmt.Errorf("unable to generate refresh token: %v", err) 84 | } 85 | 86 | return &Token{ 87 | AccessToken: accessToken, 88 | RefreshToken: refreshToken, 89 | ExpiresIn: int64(s.config.accessTokenValidity().Seconds()), 90 | RefreshExpiresIn: int64(s.config.refreshTokenValidity().Seconds()), 91 | }, nil 92 | } 93 | 94 | func generateJWTToken(claims jwt.Claims, secretKey string) (string, error) { 95 | token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) 96 | tokenStr, err := token.SignedString([]byte(secretKey)) 97 | if err != nil { 98 | return "", fmt.Errorf("unable to generate jwt token: %v", err) 99 | } 100 | return tokenStr, nil 101 | } 102 | -------------------------------------------------------------------------------- /cache/errors.go: -------------------------------------------------------------------------------- 1 | package cache 2 | 3 | import "errors" 4 | 5 | var ( 6 | ErrKeyNotFound = errors.New("key not found") 7 | ) 8 | -------------------------------------------------------------------------------- /cache/memory.go: -------------------------------------------------------------------------------- 1 | package cache 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/pkg/errors" 7 | ) 8 | 9 | type InMemory struct { 10 | data map[string]interface{} 11 | validity map[string]time.Time 12 | } 13 | 14 | func NewInMemory() *InMemory { 15 | return &InMemory{ 16 | data: make(map[string]interface{}), 17 | validity: make(map[string]time.Time), 18 | } 19 | } 20 | 21 | func (c *InMemory) Set(key string, value interface{}, ttl time.Duration) error { 22 | c.data[key] = value 23 | c.validity[key] = time.Now().Add(ttl) 24 | return nil 25 | } 26 | 27 | func (c *InMemory) Get(key string) (interface{}, error) { 28 | value, ok := c.data[key] 29 | if !ok || time.Now().After(c.validity[key]) { 30 | return nil, errors.WithStack(ErrKeyNotFound) 31 | } 32 | 33 | return value, nil 34 | } 35 | 36 | func (c *InMemory) Delete(key string) error { 37 | delete(c.data, key) 38 | delete(c.validity, key) 39 | return nil 40 | } 41 | 42 | func (c *InMemory) Clear() error { 43 | c.data = make(map[string]interface{}) 44 | c.validity = make(map[string]time.Time) 45 | return nil 46 | } 47 | 48 | func (c *InMemory) Keys() []string { 49 | keys := make([]string, 0, len(c.data)) 50 | for key := range c.data { 51 | if time.Now().Before(c.validity[key]) { 52 | keys = append(keys, key) 53 | } 54 | } 55 | return keys 56 | } 57 | -------------------------------------------------------------------------------- /config/init.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "context" 5 | "go/build" 6 | "log" 7 | "os" 8 | ) 9 | 10 | func init() { 11 | GetOrSetGoPath() 12 | } 13 | 14 | func GetOrSetGoPath() string { 15 | if custom := os.Getenv("GOPATH"); custom != "" { 16 | return custom 17 | } 18 | if err := os.Setenv("GOPATH", build.Default.GOPATH); err != nil { 19 | log.Fatal(context.Background(), "error setting GOPATH", err) 20 | } 21 | return os.Getenv("GOPATH") 22 | } 23 | -------------------------------------------------------------------------------- /config/loader.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "log" 7 | "strings" 8 | 9 | "github.com/joho/godotenv" 10 | "github.com/spf13/viper" 11 | ) 12 | 13 | type BaseConfig struct { 14 | Env string 15 | } 16 | 17 | func (c *BaseConfig) SetEnv(env string) { c.Env = env } 18 | 19 | type AppConfig interface { 20 | SourcePath() string 21 | EnvPath() string 22 | SetEnv(string) 23 | } 24 | 25 | func Load(ctx context.Context, target AppConfig) { 26 | // override from env 27 | viper.AutomaticEnv() 28 | viper.SetEnvKeyReplacer(strings.NewReplacer(".", "_")) 29 | if err := godotenv.Load(target.EnvPath()); err == nil { 30 | fmt.Print(ctx, "Environment variables set from", target.EnvPath()) 31 | } 32 | 33 | // read from config files 34 | env := viper.GetString("env") 35 | if env == "" { 36 | env = "development" 37 | } 38 | target.SetEnv(strings.ToLower(env)) 39 | viper.SetConfigFile(target.SourcePath()) 40 | if err := viper.MergeInConfig(); err != nil { 41 | log.Fatal(ctx, "error reading environment config file", err) 42 | } 43 | 44 | if err := viper.Unmarshal(target); err != nil { 45 | log.Fatal(ctx, "error parsing config", err) 46 | } 47 | // log.Printf("Loaded config %+v", target) 48 | } 49 | -------------------------------------------------------------------------------- /crud/controller.go: -------------------------------------------------------------------------------- 1 | package crud 2 | 3 | import ( 4 | "errors" 5 | 6 | "github.com/gin-gonic/gin" 7 | "github.com/krsoninikhil/go-rest-kit/apperrors" 8 | "github.com/krsoninikhil/go-rest-kit/auth" 9 | ) 10 | 11 | type ( 12 | Request[M Model] interface { 13 | ToModel(*gin.Context) M 14 | } 15 | Response[M Model] interface { 16 | FillFromModel(m M) Response[M] 17 | ItemID() int 18 | } 19 | 20 | Controller[M Model, S Response[M], R Request[M]] struct { 21 | Svc Service[M] 22 | } 23 | ) 24 | 25 | func (c *Controller[M, S, R]) Create(ctx *gin.Context, req R) (*S, error) { 26 | m := req.ToModel(ctx) 27 | res, err := c.Svc.Create(ctx, m) 28 | if err != nil { 29 | return nil, err 30 | } 31 | 32 | var response S 33 | response, ok := response.FillFromModel(*res).(S) 34 | if !ok { 35 | panic("Invalid implementation of FillFromModel, it should return same type as implementor") 36 | } 37 | return &response, err 38 | } 39 | 40 | func (c *Controller[M, S, R]) Retrieve(ctx *gin.Context, p ResourceParam) (*S, error) { 41 | res, err := c.Svc.Get(ctx, p.ID) 42 | if err != nil { 43 | return nil, err 44 | } 45 | model := *res 46 | 47 | if mc, ok := any(res).(ModelWithCreator); ok && mc.CreatedByID() != auth.UserID(ctx) { 48 | return nil, apperrors.NewPermissionError(model.ResourceName()) 49 | } 50 | 51 | var response S 52 | response, ok := response.FillFromModel(model).(S) 53 | if !ok { 54 | panic("Invalid implementation of FillFromModel, it should return same type as implementor") 55 | } 56 | return &response, err 57 | } 58 | 59 | func (c *Controller[M, S, R]) Update(ctx *gin.Context, p ResourceParam, req R) error { 60 | model := req.ToModel(ctx) 61 | 62 | if mc, ok := any(&model).(ModelWithCreator); ok && mc.CreatedByID() != auth.UserID(ctx) { 63 | return apperrors.NewPermissionError(model.ResourceName()) 64 | } 65 | 66 | _, err := c.Svc.Update(ctx, p.ID, model) 67 | return err 68 | } 69 | 70 | func (c *Controller[M, S, R]) Delete(ctx *gin.Context, p ResourceParam) error { 71 | res, err := c.Svc.Get(ctx, p.ID) 72 | if err != nil { 73 | return err 74 | } 75 | model := *res 76 | 77 | if mc, ok := any(res).(ModelWithCreator); ok && mc.CreatedByID() != auth.UserID(ctx) { 78 | return apperrors.NewPermissionError(model.ResourceName()) 79 | } 80 | return c.Svc.Delete(ctx, p.ID) 81 | } 82 | 83 | func (c *Controller[M, S, R]) List(ctx *gin.Context, p ListParam) (*ListResponse[S], error) { 84 | var pageItem S 85 | if _, ok := any(pageItem).(PageItem); !ok { 86 | return nil, errors.New("list response type must implement PageItem interface") 87 | } 88 | 89 | var ( 90 | model M 91 | creatorID int 92 | ) 93 | if _, ok := any(&model).(ModelWithCreator); ok { 94 | creatorID = auth.UserID(ctx) 95 | } 96 | 97 | items, total, err := c.Svc.List(ctx, p.QueryPage(), creatorID) 98 | if err != nil { 99 | return nil, err 100 | } 101 | 102 | var res []S 103 | for _, item := range items { 104 | var response S 105 | response, ok := response.FillFromModel(item).(S) 106 | if !ok { 107 | panic("invalid implementation of FillFromModel, it should return same type as implementor") 108 | } 109 | res = append(res, response) 110 | } 111 | return &ListResponse[S]{Items: res, Total: total, NextAfter: GetLastItemID(res)}, nil 112 | } 113 | 114 | func GetLastItemID[T PageItem](items []T) int { 115 | var res int 116 | for _, item := range items { 117 | if item.ItemID() > res { 118 | res = item.ItemID() 119 | } 120 | } 121 | return res 122 | } 123 | -------------------------------------------------------------------------------- /crud/dao.go: -------------------------------------------------------------------------------- 1 | package crud 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | 7 | "github.com/krsoninikhil/go-rest-kit/apperrors" 8 | "github.com/krsoninikhil/go-rest-kit/pgdb" 9 | "gorm.io/gorm" 10 | ) 11 | 12 | type Model interface { 13 | IsDeleted() bool 14 | ResourceName() string 15 | PK() int 16 | Joins() []string 17 | } 18 | 19 | type ModelWithCreator interface { 20 | SetCreatedBy(userID int) 21 | CreatedByID() int 22 | } 23 | 24 | type Dao[M Model] struct { 25 | *pgdb.PGDB 26 | } 27 | 28 | func (db *Dao[M]) Create(ctx context.Context, m M) (*M, error) { 29 | if err := db.DB(ctx).Create(&m).Error; errors.Is(err, gorm.ErrDuplicatedKey) { 30 | return nil, apperrors.NewConflictError(m.ResourceName(), err) 31 | } else if err != nil { 32 | return nil, apperrors.NewServerError(err) 33 | } 34 | return &m, nil 35 | } 36 | 37 | func (db *Dao[M]) Update(ctx context.Context, id int, m M) (*M, error) { 38 | if err := db.DB(ctx).Model(&m).Where("id = ?", id).Updates(m).Error; err != nil { 39 | if errors.Is(err, gorm.ErrRecordNotFound) { 40 | return nil, apperrors.NewNotFoundError(m.ResourceName()) 41 | } 42 | return nil, apperrors.NewServerError(err) 43 | } 44 | return &m, nil 45 | } 46 | 47 | func (db *Dao[M]) Get(ctx context.Context, id int) (*M, error) { 48 | var m M 49 | q := db.DB(ctx).Model(m) 50 | for _, joins := range m.Joins() { 51 | q = q.Preload(joins) // Join here would have been better but that wouldn't allow fetch related array fields 52 | } 53 | 54 | tableName := db.TableName(m) 55 | if err := q.Where(tableName+".id = ?", id).First(&m).Error; err != nil { 56 | if errors.Is(err, gorm.ErrRecordNotFound) { 57 | return nil, apperrors.NewNotFoundError(m.ResourceName()) 58 | } 59 | return nil, apperrors.NewServerError(err) 60 | } 61 | return &m, nil 62 | } 63 | 64 | func (db *Dao[M]) Delete(ctx context.Context, id int) error { 65 | var m M 66 | res := db.DB(ctx).Delete(&m, id) 67 | if res.Error != nil { 68 | if errors.Is(res.Error, gorm.ErrRecordNotFound) { 69 | return apperrors.NewNotFoundError(m.ResourceName()) 70 | } 71 | return apperrors.NewServerError(res.Error) 72 | } else if res.RowsAffected == 0 { 73 | return apperrors.NewNotFoundError(m.ResourceName()) 74 | } 75 | return nil 76 | } 77 | 78 | func (db *Dao[M]) List(ctx context.Context, page pgdb.Page, creatorID int) (res []M, total int64, err error) { 79 | var m M 80 | q := db.DB(ctx).Model(m) 81 | 82 | if mc, ok := any(&m).(ModelWithCreator); ok { 83 | mc.SetCreatedBy(creatorID) 84 | q = q.Where(mc) 85 | } else if nm, ok := any(m).(NestedModel[M]); ok { 86 | nm = any(nm.SetParentID(creatorID)).(NestedModel[M]) 87 | q = q.Where(nm) 88 | } 89 | 90 | if err := q.Count(&total).Error; err != nil { 91 | return nil, total, apperrors.NewServerError(err) 92 | } 93 | 94 | tableName := q.Statement.Table 95 | q = q.Scopes(pgdb.Paginate(page, tableName+".id")) 96 | for _, joins := range m.Joins() { 97 | q.Preload(joins) 98 | } 99 | 100 | if err := q.Find(&res).Error; err != nil { 101 | return nil, total, apperrors.NewServerError(err) 102 | } 103 | return 104 | } 105 | 106 | func (db *Dao[M]) BulkCreate(ctx context.Context, m []M) error { 107 | if err := db.DB(ctx).Create(&m).Error; err != nil { 108 | return apperrors.NewServerError(err) 109 | } 110 | return nil 111 | } 112 | -------------------------------------------------------------------------------- /crud/middleware.go: -------------------------------------------------------------------------------- 1 | package crud 2 | 3 | import ( 4 | "context" 5 | "log" 6 | "strconv" 7 | 8 | "github.com/gin-gonic/gin" 9 | "github.com/krsoninikhil/go-rest-kit/apperrors" 10 | "github.com/krsoninikhil/go-rest-kit/auth" 11 | ) 12 | 13 | type ParentDao interface { 14 | GetByUserID(ctx context.Context, userID int) ([]Model, error) 15 | } 16 | 17 | func GinParentVerifier(parentDao ParentDao) gin.HandlerFunc { 18 | return func(c *gin.Context) { 19 | userID, err := strconv.Atoi(c.GetString(auth.CtxKeyUserID)) 20 | if err != nil { 21 | handleError(c, apperrors.NewServerError(err)) 22 | return 23 | } 24 | 25 | parentID, err := strconv.Atoi(c.Param("parentID")) 26 | if err != nil { 27 | handleError(c, apperrors.NewInvalidParamsError("url", err)) 28 | return 29 | } 30 | 31 | parents, err := parentDao.GetByUserID(c, userID) 32 | if err != nil { 33 | handleError(c, apperrors.NewServerError(err)) 34 | return 35 | } 36 | for _, parent := range parents { 37 | if parent.PK() == parentID { 38 | c.Next() 39 | return 40 | } 41 | } 42 | handleError(c, apperrors.NewPermissionError("parent")) 43 | } 44 | } 45 | 46 | func handleError(c *gin.Context, err apperrors.AppError) { 47 | log.Printf("err=%s parentID=%s userID=%s", err.Error(), c.Param("parentID"), c.GetString(auth.CtxKeyUserID)) 48 | c.AbortWithStatusJSON(err.HTTPCode(), err.HTTPResponse()) 49 | } 50 | -------------------------------------------------------------------------------- /crud/nested_controller.go: -------------------------------------------------------------------------------- 1 | package crud 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/gin-gonic/gin" 7 | "github.com/krsoninikhil/go-rest-kit/apperrors" 8 | ) 9 | 10 | type ( 11 | NestedModel[M Model] interface { 12 | Model 13 | ParentID() int 14 | SetParentID(int) M 15 | } 16 | 17 | // NestedResourceParam is used to bind nested uri like /parent/:parentID/child/:id 18 | // first param is expected to be named "parentID" and second as "id" 19 | NestedResourceParam struct { 20 | ID int `uri:"id" binding:"required"` 21 | ParentID int `uri:"parentID" binding:"required"` 22 | } 23 | NestedParam struct { 24 | ParentID int `uri:"parentID" binding:"required"` 25 | ListParam 26 | } 27 | NestedResRequest[M NestedModel[M]] interface { 28 | ToModel(*gin.Context) M 29 | } 30 | NestedBulkCreateRequest[M NestedModel[M], R NestedResRequest[M]] []R 31 | 32 | // NestedController represents crud controller taking model, request and response object 33 | // as type param. Response object dependency could be eliminated if model has a 34 | // method to convert to response 35 | // but that would move the contract definition to model, which is avoided here. 36 | // Path is expected to be in format /parent/:parentID/child/:id with exact param names 37 | NestedController[M NestedModel[M], S Response[M], R NestedResRequest[M]] struct { 38 | Svc Service[M] 39 | } 40 | ) 41 | 42 | func (c *NestedController[M, S, R]) Create(ctx *gin.Context, p NestedParam, req R) (*S, error) { 43 | m := req.ToModel(ctx) 44 | m = m.SetParentID(p.ParentID) 45 | res, err := c.Svc.Create(ctx, m) 46 | if err != nil { 47 | return nil, err 48 | } 49 | 50 | var response S 51 | response, ok := response.FillFromModel(*res).(S) 52 | if !ok { 53 | panic("Invalid implementation of FillFromModel, it should return same type as implementor") 54 | } 55 | return &response, err 56 | } 57 | 58 | func (c *NestedController[M, S, R]) Retrieve(ctx *gin.Context, p NestedResourceParam) (*S, error) { 59 | res, err := c.Svc.Get(ctx, p.ID) 60 | if err != nil { 61 | return nil, err 62 | } 63 | if (*res).ParentID() != p.ParentID { 64 | return nil, apperrors.NewPermissionError((*res).ResourceName()) 65 | } 66 | var response S 67 | response, ok := response.FillFromModel(*res).(S) 68 | fmt.Println("response", p.ID, p.ParentID, res, response) 69 | if !ok { 70 | panic("Invalid implementation of FillFromModel, it should return same type as implementor") 71 | } 72 | return &response, nil 73 | } 74 | 75 | func (c *NestedController[M, S, R]) Update(ctx *gin.Context, p NestedResourceParam, req R) error { 76 | res, err := c.Svc.Get(ctx, p.ID) 77 | if err != nil { 78 | return err 79 | } 80 | if (*res).ParentID() != p.ParentID { 81 | return apperrors.NewPermissionError((*res).ResourceName()) 82 | } 83 | 84 | _, err = c.Svc.Update(ctx, p.ID, req.ToModel(ctx)) 85 | return err 86 | } 87 | 88 | func (c *NestedController[M, S, R]) Delete(ctx *gin.Context, p NestedResourceParam) error { 89 | res, err := c.Svc.Get(ctx, p.ID) 90 | if err != nil { 91 | return err 92 | } 93 | if (*res).ParentID() != p.ParentID { 94 | return apperrors.NewPermissionError((*res).ResourceName()) 95 | } 96 | if err := c.Svc.Delete(ctx, p.ID); err != nil { 97 | return err 98 | } 99 | return nil 100 | } 101 | 102 | func (c *NestedController[M, S, R]) List(ctx *gin.Context, p NestedParam) (*ListResponse[S], error) { 103 | items, total, err := c.Svc.List(ctx, p.QueryPage(), p.ParentID) 104 | if err != nil { 105 | return nil, err 106 | } 107 | var res []S 108 | for _, item := range items { 109 | var response S 110 | response, ok := response.FillFromModel(item).(S) 111 | if !ok { 112 | panic("invalid implementation of FillFromModel, it should return same type as implementor") 113 | } 114 | res = append(res, response) 115 | } 116 | return &ListResponse[S]{Items: res, Total: total, NextAfter: GetLastItemID(res)}, nil 117 | } 118 | 119 | func (c *NestedController[M, S, R]) BulkCreate(ctx *gin.Context, p NestedParam, reqs NestedBulkCreateRequest[M, R]) (*S, error) { 120 | var models = make([]M, len(reqs)) 121 | for i, req := range reqs { 122 | models[i] = req.ToModel(ctx).SetParentID(p.ParentID) 123 | } 124 | if err := c.Svc.BulkCreate(ctx, models); err != nil { 125 | return nil, err 126 | } 127 | return nil, nil 128 | } 129 | -------------------------------------------------------------------------------- /crud/types.go: -------------------------------------------------------------------------------- 1 | package crud 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/krsoninikhil/go-rest-kit/pgdb" 7 | ) 8 | 9 | type Service[M any] interface { 10 | Get(ctx context.Context, id int) (*M, error) 11 | Create(ctx context.Context, m M) (*M, error) 12 | Update(ctx context.Context, id int, m M) (*M, error) 13 | Delete(ctx context.Context, id int) error 14 | List(ctx context.Context, page pgdb.Page, creatorID int) (res []M, total int64, err error) 15 | BulkCreate(ctx context.Context, m []M) error 16 | } 17 | 18 | type PageItem interface { 19 | ItemID() int 20 | } 21 | 22 | type DaoI[M any] Service[M] 23 | 24 | type ( 25 | ListResponse[M any] struct { 26 | Items []M `json:"items"` 27 | Total int64 `json:"total"` 28 | NextAfter int `json:"next_after,omitempty"` 29 | } 30 | 31 | ResourceParam struct { 32 | ID int `uri:"parentID" binding:"required"` 33 | } 34 | 35 | ListParam struct { 36 | After int `form:"after"` 37 | Limit int `form:"limit"` 38 | Page int `form:"page"` 39 | // CreatedAfter *time.Time `form:"created_after"` 40 | // CreatedBefore *time.Time `form:"created_before"` 41 | } 42 | ) 43 | 44 | func (p ListParam) QueryPage() pgdb.Page { 45 | return pgdb.Page{ 46 | After: p.After, 47 | Limit: p.Limit, 48 | Page: p.Page, 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /examples/README.md: -------------------------------------------------------------------------------- 1 | # go-rest-kit example 2 | 3 | This example package adds the rest API for CRUD actions for following types of resources 4 | - Auth: API to sigup using phone and verify OTP and refresh the access tokens 5 | - Independent resource (`BusinessType`): CRUD APIs for a resource which doesn't require any context information 6 | - User context dependent resource (`Business`): CRUD for a resource where authenticated user id is required from context 7 | - Child resource (`Product`): CRUD for a resouce which is nested under another resource and 8 | parent id is extracted from the api path -------------------------------------------------------------------------------- /examples/config.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/krsoninikhil/go-rest-kit/auth" 7 | "github.com/krsoninikhil/go-rest-kit/config" 8 | "github.com/krsoninikhil/go-rest-kit/integrations/twilio" 9 | "github.com/krsoninikhil/go-rest-kit/pgdb" 10 | ) 11 | 12 | // Set environment variables or add a .env with following values 13 | // ENV=development 14 | // DB_PASSWORD=gorestkit 15 | // AUTH_SECRETKEY=secret 16 | // AUTH_TWILIO_AUTHTOKEN=your-twillio-auth-token 17 | 18 | type Config struct { 19 | DB pgdb.Config 20 | Twilio twilio.Config 21 | Auth auth.Config 22 | config.BaseConfig 23 | } 24 | 25 | func (c *Config) EnvPath() string { return "../.env" } 26 | func (c *Config) SourcePath() string { return fmt.Sprintf("./%s.yml", c.Env) } 27 | -------------------------------------------------------------------------------- /examples/development.yml: -------------------------------------------------------------------------------- 1 | # empty values will be set from environment 2 | db: 3 | host: localhost 4 | port: 5432 5 | name: gorestkit 6 | user: gorestkit 7 | password: "" 8 | debugMigrations: false 9 | sslRootCertPath: "" # change this if using sslmode 10 | debug: true 11 | auth: 12 | secretKey: "" 13 | accessTokenValiditySeconds: 90000 14 | refreshTokenValiditySeconds: 900000000 15 | otp: 16 | validitySeconds: 900 17 | length: 6 18 | maxAttempts: 3 19 | retryAfterSeconds: 10 20 | twilio: 21 | accountSid: "Your-Account-SID" # change this 22 | fromNumber: "Phone-Number" # change this 23 | authToken: "" 24 | -------------------------------------------------------------------------------- /examples/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "log" 6 | 7 | "github.com/gin-gonic/gin" 8 | "github.com/krsoninikhil/go-rest-kit/auth" 9 | "github.com/krsoninikhil/go-rest-kit/cache" 10 | "github.com/krsoninikhil/go-rest-kit/config" 11 | "github.com/krsoninikhil/go-rest-kit/crud" 12 | "github.com/krsoninikhil/go-rest-kit/integrations/twilio" 13 | "github.com/krsoninikhil/go-rest-kit/pgdb" 14 | "github.com/krsoninikhil/go-rest-kit/request" 15 | ) 16 | 17 | func main() { 18 | // configuration 19 | var ( 20 | ctx = context.Background() 21 | conf Config 22 | ) 23 | config.Load(ctx, &conf) 24 | 25 | // connnections 26 | var ( 27 | db = pgdb.NewPGConnection(ctx, conf.DB) 28 | cache = cache.NewInMemory() 29 | ) 30 | 31 | // inject dependencies for auth service 32 | var ( 33 | userDao = auth.NewUserDao[User](db) 34 | authSvc = auth.NewService(conf.Auth, userDao) 35 | smsProvider = twilio.NewClient(conf.Auth.Twilio) 36 | otpSvc = auth.NewOTPSvc(conf.Auth.OTP, smsProvider, cache) // 37 | authController = auth.NewController(authSvc, otpSvc, cache) 38 | ) 39 | 40 | // inject dependencies 41 | var ( 42 | businessTypeDao = crud.Dao[BusinessType]{PGDB: db} // use your own doa if you need custom implementation 43 | businessTypeCtlr = crud.Controller[BusinessType, BusinessTypeResponse, BusinessTypeRequest]{ 44 | Svc: &businessTypeDao, // using dao for service as no business logic is required here 45 | } // prewritten controller struct with CRUD methods 46 | 47 | businessDao = crud.Dao[Business]{PGDB: db} 48 | businessCtrl = crud.Controller[Business, BusinessResponse, BusinessRequest]{ 49 | Svc: &businessDao, 50 | } 51 | 52 | // nested resources 53 | productDao = crud.Dao[Product]{PGDB: db} 54 | productController = crud.NestedController[Product, ProductResponse, ProductRequest]{ 55 | Svc: &productDao, 56 | } 57 | ) 58 | 59 | r := gin.Default() 60 | 61 | r.POST("/auth/otp/send", request.BindCreate(authController.SendOTP)) 62 | r.POST("/auth/otp/verify", request.BindCreate(authController.VerifyOTP)) 63 | r.POST("/auth/token/refresh", request.BindCreate(authController.RefreshToken)) 64 | r.GET("/countries/:alpha2Code", request.BindGet(authController.CountryInfo)) 65 | 66 | r.GET("/business-types", request.BindGet(businessTypeCtlr.List)) 67 | r.POST("/business-types", request.BindCreate(businessTypeCtlr.Create)) 68 | r.GET("/business-types/:id", request.BindGet(businessTypeCtlr.Retrieve)) 69 | r.PATCH("/business-types/:id", request.BindUpdate(businessTypeCtlr.Update)) 70 | r.DELETE("/business-types/:id", request.BindDelete(businessTypeCtlr.Delete)) 71 | 72 | // use auth middleware 73 | r.Use(auth.GinStdMiddleware(conf.Auth)) 74 | 75 | r.POST("/business", request.BindCreate(businessCtrl.Create)) 76 | r.GET("/business", request.BindGet(businessCtrl.List)) 77 | r.PATCH("/business/:id", request.BindUpdate(businessCtrl.Update)) 78 | 79 | // nested resources crud, note the `request.BindNestedCreate` for create action 80 | r.POST("/business/:parentID/products", request.BindNestedCreate(productController.Create)) 81 | r.GET("/business/:parentID/products", request.BindGet(productController.List)) 82 | r.PATCH("/business/:parentID/products/:id", request.BindUpdate(productController.Update)) 83 | 84 | // start server 85 | if err := r.Run(":8080"); err != nil { 86 | log.Fatal("could not start server", err) 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /examples/models.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "database/sql" 5 | "fmt" 6 | 7 | "github.com/krsoninikhil/go-rest-kit/auth" 8 | "github.com/krsoninikhil/go-rest-kit/pgdb" 9 | ) 10 | 11 | type User struct { 12 | Name string 13 | Email sql.NullString `gorm:"uniqueIndex"` 14 | Phone string `gorm:"uniqueIndex"` 15 | DialCode string 16 | Country string 17 | Locale string 18 | pgdb.BaseModel 19 | } 20 | 21 | func (u User) ResourceName() string { return "user" } 22 | func (u User) SetPhone(phone string) auth.UserModel { 23 | u.Phone = phone 24 | return u 25 | } 26 | 27 | // SetSignupInfo enables to set any extra fields that you might have as signup step 28 | func (u User) SetSignupInfo(info auth.SigupInfo) auth.UserModel { 29 | u.DialCode = info.DialCode 30 | u.Country = info.Country 31 | u.Locale = info.Locale 32 | return u 33 | } 34 | 35 | // BusinessType is an example model without any user context 36 | type BusinessType struct { 37 | Name string 38 | Icon string 39 | pgdb.BaseModel 40 | } 41 | 42 | func (b BusinessType) ResourceName() string { return fmt.Sprintf("%T", b) } 43 | 44 | // Business is an example model with user context 45 | type Business struct { 46 | Name string 47 | BusinessTypeID int 48 | OwnerID int 49 | pgdb.BaseModel 50 | 51 | Type *BusinessType 52 | Owner *User 53 | } 54 | 55 | func (b Business) ResourceName() string { return "business" } 56 | func (b Business) SetOwnerID(id int) Business { 57 | b.OwnerID = id 58 | return b 59 | } 60 | 61 | // Product is an example model for nested resources where Business is the parent model 62 | type Product struct { 63 | Name string 64 | BusinessID int 65 | pgdb.BaseModel 66 | 67 | Business *Business 68 | } 69 | 70 | func (p Product) ResourceName() string { return "product" } 71 | func (p Product) GetName() string { return p.Name } 72 | func (p Product) ParentID() int { return p.BusinessID } 73 | func (p Product) SetParentID(id int) Product { 74 | p.BusinessID = id 75 | return p 76 | } 77 | -------------------------------------------------------------------------------- /examples/schema.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/gin-gonic/gin" 5 | "github.com/krsoninikhil/go-rest-kit/crud" 6 | ) 7 | 8 | // request and response for business type CRUD 9 | type ( 10 | BusinessTypeRequest struct { 11 | Name string `json:"name" binding:"required"` 12 | Icon string `json:"icon"` 13 | } 14 | BusinessTypeResponse struct { 15 | BusinessTypeRequest 16 | ID int `json:"id"` 17 | } 18 | ) 19 | 20 | // implement `crud.Request` 21 | func (b BusinessTypeRequest) ToModel(_ *gin.Context) BusinessType { 22 | return BusinessType{Name: b.Name, Icon: b.Icon} 23 | } 24 | 25 | // implement `crud.Response` 26 | func (b BusinessTypeResponse) FillFromModel(m BusinessType) crud.Response[BusinessType] { 27 | return BusinessTypeResponse{ 28 | ID: m.ID, 29 | BusinessTypeRequest: BusinessTypeRequest{Name: m.Name, Icon: m.Icon}, 30 | } 31 | } 32 | 33 | func (b BusinessTypeResponse) ItemID() int { return b.ID } 34 | 35 | // request and response for product CRUD 36 | type ( 37 | ProductRequest struct { 38 | Name string `json:"name" binding:"max=100,required"` 39 | } 40 | ProductResponse struct { 41 | ID int `json:"id"` 42 | Name string `json:"name"` 43 | BusinessID int `json:"business_id"` 44 | } 45 | ) 46 | 47 | func (p ProductResponse) ItemID() int { return p.ID } 48 | 49 | func (p ProductRequest) ToModel(_ *gin.Context) Product { 50 | return Product{Name: p.Name} 51 | } 52 | 53 | func (p ProductResponse) FillFromModel(m Product) crud.Response[Product] { 54 | return ProductResponse{ 55 | ID: m.ID, 56 | Name: m.Name, 57 | BusinessID: m.BusinessID, 58 | } 59 | } 60 | 61 | // request and response for Business CRUD 62 | type ( 63 | BusinessRequest struct { 64 | Name string `json:"name" binding:"required"` 65 | TypeID int `json:"type_id" binding:"required"` 66 | } 67 | BusinessResponse struct { 68 | BusinessRequest 69 | ID int `json:"id"` 70 | } 71 | ) 72 | 73 | func (b BusinessRequest) ToModel(_ *gin.Context) Business { 74 | return Business{Name: b.Name, BusinessTypeID: b.TypeID} 75 | } 76 | 77 | func (b BusinessResponse) FillFromModel(m Business) crud.Response[Business] { 78 | return BusinessResponse{ 79 | ID: m.ID, 80 | BusinessRequest: BusinessRequest{Name: m.Name, TypeID: m.BusinessTypeID}, 81 | } 82 | } 83 | 84 | func (b BusinessResponse) ItemID() int { return b.ID } 85 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/krsoninikhil/go-rest-kit 2 | 3 | go 1.22 4 | 5 | require ( 6 | github.com/dghubble/sling v1.4.1 7 | github.com/gin-gonic/gin v1.9.1 8 | github.com/golang-jwt/jwt v3.2.2+incompatible 9 | github.com/joho/godotenv v1.5.1 10 | github.com/pkg/errors v0.9.1 11 | github.com/spf13/viper v1.17.0 12 | gorm.io/driver/postgres v1.5.3 13 | gorm.io/gorm v1.25.5 14 | ) 15 | 16 | require ( 17 | github.com/bytedance/sonic v1.9.1 // indirect 18 | github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 // indirect 19 | github.com/fsnotify/fsnotify v1.6.0 // indirect 20 | github.com/gabriel-vasile/mimetype v1.4.2 // indirect 21 | github.com/gin-contrib/sse v0.1.0 // indirect 22 | github.com/go-playground/locales v0.14.1 // indirect 23 | github.com/go-playground/universal-translator v0.18.1 // indirect 24 | github.com/go-playground/validator/v10 v10.14.0 // indirect 25 | github.com/goccy/go-json v0.10.2 // indirect 26 | github.com/google/go-querystring v1.1.0 // indirect 27 | github.com/hashicorp/hcl v1.0.0 // indirect 28 | github.com/jackc/pgpassfile v1.0.0 // indirect 29 | github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a // indirect 30 | github.com/jackc/pgx/v5 v5.4.3 // indirect 31 | github.com/jinzhu/inflection v1.0.0 // indirect 32 | github.com/jinzhu/now v1.1.5 // indirect 33 | github.com/json-iterator/go v1.1.12 // indirect 34 | github.com/klauspost/cpuid/v2 v2.2.4 // indirect 35 | github.com/leodido/go-urn v1.2.4 // indirect 36 | github.com/magiconair/properties v1.8.7 // indirect 37 | github.com/mattn/go-isatty v0.0.19 // indirect 38 | github.com/mitchellh/mapstructure v1.5.0 // indirect 39 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect 40 | github.com/modern-go/reflect2 v1.0.2 // indirect 41 | github.com/pelletier/go-toml/v2 v2.1.0 // indirect 42 | github.com/sagikazarmark/locafero v0.3.0 // indirect 43 | github.com/sagikazarmark/slog-shim v0.1.0 // indirect 44 | github.com/sourcegraph/conc v0.3.0 // indirect 45 | github.com/spf13/afero v1.10.0 // indirect 46 | github.com/spf13/cast v1.5.1 // indirect 47 | github.com/spf13/pflag v1.0.5 // indirect 48 | github.com/subosito/gotenv v1.6.0 // indirect 49 | github.com/twitchyliquid64/golang-asm v0.15.1 // indirect 50 | github.com/ugorji/go/codec v1.2.11 // indirect 51 | go.uber.org/atomic v1.9.0 // indirect 52 | go.uber.org/multierr v1.9.0 // indirect 53 | golang.org/x/arch v0.3.0 // indirect 54 | golang.org/x/crypto v0.13.0 // indirect 55 | golang.org/x/exp v0.0.0-20230905200255-921286631fa9 // indirect 56 | golang.org/x/net v0.15.0 // indirect 57 | golang.org/x/sys v0.12.0 // indirect 58 | golang.org/x/text v0.13.0 // indirect 59 | google.golang.org/protobuf v1.31.0 // indirect 60 | gopkg.in/ini.v1 v1.67.0 // indirect 61 | gopkg.in/yaml.v3 v3.0.1 // indirect 62 | ) 63 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= 2 | cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= 3 | cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU= 4 | cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU= 5 | cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY= 6 | cloud.google.com/go v0.44.3/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY= 7 | cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc= 8 | cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0= 9 | cloud.google.com/go v0.50.0/go.mod h1:r9sluTvynVuxRIOHXQEHMFffphuXHOMZMycpNR5e6To= 10 | cloud.google.com/go v0.52.0/go.mod h1:pXajvRH/6o3+F9jDHZWQ5PbGhn+o8w9qiu/CffaVdO4= 11 | cloud.google.com/go v0.53.0/go.mod h1:fp/UouUEsRkN6ryDKNW/Upv/JBKnv6WDthjR6+vze6M= 12 | cloud.google.com/go v0.54.0/go.mod h1:1rq2OEkV3YMf6n/9ZvGWI3GWw0VoqH/1x2nd8Is/bPc= 13 | cloud.google.com/go v0.56.0/go.mod h1:jr7tqZxxKOVYizybht9+26Z/gUq7tiRzu+ACVAMbKVk= 14 | cloud.google.com/go v0.57.0/go.mod h1:oXiQ6Rzq3RAkkY7N6t3TcE6jE+CIBBbA36lwQ1JyzZs= 15 | cloud.google.com/go v0.62.0/go.mod h1:jmCYTdRCQuc1PHIIJ/maLInMho30T/Y0M4hTdTShOYc= 16 | cloud.google.com/go v0.65.0/go.mod h1:O5N8zS7uWy9vkA9vayVHs65eM1ubvY4h553ofrNHObY= 17 | cloud.google.com/go v0.72.0/go.mod h1:M+5Vjvlc2wnp6tjzE102Dw08nGShTscUx2nZMufOKPI= 18 | cloud.google.com/go v0.74.0/go.mod h1:VV1xSbzvo+9QJOxLDaJfTjx5e+MePCpCWwvftOeQmWk= 19 | cloud.google.com/go v0.75.0/go.mod h1:VGuuCn7PG0dwsd5XPVm2Mm3wlh3EL55/79EKB6hlPTY= 20 | cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o= 21 | cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE= 22 | cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc= 23 | cloud.google.com/go/bigquery v1.5.0/go.mod h1:snEHRnqQbz117VIFhE8bmtwIDY80NLUZUMb4Nv6dBIg= 24 | cloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4gLoIoXIAPc= 25 | cloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ= 26 | cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE= 27 | cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk= 28 | cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I= 29 | cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw= 30 | cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA= 31 | cloud.google.com/go/pubsub v1.3.1/go.mod h1:i+ucay31+CNRpDW4Lu78I4xXG+O1r/MAHgjpRVR+TSU= 32 | cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw= 33 | cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos= 34 | cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk= 35 | cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs= 36 | cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0= 37 | cloud.google.com/go/storage v1.14.0/go.mod h1:GrKmX003DSIwi9o29oFT7YDnHYwZoctc3fOKtUw0Xmo= 38 | dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= 39 | github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= 40 | github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= 41 | github.com/bytedance/sonic v1.5.0/go.mod h1:ED5hyg4y6t3/9Ku1R6dU/4KyJ48DZ4jPhfY1O2AihPM= 42 | github.com/bytedance/sonic v1.9.1 h1:6iJ6NqdoxCDr6mbY8h18oSO+cShGSMRGCEo7F2h0x8s= 43 | github.com/bytedance/sonic v1.9.1/go.mod h1:i736AoUSYt75HyZLoJW9ERYxcy6eaN6h4BZXU064P/U= 44 | github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= 45 | github.com/chenzhuoyu/base64x v0.0.0-20211019084208-fb5309c8db06/go.mod h1:DH46F32mSOjUmXrMHnKwZdA8wcEefY7UVqBKYGjpdQY= 46 | github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 h1:qSGYFH7+jGhDF8vLC+iwCD4WpbV1EBDSzWkJODFLams= 47 | github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311/go.mod h1:b583jCggY9gE99b6G5LEC39OIiVsWj+R97kbl5odCEk= 48 | github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= 49 | github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= 50 | github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= 51 | github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= 52 | github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= 53 | github.com/cncf/udpa/go v0.0.0-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= 54 | github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= 55 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 56 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 57 | github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= 58 | github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 59 | github.com/dghubble/sling v1.4.1 h1:AxjTubpVyozMvbBCtXcsWEyGGgUZutC5YGrfxPNVOcQ= 60 | github.com/dghubble/sling v1.4.1/go.mod h1:QoMB1KL3GAo+7HsD8Itd6S+6tW91who8BGZzuLvpOyc= 61 | github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= 62 | github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= 63 | github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= 64 | github.com/envoyproxy/go-control-plane v0.9.7/go.mod h1:cwu0lG7PUMfa9snN8LXBig5ynNVH9qI8YYLbd1fK2po= 65 | github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= 66 | github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= 67 | github.com/frankban/quicktest v1.14.4 h1:g2rn0vABPOOXmZUj+vbmUp0lPoXEMuhTpIluN0XL9UY= 68 | github.com/frankban/quicktest v1.14.4/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= 69 | github.com/fsnotify/fsnotify v1.6.0 h1:n+5WquG0fcWoWp6xPWfHdbskMCQaFnG6PfBrh1Ky4HY= 70 | github.com/fsnotify/fsnotify v1.6.0/go.mod h1:sl3t1tCWJFWoRz9R8WJCbQihKKwmorjAbSClcnxKAGw= 71 | github.com/gabriel-vasile/mimetype v1.4.2 h1:w5qFW6JKBz9Y393Y4q372O9A7cUSequkh1Q7OhCmWKU= 72 | github.com/gabriel-vasile/mimetype v1.4.2/go.mod h1:zApsH/mKG4w07erKIaJPFiX0Tsq9BFQgN3qGY5GnNgA= 73 | github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE= 74 | github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI= 75 | github.com/gin-gonic/gin v1.9.1 h1:4idEAncQnU5cB7BeOkPtxjfCSye0AAm1R0RVIqJ+Jmg= 76 | github.com/gin-gonic/gin v1.9.1/go.mod h1:hPrL7YrpYKXt5YId3A/Tnip5kqbEAP+KLuI3SUcPTeU= 77 | github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= 78 | github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= 79 | github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= 80 | github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= 81 | github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= 82 | github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= 83 | github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= 84 | github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= 85 | github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= 86 | github.com/go-playground/validator/v10 v10.14.0 h1:vgvQWe3XCz3gIeFDm/HnTIbj6UGmg/+t63MyGU2n5js= 87 | github.com/go-playground/validator/v10 v10.14.0/go.mod h1:9iXMNT7sEkjXb0I+enO7QXmzG6QCsPWY4zveKFVRSyU= 88 | github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU= 89 | github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= 90 | github.com/golang-jwt/jwt v3.2.2+incompatible h1:IfV12K8xAKAnZqdXVzCZ+TOjboZ2keLg81eXfW3O+oY= 91 | github.com/golang-jwt/jwt v3.2.2+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I= 92 | github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= 93 | github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= 94 | github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= 95 | github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= 96 | github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= 97 | github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= 98 | github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y= 99 | github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= 100 | github.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= 101 | github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= 102 | github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4= 103 | github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 104 | github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 105 | github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 106 | github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= 107 | github.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= 108 | github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk= 109 | github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= 110 | github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= 111 | github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= 112 | github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= 113 | github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= 114 | github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= 115 | github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= 116 | github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= 117 | github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= 118 | github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= 119 | github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= 120 | github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= 121 | github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= 122 | github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= 123 | github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 124 | github.com/google/go-cmp v0.4.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 125 | github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 126 | github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 127 | github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 128 | github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 129 | github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 130 | github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= 131 | github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= 132 | github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8= 133 | github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU= 134 | github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= 135 | github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= 136 | github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= 137 | github.com/google/martian/v3 v3.1.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= 138 | github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= 139 | github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= 140 | github.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= 141 | github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= 142 | github.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= 143 | github.com/google/pprof v0.0.0-20200430221834-fc25d7d30c6d/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= 144 | github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= 145 | github.com/google/pprof v0.0.0-20201023163331-3e6fc7fc9c4c/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= 146 | github.com/google/pprof v0.0.0-20201203190320-1bf35d6f28c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= 147 | github.com/google/pprof v0.0.0-20201218002935-b9804c9f04c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= 148 | github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= 149 | github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 150 | github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= 151 | github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= 152 | github.com/googleapis/google-cloud-go-testing v0.0.0-20200911160855-bcd43fbb19e8/go.mod h1:dvDLG8qkwmyD9a/MJJN3XJcT3xFxOKAvTZGvuZmac9g= 153 | github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= 154 | github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= 155 | github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= 156 | github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= 157 | github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= 158 | github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= 159 | github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM= 160 | github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= 161 | github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a h1:bbPeKD0xmW/Y25WS6cokEszi5g+S0QxI/d45PkRi7Nk= 162 | github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM= 163 | github.com/jackc/pgx/v5 v5.4.3 h1:cxFyXhxlvAifxnkKKdlxv8XqUf59tDlYjnV5YYfsJJY= 164 | github.com/jackc/pgx/v5 v5.4.3/go.mod h1:Ig06C2Vu0t5qXC60W8sqIthScaEnFvojjj9dSljmHRA= 165 | github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E= 166 | github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc= 167 | github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ= 168 | github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= 169 | github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= 170 | github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= 171 | github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= 172 | github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= 173 | github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= 174 | github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk= 175 | github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= 176 | github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= 177 | github.com/klauspost/cpuid/v2 v2.2.4 h1:acbojRNwl3o09bUq+yDCtZFc1aiwaAAxtcn8YkZXnvk= 178 | github.com/klauspost/cpuid/v2 v2.2.4/go.mod h1:RVVoqg1df56z8g3pUjL/3lE5UfnlrJX8tyFgg4nqhuY= 179 | github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg= 180 | github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= 181 | github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= 182 | github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= 183 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 184 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 185 | github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= 186 | github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= 187 | github.com/leodido/go-urn v1.2.4 h1:XlAE/cm/ms7TE/VMVoduSpNBoyc2dOxHs5MZSwAN63Q= 188 | github.com/leodido/go-urn v1.2.4/go.mod h1:7ZrI8mTSeBSHl/UaRyKQW1qZeMgak41ANeCNaVckg+4= 189 | github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY= 190 | github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0= 191 | github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA= 192 | github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= 193 | github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= 194 | github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= 195 | github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 196 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= 197 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 198 | github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= 199 | github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= 200 | github.com/pelletier/go-toml/v2 v2.1.0 h1:FnwAJ4oYMvbT/34k9zzHuZNrhlz48GB3/s6at6/MHO4= 201 | github.com/pelletier/go-toml/v2 v2.1.0/go.mod h1:tJU2Z3ZkXwnxa4DPO899bsyIoywizdUvyaeZurnPPDc= 202 | github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= 203 | github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 204 | github.com/pkg/sftp v1.13.1/go.mod h1:3HaPG6Dq1ILlpPZRO0HVMrsydcdLt6HRDccSgb87qRg= 205 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 206 | github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= 207 | github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 208 | github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= 209 | github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= 210 | github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8= 211 | github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= 212 | github.com/sagikazarmark/locafero v0.3.0 h1:zT7VEGWC2DTflmccN/5T1etyKvxSxpHsjb9cJvm4SvQ= 213 | github.com/sagikazarmark/locafero v0.3.0/go.mod h1:w+v7UsPNFwzF1cHuOajOOzoq4U7v/ig1mpRjqV+Bu1U= 214 | github.com/sagikazarmark/slog-shim v0.1.0 h1:diDBnUNK9N/354PgrxMywXnAwEr1QZcOr6gto+ugjYE= 215 | github.com/sagikazarmark/slog-shim v0.1.0/go.mod h1:SrcSrq8aKtyuqEI1uvTDTK1arOWRIczQRv+GVI1AkeQ= 216 | github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo= 217 | github.com/sourcegraph/conc v0.3.0/go.mod h1:Sdozi7LEKbFPqYX2/J+iBAM6HpqSLTASQIKqDmF7Mt0= 218 | github.com/spf13/afero v1.10.0 h1:EaGW2JJh15aKOejeuJ+wpFSHnbd7GE6Wvp3TsNhb6LY= 219 | github.com/spf13/afero v1.10.0/go.mod h1:UBogFpq8E9Hx+xc5CNTTEpTnuHVmXDwZcZcE1eb/UhQ= 220 | github.com/spf13/cast v1.5.1 h1:R+kOtfhWQE6TVQzY+4D7wJLBgkdVasCEFxSUBYBYIlA= 221 | github.com/spf13/cast v1.5.1/go.mod h1:b9PdjNptOpzXr7Rq1q9gJML/2cdGQAo69NKzQ10KN48= 222 | github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= 223 | github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= 224 | github.com/spf13/viper v1.17.0 h1:I5txKw7MJasPL/BrfkbA0Jyo/oELqVmux4pR/UxOMfI= 225 | github.com/spf13/viper v1.17.0/go.mod h1:BmMMMLQXSbcHK6KAOiFLz0l5JHrU89OdIRHvsk0+yVI= 226 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 227 | github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= 228 | github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= 229 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 230 | github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= 231 | github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= 232 | github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 233 | github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 234 | github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= 235 | github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= 236 | github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= 237 | github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= 238 | github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= 239 | github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8= 240 | github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU= 241 | github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI= 242 | github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= 243 | github.com/ugorji/go/codec v1.2.11 h1:BMaWp1Bb6fHwEtbplGBGJ498wD+LKlNSl25MjdZY4dU= 244 | github.com/ugorji/go/codec v1.2.11/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg= 245 | github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= 246 | github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= 247 | github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= 248 | github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= 249 | go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= 250 | go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= 251 | go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= 252 | go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= 253 | go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= 254 | go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk= 255 | go.uber.org/atomic v1.9.0 h1:ECmE8Bn/WFTYwEW/bpKD3M8VtR/zQVbavAoalC1PYyE= 256 | go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= 257 | go.uber.org/multierr v1.9.0 h1:7fIwc/ZtS0q++VgcfqFDxSBZVv/Xo49/SYnDFupUwlI= 258 | go.uber.org/multierr v1.9.0/go.mod h1:X2jQV1h+kxSjClGpnseKVIxpmcjrj7MNnI0bnlfKTVQ= 259 | golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8= 260 | golang.org/x/arch v0.3.0 h1:02VY4/ZcO/gBOH6PUaoiptASxtXU10jazRCP865E97k= 261 | golang.org/x/arch v0.3.0/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8= 262 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 263 | golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 264 | golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 265 | golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 266 | golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= 267 | golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4= 268 | golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= 269 | golang.org/x/crypto v0.13.0 h1:mvySKfSWJ+UKUii46M40LOvyWfN0s2U+46/jDd0e6Ck= 270 | golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc= 271 | golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= 272 | golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= 273 | golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= 274 | golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek= 275 | golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY= 276 | golang.org/x/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= 277 | golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= 278 | golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= 279 | golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM= 280 | golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU= 281 | golang.org/x/exp v0.0.0-20230905200255-921286631fa9 h1:GoHiUyI/Tp2nVkLI2mCxVkOjsbSXD66ic0XW0js0R9g= 282 | golang.org/x/exp v0.0.0-20230905200255-921286631fa9/go.mod h1:S2oDrQGGwySpoQPVqRShND87VCbxmc6bL1Yd2oYrm6k= 283 | golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= 284 | golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= 285 | golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= 286 | golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= 287 | golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= 288 | golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= 289 | golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= 290 | golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= 291 | golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= 292 | golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs= 293 | golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= 294 | golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= 295 | golang.org/x/lint v0.0.0-20201208152925-83fdc39ff7b5/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= 296 | golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE= 297 | golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o= 298 | golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= 299 | golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY= 300 | golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= 301 | golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= 302 | golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 303 | golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 304 | golang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 305 | golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 306 | golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 307 | golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 308 | golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 309 | golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 310 | golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 311 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 312 | golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 313 | golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 314 | golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= 315 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 316 | golang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 317 | golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 318 | golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 319 | golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 320 | golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 321 | golang.org/x/net v0.0.0-20200222125558-5a598a2470a0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 322 | golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 323 | golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 324 | golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= 325 | golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= 326 | golang.org/x/net v0.0.0-20200506145744-7e3656a0809f/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= 327 | golang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= 328 | golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= 329 | golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= 330 | golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= 331 | golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= 332 | golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= 333 | golang.org/x/net v0.0.0-20201031054903-ff519b6c9102/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= 334 | golang.org/x/net v0.0.0-20201209123823-ac852fbbde11/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= 335 | golang.org/x/net v0.0.0-20201224014010-6772e930b67b/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= 336 | golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= 337 | golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= 338 | golang.org/x/net v0.15.0 h1:ugBLEUaxABaB5AJqW9enI0ACdci2RUd4eP51NTBvuJ8= 339 | golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk= 340 | golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= 341 | golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= 342 | golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= 343 | golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= 344 | golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= 345 | golang.org/x/oauth2 v0.0.0-20200902213428-5d25da1a8d43/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= 346 | golang.org/x/oauth2 v0.0.0-20201109201403-9fd604954f58/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= 347 | golang.org/x/oauth2 v0.0.0-20201208152858-08078c50e5b5/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= 348 | golang.org/x/oauth2 v0.0.0-20210218202405-ba52d332ba99/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= 349 | golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 350 | golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 351 | golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 352 | golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 353 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 354 | golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 355 | golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 356 | golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 357 | golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 358 | golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 359 | golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 360 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 361 | golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 362 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 363 | golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 364 | golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 365 | golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 366 | golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 367 | golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 368 | golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 369 | golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 370 | golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 371 | golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 372 | golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 373 | golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 374 | golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 375 | golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 376 | golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 377 | golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 378 | golang.org/x/sys v0.0.0-20200331124033-c3d80250170d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 379 | golang.org/x/sys v0.0.0-20200501052902-10377860bb8e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 380 | golang.org/x/sys v0.0.0-20200511232937-7e40ca221e25/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 381 | golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 382 | golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 383 | golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 384 | golang.org/x/sys v0.0.0-20200905004654-be1d3432aa8f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 385 | golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 386 | golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 387 | golang.org/x/sys v0.0.0-20201201145000-ef89a241ccb3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 388 | golang.org/x/sys v0.0.0-20210104204734-6f8348627aad/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 389 | golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 390 | golang.org/x/sys v0.0.0-20210225134936-a50acf3fe073/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 391 | golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 392 | golang.org/x/sys v0.0.0-20210423185535-09eb48e85fd7/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 393 | golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 394 | golang.org/x/sys v0.0.0-20220704084225-05e143d24a9e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 395 | golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 396 | golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 397 | golang.org/x/sys v0.12.0 h1:CM0HF96J0hcLAwsHPJZjfdNzs0gftsLfgKt57wWHJ0o= 398 | golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 399 | golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= 400 | golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 401 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 402 | golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 403 | golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= 404 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 405 | golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 406 | golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 407 | golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= 408 | golang.org/x/text v0.13.0 h1:ablQoSUd0tRdKxZewP80B+BaqeKJuVhuRxj/dkrun3k= 409 | golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= 410 | golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= 411 | golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= 412 | golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= 413 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 414 | golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 415 | golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= 416 | golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= 417 | golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= 418 | golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= 419 | golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= 420 | golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= 421 | golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= 422 | golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= 423 | golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= 424 | golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= 425 | golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 426 | golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 427 | golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 428 | golang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 429 | golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 430 | golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 431 | golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 432 | golang.org/x/tools v0.0.0-20191130070609-6e064ea0cf2d/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 433 | golang.org/x/tools v0.0.0-20191216173652-a0e659d51361/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= 434 | golang.org/x/tools v0.0.0-20191227053925-7b8e75db28f4/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= 435 | golang.org/x/tools v0.0.0-20200117161641-43d50277825c/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= 436 | golang.org/x/tools v0.0.0-20200122220014-bf1340f18c4a/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= 437 | golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= 438 | golang.org/x/tools v0.0.0-20200204074204-1cc6d1ef6c74/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= 439 | golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= 440 | golang.org/x/tools v0.0.0-20200212150539-ea181f53ac56/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= 441 | golang.org/x/tools v0.0.0-20200224181240-023911ca70b2/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= 442 | golang.org/x/tools v0.0.0-20200227222343-706bc42d1f0d/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= 443 | golang.org/x/tools v0.0.0-20200304193943-95d2e580d8eb/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= 444 | golang.org/x/tools v0.0.0-20200312045724-11d5b4c81c7d/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= 445 | golang.org/x/tools v0.0.0-20200331025713-a30bf2db82d4/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8= 446 | golang.org/x/tools v0.0.0-20200501065659-ab2804fb9c9d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= 447 | golang.org/x/tools v0.0.0-20200512131952-2bc93b1c0c88/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= 448 | golang.org/x/tools v0.0.0-20200515010526-7d3b6ebf133d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= 449 | golang.org/x/tools v0.0.0-20200618134242-20370b0cb4b2/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= 450 | golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= 451 | golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= 452 | golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= 453 | golang.org/x/tools v0.0.0-20200904185747-39188db58858/go.mod h1:Cj7w3i3Rnn0Xh82ur9kSqwfTHTeVxaDqrfMjpcNT6bE= 454 | golang.org/x/tools v0.0.0-20201110124207-079ba7bd75cd/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= 455 | golang.org/x/tools v0.0.0-20201201161351-ac6f37ff4c2a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= 456 | golang.org/x/tools v0.0.0-20201208233053-a543418bbed2/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= 457 | golang.org/x/tools v0.0.0-20210105154028-b0ab187a4818/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= 458 | golang.org/x/tools v0.0.0-20210108195828-e2f9c7f1fc8e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= 459 | golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0= 460 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 461 | golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 462 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 463 | golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 464 | google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= 465 | google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M= 466 | google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= 467 | google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= 468 | google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= 469 | google.golang.org/api v0.14.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= 470 | google.golang.org/api v0.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= 471 | google.golang.org/api v0.17.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= 472 | google.golang.org/api v0.18.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= 473 | google.golang.org/api v0.19.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= 474 | google.golang.org/api v0.20.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= 475 | google.golang.org/api v0.22.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= 476 | google.golang.org/api v0.24.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= 477 | google.golang.org/api v0.28.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= 478 | google.golang.org/api v0.29.0/go.mod h1:Lcubydp8VUV7KeIHD9z2Bys/sm/vGKnG1UHuDBSrHWM= 479 | google.golang.org/api v0.30.0/go.mod h1:QGmEvQ87FHZNiUVJkT14jQNYJ4ZJjdRF23ZXz5138Fc= 480 | google.golang.org/api v0.35.0/go.mod h1:/XrVsuzM0rZmrsbjJutiuftIzeuTQcEeaYcSk/mQ1dg= 481 | google.golang.org/api v0.36.0/go.mod h1:+z5ficQTmoYpPn8LCUNVpK5I7hwkpjbcgqA7I34qYtE= 482 | google.golang.org/api v0.40.0/go.mod h1:fYKFpnQN0DsDSKRVRcQSDQNtqWPfM9i+zNPxepjRCQ8= 483 | google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= 484 | google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= 485 | google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= 486 | google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0= 487 | google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= 488 | google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= 489 | google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= 490 | google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= 491 | google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= 492 | google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= 493 | google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= 494 | google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= 495 | google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= 496 | google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= 497 | google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8= 498 | google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= 499 | google.golang.org/genproto v0.0.0-20191115194625-c23dd37a84c9/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= 500 | google.golang.org/genproto v0.0.0-20191216164720-4f79533eabd1/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= 501 | google.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= 502 | google.golang.org/genproto v0.0.0-20200115191322-ca5a22157cba/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= 503 | google.golang.org/genproto v0.0.0-20200122232147-0452cf42e150/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= 504 | google.golang.org/genproto v0.0.0-20200204135345-fa8e72b47b90/go.mod h1:GmwEX6Z4W5gMy59cAlVYjN9JhxgbQH6Gn+gFDQe2lzA= 505 | google.golang.org/genproto v0.0.0-20200212174721-66ed5ce911ce/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= 506 | google.golang.org/genproto v0.0.0-20200224152610-e50cd9704f63/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= 507 | google.golang.org/genproto v0.0.0-20200228133532-8c2c7df3a383/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= 508 | google.golang.org/genproto v0.0.0-20200305110556-506484158171/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= 509 | google.golang.org/genproto v0.0.0-20200312145019-da6875a35672/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= 510 | google.golang.org/genproto v0.0.0-20200331122359-1ee6d9798940/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= 511 | google.golang.org/genproto v0.0.0-20200430143042-b979b6f78d84/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= 512 | google.golang.org/genproto v0.0.0-20200511104702-f5ebc3bea380/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= 513 | google.golang.org/genproto v0.0.0-20200515170657-fc4c6c6a6587/go.mod h1:YsZOwe1myG/8QRHRsmBRE1LrgQY60beZKjly0O1fX9U= 514 | google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= 515 | google.golang.org/genproto v0.0.0-20200618031413-b414f8b61790/go.mod h1:jDfRM7FcilCzHH/e9qn6dsT145K34l5v+OpcnNgKAAA= 516 | google.golang.org/genproto v0.0.0-20200729003335-053ba62fc06f/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= 517 | google.golang.org/genproto v0.0.0-20200804131852-c06518451d9c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= 518 | google.golang.org/genproto v0.0.0-20200825200019-8632dd797987/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= 519 | google.golang.org/genproto v0.0.0-20200904004341-0bd0a958aa1d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= 520 | google.golang.org/genproto v0.0.0-20201109203340-2640f1f9cdfb/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= 521 | google.golang.org/genproto v0.0.0-20201201144952-b05cb90ed32e/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= 522 | google.golang.org/genproto v0.0.0-20201210142538-e3217bee35cc/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= 523 | google.golang.org/genproto v0.0.0-20201214200347-8c77b98c765d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= 524 | google.golang.org/genproto v0.0.0-20210108203827-ffc7fda8c3d7/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= 525 | google.golang.org/genproto v0.0.0-20210226172003-ab064af71705/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= 526 | google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= 527 | google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= 528 | google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= 529 | google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= 530 | google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= 531 | google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= 532 | google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= 533 | google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= 534 | google.golang.org/grpc v1.28.0/go.mod h1:rpkK4SK4GF4Ach/+MFLZUBavHOvF2JJB5uozKKal+60= 535 | google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk= 536 | google.golang.org/grpc v1.30.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= 537 | google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= 538 | google.golang.org/grpc v1.31.1/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= 539 | google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc= 540 | google.golang.org/grpc v1.34.0/go.mod h1:WotjhfgOW/POjDeRt8vscBtXq+2VjORFy659qA51WJ8= 541 | google.golang.org/grpc v1.35.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= 542 | google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= 543 | google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= 544 | google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= 545 | google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= 546 | google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= 547 | google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= 548 | google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= 549 | google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= 550 | google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4= 551 | google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= 552 | google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= 553 | google.golang.org/protobuf v1.31.0 h1:g0LDEJHgrBl9N9r17Ru3sqWhkIx2NB67okBHPwC7hs8= 554 | google.golang.org/protobuf v1.31.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= 555 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 556 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 557 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= 558 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= 559 | gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= 560 | gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA= 561 | gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= 562 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 563 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 564 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 565 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 566 | gorm.io/driver/postgres v1.5.3 h1:qKGY5CPHOuj47K/VxbCXJfFvIUeqMSXXadqdCY+MbBU= 567 | gorm.io/driver/postgres v1.5.3/go.mod h1:F+LtvlFhZT7UBiA81mC9W6Su3D4WUhSboc/36QZU0gk= 568 | gorm.io/gorm v1.25.5 h1:zR9lOiiYf09VNh5Q1gphfyia1JpiClIWG9hQaxB/mls= 569 | gorm.io/gorm v1.25.5/go.mod h1:hbnx/Oo0ChWMn1BIhpy1oYozzpM15i4YPuHDmfYtwg8= 570 | honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= 571 | honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= 572 | honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= 573 | honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= 574 | honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= 575 | honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= 576 | honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= 577 | rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= 578 | rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4= 579 | rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0= 580 | rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA= 581 | -------------------------------------------------------------------------------- /integrations/fast2sms/client.go: -------------------------------------------------------------------------------- 1 | package fast2sms 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | "strings" 7 | 8 | "github.com/dghubble/sling" 9 | "github.com/pkg/errors" 10 | ) 11 | 12 | type client struct { 13 | config Config 14 | sling *sling.Sling 15 | } 16 | 17 | func NewClient(config Config) *client { 18 | slingClient := sling.New(). 19 | Base(config.BaseURL). 20 | Set("Content-Type", "application/x-www-form-urlencoded"). 21 | Set("Accept", "application/json"). 22 | Set("Authorization", config.APIKey) 23 | 24 | return &client{ 25 | config: config, 26 | sling: slingClient, 27 | } 28 | } 29 | 30 | func (c *client) SendSMS(toNumber, otp string) error { 31 | toNumber, _ = strings.CutPrefix(toNumber, "+91") 32 | req := sendOTPRequest{ 33 | Values: otp, 34 | Route: smsRouteOTP, 35 | Numbers: toNumber, 36 | } 37 | respError := map[string]any{} 38 | resp, err := c.sling.New().Post("/dev/bulkV2").BodyForm(&req).Receive(nil, &respError) 39 | if err != nil { 40 | log.Printf("error connecting OTP provided for %s: %v", toNumber, err) 41 | return errors.Wrap(err, "error sending message") 42 | } 43 | fmt.Println("fast2sms response: ", resp.Status, respError) 44 | 45 | if resp.StatusCode != 200 { 46 | log.Printf("error sending OTP to %s: %v", toNumber, respError) 47 | return errors.New("error sending OTP") 48 | } 49 | return nil 50 | } 51 | -------------------------------------------------------------------------------- /integrations/fast2sms/entities.go: -------------------------------------------------------------------------------- 1 | package fast2sms 2 | 3 | type ( 4 | Config struct { 5 | APIKey string `validate:"required" log:"-"` 6 | BaseURL string `validate:"required"` 7 | } 8 | 9 | sendOTPRequest struct { 10 | Values string `url:"variables_values"` 11 | Route string `url:"route"` 12 | Numbers string `url:"numbers"` 13 | } 14 | ) 15 | 16 | const smsRouteOTP = "otp" 17 | -------------------------------------------------------------------------------- /integrations/twilio/client.go: -------------------------------------------------------------------------------- 1 | package twilio 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | 7 | "github.com/dghubble/sling" 8 | "github.com/pkg/errors" 9 | ) 10 | 11 | type Config struct { 12 | AccountSID string 13 | AuthToken string `log:"-"` 14 | FromNumber string 15 | } 16 | 17 | type Client struct { 18 | config Config 19 | sling *sling.Sling 20 | } 21 | 22 | func NewClient(config Config) *Client { 23 | base := sling.New().Base("https://api.twilio.com/2010-04-01/Accounts/" + config.AccountSID + "/") 24 | slingClient := base.New(). 25 | Set("Accept", "application/json"). 26 | Set("Content-Type", "application/x-www-form-urlencoded"). 27 | SetBasicAuth(config.AccountSID, config.AuthToken) 28 | return &Client{config: config, sling: slingClient} 29 | } 30 | 31 | func (c *Client) SendSMS(toNumber, message string) error { 32 | req := sendMessageRequest{ 33 | To: toNumber, 34 | From: c.config.FromNumber, 35 | Body: message, 36 | } 37 | respError := map[string]any{} 38 | resp, err := c.sling.New().Post("Messages.json").BodyForm(&req).Receive(nil, &respError) 39 | if err != nil { 40 | log.Printf("error sending message to=%s err=%v", toNumber, err) 41 | return errors.Wrap(err, "error sending message") 42 | } 43 | 44 | if resp.StatusCode == 201 { 45 | return nil 46 | } else { 47 | log.Printf("failed to send SMS, phone=%s twilio status=%d resp=%v", 48 | toNumber, resp.StatusCode, respError) 49 | return fmt.Errorf("failed to send SMS") 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /integrations/twilio/entities.go: -------------------------------------------------------------------------------- 1 | package twilio 2 | 3 | type ( 4 | sendMessageRequest struct { 5 | To string `json:"To" url:"To"` 6 | From string `json:"From" url:"From"` 7 | Body string `json:"Body" url:"Body"` 8 | } 9 | ) 10 | -------------------------------------------------------------------------------- /pgdb/db.go: -------------------------------------------------------------------------------- 1 | package pgdb 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "log" 7 | 8 | "gorm.io/driver/postgres" 9 | "gorm.io/gorm" 10 | "gorm.io/gorm/logger" 11 | ) 12 | 13 | type Config struct { 14 | Name string 15 | Host string 16 | Port int 17 | User string `log:"-"` 18 | Password string `log:"-"` 19 | SSLRootCertPath string 20 | DebugMigrations bool 21 | Debug bool 22 | } 23 | 24 | type PGDB struct { 25 | db *gorm.DB 26 | config Config 27 | } 28 | 29 | func (d *PGDB) DB(ctx context.Context) *gorm.DB { 30 | if d.config.Debug { 31 | return d.db.Debug() 32 | } 33 | return d.db 34 | } 35 | 36 | func NewPGConnection(ctx context.Context, config Config) *PGDB { 37 | dsn := fmt.Sprintf("host=%s port=%d user=%s password=%s dbname=%s", 38 | config.Host, config.Port, config.User, config.Password, config.Name) 39 | if config.SSLRootCertPath != "" { 40 | dsn = fmt.Sprintf("%s sslrootcert=%s", dsn, config.SSLRootCertPath) 41 | } else { 42 | dsn = fmt.Sprintf("%s sslmode=disable", dsn) 43 | } 44 | db, err := gorm.Open(postgres.Open(dsn), &gorm.Config{TranslateError: true}) 45 | if err != nil { 46 | log.Fatal("failed to connect postgres", err) 47 | } 48 | return &PGDB{db: db, config: config} 49 | } 50 | 51 | func (db *PGDB) Migrate(ctx context.Context, models []any) { 52 | curentLogger := db.db.Logger 53 | if !db.config.DebugMigrations { 54 | db.db.Logger = logger.Discard 55 | } 56 | if err := db.DB(ctx).AutoMigrate(models...); err != nil { 57 | log.Fatal(ctx, "could not migrate", err) 58 | } 59 | db.db.Logger = curentLogger 60 | } 61 | 62 | func (db *PGDB) TableName(m any) string { 63 | stmt := &gorm.Statement{DB: db.db} 64 | stmt.Parse(m) 65 | return stmt.Schema.Table 66 | } 67 | -------------------------------------------------------------------------------- /pgdb/model.go: -------------------------------------------------------------------------------- 1 | package pgdb 2 | 3 | import ( 4 | "time" 5 | 6 | "gorm.io/gorm" 7 | ) 8 | 9 | type BaseModel struct { 10 | ID int `gorm:"primarykey"` 11 | CreatedAt time.Time 12 | UpdatedAt time.Time 13 | DeletedAt gorm.DeletedAt `gorm:"index"` 14 | } 15 | 16 | func (d BaseModel) IsDeleted() bool { return d.DeletedAt.Valid } 17 | func (d BaseModel) PK() int { return d.ID } 18 | func (s BaseModel) Joins() []string { return []string{} } 19 | 20 | type NamedModel interface { 21 | GetName() string 22 | PK() int 23 | } 24 | 25 | func ToNameModelMap[M NamedModel](models []M) map[string]M { 26 | var res = make(map[string]M, len(models)) 27 | for _, model := range models { 28 | res[model.GetName()] = model 29 | } 30 | return res 31 | } 32 | 33 | func ToIDModelMap[M NamedModel](models []M) map[int]M { 34 | var res = make(map[int]M, len(models)) 35 | for _, model := range models { 36 | res[model.PK()] = model 37 | } 38 | return res 39 | } 40 | -------------------------------------------------------------------------------- /pgdb/paginate.go: -------------------------------------------------------------------------------- 1 | package pgdb 2 | 3 | import "gorm.io/gorm" 4 | 5 | const DefaultPageLimit = 25 6 | const MaxPageLimit = 100 7 | 8 | type Page struct { 9 | Page int `form:"page"` 10 | Limit int `form:"limit"` 11 | After int `form:"after"` 12 | } 13 | 14 | func NewPage(page, after, limit int) Page { 15 | return Page{ 16 | Page: page, 17 | Limit: limit, 18 | After: after, 19 | } 20 | } 21 | 22 | func (p Page) Offset() int { 23 | currentPage := p.Page 24 | if currentPage > 0 { 25 | currentPage -= 1 26 | } 27 | return currentPage * p.Limit 28 | } 29 | 30 | func Paginate(page Page, afterField string) func(db *gorm.DB) *gorm.DB { 31 | return func(db *gorm.DB) *gorm.DB { 32 | if page.Limit == 0 || page.Limit > MaxPageLimit { 33 | page.Limit = DefaultPageLimit 34 | } 35 | if page.After > 0 { 36 | db = db.Where(afterField+" > ?", page.After) 37 | db = db.Order(afterField + " ASC") 38 | } else if page.Page > 0 { 39 | db = db.Offset(page.Offset()) 40 | } 41 | return db.Limit(page.Limit) 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /request/binding.go: -------------------------------------------------------------------------------- 1 | package request 2 | 3 | import ( 4 | "log" 5 | "net/http" 6 | 7 | "github.com/gin-gonic/gin" 8 | "github.com/krsoninikhil/go-rest-kit/apperrors" 9 | "github.com/pkg/errors" 10 | ) 11 | 12 | type bindedHandlerFunc[Req, Res any] func(*gin.Context, Req) (*Res, error) 13 | 14 | // BindAll binds request body, uri, query params and headers to R type 15 | // and respond with S type 16 | func BindAll[R, S any](handler bindedHandlerFunc[R, S]) gin.HandlerFunc { 17 | var req R 18 | return func(c *gin.Context) { 19 | if err := bindRequestParams[R](c, &req, &req); err != nil { 20 | c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) 21 | return 22 | } 23 | res, err := handler(c, req) 24 | Respond(c, res, err) 25 | } 26 | } 27 | 28 | // bindRequestParams bind the request based on tags, order matters as 29 | // Uri params could be mentioned required and validation would fail if 30 | // looked in query param or elsewhere 31 | func bindRequestParams[P, R any](c *gin.Context, params *P, req *R) error { 32 | if params != nil { 33 | if err := c.ShouldBindUri(params); err != nil { 34 | return errors.WithStack(err) 35 | } 36 | if err := c.ShouldBindQuery(params); err != nil { 37 | return errors.WithStack(err) 38 | } 39 | if err := c.ShouldBindHeader(params); err != nil { 40 | return errors.WithStack(err) 41 | } 42 | } 43 | 44 | if req != nil { 45 | if err := c.ShouldBindJSON(req); err != nil { 46 | return errors.Wrap(err, "error binding request body") 47 | } 48 | } 49 | return nil 50 | } 51 | 52 | // Respond sets the http status code and response to gin context 53 | func Respond(c *gin.Context, res any, err error) { 54 | if err == nil { 55 | status := http.StatusOK 56 | if res == nil { 57 | status = http.StatusNoContent 58 | } else if c.Request.Method == http.MethodGet { 59 | status = http.StatusOK 60 | } else if c.Request.Method == http.MethodPost { 61 | status = http.StatusCreated 62 | } 63 | 64 | c.JSON(status, res) 65 | return 66 | } 67 | 68 | // handle error 69 | appError, ok := err.(apperrors.AppError) 70 | if !ok { 71 | appError = apperrors.NewServerError(err) 72 | } 73 | 74 | // log causes of server errors 75 | if severErr, ok := appError.(apperrors.ServerError); ok && severErr.Cause != nil { 76 | log.Printf("server error cause: %T: %+v\n", severErr, severErr.Cause) 77 | } 78 | 79 | c.JSON(appError.HTTPCode(), appError.HTTPResponse()) 80 | } 81 | -------------------------------------------------------------------------------- /request/context.go: -------------------------------------------------------------------------------- 1 | package request 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/gin-gonic/gin" 7 | ) 8 | 9 | type Context struct { 10 | userID int 11 | Gin *gin.Context 12 | 13 | context.Context 14 | } 15 | 16 | func (ctx *Context) SetUserID(userID int) { 17 | ctx.userID = userID 18 | } 19 | 20 | func (ctx *Context) UserID() int { 21 | return ctx.userID 22 | } 23 | -------------------------------------------------------------------------------- /request/crud.go: -------------------------------------------------------------------------------- 1 | package request 2 | 3 | import ( 4 | "net/http" 5 | 6 | "github.com/gin-gonic/gin" 7 | ) 8 | 9 | type ( 10 | createHandlerFunc[R, S any] func(ctx *gin.Context, req R) (*S, error) 11 | getHandlerFunc[P, S any] func(ctx *gin.Context, params P) (*S, error) 12 | updateHandlerFunc[P, R any] func(ctx *gin.Context, params P, req R) error 13 | deleteHandlerFunc[P any] func(ctx *gin.Context, params P) error 14 | 15 | // createNestedHandlerFunc represents create handler for nested resource as they 16 | // require URI params even for create method 17 | createNestedHandlerFunc[P, R, S any] func(ctx *gin.Context, params P, req R) (*S, error) 18 | ) 19 | 20 | func BindCreate[R, S any](handler createHandlerFunc[R, S]) gin.HandlerFunc { 21 | var req R 22 | return func(c *gin.Context) { 23 | if err := bindRequestParams[any, R](c, nil, &req); err != nil { 24 | c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) 25 | return 26 | } 27 | res, err := handler(c, req) 28 | Respond(c, res, err) 29 | } 30 | } 31 | 32 | func BindGet[P, S any](handler getHandlerFunc[P, S]) gin.HandlerFunc { 33 | var params P 34 | return func(c *gin.Context) { 35 | if err := bindRequestParams[P, any](c, ¶ms, nil); err != nil { 36 | c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) 37 | return 38 | } 39 | res, err := handler(c, params) 40 | Respond(c, res, err) 41 | } 42 | } 43 | 44 | func BindUpdate[P, R any](handler updateHandlerFunc[P, R]) gin.HandlerFunc { 45 | var ( 46 | req R 47 | params P 48 | ) 49 | return func(c *gin.Context) { 50 | if err := bindRequestParams(c, ¶ms, &req); err != nil { 51 | c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) 52 | return 53 | } 54 | err := handler(c, params, req) 55 | Respond(c, nil, err) 56 | } 57 | } 58 | 59 | func BindDelete[P any](handler deleteHandlerFunc[P]) gin.HandlerFunc { 60 | var params P 61 | return func(c *gin.Context) { 62 | if err := bindRequestParams[P, any](c, ¶ms, nil); err != nil { 63 | c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) 64 | return 65 | } 66 | err := handler(c, params) 67 | Respond(c, nil, err) 68 | } 69 | } 70 | 71 | func BindNestedCreate[P, R, S any](handler createNestedHandlerFunc[P, R, S]) gin.HandlerFunc { 72 | var ( 73 | req R 74 | params P 75 | ) 76 | return func(c *gin.Context) { 77 | if err := bindRequestParams(c, ¶ms, &req); err != nil { 78 | c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) 79 | return 80 | } 81 | res, err := handler(c, params, req) 82 | Respond(c, res, err) 83 | } 84 | } 85 | --------------------------------------------------------------------------------