From ff0cb5af8fea7b2bcfc0700851e5119a71e88e4a Mon Sep 17 00:00:00 2001 From: Johnny Steenbergen Date: Sun, 23 Jun 2024 10:59:25 -0500 Subject: [PATCH] feat(wild-workouts): add slimmed down wild-workouts-go-ddd-example repo This repo originates from `threedotlabs` to accompany their "Introducing Clean Architecture" blog post found [here](https://threedots.tech/post/introducing-clean-architecture/). Take a few moments... gather your thoughts about this slimmed down example. Question: What positivie aspects do you find in this design pattern? Question: What concerns do you have with this design pattern? Refs: [Threedots Tech Introduction to Clean Architecture](https://threedots.tech/post/introducing-clean-architecture/) --- wild-workouts/.gitignore | 36 + wild-workouts/LICENSE | 21 + wild-workouts/Makefile | 48 + wild-workouts/README.md | 107 ++ wild-workouts/doc.go | 10 + wild-workouts/internal/common/auth/http.go | 84 ++ .../internal/common/auth/http_mock.go | 44 + wild-workouts/internal/common/client/auth.go | 41 + wild-workouts/internal/common/client/grpc.go | 81 ++ wild-workouts/internal/common/client/net.go | 37 + .../client/trainer/openapi_client_gen.go | 568 ++++++++++ .../client/trainer/openapi_types.gen.go | 57 + .../client/trainings/openapi_client_gen.go | 1004 +++++++++++++++++ .../client/trainings/openapi_types.gen.go | 60 + .../common/client/users/openapi_client_gen.go | 234 ++++ .../common/client/users/openapi_types.gen.go | 15 + .../internal/common/decorator/command.go | 27 + .../internal/common/decorator/logging.go | 56 + .../internal/common/decorator/metrics.go | 62 + .../internal/common/decorator/query.go | 21 + .../internal/common/errors/errors.go | 53 + .../common/genproto/trainer/trainer.pb.go | 315 ++++++ .../genproto/trainer/trainer_grpc.pb.go | 209 ++++ .../common/genproto/users/users.pb.go | 305 +++++ .../common/genproto/users/users_grpc.pb.go | 137 +++ wild-workouts/internal/common/go.mod | 54 + wild-workouts/internal/common/go.sum | 627 ++++++++++ wild-workouts/internal/common/logs/cqrs.go | 15 + wild-workouts/internal/common/logs/http.go | 64 ++ wild-workouts/internal/common/logs/logrus.go | 31 + .../internal/common/metrics/dummy.go | 7 + wild-workouts/internal/common/server/grpc.go | 54 + wild-workouts/internal/common/server/http.go | 96 ++ .../common/server/httperr/http_error.go | 57 + .../internal/common/tests/clients.go | 174 +++ .../internal/common/tests/e2e_test.go | 69 ++ wild-workouts/internal/common/tests/hours.go | 15 + wild-workouts/internal/common/tests/jwt.go | 35 + wild-workouts/internal/common/tests/wait.go | 33 + .../adapters/hour_firestore_repository.go | 222 ++++ .../adapters/hour_memory_repository.go | 69 ++ .../trainer/adapters/hour_mysql_repository.go | 198 ++++ .../trainer/adapters/hour_repository_test.go | 350 ++++++ wild-workouts/internal/trainer/app/app.go | 23 + .../trainer/app/command/cancel_training.go | 50 + .../app/command/make_hours_available.go | 52 + .../app/command/make_hours_unavailable.go | 52 + .../trainer/app/command/schedule_training.go | 50 + .../trainer/app/query/hour_availability.go | 60 + .../internal/trainer/app/query/types.go | 2 + .../trainer/domain/hour/availability.go | 97 ++ .../trainer/domain/hour/availability_test.go | 125 ++ .../internal/trainer/domain/hour/hour.go | 221 ++++ .../internal/trainer/domain/hour/hour_test.go | 248 ++++ .../trainer/domain/hour/repository.go | 15 + wild-workouts/internal/trainer/fixtures.go | 99 ++ wild-workouts/internal/trainer/go.mod | 62 + wild-workouts/internal/trainer/go.sum | 628 +++++++++++ wild-workouts/internal/trainer/main.go | 45 + wild-workouts/internal/trainer/ports/grpc.go | 68 ++ wild-workouts/internal/trainer/ports/http.go | 49 + .../internal/trainer/ports/openapi_api.gen.go | 161 +++ .../trainer/ports/openapi_types.gen.go | 57 + .../internal/trainer/service/application.go | 51 + .../trainer/service/component_test.go | 108 ++ wild-workouts/sql/schema.sql | 6 + 66 files changed, 8131 insertions(+) create mode 100644 wild-workouts/.gitignore create mode 100644 wild-workouts/LICENSE create mode 100644 wild-workouts/Makefile create mode 100644 wild-workouts/README.md create mode 100644 wild-workouts/doc.go create mode 100644 wild-workouts/internal/common/auth/http.go create mode 100644 wild-workouts/internal/common/auth/http_mock.go create mode 100644 wild-workouts/internal/common/client/auth.go create mode 100644 wild-workouts/internal/common/client/grpc.go create mode 100644 wild-workouts/internal/common/client/net.go create mode 100644 wild-workouts/internal/common/client/trainer/openapi_client_gen.go create mode 100644 wild-workouts/internal/common/client/trainer/openapi_types.gen.go create mode 100644 wild-workouts/internal/common/client/trainings/openapi_client_gen.go create mode 100644 wild-workouts/internal/common/client/trainings/openapi_types.gen.go create mode 100644 wild-workouts/internal/common/client/users/openapi_client_gen.go create mode 100644 wild-workouts/internal/common/client/users/openapi_types.gen.go create mode 100644 wild-workouts/internal/common/decorator/command.go create mode 100644 wild-workouts/internal/common/decorator/logging.go create mode 100644 wild-workouts/internal/common/decorator/metrics.go create mode 100644 wild-workouts/internal/common/decorator/query.go create mode 100644 wild-workouts/internal/common/errors/errors.go create mode 100644 wild-workouts/internal/common/genproto/trainer/trainer.pb.go create mode 100644 wild-workouts/internal/common/genproto/trainer/trainer_grpc.pb.go create mode 100644 wild-workouts/internal/common/genproto/users/users.pb.go create mode 100644 wild-workouts/internal/common/genproto/users/users_grpc.pb.go create mode 100644 wild-workouts/internal/common/go.mod create mode 100644 wild-workouts/internal/common/go.sum create mode 100644 wild-workouts/internal/common/logs/cqrs.go create mode 100644 wild-workouts/internal/common/logs/http.go create mode 100644 wild-workouts/internal/common/logs/logrus.go create mode 100644 wild-workouts/internal/common/metrics/dummy.go create mode 100644 wild-workouts/internal/common/server/grpc.go create mode 100644 wild-workouts/internal/common/server/http.go create mode 100644 wild-workouts/internal/common/server/httperr/http_error.go create mode 100644 wild-workouts/internal/common/tests/clients.go create mode 100644 wild-workouts/internal/common/tests/e2e_test.go create mode 100644 wild-workouts/internal/common/tests/hours.go create mode 100644 wild-workouts/internal/common/tests/jwt.go create mode 100644 wild-workouts/internal/common/tests/wait.go create mode 100644 wild-workouts/internal/trainer/adapters/hour_firestore_repository.go create mode 100644 wild-workouts/internal/trainer/adapters/hour_memory_repository.go create mode 100644 wild-workouts/internal/trainer/adapters/hour_mysql_repository.go create mode 100644 wild-workouts/internal/trainer/adapters/hour_repository_test.go create mode 100644 wild-workouts/internal/trainer/app/app.go create mode 100644 wild-workouts/internal/trainer/app/command/cancel_training.go create mode 100644 wild-workouts/internal/trainer/app/command/make_hours_available.go create mode 100644 wild-workouts/internal/trainer/app/command/make_hours_unavailable.go create mode 100644 wild-workouts/internal/trainer/app/command/schedule_training.go create mode 100644 wild-workouts/internal/trainer/app/query/hour_availability.go create mode 100644 wild-workouts/internal/trainer/app/query/types.go create mode 100644 wild-workouts/internal/trainer/domain/hour/availability.go create mode 100644 wild-workouts/internal/trainer/domain/hour/availability_test.go create mode 100644 wild-workouts/internal/trainer/domain/hour/hour.go create mode 100644 wild-workouts/internal/trainer/domain/hour/hour_test.go create mode 100644 wild-workouts/internal/trainer/domain/hour/repository.go create mode 100644 wild-workouts/internal/trainer/fixtures.go create mode 100644 wild-workouts/internal/trainer/go.mod create mode 100644 wild-workouts/internal/trainer/go.sum create mode 100644 wild-workouts/internal/trainer/main.go create mode 100644 wild-workouts/internal/trainer/ports/grpc.go create mode 100644 wild-workouts/internal/trainer/ports/http.go create mode 100644 wild-workouts/internal/trainer/ports/openapi_api.gen.go create mode 100644 wild-workouts/internal/trainer/ports/openapi_types.gen.go create mode 100644 wild-workouts/internal/trainer/service/application.go create mode 100644 wild-workouts/internal/trainer/service/component_test.go create mode 100644 wild-workouts/sql/schema.sql diff --git a/wild-workouts/.gitignore b/wild-workouts/.gitignore new file mode 100644 index 0000000..88e11a8 --- /dev/null +++ b/wild-workouts/.gitignore @@ -0,0 +1,36 @@ +# Created by .ignore support plugin (hsz.mobi) +### Go template +# Binaries for programs and plugins +*.exe +*.exe~ +*.dll +*.so +*.dylib + +# Test binary, built with `go test -c` +*.test + +# Output of the go coverage tool, specifically when used with LiteIDE +*.out + +# Dependency directories (remove the comment below to include it) +vendor/ + +# Tools +.idea + +.terraform +*.tfstate +*.tfstate.*backup +terraform/.env +terraform/cloud-builders-community + +/web/src/repositories/clients/* +!/web/src/repositories/clients/trainer/src/ +!/web/src/repositories/clients/trainings/src/ +!/web/src/repositories/clients/users/src/ + +/service-account-file.json + +.go/ +.go-cache/ diff --git a/wild-workouts/LICENSE b/wild-workouts/LICENSE new file mode 100644 index 0000000..6489165 --- /dev/null +++ b/wild-workouts/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2021 Three Dots Labs + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/wild-workouts/Makefile b/wild-workouts/Makefile new file mode 100644 index 0000000..fe223b3 --- /dev/null +++ b/wild-workouts/Makefile @@ -0,0 +1,48 @@ +include .env +export + +.PHONY: openapi +openapi: openapi_http openapi_js + +.PHONY: openapi_http +openapi_http: + @./scripts/openapi-http.sh trainer internal/trainer/ports ports + @./scripts/openapi-http.sh trainings internal/trainings/ports ports + @./scripts/openapi-http.sh users internal/users main + +.PHONY: openapi_js +openapi_js: + @./scripts/openapi-js.sh trainer + @./scripts/openapi-js.sh trainings + @./scripts/openapi-js.sh users + +.PHONY: proto +proto: + @./scripts/proto.sh trainer + @./scripts/proto.sh users + +.PHONY: lint +lint: + @go-cleanarch + @./scripts/lint.sh common + @./scripts/lint.sh trainer + @./scripts/lint.sh trainings + @./scripts/lint.sh users + +.PHONY: fmt +fmt: + goimports -l -w internal/ + +.PHONY: mycli +mycli: + mycli -u ${MYSQL_USER} -p ${MYSQL_PASSWORD} ${MYSQL_DATABASE} + +.PHONY: c4 +c4: + cd tools/c4 && go mod tidy && sh generate.sh + +test: + @./scripts/test.sh common .e2e.env + @./scripts/test.sh trainer .test.env + @./scripts/test.sh trainings .test.env + @./scripts/test.sh users .test.env diff --git a/wild-workouts/README.md b/wild-workouts/README.md new file mode 100644 index 0000000..bd57fb5 --- /dev/null +++ b/wild-workouts/README.md @@ -0,0 +1,107 @@ +# Wild Workouts + +# NOTE: this is adapated from the Wild Workouts Project from threedot labs +# to address some thoughts doing things the Java way. + +Wild Workouts is an **example Go DDD** project that we created to show how to build Go applications that areĀ **easy to develop, maintain, and fun to work with, especially in the long term!** + +*The idea for this series, is to apply DDD by refactoring. This process is in progress! Please check articles, to know the current progress.* + +No application is perfect from the beginning. With over a dozen coming articles, we will uncover what issues you can find in the current implementation. We will also show how to fix these issues and achieve clean implementation by refactoring. + +### Articles + +#### "Too modern" application + +1. [**Too modern Go application? Building a serverless application with Google Cloud Run and Firebase**](https://threedots.tech/post/serverless-cloud-run-firebase-modern-go-application/?utm_source=github.com) _[[v1.0]](https://github.com/ThreeDotsLabs/wild-workouts-go-ddd-example/releases/tag/v1.0)_ +2. [**A complete Terraform setup of a serverless application on Google Cloud Run and Firebase**](https://threedots.tech/post/complete-setup-of-serverless-application/?utm_source=github.com) _[[v1.1]](https://github.com/ThreeDotsLabs/wild-workouts-go-ddd-example/releases/tag/v1.1)_ +3. [**Robust gRPC communication on Google Cloud Run (but not only!)**](https://threedots.tech/post/robust-grpc-google-cloud-run/?utm_source=github.com) _[[v1.2]](https://github.com/ThreeDotsLabs/wild-workouts-go-ddd-example/releases/tag/v1.2)_ +4. [**You should not build your own authentication. Let Firebase do it for you.**](https://threedots.tech/post/firebase-cloud-run-authentication/?utm_source=github.com) _[[v1.3]](https://github.com/ThreeDotsLabs/wild-workouts-go-ddd-example/releases/tag/v1.3)_ + +#### Refactoring + +5. [**Business Applications in Go: Things to know about DRY**](https://threedots.tech/post/things-to-know-about-dry/?utm_source=github.com) _[[v2.0]](https://github.com/ThreeDotsLabs/wild-workouts-go-ddd-example/releases/tag/v2.0)_ +6. [**When microservices in Go are not enough: introduction to DDD Lite**](https://threedots.tech/post/ddd-lite-in-go-introduction/?utm_source=github.com) _[[v2.1]](https://github.com/ThreeDotsLabs/wild-workouts-go-ddd-example/releases/tag/v2.1)_ +7. [**Repository pattern: painless way to simplify your Go service logic**](https://threedots.tech/post/repository-pattern-in-go/?utm_source=github.com) _[[v2.2]](https://github.com/ThreeDotsLabs/wild-workouts-go-ddd-example/releases/tag/v2.2)_ +8. [**4 practical principles of high-quality database integration tests in Go**](https://threedots.tech/post/database-integration-testing/?utm_source=github.com) _[[v2.3]](https://github.com/ThreeDotsLabs/wild-workouts-go-ddd-example/releases/tag/v2.3)_ +9. [**Introducing Clean Architecture by refactoring a Go project**](https://threedots.tech/post/introducing-clean-architecture/?utm_source=github.com) _[[v2.4]](https://github.com/ThreeDotsLabs/wild-workouts-go-ddd-example/releases/tag/v2.4)_ +10. [**Introducing basic CQRS by refactoring**](https://threedots.tech/post/basic-cqrs-in-go/?utm_source=github.com) _[[v2.5]](https://github.com/ThreeDotsLabs/wild-workouts-go-ddd-example/releases/tag/v2.5)_ +11. [**Combining DDD, CQRS, and Clean Architecture**](https://threedots.tech/post/ddd-cqrs-clean-architecture-combined/?utm_source=github.com) +12. [**Microservices test architecture. Can you sleep well without end-to-end tests?**](https://threedots.tech/post/microservices-test-architecture/?utm_source=github.com) _[[v2.6]](https://github.com/ThreeDotsLabs/wild-workouts-go-ddd-example/releases/tag/v2.6)_ +13. [**Repository secure by design: how to sleep better without fear of security vulnerabilities**](https://threedots.tech/post/repository-secure-by-design/?utm_source=github.com) +14. [**Running integration tests on Google Cloud Build using docker-compose**](https://threedots.tech/post/running-integration-tests-on-google-cloud-build/?utm_source=github.com) _[[v2.7]](https://github.com/ThreeDotsLabs/wild-workouts-go-ddd-example/releases/tag/v2.7)_ +15. *More articles are on the way!* + +### Community + +We're building a Discord community focused on modern business applications. It's the place to discuss hard topics, request a review, or ask if something's not clear. [Come join us!](https://discord.gg/kTVsGjPYDn) + +### Directories + +- [api](api/) OpenAPI and gRPC definitions +- [docker](docker/) Dockerfiles +- [internal](internal/) application code +- [scripts](scripts/) deployment and development scripts +- [terraform](terraform/) - infrastructure definition +- [web](web/) - frontend JavaScript code + +### Live Demo + +The example application is available at [https://threedotslabs-wildworkouts.web.app/](https://threedotslabs-wildworkouts.web.app/). + +### Running locally + +```go +> docker-compose up + +# ... + +web_1 | INFO Starting development server... +web_1 | DONE Compiled successfully in 6315ms11:18:26 AM +web_1 | +web_1 | +web_1 | App running at: +web_1 | - Local: http://localhost:8080/ +web_1 | +web_1 | It seems you are running Vue CLI inside a container. +web_1 | Access the dev server via http://localhost:/ +web_1 | +web_1 | Note that the development build is not optimized. +web_1 | To create a production build, run yarn build. +``` + +### Google Cloud Deployment + +```go +> cd terraform/ +> make + +Fill all required parameters: + project [current: wild-workouts project]: # <----- put your Wild Workouts Google Cloud project name here (it will be created) + user [current: email@gmail.com]: # <----- put your Google (Gmail, G-suite etc.) e-mail here + billing_account [current: My billing account]: # <----- your billing account name, can be found here https://console.cloud.google.com/billing + region [current: europe-west1]: + firebase_location [current: europe-west]: + +# it may take a couple of minutes... + +The setup is almost done! + +Now you need to enable Email/Password provider in the Firebase console. +To do this, visit https://console.firebase.google.com/u/0/project/[your-project]/authentication/providers + +You can also downgrade the subscription plan to Spark (it's set to Blaze by default). +The Spark plan is completely free and has all features needed for running this project. + +Congratulations! Your project should be available at: https://[your-project].web.app + +If it's not, check if the build finished successfully: https://console.cloud.google.com/cloud-build/builds?project=[your-project] + +If you need help, feel free to contact us at https://threedots.tech +``` + +### Screenshots + +![Wild Workouts login](https://threedots.tech/media/serverless-cloud-run-firebase-modern-go-app/login.png "Logo Title Text 1") +![Wild Workouts trainer's schedule](https://threedots.tech/media/serverless-cloud-run-firebase-modern-go-app/schedule.png "Logo Title Text 1") +![Wild Workouts schedule training](https://threedots.tech/media/serverless-cloud-run-firebase-modern-go-app/new-training.png "Logo Title Text 1") diff --git a/wild-workouts/doc.go b/wild-workouts/doc.go new file mode 100644 index 0000000..05d0c46 --- /dev/null +++ b/wild-workouts/doc.go @@ -0,0 +1,10 @@ +// package wild_workouts is an introductory repository that provides +// examples of working with the clean architecture in go. +// +// Concerns: +// * +// +// Praises: +// * + +package wild_workouts diff --git a/wild-workouts/internal/common/auth/http.go b/wild-workouts/internal/common/auth/http.go new file mode 100644 index 0000000..c191c33 --- /dev/null +++ b/wild-workouts/internal/common/auth/http.go @@ -0,0 +1,84 @@ +package auth + +import ( + "context" + "net/http" + "strings" + + "firebase.google.com/go/v4/auth" + commonerrors "github.com/ThreeDotsLabs/wild-workouts-go-ddd-example/internal/common/errors" + "github.com/ThreeDotsLabs/wild-workouts-go-ddd-example/internal/common/server/httperr" +) + +type FirebaseHttpMiddleware struct { + AuthClient *auth.Client +} + +func (a FirebaseHttpMiddleware) Middleware(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + + bearerToken := a.tokenFromHeader(r) + if bearerToken == "" { + httperr.Unauthorised("empty-bearer-token", nil, w, r) + return + } + + token, err := a.AuthClient.VerifyIDToken(ctx, bearerToken) + if err != nil { + httperr.Unauthorised("unable-to-verify-jwt", err, w, r) + return + } + + // it's always a good idea to use custom type as context value (in this case ctxKey) + // because nobody from the outside of the package will be able to override/read this value + ctx = context.WithValue(ctx, userContextKey, User{ + UUID: token.UID, + Email: token.Claims["email"].(string), + Role: token.Claims["role"].(string), + DisplayName: token.Claims["name"].(string), + }) + r = r.WithContext(ctx) + + next.ServeHTTP(w, r) + }) +} + +func (a FirebaseHttpMiddleware) tokenFromHeader(r *http.Request) string { + headerValue := r.Header.Get("Authorization") + + if len(headerValue) > 7 && strings.ToLower(headerValue[0:6]) == "bearer" { + return headerValue[7:] + } + + return "" +} + +type User struct { + UUID string + Email string + Role string + + DisplayName string +} + +type ctxKey int + +const ( + userContextKey ctxKey = iota +) + +var ( + // if we expect that the user of the function may be interested with concrete error, + // it's a good idea to provide variable with this error + NoUserInContextError = commonerrors.NewAuthorizationError("no user in context", "no-user-found") +) + +func UserFromCtx(ctx context.Context) (User, error) { + u, ok := ctx.Value(userContextKey).(User) + if ok { + return u, nil + } + + return User{}, NoUserInContextError +} diff --git a/wild-workouts/internal/common/auth/http_mock.go b/wild-workouts/internal/common/auth/http_mock.go new file mode 100644 index 0000000..24b3dd9 --- /dev/null +++ b/wild-workouts/internal/common/auth/http_mock.go @@ -0,0 +1,44 @@ +package auth + +import ( + "context" + "net/http" + + "github.com/ThreeDotsLabs/wild-workouts-go-ddd-example/internal/common/server/httperr" + "github.com/dgrijalva/jwt-go" + "github.com/dgrijalva/jwt-go/request" +) + +// HttpMockMiddleware is used in the local environment (which doesn't depend on Firebase) +func HttpMockMiddleware(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + var claims jwt.MapClaims + token, err := request.ParseFromRequest( + r, + request.AuthorizationHeaderExtractor, + func(token *jwt.Token) (i interface{}, e error) { + return []byte("mock_secret"), nil + }, + request.WithClaims(&claims), + ) + if err != nil { + httperr.BadRequest("unable-to-get-jwt", err, w, r) + return + } + + if !token.Valid { + httperr.BadRequest("invalid-jwt", nil, w, r) + return + } + + ctx := context.WithValue(r.Context(), userContextKey, User{ + UUID: claims["user_uuid"].(string), + Email: claims["email"].(string), + Role: claims["role"].(string), + DisplayName: claims["name"].(string), + }) + r = r.WithContext(ctx) + + next.ServeHTTP(w, r) + }) +} diff --git a/wild-workouts/internal/common/client/auth.go b/wild-workouts/internal/common/client/auth.go new file mode 100644 index 0000000..731d865 --- /dev/null +++ b/wild-workouts/internal/common/client/auth.go @@ -0,0 +1,41 @@ +package client + +import ( + "context" + "fmt" + "strings" + + "cloud.google.com/go/compute/metadata" + "github.com/pkg/errors" + "google.golang.org/grpc/credentials" +) + +type metadataServerToken struct { + serviceURL string +} + +func newMetadataServerToken(grpcAddr string) credentials.PerRPCCredentials { + // based on https://cloud.google.com/run/docs/authenticating/service-to-service#go + // service need to have https prefix without port + serviceURL := "https://" + strings.Split(grpcAddr, ":")[0] + + return metadataServerToken{serviceURL} +} + +// GetRequestMetadata is called on every request, so we are sure that token is always not expired +func (t metadataServerToken) GetRequestMetadata(ctx context.Context, in ...string) (map[string]string, error) { + // based on https://cloud.google.com/run/docs/authenticating/service-to-service#go + tokenURL := fmt.Sprintf("/instance/service-accounts/default/identity?audience=%s", t.serviceURL) + idToken, err := metadata.Get(tokenURL) + if err != nil { + return nil, errors.Wrap(err, "cannot query id token for gRPC") + } + + return map[string]string{ + "authorization": "Bearer " + idToken, + }, nil +} + +func (metadataServerToken) RequireTransportSecurity() bool { + return true +} diff --git a/wild-workouts/internal/common/client/grpc.go b/wild-workouts/internal/common/client/grpc.go new file mode 100644 index 0000000..c7ceea4 --- /dev/null +++ b/wild-workouts/internal/common/client/grpc.go @@ -0,0 +1,81 @@ +package client + +import ( + "crypto/tls" + "crypto/x509" + "os" + "strconv" + "time" + + "github.com/ThreeDotsLabs/wild-workouts-go-ddd-example/internal/common/genproto/trainer" + "github.com/ThreeDotsLabs/wild-workouts-go-ddd-example/internal/common/genproto/users" + "github.com/pkg/errors" + "google.golang.org/grpc" + "google.golang.org/grpc/credentials" +) + +func NewTrainerClient() (client trainer.TrainerServiceClient, close func() error, err error) { + grpcAddr := os.Getenv("TRAINER_GRPC_ADDR") + if grpcAddr == "" { + return nil, func() error { return nil }, errors.New("empty env TRAINER_GRPC_ADDR") + } + + opts, err := grpcDialOpts(grpcAddr) + if err != nil { + return nil, func() error { return nil }, err + } + + conn, err := grpc.Dial(grpcAddr, opts...) + if err != nil { + return nil, func() error { return nil }, err + } + + return trainer.NewTrainerServiceClient(conn), conn.Close, nil +} + +func WaitForTrainerService(timeout time.Duration) bool { + return waitForPort(os.Getenv("TRAINER_GRPC_ADDR"), timeout) +} + +func NewUsersClient() (client users.UsersServiceClient, close func() error, err error) { + grpcAddr := os.Getenv("USERS_GRPC_ADDR") + if grpcAddr == "" { + return nil, func() error { return nil }, errors.New("empty env USERS_GRPC_ADDR") + } + + opts, err := grpcDialOpts(grpcAddr) + if err != nil { + return nil, func() error { return nil }, err + } + + conn, err := grpc.Dial(grpcAddr, opts...) + if err != nil { + return nil, func() error { return nil }, err + } + + return users.NewUsersServiceClient(conn), conn.Close, nil +} + +func WaitForUsersService(timeout time.Duration) bool { + return waitForPort(os.Getenv("USERS_GRPC_ADDR"), timeout) +} + +func grpcDialOpts(grpcAddr string) ([]grpc.DialOption, error) { + if noTLS, _ := strconv.ParseBool(os.Getenv("GRPC_NO_TLS")); noTLS { + return []grpc.DialOption{grpc.WithInsecure()}, nil + } + + systemRoots, err := x509.SystemCertPool() + if err != nil { + return nil, errors.Wrap(err, "cannot load root CA cert") + } + creds := credentials.NewTLS(&tls.Config{ + RootCAs: systemRoots, + MinVersion: tls.VersionTLS12, + }) + + return []grpc.DialOption{ + grpc.WithTransportCredentials(creds), + grpc.WithPerRPCCredentials(newMetadataServerToken(grpcAddr)), + }, nil +} diff --git a/wild-workouts/internal/common/client/net.go b/wild-workouts/internal/common/client/net.go new file mode 100644 index 0000000..6c7425e --- /dev/null +++ b/wild-workouts/internal/common/client/net.go @@ -0,0 +1,37 @@ +package client + +import ( + "net" + "time" +) + +func waitForPort(addr string, timeout time.Duration) bool { + portAvailable := make(chan struct{}) + timeoutCh := time.After(timeout) + + go func() { + for { + select { + case <-timeoutCh: + return + default: + // continue + } + + _, err := net.Dial("tcp", addr) + if err == nil { + close(portAvailable) + return + } + + time.Sleep(time.Millisecond * 200) + } + }() + + select { + case <-portAvailable: + return true + case <-timeoutCh: + return false + } +} diff --git a/wild-workouts/internal/common/client/trainer/openapi_client_gen.go b/wild-workouts/internal/common/client/trainer/openapi_client_gen.go new file mode 100644 index 0000000..0179032 --- /dev/null +++ b/wild-workouts/internal/common/client/trainer/openapi_client_gen.go @@ -0,0 +1,568 @@ +// Package trainer provides primitives to interact with the openapi HTTP API. +// +// Code generated by github.com/deepmap/oapi-codegen version v1.8.2 DO NOT EDIT. +package trainer + +import ( + "bytes" + "context" + "encoding/json" + "fmt" + "io" + "io/ioutil" + "net/http" + "net/url" + "strings" + + "github.com/deepmap/oapi-codegen/pkg/runtime" +) + +// RequestEditorFn is the function signature for the RequestEditor callback function +type RequestEditorFn func(ctx context.Context, req *http.Request) error + +// Doer performs HTTP requests. +// +// The standard http.Client implements this interface. +type HttpRequestDoer interface { + Do(req *http.Request) (*http.Response, error) +} + +// Client which conforms to the OpenAPI3 specification for this service. +type Client struct { + // The endpoint of the server conforming to this interface, with scheme, + // https://api.deepmap.com for example. This can contain a path relative + // to the server, such as https://api.deepmap.com/dev-test, and all the + // paths in the swagger spec will be appended to the server. + Server string + + // Doer for performing requests, typically a *http.Client with any + // customized settings, such as certificate chains. + Client HttpRequestDoer + + // A list of callbacks for modifying requests which are generated before sending over + // the network. + RequestEditors []RequestEditorFn +} + +// ClientOption allows setting custom parameters during construction +type ClientOption func(*Client) error + +// Creates a new Client, with reasonable defaults +func NewClient(server string, opts ...ClientOption) (*Client, error) { + // create a client with sane default values + client := Client{ + Server: server, + } + // mutate client and add all optional params + for _, o := range opts { + if err := o(&client); err != nil { + return nil, err + } + } + // ensure the server URL always has a trailing slash + if !strings.HasSuffix(client.Server, "/") { + client.Server += "/" + } + // create httpClient, if not already present + if client.Client == nil { + client.Client = &http.Client{} + } + return &client, nil +} + +// WithHTTPClient allows overriding the default Doer, which is +// automatically created using http.Client. This is useful for tests. +func WithHTTPClient(doer HttpRequestDoer) ClientOption { + return func(c *Client) error { + c.Client = doer + return nil + } +} + +// WithRequestEditorFn allows setting up a callback function, which will be +// called right before sending the request. This can be used to mutate the request. +func WithRequestEditorFn(fn RequestEditorFn) ClientOption { + return func(c *Client) error { + c.RequestEditors = append(c.RequestEditors, fn) + return nil + } +} + +// The interface specification for the client above. +type ClientInterface interface { + // GetTrainerAvailableHours request + GetTrainerAvailableHours(ctx context.Context, params *GetTrainerAvailableHoursParams, reqEditors ...RequestEditorFn) (*http.Response, error) + + // MakeHourAvailable request with any body + MakeHourAvailableWithBody(ctx context.Context, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*http.Response, error) + + MakeHourAvailable(ctx context.Context, body MakeHourAvailableJSONRequestBody, reqEditors ...RequestEditorFn) (*http.Response, error) + + // MakeHourUnavailable request with any body + MakeHourUnavailableWithBody(ctx context.Context, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*http.Response, error) + + MakeHourUnavailable(ctx context.Context, body MakeHourUnavailableJSONRequestBody, reqEditors ...RequestEditorFn) (*http.Response, error) +} + +func (c *Client) GetTrainerAvailableHours(ctx context.Context, params *GetTrainerAvailableHoursParams, reqEditors ...RequestEditorFn) (*http.Response, error) { + req, err := NewGetTrainerAvailableHoursRequest(c.Server, params) + if err != nil { + return nil, err + } + req = req.WithContext(ctx) + if err := c.applyEditors(ctx, req, reqEditors); err != nil { + return nil, err + } + return c.Client.Do(req) +} + +func (c *Client) MakeHourAvailableWithBody(ctx context.Context, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*http.Response, error) { + req, err := NewMakeHourAvailableRequestWithBody(c.Server, contentType, body) + if err != nil { + return nil, err + } + req = req.WithContext(ctx) + if err := c.applyEditors(ctx, req, reqEditors); err != nil { + return nil, err + } + return c.Client.Do(req) +} + +func (c *Client) MakeHourAvailable(ctx context.Context, body MakeHourAvailableJSONRequestBody, reqEditors ...RequestEditorFn) (*http.Response, error) { + req, err := NewMakeHourAvailableRequest(c.Server, body) + if err != nil { + return nil, err + } + req = req.WithContext(ctx) + if err := c.applyEditors(ctx, req, reqEditors); err != nil { + return nil, err + } + return c.Client.Do(req) +} + +func (c *Client) MakeHourUnavailableWithBody(ctx context.Context, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*http.Response, error) { + req, err := NewMakeHourUnavailableRequestWithBody(c.Server, contentType, body) + if err != nil { + return nil, err + } + req = req.WithContext(ctx) + if err := c.applyEditors(ctx, req, reqEditors); err != nil { + return nil, err + } + return c.Client.Do(req) +} + +func (c *Client) MakeHourUnavailable(ctx context.Context, body MakeHourUnavailableJSONRequestBody, reqEditors ...RequestEditorFn) (*http.Response, error) { + req, err := NewMakeHourUnavailableRequest(c.Server, body) + if err != nil { + return nil, err + } + req = req.WithContext(ctx) + if err := c.applyEditors(ctx, req, reqEditors); err != nil { + return nil, err + } + return c.Client.Do(req) +} + +// NewGetTrainerAvailableHoursRequest generates requests for GetTrainerAvailableHours +func NewGetTrainerAvailableHoursRequest(server string, params *GetTrainerAvailableHoursParams) (*http.Request, error) { + var err error + + serverURL, err := url.Parse(server) + if err != nil { + return nil, err + } + + operationPath := fmt.Sprintf("/trainer/calendar") + if operationPath[0] == '/' { + operationPath = "." + operationPath + } + + queryURL, err := serverURL.Parse(operationPath) + if err != nil { + return nil, err + } + + queryValues := queryURL.Query() + + if queryFrag, err := runtime.StyleParamWithLocation("form", true, "dateFrom", runtime.ParamLocationQuery, params.DateFrom); err != nil { + return nil, err + } else if parsed, err := url.ParseQuery(queryFrag); err != nil { + return nil, err + } else { + for k, v := range parsed { + for _, v2 := range v { + queryValues.Add(k, v2) + } + } + } + + if queryFrag, err := runtime.StyleParamWithLocation("form", true, "dateTo", runtime.ParamLocationQuery, params.DateTo); err != nil { + return nil, err + } else if parsed, err := url.ParseQuery(queryFrag); err != nil { + return nil, err + } else { + for k, v := range parsed { + for _, v2 := range v { + queryValues.Add(k, v2) + } + } + } + + queryURL.RawQuery = queryValues.Encode() + + req, err := http.NewRequest("GET", queryURL.String(), nil) + if err != nil { + return nil, err + } + + return req, nil +} + +// NewMakeHourAvailableRequest calls the generic MakeHourAvailable builder with application/json body +func NewMakeHourAvailableRequest(server string, body MakeHourAvailableJSONRequestBody) (*http.Request, error) { + var bodyReader io.Reader + buf, err := json.Marshal(body) + if err != nil { + return nil, err + } + bodyReader = bytes.NewReader(buf) + return NewMakeHourAvailableRequestWithBody(server, "application/json", bodyReader) +} + +// NewMakeHourAvailableRequestWithBody generates requests for MakeHourAvailable with any type of body +func NewMakeHourAvailableRequestWithBody(server string, contentType string, body io.Reader) (*http.Request, error) { + var err error + + serverURL, err := url.Parse(server) + if err != nil { + return nil, err + } + + operationPath := fmt.Sprintf("/trainer/calendar/make-hour-available") + if operationPath[0] == '/' { + operationPath = "." + operationPath + } + + queryURL, err := serverURL.Parse(operationPath) + if err != nil { + return nil, err + } + + req, err := http.NewRequest("PUT", queryURL.String(), body) + if err != nil { + return nil, err + } + + req.Header.Add("Content-Type", contentType) + + return req, nil +} + +// NewMakeHourUnavailableRequest calls the generic MakeHourUnavailable builder with application/json body +func NewMakeHourUnavailableRequest(server string, body MakeHourUnavailableJSONRequestBody) (*http.Request, error) { + var bodyReader io.Reader + buf, err := json.Marshal(body) + if err != nil { + return nil, err + } + bodyReader = bytes.NewReader(buf) + return NewMakeHourUnavailableRequestWithBody(server, "application/json", bodyReader) +} + +// NewMakeHourUnavailableRequestWithBody generates requests for MakeHourUnavailable with any type of body +func NewMakeHourUnavailableRequestWithBody(server string, contentType string, body io.Reader) (*http.Request, error) { + var err error + + serverURL, err := url.Parse(server) + if err != nil { + return nil, err + } + + operationPath := fmt.Sprintf("/trainer/calendar/make-hour-unavailable") + if operationPath[0] == '/' { + operationPath = "." + operationPath + } + + queryURL, err := serverURL.Parse(operationPath) + if err != nil { + return nil, err + } + + req, err := http.NewRequest("PUT", queryURL.String(), body) + if err != nil { + return nil, err + } + + req.Header.Add("Content-Type", contentType) + + return req, nil +} + +func (c *Client) applyEditors(ctx context.Context, req *http.Request, additionalEditors []RequestEditorFn) error { + for _, r := range c.RequestEditors { + if err := r(ctx, req); err != nil { + return err + } + } + for _, r := range additionalEditors { + if err := r(ctx, req); err != nil { + return err + } + } + return nil +} + +// ClientWithResponses builds on ClientInterface to offer response payloads +type ClientWithResponses struct { + ClientInterface +} + +// NewClientWithResponses creates a new ClientWithResponses, which wraps +// Client with return type handling +func NewClientWithResponses(server string, opts ...ClientOption) (*ClientWithResponses, error) { + client, err := NewClient(server, opts...) + if err != nil { + return nil, err + } + return &ClientWithResponses{client}, nil +} + +// WithBaseURL overrides the baseURL. +func WithBaseURL(baseURL string) ClientOption { + return func(c *Client) error { + newBaseURL, err := url.Parse(baseURL) + if err != nil { + return err + } + c.Server = newBaseURL.String() + return nil + } +} + +// ClientWithResponsesInterface is the interface specification for the client with responses above. +type ClientWithResponsesInterface interface { + // GetTrainerAvailableHours request + GetTrainerAvailableHoursWithResponse(ctx context.Context, params *GetTrainerAvailableHoursParams, reqEditors ...RequestEditorFn) (*GetTrainerAvailableHoursResponse, error) + + // MakeHourAvailable request with any body + MakeHourAvailableWithBodyWithResponse(ctx context.Context, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*MakeHourAvailableResponse, error) + + MakeHourAvailableWithResponse(ctx context.Context, body MakeHourAvailableJSONRequestBody, reqEditors ...RequestEditorFn) (*MakeHourAvailableResponse, error) + + // MakeHourUnavailable request with any body + MakeHourUnavailableWithBodyWithResponse(ctx context.Context, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*MakeHourUnavailableResponse, error) + + MakeHourUnavailableWithResponse(ctx context.Context, body MakeHourUnavailableJSONRequestBody, reqEditors ...RequestEditorFn) (*MakeHourUnavailableResponse, error) +} + +type GetTrainerAvailableHoursResponse struct { + Body []byte + HTTPResponse *http.Response + JSON200 *[]Date + JSONDefault *Error +} + +// Status returns HTTPResponse.Status +func (r GetTrainerAvailableHoursResponse) Status() string { + if r.HTTPResponse != nil { + return r.HTTPResponse.Status + } + return http.StatusText(0) +} + +// StatusCode returns HTTPResponse.StatusCode +func (r GetTrainerAvailableHoursResponse) StatusCode() int { + if r.HTTPResponse != nil { + return r.HTTPResponse.StatusCode + } + return 0 +} + +type MakeHourAvailableResponse struct { + Body []byte + HTTPResponse *http.Response + JSON204 *[]Date + JSONDefault *Error +} + +// Status returns HTTPResponse.Status +func (r MakeHourAvailableResponse) Status() string { + if r.HTTPResponse != nil { + return r.HTTPResponse.Status + } + return http.StatusText(0) +} + +// StatusCode returns HTTPResponse.StatusCode +func (r MakeHourAvailableResponse) StatusCode() int { + if r.HTTPResponse != nil { + return r.HTTPResponse.StatusCode + } + return 0 +} + +type MakeHourUnavailableResponse struct { + Body []byte + HTTPResponse *http.Response + JSON204 *[]Date + JSONDefault *Error +} + +// Status returns HTTPResponse.Status +func (r MakeHourUnavailableResponse) Status() string { + if r.HTTPResponse != nil { + return r.HTTPResponse.Status + } + return http.StatusText(0) +} + +// StatusCode returns HTTPResponse.StatusCode +func (r MakeHourUnavailableResponse) StatusCode() int { + if r.HTTPResponse != nil { + return r.HTTPResponse.StatusCode + } + return 0 +} + +// GetTrainerAvailableHoursWithResponse request returning *GetTrainerAvailableHoursResponse +func (c *ClientWithResponses) GetTrainerAvailableHoursWithResponse(ctx context.Context, params *GetTrainerAvailableHoursParams, reqEditors ...RequestEditorFn) (*GetTrainerAvailableHoursResponse, error) { + rsp, err := c.GetTrainerAvailableHours(ctx, params, reqEditors...) + if err != nil { + return nil, err + } + return ParseGetTrainerAvailableHoursResponse(rsp) +} + +// MakeHourAvailableWithBodyWithResponse request with arbitrary body returning *MakeHourAvailableResponse +func (c *ClientWithResponses) MakeHourAvailableWithBodyWithResponse(ctx context.Context, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*MakeHourAvailableResponse, error) { + rsp, err := c.MakeHourAvailableWithBody(ctx, contentType, body, reqEditors...) + if err != nil { + return nil, err + } + return ParseMakeHourAvailableResponse(rsp) +} + +func (c *ClientWithResponses) MakeHourAvailableWithResponse(ctx context.Context, body MakeHourAvailableJSONRequestBody, reqEditors ...RequestEditorFn) (*MakeHourAvailableResponse, error) { + rsp, err := c.MakeHourAvailable(ctx, body, reqEditors...) + if err != nil { + return nil, err + } + return ParseMakeHourAvailableResponse(rsp) +} + +// MakeHourUnavailableWithBodyWithResponse request with arbitrary body returning *MakeHourUnavailableResponse +func (c *ClientWithResponses) MakeHourUnavailableWithBodyWithResponse(ctx context.Context, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*MakeHourUnavailableResponse, error) { + rsp, err := c.MakeHourUnavailableWithBody(ctx, contentType, body, reqEditors...) + if err != nil { + return nil, err + } + return ParseMakeHourUnavailableResponse(rsp) +} + +func (c *ClientWithResponses) MakeHourUnavailableWithResponse(ctx context.Context, body MakeHourUnavailableJSONRequestBody, reqEditors ...RequestEditorFn) (*MakeHourUnavailableResponse, error) { + rsp, err := c.MakeHourUnavailable(ctx, body, reqEditors...) + if err != nil { + return nil, err + } + return ParseMakeHourUnavailableResponse(rsp) +} + +// ParseGetTrainerAvailableHoursResponse parses an HTTP response from a GetTrainerAvailableHoursWithResponse call +func ParseGetTrainerAvailableHoursResponse(rsp *http.Response) (*GetTrainerAvailableHoursResponse, error) { + bodyBytes, err := ioutil.ReadAll(rsp.Body) + defer rsp.Body.Close() + if err != nil { + return nil, err + } + + response := &GetTrainerAvailableHoursResponse{ + Body: bodyBytes, + HTTPResponse: rsp, + } + + switch { + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 200: + var dest []Date + if err := json.Unmarshal(bodyBytes, &dest); err != nil { + return nil, err + } + response.JSON200 = &dest + + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && true: + var dest Error + if err := json.Unmarshal(bodyBytes, &dest); err != nil { + return nil, err + } + response.JSONDefault = &dest + + } + + return response, nil +} + +// ParseMakeHourAvailableResponse parses an HTTP response from a MakeHourAvailableWithResponse call +func ParseMakeHourAvailableResponse(rsp *http.Response) (*MakeHourAvailableResponse, error) { + bodyBytes, err := ioutil.ReadAll(rsp.Body) + defer rsp.Body.Close() + if err != nil { + return nil, err + } + + response := &MakeHourAvailableResponse{ + Body: bodyBytes, + HTTPResponse: rsp, + } + + switch { + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 204: + var dest []Date + if err := json.Unmarshal(bodyBytes, &dest); err != nil { + return nil, err + } + response.JSON204 = &dest + + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && true: + var dest Error + if err := json.Unmarshal(bodyBytes, &dest); err != nil { + return nil, err + } + response.JSONDefault = &dest + + } + + return response, nil +} + +// ParseMakeHourUnavailableResponse parses an HTTP response from a MakeHourUnavailableWithResponse call +func ParseMakeHourUnavailableResponse(rsp *http.Response) (*MakeHourUnavailableResponse, error) { + bodyBytes, err := ioutil.ReadAll(rsp.Body) + defer rsp.Body.Close() + if err != nil { + return nil, err + } + + response := &MakeHourUnavailableResponse{ + Body: bodyBytes, + HTTPResponse: rsp, + } + + switch { + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 204: + var dest []Date + if err := json.Unmarshal(bodyBytes, &dest); err != nil { + return nil, err + } + response.JSON204 = &dest + + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && true: + var dest Error + if err := json.Unmarshal(bodyBytes, &dest); err != nil { + return nil, err + } + response.JSONDefault = &dest + + } + + return response, nil +} diff --git a/wild-workouts/internal/common/client/trainer/openapi_types.gen.go b/wild-workouts/internal/common/client/trainer/openapi_types.gen.go new file mode 100644 index 0000000..b5925c7 --- /dev/null +++ b/wild-workouts/internal/common/client/trainer/openapi_types.gen.go @@ -0,0 +1,57 @@ +// Package trainer provides primitives to interact with the openapi HTTP API. +// +// Code generated by github.com/deepmap/oapi-codegen version v1.8.2 DO NOT EDIT. +package trainer + +import ( + "time" + + openapi_types "github.com/deepmap/oapi-codegen/pkg/types" +) + +const ( + BearerAuthScopes = "bearerAuth.Scopes" +) + +// Date defines model for Date. +type Date struct { + Date openapi_types.Date `json:"date"` + HasFreeHours bool `json:"hasFreeHours"` + Hours []Hour `json:"hours"` +} + +// Error defines model for Error. +type Error struct { + Message string `json:"message"` + Slug string `json:"slug"` +} + +// Hour defines model for Hour. +type Hour struct { + Available bool `json:"available"` + HasTrainingScheduled bool `json:"hasTrainingScheduled"` + Hour time.Time `json:"hour"` +} + +// HourUpdate defines model for HourUpdate. +type HourUpdate struct { + Hours []time.Time `json:"hours"` +} + +// GetTrainerAvailableHoursParams defines parameters for GetTrainerAvailableHours. +type GetTrainerAvailableHoursParams struct { + DateFrom time.Time `json:"dateFrom"` + DateTo time.Time `json:"dateTo"` +} + +// MakeHourAvailableJSONBody defines parameters for MakeHourAvailable. +type MakeHourAvailableJSONBody HourUpdate + +// MakeHourUnavailableJSONBody defines parameters for MakeHourUnavailable. +type MakeHourUnavailableJSONBody HourUpdate + +// MakeHourAvailableJSONRequestBody defines body for MakeHourAvailable for application/json ContentType. +type MakeHourAvailableJSONRequestBody MakeHourAvailableJSONBody + +// MakeHourUnavailableJSONRequestBody defines body for MakeHourUnavailable for application/json ContentType. +type MakeHourUnavailableJSONRequestBody MakeHourUnavailableJSONBody diff --git a/wild-workouts/internal/common/client/trainings/openapi_client_gen.go b/wild-workouts/internal/common/client/trainings/openapi_client_gen.go new file mode 100644 index 0000000..d9e8ed0 --- /dev/null +++ b/wild-workouts/internal/common/client/trainings/openapi_client_gen.go @@ -0,0 +1,1004 @@ +// Package trainings provides primitives to interact with the openapi HTTP API. +// +// Code generated by github.com/deepmap/oapi-codegen version v1.8.2 DO NOT EDIT. +package trainings + +import ( + "bytes" + "context" + "encoding/json" + "fmt" + "io" + "io/ioutil" + "net/http" + "net/url" + "strings" + + "github.com/deepmap/oapi-codegen/pkg/runtime" +) + +// RequestEditorFn is the function signature for the RequestEditor callback function +type RequestEditorFn func(ctx context.Context, req *http.Request) error + +// Doer performs HTTP requests. +// +// The standard http.Client implements this interface. +type HttpRequestDoer interface { + Do(req *http.Request) (*http.Response, error) +} + +// Client which conforms to the OpenAPI3 specification for this service. +type Client struct { + // The endpoint of the server conforming to this interface, with scheme, + // https://api.deepmap.com for example. This can contain a path relative + // to the server, such as https://api.deepmap.com/dev-test, and all the + // paths in the swagger spec will be appended to the server. + Server string + + // Doer for performing requests, typically a *http.Client with any + // customized settings, such as certificate chains. + Client HttpRequestDoer + + // A list of callbacks for modifying requests which are generated before sending over + // the network. + RequestEditors []RequestEditorFn +} + +// ClientOption allows setting custom parameters during construction +type ClientOption func(*Client) error + +// Creates a new Client, with reasonable defaults +func NewClient(server string, opts ...ClientOption) (*Client, error) { + // create a client with sane default values + client := Client{ + Server: server, + } + // mutate client and add all optional params + for _, o := range opts { + if err := o(&client); err != nil { + return nil, err + } + } + // ensure the server URL always has a trailing slash + if !strings.HasSuffix(client.Server, "/") { + client.Server += "/" + } + // create httpClient, if not already present + if client.Client == nil { + client.Client = &http.Client{} + } + return &client, nil +} + +// WithHTTPClient allows overriding the default Doer, which is +// automatically created using http.Client. This is useful for tests. +func WithHTTPClient(doer HttpRequestDoer) ClientOption { + return func(c *Client) error { + c.Client = doer + return nil + } +} + +// WithRequestEditorFn allows setting up a callback function, which will be +// called right before sending the request. This can be used to mutate the request. +func WithRequestEditorFn(fn RequestEditorFn) ClientOption { + return func(c *Client) error { + c.RequestEditors = append(c.RequestEditors, fn) + return nil + } +} + +// The interface specification for the client above. +type ClientInterface interface { + // GetTrainings request + GetTrainings(ctx context.Context, reqEditors ...RequestEditorFn) (*http.Response, error) + + // CreateTraining request with any body + CreateTrainingWithBody(ctx context.Context, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*http.Response, error) + + CreateTraining(ctx context.Context, body CreateTrainingJSONRequestBody, reqEditors ...RequestEditorFn) (*http.Response, error) + + // CancelTraining request + CancelTraining(ctx context.Context, trainingUUID string, reqEditors ...RequestEditorFn) (*http.Response, error) + + // ApproveRescheduleTraining request + ApproveRescheduleTraining(ctx context.Context, trainingUUID string, reqEditors ...RequestEditorFn) (*http.Response, error) + + // RejectRescheduleTraining request + RejectRescheduleTraining(ctx context.Context, trainingUUID string, reqEditors ...RequestEditorFn) (*http.Response, error) + + // RequestRescheduleTraining request with any body + RequestRescheduleTrainingWithBody(ctx context.Context, trainingUUID string, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*http.Response, error) + + RequestRescheduleTraining(ctx context.Context, trainingUUID string, body RequestRescheduleTrainingJSONRequestBody, reqEditors ...RequestEditorFn) (*http.Response, error) + + // RescheduleTraining request with any body + RescheduleTrainingWithBody(ctx context.Context, trainingUUID string, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*http.Response, error) + + RescheduleTraining(ctx context.Context, trainingUUID string, body RescheduleTrainingJSONRequestBody, reqEditors ...RequestEditorFn) (*http.Response, error) +} + +func (c *Client) GetTrainings(ctx context.Context, reqEditors ...RequestEditorFn) (*http.Response, error) { + req, err := NewGetTrainingsRequest(c.Server) + if err != nil { + return nil, err + } + req = req.WithContext(ctx) + if err := c.applyEditors(ctx, req, reqEditors); err != nil { + return nil, err + } + return c.Client.Do(req) +} + +func (c *Client) CreateTrainingWithBody(ctx context.Context, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*http.Response, error) { + req, err := NewCreateTrainingRequestWithBody(c.Server, contentType, body) + if err != nil { + return nil, err + } + req = req.WithContext(ctx) + if err := c.applyEditors(ctx, req, reqEditors); err != nil { + return nil, err + } + return c.Client.Do(req) +} + +func (c *Client) CreateTraining(ctx context.Context, body CreateTrainingJSONRequestBody, reqEditors ...RequestEditorFn) (*http.Response, error) { + req, err := NewCreateTrainingRequest(c.Server, body) + if err != nil { + return nil, err + } + req = req.WithContext(ctx) + if err := c.applyEditors(ctx, req, reqEditors); err != nil { + return nil, err + } + return c.Client.Do(req) +} + +func (c *Client) CancelTraining(ctx context.Context, trainingUUID string, reqEditors ...RequestEditorFn) (*http.Response, error) { + req, err := NewCancelTrainingRequest(c.Server, trainingUUID) + if err != nil { + return nil, err + } + req = req.WithContext(ctx) + if err := c.applyEditors(ctx, req, reqEditors); err != nil { + return nil, err + } + return c.Client.Do(req) +} + +func (c *Client) ApproveRescheduleTraining(ctx context.Context, trainingUUID string, reqEditors ...RequestEditorFn) (*http.Response, error) { + req, err := NewApproveRescheduleTrainingRequest(c.Server, trainingUUID) + if err != nil { + return nil, err + } + req = req.WithContext(ctx) + if err := c.applyEditors(ctx, req, reqEditors); err != nil { + return nil, err + } + return c.Client.Do(req) +} + +func (c *Client) RejectRescheduleTraining(ctx context.Context, trainingUUID string, reqEditors ...RequestEditorFn) (*http.Response, error) { + req, err := NewRejectRescheduleTrainingRequest(c.Server, trainingUUID) + if err != nil { + return nil, err + } + req = req.WithContext(ctx) + if err := c.applyEditors(ctx, req, reqEditors); err != nil { + return nil, err + } + return c.Client.Do(req) +} + +func (c *Client) RequestRescheduleTrainingWithBody(ctx context.Context, trainingUUID string, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*http.Response, error) { + req, err := NewRequestRescheduleTrainingRequestWithBody(c.Server, trainingUUID, contentType, body) + if err != nil { + return nil, err + } + req = req.WithContext(ctx) + if err := c.applyEditors(ctx, req, reqEditors); err != nil { + return nil, err + } + return c.Client.Do(req) +} + +func (c *Client) RequestRescheduleTraining(ctx context.Context, trainingUUID string, body RequestRescheduleTrainingJSONRequestBody, reqEditors ...RequestEditorFn) (*http.Response, error) { + req, err := NewRequestRescheduleTrainingRequest(c.Server, trainingUUID, body) + if err != nil { + return nil, err + } + req = req.WithContext(ctx) + if err := c.applyEditors(ctx, req, reqEditors); err != nil { + return nil, err + } + return c.Client.Do(req) +} + +func (c *Client) RescheduleTrainingWithBody(ctx context.Context, trainingUUID string, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*http.Response, error) { + req, err := NewRescheduleTrainingRequestWithBody(c.Server, trainingUUID, contentType, body) + if err != nil { + return nil, err + } + req = req.WithContext(ctx) + if err := c.applyEditors(ctx, req, reqEditors); err != nil { + return nil, err + } + return c.Client.Do(req) +} + +func (c *Client) RescheduleTraining(ctx context.Context, trainingUUID string, body RescheduleTrainingJSONRequestBody, reqEditors ...RequestEditorFn) (*http.Response, error) { + req, err := NewRescheduleTrainingRequest(c.Server, trainingUUID, body) + if err != nil { + return nil, err + } + req = req.WithContext(ctx) + if err := c.applyEditors(ctx, req, reqEditors); err != nil { + return nil, err + } + return c.Client.Do(req) +} + +// NewGetTrainingsRequest generates requests for GetTrainings +func NewGetTrainingsRequest(server string) (*http.Request, error) { + var err error + + serverURL, err := url.Parse(server) + if err != nil { + return nil, err + } + + operationPath := fmt.Sprintf("/trainings") + if operationPath[0] == '/' { + operationPath = "." + operationPath + } + + queryURL, err := serverURL.Parse(operationPath) + if err != nil { + return nil, err + } + + req, err := http.NewRequest("GET", queryURL.String(), nil) + if err != nil { + return nil, err + } + + return req, nil +} + +// NewCreateTrainingRequest calls the generic CreateTraining builder with application/json body +func NewCreateTrainingRequest(server string, body CreateTrainingJSONRequestBody) (*http.Request, error) { + var bodyReader io.Reader + buf, err := json.Marshal(body) + if err != nil { + return nil, err + } + bodyReader = bytes.NewReader(buf) + return NewCreateTrainingRequestWithBody(server, "application/json", bodyReader) +} + +// NewCreateTrainingRequestWithBody generates requests for CreateTraining with any type of body +func NewCreateTrainingRequestWithBody(server string, contentType string, body io.Reader) (*http.Request, error) { + var err error + + serverURL, err := url.Parse(server) + if err != nil { + return nil, err + } + + operationPath := fmt.Sprintf("/trainings") + if operationPath[0] == '/' { + operationPath = "." + operationPath + } + + queryURL, err := serverURL.Parse(operationPath) + if err != nil { + return nil, err + } + + req, err := http.NewRequest("POST", queryURL.String(), body) + if err != nil { + return nil, err + } + + req.Header.Add("Content-Type", contentType) + + return req, nil +} + +// NewCancelTrainingRequest generates requests for CancelTraining +func NewCancelTrainingRequest(server string, trainingUUID string) (*http.Request, error) { + var err error + + var pathParam0 string + + pathParam0, err = runtime.StyleParamWithLocation("simple", false, "trainingUUID", runtime.ParamLocationPath, trainingUUID) + if err != nil { + return nil, err + } + + serverURL, err := url.Parse(server) + if err != nil { + return nil, err + } + + operationPath := fmt.Sprintf("/trainings/%s", pathParam0) + if operationPath[0] == '/' { + operationPath = "." + operationPath + } + + queryURL, err := serverURL.Parse(operationPath) + if err != nil { + return nil, err + } + + req, err := http.NewRequest("DELETE", queryURL.String(), nil) + if err != nil { + return nil, err + } + + return req, nil +} + +// NewApproveRescheduleTrainingRequest generates requests for ApproveRescheduleTraining +func NewApproveRescheduleTrainingRequest(server string, trainingUUID string) (*http.Request, error) { + var err error + + var pathParam0 string + + pathParam0, err = runtime.StyleParamWithLocation("simple", false, "trainingUUID", runtime.ParamLocationPath, trainingUUID) + if err != nil { + return nil, err + } + + serverURL, err := url.Parse(server) + if err != nil { + return nil, err + } + + operationPath := fmt.Sprintf("/trainings/%s/approve-reschedule", pathParam0) + if operationPath[0] == '/' { + operationPath = "." + operationPath + } + + queryURL, err := serverURL.Parse(operationPath) + if err != nil { + return nil, err + } + + req, err := http.NewRequest("PUT", queryURL.String(), nil) + if err != nil { + return nil, err + } + + return req, nil +} + +// NewRejectRescheduleTrainingRequest generates requests for RejectRescheduleTraining +func NewRejectRescheduleTrainingRequest(server string, trainingUUID string) (*http.Request, error) { + var err error + + var pathParam0 string + + pathParam0, err = runtime.StyleParamWithLocation("simple", false, "trainingUUID", runtime.ParamLocationPath, trainingUUID) + if err != nil { + return nil, err + } + + serverURL, err := url.Parse(server) + if err != nil { + return nil, err + } + + operationPath := fmt.Sprintf("/trainings/%s/reject-reschedule", pathParam0) + if operationPath[0] == '/' { + operationPath = "." + operationPath + } + + queryURL, err := serverURL.Parse(operationPath) + if err != nil { + return nil, err + } + + req, err := http.NewRequest("PUT", queryURL.String(), nil) + if err != nil { + return nil, err + } + + return req, nil +} + +// NewRequestRescheduleTrainingRequest calls the generic RequestRescheduleTraining builder with application/json body +func NewRequestRescheduleTrainingRequest(server string, trainingUUID string, body RequestRescheduleTrainingJSONRequestBody) (*http.Request, error) { + var bodyReader io.Reader + buf, err := json.Marshal(body) + if err != nil { + return nil, err + } + bodyReader = bytes.NewReader(buf) + return NewRequestRescheduleTrainingRequestWithBody(server, trainingUUID, "application/json", bodyReader) +} + +// NewRequestRescheduleTrainingRequestWithBody generates requests for RequestRescheduleTraining with any type of body +func NewRequestRescheduleTrainingRequestWithBody(server string, trainingUUID string, contentType string, body io.Reader) (*http.Request, error) { + var err error + + var pathParam0 string + + pathParam0, err = runtime.StyleParamWithLocation("simple", false, "trainingUUID", runtime.ParamLocationPath, trainingUUID) + if err != nil { + return nil, err + } + + serverURL, err := url.Parse(server) + if err != nil { + return nil, err + } + + operationPath := fmt.Sprintf("/trainings/%s/request-reschedule", pathParam0) + if operationPath[0] == '/' { + operationPath = "." + operationPath + } + + queryURL, err := serverURL.Parse(operationPath) + if err != nil { + return nil, err + } + + req, err := http.NewRequest("PUT", queryURL.String(), body) + if err != nil { + return nil, err + } + + req.Header.Add("Content-Type", contentType) + + return req, nil +} + +// NewRescheduleTrainingRequest calls the generic RescheduleTraining builder with application/json body +func NewRescheduleTrainingRequest(server string, trainingUUID string, body RescheduleTrainingJSONRequestBody) (*http.Request, error) { + var bodyReader io.Reader + buf, err := json.Marshal(body) + if err != nil { + return nil, err + } + bodyReader = bytes.NewReader(buf) + return NewRescheduleTrainingRequestWithBody(server, trainingUUID, "application/json", bodyReader) +} + +// NewRescheduleTrainingRequestWithBody generates requests for RescheduleTraining with any type of body +func NewRescheduleTrainingRequestWithBody(server string, trainingUUID string, contentType string, body io.Reader) (*http.Request, error) { + var err error + + var pathParam0 string + + pathParam0, err = runtime.StyleParamWithLocation("simple", false, "trainingUUID", runtime.ParamLocationPath, trainingUUID) + if err != nil { + return nil, err + } + + serverURL, err := url.Parse(server) + if err != nil { + return nil, err + } + + operationPath := fmt.Sprintf("/trainings/%s/reschedule", pathParam0) + if operationPath[0] == '/' { + operationPath = "." + operationPath + } + + queryURL, err := serverURL.Parse(operationPath) + if err != nil { + return nil, err + } + + req, err := http.NewRequest("PUT", queryURL.String(), body) + if err != nil { + return nil, err + } + + req.Header.Add("Content-Type", contentType) + + return req, nil +} + +func (c *Client) applyEditors(ctx context.Context, req *http.Request, additionalEditors []RequestEditorFn) error { + for _, r := range c.RequestEditors { + if err := r(ctx, req); err != nil { + return err + } + } + for _, r := range additionalEditors { + if err := r(ctx, req); err != nil { + return err + } + } + return nil +} + +// ClientWithResponses builds on ClientInterface to offer response payloads +type ClientWithResponses struct { + ClientInterface +} + +// NewClientWithResponses creates a new ClientWithResponses, which wraps +// Client with return type handling +func NewClientWithResponses(server string, opts ...ClientOption) (*ClientWithResponses, error) { + client, err := NewClient(server, opts...) + if err != nil { + return nil, err + } + return &ClientWithResponses{client}, nil +} + +// WithBaseURL overrides the baseURL. +func WithBaseURL(baseURL string) ClientOption { + return func(c *Client) error { + newBaseURL, err := url.Parse(baseURL) + if err != nil { + return err + } + c.Server = newBaseURL.String() + return nil + } +} + +// ClientWithResponsesInterface is the interface specification for the client with responses above. +type ClientWithResponsesInterface interface { + // GetTrainings request + GetTrainingsWithResponse(ctx context.Context, reqEditors ...RequestEditorFn) (*GetTrainingsResponse, error) + + // CreateTraining request with any body + CreateTrainingWithBodyWithResponse(ctx context.Context, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*CreateTrainingResponse, error) + + CreateTrainingWithResponse(ctx context.Context, body CreateTrainingJSONRequestBody, reqEditors ...RequestEditorFn) (*CreateTrainingResponse, error) + + // CancelTraining request + CancelTrainingWithResponse(ctx context.Context, trainingUUID string, reqEditors ...RequestEditorFn) (*CancelTrainingResponse, error) + + // ApproveRescheduleTraining request + ApproveRescheduleTrainingWithResponse(ctx context.Context, trainingUUID string, reqEditors ...RequestEditorFn) (*ApproveRescheduleTrainingResponse, error) + + // RejectRescheduleTraining request + RejectRescheduleTrainingWithResponse(ctx context.Context, trainingUUID string, reqEditors ...RequestEditorFn) (*RejectRescheduleTrainingResponse, error) + + // RequestRescheduleTraining request with any body + RequestRescheduleTrainingWithBodyWithResponse(ctx context.Context, trainingUUID string, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*RequestRescheduleTrainingResponse, error) + + RequestRescheduleTrainingWithResponse(ctx context.Context, trainingUUID string, body RequestRescheduleTrainingJSONRequestBody, reqEditors ...RequestEditorFn) (*RequestRescheduleTrainingResponse, error) + + // RescheduleTraining request with any body + RescheduleTrainingWithBodyWithResponse(ctx context.Context, trainingUUID string, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*RescheduleTrainingResponse, error) + + RescheduleTrainingWithResponse(ctx context.Context, trainingUUID string, body RescheduleTrainingJSONRequestBody, reqEditors ...RequestEditorFn) (*RescheduleTrainingResponse, error) +} + +type GetTrainingsResponse struct { + Body []byte + HTTPResponse *http.Response + JSON200 *Trainings + JSONDefault *Error +} + +// Status returns HTTPResponse.Status +func (r GetTrainingsResponse) Status() string { + if r.HTTPResponse != nil { + return r.HTTPResponse.Status + } + return http.StatusText(0) +} + +// StatusCode returns HTTPResponse.StatusCode +func (r GetTrainingsResponse) StatusCode() int { + if r.HTTPResponse != nil { + return r.HTTPResponse.StatusCode + } + return 0 +} + +type CreateTrainingResponse struct { + Body []byte + HTTPResponse *http.Response + JSONDefault *Error +} + +// Status returns HTTPResponse.Status +func (r CreateTrainingResponse) Status() string { + if r.HTTPResponse != nil { + return r.HTTPResponse.Status + } + return http.StatusText(0) +} + +// StatusCode returns HTTPResponse.StatusCode +func (r CreateTrainingResponse) StatusCode() int { + if r.HTTPResponse != nil { + return r.HTTPResponse.StatusCode + } + return 0 +} + +type CancelTrainingResponse struct { + Body []byte + HTTPResponse *http.Response + JSONDefault *Error +} + +// Status returns HTTPResponse.Status +func (r CancelTrainingResponse) Status() string { + if r.HTTPResponse != nil { + return r.HTTPResponse.Status + } + return http.StatusText(0) +} + +// StatusCode returns HTTPResponse.StatusCode +func (r CancelTrainingResponse) StatusCode() int { + if r.HTTPResponse != nil { + return r.HTTPResponse.StatusCode + } + return 0 +} + +type ApproveRescheduleTrainingResponse struct { + Body []byte + HTTPResponse *http.Response + JSONDefault *Error +} + +// Status returns HTTPResponse.Status +func (r ApproveRescheduleTrainingResponse) Status() string { + if r.HTTPResponse != nil { + return r.HTTPResponse.Status + } + return http.StatusText(0) +} + +// StatusCode returns HTTPResponse.StatusCode +func (r ApproveRescheduleTrainingResponse) StatusCode() int { + if r.HTTPResponse != nil { + return r.HTTPResponse.StatusCode + } + return 0 +} + +type RejectRescheduleTrainingResponse struct { + Body []byte + HTTPResponse *http.Response + JSONDefault *Error +} + +// Status returns HTTPResponse.Status +func (r RejectRescheduleTrainingResponse) Status() string { + if r.HTTPResponse != nil { + return r.HTTPResponse.Status + } + return http.StatusText(0) +} + +// StatusCode returns HTTPResponse.StatusCode +func (r RejectRescheduleTrainingResponse) StatusCode() int { + if r.HTTPResponse != nil { + return r.HTTPResponse.StatusCode + } + return 0 +} + +type RequestRescheduleTrainingResponse struct { + Body []byte + HTTPResponse *http.Response + JSONDefault *Error +} + +// Status returns HTTPResponse.Status +func (r RequestRescheduleTrainingResponse) Status() string { + if r.HTTPResponse != nil { + return r.HTTPResponse.Status + } + return http.StatusText(0) +} + +// StatusCode returns HTTPResponse.StatusCode +func (r RequestRescheduleTrainingResponse) StatusCode() int { + if r.HTTPResponse != nil { + return r.HTTPResponse.StatusCode + } + return 0 +} + +type RescheduleTrainingResponse struct { + Body []byte + HTTPResponse *http.Response + JSONDefault *Error +} + +// Status returns HTTPResponse.Status +func (r RescheduleTrainingResponse) Status() string { + if r.HTTPResponse != nil { + return r.HTTPResponse.Status + } + return http.StatusText(0) +} + +// StatusCode returns HTTPResponse.StatusCode +func (r RescheduleTrainingResponse) StatusCode() int { + if r.HTTPResponse != nil { + return r.HTTPResponse.StatusCode + } + return 0 +} + +// GetTrainingsWithResponse request returning *GetTrainingsResponse +func (c *ClientWithResponses) GetTrainingsWithResponse(ctx context.Context, reqEditors ...RequestEditorFn) (*GetTrainingsResponse, error) { + rsp, err := c.GetTrainings(ctx, reqEditors...) + if err != nil { + return nil, err + } + return ParseGetTrainingsResponse(rsp) +} + +// CreateTrainingWithBodyWithResponse request with arbitrary body returning *CreateTrainingResponse +func (c *ClientWithResponses) CreateTrainingWithBodyWithResponse(ctx context.Context, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*CreateTrainingResponse, error) { + rsp, err := c.CreateTrainingWithBody(ctx, contentType, body, reqEditors...) + if err != nil { + return nil, err + } + return ParseCreateTrainingResponse(rsp) +} + +func (c *ClientWithResponses) CreateTrainingWithResponse(ctx context.Context, body CreateTrainingJSONRequestBody, reqEditors ...RequestEditorFn) (*CreateTrainingResponse, error) { + rsp, err := c.CreateTraining(ctx, body, reqEditors...) + if err != nil { + return nil, err + } + return ParseCreateTrainingResponse(rsp) +} + +// CancelTrainingWithResponse request returning *CancelTrainingResponse +func (c *ClientWithResponses) CancelTrainingWithResponse(ctx context.Context, trainingUUID string, reqEditors ...RequestEditorFn) (*CancelTrainingResponse, error) { + rsp, err := c.CancelTraining(ctx, trainingUUID, reqEditors...) + if err != nil { + return nil, err + } + return ParseCancelTrainingResponse(rsp) +} + +// ApproveRescheduleTrainingWithResponse request returning *ApproveRescheduleTrainingResponse +func (c *ClientWithResponses) ApproveRescheduleTrainingWithResponse(ctx context.Context, trainingUUID string, reqEditors ...RequestEditorFn) (*ApproveRescheduleTrainingResponse, error) { + rsp, err := c.ApproveRescheduleTraining(ctx, trainingUUID, reqEditors...) + if err != nil { + return nil, err + } + return ParseApproveRescheduleTrainingResponse(rsp) +} + +// RejectRescheduleTrainingWithResponse request returning *RejectRescheduleTrainingResponse +func (c *ClientWithResponses) RejectRescheduleTrainingWithResponse(ctx context.Context, trainingUUID string, reqEditors ...RequestEditorFn) (*RejectRescheduleTrainingResponse, error) { + rsp, err := c.RejectRescheduleTraining(ctx, trainingUUID, reqEditors...) + if err != nil { + return nil, err + } + return ParseRejectRescheduleTrainingResponse(rsp) +} + +// RequestRescheduleTrainingWithBodyWithResponse request with arbitrary body returning *RequestRescheduleTrainingResponse +func (c *ClientWithResponses) RequestRescheduleTrainingWithBodyWithResponse(ctx context.Context, trainingUUID string, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*RequestRescheduleTrainingResponse, error) { + rsp, err := c.RequestRescheduleTrainingWithBody(ctx, trainingUUID, contentType, body, reqEditors...) + if err != nil { + return nil, err + } + return ParseRequestRescheduleTrainingResponse(rsp) +} + +func (c *ClientWithResponses) RequestRescheduleTrainingWithResponse(ctx context.Context, trainingUUID string, body RequestRescheduleTrainingJSONRequestBody, reqEditors ...RequestEditorFn) (*RequestRescheduleTrainingResponse, error) { + rsp, err := c.RequestRescheduleTraining(ctx, trainingUUID, body, reqEditors...) + if err != nil { + return nil, err + } + return ParseRequestRescheduleTrainingResponse(rsp) +} + +// RescheduleTrainingWithBodyWithResponse request with arbitrary body returning *RescheduleTrainingResponse +func (c *ClientWithResponses) RescheduleTrainingWithBodyWithResponse(ctx context.Context, trainingUUID string, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*RescheduleTrainingResponse, error) { + rsp, err := c.RescheduleTrainingWithBody(ctx, trainingUUID, contentType, body, reqEditors...) + if err != nil { + return nil, err + } + return ParseRescheduleTrainingResponse(rsp) +} + +func (c *ClientWithResponses) RescheduleTrainingWithResponse(ctx context.Context, trainingUUID string, body RescheduleTrainingJSONRequestBody, reqEditors ...RequestEditorFn) (*RescheduleTrainingResponse, error) { + rsp, err := c.RescheduleTraining(ctx, trainingUUID, body, reqEditors...) + if err != nil { + return nil, err + } + return ParseRescheduleTrainingResponse(rsp) +} + +// ParseGetTrainingsResponse parses an HTTP response from a GetTrainingsWithResponse call +func ParseGetTrainingsResponse(rsp *http.Response) (*GetTrainingsResponse, error) { + bodyBytes, err := ioutil.ReadAll(rsp.Body) + defer rsp.Body.Close() + if err != nil { + return nil, err + } + + response := &GetTrainingsResponse{ + Body: bodyBytes, + HTTPResponse: rsp, + } + + switch { + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 200: + var dest Trainings + if err := json.Unmarshal(bodyBytes, &dest); err != nil { + return nil, err + } + response.JSON200 = &dest + + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && true: + var dest Error + if err := json.Unmarshal(bodyBytes, &dest); err != nil { + return nil, err + } + response.JSONDefault = &dest + + } + + return response, nil +} + +// ParseCreateTrainingResponse parses an HTTP response from a CreateTrainingWithResponse call +func ParseCreateTrainingResponse(rsp *http.Response) (*CreateTrainingResponse, error) { + bodyBytes, err := ioutil.ReadAll(rsp.Body) + defer rsp.Body.Close() + if err != nil { + return nil, err + } + + response := &CreateTrainingResponse{ + Body: bodyBytes, + HTTPResponse: rsp, + } + + switch { + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && true: + var dest Error + if err := json.Unmarshal(bodyBytes, &dest); err != nil { + return nil, err + } + response.JSONDefault = &dest + + } + + return response, nil +} + +// ParseCancelTrainingResponse parses an HTTP response from a CancelTrainingWithResponse call +func ParseCancelTrainingResponse(rsp *http.Response) (*CancelTrainingResponse, error) { + bodyBytes, err := ioutil.ReadAll(rsp.Body) + defer rsp.Body.Close() + if err != nil { + return nil, err + } + + response := &CancelTrainingResponse{ + Body: bodyBytes, + HTTPResponse: rsp, + } + + switch { + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && true: + var dest Error + if err := json.Unmarshal(bodyBytes, &dest); err != nil { + return nil, err + } + response.JSONDefault = &dest + + } + + return response, nil +} + +// ParseApproveRescheduleTrainingResponse parses an HTTP response from a ApproveRescheduleTrainingWithResponse call +func ParseApproveRescheduleTrainingResponse(rsp *http.Response) (*ApproveRescheduleTrainingResponse, error) { + bodyBytes, err := ioutil.ReadAll(rsp.Body) + defer rsp.Body.Close() + if err != nil { + return nil, err + } + + response := &ApproveRescheduleTrainingResponse{ + Body: bodyBytes, + HTTPResponse: rsp, + } + + switch { + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && true: + var dest Error + if err := json.Unmarshal(bodyBytes, &dest); err != nil { + return nil, err + } + response.JSONDefault = &dest + + } + + return response, nil +} + +// ParseRejectRescheduleTrainingResponse parses an HTTP response from a RejectRescheduleTrainingWithResponse call +func ParseRejectRescheduleTrainingResponse(rsp *http.Response) (*RejectRescheduleTrainingResponse, error) { + bodyBytes, err := ioutil.ReadAll(rsp.Body) + defer rsp.Body.Close() + if err != nil { + return nil, err + } + + response := &RejectRescheduleTrainingResponse{ + Body: bodyBytes, + HTTPResponse: rsp, + } + + switch { + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && true: + var dest Error + if err := json.Unmarshal(bodyBytes, &dest); err != nil { + return nil, err + } + response.JSONDefault = &dest + + } + + return response, nil +} + +// ParseRequestRescheduleTrainingResponse parses an HTTP response from a RequestRescheduleTrainingWithResponse call +func ParseRequestRescheduleTrainingResponse(rsp *http.Response) (*RequestRescheduleTrainingResponse, error) { + bodyBytes, err := ioutil.ReadAll(rsp.Body) + defer rsp.Body.Close() + if err != nil { + return nil, err + } + + response := &RequestRescheduleTrainingResponse{ + Body: bodyBytes, + HTTPResponse: rsp, + } + + switch { + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && true: + var dest Error + if err := json.Unmarshal(bodyBytes, &dest); err != nil { + return nil, err + } + response.JSONDefault = &dest + + } + + return response, nil +} + +// ParseRescheduleTrainingResponse parses an HTTP response from a RescheduleTrainingWithResponse call +func ParseRescheduleTrainingResponse(rsp *http.Response) (*RescheduleTrainingResponse, error) { + bodyBytes, err := ioutil.ReadAll(rsp.Body) + defer rsp.Body.Close() + if err != nil { + return nil, err + } + + response := &RescheduleTrainingResponse{ + Body: bodyBytes, + HTTPResponse: rsp, + } + + switch { + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && true: + var dest Error + if err := json.Unmarshal(bodyBytes, &dest); err != nil { + return nil, err + } + response.JSONDefault = &dest + + } + + return response, nil +} diff --git a/wild-workouts/internal/common/client/trainings/openapi_types.gen.go b/wild-workouts/internal/common/client/trainings/openapi_types.gen.go new file mode 100644 index 0000000..c7149c0 --- /dev/null +++ b/wild-workouts/internal/common/client/trainings/openapi_types.gen.go @@ -0,0 +1,60 @@ +// Package trainings provides primitives to interact with the openapi HTTP API. +// +// Code generated by github.com/deepmap/oapi-codegen version v1.8.2 DO NOT EDIT. +package trainings + +import ( + "time" +) + +const ( + BearerAuthScopes = "bearerAuth.Scopes" +) + +// Error defines model for Error. +type Error struct { + Message string `json:"message"` + Slug string `json:"slug"` +} + +// PostTraining defines model for PostTraining. +type PostTraining struct { + Notes string `json:"notes"` + Time time.Time `json:"time"` +} + +// Training defines model for Training. +type Training struct { + CanBeCancelled bool `json:"canBeCancelled"` + MoveProposedBy *string `json:"moveProposedBy,omitempty"` + MoveRequiresAccept bool `json:"moveRequiresAccept"` + Notes string `json:"notes"` + ProposedTime *time.Time `json:"proposedTime,omitempty"` + Time time.Time `json:"time"` + User string `json:"user"` + UserUuid string `json:"userUuid"` + Uuid string `json:"uuid"` +} + +// Trainings defines model for Trainings. +type Trainings struct { + Trainings []Training `json:"trainings"` +} + +// CreateTrainingJSONBody defines parameters for CreateTraining. +type CreateTrainingJSONBody PostTraining + +// RequestRescheduleTrainingJSONBody defines parameters for RequestRescheduleTraining. +type RequestRescheduleTrainingJSONBody PostTraining + +// RescheduleTrainingJSONBody defines parameters for RescheduleTraining. +type RescheduleTrainingJSONBody PostTraining + +// CreateTrainingJSONRequestBody defines body for CreateTraining for application/json ContentType. +type CreateTrainingJSONRequestBody CreateTrainingJSONBody + +// RequestRescheduleTrainingJSONRequestBody defines body for RequestRescheduleTraining for application/json ContentType. +type RequestRescheduleTrainingJSONRequestBody RequestRescheduleTrainingJSONBody + +// RescheduleTrainingJSONRequestBody defines body for RescheduleTraining for application/json ContentType. +type RescheduleTrainingJSONRequestBody RescheduleTrainingJSONBody diff --git a/wild-workouts/internal/common/client/users/openapi_client_gen.go b/wild-workouts/internal/common/client/users/openapi_client_gen.go new file mode 100644 index 0000000..67dcf69 --- /dev/null +++ b/wild-workouts/internal/common/client/users/openapi_client_gen.go @@ -0,0 +1,234 @@ +// Package users provides primitives to interact with the openapi HTTP API. +// +// Code generated by github.com/deepmap/oapi-codegen version v1.8.2 DO NOT EDIT. +package users + +import ( + "context" + "encoding/json" + "fmt" + "io/ioutil" + "net/http" + "net/url" + "strings" +) + +// RequestEditorFn is the function signature for the RequestEditor callback function +type RequestEditorFn func(ctx context.Context, req *http.Request) error + +// Doer performs HTTP requests. +// +// The standard http.Client implements this interface. +type HttpRequestDoer interface { + Do(req *http.Request) (*http.Response, error) +} + +// Client which conforms to the OpenAPI3 specification for this service. +type Client struct { + // The endpoint of the server conforming to this interface, with scheme, + // https://api.deepmap.com for example. This can contain a path relative + // to the server, such as https://api.deepmap.com/dev-test, and all the + // paths in the swagger spec will be appended to the server. + Server string + + // Doer for performing requests, typically a *http.Client with any + // customized settings, such as certificate chains. + Client HttpRequestDoer + + // A list of callbacks for modifying requests which are generated before sending over + // the network. + RequestEditors []RequestEditorFn +} + +// ClientOption allows setting custom parameters during construction +type ClientOption func(*Client) error + +// Creates a new Client, with reasonable defaults +func NewClient(server string, opts ...ClientOption) (*Client, error) { + // create a client with sane default values + client := Client{ + Server: server, + } + // mutate client and add all optional params + for _, o := range opts { + if err := o(&client); err != nil { + return nil, err + } + } + // ensure the server URL always has a trailing slash + if !strings.HasSuffix(client.Server, "/") { + client.Server += "/" + } + // create httpClient, if not already present + if client.Client == nil { + client.Client = &http.Client{} + } + return &client, nil +} + +// WithHTTPClient allows overriding the default Doer, which is +// automatically created using http.Client. This is useful for tests. +func WithHTTPClient(doer HttpRequestDoer) ClientOption { + return func(c *Client) error { + c.Client = doer + return nil + } +} + +// WithRequestEditorFn allows setting up a callback function, which will be +// called right before sending the request. This can be used to mutate the request. +func WithRequestEditorFn(fn RequestEditorFn) ClientOption { + return func(c *Client) error { + c.RequestEditors = append(c.RequestEditors, fn) + return nil + } +} + +// The interface specification for the client above. +type ClientInterface interface { + // GetCurrentUser request + GetCurrentUser(ctx context.Context, reqEditors ...RequestEditorFn) (*http.Response, error) +} + +func (c *Client) GetCurrentUser(ctx context.Context, reqEditors ...RequestEditorFn) (*http.Response, error) { + req, err := NewGetCurrentUserRequest(c.Server) + if err != nil { + return nil, err + } + req = req.WithContext(ctx) + if err := c.applyEditors(ctx, req, reqEditors); err != nil { + return nil, err + } + return c.Client.Do(req) +} + +// NewGetCurrentUserRequest generates requests for GetCurrentUser +func NewGetCurrentUserRequest(server string) (*http.Request, error) { + var err error + + serverURL, err := url.Parse(server) + if err != nil { + return nil, err + } + + operationPath := fmt.Sprintf("/users/current") + if operationPath[0] == '/' { + operationPath = "." + operationPath + } + + queryURL, err := serverURL.Parse(operationPath) + if err != nil { + return nil, err + } + + req, err := http.NewRequest("GET", queryURL.String(), nil) + if err != nil { + return nil, err + } + + return req, nil +} + +func (c *Client) applyEditors(ctx context.Context, req *http.Request, additionalEditors []RequestEditorFn) error { + for _, r := range c.RequestEditors { + if err := r(ctx, req); err != nil { + return err + } + } + for _, r := range additionalEditors { + if err := r(ctx, req); err != nil { + return err + } + } + return nil +} + +// ClientWithResponses builds on ClientInterface to offer response payloads +type ClientWithResponses struct { + ClientInterface +} + +// NewClientWithResponses creates a new ClientWithResponses, which wraps +// Client with return type handling +func NewClientWithResponses(server string, opts ...ClientOption) (*ClientWithResponses, error) { + client, err := NewClient(server, opts...) + if err != nil { + return nil, err + } + return &ClientWithResponses{client}, nil +} + +// WithBaseURL overrides the baseURL. +func WithBaseURL(baseURL string) ClientOption { + return func(c *Client) error { + newBaseURL, err := url.Parse(baseURL) + if err != nil { + return err + } + c.Server = newBaseURL.String() + return nil + } +} + +// ClientWithResponsesInterface is the interface specification for the client with responses above. +type ClientWithResponsesInterface interface { + // GetCurrentUser request + GetCurrentUserWithResponse(ctx context.Context, reqEditors ...RequestEditorFn) (*GetCurrentUserResponse, error) +} + +type GetCurrentUserResponse struct { + Body []byte + HTTPResponse *http.Response + JSON200 *User +} + +// Status returns HTTPResponse.Status +func (r GetCurrentUserResponse) Status() string { + if r.HTTPResponse != nil { + return r.HTTPResponse.Status + } + return http.StatusText(0) +} + +// StatusCode returns HTTPResponse.StatusCode +func (r GetCurrentUserResponse) StatusCode() int { + if r.HTTPResponse != nil { + return r.HTTPResponse.StatusCode + } + return 0 +} + +// GetCurrentUserWithResponse request returning *GetCurrentUserResponse +func (c *ClientWithResponses) GetCurrentUserWithResponse(ctx context.Context, reqEditors ...RequestEditorFn) (*GetCurrentUserResponse, error) { + rsp, err := c.GetCurrentUser(ctx, reqEditors...) + if err != nil { + return nil, err + } + return ParseGetCurrentUserResponse(rsp) +} + +// ParseGetCurrentUserResponse parses an HTTP response from a GetCurrentUserWithResponse call +func ParseGetCurrentUserResponse(rsp *http.Response) (*GetCurrentUserResponse, error) { + bodyBytes, err := ioutil.ReadAll(rsp.Body) + defer rsp.Body.Close() + if err != nil { + return nil, err + } + + response := &GetCurrentUserResponse{ + Body: bodyBytes, + HTTPResponse: rsp, + } + + switch { + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 200: + var dest User + if err := json.Unmarshal(bodyBytes, &dest); err != nil { + return nil, err + } + response.JSON200 = &dest + + } + + return response, nil +} diff --git a/wild-workouts/internal/common/client/users/openapi_types.gen.go b/wild-workouts/internal/common/client/users/openapi_types.gen.go new file mode 100644 index 0000000..277c0af --- /dev/null +++ b/wild-workouts/internal/common/client/users/openapi_types.gen.go @@ -0,0 +1,15 @@ +// Package users provides primitives to interact with the openapi HTTP API. +// +// Code generated by github.com/deepmap/oapi-codegen version v1.8.2 DO NOT EDIT. +package users + +const ( + BearerAuthScopes = "bearerAuth.Scopes" +) + +// User defines model for User. +type User struct { + Balance int `json:"balance"` + DisplayName string `json:"displayName"` + Role string `json:"role"` +} diff --git a/wild-workouts/internal/common/decorator/command.go b/wild-workouts/internal/common/decorator/command.go new file mode 100644 index 0000000..28b6d7a --- /dev/null +++ b/wild-workouts/internal/common/decorator/command.go @@ -0,0 +1,27 @@ +package decorator + +import ( + "context" + "fmt" + "strings" + + "github.com/sirupsen/logrus" +) + +func ApplyCommandDecorators[H any](handler CommandHandler[H], logger *logrus.Entry, metricsClient MetricsClient) CommandHandler[H] { + return commandLoggingDecorator[H]{ + base: commandMetricsDecorator[H]{ + base: handler, + client: metricsClient, + }, + logger: logger, + } +} + +type CommandHandler[C any] interface { + Handle(ctx context.Context, cmd C) error +} + +func generateActionName(handler any) string { + return strings.Split(fmt.Sprintf("%T", handler), ".")[1] +} diff --git a/wild-workouts/internal/common/decorator/logging.go b/wild-workouts/internal/common/decorator/logging.go new file mode 100644 index 0000000..3c531fe --- /dev/null +++ b/wild-workouts/internal/common/decorator/logging.go @@ -0,0 +1,56 @@ +package decorator + +import ( + "context" + "fmt" + + "github.com/sirupsen/logrus" +) + +type commandLoggingDecorator[C any] struct { + base CommandHandler[C] + logger *logrus.Entry +} + +func (d commandLoggingDecorator[C]) Handle(ctx context.Context, cmd C) (err error) { + handlerType := generateActionName(cmd) + + logger := d.logger.WithFields(logrus.Fields{ + "command": handlerType, + "command_body": fmt.Sprintf("%#v", cmd), + }) + + logger.Debug("Executing command") + defer func() { + if err == nil { + logger.Info("Command executed successfully") + } else { + logger.WithError(err).Error("Failed to execute command") + } + }() + + return d.base.Handle(ctx, cmd) +} + +type queryLoggingDecorator[C any, R any] struct { + base QueryHandler[C, R] + logger *logrus.Entry +} + +func (d queryLoggingDecorator[C, R]) Handle(ctx context.Context, cmd C) (result R, err error) { + logger := d.logger.WithFields(logrus.Fields{ + "query": generateActionName(cmd), + "query_body": fmt.Sprintf("%#v", cmd), + }) + + logger.Debug("Executing query") + defer func() { + if err == nil { + logger.Info("Query executed successfully") + } else { + logger.WithError(err).Error("Failed to execute query") + } + }() + + return d.base.Handle(ctx, cmd) +} diff --git a/wild-workouts/internal/common/decorator/metrics.go b/wild-workouts/internal/common/decorator/metrics.go new file mode 100644 index 0000000..4200e2d --- /dev/null +++ b/wild-workouts/internal/common/decorator/metrics.go @@ -0,0 +1,62 @@ +package decorator + +import ( + "context" + "fmt" + "strings" + "time" +) + +type MetricsClient interface { + Inc(key string, value int) +} + +type commandMetricsDecorator[C any] struct { + base CommandHandler[C] + client MetricsClient +} + +func (d commandMetricsDecorator[C]) Handle(ctx context.Context, cmd C) (err error) { + start := time.Now() + + actionName := strings.ToLower(generateActionName(cmd)) + + defer func() { + end := time.Since(start) + + d.client.Inc(fmt.Sprintf("commands.%s.duration", actionName), int(end.Seconds())) + + if err == nil { + d.client.Inc(fmt.Sprintf("commands.%s.success", actionName), 1) + } else { + d.client.Inc(fmt.Sprintf("commands.%s.failure", actionName), 1) + } + }() + + return d.base.Handle(ctx, cmd) +} + +type queryMetricsDecorator[C any, R any] struct { + base QueryHandler[C, R] + client MetricsClient +} + +func (d queryMetricsDecorator[C, R]) Handle(ctx context.Context, query C) (result R, err error) { + start := time.Now() + + actionName := strings.ToLower(generateActionName(query)) + + defer func() { + end := time.Since(start) + + d.client.Inc(fmt.Sprintf("querys.%s.duration", actionName), int(end.Seconds())) + + if err == nil { + d.client.Inc(fmt.Sprintf("querys.%s.success", actionName), 1) + } else { + d.client.Inc(fmt.Sprintf("querys.%s.failure", actionName), 1) + } + }() + + return d.base.Handle(ctx, query) +} diff --git a/wild-workouts/internal/common/decorator/query.go b/wild-workouts/internal/common/decorator/query.go new file mode 100644 index 0000000..e184d8d --- /dev/null +++ b/wild-workouts/internal/common/decorator/query.go @@ -0,0 +1,21 @@ +package decorator + +import ( + "context" + + "github.com/sirupsen/logrus" +) + +func ApplyQueryDecorators[H any, R any](handler QueryHandler[H, R], logger *logrus.Entry, metricsClient MetricsClient) QueryHandler[H, R] { + return queryLoggingDecorator[H, R]{ + base: queryMetricsDecorator[H, R]{ + base: handler, + client: metricsClient, + }, + logger: logger, + } +} + +type QueryHandler[Q any, R any] interface { + Handle(ctx context.Context, q Q) (R, error) +} diff --git a/wild-workouts/internal/common/errors/errors.go b/wild-workouts/internal/common/errors/errors.go new file mode 100644 index 0000000..bc317e3 --- /dev/null +++ b/wild-workouts/internal/common/errors/errors.go @@ -0,0 +1,53 @@ +package errors + +type ErrorType struct { + t string +} + +var ( + ErrorTypeUnknown = ErrorType{"unknown"} + ErrorTypeAuthorization = ErrorType{"authorization"} + ErrorTypeIncorrectInput = ErrorType{"incorrect-input"} +) + +type SlugError struct { + error string + slug string + errorType ErrorType +} + +func (s SlugError) Error() string { + return s.error +} + +func (s SlugError) Slug() string { + return s.slug +} + +func (s SlugError) ErrorType() ErrorType { + return s.errorType +} + +func NewSlugError(error string, slug string) SlugError { + return SlugError{ + error: error, + slug: slug, + errorType: ErrorTypeUnknown, + } +} + +func NewAuthorizationError(error string, slug string) SlugError { + return SlugError{ + error: error, + slug: slug, + errorType: ErrorTypeAuthorization, + } +} + +func NewIncorrectInputError(error string, slug string) SlugError { + return SlugError{ + error: error, + slug: slug, + errorType: ErrorTypeIncorrectInput, + } +} diff --git a/wild-workouts/internal/common/genproto/trainer/trainer.pb.go b/wild-workouts/internal/common/genproto/trainer/trainer.pb.go new file mode 100644 index 0000000..c5c6d9d --- /dev/null +++ b/wild-workouts/internal/common/genproto/trainer/trainer.pb.go @@ -0,0 +1,315 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.27.1 +// protoc v3.6.1 +// source: trainer.proto + +package trainer + +import ( + reflect "reflect" + sync "sync" + + empty "github.com/golang/protobuf/ptypes/empty" + timestamp "github.com/golang/protobuf/ptypes/timestamp" + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +type IsHourAvailableRequest struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Time *timestamp.Timestamp `protobuf:"bytes,1,opt,name=time,proto3" json:"time,omitempty"` +} + +func (x *IsHourAvailableRequest) Reset() { + *x = IsHourAvailableRequest{} + if protoimpl.UnsafeEnabled { + mi := &file_trainer_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *IsHourAvailableRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*IsHourAvailableRequest) ProtoMessage() {} + +func (x *IsHourAvailableRequest) ProtoReflect() protoreflect.Message { + mi := &file_trainer_proto_msgTypes[0] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use IsHourAvailableRequest.ProtoReflect.Descriptor instead. +func (*IsHourAvailableRequest) Descriptor() ([]byte, []int) { + return file_trainer_proto_rawDescGZIP(), []int{0} +} + +func (x *IsHourAvailableRequest) GetTime() *timestamp.Timestamp { + if x != nil { + return x.Time + } + return nil +} + +type IsHourAvailableResponse struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + IsAvailable bool `protobuf:"varint,1,opt,name=is_available,json=isAvailable,proto3" json:"is_available,omitempty"` +} + +func (x *IsHourAvailableResponse) Reset() { + *x = IsHourAvailableResponse{} + if protoimpl.UnsafeEnabled { + mi := &file_trainer_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *IsHourAvailableResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*IsHourAvailableResponse) ProtoMessage() {} + +func (x *IsHourAvailableResponse) ProtoReflect() protoreflect.Message { + mi := &file_trainer_proto_msgTypes[1] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use IsHourAvailableResponse.ProtoReflect.Descriptor instead. +func (*IsHourAvailableResponse) Descriptor() ([]byte, []int) { + return file_trainer_proto_rawDescGZIP(), []int{1} +} + +func (x *IsHourAvailableResponse) GetIsAvailable() bool { + if x != nil { + return x.IsAvailable + } + return false +} + +type UpdateHourRequest struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Time *timestamp.Timestamp `protobuf:"bytes,1,opt,name=time,proto3" json:"time,omitempty"` +} + +func (x *UpdateHourRequest) Reset() { + *x = UpdateHourRequest{} + if protoimpl.UnsafeEnabled { + mi := &file_trainer_proto_msgTypes[2] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *UpdateHourRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*UpdateHourRequest) ProtoMessage() {} + +func (x *UpdateHourRequest) ProtoReflect() protoreflect.Message { + mi := &file_trainer_proto_msgTypes[2] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use UpdateHourRequest.ProtoReflect.Descriptor instead. +func (*UpdateHourRequest) Descriptor() ([]byte, []int) { + return file_trainer_proto_rawDescGZIP(), []int{2} +} + +func (x *UpdateHourRequest) GetTime() *timestamp.Timestamp { + if x != nil { + return x.Time + } + return nil +} + +var File_trainer_proto protoreflect.FileDescriptor + +var file_trainer_proto_rawDesc = []byte{ + 0x0a, 0x0d, 0x74, 0x72, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, + 0x07, 0x74, 0x72, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x1a, 0x1b, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, + 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2f, 0x65, 0x6d, 0x70, 0x74, 0x79, 0x2e, + 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x1a, 0x1f, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2f, 0x70, 0x72, + 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2f, 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, + 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0x48, 0x0a, 0x16, 0x49, 0x73, 0x48, 0x6f, 0x75, 0x72, + 0x41, 0x76, 0x61, 0x69, 0x6c, 0x61, 0x62, 0x6c, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, + 0x12, 0x2e, 0x0a, 0x04, 0x74, 0x69, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, + 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, + 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x04, 0x74, 0x69, 0x6d, 0x65, + 0x22, 0x3c, 0x0a, 0x17, 0x49, 0x73, 0x48, 0x6f, 0x75, 0x72, 0x41, 0x76, 0x61, 0x69, 0x6c, 0x61, + 0x62, 0x6c, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x21, 0x0a, 0x0c, 0x69, + 0x73, 0x5f, 0x61, 0x76, 0x61, 0x69, 0x6c, 0x61, 0x62, 0x6c, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, + 0x08, 0x52, 0x0b, 0x69, 0x73, 0x41, 0x76, 0x61, 0x69, 0x6c, 0x61, 0x62, 0x6c, 0x65, 0x22, 0x43, + 0x0a, 0x11, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x48, 0x6f, 0x75, 0x72, 0x52, 0x65, 0x71, 0x75, + 0x65, 0x73, 0x74, 0x12, 0x2e, 0x0a, 0x04, 0x74, 0x69, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, + 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, + 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x04, 0x74, + 0x69, 0x6d, 0x65, 0x32, 0xc5, 0x02, 0x0a, 0x0e, 0x54, 0x72, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x53, + 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x12, 0x56, 0x0a, 0x0f, 0x49, 0x73, 0x48, 0x6f, 0x75, 0x72, + 0x41, 0x76, 0x61, 0x69, 0x6c, 0x61, 0x62, 0x6c, 0x65, 0x12, 0x1f, 0x2e, 0x74, 0x72, 0x61, 0x69, + 0x6e, 0x65, 0x72, 0x2e, 0x49, 0x73, 0x48, 0x6f, 0x75, 0x72, 0x41, 0x76, 0x61, 0x69, 0x6c, 0x61, + 0x62, 0x6c, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x20, 0x2e, 0x74, 0x72, 0x61, + 0x69, 0x6e, 0x65, 0x72, 0x2e, 0x49, 0x73, 0x48, 0x6f, 0x75, 0x72, 0x41, 0x76, 0x61, 0x69, 0x6c, + 0x61, 0x62, 0x6c, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x48, + 0x0a, 0x10, 0x53, 0x63, 0x68, 0x65, 0x64, 0x75, 0x6c, 0x65, 0x54, 0x72, 0x61, 0x69, 0x6e, 0x69, + 0x6e, 0x67, 0x12, 0x1a, 0x2e, 0x74, 0x72, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x2e, 0x55, 0x70, 0x64, + 0x61, 0x74, 0x65, 0x48, 0x6f, 0x75, 0x72, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x16, + 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, + 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x22, 0x00, 0x12, 0x46, 0x0a, 0x0e, 0x43, 0x61, 0x6e, 0x63, + 0x65, 0x6c, 0x54, 0x72, 0x61, 0x69, 0x6e, 0x69, 0x6e, 0x67, 0x12, 0x1a, 0x2e, 0x74, 0x72, 0x61, + 0x69, 0x6e, 0x65, 0x72, 0x2e, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x48, 0x6f, 0x75, 0x72, 0x52, + 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, + 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x22, 0x00, + 0x12, 0x49, 0x0a, 0x11, 0x4d, 0x61, 0x6b, 0x65, 0x48, 0x6f, 0x75, 0x72, 0x41, 0x76, 0x61, 0x69, + 0x6c, 0x61, 0x62, 0x6c, 0x65, 0x12, 0x1a, 0x2e, 0x74, 0x72, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x2e, + 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x48, 0x6f, 0x75, 0x72, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, + 0x74, 0x1a, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, + 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x22, 0x00, 0x42, 0x48, 0x5a, 0x46, 0x67, + 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x54, 0x68, 0x72, 0x65, 0x65, 0x44, + 0x6f, 0x74, 0x73, 0x4c, 0x61, 0x62, 0x73, 0x2f, 0x77, 0x69, 0x6c, 0x64, 0x2d, 0x77, 0x6f, 0x72, + 0x6b, 0x6f, 0x75, 0x74, 0x73, 0x2d, 0x67, 0x6f, 0x2d, 0x64, 0x64, 0x64, 0x2d, 0x65, 0x78, 0x61, + 0x6d, 0x70, 0x6c, 0x65, 0x2f, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2f, 0x74, 0x72, + 0x61, 0x69, 0x6e, 0x65, 0x72, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, +} + +var ( + file_trainer_proto_rawDescOnce sync.Once + file_trainer_proto_rawDescData = file_trainer_proto_rawDesc +) + +func file_trainer_proto_rawDescGZIP() []byte { + file_trainer_proto_rawDescOnce.Do(func() { + file_trainer_proto_rawDescData = protoimpl.X.CompressGZIP(file_trainer_proto_rawDescData) + }) + return file_trainer_proto_rawDescData +} + +var file_trainer_proto_msgTypes = make([]protoimpl.MessageInfo, 3) +var file_trainer_proto_goTypes = []interface{}{ + (*IsHourAvailableRequest)(nil), // 0: trainer.IsHourAvailableRequest + (*IsHourAvailableResponse)(nil), // 1: trainer.IsHourAvailableResponse + (*UpdateHourRequest)(nil), // 2: trainer.UpdateHourRequest + (*timestamp.Timestamp)(nil), // 3: google.protobuf.Timestamp + (*empty.Empty)(nil), // 4: google.protobuf.Empty +} +var file_trainer_proto_depIdxs = []int32{ + 3, // 0: trainer.IsHourAvailableRequest.time:type_name -> google.protobuf.Timestamp + 3, // 1: trainer.UpdateHourRequest.time:type_name -> google.protobuf.Timestamp + 0, // 2: trainer.TrainerService.IsHourAvailable:input_type -> trainer.IsHourAvailableRequest + 2, // 3: trainer.TrainerService.ScheduleTraining:input_type -> trainer.UpdateHourRequest + 2, // 4: trainer.TrainerService.CancelTraining:input_type -> trainer.UpdateHourRequest + 2, // 5: trainer.TrainerService.MakeHourAvailable:input_type -> trainer.UpdateHourRequest + 1, // 6: trainer.TrainerService.IsHourAvailable:output_type -> trainer.IsHourAvailableResponse + 4, // 7: trainer.TrainerService.ScheduleTraining:output_type -> google.protobuf.Empty + 4, // 8: trainer.TrainerService.CancelTraining:output_type -> google.protobuf.Empty + 4, // 9: trainer.TrainerService.MakeHourAvailable:output_type -> google.protobuf.Empty + 6, // [6:10] is the sub-list for method output_type + 2, // [2:6] is the sub-list for method input_type + 2, // [2:2] is the sub-list for extension type_name + 2, // [2:2] is the sub-list for extension extendee + 0, // [0:2] is the sub-list for field type_name +} + +func init() { file_trainer_proto_init() } +func file_trainer_proto_init() { + if File_trainer_proto != nil { + return + } + if !protoimpl.UnsafeEnabled { + file_trainer_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*IsHourAvailableRequest); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_trainer_proto_msgTypes[1].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*IsHourAvailableResponse); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_trainer_proto_msgTypes[2].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*UpdateHourRequest); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + } + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: file_trainer_proto_rawDesc, + NumEnums: 0, + NumMessages: 3, + NumExtensions: 0, + NumServices: 1, + }, + GoTypes: file_trainer_proto_goTypes, + DependencyIndexes: file_trainer_proto_depIdxs, + MessageInfos: file_trainer_proto_msgTypes, + }.Build() + File_trainer_proto = out.File + file_trainer_proto_rawDesc = nil + file_trainer_proto_goTypes = nil + file_trainer_proto_depIdxs = nil +} diff --git a/wild-workouts/internal/common/genproto/trainer/trainer_grpc.pb.go b/wild-workouts/internal/common/genproto/trainer/trainer_grpc.pb.go new file mode 100644 index 0000000..26fde01 --- /dev/null +++ b/wild-workouts/internal/common/genproto/trainer/trainer_grpc.pb.go @@ -0,0 +1,209 @@ +// Code generated by protoc-gen-go-grpc. DO NOT EDIT. + +package trainer + +import ( + context "context" + + empty "github.com/golang/protobuf/ptypes/empty" + grpc "google.golang.org/grpc" + codes "google.golang.org/grpc/codes" + status "google.golang.org/grpc/status" +) + +// This is a compile-time assertion to ensure that this generated file +// is compatible with the grpc package it is being compiled against. +// Requires gRPC-Go v1.32.0 or later. +const _ = grpc.SupportPackageIsVersion7 + +// TrainerServiceClient is the client API for TrainerService service. +// +// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream. +type TrainerServiceClient interface { + IsHourAvailable(ctx context.Context, in *IsHourAvailableRequest, opts ...grpc.CallOption) (*IsHourAvailableResponse, error) + ScheduleTraining(ctx context.Context, in *UpdateHourRequest, opts ...grpc.CallOption) (*empty.Empty, error) + CancelTraining(ctx context.Context, in *UpdateHourRequest, opts ...grpc.CallOption) (*empty.Empty, error) + MakeHourAvailable(ctx context.Context, in *UpdateHourRequest, opts ...grpc.CallOption) (*empty.Empty, error) +} + +type trainerServiceClient struct { + cc grpc.ClientConnInterface +} + +func NewTrainerServiceClient(cc grpc.ClientConnInterface) TrainerServiceClient { + return &trainerServiceClient{cc} +} + +func (c *trainerServiceClient) IsHourAvailable(ctx context.Context, in *IsHourAvailableRequest, opts ...grpc.CallOption) (*IsHourAvailableResponse, error) { + out := new(IsHourAvailableResponse) + err := c.cc.Invoke(ctx, "/trainer.TrainerService/IsHourAvailable", in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *trainerServiceClient) ScheduleTraining(ctx context.Context, in *UpdateHourRequest, opts ...grpc.CallOption) (*empty.Empty, error) { + out := new(empty.Empty) + err := c.cc.Invoke(ctx, "/trainer.TrainerService/ScheduleTraining", in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *trainerServiceClient) CancelTraining(ctx context.Context, in *UpdateHourRequest, opts ...grpc.CallOption) (*empty.Empty, error) { + out := new(empty.Empty) + err := c.cc.Invoke(ctx, "/trainer.TrainerService/CancelTraining", in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *trainerServiceClient) MakeHourAvailable(ctx context.Context, in *UpdateHourRequest, opts ...grpc.CallOption) (*empty.Empty, error) { + out := new(empty.Empty) + err := c.cc.Invoke(ctx, "/trainer.TrainerService/MakeHourAvailable", in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + +// TrainerServiceServer is the server API for TrainerService service. +// All implementations should embed UnimplementedTrainerServiceServer +// for forward compatibility +type TrainerServiceServer interface { + IsHourAvailable(context.Context, *IsHourAvailableRequest) (*IsHourAvailableResponse, error) + ScheduleTraining(context.Context, *UpdateHourRequest) (*empty.Empty, error) + CancelTraining(context.Context, *UpdateHourRequest) (*empty.Empty, error) + MakeHourAvailable(context.Context, *UpdateHourRequest) (*empty.Empty, error) +} + +// UnimplementedTrainerServiceServer should be embedded to have forward compatible implementations. +type UnimplementedTrainerServiceServer struct { +} + +func (UnimplementedTrainerServiceServer) IsHourAvailable(context.Context, *IsHourAvailableRequest) (*IsHourAvailableResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method IsHourAvailable not implemented") +} +func (UnimplementedTrainerServiceServer) ScheduleTraining(context.Context, *UpdateHourRequest) (*empty.Empty, error) { + return nil, status.Errorf(codes.Unimplemented, "method ScheduleTraining not implemented") +} +func (UnimplementedTrainerServiceServer) CancelTraining(context.Context, *UpdateHourRequest) (*empty.Empty, error) { + return nil, status.Errorf(codes.Unimplemented, "method CancelTraining not implemented") +} +func (UnimplementedTrainerServiceServer) MakeHourAvailable(context.Context, *UpdateHourRequest) (*empty.Empty, error) { + return nil, status.Errorf(codes.Unimplemented, "method MakeHourAvailable not implemented") +} + +// UnsafeTrainerServiceServer may be embedded to opt out of forward compatibility for this service. +// Use of this interface is not recommended, as added methods to TrainerServiceServer will +// result in compilation errors. +type UnsafeTrainerServiceServer interface { + mustEmbedUnimplementedTrainerServiceServer() +} + +func RegisterTrainerServiceServer(s grpc.ServiceRegistrar, srv TrainerServiceServer) { + s.RegisterService(&TrainerService_ServiceDesc, srv) +} + +func _TrainerService_IsHourAvailable_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(IsHourAvailableRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(TrainerServiceServer).IsHourAvailable(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: "/trainer.TrainerService/IsHourAvailable", + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(TrainerServiceServer).IsHourAvailable(ctx, req.(*IsHourAvailableRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _TrainerService_ScheduleTraining_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(UpdateHourRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(TrainerServiceServer).ScheduleTraining(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: "/trainer.TrainerService/ScheduleTraining", + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(TrainerServiceServer).ScheduleTraining(ctx, req.(*UpdateHourRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _TrainerService_CancelTraining_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(UpdateHourRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(TrainerServiceServer).CancelTraining(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: "/trainer.TrainerService/CancelTraining", + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(TrainerServiceServer).CancelTraining(ctx, req.(*UpdateHourRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _TrainerService_MakeHourAvailable_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(UpdateHourRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(TrainerServiceServer).MakeHourAvailable(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: "/trainer.TrainerService/MakeHourAvailable", + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(TrainerServiceServer).MakeHourAvailable(ctx, req.(*UpdateHourRequest)) + } + return interceptor(ctx, in, info, handler) +} + +// TrainerService_ServiceDesc is the grpc.ServiceDesc for TrainerService service. +// It's only intended for direct use with grpc.RegisterService, +// and not to be introspected or modified (even as a copy) +var TrainerService_ServiceDesc = grpc.ServiceDesc{ + ServiceName: "trainer.TrainerService", + HandlerType: (*TrainerServiceServer)(nil), + Methods: []grpc.MethodDesc{ + { + MethodName: "IsHourAvailable", + Handler: _TrainerService_IsHourAvailable_Handler, + }, + { + MethodName: "ScheduleTraining", + Handler: _TrainerService_ScheduleTraining_Handler, + }, + { + MethodName: "CancelTraining", + Handler: _TrainerService_CancelTraining_Handler, + }, + { + MethodName: "MakeHourAvailable", + Handler: _TrainerService_MakeHourAvailable_Handler, + }, + }, + Streams: []grpc.StreamDesc{}, + Metadata: "trainer.proto", +} diff --git a/wild-workouts/internal/common/genproto/users/users.pb.go b/wild-workouts/internal/common/genproto/users/users.pb.go new file mode 100644 index 0000000..102d1da --- /dev/null +++ b/wild-workouts/internal/common/genproto/users/users.pb.go @@ -0,0 +1,305 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.27.1 +// protoc v3.6.1 +// source: users.proto + +package users + +import ( + reflect "reflect" + sync "sync" + + empty "github.com/golang/protobuf/ptypes/empty" + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +type GetTrainingBalanceRequest struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + UserId string `protobuf:"bytes,1,opt,name=user_id,json=userId,proto3" json:"user_id,omitempty"` +} + +func (x *GetTrainingBalanceRequest) Reset() { + *x = GetTrainingBalanceRequest{} + if protoimpl.UnsafeEnabled { + mi := &file_users_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *GetTrainingBalanceRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GetTrainingBalanceRequest) ProtoMessage() {} + +func (x *GetTrainingBalanceRequest) ProtoReflect() protoreflect.Message { + mi := &file_users_proto_msgTypes[0] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use GetTrainingBalanceRequest.ProtoReflect.Descriptor instead. +func (*GetTrainingBalanceRequest) Descriptor() ([]byte, []int) { + return file_users_proto_rawDescGZIP(), []int{0} +} + +func (x *GetTrainingBalanceRequest) GetUserId() string { + if x != nil { + return x.UserId + } + return "" +} + +type GetTrainingBalanceResponse struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Amount int64 `protobuf:"varint,1,opt,name=amount,proto3" json:"amount,omitempty"` +} + +func (x *GetTrainingBalanceResponse) Reset() { + *x = GetTrainingBalanceResponse{} + if protoimpl.UnsafeEnabled { + mi := &file_users_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *GetTrainingBalanceResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GetTrainingBalanceResponse) ProtoMessage() {} + +func (x *GetTrainingBalanceResponse) ProtoReflect() protoreflect.Message { + mi := &file_users_proto_msgTypes[1] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use GetTrainingBalanceResponse.ProtoReflect.Descriptor instead. +func (*GetTrainingBalanceResponse) Descriptor() ([]byte, []int) { + return file_users_proto_rawDescGZIP(), []int{1} +} + +func (x *GetTrainingBalanceResponse) GetAmount() int64 { + if x != nil { + return x.Amount + } + return 0 +} + +type UpdateTrainingBalanceRequest struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + UserId string `protobuf:"bytes,1,opt,name=user_id,json=userId,proto3" json:"user_id,omitempty"` + AmountChange int64 `protobuf:"varint,2,opt,name=amount_change,json=amountChange,proto3" json:"amount_change,omitempty"` +} + +func (x *UpdateTrainingBalanceRequest) Reset() { + *x = UpdateTrainingBalanceRequest{} + if protoimpl.UnsafeEnabled { + mi := &file_users_proto_msgTypes[2] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *UpdateTrainingBalanceRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*UpdateTrainingBalanceRequest) ProtoMessage() {} + +func (x *UpdateTrainingBalanceRequest) ProtoReflect() protoreflect.Message { + mi := &file_users_proto_msgTypes[2] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use UpdateTrainingBalanceRequest.ProtoReflect.Descriptor instead. +func (*UpdateTrainingBalanceRequest) Descriptor() ([]byte, []int) { + return file_users_proto_rawDescGZIP(), []int{2} +} + +func (x *UpdateTrainingBalanceRequest) GetUserId() string { + if x != nil { + return x.UserId + } + return "" +} + +func (x *UpdateTrainingBalanceRequest) GetAmountChange() int64 { + if x != nil { + return x.AmountChange + } + return 0 +} + +var File_users_proto protoreflect.FileDescriptor + +var file_users_proto_rawDesc = []byte{ + 0x0a, 0x0b, 0x75, 0x73, 0x65, 0x72, 0x73, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x05, 0x75, + 0x73, 0x65, 0x72, 0x73, 0x1a, 0x1b, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2f, 0x70, 0x72, 0x6f, + 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2f, 0x65, 0x6d, 0x70, 0x74, 0x79, 0x2e, 0x70, 0x72, 0x6f, 0x74, + 0x6f, 0x22, 0x34, 0x0a, 0x19, 0x47, 0x65, 0x74, 0x54, 0x72, 0x61, 0x69, 0x6e, 0x69, 0x6e, 0x67, + 0x42, 0x61, 0x6c, 0x61, 0x6e, 0x63, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x17, + 0x0a, 0x07, 0x75, 0x73, 0x65, 0x72, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, + 0x06, 0x75, 0x73, 0x65, 0x72, 0x49, 0x64, 0x22, 0x34, 0x0a, 0x1a, 0x47, 0x65, 0x74, 0x54, 0x72, + 0x61, 0x69, 0x6e, 0x69, 0x6e, 0x67, 0x42, 0x61, 0x6c, 0x61, 0x6e, 0x63, 0x65, 0x52, 0x65, 0x73, + 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x16, 0x0a, 0x06, 0x61, 0x6d, 0x6f, 0x75, 0x6e, 0x74, 0x18, + 0x01, 0x20, 0x01, 0x28, 0x03, 0x52, 0x06, 0x61, 0x6d, 0x6f, 0x75, 0x6e, 0x74, 0x22, 0x5c, 0x0a, + 0x1c, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x54, 0x72, 0x61, 0x69, 0x6e, 0x69, 0x6e, 0x67, 0x42, + 0x61, 0x6c, 0x61, 0x6e, 0x63, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x17, 0x0a, + 0x07, 0x75, 0x73, 0x65, 0x72, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, + 0x75, 0x73, 0x65, 0x72, 0x49, 0x64, 0x12, 0x23, 0x0a, 0x0d, 0x61, 0x6d, 0x6f, 0x75, 0x6e, 0x74, + 0x5f, 0x63, 0x68, 0x61, 0x6e, 0x67, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0c, 0x61, + 0x6d, 0x6f, 0x75, 0x6e, 0x74, 0x43, 0x68, 0x61, 0x6e, 0x67, 0x65, 0x32, 0xc3, 0x01, 0x0a, 0x0c, + 0x55, 0x73, 0x65, 0x72, 0x73, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x12, 0x5b, 0x0a, 0x12, + 0x47, 0x65, 0x74, 0x54, 0x72, 0x61, 0x69, 0x6e, 0x69, 0x6e, 0x67, 0x42, 0x61, 0x6c, 0x61, 0x6e, + 0x63, 0x65, 0x12, 0x20, 0x2e, 0x75, 0x73, 0x65, 0x72, 0x73, 0x2e, 0x47, 0x65, 0x74, 0x54, 0x72, + 0x61, 0x69, 0x6e, 0x69, 0x6e, 0x67, 0x42, 0x61, 0x6c, 0x61, 0x6e, 0x63, 0x65, 0x52, 0x65, 0x71, + 0x75, 0x65, 0x73, 0x74, 0x1a, 0x21, 0x2e, 0x75, 0x73, 0x65, 0x72, 0x73, 0x2e, 0x47, 0x65, 0x74, + 0x54, 0x72, 0x61, 0x69, 0x6e, 0x69, 0x6e, 0x67, 0x42, 0x61, 0x6c, 0x61, 0x6e, 0x63, 0x65, 0x52, + 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x56, 0x0a, 0x15, 0x55, 0x70, 0x64, + 0x61, 0x74, 0x65, 0x54, 0x72, 0x61, 0x69, 0x6e, 0x69, 0x6e, 0x67, 0x42, 0x61, 0x6c, 0x61, 0x6e, + 0x63, 0x65, 0x12, 0x23, 0x2e, 0x75, 0x73, 0x65, 0x72, 0x73, 0x2e, 0x55, 0x70, 0x64, 0x61, 0x74, + 0x65, 0x54, 0x72, 0x61, 0x69, 0x6e, 0x69, 0x6e, 0x67, 0x42, 0x61, 0x6c, 0x61, 0x6e, 0x63, 0x65, + 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, + 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x22, + 0x00, 0x42, 0x46, 0x5a, 0x44, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, + 0x54, 0x68, 0x72, 0x65, 0x65, 0x44, 0x6f, 0x74, 0x73, 0x4c, 0x61, 0x62, 0x73, 0x2f, 0x77, 0x69, + 0x6c, 0x64, 0x2d, 0x77, 0x6f, 0x72, 0x6b, 0x6f, 0x75, 0x74, 0x73, 0x2d, 0x67, 0x6f, 0x2d, 0x64, + 0x64, 0x64, 0x2d, 0x65, 0x78, 0x61, 0x6d, 0x70, 0x6c, 0x65, 0x2f, 0x69, 0x6e, 0x74, 0x65, 0x72, + 0x6e, 0x61, 0x6c, 0x2f, 0x75, 0x73, 0x65, 0x72, 0x73, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, + 0x33, +} + +var ( + file_users_proto_rawDescOnce sync.Once + file_users_proto_rawDescData = file_users_proto_rawDesc +) + +func file_users_proto_rawDescGZIP() []byte { + file_users_proto_rawDescOnce.Do(func() { + file_users_proto_rawDescData = protoimpl.X.CompressGZIP(file_users_proto_rawDescData) + }) + return file_users_proto_rawDescData +} + +var file_users_proto_msgTypes = make([]protoimpl.MessageInfo, 3) +var file_users_proto_goTypes = []interface{}{ + (*GetTrainingBalanceRequest)(nil), // 0: users.GetTrainingBalanceRequest + (*GetTrainingBalanceResponse)(nil), // 1: users.GetTrainingBalanceResponse + (*UpdateTrainingBalanceRequest)(nil), // 2: users.UpdateTrainingBalanceRequest + (*empty.Empty)(nil), // 3: google.protobuf.Empty +} +var file_users_proto_depIdxs = []int32{ + 0, // 0: users.UsersService.GetTrainingBalance:input_type -> users.GetTrainingBalanceRequest + 2, // 1: users.UsersService.UpdateTrainingBalance:input_type -> users.UpdateTrainingBalanceRequest + 1, // 2: users.UsersService.GetTrainingBalance:output_type -> users.GetTrainingBalanceResponse + 3, // 3: users.UsersService.UpdateTrainingBalance:output_type -> google.protobuf.Empty + 2, // [2:4] is the sub-list for method output_type + 0, // [0:2] is the sub-list for method input_type + 0, // [0:0] is the sub-list for extension type_name + 0, // [0:0] is the sub-list for extension extendee + 0, // [0:0] is the sub-list for field type_name +} + +func init() { file_users_proto_init() } +func file_users_proto_init() { + if File_users_proto != nil { + return + } + if !protoimpl.UnsafeEnabled { + file_users_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*GetTrainingBalanceRequest); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_users_proto_msgTypes[1].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*GetTrainingBalanceResponse); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_users_proto_msgTypes[2].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*UpdateTrainingBalanceRequest); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + } + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: file_users_proto_rawDesc, + NumEnums: 0, + NumMessages: 3, + NumExtensions: 0, + NumServices: 1, + }, + GoTypes: file_users_proto_goTypes, + DependencyIndexes: file_users_proto_depIdxs, + MessageInfos: file_users_proto_msgTypes, + }.Build() + File_users_proto = out.File + file_users_proto_rawDesc = nil + file_users_proto_goTypes = nil + file_users_proto_depIdxs = nil +} diff --git a/wild-workouts/internal/common/genproto/users/users_grpc.pb.go b/wild-workouts/internal/common/genproto/users/users_grpc.pb.go new file mode 100644 index 0000000..f1ea68e --- /dev/null +++ b/wild-workouts/internal/common/genproto/users/users_grpc.pb.go @@ -0,0 +1,137 @@ +// Code generated by protoc-gen-go-grpc. DO NOT EDIT. + +package users + +import ( + context "context" + + empty "github.com/golang/protobuf/ptypes/empty" + grpc "google.golang.org/grpc" + codes "google.golang.org/grpc/codes" + status "google.golang.org/grpc/status" +) + +// This is a compile-time assertion to ensure that this generated file +// is compatible with the grpc package it is being compiled against. +// Requires gRPC-Go v1.32.0 or later. +const _ = grpc.SupportPackageIsVersion7 + +// UsersServiceClient is the client API for UsersService service. +// +// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream. +type UsersServiceClient interface { + GetTrainingBalance(ctx context.Context, in *GetTrainingBalanceRequest, opts ...grpc.CallOption) (*GetTrainingBalanceResponse, error) + UpdateTrainingBalance(ctx context.Context, in *UpdateTrainingBalanceRequest, opts ...grpc.CallOption) (*empty.Empty, error) +} + +type usersServiceClient struct { + cc grpc.ClientConnInterface +} + +func NewUsersServiceClient(cc grpc.ClientConnInterface) UsersServiceClient { + return &usersServiceClient{cc} +} + +func (c *usersServiceClient) GetTrainingBalance(ctx context.Context, in *GetTrainingBalanceRequest, opts ...grpc.CallOption) (*GetTrainingBalanceResponse, error) { + out := new(GetTrainingBalanceResponse) + err := c.cc.Invoke(ctx, "/users.UsersService/GetTrainingBalance", in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *usersServiceClient) UpdateTrainingBalance(ctx context.Context, in *UpdateTrainingBalanceRequest, opts ...grpc.CallOption) (*empty.Empty, error) { + out := new(empty.Empty) + err := c.cc.Invoke(ctx, "/users.UsersService/UpdateTrainingBalance", in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + +// UsersServiceServer is the server API for UsersService service. +// All implementations should embed UnimplementedUsersServiceServer +// for forward compatibility +type UsersServiceServer interface { + GetTrainingBalance(context.Context, *GetTrainingBalanceRequest) (*GetTrainingBalanceResponse, error) + UpdateTrainingBalance(context.Context, *UpdateTrainingBalanceRequest) (*empty.Empty, error) +} + +// UnimplementedUsersServiceServer should be embedded to have forward compatible implementations. +type UnimplementedUsersServiceServer struct { +} + +func (UnimplementedUsersServiceServer) GetTrainingBalance(context.Context, *GetTrainingBalanceRequest) (*GetTrainingBalanceResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method GetTrainingBalance not implemented") +} +func (UnimplementedUsersServiceServer) UpdateTrainingBalance(context.Context, *UpdateTrainingBalanceRequest) (*empty.Empty, error) { + return nil, status.Errorf(codes.Unimplemented, "method UpdateTrainingBalance not implemented") +} + +// UnsafeUsersServiceServer may be embedded to opt out of forward compatibility for this service. +// Use of this interface is not recommended, as added methods to UsersServiceServer will +// result in compilation errors. +type UnsafeUsersServiceServer interface { + mustEmbedUnimplementedUsersServiceServer() +} + +func RegisterUsersServiceServer(s grpc.ServiceRegistrar, srv UsersServiceServer) { + s.RegisterService(&UsersService_ServiceDesc, srv) +} + +func _UsersService_GetTrainingBalance_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(GetTrainingBalanceRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(UsersServiceServer).GetTrainingBalance(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: "/users.UsersService/GetTrainingBalance", + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(UsersServiceServer).GetTrainingBalance(ctx, req.(*GetTrainingBalanceRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _UsersService_UpdateTrainingBalance_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(UpdateTrainingBalanceRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(UsersServiceServer).UpdateTrainingBalance(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: "/users.UsersService/UpdateTrainingBalance", + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(UsersServiceServer).UpdateTrainingBalance(ctx, req.(*UpdateTrainingBalanceRequest)) + } + return interceptor(ctx, in, info, handler) +} + +// UsersService_ServiceDesc is the grpc.ServiceDesc for UsersService service. +// It's only intended for direct use with grpc.RegisterService, +// and not to be introspected or modified (even as a copy) +var UsersService_ServiceDesc = grpc.ServiceDesc{ + ServiceName: "users.UsersService", + HandlerType: (*UsersServiceServer)(nil), + Methods: []grpc.MethodDesc{ + { + MethodName: "GetTrainingBalance", + Handler: _UsersService_GetTrainingBalance_Handler, + }, + { + MethodName: "UpdateTrainingBalance", + Handler: _UsersService_UpdateTrainingBalance_Handler, + }, + }, + Streams: []grpc.StreamDesc{}, + Metadata: "users.proto", +} diff --git a/wild-workouts/internal/common/go.mod b/wild-workouts/internal/common/go.mod new file mode 100644 index 0000000..c0553af --- /dev/null +++ b/wild-workouts/internal/common/go.mod @@ -0,0 +1,54 @@ +module github.com/ThreeDotsLabs/wild-workouts-go-ddd-example/internal/common + +go 1.18 + +require ( + cloud.google.com/go v0.75.0 + firebase.google.com/go/v4 v4.7.1 + github.com/deepmap/oapi-codegen v1.9.0 + github.com/dgrijalva/jwt-go v3.2.0+incompatible + github.com/go-chi/chi/v5 v5.0.5 + github.com/go-chi/cors v1.0.1 + github.com/go-chi/render v1.0.1 + github.com/golang/protobuf v1.5.2 + github.com/google/uuid v1.1.2 + github.com/grpc-ecosystem/go-grpc-middleware v1.2.0 + github.com/mattn/go-colorable v0.1.8 // indirect + github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b // indirect + github.com/onsi/ginkgo v1.12.0 // indirect + github.com/onsi/gomega v1.9.0 // indirect + github.com/pkg/errors v0.9.1 + github.com/sirupsen/logrus v1.5.0 + github.com/stretchr/testify v1.7.0 + github.com/x-cray/logrus-prefixed-formatter v0.5.2 + go.opencensus.io v0.22.5 // indirect + google.golang.org/api v0.40.0 + google.golang.org/grpc v1.40.0 + google.golang.org/protobuf v1.27.1 +) + +require ( + cloud.google.com/go/firestore v1.5.0 // indirect + cloud.google.com/go/storage v1.10.0 // indirect + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e // indirect + github.com/google/go-cmp v0.5.5 // indirect + github.com/googleapis/gax-go/v2 v2.0.5 // indirect + github.com/jstemmer/go-junit-report v0.9.1 // indirect + github.com/konsorten/go-windows-terminal-sequences v1.0.1 // indirect + github.com/mattn/go-isatty v0.0.14 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect + golang.org/x/crypto v0.0.0-20210921155107-089bfa567519 // indirect + golang.org/x/lint v0.0.0-20201208152925-83fdc39ff7b5 // indirect + golang.org/x/mod v0.4.1 // indirect + golang.org/x/net v0.0.0-20210226172049-e18ecbb05110 // indirect + golang.org/x/oauth2 v0.0.0-20210218202405-ba52d332ba99 // indirect + golang.org/x/sys v0.0.0-20211031064116-611d5d643895 // indirect + golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1 // indirect + golang.org/x/text v0.3.7 // indirect + golang.org/x/tools v0.1.0 // indirect + golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 // indirect + google.golang.org/appengine v1.6.7 // indirect + google.golang.org/genproto v0.0.0-20210222152913-aa3ee6e6a81c // indirect + gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b // indirect +) diff --git a/wild-workouts/internal/common/go.sum b/wild-workouts/internal/common/go.sum new file mode 100644 index 0000000..3218797 --- /dev/null +++ b/wild-workouts/internal/common/go.sum @@ -0,0 +1,627 @@ +cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU= +cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU= +cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY= +cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc= +cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0= +cloud.google.com/go v0.50.0/go.mod h1:r9sluTvynVuxRIOHXQEHMFffphuXHOMZMycpNR5e6To= +cloud.google.com/go v0.52.0/go.mod h1:pXajvRH/6o3+F9jDHZWQ5PbGhn+o8w9qiu/CffaVdO4= +cloud.google.com/go v0.53.0/go.mod h1:fp/UouUEsRkN6ryDKNW/Upv/JBKnv6WDthjR6+vze6M= +cloud.google.com/go v0.54.0/go.mod h1:1rq2OEkV3YMf6n/9ZvGWI3GWw0VoqH/1x2nd8Is/bPc= +cloud.google.com/go v0.56.0/go.mod h1:jr7tqZxxKOVYizybht9+26Z/gUq7tiRzu+ACVAMbKVk= +cloud.google.com/go v0.57.0/go.mod h1:oXiQ6Rzq3RAkkY7N6t3TcE6jE+CIBBbA36lwQ1JyzZs= +cloud.google.com/go v0.62.0/go.mod h1:jmCYTdRCQuc1PHIIJ/maLInMho30T/Y0M4hTdTShOYc= +cloud.google.com/go v0.65.0/go.mod h1:O5N8zS7uWy9vkA9vayVHs65eM1ubvY4h553ofrNHObY= +cloud.google.com/go v0.72.0/go.mod h1:M+5Vjvlc2wnp6tjzE102Dw08nGShTscUx2nZMufOKPI= +cloud.google.com/go v0.74.0/go.mod h1:VV1xSbzvo+9QJOxLDaJfTjx5e+MePCpCWwvftOeQmWk= +cloud.google.com/go v0.75.0 h1:XgtDnVJRCPEUG21gjFiRPz4zI1Mjg16R+NYQjfmU4XY= +cloud.google.com/go v0.75.0/go.mod h1:VGuuCn7PG0dwsd5XPVm2Mm3wlh3EL55/79EKB6hlPTY= +cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o= +cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE= +cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc= +cloud.google.com/go/bigquery v1.5.0/go.mod h1:snEHRnqQbz117VIFhE8bmtwIDY80NLUZUMb4Nv6dBIg= +cloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4gLoIoXIAPc= +cloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ= +cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE= +cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk= +cloud.google.com/go/firestore v1.5.0 h1:4qNItsmc4GP6UOZPGemmHY4ZfPofVhcaKXsYw9wm9oA= +cloud.google.com/go/firestore v1.5.0/go.mod h1:c4nNYR1qdq7eaZ+jSc5fonrQN2k3M7sWATcYTiakjEo= +cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I= +cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw= +cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA= +cloud.google.com/go/pubsub v1.3.1/go.mod h1:i+ucay31+CNRpDW4Lu78I4xXG+O1r/MAHgjpRVR+TSU= +cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw= +cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos= +cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk= +cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs= +cloud.google.com/go/storage v1.10.0 h1:STgFzyU5/8miMl0//zKh2aQeTyeaUH3WN9bSUiJ09bA= +cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0= +dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= +firebase.google.com/go/v4 v4.7.1 h1:ZtyJU6zpl7Dv5tvzDmJZZljKT6TRShzoNnOAdKNK9DE= +firebase.google.com/go/v4 v4.7.1/go.mod h1:UgGSTOhEZVbB2L3dQ3z4pThDTiH869i8TDAZKnrHKbU= +github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= +github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= +github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY= +github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= +github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc= +github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= +github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= +github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= +github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= +github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= +github.com/cncf/udpa/go v0.0.0-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= +github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= +github.com/cncf/xds/go v0.0.0-20210312221358-fbca930ec8ed/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= +github.com/cyberdelia/templates v0.0.0-20141128023046-ca7fffd4298c/go.mod h1:GyV+0YP4qX0UQ7r2MoYZ+AvYDp12OF5yg4q8rGnyNh4= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/decred/dcrd/crypto/blake256 v1.0.0/go.mod h1:sQl2p6Y26YV+ZOcSTP6thNdn47hh8kt6rqSlvmrXFAc= +github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.0-20210816181553-5444fa50b93d/go.mod h1:tmAIfUFEirG/Y8jhZ9M+h36obRZAk/1fcSpXwAVlfqE= +github.com/deepmap/oapi-codegen v1.9.0 h1:qpyRY+dzjMai5QejjA53ebnBtcSvIcZOtYwVlsgdxOc= +github.com/deepmap/oapi-codegen v1.9.0/go.mod h1:7t4DbSxmAffcTEgrWvsPYEE2aOARZ8ZKWp3hDuZkHNc= +github.com/dgrijalva/jwt-go v3.2.0+incompatible h1:7qlOGliEKZXTDg6OTjfoBKDXWrumCAMpl/TFQ4/5kLM= +github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= +github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= +github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= +github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= +github.com/envoyproxy/go-control-plane v0.9.7/go.mod h1:cwu0lG7PUMfa9snN8LXBig5ynNVH9qI8YYLbd1fK2po= +github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= +github.com/envoyproxy/go-control-plane v0.9.9-0.20210512163311-63b5d3c536b0/go.mod h1:hliV/p42l8fGbc6Y9bQ70uLwIvmJyVE5k4iMKlh8wCQ= +github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= +github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= +github.com/getkin/kin-openapi v0.80.0/go.mod h1:660oXbgy5JFMKreazJaQTw7o+X00qeSyhcnluiMv+Xg= +github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= +github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI= +github.com/gin-gonic/gin v1.7.4/go.mod h1:jD2toBW3GZUr5UMcdrwQA10I7RuaFOl/SGeDjXkfUtY= +github.com/go-chi/chi/v5 v5.0.0/go.mod h1:BBug9lr0cqtdAhsu6R4AAdvufI0/XBzAQSsUqJpoZOs= +github.com/go-chi/chi/v5 v5.0.5 h1:l3RJ8T8TAqLsXFfah+RA6N4pydMbPwSdvNM+AFWvLUM= +github.com/go-chi/chi/v5 v5.0.5/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8= +github.com/go-chi/cors v1.0.1 h1:56TT/uWGoLWZpnMI/AwAmCneikXr5eLsiIq27wrKecw= +github.com/go-chi/cors v1.0.1/go.mod h1:K2Yje0VW/SJzxiyMYu6iPQYa7hMjQX2i/F491VChg1I= +github.com/go-chi/render v1.0.1 h1:4/5tis2cKaNdnv9zFLfXzcquC9HbeZgCnxGnKrltBS8= +github.com/go-chi/render v1.0.1/go.mod h1:pq4Rr7HbnsdaeHagklXub+p6Wd16Af5l9koip1OvJns= +github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= +github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= +github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= +github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= +github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= +github.com/go-openapi/jsonpointer v0.19.5/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg= +github.com/go-openapi/swag v0.19.5/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk= +github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= +github.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8= +github.com/go-playground/locales v0.14.0/go.mod h1:sawfccIbzZTqEDETgFXqTho0QybSa7l++s0DH+LDiLs= +github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+Scu5vgOQjsIJAF8j9muTVoKLVtA= +github.com/go-playground/universal-translator v0.18.0/go.mod h1:UvRDBj+xPUEGrFYl+lu/H90nyDXpg0fqeB/AQUGNTVA= +github.com/go-playground/validator/v10 v10.4.1/go.mod h1:nlOn6nFhuKACm19sB/8EGNn9GlaMV7XkbRSipzJ0Ii4= +github.com/go-playground/validator/v10 v10.9.0/go.mod h1:74x4gJWsvQexRdW8Pn3dXSGrTK4nAUsbPlLADvpJkos= +github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= +github.com/goccy/go-json v0.7.8/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= +github.com/gogo/protobuf v1.2.1 h1:/s5zKNz0uPFCZ5hddgPdo2TK2TVrUNMn0OOX8/aZMTE= +github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4= +github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= +github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e h1:1r7pUrabqp18hOBcwBwiTsbnFeTZHV9eER/QT5JVZxY= +github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y= +github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= +github.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= +github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= +github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4= +github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= +github.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= +github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk= +github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= +github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= +github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= +github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= +github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= +github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= +github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= +github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= +github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= +github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw= +github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= +github.com/golangci/lint-1 v0.0.0-20181222135242-d2cdd8c08219/go.mod h1:/X8TswGSh1pIozq4ZwCfxS0WA5JGXguxk94ar/4c87Y= +github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= +github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= +github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= +github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.4.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU= +github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/martian v2.1.0+incompatible h1:/CP5g8u/VJHijgedC/Legn3BAbAaWPgecwXBIDzw5no= +github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= +github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= +github.com/google/martian/v3 v3.1.0 h1:wCKgOCHuUEVfsaQLpPSJb7VdYCdTVZQAuOdYm1yc/60= +github.com/google/martian/v3 v3.1.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= +github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= +github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= +github.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20200430221834-fc25d7d30c6d/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20201023163331-3e6fc7fc9c4c/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/pprof v0.0.0-20201203190320-1bf35d6f28c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/pprof v0.0.0-20201218002935-b9804c9f04c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= +github.com/google/uuid v1.1.2 h1:EVhdT+1Kseyi1/pUmXKaFxYsDNy9RQYkMWRH68J/W7Y= +github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= +github.com/googleapis/gax-go/v2 v2.0.5 h1:sjZBwGj9Jlw33ImPtvFviGYvseOtDM7hkSKB7+Tv3SM= +github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= +github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So= +github.com/grpc-ecosystem/go-grpc-middleware v1.2.0 h1:0IKlLyQ3Hs9nDaiK5cSHAGmcQEIC8l2Ts1u6x5Dfrqg= +github.com/grpc-ecosystem/go-grpc-middleware v1.2.0/go.mod h1:mJzapYve32yjrKlk9GbyCZHuPgZsrbyIbyKhSzOpg6s= +github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw= +github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= +github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= +github.com/hpcloud/tail v1.0.0 h1:nfCOvKYfkgYP8hkirhJocXT2+zOD8yUNjXaWfTlyFKI= +github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= +github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= +github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= +github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= +github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= +github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= +github.com/jstemmer/go-junit-report v0.9.1 h1:6QPYqodiu3GuPL+7mfx+NwDdp2eTkp9IfEUpgAwUN0o= +github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk= +github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q= +github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +github.com/konsorten/go-windows-terminal-sequences v1.0.1 h1:mweAR1A6xJ3oS2pRaGiHgQ4OO8tzTaLawm8vnODuwDk= +github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= +github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= +github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0= +github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/labstack/echo/v4 v4.2.1/go.mod h1:AA49e0DZ8kk5jTOOCKNuPR6oTnBS0dYiM4FW1e6jwpg= +github.com/labstack/gommon v0.3.0/go.mod h1:MULnywXg0yavhxWKc+lOruYdAhDwPK9wf0OL7NoOu+k= +github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII= +github.com/leodido/go-urn v1.2.1/go.mod h1:zt4jvISO2HfUBqxjfIshjdMTYS56ZS/qv49ictyFfxY= +github.com/lestrrat-go/backoff/v2 v2.0.8/go.mod h1:rHP/q/r9aT27n24JQLa7JhSQZCKBBOiM/uP402WwN8Y= +github.com/lestrrat-go/blackmagic v1.0.0/go.mod h1:TNgH//0vYSs8VXDCfkZLgIrVTTXQELZffUV0tz3MtdQ= +github.com/lestrrat-go/codegen v1.0.2/go.mod h1:JhJw6OQAuPEfVKUCLItpaVLumDGWQznd1VaXrBk9TdM= +github.com/lestrrat-go/httpcc v1.0.0/go.mod h1:tGS/u00Vh5N6FHNkExqGGNId8e0Big+++0Gf8MBnAvE= +github.com/lestrrat-go/iter v1.0.1/go.mod h1:zIdgO1mRKhn8l9vrZJZz9TUMMFbQbLeTsbqPDrJ/OJc= +github.com/lestrrat-go/jwx v1.2.7/go.mod h1:bw24IXWbavc0R2RsOtpXL7RtMyP589yZ1+L7kd09ZGA= +github.com/lestrrat-go/option v1.0.0/go.mod h1:5ZHFbivi4xwXxhxY9XHDe2FHo6/Z7WWmtT7T5nBBp3I= +github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= +github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= +github.com/matryer/moq v0.0.0-20190312154309-6cfb0558e1bd/go.mod h1:9ELz6aaclSIGnZBoaSLZ3NAl1VTufbOrXBPvtcy6WiQ= +github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= +github.com/mattn/go-colorable v0.1.7/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= +github.com/mattn/go-colorable v0.1.8 h1:c1ghPdyEDarC70ftn0y+A/Ee++9zz8ljHG1b13eJ0s8= +github.com/mattn/go-colorable v0.1.8/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= +github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= +github.com/mattn/go-isatty v0.0.9/go.mod h1:YNRxwqDuOph6SZLI9vUUz6OYw3QyUt7WiY2yME+cCiQ= +github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= +github.com/mattn/go-isatty v0.0.14 h1:yVuAays6BHfxijgZPzw+3Zlu5yQgKGP2/hcQbHb7S9Y= +github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= +github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b h1:j7+1HpAFS1zy5+Q4qx1fWh90gTKwiN4QCGoY9TWyyO4= +github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= +github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/ginkgo v1.12.0 h1:Iw5WCbBcaAAd0fpRb1c9r5YCylv4XDoCSigm1zLevwU= +github.com/onsi/ginkgo v1.12.0/go.mod h1:oUhWkIvk5aDxtKvDDuw8gItl8pKl42LzjC9KZE0HfGg= +github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY= +github.com/onsi/gomega v1.9.0 h1:R1uwffexN6Pr340GtYRIdZmAiN4J+iw6WG4wog1DUXg= +github.com/onsi/gomega v1.9.0/go.mod h1:Ho0h+IUsWyvy1OpqCwxlQ/21gkhVunqlU8fDGcoTdcA= +github.com/opentracing/opentracing-go v1.1.0/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o= +github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= +github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ= +github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= +github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= +github.com/rogpeppe/go-internal v1.8.0 h1:FCbCCtXNOY3UtUuHUYaghJg4y7Fd14rXifAYUAtL9R8= +github.com/rogpeppe/go-internal v1.8.0/go.mod h1:WmiCO8CzOY8rg0OYDC4/i/2WRWAB6poM+XZ2dLUbcbE= +github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= +github.com/sirupsen/logrus v1.5.0 h1:1N5EYkVAPEywqZRJd7cwnRtCb6xJx7NH3T3WUTF980Q= +github.com/sirupsen/logrus v1.5.0/go.mod h1:+F7Ogzej0PZc/94MaYx/nvG9jOFMD2osvC3s+Squfpo= +github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= +github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw= +github.com/ugorji/go v1.2.6/go.mod h1:anCg0y61KIhDlPZmnH+so+RQbysYVyDko0IMgJv0Nn0= +github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY= +github.com/ugorji/go/codec v1.2.6/go.mod h1:V6TCNZ4PHqoHGFZuSG1W8nrCzzdgA2DozYxWFFpvxTw= +github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= +github.com/valyala/fasttemplate v1.0.1/go.mod h1:UQGH1tvbgY+Nz5t2n7tXsz52dQxojPUpymEIMZ47gx8= +github.com/valyala/fasttemplate v1.2.1/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ= +github.com/x-cray/logrus-prefixed-formatter v0.5.2 h1:00txxvfBM9muc0jiLIEAkAcIMJzfthRT6usrui8uGmg= +github.com/x-cray/logrus-prefixed-formatter v0.5.2/go.mod h1:2duySbKsL6M18s5GU7VPsoEPHyzalCE06qoARUCeBBE= +github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= +go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= +go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= +go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= +go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= +go.opencensus.io v0.22.5 h1:dntmOdLpSpHlVqbW5Eay97DelsZHe+55D+xC6i0dDS0= +go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk= +go.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqeYNgFYFoEGnI= +go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= +go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0= +go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20200820211705-5c72a883971a/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20201217014255-9d1352758620/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I= +golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/crypto v0.0.0-20210921155107-089bfa567519 h1:7I4JAnoQBe7ZtJcBaYHi5UtiO8tQHbUSXxL+pnGRANg= +golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= +golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek= +golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY= +golang.org/x/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= +golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= +golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= +golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM= +golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU= +golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= +golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= +golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= +golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs= +golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= +golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= +golang.org/x/lint v0.0.0-20201208152925-83fdc39ff7b5 h1:2M3HP5CCK1Si9FQhwnzYhXdG6DXeebvUHFpre8QvbyI= +golang.org/x/lint v0.0.0-20201208152925-83fdc39ff7b5/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= +golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE= +golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o= +golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= +golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY= +golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= +golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= +golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.4.1 h1:Kvvh58BN8Y9/lBi7hTekvtMpm07eUZ0ck5pRHpsMWrY= +golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200222125558-5a598a2470a0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200506145744-7e3656a0809f/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= +golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= +golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= +golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20201031054903-ff519b6c9102/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20201209123823-ac852fbbde11/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20201224014010-6772e930b67b/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110 h1:qWPm9rbaAMKs8Bq/9LRpbMqxWRVUAQwMI9fVrssnTfw= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= +golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20200902213428-5d25da1a8d43/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20201109201403-9fd604954f58/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20201208152858-08078c50e5b5/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20210218202405-ba52d332ba99 h1:5vD4XjIc0X5+kHZjx4UecYdjA6mJo+XXNoaW0EjU5Os= +golang.org/x/oauth2 v0.0.0-20210218202405-ba52d332ba99/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200331124033-c3d80250170d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200501052902-10377860bb8e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200511232937-7e40ca221e25/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200826173525-f9321e4c35a6/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200905004654-be1d3432aa8f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201201145000-ef89a241ccb3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210104204734-6f8348627aad/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210223095934-7937bea0104d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210806184541-e5e7981a1069/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20211031064116-611d5d643895 h1:iaNpwpnrgL5jzWS0vCNnfa8HqzxveCFpFx3uC/X4Tps= +golang.org/x/sys v0.0.0-20211031064116-611d5d643895/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1 h1:v+OssWQX+hTHEmOBgwxdZxK4zHq3yOs8F9J7mk0PY8E= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk= +golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.0.0-20201208040808-7e3f01d25324/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.0.0-20210220033141-f8bda1e9f3ba/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= +golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191130070609-6e064ea0cf2d/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191216173652-a0e659d51361/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20191227053925-7b8e75db28f4/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200117161641-43d50277825c/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200122220014-bf1340f18c4a/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200204074204-1cc6d1ef6c74/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200212150539-ea181f53ac56/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200224181240-023911ca70b2/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200227222343-706bc42d1f0d/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200304193943-95d2e580d8eb/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= +golang.org/x/tools v0.0.0-20200312045724-11d5b4c81c7d/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= +golang.org/x/tools v0.0.0-20200331025713-a30bf2db82d4/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8= +golang.org/x/tools v0.0.0-20200501065659-ab2804fb9c9d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200512131952-2bc93b1c0c88/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200515010526-7d3b6ebf133d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200618134242-20370b0cb4b2/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= +golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= +golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= +golang.org/x/tools v0.0.0-20200904185747-39188db58858/go.mod h1:Cj7w3i3Rnn0Xh82ur9kSqwfTHTeVxaDqrfMjpcNT6bE= +golang.org/x/tools v0.0.0-20200918232735-d647fc253266/go.mod h1:z6u4i615ZeAfBE4XtMziQW1fSVJXACjjbWkB/mvPzlU= +golang.org/x/tools v0.0.0-20201110124207-079ba7bd75cd/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.0.0-20201201161351-ac6f37ff4c2a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.0.0-20201208233053-a543418bbed2/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.0.0-20210105154028-b0ab187a4818/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.0.0-20210108195828-e2f9c7f1fc8e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.0.0-20210114065538-d78b04bdf963/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.1.0 h1:po9/4sTYwZU9lPhi1tOrb4hCv3qrhiQ77LZfGa2OjwY= +golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= +google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M= +google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= +google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= +google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= +google.golang.org/api v0.14.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= +google.golang.org/api v0.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= +google.golang.org/api v0.17.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.18.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.19.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.20.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.22.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.24.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= +google.golang.org/api v0.28.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= +google.golang.org/api v0.29.0/go.mod h1:Lcubydp8VUV7KeIHD9z2Bys/sm/vGKnG1UHuDBSrHWM= +google.golang.org/api v0.30.0/go.mod h1:QGmEvQ87FHZNiUVJkT14jQNYJ4ZJjdRF23ZXz5138Fc= +google.golang.org/api v0.35.0/go.mod h1:/XrVsuzM0rZmrsbjJutiuftIzeuTQcEeaYcSk/mQ1dg= +google.golang.org/api v0.36.0/go.mod h1:+z5ficQTmoYpPn8LCUNVpK5I7hwkpjbcgqA7I34qYtE= +google.golang.org/api v0.40.0 h1:uWrpz12dpVPn7cojP82mk02XDgTJLDPc2KbVTxrWb4A= +google.golang.org/api v0.40.0/go.mod h1:fYKFpnQN0DsDSKRVRcQSDQNtqWPfM9i+zNPxepjRCQ8= +google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= +google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0= +google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= +google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= +google.golang.org/appengine v1.6.7 h1:FZR1q0exgwxzPzp/aF+VccGrSfxfPpkBqjIIEq3ru6c= +google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= +google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= +google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= +google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= +google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8= +google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20191115194625-c23dd37a84c9/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20191216164720-4f79533eabd1/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20200115191322-ca5a22157cba/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20200122232147-0452cf42e150/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20200204135345-fa8e72b47b90/go.mod h1:GmwEX6Z4W5gMy59cAlVYjN9JhxgbQH6Gn+gFDQe2lzA= +google.golang.org/genproto v0.0.0-20200212174721-66ed5ce911ce/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200224152610-e50cd9704f63/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200228133532-8c2c7df3a383/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200305110556-506484158171/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200312145019-da6875a35672/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200331122359-1ee6d9798940/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200430143042-b979b6f78d84/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200511104702-f5ebc3bea380/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200513103714-09dca8ec2884/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200515170657-fc4c6c6a6587/go.mod h1:YsZOwe1myG/8QRHRsmBRE1LrgQY60beZKjly0O1fX9U= +google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= +google.golang.org/genproto v0.0.0-20200618031413-b414f8b61790/go.mod h1:jDfRM7FcilCzHH/e9qn6dsT145K34l5v+OpcnNgKAAA= +google.golang.org/genproto v0.0.0-20200729003335-053ba62fc06f/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20200804131852-c06518451d9c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20200825200019-8632dd797987/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20200904004341-0bd0a958aa1d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20201109203340-2640f1f9cdfb/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20201201144952-b05cb90ed32e/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20201210142538-e3217bee35cc/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20201214200347-8c77b98c765d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20210108203827-ffc7fda8c3d7/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20210222152913-aa3ee6e6a81c h1:7A9LQhrZmuCPI79/sYSbscFqBp4XFYf6oaIQuV1xji4= +google.golang.org/genproto v0.0.0-20210222152913-aa3ee6e6a81c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= +google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= +google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= +google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= +google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= +google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.28.0/go.mod h1:rpkK4SK4GF4Ach/+MFLZUBavHOvF2JJB5uozKKal+60= +google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk= +google.golang.org/grpc v1.30.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= +google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= +google.golang.org/grpc v1.31.1/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= +google.golang.org/grpc v1.33.1/go.mod h1:fr5YgcSWrqhRRxogOsw7RzIpsmvOZ6IcH4kBYTpR3n0= +google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc= +google.golang.org/grpc v1.34.0/go.mod h1:WotjhfgOW/POjDeRt8vscBtXq+2VjORFy659qA51WJ8= +google.golang.org/grpc v1.35.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= +google.golang.org/grpc v1.36.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= +google.golang.org/grpc v1.40.0 h1:AGJ0Ih4mHjSeibYkFGh1dD9KJ/eOtZ93I6hoHhukQ5Q= +google.golang.org/grpc v1.40.0/go.mod h1:ogyxbiOoUXAkP+4+xa6PZSE9DZgIHtSpzjDTB9KAK34= +google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= +google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= +google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= +google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= +google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= +google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4= +google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= +google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= +google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= +google.golang.org/protobuf v1.27.1 h1:SnqbnDw1V7RiZcXPx5MEeqPv2s79L9i7BJUlG/+RurQ= +google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= +gopkg.in/fsnotify.v1 v1.4.7 h1:xOHLXZwVvI9hhs+cLKq5+I5onOuwQLhQwiu63xxlHs4= +gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= +gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= +gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b h1:h8qDotaEPuJATrMmW04NCwg7v22aHH28wwpauUhK9Oo= +gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= +honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= +honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= +rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= +rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0= +rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA= diff --git a/wild-workouts/internal/common/logs/cqrs.go b/wild-workouts/internal/common/logs/cqrs.go new file mode 100644 index 0000000..6c33f58 --- /dev/null +++ b/wild-workouts/internal/common/logs/cqrs.go @@ -0,0 +1,15 @@ +package logs + +import ( + "github.com/sirupsen/logrus" +) + +func LogCommandExecution(commandName string, cmd interface{}, err error) { + log := logrus.WithField("cmd", cmd) + + if err == nil { + log.Info(commandName + " command succeeded") + } else { + log.WithError(err).Error(commandName + " command failed") + } +} diff --git a/wild-workouts/internal/common/logs/http.go b/wild-workouts/internal/common/logs/http.go new file mode 100644 index 0000000..be5b1d2 --- /dev/null +++ b/wild-workouts/internal/common/logs/http.go @@ -0,0 +1,64 @@ +package logs + +import ( + "fmt" + "net/http" + "time" + + "github.com/go-chi/chi/v5/middleware" + "github.com/sirupsen/logrus" +) + +func NewStructuredLogger(logger *logrus.Logger) func(next http.Handler) http.Handler { + return middleware.RequestLogger(&StructuredLogger{logger}) +} + +// based on example from chi: https://github.com/go-chi/chi/blob/master/_examples/logging/main.go +type StructuredLogger struct { + Logger *logrus.Logger +} + +func (l *StructuredLogger) NewLogEntry(r *http.Request) middleware.LogEntry { + entry := &StructuredLoggerEntry{Logger: logrus.NewEntry(l.Logger)} + logFields := logrus.Fields{} + + if reqID := middleware.GetReqID(r.Context()); reqID != "" { + logFields["req_id"] = reqID + } + + logFields["http_method"] = r.Method + + logFields["remote_addr"] = r.RemoteAddr + logFields["uri"] = r.RequestURI + + entry.Logger = entry.Logger.WithFields(logFields) + + entry.Logger.Info("Request started") + + return entry +} + +type StructuredLoggerEntry struct { + Logger logrus.FieldLogger +} + +func (l *StructuredLoggerEntry) Write(status, bytes int, header http.Header, elapsed time.Duration, extra interface{}) { + l.Logger = l.Logger.WithFields(logrus.Fields{ + "resp_status": status, "resp_bytes_length": bytes, + "resp_elapsed": elapsed.Round(time.Millisecond / 100).String(), + }) + + l.Logger.Info("Request completed ") +} + +func (l *StructuredLoggerEntry) Panic(v interface{}, stack []byte) { + l.Logger = l.Logger.WithFields(logrus.Fields{ + "stack": string(stack), + "panic": fmt.Sprintf("%+v", v), + }) +} + +func GetLogEntry(r *http.Request) logrus.FieldLogger { + entry := middleware.GetLogEntry(r).(*StructuredLoggerEntry) + return entry.Logger +} diff --git a/wild-workouts/internal/common/logs/logrus.go b/wild-workouts/internal/common/logs/logrus.go new file mode 100644 index 0000000..b6d145b --- /dev/null +++ b/wild-workouts/internal/common/logs/logrus.go @@ -0,0 +1,31 @@ +package logs + +import ( + "os" + "strconv" + + "github.com/sirupsen/logrus" + prefixed "github.com/x-cray/logrus-prefixed-formatter" +) + +func Init() { + SetFormatter(logrus.StandardLogger()) + + logrus.SetLevel(logrus.DebugLevel) +} + +func SetFormatter(logger *logrus.Logger) { + logger.SetFormatter(&logrus.JSONFormatter{ + FieldMap: logrus.FieldMap{ + logrus.FieldKeyTime: "time", + logrus.FieldKeyLevel: "severity", + logrus.FieldKeyMsg: "message", + }, + }) + + if isLocalEnv, _ := strconv.ParseBool(os.Getenv("LOCAL_ENV")); isLocalEnv { + logger.SetFormatter(&prefixed.TextFormatter{ + ForceFormatting: true, + }) + } +} diff --git a/wild-workouts/internal/common/metrics/dummy.go b/wild-workouts/internal/common/metrics/dummy.go new file mode 100644 index 0000000..bfe6b32 --- /dev/null +++ b/wild-workouts/internal/common/metrics/dummy.go @@ -0,0 +1,7 @@ +package metrics + +type NoOp struct{} + +func (d NoOp) Inc(_ string, _ int) { + // todo - add some implementation! +} diff --git a/wild-workouts/internal/common/server/grpc.go b/wild-workouts/internal/common/server/grpc.go new file mode 100644 index 0000000..a6e52f0 --- /dev/null +++ b/wild-workouts/internal/common/server/grpc.go @@ -0,0 +1,54 @@ +package server + +import ( + "fmt" + "net" + "os" + + "github.com/ThreeDotsLabs/wild-workouts-go-ddd-example/internal/common/logs" + grpc_middleware "github.com/grpc-ecosystem/go-grpc-middleware" + grpc_logrus "github.com/grpc-ecosystem/go-grpc-middleware/logging/logrus" + grpc_ctxtags "github.com/grpc-ecosystem/go-grpc-middleware/tags" + "github.com/sirupsen/logrus" + "google.golang.org/grpc" +) + +func init() { + logger := logrus.New() + logs.SetFormatter(logger) + logger.SetLevel(logrus.WarnLevel) + + grpc_logrus.ReplaceGrpcLogger(logrus.NewEntry(logger)) +} + +func RunGRPCServer(registerServer func(server *grpc.Server)) { + port := os.Getenv("PORT") + if port == "" { + port = "8080" + } + addr := fmt.Sprintf(":%s", port) + RunGRPCServerOnAddr(addr, registerServer) +} + +func RunGRPCServerOnAddr(addr string, registerServer func(server *grpc.Server)) { + logrusEntry := logrus.NewEntry(logrus.StandardLogger()) + + grpcServer := grpc.NewServer( + grpc_middleware.WithUnaryServerChain( + grpc_ctxtags.UnaryServerInterceptor(grpc_ctxtags.WithFieldExtractor(grpc_ctxtags.CodeGenRequestFieldExtractor)), + grpc_logrus.UnaryServerInterceptor(logrusEntry), + ), + grpc_middleware.WithStreamServerChain( + grpc_ctxtags.StreamServerInterceptor(grpc_ctxtags.WithFieldExtractor(grpc_ctxtags.CodeGenRequestFieldExtractor)), + grpc_logrus.StreamServerInterceptor(logrusEntry), + ), + ) + registerServer(grpcServer) + + listen, err := net.Listen("tcp", addr) + if err != nil { + logrus.Fatal(err) + } + logrus.WithField("grpcEndpoint", addr).Info("Starting: gRPC Listener") + logrus.Fatal(grpcServer.Serve(listen)) +} diff --git a/wild-workouts/internal/common/server/http.go b/wild-workouts/internal/common/server/http.go new file mode 100644 index 0000000..58f0d15 --- /dev/null +++ b/wild-workouts/internal/common/server/http.go @@ -0,0 +1,96 @@ +package server + +import ( + "context" + "net/http" + "os" + "strconv" + "strings" + + firebase "firebase.google.com/go/v4" + "github.com/ThreeDotsLabs/wild-workouts-go-ddd-example/internal/common/auth" + "github.com/ThreeDotsLabs/wild-workouts-go-ddd-example/internal/common/logs" + "github.com/go-chi/chi/v5" + "github.com/go-chi/chi/v5/middleware" + "github.com/go-chi/cors" + "github.com/sirupsen/logrus" + "google.golang.org/api/option" +) + +func RunHTTPServer(createHandler func(router chi.Router) http.Handler) { + RunHTTPServerOnAddr(":"+os.Getenv("PORT"), createHandler) +} + +func RunHTTPServerOnAddr(addr string, createHandler func(router chi.Router) http.Handler) { + apiRouter := chi.NewRouter() + setMiddlewares(apiRouter) + + rootRouter := chi.NewRouter() + // we are mounting all APIs under /api path + rootRouter.Mount("/api", createHandler(apiRouter)) + + logrus.Info("Starting HTTP server") + + err := http.ListenAndServe(addr, rootRouter) + if err != nil { + logrus.WithError(err).Panic("Unable to start HTTP server") + } +} + +func setMiddlewares(router *chi.Mux) { + router.Use(middleware.RequestID) + router.Use(middleware.RealIP) + router.Use(logs.NewStructuredLogger(logrus.StandardLogger())) + router.Use(middleware.Recoverer) + + addCorsMiddleware(router) + addAuthMiddleware(router) + + router.Use( + middleware.SetHeader("X-Content-Type-Options", "nosniff"), + middleware.SetHeader("X-Frame-Options", "deny"), + ) + router.Use(middleware.NoCache) +} + +func addAuthMiddleware(router *chi.Mux) { + if mockAuth, _ := strconv.ParseBool(os.Getenv("MOCK_AUTH")); mockAuth { + router.Use(auth.HttpMockMiddleware) + return + } + + var opts []option.ClientOption + if file := os.Getenv("SERVICE_ACCOUNT_FILE"); file != "" { + opts = append(opts, option.WithCredentialsFile(file)) + } + + config := &firebase.Config{ProjectID: os.Getenv("GCP_PROJECT")} + firebaseApp, err := firebase.NewApp(context.Background(), config, opts...) + if err != nil { + logrus.Fatalf("error initializing app: %v\n", err) + } + + authClient, err := firebaseApp.Auth(context.Background()) + if err != nil { + logrus.WithError(err).Fatal("Unable to create firebase Auth client") + } + + router.Use(auth.FirebaseHttpMiddleware{authClient}.Middleware) +} + +func addCorsMiddleware(router *chi.Mux) { + allowedOrigins := strings.Split(os.Getenv("CORS_ALLOWED_ORIGINS"), ";") + if len(allowedOrigins) == 0 { + return + } + + corsMiddleware := cors.New(cors.Options{ + AllowedOrigins: allowedOrigins, + AllowedMethods: []string{"GET", "POST", "PUT", "DELETE", "OPTIONS"}, + AllowedHeaders: []string{"Accept", "Authorization", "Content-Type", "X-CSRF-Token"}, + ExposedHeaders: []string{"Link"}, + AllowCredentials: true, + MaxAge: 300, + }) + router.Use(corsMiddleware.Handler) +} diff --git a/wild-workouts/internal/common/server/httperr/http_error.go b/wild-workouts/internal/common/server/httperr/http_error.go new file mode 100644 index 0000000..8ca82a4 --- /dev/null +++ b/wild-workouts/internal/common/server/httperr/http_error.go @@ -0,0 +1,57 @@ +package httperr + +import ( + "net/http" + + "github.com/ThreeDotsLabs/wild-workouts-go-ddd-example/internal/common/errors" + "github.com/ThreeDotsLabs/wild-workouts-go-ddd-example/internal/common/logs" + "github.com/go-chi/render" +) + +func InternalError(slug string, err error, w http.ResponseWriter, r *http.Request) { + httpRespondWithError(err, slug, w, r, "Internal server error", http.StatusInternalServerError) +} + +func Unauthorised(slug string, err error, w http.ResponseWriter, r *http.Request) { + httpRespondWithError(err, slug, w, r, "Unauthorised", http.StatusUnauthorized) +} + +func BadRequest(slug string, err error, w http.ResponseWriter, r *http.Request) { + httpRespondWithError(err, slug, w, r, "Bad request", http.StatusBadRequest) +} + +func RespondWithSlugError(err error, w http.ResponseWriter, r *http.Request) { + slugError, ok := err.(errors.SlugError) + if !ok { + InternalError("internal-server-error", err, w, r) + return + } + + switch slugError.ErrorType() { + case errors.ErrorTypeAuthorization: + Unauthorised(slugError.Slug(), slugError, w, r) + case errors.ErrorTypeIncorrectInput: + BadRequest(slugError.Slug(), slugError, w, r) + default: + InternalError(slugError.Slug(), slugError, w, r) + } +} + +func httpRespondWithError(err error, slug string, w http.ResponseWriter, r *http.Request, logMSg string, status int) { + logs.GetLogEntry(r).WithError(err).WithField("error-slug", slug).Warn(logMSg) + resp := ErrorResponse{slug, status} + + if err := render.Render(w, r, resp); err != nil { + panic(err) + } +} + +type ErrorResponse struct { + Slug string `json:"slug"` + httpStatus int +} + +func (e ErrorResponse) Render(w http.ResponseWriter, r *http.Request) error { + w.WriteHeader(e.httpStatus) + return nil +} diff --git a/wild-workouts/internal/common/tests/clients.go b/wild-workouts/internal/common/tests/clients.go new file mode 100644 index 0000000..b98dead --- /dev/null +++ b/wild-workouts/internal/common/tests/clients.go @@ -0,0 +1,174 @@ +package tests + +import ( + "context" + "fmt" + "net/http" + "os" + "strings" + "testing" + "time" + + "github.com/ThreeDotsLabs/wild-workouts-go-ddd-example/internal/common/client/trainer" + "github.com/ThreeDotsLabs/wild-workouts-go-ddd-example/internal/common/client/trainings" + "github.com/ThreeDotsLabs/wild-workouts-go-ddd-example/internal/common/client/users" + "github.com/stretchr/testify/require" +) + +func authorizationBearer(token string) func(context.Context, *http.Request) error { + return func(ctx context.Context, req *http.Request) error { + req.Header.Add("Authorization", fmt.Sprintf("Bearer %s", token)) + return nil + } +} + +type TrainerHTTPClient struct { + client *trainer.ClientWithResponses +} + +func NewTrainerHTTPClient(t *testing.T, token string) TrainerHTTPClient { + addr := os.Getenv("TRAINER_HTTP_ADDR") + ok := WaitForPort(addr) + require.True(t, ok, "Trainer HTTP timed out") + + url := fmt.Sprintf("http://%v/api", addr) + + client, err := trainer.NewClientWithResponses( + url, + trainer.WithRequestEditorFn(authorizationBearer(token)), + ) + require.NoError(t, err) + + return TrainerHTTPClient{ + client: client, + } +} + +func (c TrainerHTTPClient) MakeHourAvailable(t *testing.T, hour time.Time) int { + response, err := c.client.MakeHourAvailable(context.Background(), trainer.MakeHourAvailableJSONRequestBody{ + Hours: []time.Time{hour}, + }) + require.NoError(t, err) + require.NoError(t, response.Body.Close()) + + return response.StatusCode +} + +func (c TrainerHTTPClient) MakeHourUnavailable(t *testing.T, hour time.Time) { + response, err := c.client.MakeHourUnavailable(context.Background(), trainer.MakeHourUnavailableJSONRequestBody{ + Hours: []time.Time{hour}, + }) + require.NoError(t, err) + require.NoError(t, response.Body.Close()) + + require.Equal(t, http.StatusNoContent, response.StatusCode) +} + +func (c TrainerHTTPClient) GetTrainerAvailableHours(t *testing.T, from time.Time, to time.Time) []trainer.Date { + response, err := c.client.GetTrainerAvailableHoursWithResponse(context.Background(), &trainer.GetTrainerAvailableHoursParams{ + DateFrom: from, + DateTo: to, + }) + require.NoError(t, err) + require.Equal(t, http.StatusOK, response.StatusCode()) + + return *response.JSON200 +} + +type TrainingsHTTPClient struct { + client *trainings.ClientWithResponses +} + +func NewTrainingsHTTPClient(t *testing.T, token string) TrainingsHTTPClient { + addr := os.Getenv("TRAININGS_HTTP_ADDR") + fmt.Println("Trying trainings http:", addr) + ok := WaitForPort(addr) + require.True(t, ok, "Trainings HTTP timed out") + + url := fmt.Sprintf("http://%v/api", addr) + + client, err := trainings.NewClientWithResponses( + url, + trainings.WithRequestEditorFn(authorizationBearer(token)), + ) + require.NoError(t, err) + + return TrainingsHTTPClient{ + client: client, + } +} + +func (c TrainingsHTTPClient) CreateTraining(t *testing.T, note string, hour time.Time) string { + response, err := c.client.CreateTrainingWithResponse(context.Background(), trainings.CreateTrainingJSONRequestBody{ + Notes: note, + Time: hour, + }) + require.NoError(t, err) + require.Equal(t, http.StatusNoContent, response.StatusCode()) + + contentLocation := response.HTTPResponse.Header.Get("content-location") + + return lastPathElement(contentLocation) +} + +func (c TrainingsHTTPClient) CreateTrainingShouldFail(t *testing.T, note string, hour time.Time) { + response, err := c.client.CreateTraining(context.Background(), trainings.CreateTrainingJSONRequestBody{ + Notes: note, + Time: hour, + }) + require.NoError(t, err) + require.NoError(t, response.Body.Close()) + + require.Equal(t, http.StatusInternalServerError, response.StatusCode) +} + +func (c TrainingsHTTPClient) GetTrainings(t *testing.T) trainings.Trainings { + response, err := c.client.GetTrainingsWithResponse(context.Background()) + require.NoError(t, err) + require.Equal(t, http.StatusOK, response.StatusCode()) + + return *response.JSON200 +} + +func (c TrainingsHTTPClient) CancelTraining(t *testing.T, trainingUUID string, expectedStatusCode int) { + response, err := c.client.CancelTraining(context.Background(), trainingUUID) + require.NoError(t, err) + require.NoError(t, response.Body.Close()) + + require.Equal(t, expectedStatusCode, response.StatusCode) +} + +type UsersHTTPClient struct { + client *users.ClientWithResponses +} + +func NewUsersHTTPClient(t *testing.T, token string) UsersHTTPClient { + addr := os.Getenv("USERS_HTTP_ADDR") + ok := WaitForPort(addr) + require.True(t, ok, "Users HTTP timed out") + + url := fmt.Sprintf("http://%v/api", addr) + + client, err := users.NewClientWithResponses( + url, + users.WithRequestEditorFn(authorizationBearer(token)), + ) + require.NoError(t, err) + + return UsersHTTPClient{ + client: client, + } +} + +func (c UsersHTTPClient) GetCurrentUser(t *testing.T) users.User { + response, err := c.client.GetCurrentUserWithResponse(context.Background()) + require.NoError(t, err) + require.Equal(t, http.StatusOK, response.StatusCode()) + + return *response.JSON200 +} + +func lastPathElement(path string) string { + parts := strings.Split(path, "/") + return parts[len(parts)-1] +} diff --git a/wild-workouts/internal/common/tests/e2e_test.go b/wild-workouts/internal/common/tests/e2e_test.go new file mode 100644 index 0000000..0b7b35b --- /dev/null +++ b/wild-workouts/internal/common/tests/e2e_test.go @@ -0,0 +1,69 @@ +package tests + +import ( + "context" + "testing" + + "github.com/ThreeDotsLabs/wild-workouts-go-ddd-example/internal/common/client" + "github.com/ThreeDotsLabs/wild-workouts-go-ddd-example/internal/common/genproto/users" + "github.com/google/uuid" + "github.com/stretchr/testify/require" +) + +func TestCreateTraining(t *testing.T) { + t.Parallel() + + hour := RelativeDate(12, 12) + + userID := "TestCreateTraining-user" + trainerJWT := FakeTrainerJWT(t, uuid.New().String()) + attendeeJWT := FakeAttendeeJWT(t, userID) + trainerHTTPClient := NewTrainerHTTPClient(t, trainerJWT) + trainingsHTTPClient := NewTrainingsHTTPClient(t, attendeeJWT) + usersHTTPClient := NewUsersHTTPClient(t, attendeeJWT) + + usersGrpcClient, _, err := client.NewUsersClient() + require.NoError(t, err) + + // Cancel the training if exists and make the hour available + trainings := trainingsHTTPClient.GetTrainings(t) + for _, training := range trainings.Trainings { + if training.Time.Equal(hour) { + trainingsTrainerHTTPClient := NewTrainingsHTTPClient(t, trainerJWT) + trainingsTrainerHTTPClient.CancelTraining(t, training.Uuid, 200) + break + } + } + hours := trainerHTTPClient.GetTrainerAvailableHours(t, hour, hour) + if len(hours) > 0 { + for _, h := range hours[0].Hours { + if h.Hour.Equal(hour) { + trainerHTTPClient.MakeHourUnavailable(t, hour) + break + } + } + } + + trainerHTTPClient.MakeHourAvailable(t, hour) + + user := usersHTTPClient.GetCurrentUser(t) + originalBalance := user.Balance + + _, err = usersGrpcClient.UpdateTrainingBalance(context.Background(), &users.UpdateTrainingBalanceRequest{ + UserId: userID, + AmountChange: 1, + }) + require.NoError(t, err) + + user = usersHTTPClient.GetCurrentUser(t) + require.Equal(t, originalBalance+1, user.Balance, "Attendee's balance should be updated") + + trainingUUID := trainingsHTTPClient.CreateTraining(t, "some note", hour) + + trainingsResponse := trainingsHTTPClient.GetTrainings(t) + require.Len(t, trainingsResponse.Trainings, 1) + require.Equal(t, trainingUUID, trainingsResponse.Trainings[0].Uuid, "Attendee should see the training") + + user = usersHTTPClient.GetCurrentUser(t) + require.Equal(t, originalBalance, user.Balance, "Attendee's balance should be updated after a training is scheduled") +} diff --git a/wild-workouts/internal/common/tests/hours.go b/wild-workouts/internal/common/tests/hours.go new file mode 100644 index 0000000..6c5671d --- /dev/null +++ b/wild-workouts/internal/common/tests/hours.go @@ -0,0 +1,15 @@ +package tests + +import "time" + +// RelativeDate returns a date in the future in specified days and hours. +// This allows running the tests in parallel, since each tests uses different date and hour. +// +// The downside of this approach is that you need to be aware of used dates when adding a new test. +// In our case this is not an issue, as it's trivial to see all usages and there's just a few of them. +// +// Another, more complex approach, would be to use random dates and retry in case of an error. +func RelativeDate(days int, hour int) time.Time { + now := time.Now().UTC().AddDate(0, 0, days) + return time.Date(now.Year(), now.Month(), now.Day(), hour, 0, 0, 0, time.UTC) +} diff --git a/wild-workouts/internal/common/tests/jwt.go b/wild-workouts/internal/common/tests/jwt.go new file mode 100644 index 0000000..417e98f --- /dev/null +++ b/wild-workouts/internal/common/tests/jwt.go @@ -0,0 +1,35 @@ +package tests + +import ( + "testing" + + "github.com/dgrijalva/jwt-go" + "github.com/stretchr/testify/require" +) + +func FakeAttendeeJWT(t *testing.T, userID string) string { + return fakeJWT(t, jwt.MapClaims{ + "user_uuid": userID, + "email": "attendee@threedots.tech", + "role": "attendee", + "name": "Attendee", + }) +} + +func FakeTrainerJWT(t *testing.T, userID string) string { + return fakeJWT(t, jwt.MapClaims{ + "user_uuid": userID, + "email": "trainer@threedots.tech", + "role": "trainer", + "name": "Trainer", + }) +} + +func fakeJWT(t *testing.T, claims jwt.MapClaims) string { + token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) + + tokenString, err := token.SignedString([]byte("mock_secret")) + require.NoError(t, err) + + return tokenString +} diff --git a/wild-workouts/internal/common/tests/wait.go b/wild-workouts/internal/common/tests/wait.go new file mode 100644 index 0000000..d1f510b --- /dev/null +++ b/wild-workouts/internal/common/tests/wait.go @@ -0,0 +1,33 @@ +package tests + +import ( + "net" + "time" +) + +func WaitForPort(address string) bool { + waitChan := make(chan struct{}) + + go func() { + for { + conn, err := net.DialTimeout("tcp", address, time.Second) + if err != nil { + time.Sleep(time.Second) + continue + } + + if conn != nil { + waitChan <- struct{}{} + return + } + } + }() + + timeout := time.After(5 * time.Second) + select { + case <-waitChan: + return true + case <-timeout: + return false + } +} diff --git a/wild-workouts/internal/trainer/adapters/hour_firestore_repository.go b/wild-workouts/internal/trainer/adapters/hour_firestore_repository.go new file mode 100644 index 0000000..b1c265f --- /dev/null +++ b/wild-workouts/internal/trainer/adapters/hour_firestore_repository.go @@ -0,0 +1,222 @@ +package adapters + +import ( + "context" + "time" + + "cloud.google.com/go/firestore" + "github.com/pkg/errors" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" + + "github.com/ThreeDotsLabs/wild-workouts-go-ddd-example/internal/trainer/domain/hour" +) + +type ( + DateModel struct { + Date time.Time `firestore:"Date"` + HasFreeHours bool `firestore:"HasFreeHours"` + Hours []HourModel `firestore:"Hours"` + } + + HourModel struct { + Available bool `firestore:"Available"` + HasTrainingScheduled bool `firestore:"HasTrainingScheduled"` + Hour time.Time `firestore:"Hour"` + } +) + +type FirestoreHourRepository struct { + firestoreClient *firestore.Client + hourFactory hour.Factory +} + +func NewFirestoreHourRepository(firestoreClient *firestore.Client, hourFactory hour.Factory) *FirestoreHourRepository { + if firestoreClient == nil { + panic("missing firestoreClient") + } + if hourFactory.IsZero() { + panic("missing hourFactory") + } + + return &FirestoreHourRepository{firestoreClient, hourFactory} +} + +func (f FirestoreHourRepository) GetHour(ctx context.Context, time time.Time) (*hour.Hour, error) { + date, err := f.getDateDTO( + // getDateDTO should be used both for transactional and non transactional query, + // the best way for that is to use closure + func() (doc *firestore.DocumentSnapshot, err error) { + return f.documentRef(time).Get(ctx) + }, + time, + ) + if err != nil { + return nil, err + } + + hourFromDb, err := f.domainHourFromDateDTO(date, time) + if err != nil { + return nil, err + } + + return hourFromDb, err +} + +func (f FirestoreHourRepository) UpdateHour( + ctx context.Context, + hourTime time.Time, + updateFn func(h *hour.Hour) (*hour.Hour, error), +) error { + err := f.firestoreClient.RunTransaction(ctx, func(ctx context.Context, transaction *firestore.Transaction) error { + dateDocRef := f.documentRef(hourTime) + + firebaseDate, err := f.getDateDTO( + // getDateDTO should be used both for transactional and non transactional query, + // the best way for that is to use closure + func() (doc *firestore.DocumentSnapshot, err error) { + return transaction.Get(dateDocRef) + }, + hourTime, + ) + if err != nil { + return err + } + + hourFromDB, err := f.domainHourFromDateDTO(firebaseDate, hourTime) + if err != nil { + return err + } + + updatedHour, err := updateFn(hourFromDB) + if err != nil { + return errors.Wrap(err, "unable to update hour") + } + updateHourInDataDTO(updatedHour, &firebaseDate) + + return transaction.Set(dateDocRef, firebaseDate) + }) + + return errors.Wrap(err, "firestore transaction failed") +} + +func (f FirestoreHourRepository) trainerHoursCollection() *firestore.CollectionRef { + return f.firestoreClient.Collection("trainer-hours") +} + +func (f FirestoreHourRepository) documentRef(hourTime time.Time) *firestore.DocumentRef { + return f.trainerHoursCollection().Doc(hourTime.Format("2006-01-02")) +} + +func (f FirestoreHourRepository) getDateDTO( + getDocumentFn func() (doc *firestore.DocumentSnapshot, err error), + dateTime time.Time, +) (DateModel, error) { + doc, err := getDocumentFn() + if status.Code(err) == codes.NotFound { + // in reality this date exists, even if it's not persisted + return NewEmptyDateDTO(dateTime), nil + } + if err != nil { + return DateModel{}, err + } + + date := DateModel{} + if err := doc.DataTo(&date); err != nil { + return DateModel{}, errors.Wrap(err, "unable to unmarshal DateModel from Firestore") + } + + return date, nil +} + +// for now we are keeping backward comparability, because of that it's a bit messy and overcomplicated +// todo - we will clean it up later with CQRS :-) +func (f FirestoreHourRepository) domainHourFromDateDTO(date DateModel, hourTime time.Time) (*hour.Hour, error) { + firebaseHour, found := findHourInDateDTO(date, hourTime) + if !found { + // in reality this date exists, even if it's not persisted + return f.hourFactory.NewNotAvailableHour(hourTime) + } + + availability, err := mapAvailabilityFromDTO(firebaseHour) + if err != nil { + return nil, err + } + + return f.hourFactory.UnmarshalHourFromDatabase(firebaseHour.Hour.Local(), availability) +} + +// for now we are keeping backward comparability, because of that it's a bit messy and overcomplicated +// todo - we will clean it up later with CQRS :-) +func updateHourInDataDTO(updatedHour *hour.Hour, firebaseDate *DateModel) { + firebaseHourDTO := domainHourToDTO(updatedHour) + + hourFound := false + for i := range firebaseDate.Hours { + if !firebaseDate.Hours[i].Hour.Equal(updatedHour.Time()) { + continue + } + + firebaseDate.Hours[i] = firebaseHourDTO + hourFound = true + break + } + + if !hourFound { + firebaseDate.Hours = append(firebaseDate.Hours, firebaseHourDTO) + } + + firebaseDate.HasFreeHours = false + for _, h := range firebaseDate.Hours { + if h.Available { + firebaseDate.HasFreeHours = true + break + } + } +} + +func mapAvailabilityFromDTO(firebaseHour HourModel) (hour.Availability, error) { + if firebaseHour.Available && !firebaseHour.HasTrainingScheduled { + return hour.Available, nil + } + if !firebaseHour.Available && firebaseHour.HasTrainingScheduled { + return hour.TrainingScheduled, nil + } + if !firebaseHour.Available && !firebaseHour.HasTrainingScheduled { + return hour.NotAvailable, nil + } + + return hour.Availability{}, errors.Errorf( + "unsupported values - Available: %t, HasTrainingScheduled: %t", + firebaseHour.Available, + firebaseHour.HasTrainingScheduled, + ) +} + +func domainHourToDTO(updatedHour *hour.Hour) HourModel { + return HourModel{ + Available: updatedHour.IsAvailable(), + HasTrainingScheduled: updatedHour.HasTrainingScheduled(), + Hour: updatedHour.Time(), + } +} + +func findHourInDateDTO(firebaseDate DateModel, time time.Time) (HourModel, bool) { + for i := range firebaseDate.Hours { + firebaseHour := firebaseDate.Hours[i] + + if !firebaseHour.Hour.Equal(time) { + continue + } + + return firebaseHour, true + } + + return HourModel{}, false +} + +func NewEmptyDateDTO(t time.Time) DateModel { + return DateModel{ + Date: t.UTC().Truncate(time.Hour * 24), + } +} diff --git a/wild-workouts/internal/trainer/adapters/hour_memory_repository.go b/wild-workouts/internal/trainer/adapters/hour_memory_repository.go new file mode 100644 index 0000000..8dee554 --- /dev/null +++ b/wild-workouts/internal/trainer/adapters/hour_memory_repository.go @@ -0,0 +1,69 @@ +package adapters + +import ( + "context" + "sync" + "time" + + "github.com/ThreeDotsLabs/wild-workouts-go-ddd-example/internal/trainer/domain/hour" +) + +type MemoryHourRepository struct { + hours map[time.Time]hour.Hour + lock *sync.RWMutex + + hourFactory hour.Factory +} + +func NewMemoryHourRepository(hourFactory hour.Factory) *MemoryHourRepository { + if hourFactory.IsZero() { + panic("missing hourFactory") + } + + return &MemoryHourRepository{ + hours: map[time.Time]hour.Hour{}, + lock: &sync.RWMutex{}, + hourFactory: hourFactory, + } +} + +func (m MemoryHourRepository) GetHour(_ context.Context, hourTime time.Time) (*hour.Hour, error) { + m.lock.RLock() + defer m.lock.RUnlock() + + return m.getOrCreateHour(hourTime) +} + +func (m MemoryHourRepository) getOrCreateHour(hourTime time.Time) (*hour.Hour, error) { + currentHour, ok := m.hours[hourTime] + if !ok { + return m.hourFactory.NewNotAvailableHour(hourTime) + } + + // we don't store hours as pointers, but as values + // thanks to that, we are sure that nobody can modify Hour without using UpdateHour + return ¤tHour, nil +} + +func (m *MemoryHourRepository) UpdateHour( + _ context.Context, + hourTime time.Time, + updateFn func(h *hour.Hour) (*hour.Hour, error), +) error { + m.lock.Lock() + defer m.lock.Unlock() + + currentHour, err := m.getOrCreateHour(hourTime) + if err != nil { + return err + } + + updatedHour, err := updateFn(currentHour) + if err != nil { + return err + } + + m.hours[hourTime] = *updatedHour + + return nil +} diff --git a/wild-workouts/internal/trainer/adapters/hour_mysql_repository.go b/wild-workouts/internal/trainer/adapters/hour_mysql_repository.go new file mode 100644 index 0000000..54c4d2e --- /dev/null +++ b/wild-workouts/internal/trainer/adapters/hour_mysql_repository.go @@ -0,0 +1,198 @@ +package adapters + +import ( + "context" + "database/sql" + "os" + "time" + + "github.com/ThreeDotsLabs/wild-workouts-go-ddd-example/internal/trainer/domain/hour" + "github.com/go-sql-driver/mysql" + "github.com/jmoiron/sqlx" + "github.com/pkg/errors" + "go.uber.org/multierr" +) + +type mysqlHour struct { + ID string `db:"id"` + Hour time.Time `db:"hour"` + Availability string `db:"availability"` +} + +type MySQLHourRepository struct { + db *sqlx.DB + hourFactory hour.Factory +} + +func NewMySQLHourRepository(db *sqlx.DB, hourFactory hour.Factory) *MySQLHourRepository { + if db == nil { + panic("missing db") + } + if hourFactory.IsZero() { + panic("missing hourFactory") + } + + return &MySQLHourRepository{db: db, hourFactory: hourFactory} +} + +// sqlContextGetter is an interface provided both by transaction and standard db connection +type sqlContextGetter interface { + GetContext(ctx context.Context, dest interface{}, query string, args ...interface{}) error +} + +func (m MySQLHourRepository) GetHour(ctx context.Context, time time.Time) (*hour.Hour, error) { + return m.getOrCreateHour(ctx, m.db, time, false) +} + +func (m MySQLHourRepository) getOrCreateHour( + ctx context.Context, + db sqlContextGetter, + hourTime time.Time, + forUpdate bool, +) (*hour.Hour, error) { + dbHour := mysqlHour{} + + query := "SELECT * FROM `hours` WHERE `hour` = ?" + if forUpdate { + query += " FOR UPDATE" + } + + err := db.GetContext(ctx, &dbHour, query, hourTime.UTC()) + if errors.Is(err, sql.ErrNoRows) { + // in reality this date exists, even if it's not persisted + return m.hourFactory.NewNotAvailableHour(hourTime) + } else if err != nil { + return nil, errors.Wrap(err, "unable to get hour from db") + } + + availability, err := hour.NewAvailabilityFromString(dbHour.Availability) + if err != nil { + return nil, err + } + + domainHour, err := m.hourFactory.UnmarshalHourFromDatabase(dbHour.Hour.Local(), availability) + if err != nil { + return nil, err + } + + return domainHour, nil +} + +const mySQLDeadlockErrorCode = 1213 + +func (m MySQLHourRepository) UpdateHour( + ctx context.Context, + hourTime time.Time, + updateFn func(h *hour.Hour) (*hour.Hour, error), +) error { + for { + err := m.updateHour(ctx, hourTime, updateFn) + + if val, ok := errors.Cause(err).(*mysql.MySQLError); ok && val.Number == mySQLDeadlockErrorCode { + continue + } + + return err + } +} + +func (m MySQLHourRepository) updateHour( + ctx context.Context, + hourTime time.Time, + updateFn func(h *hour.Hour) (*hour.Hour, error), +) (err error) { + tx, err := m.db.Beginx() + if err != nil { + return errors.Wrap(err, "unable to start transaction") + } + + // Defer is executed on function just before exit. + // With defer, we are always sure that we will close our transaction properly. + defer func() { + // In `UpdateHour` we are using named return - `(err error)`. + // Thanks to that, that can check if function exits with error. + // + // Even if function exits without error, commit still can return error. + // In that case we can override nil to err `err = m.finish...`. + err = m.finishTransaction(err, tx) + }() + + existingHour, err := m.getOrCreateHour(ctx, tx, hourTime, true) + if err != nil { + return err + } + + updatedHour, err := updateFn(existingHour) + if err != nil { + return err + } + + if err := m.upsertHour(tx, updatedHour); err != nil { + return err + } + + return nil +} + +// upsertHour updates hour if hour already exists in the database. +// If your doesn't exists, it's inserted. +func (m MySQLHourRepository) upsertHour(tx *sqlx.Tx, hourToUpdate *hour.Hour) error { + updatedDbHour := mysqlHour{ + Hour: hourToUpdate.Time().UTC(), + Availability: hourToUpdate.Availability().String(), + } + + _, err := tx.NamedExec( + `INSERT INTO + hours (hour, availability) + VALUES + (:hour, :availability) + ON DUPLICATE KEY UPDATE + availability = :availability`, + updatedDbHour, + ) + if err != nil { + return errors.Wrap(err, "unable to upsert hour") + } + + return nil +} + +// finishTransaction rollbacks transaction if error is provided. +// If err is nil transaction is committed. +// +// If the rollback fails, we are using multierr library to add error about rollback failure. +// If the commit fails, commit error is returned. +func (m MySQLHourRepository) finishTransaction(err error, tx *sqlx.Tx) error { + if err != nil { + if rollbackErr := tx.Rollback(); rollbackErr != nil { + return multierr.Combine(err, rollbackErr) + } + + return err + } else { + if commitErr := tx.Commit(); commitErr != nil { + return errors.Wrap(err, "failed to commit tx") + } + + return nil + } +} + +func NewMySQLConnection() (*sqlx.DB, error) { + config := mysql.NewConfig() + + config.Net = "tcp" + config.Addr = os.Getenv("MYSQL_ADDR") + config.User = os.Getenv("MYSQL_USER") + config.Passwd = os.Getenv("MYSQL_PASSWORD") + config.DBName = os.Getenv("MYSQL_DATABASE") + config.ParseTime = true // with that parameter, we can use time.Time in mysqlHour.Hour + + db, err := sqlx.Connect("mysql", config.FormatDSN()) + if err != nil { + return nil, errors.Wrap(err, "cannot connect to MySQL") + } + + return db, nil +} diff --git a/wild-workouts/internal/trainer/adapters/hour_repository_test.go b/wild-workouts/internal/trainer/adapters/hour_repository_test.go new file mode 100644 index 0000000..0071e95 --- /dev/null +++ b/wild-workouts/internal/trainer/adapters/hour_repository_test.go @@ -0,0 +1,350 @@ +package adapters_test + +import ( + "context" + "errors" + "math/rand" + "os" + "sync" + "testing" + "time" + + "github.com/ThreeDotsLabs/wild-workouts-go-ddd-example/internal/trainer/adapters" + + "cloud.google.com/go/firestore" + "github.com/ThreeDotsLabs/wild-workouts-go-ddd-example/internal/trainer/domain/hour" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestRepository(t *testing.T) { + t.Parallel() + rand.Seed(time.Now().UTC().UnixNano()) + + repositories := createRepositories(t) + + for i := range repositories { + // When you are looping over slice and later using iterated value in goroutine (here because of t.Parallel()), + // you need to always create variable scoped in loop body! + // More info here: https://github.com/golang/go/wiki/CommonMistakes#using-goroutines-on-loop-iterator-variables + r := repositories[i] + + t.Run(r.Name, func(t *testing.T) { + // It's always a good idea to build all non-unit tests to be able to work in parallel. + // Thanks to that, your tests will be always fast and you will not be afraid to add more tests because of slowdown. + t.Parallel() + + t.Run("testUpdateHour", func(t *testing.T) { + t.Parallel() + testUpdateHour(t, r.Repository) + }) + t.Run("testUpdateHour_parallel", func(t *testing.T) { + t.Parallel() + testUpdateHour_parallel(t, r.Repository) + }) + t.Run("testHourRepository_update_existing", func(t *testing.T) { + t.Parallel() + testHourRepository_update_existing(t, r.Repository) + }) + t.Run("testUpdateHour_rollback", func(t *testing.T) { + t.Parallel() + testUpdateHour_rollback(t, r.Repository) + }) + }) + } +} + +type Repository struct { + Name string + Repository hour.Repository +} + +func createRepositories(t *testing.T) []Repository { + return []Repository{ + { + Name: "Firebase", + Repository: newFirebaseRepository(t, context.Background()), + }, + { + Name: "MySQL", + Repository: newMySQLRepository(t), + }, + { + Name: "memory", + Repository: adapters.NewMemoryHourRepository(testHourFactory), + }, + } +} + +func testUpdateHour(t *testing.T, repository hour.Repository) { + t.Helper() + ctx := context.Background() + + testCases := []struct { + Name string + CreateHour func(*testing.T) *hour.Hour + }{ + { + Name: "available_hour", + CreateHour: func(t *testing.T) *hour.Hour { + return newValidAvailableHour(t) + }, + }, + { + Name: "not_available_hour", + CreateHour: func(t *testing.T) *hour.Hour { + h := newValidAvailableHour(t) + require.NoError(t, h.MakeNotAvailable()) + + return h + }, + }, + { + Name: "hour_with_training", + CreateHour: func(t *testing.T) *hour.Hour { + h := newValidAvailableHour(t) + require.NoError(t, h.ScheduleTraining()) + + return h + }, + }, + } + + for _, tc := range testCases { + tc := tc + t.Run(tc.Name, func(t *testing.T) { + t.Parallel() + newHour := tc.CreateHour(t) + + err := repository.UpdateHour(ctx, newHour.Time(), func(_ *hour.Hour) (*hour.Hour, error) { + // UpdateHour provides us existing/new *hour.Hour, + // but we are ignoring this hour and persisting result of `CreateHour` + // we can assert this hour later in assertHourInRepository + return newHour, nil + }) + require.NoError(t, err) + + assertHourInRepository(ctx, t, repository, newHour) + }) + } +} + +func testUpdateHour_parallel(t *testing.T, repository hour.Repository) { + if _, ok := repository.(*adapters.FirestoreHourRepository); ok { + // todo - enable after fix of https://github.com/googleapis/google-cloud-go/issues/2604 + t.Skip("because of emulator bug, it's not working in Firebase") + } + + t.Helper() + ctx := context.Background() + + hourTime := newValidHourTime() + + // we are adding available hour + err := repository.UpdateHour(ctx, hourTime, func(h *hour.Hour) (*hour.Hour, error) { + if err := h.MakeAvailable(); err != nil { + return nil, err + } + return h, nil + }) + require.NoError(t, err) + + workersCount := 20 + workersDone := sync.WaitGroup{} + workersDone.Add(workersCount) + + // closing startWorkers will unblock all workers at once, + // thanks to that it will be more likely to have race condition + startWorkers := make(chan struct{}) + // if training was successfully scheduled, number of the worker is sent to this channel + trainingsScheduled := make(chan int, workersCount) + + // we are trying to do race condition, in practice only one worker should be able to finish transaction + for worker := 0; worker < workersCount; worker++ { + workerNum := worker + + go func() { + defer workersDone.Done() + <-startWorkers + + schedulingTraining := false + + err := repository.UpdateHour(ctx, hourTime, func(h *hour.Hour) (*hour.Hour, error) { + // training is already scheduled, nothing to do there + if h.HasTrainingScheduled() { + return h, nil + } + // training is not scheduled yet, so let's try to do that + if err := h.ScheduleTraining(); err != nil { + return nil, err + } + + schedulingTraining = true + + return h, nil + }) + + if schedulingTraining && err == nil { + // training is only scheduled if UpdateHour didn't return an error + trainingsScheduled <- workerNum + } + }() + } + + close(startWorkers) + + // we are waiting, when all workers did the job + workersDone.Wait() + close(trainingsScheduled) + + var workersScheduledTraining []int + + for workerNum := range trainingsScheduled { + workersScheduledTraining = append(workersScheduledTraining, workerNum) + } + + assert.Len(t, workersScheduledTraining, 1, "only one worker should schedule training") +} + +func testUpdateHour_rollback(t *testing.T, repository hour.Repository) { + t.Helper() + ctx := context.Background() + + hourTime := newValidHourTime() + + err := repository.UpdateHour(ctx, hourTime, func(h *hour.Hour) (*hour.Hour, error) { + require.NoError(t, h.MakeAvailable()) + return h, nil + }) + require.NoError(t, err) + + err = repository.UpdateHour(ctx, hourTime, func(h *hour.Hour) (*hour.Hour, error) { + assert.True(t, h.IsAvailable()) + require.NoError(t, h.MakeNotAvailable()) + + return h, errors.New("something went wrong") + }) + require.Error(t, err) + + persistedHour, err := repository.GetHour(ctx, hourTime) + require.NoError(t, err) + + assert.True(t, persistedHour.IsAvailable(), "availability change was persisted, not rolled back") +} + +// testHourRepository_update_existing is testing path of creating a new hour and updating this hour. +func testHourRepository_update_existing(t *testing.T, repository hour.Repository) { + t.Helper() + ctx := context.Background() + + testHour := newValidAvailableHour(t) + + err := repository.UpdateHour(ctx, testHour.Time(), func(_ *hour.Hour) (*hour.Hour, error) { + return testHour, nil + }) + require.NoError(t, err) + assertHourInRepository(ctx, t, repository, testHour) + + var expectedHour *hour.Hour + err = repository.UpdateHour(ctx, testHour.Time(), func(h *hour.Hour) (*hour.Hour, error) { + if err := h.ScheduleTraining(); err != nil { + return nil, err + } + expectedHour = h + return h, nil + }) + require.NoError(t, err) + + assertHourInRepository(ctx, t, repository, expectedHour) +} + +func TestNewDateDTO(t *testing.T) { + t.Parallel() + testCases := []struct { + Time time.Time + ExpectedDateTime time.Time + }{ + { + Time: time.Date(3333, 1, 1, 0, 0, 0, 0, time.UTC), + ExpectedDateTime: time.Date(3333, 1, 1, 0, 0, 0, 0, time.UTC), + }, + { + // we are storing date in UTC + // it's still 1 January 22:00 in UTC while it's midnight in +2 timezone + Time: time.Date(3333, 1, 2, 0, 0, 0, 0, time.FixedZone("FOO", 2*60*60)), + ExpectedDateTime: time.Date(3333, 1, 1, 0, 0, 0, 0, time.UTC), + }, + } + + for _, c := range testCases { + c := c + t.Run(c.Time.String(), func(t *testing.T) { + t.Parallel() + dateDTO := adapters.NewEmptyDateDTO(c.Time) + assert.True(t, dateDTO.Date.Equal(c.ExpectedDateTime), "%s != %s", dateDTO.Date, c.ExpectedDateTime) + }) + } +} + +// in general global state is not the best idea, but sometimes rules have some exceptions! +// in tests it's just simpler to re-use one instance of the factory +var testHourFactory = hour.MustNewFactory(hour.FactoryConfig{ + // 500 weeks gives us enough entropy to avoid duplicated dates + // (even if duplicate dates should be not a problem) + MaxWeeksInTheFutureToSet: 500, + MinUtcHour: 0, + MaxUtcHour: 24, +}) + +func newFirebaseRepository(t *testing.T, ctx context.Context) *adapters.FirestoreHourRepository { + firestoreClient, err := firestore.NewClient(ctx, os.Getenv("GCP_PROJECT")) + require.NoError(t, err) + + return adapters.NewFirestoreHourRepository(firestoreClient, testHourFactory) +} + +func newMySQLRepository(t *testing.T) *adapters.MySQLHourRepository { + db, err := adapters.NewMySQLConnection() + require.NoError(t, err) + + return adapters.NewMySQLHourRepository(db, testHourFactory) +} + +func newValidAvailableHour(t *testing.T) *hour.Hour { + hourTime := newValidHourTime() + + hour, err := testHourFactory.NewAvailableHour(hourTime) + require.NoError(t, err) + + return hour +} + +// usedHours is storing hours used during the test, +// to ensure that within one test run we are not using the same hour +// (it should be not a problem between test runs) +var usedHours = sync.Map{} + +func newValidHourTime() time.Time { + for { + minTime := time.Now().AddDate(0, 0, 1) + + minTimestamp := minTime.Unix() + maxTimestamp := minTime.AddDate(0, 0, testHourFactory.Config().MaxWeeksInTheFutureToSet*7).Unix() + + t := time.Unix(rand.Int63n(maxTimestamp-minTimestamp)+minTimestamp, 0).Truncate(time.Hour).Local() + + _, alreadyUsed := usedHours.LoadOrStore(t.Unix(), true) + if !alreadyUsed { + return t + } + } +} + +func assertHourInRepository(ctx context.Context, t *testing.T, repo hour.Repository, hour *hour.Hour) { + require.NotNil(t, hour) + + hourFromRepo, err := repo.GetHour(ctx, hour.Time()) + require.NoError(t, err) + + assert.Equal(t, hour, hourFromRepo) +} diff --git a/wild-workouts/internal/trainer/app/app.go b/wild-workouts/internal/trainer/app/app.go new file mode 100644 index 0000000..d341720 --- /dev/null +++ b/wild-workouts/internal/trainer/app/app.go @@ -0,0 +1,23 @@ +package app + +import ( + "github.com/ThreeDotsLabs/wild-workouts-go-ddd-example/internal/trainer/app/command" + "github.com/ThreeDotsLabs/wild-workouts-go-ddd-example/internal/trainer/app/query" +) + +type Application struct { + Commands Commands + Queries Queries +} + +type Commands struct { + CancelTraining command.CancelTrainingHandler + ScheduleTraining command.ScheduleTrainingHandler + + MakeHoursAvailable command.MakeHoursAvailableHandler + MakeHoursUnavailable command.MakeHoursUnavailableHandler +} + +type Queries struct { + HourAvailability query.HourAvailabilityHandler +} diff --git a/wild-workouts/internal/trainer/app/command/cancel_training.go b/wild-workouts/internal/trainer/app/command/cancel_training.go new file mode 100644 index 0000000..4aa378b --- /dev/null +++ b/wild-workouts/internal/trainer/app/command/cancel_training.go @@ -0,0 +1,50 @@ +package command + +import ( + "context" + "time" + + "github.com/ThreeDotsLabs/wild-workouts-go-ddd-example/internal/common/decorator" + "github.com/ThreeDotsLabs/wild-workouts-go-ddd-example/internal/common/errors" + "github.com/ThreeDotsLabs/wild-workouts-go-ddd-example/internal/trainer/domain/hour" + "github.com/sirupsen/logrus" +) + +type CancelTraining struct { + Hour time.Time +} + +type CancelTrainingHandler decorator.CommandHandler[CancelTraining] + +type cancelTrainingHandler struct { + hourRepo hour.Repository +} + +func NewCancelTrainingHandler( + hourRepo hour.Repository, + logger *logrus.Entry, + metricsClient decorator.MetricsClient, +) CancelTrainingHandler { + if hourRepo == nil { + panic("nil hourRepo") + } + + return decorator.ApplyCommandDecorators[CancelTraining]( + cancelTrainingHandler{hourRepo: hourRepo}, + logger, + metricsClient, + ) +} + +func (h cancelTrainingHandler) Handle(ctx context.Context, cmd CancelTraining) error { + if err := h.hourRepo.UpdateHour(ctx, cmd.Hour, func(h *hour.Hour) (*hour.Hour, error) { + if err := h.CancelTraining(); err != nil { + return nil, err + } + return h, nil + }); err != nil { + return errors.NewSlugError(err.Error(), "unable-to-update-availability") + } + + return nil +} diff --git a/wild-workouts/internal/trainer/app/command/make_hours_available.go b/wild-workouts/internal/trainer/app/command/make_hours_available.go new file mode 100644 index 0000000..e69fd09 --- /dev/null +++ b/wild-workouts/internal/trainer/app/command/make_hours_available.go @@ -0,0 +1,52 @@ +package command + +import ( + "context" + "time" + + "github.com/ThreeDotsLabs/wild-workouts-go-ddd-example/internal/common/decorator" + "github.com/ThreeDotsLabs/wild-workouts-go-ddd-example/internal/common/errors" + "github.com/ThreeDotsLabs/wild-workouts-go-ddd-example/internal/trainer/domain/hour" + "github.com/sirupsen/logrus" +) + +type MakeHoursAvailable struct { + Hours []time.Time +} + +type MakeHoursAvailableHandler decorator.CommandHandler[MakeHoursAvailable] + +type makeHoursAvailableHandler struct { + hourRepo hour.Repository +} + +func NewMakeHoursAvailableHandler( + hourRepo hour.Repository, + logger *logrus.Entry, + metricsClient decorator.MetricsClient, +) MakeHoursAvailableHandler { + if hourRepo == nil { + panic("hourRepo is nil") + } + + return decorator.ApplyCommandDecorators[MakeHoursAvailable]( + makeHoursAvailableHandler{hourRepo: hourRepo}, + logger, + metricsClient, + ) +} + +func (c makeHoursAvailableHandler) Handle(ctx context.Context, cmd MakeHoursAvailable) error { + for _, hourToUpdate := range cmd.Hours { + if err := c.hourRepo.UpdateHour(ctx, hourToUpdate, func(h *hour.Hour) (*hour.Hour, error) { + if err := h.MakeAvailable(); err != nil { + return nil, err + } + return h, nil + }); err != nil { + return errors.NewSlugError(err.Error(), "unable-to-update-availability") + } + } + + return nil +} diff --git a/wild-workouts/internal/trainer/app/command/make_hours_unavailable.go b/wild-workouts/internal/trainer/app/command/make_hours_unavailable.go new file mode 100644 index 0000000..f209003 --- /dev/null +++ b/wild-workouts/internal/trainer/app/command/make_hours_unavailable.go @@ -0,0 +1,52 @@ +package command + +import ( + "context" + "time" + + "github.com/ThreeDotsLabs/wild-workouts-go-ddd-example/internal/common/decorator" + "github.com/ThreeDotsLabs/wild-workouts-go-ddd-example/internal/common/errors" + "github.com/ThreeDotsLabs/wild-workouts-go-ddd-example/internal/trainer/domain/hour" + "github.com/sirupsen/logrus" +) + +type MakeHoursUnavailable struct { + Hours []time.Time +} + +type MakeHoursUnavailableHandler decorator.CommandHandler[MakeHoursUnavailable] + +type makeHoursUnavailableHandler struct { + hourRepo hour.Repository +} + +func NewMakeHoursUnavailableHandler( + hourRepo hour.Repository, + logger *logrus.Entry, + metricsClient decorator.MetricsClient, +) MakeHoursUnavailableHandler { + if hourRepo == nil { + panic("hourRepo is nil") + } + + return decorator.ApplyCommandDecorators[MakeHoursUnavailable]( + makeHoursUnavailableHandler{hourRepo: hourRepo}, + logger, + metricsClient, + ) +} + +func (c makeHoursUnavailableHandler) Handle(ctx context.Context, cmd MakeHoursUnavailable) error { + for _, hourToUpdate := range cmd.Hours { + if err := c.hourRepo.UpdateHour(ctx, hourToUpdate, func(h *hour.Hour) (*hour.Hour, error) { + if err := h.MakeNotAvailable(); err != nil { + return nil, err + } + return h, nil + }); err != nil { + return errors.NewSlugError(err.Error(), "unable-to-update-availability") + } + } + + return nil +} diff --git a/wild-workouts/internal/trainer/app/command/schedule_training.go b/wild-workouts/internal/trainer/app/command/schedule_training.go new file mode 100644 index 0000000..70869f4 --- /dev/null +++ b/wild-workouts/internal/trainer/app/command/schedule_training.go @@ -0,0 +1,50 @@ +package command + +import ( + "context" + "time" + + "github.com/ThreeDotsLabs/wild-workouts-go-ddd-example/internal/common/decorator" + "github.com/ThreeDotsLabs/wild-workouts-go-ddd-example/internal/common/errors" + "github.com/ThreeDotsLabs/wild-workouts-go-ddd-example/internal/trainer/domain/hour" + "github.com/sirupsen/logrus" +) + +type ScheduleTraining struct { + Hour time.Time +} + +type ScheduleTrainingHandler decorator.CommandHandler[ScheduleTraining] + +type scheduleTrainingHandler struct { + hourRepo hour.Repository +} + +func NewScheduleTrainingHandler( + hourRepo hour.Repository, + logger *logrus.Entry, + metricsClient decorator.MetricsClient, +) ScheduleTrainingHandler { + if hourRepo == nil { + panic("nil hourRepo") + } + + return decorator.ApplyCommandDecorators[ScheduleTraining]( + scheduleTrainingHandler{hourRepo: hourRepo}, + logger, + metricsClient, + ) +} + +func (h scheduleTrainingHandler) Handle(ctx context.Context, cmd ScheduleTraining) error { + if err := h.hourRepo.UpdateHour(ctx, cmd.Hour, func(h *hour.Hour) (*hour.Hour, error) { + if err := h.ScheduleTraining(); err != nil { + return nil, err + } + return h, nil + }); err != nil { + return errors.NewSlugError(err.Error(), "unable-to-update-availability") + } + + return nil +} diff --git a/wild-workouts/internal/trainer/app/query/hour_availability.go b/wild-workouts/internal/trainer/app/query/hour_availability.go new file mode 100644 index 0000000..aa43327 --- /dev/null +++ b/wild-workouts/internal/trainer/app/query/hour_availability.go @@ -0,0 +1,60 @@ +package query + +import ( + "context" + "time" + + "github.com/sirupsen/logrus" + + "github.com/ThreeDotsLabs/wild-workouts-go-ddd-example/internal/common/decorator" + "github.com/ThreeDotsLabs/wild-workouts-go-ddd-example/internal/trainer/domain/hour" +) + +type ( + Date struct { + Date time.Time + HasFreeHours bool + Hours []Hour + } + + Hour struct { + Available bool + HasTrainingScheduled bool + Hour time.Time + } + + HourAvailability struct { + Hour time.Time + } +) + +type HourAvailabilityHandler decorator.QueryHandler[HourAvailability, bool] + +type hourAvailabilityHandler struct { + hourRepo hour.Repository +} + +func NewHourAvailabilityHandler( + hourRepo hour.Repository, + logger *logrus.Entry, + metricsClient decorator.MetricsClient, +) HourAvailabilityHandler { + if hourRepo == nil { + panic("nil hourRepo") + } + + return decorator.ApplyQueryDecorators[HourAvailability, bool]( + hourAvailabilityHandler{hourRepo: hourRepo}, + logger, + metricsClient, + ) +} + +func (h hourAvailabilityHandler) Handle(ctx context.Context, query HourAvailability) (bool, error) { + hour, err := h.hourRepo.GetHour(ctx, query.Hour) + if err != nil { + return false, err + } + + return hour.IsAvailable(), nil +} diff --git a/wild-workouts/internal/trainer/app/query/types.go b/wild-workouts/internal/trainer/app/query/types.go new file mode 100644 index 0000000..4c60abc --- /dev/null +++ b/wild-workouts/internal/trainer/app/query/types.go @@ -0,0 +1,2 @@ +package query + diff --git a/wild-workouts/internal/trainer/domain/hour/availability.go b/wild-workouts/internal/trainer/domain/hour/availability.go new file mode 100644 index 0000000..70112ef --- /dev/null +++ b/wild-workouts/internal/trainer/domain/hour/availability.go @@ -0,0 +1,97 @@ +package hour + +import "github.com/pkg/errors" + +var ( + Available = Availability{"available"} + NotAvailable = Availability{"not_available"} + TrainingScheduled = Availability{"training_scheduled"} +) + +var availabilityValues = []Availability{ + Available, + NotAvailable, + TrainingScheduled, +} + +// Availability is enum. +// +// Using struct instead of `type Availability string` for enums allows us to ensure, +// that we have full control of what values are possible. +// With `type Availability string` you are able to create `Availability("i_can_put_anything_here")` +type Availability struct { + a string +} + +func NewAvailabilityFromString(availabilityStr string) (Availability, error) { + for _, availability := range availabilityValues { + if availability.String() == availabilityStr { + return availability, nil + } + } + return Availability{}, errors.Errorf("unknown '%s' availability", availabilityStr) +} + +// Every type in Go have zero value. In that case it's `Availability{}`. +// It's always a good idea to check if provided value is not zero! +func (h Availability) IsZero() bool { + return h == Availability{} +} + +func (h Availability) String() string { + return h.a +} + +var ( + ErrTrainingScheduled = errors.New("unable to modify hour, because scheduled training") + ErrNoTrainingScheduled = errors.New("training is not scheduled") + ErrHourNotAvailable = errors.New("hour is not available") +) + +func (h Hour) Availability() Availability { + return h.availability +} + +func (h Hour) IsAvailable() bool { + return h.availability == Available +} + +func (h Hour) HasTrainingScheduled() bool { + return h.availability == TrainingScheduled +} + +func (h *Hour) MakeNotAvailable() error { + if h.HasTrainingScheduled() { + return ErrTrainingScheduled + } + + h.availability = NotAvailable + return nil +} + +func (h *Hour) MakeAvailable() error { + if h.HasTrainingScheduled() { + return ErrTrainingScheduled + } + + h.availability = Available + return nil +} + +func (h *Hour) ScheduleTraining() error { + if !h.IsAvailable() { + return ErrHourNotAvailable + } + + h.availability = TrainingScheduled + return nil +} + +func (h *Hour) CancelTraining() error { + if !h.HasTrainingScheduled() { + return ErrNoTrainingScheduled + } + + h.availability = Available + return nil +} diff --git a/wild-workouts/internal/trainer/domain/hour/availability_test.go b/wild-workouts/internal/trainer/domain/hour/availability_test.go new file mode 100644 index 0000000..1a35b83 --- /dev/null +++ b/wild-workouts/internal/trainer/domain/hour/availability_test.go @@ -0,0 +1,125 @@ +package hour_test + +import ( + "testing" + + "github.com/ThreeDotsLabs/wild-workouts-go-ddd-example/internal/trainer/domain/hour" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestHour_MakeNotAvailable(t *testing.T) { + t.Parallel() + h, err := testHourFactory.NewAvailableHour(validTrainingHour()) + require.NoError(t, err) + + require.NoError(t, h.MakeNotAvailable()) + assert.False(t, h.IsAvailable()) +} + +func TestHour_MakeNotAvailable_with_scheduled_training(t *testing.T) { + t.Parallel() + h := newHourWithScheduledTraining(t) + + assert.Equal(t, hour.ErrTrainingScheduled, h.MakeNotAvailable()) +} + +func TestHour_MakeAvailable(t *testing.T) { + t.Parallel() + h, err := testHourFactory.NewAvailableHour(validTrainingHour()) + require.NoError(t, err) + + require.NoError(t, h.MakeNotAvailable()) + + require.NoError(t, h.MakeAvailable()) + assert.True(t, h.IsAvailable()) +} + +func TestHour_MakeAvailable_with_scheduled_training(t *testing.T) { + t.Parallel() + h := newHourWithScheduledTraining(t) + + assert.Equal(t, hour.ErrTrainingScheduled, h.MakeAvailable()) +} + +func TestHour_ScheduleTraining(t *testing.T) { + t.Parallel() + h, err := testHourFactory.NewAvailableHour(validTrainingHour()) + require.NoError(t, err) + + require.NoError(t, h.ScheduleTraining()) + + assert.True(t, h.HasTrainingScheduled()) + assert.False(t, h.IsAvailable()) +} + +func TestHour_ScheduleTraining_with_not_available(t *testing.T) { + t.Parallel() + h := newNotAvailableHour(t) + assert.Equal(t, hour.ErrHourNotAvailable, h.ScheduleTraining()) +} + +func TestHour_CancelTraining(t *testing.T) { + t.Parallel() + h := newHourWithScheduledTraining(t) + + require.NoError(t, h.CancelTraining()) + + assert.False(t, h.HasTrainingScheduled()) + assert.True(t, h.IsAvailable()) +} + +func TestHour_CancelTraining_no_training_scheduled(t *testing.T) { + t.Parallel() + h, err := testHourFactory.NewAvailableHour(validTrainingHour()) + require.NoError(t, err) + + assert.Equal(t, hour.ErrNoTrainingScheduled, h.CancelTraining()) +} + +func TestNewAvailabilityFromString(t *testing.T) { + t.Parallel() + testCases := []hour.Availability{ + hour.Available, + hour.NotAvailable, + hour.TrainingScheduled, + } + + for _, tc := range testCases { + expectedAvailability := tc + t.Run(expectedAvailability.String(), func(t *testing.T) { + t.Parallel() + availability, err := hour.NewAvailabilityFromString(expectedAvailability.String()) + require.NoError(t, err) + + assert.Equal(t, expectedAvailability, availability) + }) + } +} + +func TestNewAvailabilityFromString_invalid(t *testing.T) { + t.Parallel() + _, err := hour.NewAvailabilityFromString("invalid_value") + assert.Error(t, err) + + _, err = hour.NewAvailabilityFromString("") + assert.Error(t, err) +} + +func newHourWithScheduledTraining(t *testing.T) *hour.Hour { + h, err := testHourFactory.NewAvailableHour(validTrainingHour()) + require.NoError(t, err) + + require.NoError(t, h.ScheduleTraining()) + + return h +} + +func newNotAvailableHour(t *testing.T) *hour.Hour { + h, err := testHourFactory.NewAvailableHour(validTrainingHour()) + require.NoError(t, err) + + require.NoError(t, h.MakeNotAvailable()) + + return h +} diff --git a/wild-workouts/internal/trainer/domain/hour/hour.go b/wild-workouts/internal/trainer/domain/hour/hour.go new file mode 100644 index 0000000..04f1373 --- /dev/null +++ b/wild-workouts/internal/trainer/domain/hour/hour.go @@ -0,0 +1,221 @@ +package hour + +import ( + "fmt" + "time" + + "github.com/pkg/errors" + "go.uber.org/multierr" +) + +type Hour struct { + hour time.Time + + availability Availability +} + +type FactoryConfig struct { + MaxWeeksInTheFutureToSet int + MinUtcHour int + MaxUtcHour int +} + +func (f FactoryConfig) Validate() error { + var err error + + if f.MaxWeeksInTheFutureToSet < 1 { + err = multierr.Append( + err, + errors.Errorf( + "MaxWeeksInTheFutureToSet should be greater than 1, but is %d", + f.MaxWeeksInTheFutureToSet, + ), + ) + } + if f.MinUtcHour < 0 || f.MinUtcHour > 24 { + err = multierr.Append( + err, + errors.Errorf( + "MinUtcHour should be value between 0 and 24, but is %d", + f.MinUtcHour, + ), + ) + } + if f.MaxUtcHour < 0 || f.MaxUtcHour > 24 { + err = multierr.Append( + err, + errors.Errorf( + "MinUtcHour should be value between 0 and 24, but is %d", + f.MaxUtcHour, + ), + ) + } + + if f.MinUtcHour > f.MaxUtcHour { + err = multierr.Append( + err, + errors.Errorf( + "MinUtcHour (%d) can't be after MaxUtcHour (%d)", + f.MinUtcHour, f.MaxUtcHour, + ), + ) + } + + return err +} + +type Factory struct { + // it's better to keep FactoryConfig as a private attribute, + // thanks to that we are always sure that our configuration is not changed in the not allowed way + fc FactoryConfig +} + +func NewFactory(fc FactoryConfig) (Factory, error) { + if err := fc.Validate(); err != nil { + return Factory{}, errors.Wrap(err, "invalid config passed to factory") + } + + return Factory{fc: fc}, nil +} + +func MustNewFactory(fc FactoryConfig) Factory { + f, err := NewFactory(fc) + if err != nil { + panic(err) + } + + return f +} + +func (f Factory) Config() FactoryConfig { + return f.fc +} + +func (f Factory) IsZero() bool { + return f == Factory{} +} + +func (f Factory) NewAvailableHour(hour time.Time) (*Hour, error) { + if err := f.validateTime(hour); err != nil { + return nil, err + } + + return &Hour{ + hour: hour, + availability: Available, + }, nil +} + +func (f Factory) NewNotAvailableHour(hour time.Time) (*Hour, error) { + if err := f.validateTime(hour); err != nil { + return nil, err + } + + return &Hour{ + hour: hour, + availability: NotAvailable, + }, nil +} + +// UnmarshalHourFromDatabase unmarshals Hour from the database. +// +// It should be used only for unmarshalling from the database! +// You can't use UnmarshalHourFromDatabase as constructor - It may put domain into the invalid state! +func (f Factory) UnmarshalHourFromDatabase(hour time.Time, availability Availability) (*Hour, error) { + if err := f.validateTime(hour); err != nil { + return nil, err + } + + if availability.IsZero() { + return nil, errors.New("empty availability") + } + + return &Hour{ + hour: hour, + availability: availability, + }, nil +} + +var ( + ErrNotFullHour = errors.New("hour should be a full hour") + ErrPastHour = errors.New("cannot create hour from past") +) + +// If you have the error with a more complex context, +// it's a good idea to define it as a separate type. +// There is nothing worst, than error "invalid date" without knowing what date was passed and what is the valid value! +type TooDistantDateError struct { + MaxWeeksInTheFutureToSet int + ProvidedDate time.Time +} + +func (e TooDistantDateError) Error() string { + return fmt.Sprintf( + "schedule can be only set for next %d weeks, provided date: %s", + e.MaxWeeksInTheFutureToSet, + e.ProvidedDate, + ) +} + +type TooEarlyHourError struct { + MinUtcHour int + ProvidedTime time.Time +} + +func (e TooEarlyHourError) Error() string { + return fmt.Sprintf( + "too early hour, min UTC hour: %d, provided time: %s", + e.MinUtcHour, + e.ProvidedTime, + ) +} + +type TooLateHourError struct { + MaxUtcHour int + ProvidedTime time.Time +} + +func (e TooLateHourError) Error() string { + return fmt.Sprintf( + "too late hour, min UTC hour: %d, provided time: %s", + e.MaxUtcHour, + e.ProvidedTime, + ) +} + +func (f Factory) validateTime(hour time.Time) error { + if !hour.Round(time.Hour).Equal(hour) { + return ErrNotFullHour + } + + // AddDate is better than Add for adding days, because not every day have 24h! + if hour.After(time.Now().AddDate(0, 0, f.fc.MaxWeeksInTheFutureToSet*7)) { + return TooDistantDateError{ + MaxWeeksInTheFutureToSet: f.fc.MaxWeeksInTheFutureToSet, + ProvidedDate: hour, + } + } + + currentHour := time.Now().Truncate(time.Hour) + if hour.Before(currentHour) || hour.Equal(currentHour) { + return ErrPastHour + } + if hour.UTC().Hour() > f.fc.MaxUtcHour { + return TooLateHourError{ + MaxUtcHour: f.fc.MaxUtcHour, + ProvidedTime: hour, + } + } + if hour.UTC().Hour() < f.fc.MinUtcHour { + return TooEarlyHourError{ + MinUtcHour: f.fc.MinUtcHour, + ProvidedTime: hour, + } + } + + return nil +} + +func (h *Hour) Time() time.Time { + return h.hour +} diff --git a/wild-workouts/internal/trainer/domain/hour/hour_test.go b/wild-workouts/internal/trainer/domain/hour/hour_test.go new file mode 100644 index 0000000..f82cafd --- /dev/null +++ b/wild-workouts/internal/trainer/domain/hour/hour_test.go @@ -0,0 +1,248 @@ +package hour_test + +import ( + "testing" + "time" + + "github.com/ThreeDotsLabs/wild-workouts-go-ddd-example/internal/trainer/domain/hour" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +var testHourFactory = hour.MustNewFactory(hour.FactoryConfig{ + MaxWeeksInTheFutureToSet: 100, + MinUtcHour: 0, + MaxUtcHour: 24, +}) + +func TestNewAvailableHour(t *testing.T) { + t.Parallel() + h, err := testHourFactory.NewAvailableHour(validTrainingHour()) + require.NoError(t, err) + + assert.True(t, h.IsAvailable()) +} + +func TestNewAvailableHour_not_full_hour(t *testing.T) { + t.Parallel() + constructorTime := trainingHourWithMinutes(13) + + _, err := testHourFactory.NewAvailableHour(constructorTime) + assert.Equal(t, hour.ErrNotFullHour, err) +} + +func TestNewAvailableHour_too_distant_date(t *testing.T) { + t.Parallel() + maxWeeksInFuture := 1 + + factory := hour.MustNewFactory(hour.FactoryConfig{ + MaxWeeksInTheFutureToSet: maxWeeksInFuture, + MinUtcHour: 0, + MaxUtcHour: 0, + }) + + constructorTime := time.Now().Truncate(time.Hour*24).AddDate(0, 0, maxWeeksInFuture*7+1) + + _, err := factory.NewAvailableHour(constructorTime) + assert.Equal( + t, + hour.TooDistantDateError{ + MaxWeeksInTheFutureToSet: maxWeeksInFuture, + ProvidedDate: constructorTime, + }, + err, + ) +} + +func TestNewAvailableHour_past_date(t *testing.T) { + t.Parallel() + pastHour := time.Now().Truncate(time.Hour).Add(-time.Hour) + _, err := testHourFactory.NewAvailableHour(pastHour) + assert.Equal(t, hour.ErrPastHour, err) + + currentHour := time.Now().Truncate(time.Hour) + _, err = testHourFactory.NewAvailableHour(currentHour) + assert.Equal(t, hour.ErrPastHour, err) +} + +func TestNewAvailableHour_too_early_hour(t *testing.T) { + t.Parallel() + factory := hour.MustNewFactory(hour.FactoryConfig{ + MaxWeeksInTheFutureToSet: 10, + MinUtcHour: 12, + MaxUtcHour: 18, + }) + + // we are using next day, to be sure that provided hour is not in the past + currentTime := time.Now().AddDate(0, 0, 1) + + tooEarlyHour := time.Date( + currentTime.Year(), currentTime.Month(), currentTime.Day(), + factory.Config().MinUtcHour-1, 0, 0, 0, + time.UTC, + ) + + _, err := factory.NewAvailableHour(tooEarlyHour) + assert.Equal( + t, + hour.TooEarlyHourError{ + MinUtcHour: factory.Config().MinUtcHour, + ProvidedTime: tooEarlyHour, + }, + err, + ) +} + +func TestNewAvailableHour_too_late_hour(t *testing.T) { + t.Parallel() + factory := hour.MustNewFactory(hour.FactoryConfig{ + MaxWeeksInTheFutureToSet: 10, + MinUtcHour: 12, + MaxUtcHour: 18, + }) + + // we are using next day, to be sure that provided hour is not in the past + currentTime := time.Now().AddDate(0, 0, 1) + + tooEarlyHour := time.Date( + currentTime.Year(), currentTime.Month(), currentTime.Day(), + factory.Config().MaxUtcHour+1, 0, 0, 0, + time.UTC, + ) + + _, err := factory.NewAvailableHour(tooEarlyHour) + assert.Equal( + t, + hour.TooLateHourError{ + MaxUtcHour: factory.Config().MaxUtcHour, + ProvidedTime: tooEarlyHour, + }, + err, + ) +} + +func TestHour_Time(t *testing.T) { + t.Parallel() + expectedTime := validTrainingHour() + + h, err := testHourFactory.NewAvailableHour(expectedTime) + require.NoError(t, err) + + assert.Equal(t, expectedTime, h.Time()) +} + +func TestUnmarshalHourFromDatabase(t *testing.T) { + t.Parallel() + trainingTime := validTrainingHour() + + h, err := testHourFactory.UnmarshalHourFromDatabase(trainingTime, hour.TrainingScheduled) + require.NoError(t, err) + + assert.Equal(t, trainingTime, h.Time()) + assert.True(t, h.HasTrainingScheduled()) +} + +func TestFactoryConfig_Validate(t *testing.T) { + t.Parallel() + testCases := []struct { + Name string + Config hour.FactoryConfig + ExpectedErr string + }{ + { + Name: "valid", + Config: hour.FactoryConfig{ + MaxWeeksInTheFutureToSet: 10, + MinUtcHour: 10, + MaxUtcHour: 12, + }, + ExpectedErr: "", + }, + { + Name: "equal_min_and_max_hour", + Config: hour.FactoryConfig{ + MaxWeeksInTheFutureToSet: 10, + MinUtcHour: 12, + MaxUtcHour: 12, + }, + ExpectedErr: "", + }, + { + Name: "min_hour_after_max_hour", + Config: hour.FactoryConfig{ + MaxWeeksInTheFutureToSet: 10, + MinUtcHour: 13, + MaxUtcHour: 12, + }, + ExpectedErr: "MinUtcHour (13) can't be after MaxUtcHour (12)", + }, + { + Name: "zero_max_weeks", + Config: hour.FactoryConfig{ + MaxWeeksInTheFutureToSet: 0, + MinUtcHour: 10, + MaxUtcHour: 12, + }, + ExpectedErr: "MaxWeeksInTheFutureToSet should be greater than 1, but is 0", + }, + { + Name: "sub_zero_min_hour", + Config: hour.FactoryConfig{ + MaxWeeksInTheFutureToSet: 10, + MinUtcHour: -1, + MaxUtcHour: 12, + }, + ExpectedErr: "MinUtcHour should be value between 0 and 24, but is -1", + }, + { + Name: "sub_zero_max_hour", + Config: hour.FactoryConfig{ + MaxWeeksInTheFutureToSet: 10, + MinUtcHour: 10, + MaxUtcHour: -1, + }, + ExpectedErr: "MinUtcHour should be value between 0 and 24, but is -1; MinUtcHour (10) can't be after MaxUtcHour (-1)", + }, + } + + for _, c := range testCases { + c := c + t.Run(c.Name, func(t *testing.T) { + t.Parallel() + err := c.Config.Validate() + + if c.ExpectedErr != "" { + assert.EqualError(t, err, c.ExpectedErr) + } else { + assert.NoError(t, err) + } + }) + } +} + +func TestNewFactory_invalid_config(t *testing.T) { + t.Parallel() + f, err := hour.NewFactory(hour.FactoryConfig{}) + assert.Error(t, err) + assert.Zero(t, f) +} + +func validTrainingHour() time.Time { + tomorrow := time.Now().Add(time.Hour * 24) + + return time.Date( + tomorrow.Year(), tomorrow.Month(), tomorrow.Day(), + testHourFactory.Config().MinUtcHour, 0, 0, 0, + time.UTC, + ) +} + +func trainingHourWithMinutes(minute int) time.Time { + tomorrow := time.Now().Add(time.Hour * 24) + + return time.Date( + tomorrow.Year(), tomorrow.Month(), tomorrow.Day(), + testHourFactory.Config().MaxUtcHour, minute, 0, 0, + time.UTC, + ) +} diff --git a/wild-workouts/internal/trainer/domain/hour/repository.go b/wild-workouts/internal/trainer/domain/hour/repository.go new file mode 100644 index 0000000..ebef424 --- /dev/null +++ b/wild-workouts/internal/trainer/domain/hour/repository.go @@ -0,0 +1,15 @@ +package hour + +import ( + "context" + "time" +) + +type Repository interface { + GetHour(ctx context.Context, hourTime time.Time) (*Hour, error) + UpdateHour( + ctx context.Context, + hourTime time.Time, + updateFn func(h *Hour) (*Hour, error), + ) error +} diff --git a/wild-workouts/internal/trainer/fixtures.go b/wild-workouts/internal/trainer/fixtures.go new file mode 100644 index 0000000..5d2bc4f --- /dev/null +++ b/wild-workouts/internal/trainer/fixtures.go @@ -0,0 +1,99 @@ +package main + +import ( + "context" + "math/rand" + "time" + + "github.com/ThreeDotsLabs/wild-workouts-go-ddd-example/internal/common/client" + "github.com/ThreeDotsLabs/wild-workouts-go-ddd-example/internal/trainer/app" + "github.com/ThreeDotsLabs/wild-workouts-go-ddd-example/internal/trainer/app/command" + "github.com/ThreeDotsLabs/wild-workouts-go-ddd-example/internal/trainer/app/query" + "github.com/pkg/errors" + "github.com/sirupsen/logrus" +) + +const daysToSet = 30 + +func loadFixtures(app app.Application) { + start := time.Now() + ctx := context.Background() + + logrus.Debug("Waiting for trainer service") + working := client.WaitForTrainerService(time.Second * 30) + if !working { + logrus.Error("Trainer gRPC service is not up") + return + } + + logrus.WithField("after", time.Since(start)).Debug("Trainer service is available") + + if !canLoadFixtures(app, ctx) { + logrus.Debug("Trainer fixtures are already loaded") + return + } + + for { + err := loadTrainerFixtures(ctx, app) + if err == nil { + break + } + + logrus.WithError(err).Error("Cannot load trainer fixtures") + time.Sleep(10 * time.Second) + } + + logrus.WithField("after", time.Since(start)).Debug("Trainer fixtures loaded") +} + +func loadTrainerFixtures(ctx context.Context, application app.Application) error { + maxDate := time.Now().AddDate(0, 0, daysToSet) + localRand := rand.New(rand.NewSource(3)) + + for date := time.Now(); date.Before(maxDate); date = date.AddDate(0, 0, 1) { + for hour := 12; hour <= 20; hour++ { + trainingTime := time.Date(date.Year(), date.Month(), date.Day(), hour, 0, 0, 0, time.UTC) + + if trainingTime.Add(time.Hour).Before(time.Now()) { + // this hour is already "in progress" + continue + } + + if localRand.NormFloat64() > 0 { + err := application.Commands.MakeHoursAvailable.Handle( + ctx, + command.MakeHoursAvailable{Hours: []time.Time{trainingTime}}, + ) + if err != nil { + return errors.Wrap(err, "unable to update hour") + } + } + } + } + + return nil +} + +func canLoadFixtures(app app.Application, ctx context.Context) bool { + for { + dates, err := app.Queries.TrainerAvailableHours.Handle(ctx, query.AvailableHours{ + From: time.Now(), + To: time.Now().AddDate(0, 0, daysToSet), + }) + if err == nil { + for _, date := range dates { + for _, hour := range date.Hours { + if hour.Available { + // we don't need fixtures if any hour is already available for training + return false + } + } + } + + return true + } + + logrus.WithError(err).Error("Cannot check if fixtures can be loaded") + time.Sleep(10 * time.Second) + } +} diff --git a/wild-workouts/internal/trainer/go.mod b/wild-workouts/internal/trainer/go.mod new file mode 100644 index 0000000..e3f15f7 --- /dev/null +++ b/wild-workouts/internal/trainer/go.mod @@ -0,0 +1,62 @@ +module github.com/ThreeDotsLabs/wild-workouts-go-ddd-example/internal/trainer + +go 1.18 + +require ( + cloud.google.com/go/firestore v1.5.0 + github.com/ThreeDotsLabs/wild-workouts-go-ddd-example/internal/common v0.0.0-00010101000000-000000000000 + github.com/deepmap/oapi-codegen v1.9.0 + github.com/go-chi/chi/v5 v5.0.5 + github.com/go-chi/render v1.0.1 + github.com/go-sql-driver/mysql v1.4.0 + github.com/golang/protobuf v1.5.2 + github.com/google/uuid v1.1.2 + github.com/jmoiron/sqlx v1.2.0 + github.com/pkg/errors v0.9.1 + github.com/sirupsen/logrus v1.5.0 + github.com/stretchr/testify v1.7.0 + go.uber.org/multierr v1.1.0 + golang.org/x/sys v0.0.0-20211031064116-611d5d643895 // indirect + google.golang.org/api v0.40.0 + google.golang.org/grpc v1.40.0 +) + +require ( + cloud.google.com/go v0.75.0 // indirect + cloud.google.com/go/storage v1.10.0 // indirect + firebase.google.com/go/v4 v4.7.1 // indirect + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/dgrijalva/jwt-go v3.2.0+incompatible // indirect + github.com/go-chi/cors v1.0.1 // indirect + github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e // indirect + github.com/google/go-cmp v0.5.5 // indirect + github.com/googleapis/gax-go/v2 v2.0.5 // indirect + github.com/grpc-ecosystem/go-grpc-middleware v1.2.0 // indirect + github.com/hpcloud/tail v1.0.0 // indirect + github.com/jstemmer/go-junit-report v0.9.1 // indirect + github.com/konsorten/go-windows-terminal-sequences v1.0.1 // indirect + github.com/mattn/go-colorable v0.1.8 // indirect + github.com/mattn/go-isatty v0.0.14 // indirect + github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect + github.com/x-cray/logrus-prefixed-formatter v0.5.2 // indirect + go.opencensus.io v0.22.5 // indirect + go.uber.org/atomic v1.4.0 // indirect + golang.org/x/crypto v0.0.0-20210921155107-089bfa567519 // indirect + golang.org/x/lint v0.0.0-20201208152925-83fdc39ff7b5 // indirect + golang.org/x/mod v0.4.1 // indirect + golang.org/x/net v0.0.0-20210226172049-e18ecbb05110 // indirect + golang.org/x/oauth2 v0.0.0-20210218202405-ba52d332ba99 // indirect + golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1 // indirect + golang.org/x/text v0.3.7 // indirect + golang.org/x/tools v0.1.0 // indirect + golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 // indirect + google.golang.org/appengine v1.6.7 // indirect + google.golang.org/genproto v0.0.0-20210222152913-aa3ee6e6a81c // indirect + google.golang.org/protobuf v1.27.1 // indirect + gopkg.in/fsnotify.v1 v1.4.7 // indirect + gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 // indirect + gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b // indirect +) + +replace github.com/ThreeDotsLabs/wild-workouts-go-ddd-example/internal/common => ../common/ diff --git a/wild-workouts/internal/trainer/go.sum b/wild-workouts/internal/trainer/go.sum new file mode 100644 index 0000000..b2a2099 --- /dev/null +++ b/wild-workouts/internal/trainer/go.sum @@ -0,0 +1,628 @@ +cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU= +cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU= +cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY= +cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc= +cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0= +cloud.google.com/go v0.50.0/go.mod h1:r9sluTvynVuxRIOHXQEHMFffphuXHOMZMycpNR5e6To= +cloud.google.com/go v0.52.0/go.mod h1:pXajvRH/6o3+F9jDHZWQ5PbGhn+o8w9qiu/CffaVdO4= +cloud.google.com/go v0.53.0/go.mod h1:fp/UouUEsRkN6ryDKNW/Upv/JBKnv6WDthjR6+vze6M= +cloud.google.com/go v0.54.0/go.mod h1:1rq2OEkV3YMf6n/9ZvGWI3GWw0VoqH/1x2nd8Is/bPc= +cloud.google.com/go v0.56.0/go.mod h1:jr7tqZxxKOVYizybht9+26Z/gUq7tiRzu+ACVAMbKVk= +cloud.google.com/go v0.57.0/go.mod h1:oXiQ6Rzq3RAkkY7N6t3TcE6jE+CIBBbA36lwQ1JyzZs= +cloud.google.com/go v0.62.0/go.mod h1:jmCYTdRCQuc1PHIIJ/maLInMho30T/Y0M4hTdTShOYc= +cloud.google.com/go v0.65.0/go.mod h1:O5N8zS7uWy9vkA9vayVHs65eM1ubvY4h553ofrNHObY= +cloud.google.com/go v0.72.0/go.mod h1:M+5Vjvlc2wnp6tjzE102Dw08nGShTscUx2nZMufOKPI= +cloud.google.com/go v0.74.0/go.mod h1:VV1xSbzvo+9QJOxLDaJfTjx5e+MePCpCWwvftOeQmWk= +cloud.google.com/go v0.75.0 h1:XgtDnVJRCPEUG21gjFiRPz4zI1Mjg16R+NYQjfmU4XY= +cloud.google.com/go v0.75.0/go.mod h1:VGuuCn7PG0dwsd5XPVm2Mm3wlh3EL55/79EKB6hlPTY= +cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o= +cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE= +cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc= +cloud.google.com/go/bigquery v1.5.0/go.mod h1:snEHRnqQbz117VIFhE8bmtwIDY80NLUZUMb4Nv6dBIg= +cloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4gLoIoXIAPc= +cloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ= +cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE= +cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk= +cloud.google.com/go/firestore v1.5.0 h1:4qNItsmc4GP6UOZPGemmHY4ZfPofVhcaKXsYw9wm9oA= +cloud.google.com/go/firestore v1.5.0/go.mod h1:c4nNYR1qdq7eaZ+jSc5fonrQN2k3M7sWATcYTiakjEo= +cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I= +cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw= +cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA= +cloud.google.com/go/pubsub v1.3.1/go.mod h1:i+ucay31+CNRpDW4Lu78I4xXG+O1r/MAHgjpRVR+TSU= +cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw= +cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos= +cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk= +cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs= +cloud.google.com/go/storage v1.10.0 h1:STgFzyU5/8miMl0//zKh2aQeTyeaUH3WN9bSUiJ09bA= +cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0= +dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= +firebase.google.com/go/v4 v4.7.1 h1:ZtyJU6zpl7Dv5tvzDmJZZljKT6TRShzoNnOAdKNK9DE= +firebase.google.com/go/v4 v4.7.1/go.mod h1:UgGSTOhEZVbB2L3dQ3z4pThDTiH869i8TDAZKnrHKbU= +github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= +github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= +github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY= +github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= +github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc= +github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= +github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= +github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= +github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= +github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= +github.com/cncf/udpa/go v0.0.0-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= +github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= +github.com/cncf/xds/go v0.0.0-20210312221358-fbca930ec8ed/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= +github.com/cyberdelia/templates v0.0.0-20141128023046-ca7fffd4298c/go.mod h1:GyV+0YP4qX0UQ7r2MoYZ+AvYDp12OF5yg4q8rGnyNh4= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/decred/dcrd/crypto/blake256 v1.0.0/go.mod h1:sQl2p6Y26YV+ZOcSTP6thNdn47hh8kt6rqSlvmrXFAc= +github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.0-20210816181553-5444fa50b93d/go.mod h1:tmAIfUFEirG/Y8jhZ9M+h36obRZAk/1fcSpXwAVlfqE= +github.com/deepmap/oapi-codegen v1.9.0 h1:qpyRY+dzjMai5QejjA53ebnBtcSvIcZOtYwVlsgdxOc= +github.com/deepmap/oapi-codegen v1.9.0/go.mod h1:7t4DbSxmAffcTEgrWvsPYEE2aOARZ8ZKWp3hDuZkHNc= +github.com/dgrijalva/jwt-go v3.2.0+incompatible h1:7qlOGliEKZXTDg6OTjfoBKDXWrumCAMpl/TFQ4/5kLM= +github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= +github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= +github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= +github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= +github.com/envoyproxy/go-control-plane v0.9.7/go.mod h1:cwu0lG7PUMfa9snN8LXBig5ynNVH9qI8YYLbd1fK2po= +github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= +github.com/envoyproxy/go-control-plane v0.9.9-0.20210512163311-63b5d3c536b0/go.mod h1:hliV/p42l8fGbc6Y9bQ70uLwIvmJyVE5k4iMKlh8wCQ= +github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= +github.com/getkin/kin-openapi v0.80.0/go.mod h1:660oXbgy5JFMKreazJaQTw7o+X00qeSyhcnluiMv+Xg= +github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= +github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI= +github.com/gin-gonic/gin v1.7.4/go.mod h1:jD2toBW3GZUr5UMcdrwQA10I7RuaFOl/SGeDjXkfUtY= +github.com/go-chi/chi/v5 v5.0.0/go.mod h1:BBug9lr0cqtdAhsu6R4AAdvufI0/XBzAQSsUqJpoZOs= +github.com/go-chi/chi/v5 v5.0.5 h1:l3RJ8T8TAqLsXFfah+RA6N4pydMbPwSdvNM+AFWvLUM= +github.com/go-chi/chi/v5 v5.0.5/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8= +github.com/go-chi/cors v1.0.1 h1:56TT/uWGoLWZpnMI/AwAmCneikXr5eLsiIq27wrKecw= +github.com/go-chi/cors v1.0.1/go.mod h1:K2Yje0VW/SJzxiyMYu6iPQYa7hMjQX2i/F491VChg1I= +github.com/go-chi/render v1.0.1 h1:4/5tis2cKaNdnv9zFLfXzcquC9HbeZgCnxGnKrltBS8= +github.com/go-chi/render v1.0.1/go.mod h1:pq4Rr7HbnsdaeHagklXub+p6Wd16Af5l9koip1OvJns= +github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= +github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= +github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= +github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= +github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= +github.com/go-openapi/jsonpointer v0.19.5/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg= +github.com/go-openapi/swag v0.19.5/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk= +github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= +github.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8= +github.com/go-playground/locales v0.14.0/go.mod h1:sawfccIbzZTqEDETgFXqTho0QybSa7l++s0DH+LDiLs= +github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+Scu5vgOQjsIJAF8j9muTVoKLVtA= +github.com/go-playground/universal-translator v0.18.0/go.mod h1:UvRDBj+xPUEGrFYl+lu/H90nyDXpg0fqeB/AQUGNTVA= +github.com/go-playground/validator/v10 v10.4.1/go.mod h1:nlOn6nFhuKACm19sB/8EGNn9GlaMV7XkbRSipzJ0Ii4= +github.com/go-playground/validator/v10 v10.9.0/go.mod h1:74x4gJWsvQexRdW8Pn3dXSGrTK4nAUsbPlLADvpJkos= +github.com/go-sql-driver/mysql v1.4.0 h1:7LxgVwFb2hIQtMm87NdgAVfXjnt4OePseqT1tKx+opk= +github.com/go-sql-driver/mysql v1.4.0/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w= +github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= +github.com/goccy/go-json v0.7.8/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= +github.com/gogo/protobuf v1.2.1 h1:/s5zKNz0uPFCZ5hddgPdo2TK2TVrUNMn0OOX8/aZMTE= +github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4= +github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= +github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e h1:1r7pUrabqp18hOBcwBwiTsbnFeTZHV9eER/QT5JVZxY= +github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y= +github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= +github.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= +github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= +github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4= +github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= +github.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= +github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk= +github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= +github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= +github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= +github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= +github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= +github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= +github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= +github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= +github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= +github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw= +github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= +github.com/golangci/lint-1 v0.0.0-20181222135242-d2cdd8c08219/go.mod h1:/X8TswGSh1pIozq4ZwCfxS0WA5JGXguxk94ar/4c87Y= +github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= +github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= +github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= +github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.4.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU= +github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/martian v2.1.0+incompatible h1:/CP5g8u/VJHijgedC/Legn3BAbAaWPgecwXBIDzw5no= +github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= +github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= +github.com/google/martian/v3 v3.1.0 h1:wCKgOCHuUEVfsaQLpPSJb7VdYCdTVZQAuOdYm1yc/60= +github.com/google/martian/v3 v3.1.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= +github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= +github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= +github.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20200430221834-fc25d7d30c6d/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20201023163331-3e6fc7fc9c4c/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/pprof v0.0.0-20201203190320-1bf35d6f28c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/pprof v0.0.0-20201218002935-b9804c9f04c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= +github.com/google/uuid v1.1.2 h1:EVhdT+1Kseyi1/pUmXKaFxYsDNy9RQYkMWRH68J/W7Y= +github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= +github.com/googleapis/gax-go/v2 v2.0.5 h1:sjZBwGj9Jlw33ImPtvFviGYvseOtDM7hkSKB7+Tv3SM= +github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= +github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So= +github.com/grpc-ecosystem/go-grpc-middleware v1.2.0 h1:0IKlLyQ3Hs9nDaiK5cSHAGmcQEIC8l2Ts1u6x5Dfrqg= +github.com/grpc-ecosystem/go-grpc-middleware v1.2.0/go.mod h1:mJzapYve32yjrKlk9GbyCZHuPgZsrbyIbyKhSzOpg6s= +github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw= +github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= +github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= +github.com/hpcloud/tail v1.0.0 h1:nfCOvKYfkgYP8hkirhJocXT2+zOD8yUNjXaWfTlyFKI= +github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= +github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= +github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= +github.com/jmoiron/sqlx v1.2.0 h1:41Ip0zITnmWNR/vHV+S4m+VoUivnWY5E4OJfLZjCJMA= +github.com/jmoiron/sqlx v1.2.0/go.mod h1:1FEQNm3xlJgrMD+FBdI9+xvCksHtbpVBBw5dYhBSsks= +github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= +github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= +github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= +github.com/jstemmer/go-junit-report v0.9.1 h1:6QPYqodiu3GuPL+7mfx+NwDdp2eTkp9IfEUpgAwUN0o= +github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk= +github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q= +github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +github.com/konsorten/go-windows-terminal-sequences v1.0.1 h1:mweAR1A6xJ3oS2pRaGiHgQ4OO8tzTaLawm8vnODuwDk= +github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= +github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= +github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0= +github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/labstack/echo/v4 v4.2.1/go.mod h1:AA49e0DZ8kk5jTOOCKNuPR6oTnBS0dYiM4FW1e6jwpg= +github.com/labstack/gommon v0.3.0/go.mod h1:MULnywXg0yavhxWKc+lOruYdAhDwPK9wf0OL7NoOu+k= +github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII= +github.com/leodido/go-urn v1.2.1/go.mod h1:zt4jvISO2HfUBqxjfIshjdMTYS56ZS/qv49ictyFfxY= +github.com/lestrrat-go/backoff/v2 v2.0.8/go.mod h1:rHP/q/r9aT27n24JQLa7JhSQZCKBBOiM/uP402WwN8Y= +github.com/lestrrat-go/blackmagic v1.0.0/go.mod h1:TNgH//0vYSs8VXDCfkZLgIrVTTXQELZffUV0tz3MtdQ= +github.com/lestrrat-go/codegen v1.0.2/go.mod h1:JhJw6OQAuPEfVKUCLItpaVLumDGWQznd1VaXrBk9TdM= +github.com/lestrrat-go/httpcc v1.0.0/go.mod h1:tGS/u00Vh5N6FHNkExqGGNId8e0Big+++0Gf8MBnAvE= +github.com/lestrrat-go/iter v1.0.1/go.mod h1:zIdgO1mRKhn8l9vrZJZz9TUMMFbQbLeTsbqPDrJ/OJc= +github.com/lestrrat-go/jwx v1.2.7/go.mod h1:bw24IXWbavc0R2RsOtpXL7RtMyP589yZ1+L7kd09ZGA= +github.com/lestrrat-go/option v1.0.0/go.mod h1:5ZHFbivi4xwXxhxY9XHDe2FHo6/Z7WWmtT7T5nBBp3I= +github.com/lib/pq v1.0.0 h1:X5PMW56eZitiTeO7tKzZxFCSpbFZJtkMMooicw2us9A= +github.com/lib/pq v1.0.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= +github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= +github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= +github.com/matryer/moq v0.0.0-20190312154309-6cfb0558e1bd/go.mod h1:9ELz6aaclSIGnZBoaSLZ3NAl1VTufbOrXBPvtcy6WiQ= +github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= +github.com/mattn/go-colorable v0.1.7/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= +github.com/mattn/go-colorable v0.1.8 h1:c1ghPdyEDarC70ftn0y+A/Ee++9zz8ljHG1b13eJ0s8= +github.com/mattn/go-colorable v0.1.8/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= +github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= +github.com/mattn/go-isatty v0.0.9/go.mod h1:YNRxwqDuOph6SZLI9vUUz6OYw3QyUt7WiY2yME+cCiQ= +github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= +github.com/mattn/go-isatty v0.0.14 h1:yVuAays6BHfxijgZPzw+3Zlu5yQgKGP2/hcQbHb7S9Y= +github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= +github.com/mattn/go-sqlite3 v1.9.0 h1:pDRiWfl+++eC2FEFRy6jXmQlvp4Yh3z1MJKg4UeYM/4= +github.com/mattn/go-sqlite3 v1.9.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc= +github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b h1:j7+1HpAFS1zy5+Q4qx1fWh90gTKwiN4QCGoY9TWyyO4= +github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= +github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/onsi/ginkgo v1.12.0 h1:Iw5WCbBcaAAd0fpRb1c9r5YCylv4XDoCSigm1zLevwU= +github.com/onsi/gomega v1.9.0 h1:R1uwffexN6Pr340GtYRIdZmAiN4J+iw6WG4wog1DUXg= +github.com/opentracing/opentracing-go v1.1.0/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o= +github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= +github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ= +github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= +github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= +github.com/rogpeppe/go-internal v1.8.0 h1:FCbCCtXNOY3UtUuHUYaghJg4y7Fd14rXifAYUAtL9R8= +github.com/rogpeppe/go-internal v1.8.0/go.mod h1:WmiCO8CzOY8rg0OYDC4/i/2WRWAB6poM+XZ2dLUbcbE= +github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= +github.com/sirupsen/logrus v1.5.0 h1:1N5EYkVAPEywqZRJd7cwnRtCb6xJx7NH3T3WUTF980Q= +github.com/sirupsen/logrus v1.5.0/go.mod h1:+F7Ogzej0PZc/94MaYx/nvG9jOFMD2osvC3s+Squfpo= +github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= +github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw= +github.com/ugorji/go v1.2.6/go.mod h1:anCg0y61KIhDlPZmnH+so+RQbysYVyDko0IMgJv0Nn0= +github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY= +github.com/ugorji/go/codec v1.2.6/go.mod h1:V6TCNZ4PHqoHGFZuSG1W8nrCzzdgA2DozYxWFFpvxTw= +github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= +github.com/valyala/fasttemplate v1.0.1/go.mod h1:UQGH1tvbgY+Nz5t2n7tXsz52dQxojPUpymEIMZ47gx8= +github.com/valyala/fasttemplate v1.2.1/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ= +github.com/x-cray/logrus-prefixed-formatter v0.5.2 h1:00txxvfBM9muc0jiLIEAkAcIMJzfthRT6usrui8uGmg= +github.com/x-cray/logrus-prefixed-formatter v0.5.2/go.mod h1:2duySbKsL6M18s5GU7VPsoEPHyzalCE06qoARUCeBBE= +github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= +go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= +go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= +go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= +go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= +go.opencensus.io v0.22.5 h1:dntmOdLpSpHlVqbW5Eay97DelsZHe+55D+xC6i0dDS0= +go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk= +go.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqeYNgFYFoEGnI= +go.uber.org/atomic v1.4.0 h1:cxzIVoETapQEqDhQu3QfnvXAV4AlzcvUCxkVUFw3+EU= +go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= +go.uber.org/multierr v1.1.0 h1:HoEmRHQPVSqub6w2z2d2EOVs2fjyFRGyofhKuyDq0QI= +go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0= +go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20200820211705-5c72a883971a/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20201217014255-9d1352758620/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I= +golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/crypto v0.0.0-20210921155107-089bfa567519 h1:7I4JAnoQBe7ZtJcBaYHi5UtiO8tQHbUSXxL+pnGRANg= +golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= +golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek= +golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY= +golang.org/x/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= +golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= +golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= +golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM= +golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU= +golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= +golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= +golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= +golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs= +golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= +golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= +golang.org/x/lint v0.0.0-20201208152925-83fdc39ff7b5 h1:2M3HP5CCK1Si9FQhwnzYhXdG6DXeebvUHFpre8QvbyI= +golang.org/x/lint v0.0.0-20201208152925-83fdc39ff7b5/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= +golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE= +golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o= +golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= +golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY= +golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= +golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= +golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.4.1 h1:Kvvh58BN8Y9/lBi7hTekvtMpm07eUZ0ck5pRHpsMWrY= +golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200222125558-5a598a2470a0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200506145744-7e3656a0809f/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= +golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= +golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= +golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20201031054903-ff519b6c9102/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20201209123823-ac852fbbde11/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20201224014010-6772e930b67b/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110 h1:qWPm9rbaAMKs8Bq/9LRpbMqxWRVUAQwMI9fVrssnTfw= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= +golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20200902213428-5d25da1a8d43/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20201109201403-9fd604954f58/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20201208152858-08078c50e5b5/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20210218202405-ba52d332ba99 h1:5vD4XjIc0X5+kHZjx4UecYdjA6mJo+XXNoaW0EjU5Os= +golang.org/x/oauth2 v0.0.0-20210218202405-ba52d332ba99/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200331124033-c3d80250170d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200501052902-10377860bb8e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200511232937-7e40ca221e25/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200826173525-f9321e4c35a6/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200905004654-be1d3432aa8f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201201145000-ef89a241ccb3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210104204734-6f8348627aad/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210223095934-7937bea0104d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210806184541-e5e7981a1069/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20211031064116-611d5d643895 h1:iaNpwpnrgL5jzWS0vCNnfa8HqzxveCFpFx3uC/X4Tps= +golang.org/x/sys v0.0.0-20211031064116-611d5d643895/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1 h1:v+OssWQX+hTHEmOBgwxdZxK4zHq3yOs8F9J7mk0PY8E= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk= +golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.0.0-20201208040808-7e3f01d25324/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.0.0-20210220033141-f8bda1e9f3ba/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= +golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191130070609-6e064ea0cf2d/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191216173652-a0e659d51361/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20191227053925-7b8e75db28f4/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200117161641-43d50277825c/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200122220014-bf1340f18c4a/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200204074204-1cc6d1ef6c74/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200212150539-ea181f53ac56/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200224181240-023911ca70b2/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200227222343-706bc42d1f0d/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200304193943-95d2e580d8eb/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= +golang.org/x/tools v0.0.0-20200312045724-11d5b4c81c7d/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= +golang.org/x/tools v0.0.0-20200331025713-a30bf2db82d4/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8= +golang.org/x/tools v0.0.0-20200501065659-ab2804fb9c9d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200512131952-2bc93b1c0c88/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200515010526-7d3b6ebf133d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200618134242-20370b0cb4b2/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= +golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= +golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= +golang.org/x/tools v0.0.0-20200904185747-39188db58858/go.mod h1:Cj7w3i3Rnn0Xh82ur9kSqwfTHTeVxaDqrfMjpcNT6bE= +golang.org/x/tools v0.0.0-20200918232735-d647fc253266/go.mod h1:z6u4i615ZeAfBE4XtMziQW1fSVJXACjjbWkB/mvPzlU= +golang.org/x/tools v0.0.0-20201110124207-079ba7bd75cd/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.0.0-20201201161351-ac6f37ff4c2a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.0.0-20201208233053-a543418bbed2/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.0.0-20210105154028-b0ab187a4818/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.0.0-20210108195828-e2f9c7f1fc8e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.0.0-20210114065538-d78b04bdf963/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.1.0 h1:po9/4sTYwZU9lPhi1tOrb4hCv3qrhiQ77LZfGa2OjwY= +golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= +google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M= +google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= +google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= +google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= +google.golang.org/api v0.14.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= +google.golang.org/api v0.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= +google.golang.org/api v0.17.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.18.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.19.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.20.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.22.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.24.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= +google.golang.org/api v0.28.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= +google.golang.org/api v0.29.0/go.mod h1:Lcubydp8VUV7KeIHD9z2Bys/sm/vGKnG1UHuDBSrHWM= +google.golang.org/api v0.30.0/go.mod h1:QGmEvQ87FHZNiUVJkT14jQNYJ4ZJjdRF23ZXz5138Fc= +google.golang.org/api v0.35.0/go.mod h1:/XrVsuzM0rZmrsbjJutiuftIzeuTQcEeaYcSk/mQ1dg= +google.golang.org/api v0.36.0/go.mod h1:+z5ficQTmoYpPn8LCUNVpK5I7hwkpjbcgqA7I34qYtE= +google.golang.org/api v0.40.0 h1:uWrpz12dpVPn7cojP82mk02XDgTJLDPc2KbVTxrWb4A= +google.golang.org/api v0.40.0/go.mod h1:fYKFpnQN0DsDSKRVRcQSDQNtqWPfM9i+zNPxepjRCQ8= +google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= +google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0= +google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= +google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= +google.golang.org/appengine v1.6.7 h1:FZR1q0exgwxzPzp/aF+VccGrSfxfPpkBqjIIEq3ru6c= +google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= +google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= +google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= +google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= +google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8= +google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20191115194625-c23dd37a84c9/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20191216164720-4f79533eabd1/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20200115191322-ca5a22157cba/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20200122232147-0452cf42e150/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20200204135345-fa8e72b47b90/go.mod h1:GmwEX6Z4W5gMy59cAlVYjN9JhxgbQH6Gn+gFDQe2lzA= +google.golang.org/genproto v0.0.0-20200212174721-66ed5ce911ce/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200224152610-e50cd9704f63/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200228133532-8c2c7df3a383/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200305110556-506484158171/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200312145019-da6875a35672/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200331122359-1ee6d9798940/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200430143042-b979b6f78d84/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200511104702-f5ebc3bea380/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200513103714-09dca8ec2884/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200515170657-fc4c6c6a6587/go.mod h1:YsZOwe1myG/8QRHRsmBRE1LrgQY60beZKjly0O1fX9U= +google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= +google.golang.org/genproto v0.0.0-20200618031413-b414f8b61790/go.mod h1:jDfRM7FcilCzHH/e9qn6dsT145K34l5v+OpcnNgKAAA= +google.golang.org/genproto v0.0.0-20200729003335-053ba62fc06f/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20200804131852-c06518451d9c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20200825200019-8632dd797987/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20200904004341-0bd0a958aa1d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20201109203340-2640f1f9cdfb/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20201201144952-b05cb90ed32e/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20201210142538-e3217bee35cc/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20201214200347-8c77b98c765d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20210108203827-ffc7fda8c3d7/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20210222152913-aa3ee6e6a81c h1:7A9LQhrZmuCPI79/sYSbscFqBp4XFYf6oaIQuV1xji4= +google.golang.org/genproto v0.0.0-20210222152913-aa3ee6e6a81c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= +google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= +google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= +google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= +google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= +google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.28.0/go.mod h1:rpkK4SK4GF4Ach/+MFLZUBavHOvF2JJB5uozKKal+60= +google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk= +google.golang.org/grpc v1.30.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= +google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= +google.golang.org/grpc v1.31.1/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= +google.golang.org/grpc v1.33.1/go.mod h1:fr5YgcSWrqhRRxogOsw7RzIpsmvOZ6IcH4kBYTpR3n0= +google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc= +google.golang.org/grpc v1.34.0/go.mod h1:WotjhfgOW/POjDeRt8vscBtXq+2VjORFy659qA51WJ8= +google.golang.org/grpc v1.35.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= +google.golang.org/grpc v1.36.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= +google.golang.org/grpc v1.40.0 h1:AGJ0Ih4mHjSeibYkFGh1dD9KJ/eOtZ93I6hoHhukQ5Q= +google.golang.org/grpc v1.40.0/go.mod h1:ogyxbiOoUXAkP+4+xa6PZSE9DZgIHtSpzjDTB9KAK34= +google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= +google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= +google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= +google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= +google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= +google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4= +google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= +google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= +google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= +google.golang.org/protobuf v1.27.1 h1:SnqbnDw1V7RiZcXPx5MEeqPv2s79L9i7BJUlG/+RurQ= +google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= +gopkg.in/fsnotify.v1 v1.4.7 h1:xOHLXZwVvI9hhs+cLKq5+I5onOuwQLhQwiu63xxlHs4= +gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= +gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= +gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b h1:h8qDotaEPuJATrMmW04NCwg7v22aHH28wwpauUhK9Oo= +gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= +honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= +honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= +rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= +rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0= +rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA= diff --git a/wild-workouts/internal/trainer/main.go b/wild-workouts/internal/trainer/main.go new file mode 100644 index 0000000..7374ef7 --- /dev/null +++ b/wild-workouts/internal/trainer/main.go @@ -0,0 +1,45 @@ +package main + +import ( + "context" + "fmt" + "net/http" + "os" + "strings" + + "github.com/ThreeDotsLabs/wild-workouts-go-ddd-example/internal/common/genproto/trainer" + "github.com/ThreeDotsLabs/wild-workouts-go-ddd-example/internal/common/logs" + "github.com/ThreeDotsLabs/wild-workouts-go-ddd-example/internal/common/server" + "github.com/ThreeDotsLabs/wild-workouts-go-ddd-example/internal/trainer/ports" + "github.com/ThreeDotsLabs/wild-workouts-go-ddd-example/internal/trainer/service" + "github.com/go-chi/chi/v5" + "google.golang.org/grpc" +) + +func main() { + logs.Init() + + ctx := context.Background() + + application := service.NewApplication(ctx) + + serverType := strings.ToLower(os.Getenv("SERVER_TO_RUN")) + switch serverType { + case "http": + go loadFixtures(application) + + server.RunHTTPServer(func(router chi.Router) http.Handler { + return ports.HandlerFromMux( + ports.NewHttpServer(application), + router, + ) + }) + case "grpc": + server.RunGRPCServer(func(server *grpc.Server) { + svc := ports.NewGrpcServer(application) + trainer.RegisterTrainerServiceServer(server, svc) + }) + default: + panic(fmt.Sprintf("server type '%s' is not supported", serverType)) + } +} diff --git a/wild-workouts/internal/trainer/ports/grpc.go b/wild-workouts/internal/trainer/ports/grpc.go new file mode 100644 index 0000000..69fc4de --- /dev/null +++ b/wild-workouts/internal/trainer/ports/grpc.go @@ -0,0 +1,68 @@ +package ports + +import ( + "context" + "time" + + "github.com/ThreeDotsLabs/wild-workouts-go-ddd-example/internal/common/genproto/trainer" + "github.com/ThreeDotsLabs/wild-workouts-go-ddd-example/internal/trainer/app" + "github.com/ThreeDotsLabs/wild-workouts-go-ddd-example/internal/trainer/app/command" + "github.com/ThreeDotsLabs/wild-workouts-go-ddd-example/internal/trainer/app/query" + "github.com/golang/protobuf/ptypes/empty" + "github.com/golang/protobuf/ptypes/timestamp" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" +) + +type GrpcServer struct { + app app.Application +} + +func NewGrpcServer(application app.Application) GrpcServer { + return GrpcServer{app: application} +} + +func (g GrpcServer) MakeHourAvailable(ctx context.Context, request *trainer.UpdateHourRequest) (*empty.Empty, error) { + trainingTime := protoTimestampToTime(request.Time) + + if err := g.app.Commands.MakeHoursAvailable.Handle(ctx, command.MakeHoursAvailable{Hours: []time.Time{trainingTime}}); err != nil { + return nil, status.Error(codes.Internal, err.Error()) + } + + return &empty.Empty{}, nil +} + +func (g GrpcServer) ScheduleTraining(ctx context.Context, request *trainer.UpdateHourRequest) (*empty.Empty, error) { + trainingTime := protoTimestampToTime(request.Time) + + if err := g.app.Commands.ScheduleTraining.Handle(ctx, command.ScheduleTraining{Hour: trainingTime}); err != nil { + return nil, status.Error(codes.Internal, err.Error()) + } + + return &empty.Empty{}, nil +} + +func (g GrpcServer) CancelTraining(ctx context.Context, request *trainer.UpdateHourRequest) (*empty.Empty, error) { + trainingTime := protoTimestampToTime(request.Time) + + if err := g.app.Commands.CancelTraining.Handle(ctx, command.CancelTraining{Hour: trainingTime}); err != nil { + return nil, status.Error(codes.Internal, err.Error()) + } + + return &empty.Empty{}, nil +} + +func (g GrpcServer) IsHourAvailable(ctx context.Context, request *trainer.IsHourAvailableRequest) (*trainer.IsHourAvailableResponse, error) { + trainingTime := protoTimestampToTime(request.Time) + + isAvailable, err := g.app.Queries.HourAvailability.Handle(ctx, query.HourAvailability{Hour: trainingTime}) + if err != nil { + return nil, status.Error(codes.Internal, err.Error()) + } + + return &trainer.IsHourAvailableResponse{IsAvailable: isAvailable}, nil +} + +func protoTimestampToTime(timestamp *timestamp.Timestamp) time.Time { + return timestamp.AsTime().UTC().Truncate(time.Hour) +} diff --git a/wild-workouts/internal/trainer/ports/http.go b/wild-workouts/internal/trainer/ports/http.go new file mode 100644 index 0000000..e4bb70b --- /dev/null +++ b/wild-workouts/internal/trainer/ports/http.go @@ -0,0 +1,49 @@ +package ports + +import ( + "net/http" + + "github.com/go-chi/render" + + "github.com/ThreeDotsLabs/wild-workouts-go-ddd-example/internal/common/auth" + "github.com/ThreeDotsLabs/wild-workouts-go-ddd-example/internal/common/server/httperr" + "github.com/ThreeDotsLabs/wild-workouts-go-ddd-example/internal/trainer/app" + "github.com/ThreeDotsLabs/wild-workouts-go-ddd-example/internal/trainer/app/command" +) + +type HttpServer struct { + app app.Application +} + +func NewHttpServer(application app.Application) HttpServer { + return HttpServer{ + app: application, + } +} + +func (h HttpServer) MakeHourUnavailable(w http.ResponseWriter, r *http.Request) { + user, err := auth.UserFromCtx(r.Context()) + if err != nil { + httperr.RespondWithSlugError(err, w, r) + return + } + + if user.Role != "trainer" { + httperr.Unauthorised("invalid-role", nil, w, r) + return + } + + hourUpdate := &HourUpdate{} + if err := render.Decode(r, hourUpdate); err != nil { + httperr.RespondWithSlugError(err, w, r) + return + } + + err = h.app.Commands.MakeHoursUnavailable.Handle(r.Context(), command.MakeHoursUnavailable{Hours: hourUpdate.Hours}) + if err != nil { + httperr.RespondWithSlugError(err, w, r) + return + } + + w.WriteHeader(http.StatusNoContent) +} diff --git a/wild-workouts/internal/trainer/ports/openapi_api.gen.go b/wild-workouts/internal/trainer/ports/openapi_api.gen.go new file mode 100644 index 0000000..6531fd1 --- /dev/null +++ b/wild-workouts/internal/trainer/ports/openapi_api.gen.go @@ -0,0 +1,161 @@ +// Package ports provides primitives to interact with the openapi HTTP API. +// +// Code generated by github.com/deepmap/oapi-codegen version v1.8.2 DO NOT EDIT. +package ports + +import ( + "context" + "fmt" + "net/http" + + "github.com/deepmap/oapi-codegen/pkg/runtime" + "github.com/go-chi/chi/v5" +) + +// ServerInterface represents all server handlers. +type ServerInterface interface { + // (PUT /trainer/calendar/make-hour-unavailable) + MakeHourUnavailable(w http.ResponseWriter, r *http.Request) +} + +// ServerInterfaceWrapper converts contexts to parameters. +type ServerInterfaceWrapper struct { + Handler ServerInterface + HandlerMiddlewares []MiddlewareFunc +} + +type MiddlewareFunc func(http.HandlerFunc) http.HandlerFunc + +// GetTrainerAvailableHours operation middleware +func (siw *ServerInterfaceWrapper) GetTrainerAvailableHours(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + + var err error + + ctx = context.WithValue(ctx, BearerAuthScopes, []string{""}) + + // Parameter object where we will unmarshal all parameters from the context + var params GetTrainerAvailableHoursParams + + // ------------- Required query parameter "dateFrom" ------------- + if paramValue := r.URL.Query().Get("dateFrom"); paramValue != "" { + + } else { + http.Error(w, "Query argument dateFrom is required, but not found", http.StatusBadRequest) + return + } + + err = runtime.BindQueryParameter("form", true, true, "dateFrom", r.URL.Query(), ¶ms.DateFrom) + if err != nil { + http.Error(w, fmt.Sprintf("Invalid format for parameter dateFrom: %s", err), http.StatusBadRequest) + return + } + + // ------------- Required query parameter "dateTo" ------------- + if paramValue := r.URL.Query().Get("dateTo"); paramValue != "" { + + } else { + http.Error(w, "Query argument dateTo is required, but not found", http.StatusBadRequest) + return + } + + err = runtime.BindQueryParameter("form", true, true, "dateTo", r.URL.Query(), ¶ms.DateTo) + if err != nil { + http.Error(w, fmt.Sprintf("Invalid format for parameter dateTo: %s", err), http.StatusBadRequest) + return + } + + var handler = func(w http.ResponseWriter, r *http.Request) { + siw.Handler.GetTrainerAvailableHours(w, r, params) + } + + for _, middleware := range siw.HandlerMiddlewares { + handler = middleware(handler) + } + + handler(w, r.WithContext(ctx)) +} + +// MakeHourAvailable operation middleware +func (siw *ServerInterfaceWrapper) MakeHourAvailable(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + + ctx = context.WithValue(ctx, BearerAuthScopes, []string{""}) + + var handler = func(w http.ResponseWriter, r *http.Request) { + siw.Handler.MakeHourAvailable(w, r) + } + + for _, middleware := range siw.HandlerMiddlewares { + handler = middleware(handler) + } + + handler(w, r.WithContext(ctx)) +} + +// MakeHourUnavailable operation middleware +func (siw *ServerInterfaceWrapper) MakeHourUnavailable(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + + ctx = context.WithValue(ctx, BearerAuthScopes, []string{""}) + + var handler = func(w http.ResponseWriter, r *http.Request) { + siw.Handler.MakeHourUnavailable(w, r) + } + + for _, middleware := range siw.HandlerMiddlewares { + handler = middleware(handler) + } + + handler(w, r.WithContext(ctx)) +} + +// Handler creates http.Handler with routing matching OpenAPI spec. +func Handler(si ServerInterface) http.Handler { + return HandlerWithOptions(si, ChiServerOptions{}) +} + +type ChiServerOptions struct { + BaseURL string + BaseRouter chi.Router + Middlewares []MiddlewareFunc +} + +// HandlerFromMux creates http.Handler with routing matching OpenAPI spec based on the provided mux. +func HandlerFromMux(si ServerInterface, r chi.Router) http.Handler { + return HandlerWithOptions(si, ChiServerOptions{ + BaseRouter: r, + }) +} + +func HandlerFromMuxWithBaseURL(si ServerInterface, r chi.Router, baseURL string) http.Handler { + return HandlerWithOptions(si, ChiServerOptions{ + BaseURL: baseURL, + BaseRouter: r, + }) +} + +// HandlerWithOptions creates http.Handler with additional options +func HandlerWithOptions(si ServerInterface, options ChiServerOptions) http.Handler { + r := options.BaseRouter + + if r == nil { + r = chi.NewRouter() + } + wrapper := ServerInterfaceWrapper{ + Handler: si, + HandlerMiddlewares: options.Middlewares, + } + + r.Group(func(r chi.Router) { + r.Get(options.BaseURL+"/trainer/calendar", wrapper.GetTrainerAvailableHours) + }) + r.Group(func(r chi.Router) { + r.Put(options.BaseURL+"/trainer/calendar/make-hour-available", wrapper.MakeHourAvailable) + }) + r.Group(func(r chi.Router) { + r.Put(options.BaseURL+"/trainer/calendar/make-hour-unavailable", wrapper.MakeHourUnavailable) + }) + + return r +} diff --git a/wild-workouts/internal/trainer/ports/openapi_types.gen.go b/wild-workouts/internal/trainer/ports/openapi_types.gen.go new file mode 100644 index 0000000..e03cd85 --- /dev/null +++ b/wild-workouts/internal/trainer/ports/openapi_types.gen.go @@ -0,0 +1,57 @@ +// Package ports provides primitives to interact with the openapi HTTP API. +// +// Code generated by github.com/deepmap/oapi-codegen version v1.8.2 DO NOT EDIT. +package ports + +import ( + "time" + + openapi_types "github.com/deepmap/oapi-codegen/pkg/types" +) + +const ( + BearerAuthScopes = "bearerAuth.Scopes" +) + +// Date defines model for Date. +type Date struct { + Date openapi_types.Date `json:"date"` + HasFreeHours bool `json:"hasFreeHours"` + Hours []Hour `json:"hours"` +} + +// Error defines model for Error. +type Error struct { + Message string `json:"message"` + Slug string `json:"slug"` +} + +// Hour defines model for Hour. +type Hour struct { + Available bool `json:"available"` + HasTrainingScheduled bool `json:"hasTrainingScheduled"` + Hour time.Time `json:"hour"` +} + +// HourUpdate defines model for HourUpdate. +type HourUpdate struct { + Hours []time.Time `json:"hours"` +} + +// GetTrainerAvailableHoursParams defines parameters for GetTrainerAvailableHours. +type GetTrainerAvailableHoursParams struct { + DateFrom time.Time `json:"dateFrom"` + DateTo time.Time `json:"dateTo"` +} + +// MakeHourAvailableJSONBody defines parameters for MakeHourAvailable. +type MakeHourAvailableJSONBody HourUpdate + +// MakeHourUnavailableJSONBody defines parameters for MakeHourUnavailable. +type MakeHourUnavailableJSONBody HourUpdate + +// MakeHourAvailableJSONRequestBody defines body for MakeHourAvailable for application/json ContentType. +type MakeHourAvailableJSONRequestBody MakeHourAvailableJSONBody + +// MakeHourUnavailableJSONRequestBody defines body for MakeHourUnavailable for application/json ContentType. +type MakeHourUnavailableJSONRequestBody MakeHourUnavailableJSONBody diff --git a/wild-workouts/internal/trainer/service/application.go b/wild-workouts/internal/trainer/service/application.go new file mode 100644 index 0000000..ba18d72 --- /dev/null +++ b/wild-workouts/internal/trainer/service/application.go @@ -0,0 +1,51 @@ +package service + +import ( + "context" + "os" + + "cloud.google.com/go/firestore" + "github.com/sirupsen/logrus" + + "github.com/ThreeDotsLabs/wild-workouts-go-ddd-example/internal/common/metrics" + "github.com/ThreeDotsLabs/wild-workouts-go-ddd-example/internal/trainer/adapters" + "github.com/ThreeDotsLabs/wild-workouts-go-ddd-example/internal/trainer/app" + "github.com/ThreeDotsLabs/wild-workouts-go-ddd-example/internal/trainer/app/command" + "github.com/ThreeDotsLabs/wild-workouts-go-ddd-example/internal/trainer/app/query" + "github.com/ThreeDotsLabs/wild-workouts-go-ddd-example/internal/trainer/domain/hour" +) + +func NewApplication(ctx context.Context) app.Application { + firestoreClient, err := firestore.NewClient(ctx, os.Getenv("GCP_PROJECT")) + if err != nil { + panic(err) + } + + factoryConfig := hour.FactoryConfig{ + MaxWeeksInTheFutureToSet: 6, + MinUtcHour: 12, + MaxUtcHour: 20, + } + + hourFactory, err := hour.NewFactory(factoryConfig) + if err != nil { + panic(err) + } + + hourRepository := adapters.NewFirestoreHourRepository(firestoreClient, hourFactory) + + logger := logrus.NewEntry(logrus.StandardLogger()) + metricsClient := metrics.NoOp{} + + return app.Application{ + Commands: app.Commands{ + CancelTraining: command.NewCancelTrainingHandler(hourRepository, logger, metricsClient), + ScheduleTraining: command.NewScheduleTrainingHandler(hourRepository, logger, metricsClient), + MakeHoursAvailable: command.NewMakeHoursAvailableHandler(hourRepository, logger, metricsClient), + MakeHoursUnavailable: command.NewMakeHoursUnavailableHandler(hourRepository, logger, metricsClient), + }, + Queries: app.Queries{ + HourAvailability: query.NewHourAvailabilityHandler(hourRepository, logger, metricsClient), + }, + } +} diff --git a/wild-workouts/internal/trainer/service/component_test.go b/wild-workouts/internal/trainer/service/component_test.go new file mode 100644 index 0000000..942e206 --- /dev/null +++ b/wild-workouts/internal/trainer/service/component_test.go @@ -0,0 +1,108 @@ +package service + +import ( + "context" + "log" + "net/http" + "os" + "testing" + "time" + + trainerHTTP "github.com/ThreeDotsLabs/wild-workouts-go-ddd-example/internal/common/client/trainer" + "github.com/ThreeDotsLabs/wild-workouts-go-ddd-example/internal/common/genproto/trainer" + "github.com/ThreeDotsLabs/wild-workouts-go-ddd-example/internal/common/server" + "github.com/ThreeDotsLabs/wild-workouts-go-ddd-example/internal/common/tests" + "github.com/ThreeDotsLabs/wild-workouts-go-ddd-example/internal/trainer/ports" + "github.com/go-chi/chi/v5" + "github.com/google/uuid" + "github.com/stretchr/testify/require" + "google.golang.org/grpc" +) + +func TestHoursAvailability(t *testing.T) { + t.Parallel() + + token := tests.FakeTrainerJWT(t, uuid.New().String()) + client := tests.NewTrainerHTTPClient(t, token) + + hour := tests.RelativeDate(11, 12) + expectedHour := trainerHTTP.Hour{ + Available: true, + HasTrainingScheduled: false, + Hour: hour, + } + + date := hour.Truncate(24 * time.Hour) + from := date.AddDate(0, 0, -1) + to := date.AddDate(0, 0, 1) + + getHours := func() []trainerHTTP.Hour { + dates := client.GetTrainerAvailableHours(t, from, to) + for _, d := range dates { + if d.Date.Equal(date) { + return d.Hours + } + } + t.Fatalf("Date not found in dates: %+v", dates) + return nil + } + + client.MakeHourUnavailable(t, hour) + require.NotContains(t, getHours(), expectedHour) + + code := client.MakeHourAvailable(t, hour) + require.Equal(t, http.StatusNoContent, code) + require.Contains(t, getHours(), expectedHour) + + client.MakeHourUnavailable(t, hour) + require.NotContains(t, getHours(), expectedHour) +} + +func TestUnauthorizedForAttendee(t *testing.T) { + t.Parallel() + + token := tests.FakeAttendeeJWT(t, uuid.New().String()) + client := tests.NewTrainerHTTPClient(t, token) + + hour := tests.RelativeDate(11, 13) + + code := client.MakeHourAvailable(t, hour) + require.Equal(t, http.StatusUnauthorized, code) +} + +func startService() bool { + app := NewApplication(context.Background()) + + trainerHTTPAddr := os.Getenv("TRAINER_HTTP_ADDR") + go server.RunHTTPServerOnAddr(trainerHTTPAddr, func(router chi.Router) http.Handler { + return ports.HandlerFromMux(ports.NewHttpServer(app), router) + }) + + trainerGrpcAddr := os.Getenv("TRAINER_GRPC_ADDR") + go server.RunGRPCServerOnAddr(trainerGrpcAddr, func(server *grpc.Server) { + svc := ports.NewGrpcServer(app) + trainer.RegisterTrainerServiceServer(server, svc) + }) + + ok := tests.WaitForPort(trainerHTTPAddr) + if !ok { + log.Println("Timed out waiting for trainer HTTP to come up") + return false + } + + ok = tests.WaitForPort(trainerGrpcAddr) + if !ok { + log.Println("Timed out waiting for trainer gRPC to come up") + } + + return ok +} + +func TestMain(m *testing.M) { + if !startService() { + log.Println("Timed out waiting for trainings HTTP to come up") + os.Exit(1) + } + + os.Exit(m.Run()) +} diff --git a/wild-workouts/sql/schema.sql b/wild-workouts/sql/schema.sql new file mode 100644 index 0000000..ce7f40f --- /dev/null +++ b/wild-workouts/sql/schema.sql @@ -0,0 +1,6 @@ +CREATE TABLE `hours` +( + hour TIMESTAMP NOT NULL DEFAULT 0, + availability ENUM ('available', 'not_available', 'training_scheduled') NOT NULL, + PRIMARY KEY (hour) +); \ No newline at end of file