├── .env.example ├── .github └── workflows │ ├── build&push.yml │ └── lint&test.yml ├── .gitignore ├── .golangci.yml ├── .pre-commit-config.yaml ├── Makefile ├── README.md ├── api ├── openapi.yaml └── protobuf.proto ├── bin └── .gitkeep ├── build └── Dockerfile ├── check-go-generate.sh ├── cmd ├── migrator │ └── main.go └── server │ └── main.go ├── docker-compose.yml ├── generated ├── generate.go ├── openapi │ ├── client.go │ ├── server.go │ ├── spec.go │ └── types.go └── protobuf │ ├── protobuf.pb.go │ └── protobuf_grpc.pb.go ├── go.mod ├── go.sum ├── integration_tests ├── ping_test.go ├── suites │ └── run_server_suite.go └── users │ ├── application │ ├── crud_test.go │ └── suite_test.go │ └── infrastructure │ ├── postgres_test.go │ └── redis_test.go ├── internal ├── application │ ├── grpc_server.go │ ├── http_server.go │ ├── interfaces.go │ ├── orders │ │ ├── create.go │ │ └── handlers.go │ ├── suite.go │ └── users │ │ ├── create.go │ │ ├── get.go │ │ ├── handlers.go │ │ └── users_test │ │ ├── create_test.go │ │ ├── get_test.go │ │ └── suite_test.go ├── config.go ├── domain │ ├── orders │ │ ├── errors.go │ │ ├── item.go │ │ ├── order.go │ │ └── statuses.go │ ├── products │ │ └── product.go │ └── users │ │ └── user.go ├── infrastructure │ ├── orders │ │ ├── in_memory.go │ │ └── postgres.go │ ├── products │ │ ├── in_memory.go │ │ └── postgres.go │ └── users │ │ ├── in_memory.go │ │ ├── postgres.go │ │ └── redis.go ├── migrate.go ├── run.go └── service │ └── orders │ ├── create.go │ └── service.go ├── migrations ├── 000001_add_users_table.down.sql ├── 000001_add_users_table.up.sql ├── 20240919100509_add_products_table.down.sql ├── 20240919100509_add_products_table.up.sql ├── 20240919131123_add_orders_table.down.sql └── 20240919131123_add_orders_table.up.sql ├── pkg ├── contextkeys │ └── contextkeys.go ├── echomiddleware │ ├── echomiddleware_test │ │ └── sloglogger_test.go │ ├── requestcontext.go │ ├── sentrycontext.go │ ├── shared.go │ └── sloglogger.go ├── environment │ └── type.go ├── logger │ ├── logger_test │ │ └── setup_test.go │ └── setup.go ├── postgres │ ├── cluster.go │ ├── connection.go │ └── transaction.go └── sentry │ └── init.go └── profiles └── .gitkeep /.env.example: -------------------------------------------------------------------------------- 1 | POSTGRES_USER=postgres 2 | POSTGRES_PASSWORD=postgres 3 | POSTGRES_HOSTS=localhost 4 | POSTGRES_PORT=5432 5 | POSTGRES_DATABASE=postgres 6 | POSTGRES_SSL=false 7 | POSTGRES_MIGRATION_PATH=file://migrations 8 | -------------------------------------------------------------------------------- /.github/workflows/build&push.yml: -------------------------------------------------------------------------------- 1 | name: Build and Push 2 | 3 | on: 4 | push: 5 | tags: 6 | - '*' 7 | 8 | jobs: 9 | build_and_push: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - name: Checkout code 13 | uses: actions/checkout@v4 14 | 15 | - name: Set up Go 16 | uses: actions/setup-go@v5 17 | with: 18 | go-version: 1.23.1 19 | 20 | - name: Build Go binary for Linux 21 | run: GOOS=linux GOARCH=amd64 go build -o ./bin/your-service-name ./cmd/main.go 22 | 23 | - name: Login to Docker 24 | uses: docker/login-action@v3 25 | with: 26 | registry: ghcr.io 27 | username: ${{ github.actor }} 28 | # create your personal access token in GitHub and add it to the repository secrets 29 | password: ${{ secrets.GHCR_ACCESS_TOKEN }} 30 | 31 | - name: Build and push Docker image 32 | uses: docker/build-push-action@v5 33 | with: 34 | context: . 35 | file: ./build/Dockerfile 36 | push: true 37 | tags: ghcr.io/gonozov0/your-service-name:${{ github.ref_name }} 38 | -------------------------------------------------------------------------------- /.github/workflows/lint&test.yml: -------------------------------------------------------------------------------- 1 | name: Lint and Test 2 | 3 | on: 4 | push: 5 | branches: [ main ] 6 | tags-ignore: ['*'] 7 | pull_request: 8 | branches: [ main ] 9 | 10 | jobs: 11 | lint: 12 | runs-on: ubuntu-latest 13 | 14 | steps: 15 | - name: Checkout code 16 | uses: actions/checkout@v4 17 | 18 | - name: Set up Go 19 | uses: actions/setup-go@v5 20 | with: 21 | go-version: 1.23.1 22 | 23 | - name: Install protoc 24 | run: | 25 | curl -LO https://github.com/protocolbuffers/protobuf/releases/download/v28.1/protoc-28.1-linux-x86_64.zip 26 | unzip protoc-28.1-linux-x86_64.zip -d $HOME/protoc 27 | echo "$HOME/protoc/bin" >> $GITHUB_PATH 28 | 29 | - name: Install other dependencies 30 | run: | 31 | go install golang.org/x/tools/cmd/goimports@latest 32 | go install github.com/golangci/golangci-lint/cmd/golangci-lint@latest 33 | go install github.com/segmentio/golines@latest 34 | go install github.com/oapi-codegen/oapi-codegen/v2/cmd/oapi-codegen@latest 35 | go install google.golang.org/protobuf/cmd/protoc-gen-go@latest 36 | go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@latest 37 | 38 | - name: Lint code 39 | run: | 40 | unformatted=$(go fmt ./...) 41 | if [ -n "$unformatted" ]; then 42 | echo "These files are not formatted with 'go fmt': $unformatted" 43 | exit 1 44 | fi 45 | unformatted=$(find . -name '*.go' ! -path "./generated/*" -exec goimports -local go-echo-template/ -l {} +) 46 | if [ -n "$unformatted" ]; then 47 | echo "These files are not formatted with 'goimports': $unformatted" 48 | exit 1 49 | fi 50 | unformatted=$(find . -name '*.go' ! -path "./generated/*" -exec golines -w {} -m 120 \;) 51 | if [ -n "$unformatted" ]; then 52 | echo "These files are not formatted with 'golines': $unformatted" 53 | exit 1 54 | fi 55 | golangci-lint run ./... 56 | 57 | - name: Check go generate 58 | run: ./check-go-generate.sh 59 | shell: bash 60 | 61 | test: 62 | runs-on: ubuntu-latest 63 | services: 64 | redis: 65 | image: redis:latest 66 | ports: 67 | - 6379:6379 68 | postgres: 69 | image: postgres:latest 70 | ports: 71 | - 5432:5432 72 | env: 73 | POSTGRES_USER: postgres 74 | POSTGRES_PASSWORD: postgres 75 | POSTGRES_DB: postgres 76 | 77 | steps: 78 | - name: Checkout code 79 | uses: actions/checkout@v4 80 | 81 | - name: Set up Go 82 | uses: actions/setup-go@v5 83 | with: 84 | go-version: 1.23.1 85 | 86 | - name: Apply migrations 87 | run: go run ./cmd/migrator/main.go 88 | env: 89 | POSTGRES_HOSTS: localhost 90 | POSTGRES_PORT: 5432 91 | POSTGRES_USER: postgres 92 | POSTGRES_PASSWORD: postgres 93 | POSTGRES_DATABASE: postgres 94 | POSTGRES_SSL: false 95 | POSTGRES_MIGRATION_PATH: "file://migrations" 96 | 97 | - name: Run tests 98 | # TODO: fix test execution in 1 thread 99 | run: go test -p=1 -coverpkg=./... -count=1 -coverprofile=coverage.out ./... 100 | env: 101 | REDIS_ADDRESS: localhost:6379 102 | POSTGRES_HOSTS: localhost 103 | POSTGRES_PORT: 5432 104 | POSTGRES_USER: postgres 105 | POSTGRES_PASSWORD: postgres 106 | POSTGRES_DATABASE: postgres 107 | POSTGRES_SSL: false 108 | 109 | - name: Upload coverage to GitHub Artifacts 110 | uses: actions/upload-artifact@v4 111 | with: 112 | name: coverage 113 | path: coverage.out 114 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # If you prefer the allow list template instead of the deny list, see community template: 2 | # https://github.com/github/gitignore/blob/main/community/Golang/Go.AllowList.gitignore 3 | # 4 | # Binaries for programs and plugins 5 | *.exe 6 | *.exe~ 7 | *.dll 8 | *.so 9 | *.dylib 10 | 11 | # Test binary, built with `go test -c` 12 | *.test 13 | 14 | # Output of the go coverage tool, specifically when used with LiteIDE 15 | *.out 16 | *.html 17 | 18 | # Dependency directories (remove the comment below to include it) 19 | # vendor/ 20 | .DS_Store 21 | 22 | 23 | # Go workspace file 24 | go.work 25 | 26 | .env 27 | .env.fish 28 | .idea 29 | .code 30 | 31 | bin/* 32 | !bin/.gitkeep 33 | 34 | profiles/* 35 | !profiles/.gitkeep 36 | -------------------------------------------------------------------------------- /.golangci.yml: -------------------------------------------------------------------------------- 1 | # original version: https://gist.github.com/maratori/47a4d00457a92aa426dbd48a18776322 2 | 3 | # This code is licensed under the terms of the MIT license https://opensource.org/license/mit 4 | # Copyright (c) 2021 Marat Reymers 5 | 6 | ## Golden config for golangci-lint v1.61.0 7 | # 8 | # This is the best config for golangci-lint based on my experience and opinion. 9 | # It is very strict, but not extremely strict. 10 | # Feel free to adapt and change it for your needs. 11 | 12 | run: 13 | # Timeout for analysis, e.g. 30s, 5m. 14 | # Default: 1m 15 | timeout: 3m 16 | allow-parallel-runners: true # edited 17 | 18 | 19 | # This file contains only configs which differ from defaults. 20 | # All possible options can be found here https://github.com/golangci/golangci-lint/blob/master/.golangci.reference.yml 21 | linters-settings: 22 | revive: # edited 23 | rules: 24 | - name: blank-imports 25 | severity: warning 26 | disabled: true 27 | 28 | cyclop: 29 | # The maximal code complexity to report. 30 | # Default: 10 31 | max-complexity: 30 32 | # The maximal average package complexity. 33 | # If it's higher than 0.0 (float) the check is enabled 34 | # Default: 0.0 35 | package-average: 10.0 36 | 37 | errcheck: 38 | # Report about not checking of errors in type assertions: `a := b.(MyStruct)`. 39 | # Such cases aren't reported by default. 40 | # Default: false 41 | check-type-assertions: true 42 | 43 | exhaustive: 44 | # Program elements to check for exhaustiveness. 45 | # Default: [ switch ] 46 | check: 47 | - switch 48 | - map 49 | 50 | exhaustruct: 51 | # List of regular expressions to exclude struct packages and their names from checks. 52 | # Regular expressions must match complete canonical struct package/name/structname. 53 | # Default: [] 54 | exclude: 55 | # std libs 56 | - "^net/http.Client$" 57 | - "^net/http.Cookie$" 58 | - "^net/http.Request$" 59 | - "^net/http.Response$" 60 | - "^net/http.Server$" 61 | - "^net/http.Transport$" 62 | - "^net/url.URL$" 63 | - "^os/exec.Cmd$" 64 | - "^reflect.StructField$" 65 | # public libs 66 | - "^github.com/Shopify/sarama.Config$" 67 | - "^github.com/Shopify/sarama.ProducerMessage$" 68 | - "^github.com/mitchellh/mapstructure.DecoderConfig$" 69 | - "^github.com/prometheus/client_golang/.+Opts$" 70 | - "^github.com/spf13/cobra.Command$" 71 | - "^github.com/spf13/cobra.CompletionOptions$" 72 | - "^github.com/stretchr/testify/mock.Mock$" 73 | - "^github.com/testcontainers/testcontainers-go.+Request$" 74 | - "^github.com/testcontainers/testcontainers-go.FromDockerfile$" 75 | - "^golang.org/x/tools/go/analysis.Analyzer$" 76 | - "^google.golang.org/protobuf/.+Options$" 77 | - "^gopkg.in/yaml.v3.Node$" 78 | 79 | funlen: 80 | # Checks the number of lines in a function. 81 | # If lower than 0, disable the check. 82 | # Default: 60 83 | lines: 100 84 | # Checks the number of statements in a function. 85 | # If lower than 0, disable the check. 86 | # Default: 40 87 | statements: 50 88 | # Ignore comments when counting lines. 89 | # Default false 90 | ignore-comments: true 91 | 92 | gocognit: 93 | # Minimal code complexity to report. 94 | # Default: 30 (but we recommend 10-20) 95 | min-complexity: 20 96 | 97 | gocritic: 98 | # Settings passed to gocritic. 99 | # The settings key is the name of a supported gocritic checker. 100 | # The list of supported checkers can be find in https://go-critic.github.io/overview. 101 | settings: 102 | captLocal: 103 | # Whether to restrict checker to params only. 104 | # Default: true 105 | paramsOnly: false 106 | underef: 107 | # Whether to skip (*x).method() calls where x is a pointer receiver. 108 | # Default: true 109 | skipRecvDeref: false 110 | 111 | gomodguard: 112 | blocked: 113 | # List of blocked modules. 114 | # Default: [] 115 | modules: 116 | - github.com/golang/protobuf: 117 | recommendations: 118 | - google.golang.org/protobuf 119 | reason: "see https://developers.google.com/protocol-buffers/docs/reference/go/faq#modules" 120 | - github.com/satori/go.uuid: 121 | recommendations: 122 | - github.com/google/uuid 123 | reason: "satori's package is not maintained" 124 | - github.com/gofrs/uuid: 125 | recommendations: 126 | - github.com/gofrs/uuid/v5 127 | reason: "gofrs' package was not go module before v5" 128 | 129 | govet: 130 | # Enable all analyzers. 131 | # Default: false 132 | enable-all: true 133 | # Disable analyzers by name. 134 | # Run `go tool vet help` to see all analyzers. 135 | # Default: [] 136 | disable: 137 | - fieldalignment # too strict 138 | - shadow # edited 139 | 140 | inamedparam: 141 | # Skips check for interface methods with only a single parameter. 142 | # Default: false 143 | skip-single-param: true 144 | 145 | mnd: 146 | # List of function patterns to exclude from analysis. 147 | # Values always ignored: `time.Date`, 148 | # `strconv.FormatInt`, `strconv.FormatUint`, `strconv.FormatFloat`, 149 | # `strconv.ParseInt`, `strconv.ParseUint`, `strconv.ParseFloat`. 150 | # Default: [] 151 | ignored-functions: 152 | - args.Error 153 | - flag.Arg 154 | - flag.Duration.* 155 | - flag.Float.* 156 | - flag.Int.* 157 | - flag.Uint.* 158 | - os.Chmod 159 | - os.Mkdir.* 160 | - os.OpenFile 161 | - os.WriteFile 162 | - prometheus.ExponentialBuckets.* 163 | - prometheus.LinearBuckets 164 | 165 | nakedret: 166 | # Make an issue if func has more lines of code than this setting, and it has naked returns. 167 | # Default: 30 168 | max-func-lines: 0 169 | 170 | nolintlint: 171 | # Exclude following linters from requiring an explanation. 172 | # Default: [] 173 | allow-no-explanation: [ funlen, gocognit, lll ] 174 | # Enable to require an explanation of nonzero length after each nolint directive. 175 | # Default: false 176 | require-explanation: true 177 | # Enable to require nolint directives to mention the specific linter being suppressed. 178 | # Default: false 179 | require-specific: true 180 | 181 | perfsprint: 182 | # Optimizes into strings concatenation. 183 | # Default: true 184 | strconcat: false 185 | 186 | rowserrcheck: 187 | # database/sql is always checked 188 | # Default: [] 189 | packages: 190 | - github.com/jmoiron/sqlx 191 | 192 | sloglint: 193 | # Enforce not using global loggers. 194 | # Values: 195 | # - "": disabled 196 | # - "all": report all global loggers 197 | # - "default": report only the default slog logger 198 | # https://github.com/go-simpler/sloglint?tab=readme-ov-file#no-global 199 | # Default: "" 200 | no-global: "" 201 | # Enforce using methods that accept a context. 202 | # Values: 203 | # - "": disabled 204 | # - "all": report all contextless calls 205 | # - "scope": report only if a context exists in the scope of the outermost function 206 | # https://github.com/go-simpler/sloglint?tab=readme-ov-file#context-only 207 | # Default: "" 208 | context: "scope" 209 | 210 | decorder: 211 | gci: 212 | loggercheck: 213 | maintidx: 214 | nestif: 215 | grouper: 216 | wsl: 217 | varnamelen: 218 | testifylint: 219 | asasalint: 220 | bidichk: 221 | prealloc: 222 | protogetter: 223 | staticcheck: 224 | errorlint: 225 | misspell: 226 | spancheck: 227 | thelper: 228 | testpackage: 229 | gosec: 230 | godox: 231 | tagalign: 232 | unconvert: 233 | unused: 234 | usestdlibvars: 235 | depguard: 236 | paralleltest: 237 | godot: 238 | gosimple: 239 | gocyclo: 240 | predeclared: 241 | importas: 242 | gofumpt: 243 | errchkjson: 244 | musttag: 245 | goconst: 246 | reassign: 247 | dogsled: 248 | nilnil: 249 | unparam: 250 | ginkgolinter: 251 | goheader: 252 | promlinter: 253 | nlreturn: 254 | dupl: 255 | forbidigo: 256 | ireturn: 257 | nonamedreturns: 258 | gomoddirectives: 259 | custom: 260 | tagliatelle: 261 | wrapcheck: 262 | lll: 263 | makezero: 264 | copyloopvar: 265 | dupword: 266 | gosmopolitan: 267 | gofmt: 268 | stylecheck: 269 | whitespace: 270 | interfacebloat: 271 | goimports: 272 | tenv: 273 | # The option `all` will run against whole test files (`_test.go`) regardless of method/function signatures. 274 | # Otherwise, only methods that take `*testing.T`, `*testing.B`, and `testing.TB` as arguments are checked. 275 | # Default: false 276 | all: true 277 | 278 | 279 | linters: 280 | disable-all: true 281 | enable: 282 | ## enabled by default 283 | - errcheck # checking for unchecked errors, these unchecked errors can be critical bugs in some cases 284 | - gosimple # specializes in simplifying a code 285 | - govet # reports suspicious constructs, such as Printf calls whose arguments do not align with the format string 286 | - ineffassign # detects when assignments to existing variables are not used 287 | - staticcheck # is a go vet on steroids, applying a ton of static analysis checks 288 | - typecheck # like the front-end of a Go compiler, parses and type-checks Go code 289 | - unused # checks for unused constants, variables, functions and types 290 | ## disabled by default 291 | - asasalint # checks for pass []any as any in variadic func(...any) 292 | - asciicheck # checks that your code does not contain non-ASCII identifiers 293 | - bidichk # checks for dangerous unicode character sequences 294 | - bodyclose # checks whether HTTP response body is closed successfully 295 | - canonicalheader # checks whether net/http.Header uses canonical header 296 | - copyloopvar # detects places where loop variables are copied (Go 1.22+) 297 | - cyclop # checks function and package cyclomatic complexity 298 | - dupl # tool for code clone detection 299 | - durationcheck # checks for two durations multiplied together 300 | - errname # checks that sentinel errors are prefixed with the Err and error types are suffixed with the Error 301 | - errorlint # finds code that will cause problems with the error wrapping scheme introduced in Go 1.13 302 | - exhaustive # checks exhaustiveness of enum switch statements 303 | - fatcontext # detects nested contexts in loops 304 | - forbidigo # forbids identifiers 305 | - funlen # tool for detection of long functions 306 | - gocheckcompilerdirectives # validates go compiler directive comments (//go:) 307 | - gochecknoglobals # checks that no global variables exist 308 | - gochecknoinits # checks that no init functions are present in Go code 309 | - gochecksumtype # checks exhaustiveness on Go "sum types" 310 | - gocognit # computes and checks the cognitive complexity of functions 311 | - goconst # finds repeated strings that could be replaced by a constant 312 | - gocritic # provides diagnostics that check for bugs, performance and style issues 313 | - gocyclo # computes and checks the cyclomatic complexity of functions 314 | - godot # checks if comments end in a period 315 | - goimports # in addition to fixing imports, goimports also formats your code in the same style as gofmt 316 | - gomoddirectives # manages the use of 'replace', 'retract', and 'excludes' directives in go.mod 317 | - gomodguard # allow and block lists linter for direct Go module dependencies. This is different from depguard where there are different block types for example version constraints and module recommendations 318 | - goprintffuncname # checks that printf-like functions are named with f at the end 319 | - gosec # inspects source code for security problems 320 | - intrange # finds places where for loops could make use of an integer range 321 | - lll # reports long lines 322 | - loggercheck # checks key value pairs for common logger libraries (kitlog,klog,logr,zap) 323 | - makezero # finds slice declarations with non-zero initial length 324 | - mirror # reports wrong mirror patterns of bytes/strings usage 325 | - mnd # detects magic numbers 326 | - musttag # enforces field tags in (un)marshaled structs 327 | - nakedret # finds naked returns in functions greater than a specified function length 328 | - nestif # reports deeply nested if statements 329 | - nilerr # finds the code that returns nil even if it checks that the error is not nil 330 | - nilnil # checks that there is no simultaneous return of nil error and an invalid value 331 | - noctx # finds sending http request without context.Context 332 | - nolintlint # reports ill-formed or insufficient nolint directives 333 | - nonamedreturns # reports all named returns 334 | - nosprintfhostport # checks for misuse of Sprintf to construct a host with port in a URL 335 | - perfsprint # checks that fmt.Sprintf can be replaced with a faster alternative 336 | - predeclared # finds code that shadows one of Go's predeclared identifiers 337 | - promlinter # checks Prometheus metrics naming via promlint 338 | - protogetter # reports direct reads from proto message fields when getters should be used 339 | - reassign # checks that package variables are not reassigned 340 | - revive # fast, configurable, extensible, flexible, and beautiful linter for Go, drop-in replacement of golint 341 | - rowserrcheck # checks whether Err of rows is checked successfully 342 | - sloglint # ensure consistent code style when using log/slog 343 | - spancheck # checks for mistakes with OpenTelemetry/Census spans 344 | - sqlclosecheck # checks that sql.Rows and sql.Stmt are closed 345 | - stylecheck # is a replacement for golint 346 | - tenv # detects using os.Setenv instead of t.Setenv since Go1.17 347 | - testableexamples # checks if examples are testable (have an expected output) 348 | - testifylint # checks usage of github.com/stretchr/testify 349 | - testpackage # makes you use a separate _test package 350 | - tparallel # detects inappropriate usage of t.Parallel() method in your Go test codes 351 | - unconvert # removes unnecessary type conversions 352 | - unparam # reports unused function parameters 353 | - usestdlibvars # detects the possibility to use variables/constants from the Go standard library 354 | - wastedassign # finds wasted assignment statements 355 | - whitespace # detects leading and trailing whitespace 356 | 357 | ## you may want to enable 358 | #- decorder # checks declaration order and count of types, constants, variables and functions 359 | #- exhaustruct # [highly recommend to enable] checks if all structure fields are initialized 360 | #- gci # controls golang package import order and makes it always deterministic 361 | #- ginkgolinter # [if you use ginkgo/gomega] enforces standards of using ginkgo and gomega 362 | #- godox # detects FIXME, TODO and other comment keywords 363 | #- goheader # checks is file header matches to pattern 364 | #- inamedparam # [great idea, but too strict, need to ignore a lot of cases by default] reports interfaces with unnamed method parameters 365 | #- interfacebloat # checks the number of methods inside an interface 366 | #- ireturn # accept interfaces, return concrete types 367 | #- prealloc # [premature optimization, but can be used in some cases] finds slice declarations that could potentially be preallocated 368 | #- tagalign # checks that struct tags are well aligned 369 | #- varnamelen # [great idea, but too many false positives] checks that the length of a variable's name matches its scope 370 | #- wrapcheck # checks that errors returned from external packages are wrapped 371 | #- zerologlint # detects the wrong usage of zerolog that a user forgets to dispatch zerolog.Event 372 | 373 | ## disabled 374 | #- containedctx # detects struct contained context.Context field 375 | #- contextcheck # [too many false positives] checks the function whether use a non-inherited context 376 | #- depguard # [replaced by gomodguard] checks if package imports are in a list of acceptable packages 377 | #- dogsled # checks assignments with too many blank identifiers (e.g. x, _, _, _, := f()) 378 | #- dupword # [useless without config] checks for duplicate words in the source code 379 | #- err113 # [too strict] checks the errors handling expressions 380 | #- errchkjson # [don't see profit + I'm against of omitting errors like in the first example https://github.com/breml/errchkjson] checks types passed to the json encoding functions. Reports unsupported types and optionally reports occasions, where the check for the returned error can be omitted 381 | #- execinquery # [deprecated] checks query string in Query function which reads your Go src files and warning it finds 382 | #- exportloopref # [not necessary from Go 1.22] checks for pointers to enclosing loop variables 383 | #- forcetypeassert # [replaced by errcheck] finds forced type assertions 384 | #- gofmt # [replaced by goimports] checks whether code was gofmt-ed 385 | #- gofumpt # [replaced by goimports, gofumports is not available yet] checks whether code was gofumpt-ed 386 | #- gosmopolitan # reports certain i18n/l10n anti-patterns in your Go codebase 387 | #- grouper # analyzes expression groups 388 | #- importas # enforces consistent import aliases 389 | #- maintidx # measures the maintainability index of each function 390 | #- misspell # [useless] finds commonly misspelled English words in comments 391 | #- nlreturn # [too strict and mostly code is not more readable] checks for a new line before return and branch statements to increase code clarity 392 | #- paralleltest # [too many false positives] detects missing usage of t.Parallel() method in your Go test 393 | #- tagliatelle # checks the struct tags 394 | #- thelper # detects golang test helpers without t.Helper() call and checks the consistency of test helpers 395 | #- wsl # [too strict and mostly code is not more readable] whitespace linter forces you to use empty lines 396 | 397 | 398 | issues: 399 | # Maximum count of issues with the same text. 400 | # Set to 0 to disable. 401 | # Default: 3 402 | max-same-issues: 50 403 | 404 | exclude-rules: 405 | - source: "(noinspection|TODO)" 406 | linters: [ godot ] 407 | - source: "//noinspection" 408 | linters: [ gocritic ] 409 | - path: "_test\\.go" 410 | linters: 411 | - bodyclose 412 | - dupl 413 | - funlen 414 | - goconst 415 | - gosec 416 | - noctx 417 | - wrapcheck -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | repos: 2 | - repo: local 3 | hooks: 4 | - id: go-fmt 5 | name: go-fmt 6 | entry: sh -c 'go fmt ./...' 7 | language: system 8 | types: [go] 9 | 10 | - id: goimports 11 | name: goimports 12 | entry: sh -c 'find . -name "*.go" ! -path "./generated/*" -exec goimports -local go-echo-template/ -w {} +' 13 | language: system 14 | types: [go] 15 | 16 | - id: golines 17 | name: golines 18 | entry: sh -c 'find . -name "*.go" ! -path "./generated/*" -exec golines -w {} -m 120 \;' 19 | language: system 20 | types: [go] 21 | 22 | - id: golangci-lint 23 | name: golangci-lint 24 | entry: sh -c 'golangci-lint run ./...' 25 | language: system 26 | types: [go] 27 | 28 | - id: check-go-generate 29 | name: Check Go Generate 30 | entry: sh -c './check-go-generate.sh' 31 | language: system 32 | types: [go] 33 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | include .env 2 | export 3 | 4 | .PHONY: help unit_test integration_test test lint coverage_report cpu_profile mem_profile migrate_up migrate_down create_migration 5 | 6 | help: 7 | cat Makefile 8 | 9 | install: 10 | brew install protobuf 11 | go install golang.org/x/tools/cmd/goimports@latest 12 | go install github.com/golangci/golangci-lint/cmd/golangci-lint@latest 13 | go install github.com/segmentio/golines@latest 14 | go install github.com/oapi-codegen/oapi-codegen/v2/cmd/oapi-codegen@latest 15 | go install google.golang.org/protobuf/cmd/protoc-gen-go@latest 16 | go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@latest 17 | 18 | unit_test: 19 | go test -v ./internal/... 20 | 21 | integration_test: 22 | go test -v ./integration_test/... 23 | 24 | test: unit_test integration_test 25 | 26 | lint: 27 | go fmt ./... 28 | find . -name '*.go' ! -path "./generated/*" -exec goimports -local go-echo-template/ -w {} + 29 | find . -name '*.go' ! -path "./generated/*" -exec golines -w {} -m 120 \; 30 | golangci-lint run ./... 31 | ./check-go-generate.sh 32 | 33 | coverage_report: 34 | # TODO: fix test execution in 1 thread 35 | go test -p=1 -coverpkg=./... -count=1 -coverprofile=.coverage.out ./... 36 | go tool cover -html .coverage.out -o .coverage.html 37 | open ./.coverage.html 38 | 39 | cpu_profile: 40 | go test -cpuprofile=profiles/cpu.prof ./e2e_test 41 | go tool pprof -http=:6061 profiles/cpu.prof 42 | 43 | mem_profile: 44 | go test -memprofile=profiles/mem.prof ./e2e_test 45 | go tool pprof -http=:6061 profiles/mem.prof 46 | 47 | DB_URL=postgres://$(POSTGRES_USER):$(POSTGRES_PASSWORD)@$(POSTGRES_HOSTS):$(POSTGRES_PORT)/$(POSTGRES_DATABASE)?sslmode=$(if $(filter $(POSTGRES_SSL),true),require,disable) 48 | 49 | migrate_up: 50 | migrate -path migrations -database "$(DB_URL)" up 51 | 52 | migrate_down: 53 | migrate -path migrations -database "$(DB_URL)" down $(count) 54 | 55 | create_migration: 56 | migrate create -ext sql -dir migrations $(name) 57 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # go-echo-template 2 | 3 | The project is a template for Go repositories using the Echo framework. 4 | Contains examples of the code structure for a project with a domain-driven design (DDD) approach. 5 | 6 | 7 | ## Настойка dev окружения 8 | 9 | ### Go 10 | 11 | goenv — это инструмент для управления версиями Go, который поддерживает множество операционных систем, включая Windows. 12 | 13 | ```sh 14 | brew install goenv 15 | ``` 16 | 17 | После установки вам нужно будет инициализировать `goenv`. Добавьте следующие строки в ваш файл профиля (`~/.bash_profile`, `~/.zshrc`, `~/.profile` и т.д.), чтобы инициализировать `goenv` при каждом открытии терминала: 18 | 19 | ```sh 20 | export GOENV_ROOT="$HOME/.goenv" 21 | export PATH="$GOENV_ROOT/bin:$PATH" 22 | eval "$(goenv init -)" 23 | ``` 24 | 25 | Не забудьте применить изменения в файле профиля, выполнив команду `source ~/.bash_profile` (или аналогичную, в зависимости от вашей оболочки). 26 | 27 | После установки `goenv`, вы можете устанавливать разные версии Go и переключаться между ними. Для установки новой версии используйте: 28 | ```sh 29 | goenv install 1.x.x 30 | ``` 31 | А для выбора версии Go для текущего проекта или глобально для пользователя: 32 | ```sh 33 | goenv local 1.x.x 34 | goenv global 1.x.x 35 | ``` 36 | 37 | Чтобы настроить конкретную версию `Go` для проекта в IDE (например GoLand), выберите в качестве `GOROOT` нужную версию в директории `~/.goenv/versions` 38 | 39 | ### Линтеры и кодогенерация 40 | 41 | Для запуска `make lint` и `go generate ./...` необходимо установить следующие утилиты: 42 | 43 | ```sh 44 | brew install protobuf 45 | go install golang.org/x/tools/cmd/goimports@latest 46 | go install github.com/segmentio/golines@latest 47 | go install github.com/oapi-codegen/oapi-codegen/v2/cmd/oapi-codegen@latest 48 | go install github.com/golangci/golangci-lint/cmd/golangci-lint@latest 49 | go install google.golang.org/protobuf/cmd/protoc-gen-go@latest 50 | go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@latest 51 | ``` 52 | 53 | либо просто выполнить команду `make install` 54 | 55 | ### Pre-commit hooks 56 | 57 | Для установки pre-commit hooks выполните команды: 58 | 59 | ```sh 60 | brew install pre-commit 61 | pre-commit install 62 | ``` 63 | 64 | ### Golang-migrate 65 | 66 | Для выполнения миграций базы данных используется утилита `golang-migrate`. 67 | Чтобы установить утилиту, выполните команду: 68 | 69 | ```shell 70 | brew install golang-migrate 71 | ``` 72 | 73 | Чтобы применить миграции к базе, существует make команда: 74 | 75 | ```shell 76 | make migrate_up 77 | ``` 78 | 79 | Также для отката миграций (параметр `count` указывает количество миграций, которые нужно откатить): 80 | ```shell 81 | make migrate_down count=1 82 | ``` 83 | 84 | Для создания новой миграции используйте команду: 85 | ```shell 86 | make create_migration name=migration_name 87 | ``` 88 | -------------------------------------------------------------------------------- /api/openapi.yaml: -------------------------------------------------------------------------------- 1 | openapi: 3.0.3 2 | info: 3 | title: Go echo ddd template API 4 | version: 0.0.1 5 | paths: 6 | /users: 7 | post: 8 | summary: Create a new user 9 | description: Create a new user with the provided name and email 10 | tags: 11 | - users 12 | requestBody: 13 | required: true 14 | content: 15 | application/json: 16 | schema: 17 | $ref: '#/components/schemas/CreateUserRequest' 18 | responses: 19 | '201': 20 | description: User successfully created 21 | content: 22 | application/json: 23 | schema: 24 | $ref: '#/components/schemas/CreateUserResponse' 25 | '400': 26 | description: Invalid input data 27 | content: 28 | application/json: 29 | schema: 30 | $ref: '#/components/schemas/ErrorResponse' 31 | '409': 32 | description: User already exists 33 | content: 34 | application/json: 35 | schema: 36 | $ref: '#/components/schemas/ErrorResponse' 37 | '500': 38 | description: Internal server error 39 | content: 40 | application/json: 41 | schema: 42 | $ref: '#/components/schemas/ErrorResponse' 43 | /users/{id}: 44 | get: 45 | summary: Get a user by ID 46 | description: Retrieves user details using the user ID provided in the path 47 | tags: 48 | - users 49 | parameters: 50 | - in: path 51 | name: id 52 | required: true 53 | schema: 54 | type: string 55 | format: uuid 56 | description: User ID 57 | responses: 58 | '200': 59 | description: User details successfully retrieved 60 | content: 61 | application/json: 62 | schema: 63 | $ref: '#/components/schemas/GetUserResponse' 64 | '400': 65 | description: Invalid user ID 66 | content: 67 | application/json: 68 | schema: 69 | $ref: '#/components/schemas/ErrorResponse' 70 | '404': 71 | description: User not found 72 | content: 73 | application/json: 74 | schema: 75 | $ref: '#/components/schemas/ErrorResponse' 76 | '500': 77 | description: Failed to get user 78 | content: 79 | application/json: 80 | schema: 81 | $ref: '#/components/schemas/ErrorResponse' 82 | 83 | /orders: 84 | post: 85 | summary: Create an order 86 | description: Creates an order with provided items. 87 | tags: 88 | - orders 89 | requestBody: 90 | required: true 91 | content: 92 | application/json: 93 | schema: 94 | $ref: '#/components/schemas/CreateOrderRequest' 95 | responses: 96 | '201': 97 | description: Order successfully created. 98 | content: 99 | application/json: 100 | schema: 101 | $ref: '#/components/schemas/CreateOrderResponse' 102 | '400': 103 | description: Invalid request data. 104 | content: 105 | application/json: 106 | schema: 107 | $ref: '#/components/schemas/ErrorResponse' 108 | '404': 109 | description: User or product not found. 110 | content: 111 | application/json: 112 | schema: 113 | $ref: '#/components/schemas/ErrorResponse' 114 | '409': 115 | description: Products already reserved. 116 | content: 117 | application/json: 118 | schema: 119 | $ref: '#/components/schemas/ErrorResponse' 120 | '500': 121 | description: Internal server error. 122 | content: 123 | application/json: 124 | schema: 125 | $ref: '#/components/schemas/ErrorResponse' 126 | 127 | components: 128 | schemas: 129 | CreateUserRequest: 130 | type: object 131 | required: 132 | - name 133 | - email 134 | properties: 135 | name: 136 | type: string 137 | email: 138 | type: string 139 | format: email 140 | CreateUserResponse: 141 | type: object 142 | properties: 143 | id: 144 | type: string 145 | format: uuid 146 | GetUserResponse: 147 | type: object 148 | properties: 149 | id: 150 | type: string 151 | format: uuid 152 | name: 153 | type: string 154 | email: 155 | type: string 156 | format: email 157 | CreateOrderRequest: 158 | type: object 159 | required: 160 | - items 161 | properties: 162 | items: 163 | type: array 164 | items: 165 | $ref: '#/components/schemas/OrderItem' 166 | OrderItem: 167 | type: object 168 | properties: 169 | id: 170 | type: string 171 | format: uuid 172 | CreateOrderResponse: 173 | type: object 174 | properties: 175 | id: 176 | type: string 177 | format: uuid 178 | ErrorResponse: 179 | type: object 180 | properties: 181 | message: 182 | type: string -------------------------------------------------------------------------------- /api/protobuf.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | package protobuf; 4 | 5 | option go_package = "go-echo-template/generated/protobuf;protobuf"; 6 | 7 | message CreateUserRequest { 8 | string name = 1; 9 | string email = 2; 10 | } 11 | 12 | message CreateUserResponse { 13 | string id = 1; 14 | } 15 | 16 | message GetUserRequest { 17 | string id = 1; 18 | } 19 | 20 | message GetUserResponse { 21 | string id = 1; 22 | string name = 2; 23 | string email = 3; 24 | } 25 | 26 | message CreateOrderRequest { 27 | repeated OrderItem items = 1; 28 | } 29 | 30 | message OrderItem { 31 | string id = 1; 32 | } 33 | 34 | message CreateOrderResponse { 35 | string id = 1; 36 | } 37 | 38 | service UserService { 39 | rpc CreateUser(CreateUserRequest) returns (CreateUserResponse); 40 | rpc GetUser(GetUserRequest) returns (GetUserResponse); 41 | } 42 | 43 | service OrderService { 44 | rpc CreateOrder(CreateOrderRequest) returns (CreateOrderResponse); 45 | } 46 | -------------------------------------------------------------------------------- /bin/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gonozov0/go-ddd-template/465380bd0ab9f8ddd2e7a975bf612b285f566cda/bin/.gitkeep -------------------------------------------------------------------------------- /build/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM debian:bookworm-slim 2 | 3 | RUN apt-get update && apt-get install -y --no-install-recommends ca-certificates 4 | 5 | COPY ./bin/your-service-name /your-service-name 6 | 7 | CMD ["/your-service-name"] -------------------------------------------------------------------------------- /check-go-generate.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | temp_dir=$(mktemp -d) 4 | cp -r ./generated/* "$temp_dir" 5 | 6 | go generate ./... 7 | 8 | diff -r "$temp_dir" ./generated 9 | if [ $? -ne 0 ]; then 10 | echo "Generated files have changes. Please update them in your local repository." 11 | exit 1 12 | fi 13 | 14 | rm -rf "$temp_dir" 15 | -------------------------------------------------------------------------------- /cmd/migrator/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "log/slog" 5 | "os" 6 | 7 | "go-echo-template/internal" 8 | ) 9 | 10 | func main() { 11 | cfg, err := internal.LoadConfig() 12 | if err != nil { 13 | slog.Error("Could not load config", "err", err) 14 | os.Exit(1) 15 | } 16 | if err := internal.Migrate(cfg); err != nil { 17 | slog.Error("Failed to migrate", "err", err) 18 | os.Exit(1) 19 | } 20 | 21 | slog.Info("Migrations applied successfully") 22 | } 23 | -------------------------------------------------------------------------------- /cmd/server/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "log/slog" 5 | "os" 6 | 7 | "go-echo-template/internal" 8 | ) 9 | 10 | func main() { 11 | cfg, err := internal.LoadConfig() 12 | if err != nil { 13 | slog.Error("Could not load config", "err", err) 14 | os.Exit(1) 15 | } 16 | if err := internal.Run(cfg); err != nil { 17 | slog.Error("Failed to run server", "err", err) 18 | os.Exit(1) 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | services: 2 | redis: 3 | image: redis 4 | ports: 5 | - "6379:6379" 6 | 7 | postgres: 8 | image: postgres 9 | ports: 10 | - "5432:5432" 11 | environment: 12 | POSTGRES_USER: postgres 13 | POSTGRES_PASSWORD: postgres 14 | POSTGRES_DB: postgres 15 | POSTGRES_MAX_CONNECTIONS: 100 16 | volumes: 17 | - postgres_data:/var/lib/postgresql/data 18 | 19 | volumes: 20 | postgres_data: 21 | -------------------------------------------------------------------------------- /generated/generate.go: -------------------------------------------------------------------------------- 1 | package generated 2 | 3 | //go:generate oapi-codegen -generate types -package openapi -o ./openapi/types.go ../api/openapi.yaml 4 | //go:generate oapi-codegen -generate server -package openapi -o ./openapi/server.go ../api/openapi.yaml 5 | //go:generate oapi-codegen -generate client -package openapi -o ./openapi/client.go ../api/openapi.yaml 6 | //go:generate oapi-codegen -generate spec -package openapi -o ./openapi/spec.go ../api/openapi.yaml 7 | 8 | //go:generate protoc --proto_path=../api --go_out=./protobuf/ --go_opt=paths=source_relative --go-grpc_out=./protobuf/ --go-grpc_opt=paths=source_relative protobuf.proto 9 | -------------------------------------------------------------------------------- /generated/openapi/client.go: -------------------------------------------------------------------------------- 1 | // Package openapi provides primitives to interact with the openapi HTTP API. 2 | // 3 | // Code generated by github.com/oapi-codegen/oapi-codegen/v2 version v2.4.0 DO NOT EDIT. 4 | package openapi 5 | 6 | import ( 7 | "bytes" 8 | "context" 9 | "encoding/json" 10 | "fmt" 11 | "io" 12 | "net/http" 13 | "net/url" 14 | "strings" 15 | 16 | "github.com/oapi-codegen/runtime" 17 | openapi_types "github.com/oapi-codegen/runtime/types" 18 | ) 19 | 20 | // RequestEditorFn is the function signature for the RequestEditor callback function 21 | type RequestEditorFn func(ctx context.Context, req *http.Request) error 22 | 23 | // Doer performs HTTP requests. 24 | // 25 | // The standard http.Client implements this interface. 26 | type HttpRequestDoer interface { 27 | Do(req *http.Request) (*http.Response, error) 28 | } 29 | 30 | // Client which conforms to the OpenAPI3 specification for this service. 31 | type Client struct { 32 | // The endpoint of the server conforming to this interface, with scheme, 33 | // https://api.deepmap.com for example. This can contain a path relative 34 | // to the server, such as https://api.deepmap.com/dev-test, and all the 35 | // paths in the swagger spec will be appended to the server. 36 | Server string 37 | 38 | // Doer for performing requests, typically a *http.Client with any 39 | // customized settings, such as certificate chains. 40 | Client HttpRequestDoer 41 | 42 | // A list of callbacks for modifying requests which are generated before sending over 43 | // the network. 44 | RequestEditors []RequestEditorFn 45 | } 46 | 47 | // ClientOption allows setting custom parameters during construction 48 | type ClientOption func(*Client) error 49 | 50 | // Creates a new Client, with reasonable defaults 51 | func NewClient(server string, opts ...ClientOption) (*Client, error) { 52 | // create a client with sane default values 53 | client := Client{ 54 | Server: server, 55 | } 56 | // mutate client and add all optional params 57 | for _, o := range opts { 58 | if err := o(&client); err != nil { 59 | return nil, err 60 | } 61 | } 62 | // ensure the server URL always has a trailing slash 63 | if !strings.HasSuffix(client.Server, "/") { 64 | client.Server += "/" 65 | } 66 | // create httpClient, if not already present 67 | if client.Client == nil { 68 | client.Client = &http.Client{} 69 | } 70 | return &client, nil 71 | } 72 | 73 | // WithHTTPClient allows overriding the default Doer, which is 74 | // automatically created using http.Client. This is useful for tests. 75 | func WithHTTPClient(doer HttpRequestDoer) ClientOption { 76 | return func(c *Client) error { 77 | c.Client = doer 78 | return nil 79 | } 80 | } 81 | 82 | // WithRequestEditorFn allows setting up a callback function, which will be 83 | // called right before sending the request. This can be used to mutate the request. 84 | func WithRequestEditorFn(fn RequestEditorFn) ClientOption { 85 | return func(c *Client) error { 86 | c.RequestEditors = append(c.RequestEditors, fn) 87 | return nil 88 | } 89 | } 90 | 91 | // The interface specification for the client above. 92 | type ClientInterface interface { 93 | // PostOrdersWithBody request with any body 94 | PostOrdersWithBody(ctx context.Context, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*http.Response, error) 95 | 96 | PostOrders(ctx context.Context, body PostOrdersJSONRequestBody, reqEditors ...RequestEditorFn) (*http.Response, error) 97 | 98 | // PostUsersWithBody request with any body 99 | PostUsersWithBody(ctx context.Context, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*http.Response, error) 100 | 101 | PostUsers(ctx context.Context, body PostUsersJSONRequestBody, reqEditors ...RequestEditorFn) (*http.Response, error) 102 | 103 | // GetUsersId request 104 | GetUsersId(ctx context.Context, id openapi_types.UUID, reqEditors ...RequestEditorFn) (*http.Response, error) 105 | } 106 | 107 | func (c *Client) PostOrdersWithBody(ctx context.Context, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*http.Response, error) { 108 | req, err := NewPostOrdersRequestWithBody(c.Server, contentType, body) 109 | if err != nil { 110 | return nil, err 111 | } 112 | req = req.WithContext(ctx) 113 | if err := c.applyEditors(ctx, req, reqEditors); err != nil { 114 | return nil, err 115 | } 116 | return c.Client.Do(req) 117 | } 118 | 119 | func (c *Client) PostOrders(ctx context.Context, body PostOrdersJSONRequestBody, reqEditors ...RequestEditorFn) (*http.Response, error) { 120 | req, err := NewPostOrdersRequest(c.Server, body) 121 | if err != nil { 122 | return nil, err 123 | } 124 | req = req.WithContext(ctx) 125 | if err := c.applyEditors(ctx, req, reqEditors); err != nil { 126 | return nil, err 127 | } 128 | return c.Client.Do(req) 129 | } 130 | 131 | func (c *Client) PostUsersWithBody(ctx context.Context, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*http.Response, error) { 132 | req, err := NewPostUsersRequestWithBody(c.Server, contentType, body) 133 | if err != nil { 134 | return nil, err 135 | } 136 | req = req.WithContext(ctx) 137 | if err := c.applyEditors(ctx, req, reqEditors); err != nil { 138 | return nil, err 139 | } 140 | return c.Client.Do(req) 141 | } 142 | 143 | func (c *Client) PostUsers(ctx context.Context, body PostUsersJSONRequestBody, reqEditors ...RequestEditorFn) (*http.Response, error) { 144 | req, err := NewPostUsersRequest(c.Server, body) 145 | if err != nil { 146 | return nil, err 147 | } 148 | req = req.WithContext(ctx) 149 | if err := c.applyEditors(ctx, req, reqEditors); err != nil { 150 | return nil, err 151 | } 152 | return c.Client.Do(req) 153 | } 154 | 155 | func (c *Client) GetUsersId(ctx context.Context, id openapi_types.UUID, reqEditors ...RequestEditorFn) (*http.Response, error) { 156 | req, err := NewGetUsersIdRequest(c.Server, id) 157 | if err != nil { 158 | return nil, err 159 | } 160 | req = req.WithContext(ctx) 161 | if err := c.applyEditors(ctx, req, reqEditors); err != nil { 162 | return nil, err 163 | } 164 | return c.Client.Do(req) 165 | } 166 | 167 | // NewPostOrdersRequest calls the generic PostOrders builder with application/json body 168 | func NewPostOrdersRequest(server string, body PostOrdersJSONRequestBody) (*http.Request, error) { 169 | var bodyReader io.Reader 170 | buf, err := json.Marshal(body) 171 | if err != nil { 172 | return nil, err 173 | } 174 | bodyReader = bytes.NewReader(buf) 175 | return NewPostOrdersRequestWithBody(server, "application/json", bodyReader) 176 | } 177 | 178 | // NewPostOrdersRequestWithBody generates requests for PostOrders with any type of body 179 | func NewPostOrdersRequestWithBody(server string, contentType string, body io.Reader) (*http.Request, error) { 180 | var err error 181 | 182 | serverURL, err := url.Parse(server) 183 | if err != nil { 184 | return nil, err 185 | } 186 | 187 | operationPath := fmt.Sprintf("/orders") 188 | if operationPath[0] == '/' { 189 | operationPath = "." + operationPath 190 | } 191 | 192 | queryURL, err := serverURL.Parse(operationPath) 193 | if err != nil { 194 | return nil, err 195 | } 196 | 197 | req, err := http.NewRequest("POST", queryURL.String(), body) 198 | if err != nil { 199 | return nil, err 200 | } 201 | 202 | req.Header.Add("Content-Type", contentType) 203 | 204 | return req, nil 205 | } 206 | 207 | // NewPostUsersRequest calls the generic PostUsers builder with application/json body 208 | func NewPostUsersRequest(server string, body PostUsersJSONRequestBody) (*http.Request, error) { 209 | var bodyReader io.Reader 210 | buf, err := json.Marshal(body) 211 | if err != nil { 212 | return nil, err 213 | } 214 | bodyReader = bytes.NewReader(buf) 215 | return NewPostUsersRequestWithBody(server, "application/json", bodyReader) 216 | } 217 | 218 | // NewPostUsersRequestWithBody generates requests for PostUsers with any type of body 219 | func NewPostUsersRequestWithBody(server string, contentType string, body io.Reader) (*http.Request, error) { 220 | var err error 221 | 222 | serverURL, err := url.Parse(server) 223 | if err != nil { 224 | return nil, err 225 | } 226 | 227 | operationPath := fmt.Sprintf("/users") 228 | if operationPath[0] == '/' { 229 | operationPath = "." + operationPath 230 | } 231 | 232 | queryURL, err := serverURL.Parse(operationPath) 233 | if err != nil { 234 | return nil, err 235 | } 236 | 237 | req, err := http.NewRequest("POST", queryURL.String(), body) 238 | if err != nil { 239 | return nil, err 240 | } 241 | 242 | req.Header.Add("Content-Type", contentType) 243 | 244 | return req, nil 245 | } 246 | 247 | // NewGetUsersIdRequest generates requests for GetUsersId 248 | func NewGetUsersIdRequest(server string, id openapi_types.UUID) (*http.Request, error) { 249 | var err error 250 | 251 | var pathParam0 string 252 | 253 | pathParam0, err = runtime.StyleParamWithLocation("simple", false, "id", runtime.ParamLocationPath, id) 254 | if err != nil { 255 | return nil, err 256 | } 257 | 258 | serverURL, err := url.Parse(server) 259 | if err != nil { 260 | return nil, err 261 | } 262 | 263 | operationPath := fmt.Sprintf("/users/%s", pathParam0) 264 | if operationPath[0] == '/' { 265 | operationPath = "." + operationPath 266 | } 267 | 268 | queryURL, err := serverURL.Parse(operationPath) 269 | if err != nil { 270 | return nil, err 271 | } 272 | 273 | req, err := http.NewRequest("GET", queryURL.String(), nil) 274 | if err != nil { 275 | return nil, err 276 | } 277 | 278 | return req, nil 279 | } 280 | 281 | func (c *Client) applyEditors(ctx context.Context, req *http.Request, additionalEditors []RequestEditorFn) error { 282 | for _, r := range c.RequestEditors { 283 | if err := r(ctx, req); err != nil { 284 | return err 285 | } 286 | } 287 | for _, r := range additionalEditors { 288 | if err := r(ctx, req); err != nil { 289 | return err 290 | } 291 | } 292 | return nil 293 | } 294 | 295 | // ClientWithResponses builds on ClientInterface to offer response payloads 296 | type ClientWithResponses struct { 297 | ClientInterface 298 | } 299 | 300 | // NewClientWithResponses creates a new ClientWithResponses, which wraps 301 | // Client with return type handling 302 | func NewClientWithResponses(server string, opts ...ClientOption) (*ClientWithResponses, error) { 303 | client, err := NewClient(server, opts...) 304 | if err != nil { 305 | return nil, err 306 | } 307 | return &ClientWithResponses{client}, nil 308 | } 309 | 310 | // WithBaseURL overrides the baseURL. 311 | func WithBaseURL(baseURL string) ClientOption { 312 | return func(c *Client) error { 313 | newBaseURL, err := url.Parse(baseURL) 314 | if err != nil { 315 | return err 316 | } 317 | c.Server = newBaseURL.String() 318 | return nil 319 | } 320 | } 321 | 322 | // ClientWithResponsesInterface is the interface specification for the client with responses above. 323 | type ClientWithResponsesInterface interface { 324 | // PostOrdersWithBodyWithResponse request with any body 325 | PostOrdersWithBodyWithResponse(ctx context.Context, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*PostOrdersResponse, error) 326 | 327 | PostOrdersWithResponse(ctx context.Context, body PostOrdersJSONRequestBody, reqEditors ...RequestEditorFn) (*PostOrdersResponse, error) 328 | 329 | // PostUsersWithBodyWithResponse request with any body 330 | PostUsersWithBodyWithResponse(ctx context.Context, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*PostUsersResponse, error) 331 | 332 | PostUsersWithResponse(ctx context.Context, body PostUsersJSONRequestBody, reqEditors ...RequestEditorFn) (*PostUsersResponse, error) 333 | 334 | // GetUsersIdWithResponse request 335 | GetUsersIdWithResponse(ctx context.Context, id openapi_types.UUID, reqEditors ...RequestEditorFn) (*GetUsersIdResponse, error) 336 | } 337 | 338 | type PostOrdersResponse struct { 339 | Body []byte 340 | HTTPResponse *http.Response 341 | JSON201 *CreateOrderResponse 342 | JSON400 *ErrorResponse 343 | JSON404 *ErrorResponse 344 | JSON409 *ErrorResponse 345 | JSON500 *ErrorResponse 346 | } 347 | 348 | // Status returns HTTPResponse.Status 349 | func (r PostOrdersResponse) Status() string { 350 | if r.HTTPResponse != nil { 351 | return r.HTTPResponse.Status 352 | } 353 | return http.StatusText(0) 354 | } 355 | 356 | // StatusCode returns HTTPResponse.StatusCode 357 | func (r PostOrdersResponse) StatusCode() int { 358 | if r.HTTPResponse != nil { 359 | return r.HTTPResponse.StatusCode 360 | } 361 | return 0 362 | } 363 | 364 | type PostUsersResponse struct { 365 | Body []byte 366 | HTTPResponse *http.Response 367 | JSON201 *CreateUserResponse 368 | JSON400 *ErrorResponse 369 | JSON409 *ErrorResponse 370 | JSON500 *ErrorResponse 371 | } 372 | 373 | // Status returns HTTPResponse.Status 374 | func (r PostUsersResponse) Status() string { 375 | if r.HTTPResponse != nil { 376 | return r.HTTPResponse.Status 377 | } 378 | return http.StatusText(0) 379 | } 380 | 381 | // StatusCode returns HTTPResponse.StatusCode 382 | func (r PostUsersResponse) StatusCode() int { 383 | if r.HTTPResponse != nil { 384 | return r.HTTPResponse.StatusCode 385 | } 386 | return 0 387 | } 388 | 389 | type GetUsersIdResponse struct { 390 | Body []byte 391 | HTTPResponse *http.Response 392 | JSON200 *GetUserResponse 393 | JSON400 *ErrorResponse 394 | JSON404 *ErrorResponse 395 | JSON500 *ErrorResponse 396 | } 397 | 398 | // Status returns HTTPResponse.Status 399 | func (r GetUsersIdResponse) Status() string { 400 | if r.HTTPResponse != nil { 401 | return r.HTTPResponse.Status 402 | } 403 | return http.StatusText(0) 404 | } 405 | 406 | // StatusCode returns HTTPResponse.StatusCode 407 | func (r GetUsersIdResponse) StatusCode() int { 408 | if r.HTTPResponse != nil { 409 | return r.HTTPResponse.StatusCode 410 | } 411 | return 0 412 | } 413 | 414 | // PostOrdersWithBodyWithResponse request with arbitrary body returning *PostOrdersResponse 415 | func (c *ClientWithResponses) PostOrdersWithBodyWithResponse(ctx context.Context, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*PostOrdersResponse, error) { 416 | rsp, err := c.PostOrdersWithBody(ctx, contentType, body, reqEditors...) 417 | if err != nil { 418 | return nil, err 419 | } 420 | return ParsePostOrdersResponse(rsp) 421 | } 422 | 423 | func (c *ClientWithResponses) PostOrdersWithResponse(ctx context.Context, body PostOrdersJSONRequestBody, reqEditors ...RequestEditorFn) (*PostOrdersResponse, error) { 424 | rsp, err := c.PostOrders(ctx, body, reqEditors...) 425 | if err != nil { 426 | return nil, err 427 | } 428 | return ParsePostOrdersResponse(rsp) 429 | } 430 | 431 | // PostUsersWithBodyWithResponse request with arbitrary body returning *PostUsersResponse 432 | func (c *ClientWithResponses) PostUsersWithBodyWithResponse(ctx context.Context, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*PostUsersResponse, error) { 433 | rsp, err := c.PostUsersWithBody(ctx, contentType, body, reqEditors...) 434 | if err != nil { 435 | return nil, err 436 | } 437 | return ParsePostUsersResponse(rsp) 438 | } 439 | 440 | func (c *ClientWithResponses) PostUsersWithResponse(ctx context.Context, body PostUsersJSONRequestBody, reqEditors ...RequestEditorFn) (*PostUsersResponse, error) { 441 | rsp, err := c.PostUsers(ctx, body, reqEditors...) 442 | if err != nil { 443 | return nil, err 444 | } 445 | return ParsePostUsersResponse(rsp) 446 | } 447 | 448 | // GetUsersIdWithResponse request returning *GetUsersIdResponse 449 | func (c *ClientWithResponses) GetUsersIdWithResponse(ctx context.Context, id openapi_types.UUID, reqEditors ...RequestEditorFn) (*GetUsersIdResponse, error) { 450 | rsp, err := c.GetUsersId(ctx, id, reqEditors...) 451 | if err != nil { 452 | return nil, err 453 | } 454 | return ParseGetUsersIdResponse(rsp) 455 | } 456 | 457 | // ParsePostOrdersResponse parses an HTTP response from a PostOrdersWithResponse call 458 | func ParsePostOrdersResponse(rsp *http.Response) (*PostOrdersResponse, error) { 459 | bodyBytes, err := io.ReadAll(rsp.Body) 460 | defer func() { _ = rsp.Body.Close() }() 461 | if err != nil { 462 | return nil, err 463 | } 464 | 465 | response := &PostOrdersResponse{ 466 | Body: bodyBytes, 467 | HTTPResponse: rsp, 468 | } 469 | 470 | switch { 471 | case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 201: 472 | var dest CreateOrderResponse 473 | if err := json.Unmarshal(bodyBytes, &dest); err != nil { 474 | return nil, err 475 | } 476 | response.JSON201 = &dest 477 | 478 | case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 400: 479 | var dest ErrorResponse 480 | if err := json.Unmarshal(bodyBytes, &dest); err != nil { 481 | return nil, err 482 | } 483 | response.JSON400 = &dest 484 | 485 | case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 404: 486 | var dest ErrorResponse 487 | if err := json.Unmarshal(bodyBytes, &dest); err != nil { 488 | return nil, err 489 | } 490 | response.JSON404 = &dest 491 | 492 | case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 409: 493 | var dest ErrorResponse 494 | if err := json.Unmarshal(bodyBytes, &dest); err != nil { 495 | return nil, err 496 | } 497 | response.JSON409 = &dest 498 | 499 | case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 500: 500 | var dest ErrorResponse 501 | if err := json.Unmarshal(bodyBytes, &dest); err != nil { 502 | return nil, err 503 | } 504 | response.JSON500 = &dest 505 | 506 | } 507 | 508 | return response, nil 509 | } 510 | 511 | // ParsePostUsersResponse parses an HTTP response from a PostUsersWithResponse call 512 | func ParsePostUsersResponse(rsp *http.Response) (*PostUsersResponse, error) { 513 | bodyBytes, err := io.ReadAll(rsp.Body) 514 | defer func() { _ = rsp.Body.Close() }() 515 | if err != nil { 516 | return nil, err 517 | } 518 | 519 | response := &PostUsersResponse{ 520 | Body: bodyBytes, 521 | HTTPResponse: rsp, 522 | } 523 | 524 | switch { 525 | case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 201: 526 | var dest CreateUserResponse 527 | if err := json.Unmarshal(bodyBytes, &dest); err != nil { 528 | return nil, err 529 | } 530 | response.JSON201 = &dest 531 | 532 | case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 400: 533 | var dest ErrorResponse 534 | if err := json.Unmarshal(bodyBytes, &dest); err != nil { 535 | return nil, err 536 | } 537 | response.JSON400 = &dest 538 | 539 | case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 409: 540 | var dest ErrorResponse 541 | if err := json.Unmarshal(bodyBytes, &dest); err != nil { 542 | return nil, err 543 | } 544 | response.JSON409 = &dest 545 | 546 | case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 500: 547 | var dest ErrorResponse 548 | if err := json.Unmarshal(bodyBytes, &dest); err != nil { 549 | return nil, err 550 | } 551 | response.JSON500 = &dest 552 | 553 | } 554 | 555 | return response, nil 556 | } 557 | 558 | // ParseGetUsersIdResponse parses an HTTP response from a GetUsersIdWithResponse call 559 | func ParseGetUsersIdResponse(rsp *http.Response) (*GetUsersIdResponse, error) { 560 | bodyBytes, err := io.ReadAll(rsp.Body) 561 | defer func() { _ = rsp.Body.Close() }() 562 | if err != nil { 563 | return nil, err 564 | } 565 | 566 | response := &GetUsersIdResponse{ 567 | Body: bodyBytes, 568 | HTTPResponse: rsp, 569 | } 570 | 571 | switch { 572 | case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 200: 573 | var dest GetUserResponse 574 | if err := json.Unmarshal(bodyBytes, &dest); err != nil { 575 | return nil, err 576 | } 577 | response.JSON200 = &dest 578 | 579 | case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 400: 580 | var dest ErrorResponse 581 | if err := json.Unmarshal(bodyBytes, &dest); err != nil { 582 | return nil, err 583 | } 584 | response.JSON400 = &dest 585 | 586 | case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 404: 587 | var dest ErrorResponse 588 | if err := json.Unmarshal(bodyBytes, &dest); err != nil { 589 | return nil, err 590 | } 591 | response.JSON404 = &dest 592 | 593 | case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 500: 594 | var dest ErrorResponse 595 | if err := json.Unmarshal(bodyBytes, &dest); err != nil { 596 | return nil, err 597 | } 598 | response.JSON500 = &dest 599 | 600 | } 601 | 602 | return response, nil 603 | } 604 | -------------------------------------------------------------------------------- /generated/openapi/server.go: -------------------------------------------------------------------------------- 1 | // Package openapi provides primitives to interact with the openapi HTTP API. 2 | // 3 | // Code generated by github.com/oapi-codegen/oapi-codegen/v2 version v2.4.0 DO NOT EDIT. 4 | package openapi 5 | 6 | import ( 7 | "fmt" 8 | "net/http" 9 | 10 | "github.com/labstack/echo/v4" 11 | "github.com/oapi-codegen/runtime" 12 | openapi_types "github.com/oapi-codegen/runtime/types" 13 | ) 14 | 15 | // ServerInterface represents all server handlers. 16 | type ServerInterface interface { 17 | // Create an order 18 | // (POST /orders) 19 | PostOrders(ctx echo.Context) error 20 | // Create a new user 21 | // (POST /users) 22 | PostUsers(ctx echo.Context) error 23 | // Get a user by ID 24 | // (GET /users/{id}) 25 | GetUsersId(ctx echo.Context, id openapi_types.UUID) error 26 | } 27 | 28 | // ServerInterfaceWrapper converts echo contexts to parameters. 29 | type ServerInterfaceWrapper struct { 30 | Handler ServerInterface 31 | } 32 | 33 | // PostOrders converts echo context to params. 34 | func (w *ServerInterfaceWrapper) PostOrders(ctx echo.Context) error { 35 | var err error 36 | 37 | // Invoke the callback with all the unmarshaled arguments 38 | err = w.Handler.PostOrders(ctx) 39 | return err 40 | } 41 | 42 | // PostUsers converts echo context to params. 43 | func (w *ServerInterfaceWrapper) PostUsers(ctx echo.Context) error { 44 | var err error 45 | 46 | // Invoke the callback with all the unmarshaled arguments 47 | err = w.Handler.PostUsers(ctx) 48 | return err 49 | } 50 | 51 | // GetUsersId converts echo context to params. 52 | func (w *ServerInterfaceWrapper) GetUsersId(ctx echo.Context) error { 53 | var err error 54 | // ------------- Path parameter "id" ------------- 55 | var id openapi_types.UUID 56 | 57 | err = runtime.BindStyledParameterWithOptions("simple", "id", ctx.Param("id"), &id, runtime.BindStyledParameterOptions{ParamLocation: runtime.ParamLocationPath, Explode: false, Required: true}) 58 | if err != nil { 59 | return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Invalid format for parameter id: %s", err)) 60 | } 61 | 62 | // Invoke the callback with all the unmarshaled arguments 63 | err = w.Handler.GetUsersId(ctx, id) 64 | return err 65 | } 66 | 67 | // This is a simple interface which specifies echo.Route addition functions which 68 | // are present on both echo.Echo and echo.Group, since we want to allow using 69 | // either of them for path registration 70 | type EchoRouter interface { 71 | CONNECT(path string, h echo.HandlerFunc, m ...echo.MiddlewareFunc) *echo.Route 72 | DELETE(path string, h echo.HandlerFunc, m ...echo.MiddlewareFunc) *echo.Route 73 | GET(path string, h echo.HandlerFunc, m ...echo.MiddlewareFunc) *echo.Route 74 | HEAD(path string, h echo.HandlerFunc, m ...echo.MiddlewareFunc) *echo.Route 75 | OPTIONS(path string, h echo.HandlerFunc, m ...echo.MiddlewareFunc) *echo.Route 76 | PATCH(path string, h echo.HandlerFunc, m ...echo.MiddlewareFunc) *echo.Route 77 | POST(path string, h echo.HandlerFunc, m ...echo.MiddlewareFunc) *echo.Route 78 | PUT(path string, h echo.HandlerFunc, m ...echo.MiddlewareFunc) *echo.Route 79 | TRACE(path string, h echo.HandlerFunc, m ...echo.MiddlewareFunc) *echo.Route 80 | } 81 | 82 | // RegisterHandlers adds each server route to the EchoRouter. 83 | func RegisterHandlers(router EchoRouter, si ServerInterface) { 84 | RegisterHandlersWithBaseURL(router, si, "") 85 | } 86 | 87 | // Registers handlers, and prepends BaseURL to the paths, so that the paths 88 | // can be served under a prefix. 89 | func RegisterHandlersWithBaseURL(router EchoRouter, si ServerInterface, baseURL string) { 90 | 91 | wrapper := ServerInterfaceWrapper{ 92 | Handler: si, 93 | } 94 | 95 | router.POST(baseURL+"/orders", wrapper.PostOrders) 96 | router.POST(baseURL+"/users", wrapper.PostUsers) 97 | router.GET(baseURL+"/users/:id", wrapper.GetUsersId) 98 | 99 | } 100 | -------------------------------------------------------------------------------- /generated/openapi/spec.go: -------------------------------------------------------------------------------- 1 | // Package openapi provides primitives to interact with the openapi HTTP API. 2 | // 3 | // Code generated by github.com/oapi-codegen/oapi-codegen/v2 version v2.4.0 DO NOT EDIT. 4 | package openapi 5 | 6 | import ( 7 | "bytes" 8 | "compress/gzip" 9 | "encoding/base64" 10 | "fmt" 11 | "net/url" 12 | "path" 13 | "strings" 14 | 15 | "github.com/getkin/kin-openapi/openapi3" 16 | ) 17 | 18 | // Base64 encoded, gzipped, json marshaled Swagger object 19 | var swaggerSpec = []string{ 20 | 21 | "H4sIAAAAAAAC/8yWwW4iORCGX8Wq3SMCssketm87k5mIU6JIOUUcKu0CHHXbnXI1GYR495HthtDTQDIS", 22 | "gblEHVP+/btcX9lLyF1ZOUtWPGRL8PmMSoyfX5lQ6JY18T291OQljFbsKmIxFGOMUNn++JtpAhn8NXjT", 23 | "HTSig6g1Eiph1QNZVAQZIDMuYLXqAdNLbZg0ZI+N3HgT5Z6eKZcwreXKV8562mFLh78TxyUKZFDXRsNG", 24 | "ywsbO41L7lF/8Ae2TCWaoiWfRjr6PbBYRnPdhbf3GqN6jcr4HVNH3/E3ZndAtyTvcbpnFx2xG5LDNn8j", 25 | "eR/a0aEcd9y9ld+x0heGjJ24aMBIEX67cYrymVNaayVUVgUKqf/vRtCDObE3zkIGw/6wfxE8uYosVgYy", 26 | "uOwP+5fQgwplFj0NXPAbPyuXKlGTz9lUkkRSXXiFVsVQ9Wpkpip2c6NJq8hQH+IajGHOSEMGd87LbVJO", 27 | "dUhevji9CPq5s0I2LoVVVZg8Ths8+7Deujm8R/mOvrFq17xwTXEglUnc4j/Di89x0JRitNDOXwxQvs5z", 28 | "8n5SF8VC5XGm7oejuRoOj+aoTdkOLyM7x8Jo1RyI0ijYuLg6nYsAr3IcakjXuSjrRE1cbdcJ+e90Vu6S", 29 | "Ba+wYEK9UEyeeN6czb+nPRshtlioaIAVhQn92A98XZbIiw2MGxZD98CpD/29wXgc4ge1/wDSCpWlVxVi", 30 | "E9MyozeuQ8dTaLVat84u3w/+s/HeviLPQnfrntlXyrvYPhvaxlZ1AvvkMMVkrEGiH8aL/zMo2gPRpvy3", 31 | "MErkbFE0WBq9CramtIOkexI2NCefMNIkaIrwj7HTyFMcHl1v3Zc2cYYy60DVPGz8SMcbmrEkiSA/Lnel", 32 | "enQN4WEAGTRq6ZUC8VXRJqW3ld/33h/jDlXHO79fn277qmidyBZa3CT7fHA1p3meG3NzTZ4cqu9oCtJK", 33 | "nJqSJGLaRN2QKEzZeVqkuuwAtVr9DAAA///RrDICBg4AAA==", 34 | } 35 | 36 | // GetSwagger returns the content of the embedded swagger specification file 37 | // or error if failed to decode 38 | func decodeSpec() ([]byte, error) { 39 | zipped, err := base64.StdEncoding.DecodeString(strings.Join(swaggerSpec, "")) 40 | if err != nil { 41 | return nil, fmt.Errorf("error base64 decoding spec: %w", err) 42 | } 43 | zr, err := gzip.NewReader(bytes.NewReader(zipped)) 44 | if err != nil { 45 | return nil, fmt.Errorf("error decompressing spec: %w", err) 46 | } 47 | var buf bytes.Buffer 48 | _, err = buf.ReadFrom(zr) 49 | if err != nil { 50 | return nil, fmt.Errorf("error decompressing spec: %w", err) 51 | } 52 | 53 | return buf.Bytes(), nil 54 | } 55 | 56 | var rawSpec = decodeSpecCached() 57 | 58 | // a naive cached of a decoded swagger spec 59 | func decodeSpecCached() func() ([]byte, error) { 60 | data, err := decodeSpec() 61 | return func() ([]byte, error) { 62 | return data, err 63 | } 64 | } 65 | 66 | // Constructs a synthetic filesystem for resolving external references when loading openapi specifications. 67 | func PathToRawSpec(pathToFile string) map[string]func() ([]byte, error) { 68 | res := make(map[string]func() ([]byte, error)) 69 | if len(pathToFile) > 0 { 70 | res[pathToFile] = rawSpec 71 | } 72 | 73 | return res 74 | } 75 | 76 | // GetSwagger returns the Swagger specification corresponding to the generated code 77 | // in this file. The external references of Swagger specification are resolved. 78 | // The logic of resolving external references is tightly connected to "import-mapping" feature. 79 | // Externally referenced files must be embedded in the corresponding golang packages. 80 | // Urls can be supported but this task was out of the scope. 81 | func GetSwagger() (swagger *openapi3.T, err error) { 82 | resolvePath := PathToRawSpec("") 83 | 84 | loader := openapi3.NewLoader() 85 | loader.IsExternalRefsAllowed = true 86 | loader.ReadFromURIFunc = func(loader *openapi3.Loader, url *url.URL) ([]byte, error) { 87 | pathToFile := url.String() 88 | pathToFile = path.Clean(pathToFile) 89 | getSpec, ok := resolvePath[pathToFile] 90 | if !ok { 91 | err1 := fmt.Errorf("path not found: %s", pathToFile) 92 | return nil, err1 93 | } 94 | return getSpec() 95 | } 96 | var specData []byte 97 | specData, err = rawSpec() 98 | if err != nil { 99 | return 100 | } 101 | swagger, err = loader.LoadFromData(specData) 102 | if err != nil { 103 | return 104 | } 105 | return 106 | } 107 | -------------------------------------------------------------------------------- /generated/openapi/types.go: -------------------------------------------------------------------------------- 1 | // Package openapi provides primitives to interact with the openapi HTTP API. 2 | // 3 | // Code generated by github.com/oapi-codegen/oapi-codegen/v2 version v2.4.0 DO NOT EDIT. 4 | package openapi 5 | 6 | import ( 7 | openapi_types "github.com/oapi-codegen/runtime/types" 8 | ) 9 | 10 | // CreateOrderRequest defines model for CreateOrderRequest. 11 | type CreateOrderRequest struct { 12 | Items []OrderItem `json:"items"` 13 | } 14 | 15 | // CreateOrderResponse defines model for CreateOrderResponse. 16 | type CreateOrderResponse struct { 17 | Id *openapi_types.UUID `json:"id,omitempty"` 18 | } 19 | 20 | // CreateUserRequest defines model for CreateUserRequest. 21 | type CreateUserRequest struct { 22 | Email openapi_types.Email `json:"email"` 23 | Name string `json:"name"` 24 | } 25 | 26 | // CreateUserResponse defines model for CreateUserResponse. 27 | type CreateUserResponse struct { 28 | Id *openapi_types.UUID `json:"id,omitempty"` 29 | } 30 | 31 | // ErrorResponse defines model for ErrorResponse. 32 | type ErrorResponse struct { 33 | Message *string `json:"message,omitempty"` 34 | } 35 | 36 | // GetUserResponse defines model for GetUserResponse. 37 | type GetUserResponse struct { 38 | Email *openapi_types.Email `json:"email,omitempty"` 39 | Id *openapi_types.UUID `json:"id,omitempty"` 40 | Name *string `json:"name,omitempty"` 41 | } 42 | 43 | // OrderItem defines model for OrderItem. 44 | type OrderItem struct { 45 | Id *openapi_types.UUID `json:"id,omitempty"` 46 | } 47 | 48 | // PostOrdersJSONRequestBody defines body for PostOrders for application/json ContentType. 49 | type PostOrdersJSONRequestBody = CreateOrderRequest 50 | 51 | // PostUsersJSONRequestBody defines body for PostUsers for application/json ContentType. 52 | type PostUsersJSONRequestBody = CreateUserRequest 53 | -------------------------------------------------------------------------------- /generated/protobuf/protobuf.pb.go: -------------------------------------------------------------------------------- 1 | // Code generated by protoc-gen-go. DO NOT EDIT. 2 | // versions: 3 | // protoc-gen-go v1.34.2 4 | // protoc v5.28.1 5 | // source: protobuf.proto 6 | 7 | package protobuf 8 | 9 | import ( 10 | protoreflect "google.golang.org/protobuf/reflect/protoreflect" 11 | protoimpl "google.golang.org/protobuf/runtime/protoimpl" 12 | reflect "reflect" 13 | sync "sync" 14 | ) 15 | 16 | const ( 17 | // Verify that this generated code is sufficiently up-to-date. 18 | _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) 19 | // Verify that runtime/protoimpl is sufficiently up-to-date. 20 | _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) 21 | ) 22 | 23 | type CreateUserRequest struct { 24 | state protoimpl.MessageState 25 | sizeCache protoimpl.SizeCache 26 | unknownFields protoimpl.UnknownFields 27 | 28 | Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` 29 | Email string `protobuf:"bytes,2,opt,name=email,proto3" json:"email,omitempty"` 30 | } 31 | 32 | func (x *CreateUserRequest) Reset() { 33 | *x = CreateUserRequest{} 34 | if protoimpl.UnsafeEnabled { 35 | mi := &file_protobuf_proto_msgTypes[0] 36 | ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) 37 | ms.StoreMessageInfo(mi) 38 | } 39 | } 40 | 41 | func (x *CreateUserRequest) String() string { 42 | return protoimpl.X.MessageStringOf(x) 43 | } 44 | 45 | func (*CreateUserRequest) ProtoMessage() {} 46 | 47 | func (x *CreateUserRequest) ProtoReflect() protoreflect.Message { 48 | mi := &file_protobuf_proto_msgTypes[0] 49 | if protoimpl.UnsafeEnabled && x != nil { 50 | ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) 51 | if ms.LoadMessageInfo() == nil { 52 | ms.StoreMessageInfo(mi) 53 | } 54 | return ms 55 | } 56 | return mi.MessageOf(x) 57 | } 58 | 59 | // Deprecated: Use CreateUserRequest.ProtoReflect.Descriptor instead. 60 | func (*CreateUserRequest) Descriptor() ([]byte, []int) { 61 | return file_protobuf_proto_rawDescGZIP(), []int{0} 62 | } 63 | 64 | func (x *CreateUserRequest) GetName() string { 65 | if x != nil { 66 | return x.Name 67 | } 68 | return "" 69 | } 70 | 71 | func (x *CreateUserRequest) GetEmail() string { 72 | if x != nil { 73 | return x.Email 74 | } 75 | return "" 76 | } 77 | 78 | type CreateUserResponse struct { 79 | state protoimpl.MessageState 80 | sizeCache protoimpl.SizeCache 81 | unknownFields protoimpl.UnknownFields 82 | 83 | Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"` 84 | } 85 | 86 | func (x *CreateUserResponse) Reset() { 87 | *x = CreateUserResponse{} 88 | if protoimpl.UnsafeEnabled { 89 | mi := &file_protobuf_proto_msgTypes[1] 90 | ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) 91 | ms.StoreMessageInfo(mi) 92 | } 93 | } 94 | 95 | func (x *CreateUserResponse) String() string { 96 | return protoimpl.X.MessageStringOf(x) 97 | } 98 | 99 | func (*CreateUserResponse) ProtoMessage() {} 100 | 101 | func (x *CreateUserResponse) ProtoReflect() protoreflect.Message { 102 | mi := &file_protobuf_proto_msgTypes[1] 103 | if protoimpl.UnsafeEnabled && x != nil { 104 | ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) 105 | if ms.LoadMessageInfo() == nil { 106 | ms.StoreMessageInfo(mi) 107 | } 108 | return ms 109 | } 110 | return mi.MessageOf(x) 111 | } 112 | 113 | // Deprecated: Use CreateUserResponse.ProtoReflect.Descriptor instead. 114 | func (*CreateUserResponse) Descriptor() ([]byte, []int) { 115 | return file_protobuf_proto_rawDescGZIP(), []int{1} 116 | } 117 | 118 | func (x *CreateUserResponse) GetId() string { 119 | if x != nil { 120 | return x.Id 121 | } 122 | return "" 123 | } 124 | 125 | type GetUserRequest struct { 126 | state protoimpl.MessageState 127 | sizeCache protoimpl.SizeCache 128 | unknownFields protoimpl.UnknownFields 129 | 130 | Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"` 131 | } 132 | 133 | func (x *GetUserRequest) Reset() { 134 | *x = GetUserRequest{} 135 | if protoimpl.UnsafeEnabled { 136 | mi := &file_protobuf_proto_msgTypes[2] 137 | ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) 138 | ms.StoreMessageInfo(mi) 139 | } 140 | } 141 | 142 | func (x *GetUserRequest) String() string { 143 | return protoimpl.X.MessageStringOf(x) 144 | } 145 | 146 | func (*GetUserRequest) ProtoMessage() {} 147 | 148 | func (x *GetUserRequest) ProtoReflect() protoreflect.Message { 149 | mi := &file_protobuf_proto_msgTypes[2] 150 | if protoimpl.UnsafeEnabled && x != nil { 151 | ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) 152 | if ms.LoadMessageInfo() == nil { 153 | ms.StoreMessageInfo(mi) 154 | } 155 | return ms 156 | } 157 | return mi.MessageOf(x) 158 | } 159 | 160 | // Deprecated: Use GetUserRequest.ProtoReflect.Descriptor instead. 161 | func (*GetUserRequest) Descriptor() ([]byte, []int) { 162 | return file_protobuf_proto_rawDescGZIP(), []int{2} 163 | } 164 | 165 | func (x *GetUserRequest) GetId() string { 166 | if x != nil { 167 | return x.Id 168 | } 169 | return "" 170 | } 171 | 172 | type GetUserResponse struct { 173 | state protoimpl.MessageState 174 | sizeCache protoimpl.SizeCache 175 | unknownFields protoimpl.UnknownFields 176 | 177 | Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"` 178 | Name string `protobuf:"bytes,2,opt,name=name,proto3" json:"name,omitempty"` 179 | Email string `protobuf:"bytes,3,opt,name=email,proto3" json:"email,omitempty"` 180 | } 181 | 182 | func (x *GetUserResponse) Reset() { 183 | *x = GetUserResponse{} 184 | if protoimpl.UnsafeEnabled { 185 | mi := &file_protobuf_proto_msgTypes[3] 186 | ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) 187 | ms.StoreMessageInfo(mi) 188 | } 189 | } 190 | 191 | func (x *GetUserResponse) String() string { 192 | return protoimpl.X.MessageStringOf(x) 193 | } 194 | 195 | func (*GetUserResponse) ProtoMessage() {} 196 | 197 | func (x *GetUserResponse) ProtoReflect() protoreflect.Message { 198 | mi := &file_protobuf_proto_msgTypes[3] 199 | if protoimpl.UnsafeEnabled && x != nil { 200 | ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) 201 | if ms.LoadMessageInfo() == nil { 202 | ms.StoreMessageInfo(mi) 203 | } 204 | return ms 205 | } 206 | return mi.MessageOf(x) 207 | } 208 | 209 | // Deprecated: Use GetUserResponse.ProtoReflect.Descriptor instead. 210 | func (*GetUserResponse) Descriptor() ([]byte, []int) { 211 | return file_protobuf_proto_rawDescGZIP(), []int{3} 212 | } 213 | 214 | func (x *GetUserResponse) GetId() string { 215 | if x != nil { 216 | return x.Id 217 | } 218 | return "" 219 | } 220 | 221 | func (x *GetUserResponse) GetName() string { 222 | if x != nil { 223 | return x.Name 224 | } 225 | return "" 226 | } 227 | 228 | func (x *GetUserResponse) GetEmail() string { 229 | if x != nil { 230 | return x.Email 231 | } 232 | return "" 233 | } 234 | 235 | type CreateOrderRequest struct { 236 | state protoimpl.MessageState 237 | sizeCache protoimpl.SizeCache 238 | unknownFields protoimpl.UnknownFields 239 | 240 | Items []*OrderItem `protobuf:"bytes,1,rep,name=items,proto3" json:"items,omitempty"` 241 | } 242 | 243 | func (x *CreateOrderRequest) Reset() { 244 | *x = CreateOrderRequest{} 245 | if protoimpl.UnsafeEnabled { 246 | mi := &file_protobuf_proto_msgTypes[4] 247 | ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) 248 | ms.StoreMessageInfo(mi) 249 | } 250 | } 251 | 252 | func (x *CreateOrderRequest) String() string { 253 | return protoimpl.X.MessageStringOf(x) 254 | } 255 | 256 | func (*CreateOrderRequest) ProtoMessage() {} 257 | 258 | func (x *CreateOrderRequest) ProtoReflect() protoreflect.Message { 259 | mi := &file_protobuf_proto_msgTypes[4] 260 | if protoimpl.UnsafeEnabled && x != nil { 261 | ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) 262 | if ms.LoadMessageInfo() == nil { 263 | ms.StoreMessageInfo(mi) 264 | } 265 | return ms 266 | } 267 | return mi.MessageOf(x) 268 | } 269 | 270 | // Deprecated: Use CreateOrderRequest.ProtoReflect.Descriptor instead. 271 | func (*CreateOrderRequest) Descriptor() ([]byte, []int) { 272 | return file_protobuf_proto_rawDescGZIP(), []int{4} 273 | } 274 | 275 | func (x *CreateOrderRequest) GetItems() []*OrderItem { 276 | if x != nil { 277 | return x.Items 278 | } 279 | return nil 280 | } 281 | 282 | type OrderItem struct { 283 | state protoimpl.MessageState 284 | sizeCache protoimpl.SizeCache 285 | unknownFields protoimpl.UnknownFields 286 | 287 | Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"` 288 | } 289 | 290 | func (x *OrderItem) Reset() { 291 | *x = OrderItem{} 292 | if protoimpl.UnsafeEnabled { 293 | mi := &file_protobuf_proto_msgTypes[5] 294 | ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) 295 | ms.StoreMessageInfo(mi) 296 | } 297 | } 298 | 299 | func (x *OrderItem) String() string { 300 | return protoimpl.X.MessageStringOf(x) 301 | } 302 | 303 | func (*OrderItem) ProtoMessage() {} 304 | 305 | func (x *OrderItem) ProtoReflect() protoreflect.Message { 306 | mi := &file_protobuf_proto_msgTypes[5] 307 | if protoimpl.UnsafeEnabled && x != nil { 308 | ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) 309 | if ms.LoadMessageInfo() == nil { 310 | ms.StoreMessageInfo(mi) 311 | } 312 | return ms 313 | } 314 | return mi.MessageOf(x) 315 | } 316 | 317 | // Deprecated: Use OrderItem.ProtoReflect.Descriptor instead. 318 | func (*OrderItem) Descriptor() ([]byte, []int) { 319 | return file_protobuf_proto_rawDescGZIP(), []int{5} 320 | } 321 | 322 | func (x *OrderItem) GetId() string { 323 | if x != nil { 324 | return x.Id 325 | } 326 | return "" 327 | } 328 | 329 | type CreateOrderResponse struct { 330 | state protoimpl.MessageState 331 | sizeCache protoimpl.SizeCache 332 | unknownFields protoimpl.UnknownFields 333 | 334 | Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"` 335 | } 336 | 337 | func (x *CreateOrderResponse) Reset() { 338 | *x = CreateOrderResponse{} 339 | if protoimpl.UnsafeEnabled { 340 | mi := &file_protobuf_proto_msgTypes[6] 341 | ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) 342 | ms.StoreMessageInfo(mi) 343 | } 344 | } 345 | 346 | func (x *CreateOrderResponse) String() string { 347 | return protoimpl.X.MessageStringOf(x) 348 | } 349 | 350 | func (*CreateOrderResponse) ProtoMessage() {} 351 | 352 | func (x *CreateOrderResponse) ProtoReflect() protoreflect.Message { 353 | mi := &file_protobuf_proto_msgTypes[6] 354 | if protoimpl.UnsafeEnabled && x != nil { 355 | ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) 356 | if ms.LoadMessageInfo() == nil { 357 | ms.StoreMessageInfo(mi) 358 | } 359 | return ms 360 | } 361 | return mi.MessageOf(x) 362 | } 363 | 364 | // Deprecated: Use CreateOrderResponse.ProtoReflect.Descriptor instead. 365 | func (*CreateOrderResponse) Descriptor() ([]byte, []int) { 366 | return file_protobuf_proto_rawDescGZIP(), []int{6} 367 | } 368 | 369 | func (x *CreateOrderResponse) GetId() string { 370 | if x != nil { 371 | return x.Id 372 | } 373 | return "" 374 | } 375 | 376 | var File_protobuf_proto protoreflect.FileDescriptor 377 | 378 | var file_protobuf_proto_rawDesc = []byte{ 379 | 0x0a, 0x0e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 380 | 0x12, 0x08, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x22, 0x3d, 0x0a, 0x11, 0x43, 0x72, 381 | 0x65, 0x61, 0x74, 0x65, 0x55, 0x73, 0x65, 0x72, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 382 | 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 383 | 0x61, 0x6d, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x65, 0x6d, 0x61, 0x69, 0x6c, 0x18, 0x02, 0x20, 0x01, 384 | 0x28, 0x09, 0x52, 0x05, 0x65, 0x6d, 0x61, 0x69, 0x6c, 0x22, 0x24, 0x0a, 0x12, 0x43, 0x72, 0x65, 385 | 0x61, 0x74, 0x65, 0x55, 0x73, 0x65, 0x72, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 386 | 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x69, 0x64, 0x22, 387 | 0x20, 0x0a, 0x0e, 0x47, 0x65, 0x74, 0x55, 0x73, 0x65, 0x72, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 388 | 0x74, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x69, 389 | 0x64, 0x22, 0x4b, 0x0a, 0x0f, 0x47, 0x65, 0x74, 0x55, 0x73, 0x65, 0x72, 0x52, 0x65, 0x73, 0x70, 390 | 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 391 | 0x52, 0x02, 0x69, 0x64, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x02, 0x20, 0x01, 392 | 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x65, 0x6d, 0x61, 0x69, 393 | 0x6c, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x65, 0x6d, 0x61, 0x69, 0x6c, 0x22, 0x3f, 394 | 0x0a, 0x12, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x4f, 0x72, 0x64, 0x65, 0x72, 0x52, 0x65, 0x71, 395 | 0x75, 0x65, 0x73, 0x74, 0x12, 0x29, 0x0a, 0x05, 0x69, 0x74, 0x65, 0x6d, 0x73, 0x18, 0x01, 0x20, 396 | 0x03, 0x28, 0x0b, 0x32, 0x13, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x4f, 397 | 0x72, 0x64, 0x65, 0x72, 0x49, 0x74, 0x65, 0x6d, 0x52, 0x05, 0x69, 0x74, 0x65, 0x6d, 0x73, 0x22, 398 | 0x1b, 0x0a, 0x09, 0x4f, 0x72, 0x64, 0x65, 0x72, 0x49, 0x74, 0x65, 0x6d, 0x12, 0x0e, 0x0a, 0x02, 399 | 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x69, 0x64, 0x22, 0x25, 0x0a, 0x13, 400 | 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x4f, 0x72, 0x64, 0x65, 0x72, 0x52, 0x65, 0x73, 0x70, 0x6f, 401 | 0x6e, 0x73, 0x65, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 402 | 0x02, 0x69, 0x64, 0x32, 0x96, 0x01, 0x0a, 0x0b, 0x55, 0x73, 0x65, 0x72, 0x53, 0x65, 0x72, 0x76, 403 | 0x69, 0x63, 0x65, 0x12, 0x47, 0x0a, 0x0a, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x55, 0x73, 0x65, 404 | 0x72, 0x12, 0x1b, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x43, 0x72, 0x65, 405 | 0x61, 0x74, 0x65, 0x55, 0x73, 0x65, 0x72, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1c, 406 | 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 407 | 0x55, 0x73, 0x65, 0x72, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x3e, 0x0a, 0x07, 408 | 0x47, 0x65, 0x74, 0x55, 0x73, 0x65, 0x72, 0x12, 0x18, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 409 | 0x75, 0x66, 0x2e, 0x47, 0x65, 0x74, 0x55, 0x73, 0x65, 0x72, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 410 | 0x74, 0x1a, 0x19, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x47, 0x65, 0x74, 411 | 0x55, 0x73, 0x65, 0x72, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x32, 0x5a, 0x0a, 0x0c, 412 | 0x4f, 0x72, 0x64, 0x65, 0x72, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x12, 0x4a, 0x0a, 0x0b, 413 | 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x4f, 0x72, 0x64, 0x65, 0x72, 0x12, 0x1c, 0x2e, 0x70, 0x72, 414 | 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x4f, 0x72, 0x64, 415 | 0x65, 0x72, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1d, 0x2e, 0x70, 0x72, 0x6f, 0x74, 416 | 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x4f, 0x72, 0x64, 0x65, 0x72, 417 | 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x42, 0x2e, 0x5a, 0x2c, 0x67, 0x6f, 0x2d, 0x65, 418 | 0x63, 0x68, 0x6f, 0x2d, 0x74, 0x65, 0x6d, 0x70, 0x6c, 0x61, 0x74, 0x65, 0x2f, 0x67, 0x65, 0x6e, 419 | 0x65, 0x72, 0x61, 0x74, 0x65, 0x64, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x3b, 420 | 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, 421 | } 422 | 423 | var ( 424 | file_protobuf_proto_rawDescOnce sync.Once 425 | file_protobuf_proto_rawDescData = file_protobuf_proto_rawDesc 426 | ) 427 | 428 | func file_protobuf_proto_rawDescGZIP() []byte { 429 | file_protobuf_proto_rawDescOnce.Do(func() { 430 | file_protobuf_proto_rawDescData = protoimpl.X.CompressGZIP(file_protobuf_proto_rawDescData) 431 | }) 432 | return file_protobuf_proto_rawDescData 433 | } 434 | 435 | var file_protobuf_proto_msgTypes = make([]protoimpl.MessageInfo, 7) 436 | var file_protobuf_proto_goTypes = []any{ 437 | (*CreateUserRequest)(nil), // 0: protobuf.CreateUserRequest 438 | (*CreateUserResponse)(nil), // 1: protobuf.CreateUserResponse 439 | (*GetUserRequest)(nil), // 2: protobuf.GetUserRequest 440 | (*GetUserResponse)(nil), // 3: protobuf.GetUserResponse 441 | (*CreateOrderRequest)(nil), // 4: protobuf.CreateOrderRequest 442 | (*OrderItem)(nil), // 5: protobuf.OrderItem 443 | (*CreateOrderResponse)(nil), // 6: protobuf.CreateOrderResponse 444 | } 445 | var file_protobuf_proto_depIdxs = []int32{ 446 | 5, // 0: protobuf.CreateOrderRequest.items:type_name -> protobuf.OrderItem 447 | 0, // 1: protobuf.UserService.CreateUser:input_type -> protobuf.CreateUserRequest 448 | 2, // 2: protobuf.UserService.GetUser:input_type -> protobuf.GetUserRequest 449 | 4, // 3: protobuf.OrderService.CreateOrder:input_type -> protobuf.CreateOrderRequest 450 | 1, // 4: protobuf.UserService.CreateUser:output_type -> protobuf.CreateUserResponse 451 | 3, // 5: protobuf.UserService.GetUser:output_type -> protobuf.GetUserResponse 452 | 6, // 6: protobuf.OrderService.CreateOrder:output_type -> protobuf.CreateOrderResponse 453 | 4, // [4:7] is the sub-list for method output_type 454 | 1, // [1:4] is the sub-list for method input_type 455 | 1, // [1:1] is the sub-list for extension type_name 456 | 1, // [1:1] is the sub-list for extension extendee 457 | 0, // [0:1] is the sub-list for field type_name 458 | } 459 | 460 | func init() { file_protobuf_proto_init() } 461 | func file_protobuf_proto_init() { 462 | if File_protobuf_proto != nil { 463 | return 464 | } 465 | if !protoimpl.UnsafeEnabled { 466 | file_protobuf_proto_msgTypes[0].Exporter = func(v any, i int) any { 467 | switch v := v.(*CreateUserRequest); i { 468 | case 0: 469 | return &v.state 470 | case 1: 471 | return &v.sizeCache 472 | case 2: 473 | return &v.unknownFields 474 | default: 475 | return nil 476 | } 477 | } 478 | file_protobuf_proto_msgTypes[1].Exporter = func(v any, i int) any { 479 | switch v := v.(*CreateUserResponse); i { 480 | case 0: 481 | return &v.state 482 | case 1: 483 | return &v.sizeCache 484 | case 2: 485 | return &v.unknownFields 486 | default: 487 | return nil 488 | } 489 | } 490 | file_protobuf_proto_msgTypes[2].Exporter = func(v any, i int) any { 491 | switch v := v.(*GetUserRequest); i { 492 | case 0: 493 | return &v.state 494 | case 1: 495 | return &v.sizeCache 496 | case 2: 497 | return &v.unknownFields 498 | default: 499 | return nil 500 | } 501 | } 502 | file_protobuf_proto_msgTypes[3].Exporter = func(v any, i int) any { 503 | switch v := v.(*GetUserResponse); i { 504 | case 0: 505 | return &v.state 506 | case 1: 507 | return &v.sizeCache 508 | case 2: 509 | return &v.unknownFields 510 | default: 511 | return nil 512 | } 513 | } 514 | file_protobuf_proto_msgTypes[4].Exporter = func(v any, i int) any { 515 | switch v := v.(*CreateOrderRequest); i { 516 | case 0: 517 | return &v.state 518 | case 1: 519 | return &v.sizeCache 520 | case 2: 521 | return &v.unknownFields 522 | default: 523 | return nil 524 | } 525 | } 526 | file_protobuf_proto_msgTypes[5].Exporter = func(v any, i int) any { 527 | switch v := v.(*OrderItem); i { 528 | case 0: 529 | return &v.state 530 | case 1: 531 | return &v.sizeCache 532 | case 2: 533 | return &v.unknownFields 534 | default: 535 | return nil 536 | } 537 | } 538 | file_protobuf_proto_msgTypes[6].Exporter = func(v any, i int) any { 539 | switch v := v.(*CreateOrderResponse); i { 540 | case 0: 541 | return &v.state 542 | case 1: 543 | return &v.sizeCache 544 | case 2: 545 | return &v.unknownFields 546 | default: 547 | return nil 548 | } 549 | } 550 | } 551 | type x struct{} 552 | out := protoimpl.TypeBuilder{ 553 | File: protoimpl.DescBuilder{ 554 | GoPackagePath: reflect.TypeOf(x{}).PkgPath(), 555 | RawDescriptor: file_protobuf_proto_rawDesc, 556 | NumEnums: 0, 557 | NumMessages: 7, 558 | NumExtensions: 0, 559 | NumServices: 2, 560 | }, 561 | GoTypes: file_protobuf_proto_goTypes, 562 | DependencyIndexes: file_protobuf_proto_depIdxs, 563 | MessageInfos: file_protobuf_proto_msgTypes, 564 | }.Build() 565 | File_protobuf_proto = out.File 566 | file_protobuf_proto_rawDesc = nil 567 | file_protobuf_proto_goTypes = nil 568 | file_protobuf_proto_depIdxs = nil 569 | } 570 | -------------------------------------------------------------------------------- /generated/protobuf/protobuf_grpc.pb.go: -------------------------------------------------------------------------------- 1 | // Code generated by protoc-gen-go-grpc. DO NOT EDIT. 2 | // versions: 3 | // - protoc-gen-go-grpc v1.5.1 4 | // - protoc v5.28.1 5 | // source: protobuf.proto 6 | 7 | package protobuf 8 | 9 | import ( 10 | context "context" 11 | grpc "google.golang.org/grpc" 12 | codes "google.golang.org/grpc/codes" 13 | status "google.golang.org/grpc/status" 14 | ) 15 | 16 | // This is a compile-time assertion to ensure that this generated file 17 | // is compatible with the grpc package it is being compiled against. 18 | // Requires gRPC-Go v1.64.0 or later. 19 | const _ = grpc.SupportPackageIsVersion9 20 | 21 | const ( 22 | UserService_CreateUser_FullMethodName = "/protobuf.UserService/CreateUser" 23 | UserService_GetUser_FullMethodName = "/protobuf.UserService/GetUser" 24 | ) 25 | 26 | // UserServiceClient is the client API for UserService service. 27 | // 28 | // 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. 29 | type UserServiceClient interface { 30 | CreateUser(ctx context.Context, in *CreateUserRequest, opts ...grpc.CallOption) (*CreateUserResponse, error) 31 | GetUser(ctx context.Context, in *GetUserRequest, opts ...grpc.CallOption) (*GetUserResponse, error) 32 | } 33 | 34 | type userServiceClient struct { 35 | cc grpc.ClientConnInterface 36 | } 37 | 38 | func NewUserServiceClient(cc grpc.ClientConnInterface) UserServiceClient { 39 | return &userServiceClient{cc} 40 | } 41 | 42 | func (c *userServiceClient) CreateUser(ctx context.Context, in *CreateUserRequest, opts ...grpc.CallOption) (*CreateUserResponse, error) { 43 | cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) 44 | out := new(CreateUserResponse) 45 | err := c.cc.Invoke(ctx, UserService_CreateUser_FullMethodName, in, out, cOpts...) 46 | if err != nil { 47 | return nil, err 48 | } 49 | return out, nil 50 | } 51 | 52 | func (c *userServiceClient) GetUser(ctx context.Context, in *GetUserRequest, opts ...grpc.CallOption) (*GetUserResponse, error) { 53 | cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) 54 | out := new(GetUserResponse) 55 | err := c.cc.Invoke(ctx, UserService_GetUser_FullMethodName, in, out, cOpts...) 56 | if err != nil { 57 | return nil, err 58 | } 59 | return out, nil 60 | } 61 | 62 | // UserServiceServer is the server API for UserService service. 63 | // All implementations must embed UnimplementedUserServiceServer 64 | // for forward compatibility. 65 | type UserServiceServer interface { 66 | CreateUser(context.Context, *CreateUserRequest) (*CreateUserResponse, error) 67 | GetUser(context.Context, *GetUserRequest) (*GetUserResponse, error) 68 | mustEmbedUnimplementedUserServiceServer() 69 | } 70 | 71 | // UnimplementedUserServiceServer must be embedded to have 72 | // forward compatible implementations. 73 | // 74 | // NOTE: this should be embedded by value instead of pointer to avoid a nil 75 | // pointer dereference when methods are called. 76 | type UnimplementedUserServiceServer struct{} 77 | 78 | func (UnimplementedUserServiceServer) CreateUser(context.Context, *CreateUserRequest) (*CreateUserResponse, error) { 79 | return nil, status.Errorf(codes.Unimplemented, "method CreateUser not implemented") 80 | } 81 | func (UnimplementedUserServiceServer) GetUser(context.Context, *GetUserRequest) (*GetUserResponse, error) { 82 | return nil, status.Errorf(codes.Unimplemented, "method GetUser not implemented") 83 | } 84 | func (UnimplementedUserServiceServer) mustEmbedUnimplementedUserServiceServer() {} 85 | func (UnimplementedUserServiceServer) testEmbeddedByValue() {} 86 | 87 | // UnsafeUserServiceServer may be embedded to opt out of forward compatibility for this service. 88 | // Use of this interface is not recommended, as added methods to UserServiceServer will 89 | // result in compilation errors. 90 | type UnsafeUserServiceServer interface { 91 | mustEmbedUnimplementedUserServiceServer() 92 | } 93 | 94 | func RegisterUserServiceServer(s grpc.ServiceRegistrar, srv UserServiceServer) { 95 | // If the following call pancis, it indicates UnimplementedUserServiceServer was 96 | // embedded by pointer and is nil. This will cause panics if an 97 | // unimplemented method is ever invoked, so we test this at initialization 98 | // time to prevent it from happening at runtime later due to I/O. 99 | if t, ok := srv.(interface{ testEmbeddedByValue() }); ok { 100 | t.testEmbeddedByValue() 101 | } 102 | s.RegisterService(&UserService_ServiceDesc, srv) 103 | } 104 | 105 | func _UserService_CreateUser_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { 106 | in := new(CreateUserRequest) 107 | if err := dec(in); err != nil { 108 | return nil, err 109 | } 110 | if interceptor == nil { 111 | return srv.(UserServiceServer).CreateUser(ctx, in) 112 | } 113 | info := &grpc.UnaryServerInfo{ 114 | Server: srv, 115 | FullMethod: UserService_CreateUser_FullMethodName, 116 | } 117 | handler := func(ctx context.Context, req interface{}) (interface{}, error) { 118 | return srv.(UserServiceServer).CreateUser(ctx, req.(*CreateUserRequest)) 119 | } 120 | return interceptor(ctx, in, info, handler) 121 | } 122 | 123 | func _UserService_GetUser_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { 124 | in := new(GetUserRequest) 125 | if err := dec(in); err != nil { 126 | return nil, err 127 | } 128 | if interceptor == nil { 129 | return srv.(UserServiceServer).GetUser(ctx, in) 130 | } 131 | info := &grpc.UnaryServerInfo{ 132 | Server: srv, 133 | FullMethod: UserService_GetUser_FullMethodName, 134 | } 135 | handler := func(ctx context.Context, req interface{}) (interface{}, error) { 136 | return srv.(UserServiceServer).GetUser(ctx, req.(*GetUserRequest)) 137 | } 138 | return interceptor(ctx, in, info, handler) 139 | } 140 | 141 | // UserService_ServiceDesc is the grpc.ServiceDesc for UserService service. 142 | // It's only intended for direct use with grpc.RegisterService, 143 | // and not to be introspected or modified (even as a copy) 144 | var UserService_ServiceDesc = grpc.ServiceDesc{ 145 | ServiceName: "protobuf.UserService", 146 | HandlerType: (*UserServiceServer)(nil), 147 | Methods: []grpc.MethodDesc{ 148 | { 149 | MethodName: "CreateUser", 150 | Handler: _UserService_CreateUser_Handler, 151 | }, 152 | { 153 | MethodName: "GetUser", 154 | Handler: _UserService_GetUser_Handler, 155 | }, 156 | }, 157 | Streams: []grpc.StreamDesc{}, 158 | Metadata: "protobuf.proto", 159 | } 160 | 161 | const ( 162 | OrderService_CreateOrder_FullMethodName = "/protobuf.OrderService/CreateOrder" 163 | ) 164 | 165 | // OrderServiceClient is the client API for OrderService service. 166 | // 167 | // 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. 168 | type OrderServiceClient interface { 169 | CreateOrder(ctx context.Context, in *CreateOrderRequest, opts ...grpc.CallOption) (*CreateOrderResponse, error) 170 | } 171 | 172 | type orderServiceClient struct { 173 | cc grpc.ClientConnInterface 174 | } 175 | 176 | func NewOrderServiceClient(cc grpc.ClientConnInterface) OrderServiceClient { 177 | return &orderServiceClient{cc} 178 | } 179 | 180 | func (c *orderServiceClient) CreateOrder(ctx context.Context, in *CreateOrderRequest, opts ...grpc.CallOption) (*CreateOrderResponse, error) { 181 | cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) 182 | out := new(CreateOrderResponse) 183 | err := c.cc.Invoke(ctx, OrderService_CreateOrder_FullMethodName, in, out, cOpts...) 184 | if err != nil { 185 | return nil, err 186 | } 187 | return out, nil 188 | } 189 | 190 | // OrderServiceServer is the server API for OrderService service. 191 | // All implementations must embed UnimplementedOrderServiceServer 192 | // for forward compatibility. 193 | type OrderServiceServer interface { 194 | CreateOrder(context.Context, *CreateOrderRequest) (*CreateOrderResponse, error) 195 | mustEmbedUnimplementedOrderServiceServer() 196 | } 197 | 198 | // UnimplementedOrderServiceServer must be embedded to have 199 | // forward compatible implementations. 200 | // 201 | // NOTE: this should be embedded by value instead of pointer to avoid a nil 202 | // pointer dereference when methods are called. 203 | type UnimplementedOrderServiceServer struct{} 204 | 205 | func (UnimplementedOrderServiceServer) CreateOrder(context.Context, *CreateOrderRequest) (*CreateOrderResponse, error) { 206 | return nil, status.Errorf(codes.Unimplemented, "method CreateOrder not implemented") 207 | } 208 | func (UnimplementedOrderServiceServer) mustEmbedUnimplementedOrderServiceServer() {} 209 | func (UnimplementedOrderServiceServer) testEmbeddedByValue() {} 210 | 211 | // UnsafeOrderServiceServer may be embedded to opt out of forward compatibility for this service. 212 | // Use of this interface is not recommended, as added methods to OrderServiceServer will 213 | // result in compilation errors. 214 | type UnsafeOrderServiceServer interface { 215 | mustEmbedUnimplementedOrderServiceServer() 216 | } 217 | 218 | func RegisterOrderServiceServer(s grpc.ServiceRegistrar, srv OrderServiceServer) { 219 | // If the following call pancis, it indicates UnimplementedOrderServiceServer was 220 | // embedded by pointer and is nil. This will cause panics if an 221 | // unimplemented method is ever invoked, so we test this at initialization 222 | // time to prevent it from happening at runtime later due to I/O. 223 | if t, ok := srv.(interface{ testEmbeddedByValue() }); ok { 224 | t.testEmbeddedByValue() 225 | } 226 | s.RegisterService(&OrderService_ServiceDesc, srv) 227 | } 228 | 229 | func _OrderService_CreateOrder_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { 230 | in := new(CreateOrderRequest) 231 | if err := dec(in); err != nil { 232 | return nil, err 233 | } 234 | if interceptor == nil { 235 | return srv.(OrderServiceServer).CreateOrder(ctx, in) 236 | } 237 | info := &grpc.UnaryServerInfo{ 238 | Server: srv, 239 | FullMethod: OrderService_CreateOrder_FullMethodName, 240 | } 241 | handler := func(ctx context.Context, req interface{}) (interface{}, error) { 242 | return srv.(OrderServiceServer).CreateOrder(ctx, req.(*CreateOrderRequest)) 243 | } 244 | return interceptor(ctx, in, info, handler) 245 | } 246 | 247 | // OrderService_ServiceDesc is the grpc.ServiceDesc for OrderService service. 248 | // It's only intended for direct use with grpc.RegisterService, 249 | // and not to be introspected or modified (even as a copy) 250 | var OrderService_ServiceDesc = grpc.ServiceDesc{ 251 | ServiceName: "protobuf.OrderService", 252 | HandlerType: (*OrderServiceServer)(nil), 253 | Methods: []grpc.MethodDesc{ 254 | { 255 | MethodName: "CreateOrder", 256 | Handler: _OrderService_CreateOrder_Handler, 257 | }, 258 | }, 259 | Streams: []grpc.StreamDesc{}, 260 | Metadata: "protobuf.proto", 261 | } 262 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module go-echo-template 2 | 3 | go 1.23 4 | 5 | require ( 6 | github.com/getkin/kin-openapi v0.127.0 7 | github.com/getsentry/sentry-go v0.29.0 8 | github.com/go-redis/redis/v8 v8.11.5 9 | github.com/golang-migrate/migrate/v4 v4.18.1 10 | github.com/google/uuid v1.6.0 11 | github.com/jackc/pgx/v5 v5.7.1 12 | github.com/jmoiron/sqlx v1.4.0 13 | github.com/labstack/echo/v4 v4.12.0 14 | github.com/levigross/grequests v0.0.0-20231203190023-9c307ef1f48d 15 | github.com/oapi-codegen/runtime v1.1.1 16 | github.com/stretchr/testify v1.9.0 17 | golang.org/x/net v0.29.0 18 | golang.org/x/sync v0.8.0 19 | golang.yandex/hasql v1.1.1 20 | google.golang.org/grpc v1.66.2 21 | google.golang.org/protobuf v1.34.2 22 | ) 23 | 24 | require ( 25 | github.com/apapsch/go-jsonmerge/v2 v2.0.0 // indirect 26 | github.com/cespare/xxhash/v2 v2.3.0 // indirect 27 | github.com/davecgh/go-spew v1.1.1 // indirect 28 | github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect 29 | github.com/go-openapi/jsonpointer v0.21.0 // indirect 30 | github.com/go-openapi/swag v0.23.0 // indirect 31 | github.com/golang-jwt/jwt v3.2.2+incompatible // indirect 32 | github.com/google/go-querystring v1.1.0 // indirect 33 | github.com/hashicorp/errwrap v1.1.0 // indirect 34 | github.com/hashicorp/go-multierror v1.1.1 // indirect 35 | github.com/invopop/yaml v0.3.1 // indirect 36 | github.com/jackc/pgpassfile v1.0.0 // indirect 37 | github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect 38 | github.com/jackc/puddle/v2 v2.2.2 // indirect 39 | github.com/josharian/intern v1.0.0 // indirect 40 | github.com/labstack/gommon v0.4.2 // indirect 41 | github.com/lib/pq v1.10.9 // indirect 42 | github.com/mailru/easyjson v0.7.7 // indirect 43 | github.com/mattn/go-colorable v0.1.13 // indirect 44 | github.com/mattn/go-isatty v0.0.20 // indirect 45 | github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 // indirect 46 | github.com/perimeterx/marshmallow v1.1.5 // indirect 47 | github.com/pmezard/go-difflib v1.0.0 // indirect 48 | github.com/stretchr/objx v0.5.2 // indirect 49 | github.com/valyala/bytebufferpool v1.0.0 // indirect 50 | github.com/valyala/fasttemplate v1.2.2 // indirect 51 | go.uber.org/atomic v1.7.0 // indirect 52 | golang.org/x/crypto v0.27.0 // indirect 53 | golang.org/x/sys v0.25.0 // indirect 54 | golang.org/x/text v0.18.0 // indirect 55 | golang.org/x/time v0.6.0 // indirect 56 | google.golang.org/genproto/googleapis/rpc v0.0.0-20240823204242-4ba0660f739c // indirect 57 | gopkg.in/yaml.v3 v3.0.1 // indirect 58 | ) 59 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4= 2 | github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 h1:L/gRVlceqvL25UVaW/CKtUDjefjrs0SPonmDGUVOYP0= 3 | github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E= 4 | github.com/DATA-DOG/go-sqlmock v1.5.0 h1:Shsta01QNfFxHCfpW6YH2STWB0MudeXXEWMr20OEh60= 5 | github.com/DATA-DOG/go-sqlmock v1.5.0/go.mod h1:f/Ixk793poVmq4qj/V1dPUg2JEAKC73Q5eFN3EC/SaM= 6 | github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY= 7 | github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= 8 | github.com/RaveNoX/go-jsoncommentstrip v1.0.0/go.mod h1:78ihd09MekBnJnxpICcwzCMzGrKSKYe4AqU6PDYYpjk= 9 | github.com/apapsch/go-jsonmerge/v2 v2.0.0 h1:axGnT1gRIfimI7gJifB699GoE/oq+F2MU7Dml6nw9rQ= 10 | github.com/apapsch/go-jsonmerge/v2 v2.0.0/go.mod h1:lvDnEdqiQrp0O42VQGgmlKpxL1AP2+08jFMw88y4klk= 11 | github.com/bmatcuk/doublestar v1.1.1/go.mod h1:UD6OnuiIn0yFxxA2le/rnRU1G4RaI4UvFv1sNto9p6w= 12 | github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= 13 | github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= 14 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 15 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 16 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 17 | github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78= 18 | github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc= 19 | github.com/dhui/dktest v0.4.3 h1:wquqUxAFdcUgabAVLvSCOKOlag5cIZuaOjYIBOWdsR0= 20 | github.com/dhui/dktest v0.4.3/go.mod h1:zNK8IwktWzQRm6I/l2Wjp7MakiyaFWv4G1hjmodmMTs= 21 | github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk= 22 | github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E= 23 | github.com/docker/docker v27.2.0+incompatible h1:Rk9nIVdfH3+Vz4cyI/uhbINhEZ/oLmc+CBXmH6fbNk4= 24 | github.com/docker/docker v27.2.0+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= 25 | github.com/docker/go-connections v0.5.0 h1:USnMq7hx7gwdVZq1L49hLXaFtUdTADjXGp+uj1Br63c= 26 | github.com/docker/go-connections v0.5.0/go.mod h1:ov60Kzw0kKElRwhNs9UlUHAE/F9Fe6GLaXnqyDdmEXc= 27 | github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4= 28 | github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= 29 | github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= 30 | github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= 31 | github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4= 32 | github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= 33 | github.com/getkin/kin-openapi v0.127.0 h1:Mghqi3Dhryf3F8vR370nN67pAERW+3a95vomb3MAREY= 34 | github.com/getkin/kin-openapi v0.127.0/go.mod h1:OZrfXzUfGrNbsKj+xmFBx6E5c6yH3At/tAKSc2UszXM= 35 | github.com/getsentry/sentry-go v0.29.0 h1:YtWluuCFg9OfcqnaujpY918N/AhCCwarIDWOYSBAjCA= 36 | github.com/getsentry/sentry-go v0.29.0/go.mod h1:jhPesDAL0Q0W2+2YEuVOvdWmVtdsr1+jtBrlDEVWwLY= 37 | github.com/go-errors/errors v1.4.2 h1:J6MZopCL4uSllY1OfXM374weqZFFItUbrImctkmUxIA= 38 | github.com/go-errors/errors v1.4.2/go.mod h1:sIVyrIiJhuEF+Pj9Ebtd6P/rEYROXFi3BopGUQ5a5Og= 39 | github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= 40 | github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= 41 | github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= 42 | github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= 43 | github.com/go-openapi/jsonpointer v0.21.0 h1:YgdVicSA9vH5RiHs9TZW5oyafXZFc6+2Vc1rr/O9oNQ= 44 | github.com/go-openapi/jsonpointer v0.21.0/go.mod h1:IUyH9l/+uyhIYQ/PXVA41Rexl+kOkAPDdXEYns6fzUY= 45 | github.com/go-openapi/swag v0.23.0 h1:vsEVJDUo2hPJ2tu0/Xc+4noaxyEffXNIs3cOULZ+GrE= 46 | github.com/go-openapi/swag v0.23.0/go.mod h1:esZ8ITTYEsH1V2trKHjAN8Ai7xHb8RV+YSZ577vPjgQ= 47 | github.com/go-redis/redis/v8 v8.11.5 h1:AcZZR7igkdvfVmQTPnu9WE37LRrO/YrBH5zWyjDC0oI= 48 | github.com/go-redis/redis/v8 v8.11.5/go.mod h1:gREzHqY1hg6oD9ngVRbLStwAWKhA0FEgq8Jd4h5lpwo= 49 | github.com/go-sql-driver/mysql v1.6.0 h1:BCTh4TKNUYmOmMUcQ3IipzF5prigylS7XXjEkfCHuOE= 50 | github.com/go-sql-driver/mysql v1.6.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg= 51 | github.com/go-sql-driver/mysql v1.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpvNJ1Y= 52 | github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg= 53 | github.com/go-test/deep v1.0.8 h1:TDsG77qcSprGbC6vTN8OuXp5g+J+b5Pcguhf7Zt61VM= 54 | github.com/go-test/deep v1.0.8/go.mod h1:5C2ZWiW0ErCdrYzpqxLbTX7MG14M9iiw8DgHncVwcsE= 55 | github.com/gofrs/uuid v4.2.0+incompatible h1:yyYWMnhkhrKwwr8gAOcOCYxOOscHgDS9yZgBrnJfGa0= 56 | github.com/gofrs/uuid v4.2.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM= 57 | github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= 58 | github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= 59 | github.com/golang-jwt/jwt v3.2.2+incompatible h1:IfV12K8xAKAnZqdXVzCZ+TOjboZ2keLg81eXfW3O+oY= 60 | github.com/golang-jwt/jwt v3.2.2+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I= 61 | github.com/golang-migrate/migrate/v4 v4.18.1 h1:JML/k+t4tpHCpQTCAD62Nu43NUFzHY4CV3uAuvHGC+Y= 62 | github.com/golang-migrate/migrate/v4 v4.18.1/go.mod h1:HAX6m3sQgcdO81tdjn5exv20+3Kb13cmGli1hrD6hks= 63 | github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 64 | github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= 65 | github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= 66 | github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8= 67 | github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU= 68 | github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= 69 | github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 70 | github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= 71 | github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I= 72 | github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= 73 | github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo= 74 | github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM= 75 | github.com/invopop/yaml v0.3.1 h1:f0+ZpmhfBSS4MhG+4HYseMdJhoeeopbSKbq5Rpeelso= 76 | github.com/invopop/yaml v0.3.1/go.mod h1:PMOp3nn4/12yEZUFfmOuNHJsZToEEOwoWsT+D81KkeA= 77 | github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM= 78 | github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= 79 | github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo= 80 | github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM= 81 | github.com/jackc/pgx/v5 v5.7.1 h1:x7SYsPBYDkHDksogeSmZZ5xzThcTgRz++I5E+ePFUcs= 82 | github.com/jackc/pgx/v5 v5.7.1/go.mod h1:e7O26IywZZ+naJtWWos6i6fvWK+29etgITqrqHLfoZA= 83 | github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo= 84 | github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4= 85 | github.com/jmoiron/sqlx v1.3.5 h1:vFFPA71p1o5gAeqtEAwLU4dnX2napprKtHr7PYIcN3g= 86 | github.com/jmoiron/sqlx v1.3.5/go.mod h1:nRVWtLre0KfCLJvgxzCsLVMogSvQ1zNJtpYr2Ccp0mQ= 87 | github.com/jmoiron/sqlx v1.4.0 h1:1PLqN7S1UYp5t4SrVVnt4nUVNemrDAtxlulVe+Qgm3o= 88 | github.com/jmoiron/sqlx v1.4.0/go.mod h1:ZrZ7UsYB/weZdl2Bxg6jCRO9c3YHl8r3ahlKmRT4JLY= 89 | github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= 90 | github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= 91 | github.com/juju/gnuflag v0.0.0-20171113085948-2ce1bb71843d/go.mod h1:2PavIy+JPciBPrBUjwbNvtwB6RQlve+hkpll6QSNmOE= 92 | github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= 93 | github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= 94 | github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= 95 | github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= 96 | github.com/labstack/echo/v4 v4.12.0 h1:IKpw49IMryVB2p1a4dzwlhP1O2Tf2E0Ir/450lH+kI0= 97 | github.com/labstack/echo/v4 v4.12.0/go.mod h1:UP9Cr2DJXbOK3Kr9ONYzNowSh7HP0aG0ShAyycHSJvM= 98 | github.com/labstack/gommon v0.4.2 h1:F8qTUNXgG1+6WQmqoUWnz8WiEU60mXVVw0P4ht1WRA0= 99 | github.com/labstack/gommon v0.4.2/go.mod h1:QlUFxVM+SNXhDL/Z7YhocGIBYOiwB0mXm1+1bAPHPyU= 100 | github.com/levigross/grequests v0.0.0-20231203190023-9c307ef1f48d h1:8fVmm2qScPn4JAF/YdTtqrPP3n58FgZ4GbKTNfaPuRs= 101 | github.com/levigross/grequests v0.0.0-20231203190023-9c307ef1f48d/go.mod h1:dFu6nuJHC3u9kCDcyGrEL7LwhK2m6Mt+alyiiIjDrRY= 102 | github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= 103 | github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw= 104 | github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= 105 | github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= 106 | github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= 107 | github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= 108 | github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= 109 | github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= 110 | github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= 111 | github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= 112 | github.com/mattn/go-sqlite3 v1.14.6/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU= 113 | github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU= 114 | github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= 115 | github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0= 116 | github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo= 117 | github.com/moby/term v0.5.0 h1:xt8Q1nalod/v7BqbG21f8mQPqH+xAaC9C3N3wfWbVP0= 118 | github.com/moby/term v0.5.0/go.mod h1:8FzsFHVUBGZdbDsJw/ot+X+d5HLUbvklYLJ9uGfcI3Y= 119 | github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 h1:RWengNIwukTxcDr9M+97sNutRR1RKhG96O6jWumTTnw= 120 | github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826/go.mod h1:TaXosZuwdSHYgviHp1DAtfrULt5eUgsSMsZf+YrPgl8= 121 | github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A= 122 | github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc= 123 | github.com/nxadm/tail v1.4.11 h1:8feyoE3OzPrcshW5/MJ4sGESc5cqmGkGCWlco4l0bqY= 124 | github.com/nxadm/tail v1.4.11/go.mod h1:OTaG3NK980DZzxbRq6lEuzgU+mug70nY11sMd4JXXHc= 125 | github.com/oapi-codegen/runtime v1.1.1 h1:EXLHh0DXIJnWhdRPN2w4MXAzFyE4CskzhNLUmtpMYro= 126 | github.com/oapi-codegen/runtime v1.1.1/go.mod h1:SK9X900oXmPWilYR5/WKPzt3Kqxn/uS/+lbpREv+eCg= 127 | github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE= 128 | github.com/onsi/ginkgo v1.16.5/go.mod h1:+E8gABHa3K6zRBolWtd+ROzc/U5bkGt0FwiG042wbpU= 129 | github.com/onsi/gomega v1.18.1 h1:M1GfJqGRrBrrGGsbxzV5dqM2U2ApXefZCQpkukxYRLE= 130 | github.com/onsi/gomega v1.18.1/go.mod h1:0q+aL8jAiMXy9hbwj2mr5GziHiwhAIQpFmmtT5hitRs= 131 | github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= 132 | github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= 133 | github.com/opencontainers/image-spec v1.1.0 h1:8SG7/vwALn54lVB/0yZ/MMwhFrPYtpEHQb2IpWsCzug= 134 | github.com/opencontainers/image-spec v1.1.0/go.mod h1:W4s4sFTMaBeK1BQLXbG4AdM2szdn85PY75RI83NrTrM= 135 | github.com/perimeterx/marshmallow v1.1.5 h1:a2LALqQ1BlHM8PZblsDdidgv1mWi1DgC2UmX50IvK2s= 136 | github.com/perimeterx/marshmallow v1.1.5/go.mod h1:dsXbUu8CRzfYP5a87xpp0xq9S3u0Vchtcl8we9tYaXw= 137 | github.com/pingcap/errors v0.11.4 h1:lFuQV/oaUMGcD2tqt+01ROSmJs75VG1ToEOkZIZ4nE4= 138 | github.com/pingcap/errors v0.11.4/go.mod h1:Oi8TUi2kEtXXLMJk9l1cGmz20kV3TaQ0usTwv5KuLY8= 139 | github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= 140 | github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 141 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 142 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 143 | github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8= 144 | github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4= 145 | github.com/spkg/bom v0.0.0-20160624110644-59b7046e48ad/go.mod h1:qLr4V1qq6nMqFKkMo8ZTx3f+BZEkzsRUY10Xsm2mwU0= 146 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 147 | github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= 148 | github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= 149 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 150 | github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 151 | github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= 152 | github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= 153 | github.com/ugorji/go/codec v1.2.11 h1:BMaWp1Bb6fHwEtbplGBGJ498wD+LKlNSl25MjdZY4dU= 154 | github.com/ugorji/go/codec v1.2.11/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg= 155 | github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= 156 | github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= 157 | github.com/valyala/fasttemplate v1.2.2 h1:lxLXG0uE3Qnshl9QyaK6XJxMXlQZELvChBOCmQD0Loo= 158 | github.com/valyala/fasttemplate v1.2.2/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ= 159 | go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.54.0 h1:TT4fX+nBOA/+LUkobKGW1ydGcn+G3vRw9+g5HwCphpk= 160 | go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.54.0/go.mod h1:L7UH0GbB0p47T4Rri3uHjbpCFYrVrwc1I25QhNPiGK8= 161 | go.opentelemetry.io/otel v1.29.0 h1:PdomN/Al4q/lN6iBJEN3AwPvUiHPMlt93c8bqTG5Llw= 162 | go.opentelemetry.io/otel v1.29.0/go.mod h1:N/WtXPs1CNCUEx+Agz5uouwCba+i+bJGFicT8SR4NP8= 163 | go.opentelemetry.io/otel/metric v1.29.0 h1:vPf/HFWTNkPu1aYeIsc98l4ktOQaL6LeSoeV2g+8YLc= 164 | go.opentelemetry.io/otel/metric v1.29.0/go.mod h1:auu/QWieFVWx+DmQOUMgj0F8LHWdgalxXqvp7BII/W8= 165 | go.opentelemetry.io/otel/trace v1.29.0 h1:J/8ZNK4XgR7a21DZUAsbF8pZ5Jcw1VhACmnYt39JTi4= 166 | go.opentelemetry.io/otel/trace v1.29.0/go.mod h1:eHl3w0sp3paPkYstJOmAimxhiFXPg+MMTlEh3nsQgWQ= 167 | go.uber.org/atomic v1.7.0 h1:ADUqmZGgLDDfbSL9ZmPxKTybcoEYHgpYfELNoN+7hsw= 168 | go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= 169 | golang.org/x/crypto v0.27.0 h1:GXm2NjJrPaiv/h1tb2UH8QfgC/hOf/+z0p6PT8o1w7A= 170 | golang.org/x/crypto v0.27.0/go.mod h1:1Xngt8kV6Dvbssa53Ziq6Eqn0HqbZi5Z6R0ZpwQzt70= 171 | golang.org/x/net v0.29.0 h1:5ORfpBpCs4HzDYoodCDBbwHzdR5UrLBZ3sOnUJmFoHo= 172 | golang.org/x/net v0.29.0/go.mod h1:gLkgy8jTGERgjzMic6DS9+SP0ajcu6Xu3Orq/SpETg0= 173 | golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ= 174 | golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= 175 | golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 176 | golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 177 | golang.org/x/sys v0.25.0 h1:r+8e+loiHxRqhXVl6ML1nO3l1+oFoWbnlu2Ehimmi34= 178 | golang.org/x/sys v0.25.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 179 | golang.org/x/text v0.18.0 h1:XvMDiNzPAl0jr17s6W9lcaIhGUfUORdGCNsuLmPG224= 180 | golang.org/x/text v0.18.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= 181 | golang.org/x/time v0.6.0 h1:eTDhh4ZXt5Qf0augr54TN6suAUudPcawVZeIAPU7D4U= 182 | golang.org/x/time v0.6.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= 183 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 184 | golang.yandex/hasql v1.1.1 h1:LHBFTVn2hjuw7Owb8vXjRxuJRp1Gn86tBpFd6fDEQe4= 185 | golang.yandex/hasql v1.1.1/go.mod h1:kFvgn/xKoQwD1GOq0tGijvf5o3eltgQUs++Vx+Vxfp8= 186 | google.golang.org/genproto/googleapis/rpc v0.0.0-20240823204242-4ba0660f739c h1:Kqjm4WpoWvwhMPcrAczoTyMySQmYa9Wy2iL6Con4zn8= 187 | google.golang.org/genproto/googleapis/rpc v0.0.0-20240823204242-4ba0660f739c/go.mod h1:UqMtugtsSgubUsoxbuAoiCXvqvErP7Gf0so0mK9tHxU= 188 | google.golang.org/grpc v1.66.2 h1:3QdXkuq3Bkh7w+ywLdLvM56cmGvQHUMZpiCzt6Rqaoo= 189 | google.golang.org/grpc v1.66.2/go.mod h1:s3/l6xSSCURdVfAnL+TqCNMyTDAGN6+lZeVxnZR128Y= 190 | google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg= 191 | google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw= 192 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 193 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= 194 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= 195 | gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= 196 | gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= 197 | gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= 198 | gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= 199 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 200 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 201 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 202 | -------------------------------------------------------------------------------- /integration_tests/ping_test.go: -------------------------------------------------------------------------------- 1 | package integration_tests_test 2 | 3 | import ( 4 | "net/http" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/suite" 8 | 9 | "go-echo-template/integration_tests/suites" 10 | 11 | "github.com/levigross/grequests" 12 | ) 13 | 14 | type TestSuite struct { 15 | suite.Suite 16 | suites.RunServerSuite 17 | } 18 | 19 | func (suite *TestSuite) SetupSuite() { 20 | suite.RunServerSuite.SetupSuite("8081") 21 | } 22 | 23 | func (suite *TestSuite) TestPing() { 24 | resp, err := grequests.Get(suite.HTTPServerURL+"/ping", nil) 25 | suite.Require().NoError(err) 26 | suite.Require().Equal(http.StatusOK, resp.StatusCode) 27 | suite.Require().Equal("pong", resp.String()) 28 | } 29 | 30 | func TestAPISuite(t *testing.T) { 31 | t.Parallel() 32 | suite.Run(t, new(TestSuite)) 33 | } 34 | -------------------------------------------------------------------------------- /integration_tests/suites/run_server_suite.go: -------------------------------------------------------------------------------- 1 | package suites 2 | 3 | import ( 4 | "log/slog" 5 | "os" 6 | "sync" 7 | "time" 8 | 9 | "go-echo-template/internal" 10 | 11 | "github.com/stretchr/testify/suite" 12 | "google.golang.org/grpc" 13 | "google.golang.org/grpc/credentials/insecure" 14 | ) 15 | 16 | const ( 17 | waitServerTimeout = 2 * time.Second 18 | ) 19 | 20 | type RunServerSuite struct { 21 | suite.Suite 22 | HTTPServerURL string 23 | GRPCServerURL string 24 | Conn *grpc.ClientConn 25 | wg sync.WaitGroup 26 | } 27 | 28 | func (suite *RunServerSuite) SetupSuite(port string) { 29 | cfg, err := internal.LoadConfig() 30 | if err != nil { 31 | suite.Fail("Failed to load config", err) 32 | } 33 | cfg.Server.Port = port 34 | cfg.Server.PprofPort = "" 35 | 36 | suite.GRPCServerURL = "localhost:" + port 37 | suite.HTTPServerURL = "http://" + suite.GRPCServerURL 38 | 39 | suite.wg.Add(1) 40 | go func() { 41 | defer suite.wg.Done() 42 | err := internal.Run(cfg) 43 | if err != nil { 44 | slog.Error("Failed to run server", "err", err) 45 | os.Exit(1) 46 | } 47 | }() 48 | 49 | time.Sleep(waitServerTimeout) 50 | 51 | suite.Conn, err = grpc.NewClient( 52 | suite.GRPCServerURL, 53 | grpc.WithTransportCredentials(insecure.NewCredentials()), 54 | ) 55 | if err != nil { 56 | suite.Fail("Failed to dial server", err) 57 | } 58 | } 59 | 60 | func (suite *RunServerSuite) TearDownSuite() { 61 | if suite.Conn != nil { 62 | suite.Conn.Close() 63 | } 64 | 65 | p, err := os.FindProcess(os.Getpid()) 66 | if err != nil { 67 | suite.Fail("Failed to find process", err) 68 | } 69 | 70 | err = p.Signal(os.Interrupt) 71 | if err != nil { 72 | suite.Fail("Failed to send interrupt signal", err) 73 | } 74 | 75 | suite.wg.Wait() 76 | } 77 | -------------------------------------------------------------------------------- /integration_tests/users/application/crud_test.go: -------------------------------------------------------------------------------- 1 | package application_test 2 | 3 | import ( 4 | "context" 5 | "encoding/json" 6 | "io" 7 | 8 | "github.com/google/uuid" 9 | 10 | "go-echo-template/generated/openapi" 11 | "go-echo-template/generated/protobuf" 12 | 13 | openapi_types "github.com/oapi-codegen/runtime/types" 14 | ) 15 | 16 | func (s *UsersSuite) TestHTTP() { 17 | var userID openapi_types.UUID 18 | user := openapi.CreateUserRequest{ 19 | Name: "John Doe", 20 | Email: "johndoe@example.com", 21 | } 22 | 23 | s.Run("Create", func() { 24 | createResp, err := s.httpClient.PostUsers(context.Background(), user) 25 | s.Require().NoError(err, "Failed to create user via HTTP") 26 | s.Require().Equal(201, createResp.StatusCode, "Failed to create user via HTTP") 27 | 28 | body, err := io.ReadAll(createResp.Body) 29 | s.Require().NoError(err, "Failed to read response body") 30 | defer createResp.Body.Close() 31 | var resp openapi.CreateUserResponse 32 | err = json.Unmarshal(body, &resp) 33 | s.Require().NoError(err, "Failed to unmarshal response body") 34 | s.Require().NotEmpty(resp.Id, "Received empty user ID") 35 | 36 | userID = *resp.Id 37 | }) 38 | 39 | s.Run("Get", func() { 40 | getResp, err := s.httpClient.GetUsersId(context.Background(), userID) 41 | s.Require().NoError(err, "Failed to get user via HTTP") 42 | s.Require().Equal(200, getResp.StatusCode, "Failed to get user via HTTP") 43 | 44 | body, err := io.ReadAll(getResp.Body) 45 | s.Require().NoError(err, "Failed to read response body") 46 | defer getResp.Body.Close() 47 | var resp openapi.GetUserResponse 48 | err = json.Unmarshal(body, &resp) 49 | s.Require().NoError(err, "Failed to unmarshal response body") 50 | s.Require().Equal(user.Name, *resp.Name, "User names do not match") 51 | s.Require().Equal(user.Email, *resp.Email, "User emails do not match") 52 | s.Require().Equal(userID, *resp.Id, "User IDs do not match") 53 | }) 54 | 55 | err := s.repo.DeleteUser(context.Background(), userID) 56 | s.Require().NoError(err) 57 | } 58 | 59 | func (s *UsersSuite) TestGRPC() { 60 | var userID string 61 | createReq := &protobuf.CreateUserRequest{ 62 | Name: "John Doe", 63 | Email: "johndoe@example.com", 64 | } 65 | 66 | s.Run("Create", func() { 67 | createResp, err := s.grpcClient.CreateUser(context.Background(), createReq) 68 | 69 | s.Require().NoError(err) 70 | s.Require().NotNil(createResp) 71 | s.Require().NotEmpty(createResp.GetId()) 72 | 73 | userID = createResp.GetId() 74 | }) 75 | 76 | s.Run("Get", func() { 77 | getReq := &protobuf.GetUserRequest{ 78 | Id: userID, 79 | } 80 | getResp, err := s.grpcClient.GetUser(context.Background(), getReq) 81 | 82 | s.Require().NoError(err) 83 | s.Require().NotNil(getResp) 84 | s.Require().Equal(createReq.GetName(), getResp.GetName()) 85 | s.Require().Equal(createReq.GetEmail(), getResp.GetEmail()) 86 | s.Require().Equal(userID, getResp.GetId()) 87 | }) 88 | 89 | err := s.repo.DeleteUser(context.Background(), uuid.MustParse(userID)) 90 | s.Require().NoError(err) 91 | } 92 | -------------------------------------------------------------------------------- /integration_tests/users/application/suite_test.go: -------------------------------------------------------------------------------- 1 | package application_test 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "testing" 7 | 8 | "github.com/stretchr/testify/suite" 9 | 10 | "go-echo-template/generated/openapi" 11 | "go-echo-template/generated/protobuf" 12 | "go-echo-template/integration_tests/suites" 13 | "go-echo-template/internal" 14 | infra "go-echo-template/internal/infrastructure/users" 15 | "go-echo-template/pkg/postgres" 16 | ) 17 | 18 | type UsersSuite struct { 19 | suite.Suite 20 | suites.RunServerSuite 21 | 22 | grpcClient protobuf.UserServiceClient 23 | httpClient *openapi.Client 24 | repo *infra.PostgresRepo 25 | } 26 | 27 | func (s *UsersSuite) SetupSuite() { 28 | s.RunServerSuite.SetupSuite("8082") 29 | s.grpcClient = protobuf.NewUserServiceClient(s.Conn) 30 | var err error 31 | s.httpClient, err = openapi.NewClient(s.HTTPServerURL) 32 | if err != nil { 33 | s.Fail("Failed to create HTTP client", err) 34 | } 35 | s.repo, err = initPostgresRepo() 36 | if err != nil { 37 | s.Fail("Failed to init postgres repo", err) 38 | } 39 | } 40 | 41 | func (s *UsersSuite) TearDownSuite() { 42 | s.RunServerSuite.TearDownSuite() 43 | } 44 | 45 | func TestUsersSuite(t *testing.T) { 46 | t.Parallel() 47 | suite.Run(t, new(UsersSuite)) 48 | } 49 | 50 | func initPostgresRepo() (*infra.PostgresRepo, error) { 51 | cfg, err := internal.LoadConfig() 52 | if err != nil { 53 | return nil, fmt.Errorf("failed to load config: %w", err) 54 | } 55 | connData, err := postgres.NewConnectionData( 56 | cfg.Postgres.Hosts, 57 | cfg.Postgres.Database, 58 | cfg.Postgres.User, 59 | cfg.Postgres.Password, 60 | cfg.Postgres.Port, 61 | cfg.Postgres.SSL, 62 | ) 63 | if err != nil { 64 | return nil, fmt.Errorf("failed to init postgres connection data: %w", err) 65 | } 66 | cluster, err := postgres.InitCluster(context.Background(), connData) 67 | if err != nil { 68 | return nil, fmt.Errorf("failed to init postgres cluster: %w", err) 69 | } 70 | return infra.NewPostgresRepo(cluster), nil 71 | } 72 | -------------------------------------------------------------------------------- /integration_tests/users/infrastructure/postgres_test.go: -------------------------------------------------------------------------------- 1 | package infrastructure_test 2 | 3 | import ( 4 | "context" 5 | "testing" 6 | 7 | "go-echo-template/internal" 8 | domain "go-echo-template/internal/domain/users" 9 | "go-echo-template/pkg/postgres" 10 | 11 | infra "go-echo-template/internal/infrastructure/users" 12 | 13 | "github.com/stretchr/testify/suite" 14 | ) 15 | 16 | type PostgresRepoSuite struct { 17 | suite.Suite 18 | repo *infra.PostgresRepo 19 | } 20 | 21 | func (suite *PostgresRepoSuite) SetupSuite() { 22 | cfg, err := internal.LoadConfig() 23 | if err != nil { 24 | suite.Fail("Failed to load config", err) 25 | } 26 | connData, err := postgres.NewConnectionData( 27 | cfg.Postgres.Hosts, 28 | cfg.Postgres.Database, 29 | cfg.Postgres.User, 30 | cfg.Postgres.Password, 31 | cfg.Postgres.Port, 32 | cfg.Postgres.SSL, 33 | ) 34 | if err != nil { 35 | suite.Fail("Failed to init postgres connection data", err) 36 | } 37 | cluster, err := postgres.InitCluster(context.Background(), connData) 38 | if err != nil { 39 | suite.Fail("Failed to init postgres cluster", err) 40 | } 41 | suite.repo = infra.NewPostgresRepo(cluster) 42 | } 43 | 44 | func (suite *PostgresRepoSuite) TestUserCRUD() { 45 | email := "test@test.com" 46 | created, err := suite.repo.CreateUser(context.Background(), email, func() (*domain.User, error) { 47 | return domain.CreateUser("test", email) 48 | }) 49 | suite.Require().NoError(err) 50 | 51 | gotten, err := suite.repo.GetUser(context.Background(), created.ID()) 52 | suite.Require().NoError(err) 53 | suite.Require().Equal(created, gotten) 54 | 55 | updated, err := suite.repo.UpdateUser(context.Background(), created.ID(), func(u *domain.User) (bool, error) { 56 | err := u.ChangeEmail("test@test2.com") 57 | return true, err 58 | }) 59 | suite.Require().NoError(err) 60 | 61 | gotten, err = suite.repo.GetUser(context.Background(), created.ID()) 62 | suite.Require().NoError(err) 63 | suite.Require().Equal(updated, gotten) 64 | 65 | err = suite.repo.DeleteUser(context.Background(), created.ID()) 66 | suite.Require().NoError(err) 67 | } 68 | 69 | func TestPostgresRepoSuite(t *testing.T) { 70 | t.Parallel() 71 | suite.Run(t, new(PostgresRepoSuite)) 72 | } 73 | -------------------------------------------------------------------------------- /integration_tests/users/infrastructure/redis_test.go: -------------------------------------------------------------------------------- 1 | package infrastructure_test 2 | 3 | import ( 4 | "context" 5 | "testing" 6 | 7 | "go-echo-template/internal" 8 | "go-echo-template/internal/domain/users" 9 | usersInfra "go-echo-template/internal/infrastructure/users" 10 | 11 | "github.com/google/uuid" 12 | "github.com/stretchr/testify/suite" 13 | ) 14 | 15 | type RedisRepoSuite struct { 16 | suite.Suite 17 | repo *usersInfra.RedisRepo 18 | ctx context.Context 19 | keysToDelete []uuid.UUID 20 | } 21 | 22 | func (suite *RedisRepoSuite) SetupSuite() { 23 | config, err := internal.LoadConfig() 24 | suite.Require().NoError(err) 25 | 26 | repo := usersInfra.NewRedisRepo( 27 | config.Redis.ClusterMode, 28 | config.Redis.TLSEnabled, 29 | config.Redis.Address, 30 | config.Redis.Username, 31 | config.Redis.Password, 32 | config.Redis.Expiration, 33 | ) 34 | suite.repo = repo 35 | suite.ctx = context.Background() 36 | suite.keysToDelete = make([]uuid.UUID, 0) 37 | } 38 | 39 | func (suite *RedisRepoSuite) TearDownTest() { 40 | for _, key := range suite.keysToDelete { 41 | err := suite.repo.DeleteUser(suite.ctx, key) 42 | suite.Require().NoError(err) 43 | } 44 | suite.keysToDelete = nil 45 | } 46 | 47 | func (suite *RedisRepoSuite) TestSaveUser() { 48 | key := uuid.New() 49 | suite.keysToDelete = append(suite.keysToDelete, key) 50 | 51 | user, err := users.NewUser(key, "test", "test@test.com") 52 | suite.Require().NoError(err) 53 | 54 | err = suite.repo.SaveUser(suite.ctx, *user) 55 | suite.Require().NoError(err) 56 | } 57 | 58 | func (suite *RedisRepoSuite) TestGetUser() { 59 | key := uuid.New() 60 | suite.keysToDelete = append(suite.keysToDelete, key) 61 | 62 | user, err := users.NewUser(key, "test", "test@test.com") 63 | suite.Require().NoError(err) 64 | 65 | err = suite.repo.SaveUser(suite.ctx, *user) 66 | suite.Require().NoError(err) 67 | 68 | u, err := suite.repo.GetUser(suite.ctx, key) 69 | suite.Require().NoError(err) 70 | suite.Equal(user, u) 71 | } 72 | 73 | func (suite *RedisRepoSuite) TestGetUserNotFound() { 74 | _, err := suite.repo.GetUser(suite.ctx, uuid.New()) 75 | suite.Require().ErrorIs(err, users.ErrUserNotFound) 76 | } 77 | 78 | func TestRedisRepoSuite(t *testing.T) { 79 | t.Parallel() 80 | suite.Run(t, new(RedisRepoSuite)) 81 | } 82 | -------------------------------------------------------------------------------- /internal/application/grpc_server.go: -------------------------------------------------------------------------------- 1 | package application 2 | 3 | import ( 4 | "go-echo-template/generated/protobuf" 5 | "go-echo-template/internal/application/orders" 6 | "go-echo-template/internal/application/users" 7 | 8 | "google.golang.org/grpc" 9 | ) 10 | 11 | type gRPCServer struct { 12 | users.UserHandlers 13 | orders.OrderHandlers 14 | } 15 | 16 | func SetupGRPCServer(userRepo UserRepository, orderRepo OrderRepository, productRepo ProductRepository) *grpc.Server { 17 | s := grpc.NewServer() 18 | 19 | server := gRPCServer{} 20 | server.UserHandlers = users.SetupHandlers(userRepo) 21 | server.OrderHandlers = orders.SetupHandlers(orderRepo, userRepo, productRepo) 22 | 23 | protobuf.RegisterOrderServiceServer(s, server) 24 | protobuf.RegisterUserServiceServer(s, server) 25 | 26 | return s 27 | } 28 | -------------------------------------------------------------------------------- /internal/application/http_server.go: -------------------------------------------------------------------------------- 1 | package application 2 | 3 | import ( 4 | "log/slog" 5 | "net/http" 6 | 7 | "go-echo-template/generated/openapi" 8 | "go-echo-template/internal/application/orders" 9 | "go-echo-template/internal/application/users" 10 | "go-echo-template/pkg/echomiddleware" 11 | 12 | sentryecho "github.com/getsentry/sentry-go/echo" 13 | "github.com/labstack/echo/v4" 14 | "github.com/labstack/echo/v4/middleware" 15 | ) 16 | 17 | type httpServer struct { 18 | users.UserHandlers 19 | orders.OrderHandlers 20 | } 21 | 22 | func SetupHTTPServer(userRepo UserRepository, orderRepo OrderRepository, productRepo ProductRepository) *echo.Echo { 23 | e := echo.New() 24 | 25 | e.Pre(middleware.RemoveTrailingSlash()) 26 | e.Use(echomiddleware.SlogLoggerMiddleware(slog.Default())) 27 | e.Use(echomiddleware.PutRequestIDContext) 28 | e.Use(middleware.Recover()) 29 | e.Use(sentryecho.New(sentryecho.Options{Repanic: true})) 30 | e.Use(echomiddleware.PutSentryContext) 31 | 32 | e.GET("/ping", func(c echo.Context) error { 33 | return c.String(http.StatusOK, "pong") 34 | }) 35 | 36 | server := httpServer{} 37 | server.UserHandlers = users.SetupHandlers(userRepo) 38 | server.OrderHandlers = orders.SetupHandlers(orderRepo, userRepo, productRepo) 39 | 40 | openapi.RegisterHandlers(e, server) 41 | 42 | return e 43 | } 44 | -------------------------------------------------------------------------------- /internal/application/interfaces.go: -------------------------------------------------------------------------------- 1 | package application 2 | 3 | import ( 4 | "context" 5 | 6 | "go-echo-template/internal/domain/orders" 7 | "go-echo-template/internal/domain/products" 8 | "go-echo-template/internal/domain/users" 9 | 10 | "github.com/google/uuid" 11 | ) 12 | 13 | type UserRepository interface { 14 | CreateUser(ctx context.Context, email string, createFn func() (*users.User, error)) (*users.User, error) 15 | UpdateUser(ctx context.Context, id uuid.UUID, updateFn func(*users.User) (bool, error)) (*users.User, error) 16 | GetUser(ctx context.Context, id uuid.UUID) (*users.User, error) 17 | } 18 | 19 | type OrderRepository interface { 20 | CreateOrder(ctx context.Context, itemIDs []uuid.UUID, createFn func() (*orders.Order, error)) (*orders.Order, error) 21 | GetOrder(ctx context.Context, id uuid.UUID) (*orders.Order, error) 22 | } 23 | 24 | type ProductRepository interface { 25 | GetProducts(ctx context.Context, ids []uuid.UUID) ([]products.Product, error) 26 | } 27 | -------------------------------------------------------------------------------- /internal/application/orders/create.go: -------------------------------------------------------------------------------- 1 | package orders 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "net/http" 7 | 8 | ordersService "go-echo-template/internal/service/orders" 9 | 10 | ordersDomain "go-echo-template/internal/domain/orders" 11 | 12 | "github.com/google/uuid" 13 | "github.com/labstack/echo/v4" 14 | "google.golang.org/grpc/codes" 15 | "google.golang.org/grpc/status" 16 | 17 | "go-echo-template/generated/openapi" 18 | "go-echo-template/generated/protobuf" 19 | productsDomain "go-echo-template/internal/domain/products" 20 | usersDomain "go-echo-template/internal/domain/users" 21 | ) 22 | 23 | func (h OrderHandlers) PostOrders(c echo.Context) error { 24 | var req openapi.CreateOrderRequest 25 | if err := c.Bind(&req); err != nil { 26 | return err 27 | } 28 | 29 | items := make([]ordersService.Item, 0, len(req.Items)) 30 | for _, i := range req.Items { 31 | items = append(items, ordersService.Item{ 32 | ID: *i.Id, 33 | }) 34 | } 35 | 36 | service := ordersService.NewOrderCreationService(h.orderRepo, h.userRepo, h.productRepo) 37 | // TODO: implement authentication interceptor 38 | order, err := service.CreateOrder(c.Request().Context(), c.Get("user_id").(uuid.UUID), items) 39 | if err != nil { 40 | msg := err.Error() 41 | if errors.Is(err, ordersDomain.ErrProductAlreadyReserved) { 42 | return c.JSON(http.StatusConflict, openapi.ErrorResponse{Message: &msg}) 43 | } 44 | if errors.Is(err, usersDomain.ErrUserNotFound) { 45 | return c.JSON(http.StatusNotFound, openapi.ErrorResponse{Message: &msg}) 46 | } 47 | if errors.Is(err, productsDomain.ErrProductNotFound) { 48 | return c.JSON(http.StatusNotFound, openapi.ErrorResponse{Message: &msg}) 49 | } 50 | return c.JSON(http.StatusInternalServerError, openapi.ErrorResponse{Message: &msg}) 51 | } 52 | id := order.ID() 53 | return c.JSON(http.StatusCreated, openapi.CreateOrderResponse{Id: &id}) 54 | } 55 | 56 | func (h OrderHandlers) CreateOrder( 57 | ctx context.Context, 58 | req *protobuf.CreateOrderRequest, 59 | ) (*protobuf.CreateOrderResponse, error) { 60 | items := make([]ordersService.Item, 0, len(req.GetItems())) 61 | for _, i := range req.GetItems() { 62 | uid, err := uuid.Parse(i.GetId()) 63 | if err != nil { 64 | return nil, status.Errorf(codes.InvalidArgument, "invalid UUID: %s", i.GetId()) 65 | } 66 | items = append(items, ordersService.Item{ 67 | ID: uid, 68 | }) 69 | } 70 | 71 | service := ordersService.NewOrderCreationService(h.orderRepo, h.userRepo, h.productRepo) 72 | // TODO: implement authentication interceptor 73 | order, err := service.CreateOrder(ctx, ctx.Value("user_id").(uuid.UUID), items) 74 | if err != nil { 75 | if errors.Is(err, ordersDomain.ErrProductAlreadyReserved) { 76 | return nil, status.Errorf(codes.Aborted, "product already reserved") 77 | } 78 | if errors.Is(err, usersDomain.ErrUserNotFound) { 79 | return nil, status.Errorf(codes.NotFound, "user not found") 80 | } 81 | if errors.Is(err, productsDomain.ErrProductNotFound) { 82 | return nil, status.Errorf(codes.NotFound, "product not found") 83 | } 84 | return nil, status.Errorf(codes.Internal, "internal server error") 85 | } 86 | return &protobuf.CreateOrderResponse{ 87 | Id: order.ID().String(), 88 | }, nil 89 | } 90 | -------------------------------------------------------------------------------- /internal/application/orders/handlers.go: -------------------------------------------------------------------------------- 1 | package orders 2 | 3 | import ( 4 | "go-echo-template/generated/protobuf" 5 | service "go-echo-template/internal/service/orders" 6 | ) 7 | 8 | type OrderHandlers struct { 9 | protobuf.UnimplementedOrderServiceServer 10 | orderRepo service.OrderRepository 11 | userRepo service.UserRepository 12 | productRepo service.ProductRepository 13 | } 14 | 15 | func SetupHandlers(or service.OrderRepository, ur service.UserRepository, pr service.ProductRepository) OrderHandlers { 16 | return OrderHandlers{ 17 | orderRepo: or, 18 | userRepo: ur, 19 | productRepo: pr, 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /internal/application/suite.go: -------------------------------------------------------------------------------- 1 | package application 2 | 3 | import ( 4 | ordersInfra "go-echo-template/internal/infrastructure/orders" 5 | productsInfra "go-echo-template/internal/infrastructure/products" 6 | usersInfra "go-echo-template/internal/infrastructure/users" 7 | 8 | "github.com/labstack/echo/v4" 9 | "github.com/stretchr/testify/suite" 10 | ) 11 | 12 | type ServerSuite struct { 13 | suite.Suite 14 | HTTPServer *echo.Echo 15 | UsersRepo *usersInfra.InMemoryRepo 16 | OrdersRepo *ordersInfra.InMemoryRepo 17 | ProductsRepo *productsInfra.InMemoryRepo 18 | } 19 | 20 | func (s *ServerSuite) SetupTest() { 21 | s.UsersRepo = usersInfra.NewInMemoryRepo() 22 | s.OrdersRepo = ordersInfra.NewInMemoryRepo() 23 | s.ProductsRepo = productsInfra.NewInMemoryRepo() 24 | s.HTTPServer = SetupHTTPServer(s.UsersRepo, s.OrdersRepo, s.ProductsRepo) 25 | } 26 | -------------------------------------------------------------------------------- /internal/application/users/create.go: -------------------------------------------------------------------------------- 1 | package users 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "net/http" 7 | 8 | "go-echo-template/generated/openapi" 9 | "go-echo-template/generated/protobuf" 10 | domain "go-echo-template/internal/domain/users" 11 | 12 | "github.com/labstack/echo/v4" 13 | "google.golang.org/grpc/codes" 14 | "google.golang.org/grpc/status" 15 | ) 16 | 17 | func (h UserHandlers) PostUsers(c echo.Context) error { 18 | ctx := c.Request().Context() 19 | var req openapi.CreateUserRequest 20 | if err := c.Bind(&req); err != nil { 21 | return err 22 | } 23 | 24 | email := string(req.Email) 25 | user, err := h.repo.CreateUser(ctx, email, func() (*domain.User, error) { 26 | return domain.CreateUser(req.Name, email) 27 | }) 28 | if err != nil { 29 | msg := err.Error() 30 | if errors.Is(err, domain.ErrInvalidUser) || errors.Is(err, domain.ErrUserValidation) { 31 | return c.JSON(http.StatusBadRequest, openapi.ErrorResponse{Message: &msg}) 32 | } 33 | if errors.Is(err, domain.ErrUserAlreadyExist) { 34 | return c.JSON(http.StatusConflict, openapi.ErrorResponse{Message: &msg}) 35 | } 36 | return c.JSON(http.StatusInternalServerError, openapi.ErrorResponse{Message: &msg}) 37 | } 38 | 39 | id := user.ID() 40 | return c.JSON(http.StatusCreated, openapi.CreateUserResponse{Id: &id}) 41 | } 42 | 43 | func (h UserHandlers) CreateUser( 44 | ctx context.Context, 45 | req *protobuf.CreateUserRequest, 46 | ) (*protobuf.CreateUserResponse, error) { 47 | email := req.GetEmail() 48 | user, err := h.repo.CreateUser(ctx, email, func() (*domain.User, error) { 49 | return domain.CreateUser(req.GetName(), email) 50 | }) 51 | if err != nil { 52 | if errors.Is(err, domain.ErrInvalidUser) || errors.Is(err, domain.ErrUserValidation) { 53 | return nil, status.Error(codes.InvalidArgument, err.Error()) 54 | } 55 | return nil, status.Error(codes.Internal, err.Error()) 56 | } 57 | 58 | return &protobuf.CreateUserResponse{ 59 | Id: user.ID().String(), 60 | }, nil 61 | } 62 | -------------------------------------------------------------------------------- /internal/application/users/get.go: -------------------------------------------------------------------------------- 1 | package users 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "net/http" 7 | 8 | "github.com/google/uuid" 9 | "github.com/labstack/echo/v4" 10 | openapi_types "github.com/oapi-codegen/runtime/types" 11 | "google.golang.org/grpc/codes" 12 | "google.golang.org/grpc/status" 13 | 14 | "go-echo-template/generated/openapi" 15 | "go-echo-template/generated/protobuf" 16 | "go-echo-template/internal/domain/users" 17 | ) 18 | 19 | //nolint:stylecheck // fit to generated code 20 | func (h UserHandlers) GetUsersId(c echo.Context, id openapi_types.UUID) error { 21 | user, err := h.repo.GetUser(c.Request().Context(), id) 22 | if err != nil { 23 | msg := err.Error() 24 | if errors.Is(err, users.ErrUserNotFound) { 25 | return c.JSON(http.StatusNotFound, openapi.ErrorResponse{Message: &msg}) 26 | } 27 | return c.JSON(http.StatusInternalServerError, openapi.ErrorResponse{Message: &msg}) 28 | } 29 | 30 | name := user.Name() 31 | email := openapi_types.Email(user.Email()) 32 | return c.JSON(http.StatusOK, openapi.GetUserResponse{ 33 | Id: &id, 34 | Name: &name, 35 | Email: &email, 36 | }) 37 | } 38 | 39 | func (h UserHandlers) GetUser(ctx context.Context, req *protobuf.GetUserRequest) (*protobuf.GetUserResponse, error) { 40 | uid, err := uuid.Parse(req.GetId()) 41 | if err != nil { 42 | return nil, status.Error(codes.InvalidArgument, "invalid UUID") 43 | } 44 | user, err := h.repo.GetUser(ctx, uid) 45 | if err != nil { 46 | if errors.Is(err, users.ErrUserNotFound) { 47 | return nil, status.Error(codes.NotFound, "user not found") 48 | } 49 | return nil, err 50 | } 51 | 52 | return &protobuf.GetUserResponse{ 53 | Id: user.ID().String(), 54 | Name: user.Name(), 55 | Email: user.Email(), 56 | }, nil 57 | } 58 | -------------------------------------------------------------------------------- /internal/application/users/handlers.go: -------------------------------------------------------------------------------- 1 | package users 2 | 3 | import ( 4 | "context" 5 | 6 | "go-echo-template/generated/protobuf" 7 | "go-echo-template/internal/domain/users" 8 | 9 | "github.com/google/uuid" 10 | ) 11 | 12 | type Repository interface { 13 | CreateUser(ctx context.Context, email string, createFn func() (*users.User, error)) (*users.User, error) 14 | UpdateUser(ctx context.Context, id uuid.UUID, updateFn func(*users.User) (bool, error)) (*users.User, error) 15 | GetUser(ctx context.Context, id uuid.UUID) (*users.User, error) 16 | } 17 | 18 | type UserHandlers struct { 19 | protobuf.UnimplementedUserServiceServer 20 | repo Repository 21 | } 22 | 23 | func SetupHandlers(repo Repository) UserHandlers { 24 | return UserHandlers{ 25 | repo: repo, 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /internal/application/users/users_test/create_test.go: -------------------------------------------------------------------------------- 1 | package users_test 2 | 3 | import ( 4 | "bytes" 5 | "context" 6 | "encoding/json" 7 | "net/http" 8 | "net/http/httptest" 9 | 10 | "go-echo-template/generated/openapi" 11 | "go-echo-template/generated/protobuf" 12 | 13 | "github.com/google/uuid" 14 | "github.com/labstack/echo/v4" 15 | ) 16 | 17 | func (s *UsersSuite) TestCreateUser() { 18 | s.Run("HTTP", func() { 19 | userReq := openapi.CreateUserRequest{ 20 | Name: "John Doe", 21 | Email: "john.doe@example.com", 22 | } 23 | reqBody, _ := json.Marshal(userReq) 24 | 25 | req := httptest.NewRequest(http.MethodPost, "/users", bytes.NewBuffer(reqBody)) 26 | req.Header.Set(echo.HeaderContentType, echo.MIMEApplicationJSON) 27 | rec := httptest.NewRecorder() 28 | s.HTTPServer.ServeHTTP(rec, req) 29 | 30 | s.Require().Equal(http.StatusCreated, rec.Code) 31 | var resp openapi.CreateUserResponse 32 | err := json.Unmarshal(rec.Body.Bytes(), &resp) 33 | s.Require().NoError(err) 34 | s.Require().NotEqual("", resp.Id) 35 | s.Require().NotEqual(uuid.Nil, resp.Id) 36 | }) 37 | 38 | s.Run("GRPC", func() { 39 | userReq := protobuf.CreateUserRequest{ 40 | Name: "John Lee", 41 | Email: "john.lee@example.com", 42 | } 43 | resp, err := s.GRPCHandlers.CreateUser(context.Background(), &userReq) 44 | 45 | s.Require().NoError(err) 46 | s.Require().NotEqual("", resp.GetId()) 47 | s.Require().NotEqual(uuid.Nil, resp.GetId()) 48 | }) 49 | } 50 | -------------------------------------------------------------------------------- /internal/application/users/users_test/get_test.go: -------------------------------------------------------------------------------- 1 | package users_test 2 | 3 | import ( 4 | "context" 5 | "encoding/json" 6 | "net/http" 7 | "net/http/httptest" 8 | 9 | "go-echo-template/generated/openapi" 10 | "go-echo-template/generated/protobuf" 11 | "go-echo-template/internal/domain/users" 12 | 13 | "github.com/google/uuid" 14 | ) 15 | 16 | func (s *UsersSuite) TestGetUser() { 17 | user, _ := users.CreateUser("test", "test@test.com") 18 | err := s.UsersRepo.SaveUser(context.Background(), *user) 19 | s.Require().NoError(err) 20 | 21 | s.Run("HTTP", func() { 22 | req := httptest.NewRequest(http.MethodGet, "/users/"+user.ID().String(), nil) 23 | rec := httptest.NewRecorder() 24 | s.HTTPServer.ServeHTTP(rec, req) 25 | 26 | s.Require().Equal(http.StatusOK, rec.Code, rec.Body.String()) 27 | var resp openapi.GetUserResponse 28 | err = json.Unmarshal(rec.Body.Bytes(), &resp) 29 | s.Require().NoError(err) 30 | s.Require().Equal(user.ID(), *resp.Id) 31 | s.Require().Equal(user.Name(), *resp.Name) 32 | s.Require().Equal(user.Email(), string(*resp.Email)) 33 | }) 34 | 35 | s.Run("GRPC", func() { 36 | req := &protobuf.GetUserRequest{Id: user.ID().String()} 37 | resp, err := s.GRPCHandlers.GetUser(context.Background(), req) 38 | 39 | s.Require().NoError(err) 40 | s.Require().Equal(user.ID().String(), resp.GetId()) 41 | s.Require().Equal(user.Name(), resp.GetName()) 42 | s.Require().Equal(user.Email(), resp.GetEmail()) 43 | }) 44 | } 45 | 46 | func (s *UsersSuite) TestGetUserNotFound() { 47 | s.Run("HTTP", func() { 48 | req := httptest.NewRequest(http.MethodGet, "/users/"+uuid.New().String(), nil) 49 | rec := httptest.NewRecorder() 50 | s.HTTPServer.ServeHTTP(rec, req) 51 | 52 | s.Require().Equal(http.StatusNotFound, rec.Code, rec.Body.String()) 53 | s.Require().Equal(`{"message":"user not found"}`+"\n", rec.Body.String()) 54 | }) 55 | 56 | s.Run("GRPC", func() { 57 | req := &protobuf.GetUserRequest{Id: uuid.New().String()} 58 | _, err := s.GRPCHandlers.GetUser(context.Background(), req) 59 | 60 | s.Require().Error(err) 61 | s.Require().Equal("rpc error: code = NotFound desc = user not found", err.Error()) 62 | }) 63 | } 64 | -------------------------------------------------------------------------------- /internal/application/users/users_test/suite_test.go: -------------------------------------------------------------------------------- 1 | package users_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "go-echo-template/internal/application" 7 | "go-echo-template/internal/application/users" 8 | 9 | "github.com/stretchr/testify/suite" 10 | ) 11 | 12 | type UsersSuite struct { 13 | suite.Suite 14 | application.ServerSuite 15 | GRPCHandlers users.UserHandlers 16 | } 17 | 18 | func (s *UsersSuite) SetupTest() { 19 | s.ServerSuite.SetupTest() 20 | s.GRPCHandlers = users.SetupHandlers(s.UsersRepo) 21 | } 22 | 23 | func TestUsersSuite(t *testing.T) { 24 | t.Parallel() 25 | suite.Run(t, new(UsersSuite)) 26 | } 27 | -------------------------------------------------------------------------------- /internal/config.go: -------------------------------------------------------------------------------- 1 | package internal 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "strings" 7 | "time" 8 | 9 | "go-echo-template/pkg/environment" 10 | ) 11 | 12 | const ( 13 | falseStr = "false" 14 | trueStr = "true" 15 | ) 16 | 17 | type Config struct { 18 | Server Server 19 | Sentry Sentry 20 | Redis Redis 21 | Postgres Postgres 22 | } 23 | 24 | func LoadConfig() (Config, error) { 25 | var ( 26 | config Config 27 | err error 28 | ) 29 | 30 | config.Server, err = loadServer() 31 | if err != nil { 32 | return config, fmt.Errorf("could not load server config: %w", err) 33 | } 34 | config.Sentry = loadSentry() 35 | config.Redis, err = loadRedis() 36 | if err != nil { 37 | return config, fmt.Errorf("could not load redis config: %w", err) 38 | } 39 | config.Postgres = loadPostgres() 40 | 41 | return config, nil 42 | } 43 | 44 | type Server struct { 45 | Environment environment.Type 46 | Port string 47 | InterruptTimeout time.Duration 48 | ReadHeaderTimeout time.Duration 49 | PprofPort string 50 | } 51 | 52 | func loadServer() (Server, error) { 53 | var server Server 54 | 55 | server.Environment = environment.Type(getEnv("ENV_TYPE", string(environment.Testing))) 56 | server.Port = getEnv("SERVER_PORT", "8080") 57 | interruptTimeout, err := time.ParseDuration(getEnv("INTERRUPT_TIMEOUT", "2s")) 58 | if err != nil { 59 | return server, fmt.Errorf("could not parse interrupt timeout: %w", err) 60 | } 61 | server.InterruptTimeout = interruptTimeout 62 | readHeaderTimeout, err := time.ParseDuration(getEnv("READ_HEADER_TIMEOUT", "5s")) 63 | if err != nil { 64 | return server, fmt.Errorf("could not parse read header timeout: %w", err) 65 | } 66 | server.ReadHeaderTimeout = readHeaderTimeout 67 | server.PprofPort = getEnv("PPROF_PORT", "6060") 68 | 69 | return server, nil 70 | } 71 | 72 | type Sentry struct { 73 | DSN string 74 | Environment environment.Type 75 | } 76 | 77 | func loadSentry() Sentry { 78 | var sentry Sentry 79 | 80 | sentry.Environment = environment.Type(getEnv("SENTRY_ENVIRONMENT", string(environment.Testing))) 81 | sentry.DSN = getEnv("SENTRY_DSN", "") 82 | 83 | return sentry 84 | } 85 | 86 | type Redis struct { 87 | ClusterMode bool 88 | TLSEnabled bool 89 | Address string 90 | Username string 91 | Password string 92 | Expiration time.Duration 93 | } 94 | 95 | func loadRedis() (Redis, error) { 96 | var redis Redis 97 | 98 | redis.ClusterMode = getEnv("REDIS_CLUSTER_MODE", falseStr) == trueStr 99 | redis.TLSEnabled = getEnv("REDIS_TLS_ENABLED", falseStr) == trueStr 100 | redis.Address = getEnv("REDIS_ADDRESS", "localhost:6379") 101 | redis.Username = getEnv("REDIS_USERNAME", "") 102 | redis.Password = getEnv("REDIS_PASSWORD", "") 103 | redisExpiration := getEnv("REDIS_EXPIRATION", "1m") 104 | expiration, err := time.ParseDuration(redisExpiration) 105 | if err != nil { 106 | return redis, fmt.Errorf("could not parse redis expiration: %w", err) 107 | } 108 | redis.Expiration = expiration 109 | 110 | return redis, nil 111 | } 112 | 113 | type Postgres struct { 114 | Hosts []string 115 | Port string 116 | User string 117 | Password string 118 | Database string 119 | SSL bool 120 | MigrationPath string 121 | } 122 | 123 | func loadPostgres() Postgres { 124 | var postgres Postgres 125 | 126 | postgres.Hosts = strings.Split(getEnv("POSTGRES_HOSTS", "localhost"), ",") 127 | postgres.Port = getEnv("POSTGRES_PORT", "5432") 128 | postgres.User = getEnv("POSTGRES_USER", "postgres") 129 | postgres.Password = getEnv("POSTGRES_PASSWORD", "postgres") 130 | postgres.Database = getEnv("POSTGRES_DATABASE", "postgres") 131 | postgres.SSL = getEnv("POSTGRES_SSL", falseStr) == trueStr 132 | postgres.MigrationPath = getEnv("POSTGRES_MIGRATION_PATH", "") 133 | 134 | return postgres 135 | } 136 | 137 | func getEnv(key, fallback string) string { 138 | if value, exists := os.LookupEnv(key); exists { 139 | return value 140 | } 141 | return fallback 142 | } 143 | -------------------------------------------------------------------------------- /internal/domain/orders/errors.go: -------------------------------------------------------------------------------- 1 | package orders 2 | 3 | import ( 4 | "errors" 5 | ) 6 | 7 | var ( 8 | ErrOrderNotFound = errors.New("order not found") 9 | ErrProductAlreadyReserved = errors.New("product already reserved") 10 | ErrProductNotFound = errors.New("product not found") 11 | ) 12 | -------------------------------------------------------------------------------- /internal/domain/orders/item.go: -------------------------------------------------------------------------------- 1 | package orders 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | 7 | "github.com/google/uuid" 8 | ) 9 | 10 | var ( 11 | ErrInvalidItem = errors.New("invalid order item") 12 | ) 13 | 14 | type Item struct { 15 | id uuid.UUID 16 | name string 17 | price float64 18 | } 19 | 20 | func NewItem(id uuid.UUID, name string, price float64) (*Item, error) { 21 | if name == "" { 22 | return nil, fmt.Errorf("%w: invalid name", ErrInvalidItem) 23 | } 24 | if price <= 0 { 25 | return nil, fmt.Errorf("%w: invalid price: %f", ErrInvalidItem, price) 26 | } 27 | 28 | return &Item{ 29 | id: id, 30 | name: name, 31 | price: price, 32 | }, nil 33 | } 34 | 35 | func (i *Item) ID() uuid.UUID { 36 | return i.id 37 | } 38 | 39 | func (i *Item) Name() string { 40 | return i.name 41 | } 42 | 43 | func (i *Item) Price() float64 { 44 | return i.price 45 | } 46 | -------------------------------------------------------------------------------- /internal/domain/orders/order.go: -------------------------------------------------------------------------------- 1 | package orders 2 | 3 | import ( 4 | "github.com/google/uuid" 5 | ) 6 | 7 | type Order struct { 8 | id uuid.UUID 9 | userID uuid.UUID 10 | status OrderStatus 11 | items []Item 12 | } 13 | 14 | func NewOrder(id uuid.UUID, userID uuid.UUID, status OrderStatus, items []Item) (*Order, error) { 15 | return &Order{ 16 | id: id, 17 | userID: userID, 18 | status: status, 19 | items: items, 20 | }, nil 21 | } 22 | 23 | func CreateOrder(userID uuid.UUID, items []Item) (*Order, error) { 24 | return NewOrder(uuid.New(), userID, OrderStatusCreated, items) 25 | } 26 | 27 | func (o *Order) ID() uuid.UUID { 28 | return o.id 29 | } 30 | 31 | func (o *Order) UserID() uuid.UUID { 32 | return o.userID 33 | } 34 | 35 | func (o *Order) Status() OrderStatus { 36 | return o.status 37 | } 38 | 39 | func (o *Order) Items() []Item { 40 | return o.items 41 | } 42 | 43 | func (o *Order) Price() float64 { 44 | var total float64 45 | for _, item := range o.items { 46 | total += item.Price() 47 | } 48 | return total 49 | } 50 | -------------------------------------------------------------------------------- /internal/domain/orders/statuses.go: -------------------------------------------------------------------------------- 1 | package orders 2 | 3 | type OrderStatus string 4 | 5 | const ( 6 | OrderStatusCreated OrderStatus = "created" 7 | OrderStatusPaymentProcessing OrderStatus = "payment_processing" 8 | OrderStatusPaid OrderStatus = "paid" 9 | OrderStatusShipped OrderStatus = "shipped" 10 | OrderStatusDelivered OrderStatus = "delivered" 11 | ) 12 | -------------------------------------------------------------------------------- /internal/domain/products/product.go: -------------------------------------------------------------------------------- 1 | package products 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | 7 | "github.com/google/uuid" 8 | ) 9 | 10 | var ( 11 | ErrProductNotFound = errors.New("product not found") 12 | ErrInvalidProduct = errors.New("invalid product") 13 | ) 14 | 15 | type Product struct { 16 | id uuid.UUID 17 | name string 18 | price float64 19 | } 20 | 21 | func NewProduct(id uuid.UUID, name string, price float64) (*Product, error) { 22 | if err := validateProductName(name); err != nil { 23 | return nil, err 24 | } 25 | if err := validateProductPrice(price); err != nil { 26 | return nil, err 27 | } 28 | 29 | return &Product{ 30 | id: id, 31 | name: name, 32 | price: price, 33 | }, nil 34 | } 35 | 36 | func CreateProduct(name string, price float64) (*Product, error) { 37 | return NewProduct(uuid.New(), name, price) 38 | } 39 | 40 | func (p *Product) ID() uuid.UUID { 41 | return p.id 42 | } 43 | 44 | func (p *Product) Name() string { 45 | return p.name 46 | } 47 | 48 | func (p *Product) Price() float64 { 49 | return p.price 50 | } 51 | 52 | func validateProductName(name string) error { 53 | if name == "" { 54 | return fmt.Errorf("%w: name is required", ErrInvalidProduct) 55 | } 56 | return nil 57 | } 58 | 59 | func validateProductPrice(price float64) error { 60 | if price <= 0 { 61 | return fmt.Errorf("%w: price must be greater than 0", ErrInvalidProduct) 62 | } 63 | return nil 64 | } 65 | -------------------------------------------------------------------------------- /internal/domain/users/user.go: -------------------------------------------------------------------------------- 1 | package users 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | 7 | "github.com/google/uuid" 8 | ) 9 | 10 | var ( 11 | ErrUserNotFound = errors.New("user not found") 12 | ErrInvalidUser = errors.New("invalid user") 13 | ErrUserValidation = errors.New("validation error") 14 | ErrUserAlreadyExist = errors.New("user already exist") 15 | ) 16 | 17 | type User struct { 18 | id uuid.UUID 19 | name string 20 | email string 21 | } 22 | 23 | func NewUser(id uuid.UUID, name, email string) (*User, error) { 24 | if err := validateUsername(name); err != nil { 25 | return nil, err 26 | } 27 | if err := validateEmail(email); err != nil { 28 | return nil, err 29 | } 30 | 31 | return &User{ 32 | id: id, 33 | name: name, 34 | email: email, 35 | }, nil 36 | } 37 | 38 | func CreateUser(name, email string) (*User, error) { 39 | return NewUser(uuid.New(), name, email) 40 | } 41 | 42 | func (u *User) ID() uuid.UUID { 43 | return u.id 44 | } 45 | 46 | func (u *User) Name() string { 47 | return u.name 48 | } 49 | 50 | func (u *User) Email() string { 51 | return u.email 52 | } 53 | 54 | func (u *User) SendToEmail(_ string) error { 55 | return errors.New("not implemented") 56 | } 57 | 58 | func (u *User) ChangeEmail(email string) error { 59 | if err := validateEmail(email); err != nil { 60 | return err 61 | } 62 | u.email = email 63 | return nil 64 | } 65 | 66 | func validateUsername(username string) error { 67 | if username == "" { 68 | return fmt.Errorf("%w: name is required", ErrUserValidation) 69 | } 70 | return nil 71 | } 72 | 73 | func validateEmail(email string) error { 74 | if email == "" { 75 | return fmt.Errorf("%w: email is required", ErrUserValidation) 76 | } 77 | return nil 78 | } 79 | -------------------------------------------------------------------------------- /internal/infrastructure/orders/in_memory.go: -------------------------------------------------------------------------------- 1 | package orders 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | 7 | domain "go-echo-template/internal/domain/orders" 8 | 9 | "github.com/google/uuid" 10 | ) 11 | 12 | type product struct { 13 | Available bool 14 | } 15 | 16 | type order struct { 17 | UserID uuid.UUID 18 | Status domain.OrderStatus 19 | Items []domain.Item 20 | } 21 | 22 | type InMemoryRepo struct { 23 | domain map[uuid.UUID]order 24 | products map[uuid.UUID]product 25 | } 26 | 27 | func NewInMemoryRepo() *InMemoryRepo { 28 | return &InMemoryRepo{ 29 | domain: make(map[uuid.UUID]order), 30 | products: make(map[uuid.UUID]product), 31 | } 32 | } 33 | 34 | func (r *InMemoryRepo) CreateOrder( 35 | _ context.Context, 36 | itemIDs []uuid.UUID, 37 | createFn func() (*domain.Order, error), 38 | ) (*domain.Order, error) { 39 | if err := r.reserveProducts(itemIDs); err != nil { 40 | return nil, fmt.Errorf("failed to reserve products: %w", err) 41 | } 42 | 43 | entity, err := createFn() 44 | if err != nil { 45 | return nil, fmt.Errorf("failed to create order: %w", err) 46 | } 47 | 48 | r.domain[entity.ID()] = order{ 49 | UserID: entity.UserID(), 50 | Status: entity.Status(), 51 | Items: entity.Items(), 52 | } 53 | 54 | return entity, nil 55 | } 56 | 57 | func (r *InMemoryRepo) reserveProducts(ids []uuid.UUID) error { 58 | for _, id := range ids { 59 | p, ok := r.products[id] 60 | if !ok { 61 | return fmt.Errorf("%w: id %s", domain.ErrProductNotFound, id) 62 | } 63 | if !p.Available { 64 | return fmt.Errorf("%w: id %s", domain.ErrProductAlreadyReserved, id) 65 | } 66 | r.products[id] = product{ 67 | Available: false, 68 | } 69 | } 70 | 71 | return nil 72 | } 73 | 74 | func (r *InMemoryRepo) GetOrder(_ context.Context, id uuid.UUID) (*domain.Order, error) { 75 | o, ok := r.domain[id] 76 | if !ok { 77 | return nil, domain.ErrOrderNotFound 78 | } 79 | 80 | order, err := domain.NewOrder(id, o.UserID, o.Status, o.Items) 81 | if err != nil { 82 | return nil, err 83 | } 84 | 85 | return order, nil 86 | } 87 | -------------------------------------------------------------------------------- /internal/infrastructure/orders/postgres.go: -------------------------------------------------------------------------------- 1 | package orders 2 | 3 | import ( 4 | "context" 5 | "encoding/json" 6 | "fmt" 7 | 8 | "github.com/jmoiron/sqlx" 9 | 10 | "go-echo-template/pkg/postgres" 11 | 12 | hasql "golang.yandex/hasql/sqlx" 13 | 14 | domain "go-echo-template/internal/domain/orders" 15 | 16 | "github.com/google/uuid" 17 | ) 18 | 19 | type itemDB struct { 20 | ID uuid.UUID `json:"id"` 21 | Name string `json:"name"` 22 | Price float64 `json:"price"` 23 | } 24 | 25 | type orderDB struct { 26 | ID uuid.UUID `db:"id"` 27 | UserID uuid.UUID `db:"user_id"` 28 | Status domain.OrderStatus `db:"status"` 29 | Items json.RawMessage `db:"items"` 30 | } 31 | 32 | type productDB struct { 33 | ID uuid.UUID `db:"id"` 34 | Available bool `db:"available"` 35 | } 36 | 37 | type PostgresRepo struct { 38 | cluster *hasql.Cluster 39 | } 40 | 41 | func NewPostgresRepo(cluster *hasql.Cluster) *PostgresRepo { 42 | return &PostgresRepo{ 43 | cluster: cluster, 44 | } 45 | } 46 | 47 | func (r *PostgresRepo) CreateOrder( 48 | ctx context.Context, 49 | itemIDs []uuid.UUID, 50 | createFn func() (*domain.Order, error), 51 | ) (*domain.Order, error) { 52 | db := r.cluster.Primary().DBx() 53 | 54 | var order *domain.Order 55 | err := postgres.RunInTx(ctx, db, func(tx *sqlx.Tx) error { 56 | err := r.reserveProducts(ctx, tx, itemIDs) 57 | if err != nil { 58 | return fmt.Errorf("failed to reserve products: %w", err) 59 | } 60 | 61 | order, err = createFn() 62 | if err != nil { 63 | return fmt.Errorf("failed to create order: %w", err) 64 | } 65 | 66 | items := make([]itemDB, 0, len(order.Items())) 67 | for _, item := range order.Items() { 68 | items = append(items, itemDB{ 69 | ID: item.ID(), 70 | Name: item.Name(), 71 | Price: item.Price(), 72 | }) 73 | } 74 | itemsJSON, err := json.Marshal(items) 75 | if err != nil { 76 | return fmt.Errorf("failed to marshal items: %w", err) 77 | } 78 | orderDB := orderDB{ 79 | ID: order.ID(), 80 | UserID: order.UserID(), 81 | Status: order.Status(), 82 | Items: itemsJSON, 83 | } 84 | 85 | query := ` 86 | INSERT INTO orders (id, user_id, status, items) 87 | VALUES (:id, :user_id, :status, :items) 88 | ` 89 | _, err = db.NamedExecContext(ctx, query, orderDB) 90 | if err != nil { 91 | return fmt.Errorf("failed to execute query: %w", err) 92 | } 93 | 94 | return nil 95 | }) 96 | return order, err 97 | } 98 | 99 | func (r *PostgresRepo) reserveProducts(ctx context.Context, tx *sqlx.Tx, ids []uuid.UUID) error { 100 | query := `SELECT id, available FROM products WHERE id = ANY($1) FOR UPDATE` 101 | var products []productDB 102 | if err := tx.SelectContext(ctx, &products, query, ids); err != nil { 103 | return fmt.Errorf("failed to select products: %w", err) 104 | } 105 | if len(products) != len(ids) { 106 | return fmt.Errorf("%w: selected %d products, expected %d", domain.ErrProductNotFound, len(products), len(ids)) 107 | } 108 | 109 | for _, product := range products { 110 | if !product.Available { 111 | return fmt.Errorf("%w: id %s", domain.ErrProductAlreadyReserved, product.ID) 112 | } 113 | } 114 | 115 | query = `UPDATE products SET available = false WHERE id = ANY($1)` 116 | if _, err := tx.ExecContext(ctx, query, ids); err != nil { 117 | return fmt.Errorf("failed to update products' availability: %w", err) 118 | } 119 | 120 | return nil 121 | } 122 | 123 | func (r *PostgresRepo) GetOrder(ctx context.Context, id uuid.UUID) (*domain.Order, error) { 124 | db := r.cluster.StandbyPreferred().DBx() 125 | 126 | var order orderDB 127 | query := `SELECT id, user_id, status, items FROM orders WHERE id = $1` 128 | if err := db.GetContext(ctx, &order, query, id); err != nil { 129 | return nil, fmt.Errorf("failed to select order: %w", err) 130 | } 131 | 132 | var items []domain.Item 133 | if err := json.Unmarshal(order.Items, &items); err != nil { 134 | return nil, fmt.Errorf("failed to unmarshal items: %w", err) 135 | } 136 | 137 | entity, err := domain.NewOrder(order.ID, order.UserID, order.Status, items) 138 | if err != nil { 139 | return nil, fmt.Errorf("failed to init order entity: %w", err) 140 | } 141 | 142 | return entity, nil 143 | } 144 | -------------------------------------------------------------------------------- /internal/infrastructure/products/in_memory.go: -------------------------------------------------------------------------------- 1 | package products 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | 7 | "go-echo-template/internal/domain/products" 8 | 9 | "github.com/google/uuid" 10 | ) 11 | 12 | type product struct { 13 | Name string 14 | Price float64 15 | } 16 | 17 | type InMemoryRepo struct { 18 | products map[uuid.UUID]product 19 | } 20 | 21 | func NewInMemoryRepo() *InMemoryRepo { 22 | return &InMemoryRepo{ 23 | products: make(map[uuid.UUID]product), 24 | } 25 | } 26 | 27 | func (r *InMemoryRepo) CreateProducts(_ context.Context, createFn func() ([]products.Product, error)) error { 28 | ps, err := createFn() 29 | if err != nil { 30 | return fmt.Errorf("failed to create products: %w", err) 31 | } 32 | for _, p := range ps { 33 | r.products[p.ID()] = product{ 34 | Name: p.Name(), 35 | Price: p.Price(), 36 | } 37 | } 38 | return nil 39 | } 40 | 41 | func (r *InMemoryRepo) GetProducts(_ context.Context, ids []uuid.UUID) ([]products.Product, error) { 42 | ps := make([]products.Product, 0, len(ids)) 43 | for _, id := range ids { 44 | p, ok := r.products[id] 45 | if !ok { 46 | return nil, fmt.Errorf("%w: id %s", products.ErrProductNotFound, id) 47 | } 48 | product, err := products.NewProduct(id, p.Name, p.Price) 49 | if err != nil { 50 | return nil, err 51 | } 52 | ps = append(ps, *product) 53 | } 54 | return ps, nil 55 | } 56 | -------------------------------------------------------------------------------- /internal/infrastructure/products/postgres.go: -------------------------------------------------------------------------------- 1 | package products 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | 7 | "github.com/jmoiron/sqlx" 8 | 9 | hasql "golang.yandex/hasql/sqlx" 10 | 11 | domain "go-echo-template/internal/domain/products" 12 | "go-echo-template/pkg/postgres" 13 | 14 | "github.com/google/uuid" 15 | ) 16 | 17 | type productsDB struct { 18 | ID uuid.UUID `db:"id"` 19 | Name string `db:"name"` 20 | Price float64 `db:"price"` 21 | } 22 | 23 | type PostgresRepo struct { 24 | cluster *hasql.Cluster 25 | } 26 | 27 | func NewPostgresRepo(cluster *hasql.Cluster) *PostgresRepo { 28 | return &PostgresRepo{ 29 | cluster: cluster, 30 | } 31 | } 32 | 33 | func (r *PostgresRepo) CreateProducts(ctx context.Context, createFn func() ([]domain.Product, error)) error { 34 | db := r.cluster.Primary().DBx() 35 | return postgres.RunInTx(ctx, db, func(tx *sqlx.Tx) error { 36 | ps, err := createFn() 37 | if err != nil { 38 | return fmt.Errorf("failed to create products: %w", err) 39 | } 40 | 41 | //nolint:sqlclosecheck // Linter bug because of RunInTx, it's closing the statement in defer 42 | stmt, err := tx.PreparexContext(ctx, ` 43 | INSERT INTO products (id, name, price) 44 | VALUES ($1, $2, $3) 45 | ON CONFLICT (id) DO UPDATE SET 46 | name = excluded.name, 47 | price = excluded.price 48 | `) 49 | if err != nil { 50 | return fmt.Errorf("failed to prepare statement: %w", err) 51 | } 52 | defer stmt.Close() 53 | 54 | for _, p := range ps { 55 | _, err = stmt.ExecContext(ctx, p.ID(), p.Name(), p.Price()) 56 | if err != nil { 57 | return fmt.Errorf("failed to execute statement: %w", err) 58 | } 59 | } 60 | 61 | return nil 62 | }) 63 | } 64 | 65 | func (r *PostgresRepo) GetProducts(ctx context.Context, id []uuid.UUID) ([]domain.Product, error) { 66 | db := r.cluster.StandbyPreferred().DBx() 67 | var productsDB []productsDB 68 | query := `SELECT id, name, price FROM products WHERE id = ANY($1) FOR UPDATE` 69 | if err := db.SelectContext(ctx, &productsDB, query, id); err != nil { 70 | return nil, fmt.Errorf("failed to select products: %w", err) 71 | } 72 | 73 | products := make([]domain.Product, 0, len(productsDB)) 74 | for _, p := range productsDB { 75 | product, err := domain.NewProduct(p.ID, p.Name, p.Price) 76 | if err != nil { 77 | return nil, fmt.Errorf("failed to init product entity: %w", err) 78 | } 79 | products = append(products, *product) 80 | } 81 | 82 | return products, nil 83 | } 84 | -------------------------------------------------------------------------------- /internal/infrastructure/users/in_memory.go: -------------------------------------------------------------------------------- 1 | package users 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | 7 | domain "go-echo-template/internal/domain/users" 8 | 9 | "github.com/google/uuid" 10 | ) 11 | 12 | type user struct { 13 | Name string 14 | Email string 15 | } 16 | 17 | type InMemoryRepo struct { 18 | users map[uuid.UUID]user 19 | } 20 | 21 | func NewInMemoryRepo() *InMemoryRepo { 22 | return &InMemoryRepo{ 23 | users: make(map[uuid.UUID]user), 24 | } 25 | } 26 | 27 | func (r *InMemoryRepo) CreateUser( 28 | _ context.Context, 29 | email string, 30 | createFn func() (*domain.User, error), 31 | ) (*domain.User, error) { 32 | if r.checkUserExist(email) { 33 | return nil, domain.ErrUserAlreadyExist 34 | } 35 | 36 | u, err := createFn() 37 | if err != nil { 38 | return nil, fmt.Errorf("failed to create user: %w", err) 39 | } 40 | 41 | r.users[u.ID()] = user{ 42 | Name: u.Name(), 43 | Email: email, 44 | } 45 | return u, nil 46 | } 47 | 48 | func (r *InMemoryRepo) UpdateUser( 49 | _ context.Context, 50 | id uuid.UUID, 51 | updateFn func(*domain.User) (bool, error), 52 | ) (*domain.User, error) { 53 | u, ok := r.users[id] 54 | if !ok { 55 | return nil, domain.ErrUserNotFound 56 | } 57 | 58 | entity, err := domain.NewUser(id, u.Name, u.Email) 59 | if err != nil { 60 | return nil, err 61 | } 62 | updated, err := updateFn(entity) 63 | if err != nil { 64 | return nil, fmt.Errorf("failed to update user: %w", err) 65 | } 66 | if !updated { 67 | return entity, nil 68 | } 69 | 70 | r.users[id] = user{ 71 | Name: entity.Name(), 72 | Email: entity.Email(), 73 | } 74 | return entity, nil 75 | } 76 | 77 | func (r *InMemoryRepo) SaveUser(_ context.Context, u domain.User) error { 78 | r.users[u.ID()] = user{ 79 | Name: u.Name(), 80 | Email: u.Email(), 81 | } 82 | return nil 83 | } 84 | 85 | func (r *InMemoryRepo) GetUser(_ context.Context, id uuid.UUID) (*domain.User, error) { 86 | u, ok := r.users[id] 87 | if !ok { 88 | return nil, domain.ErrUserNotFound 89 | } 90 | user, err := domain.NewUser(id, u.Name, u.Email) 91 | if err != nil { 92 | return nil, err 93 | } 94 | return user, nil 95 | } 96 | 97 | func (r *InMemoryRepo) checkUserExist(email string) bool { 98 | for _, u := range r.users { 99 | if u.Email == email { 100 | return true 101 | } 102 | } 103 | return false 104 | } 105 | -------------------------------------------------------------------------------- /internal/infrastructure/users/postgres.go: -------------------------------------------------------------------------------- 1 | package users 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | 7 | "github.com/google/uuid" 8 | hasql "golang.yandex/hasql/sqlx" 9 | 10 | domain "go-echo-template/internal/domain/users" 11 | ) 12 | 13 | type userDB struct { 14 | ID uuid.UUID `db:"id"` 15 | Name string `db:"name"` 16 | Email string `db:"email"` 17 | } 18 | 19 | type PostgresRepo struct { 20 | cluster *hasql.Cluster 21 | } 22 | 23 | func NewPostgresRepo(cluster *hasql.Cluster) *PostgresRepo { 24 | return &PostgresRepo{ 25 | cluster: cluster, 26 | } 27 | } 28 | 29 | func (r *PostgresRepo) CreateUser( 30 | ctx context.Context, 31 | email string, 32 | createFn func() (*domain.User, error), 33 | ) (*domain.User, error) { 34 | exist, err := r.checkUserExist(ctx, email) 35 | if err != nil { 36 | return nil, fmt.Errorf("failed to check user exist: %w", err) 37 | } 38 | if exist { 39 | return nil, domain.ErrUserAlreadyExist 40 | } 41 | 42 | user, err := createFn() 43 | if err != nil { 44 | return nil, fmt.Errorf("failed to create user: %w", err) 45 | } 46 | 47 | db := r.cluster.Primary().DBx() 48 | userDB := userDB{ 49 | ID: user.ID(), 50 | Name: user.Name(), 51 | Email: user.Email(), 52 | } 53 | query := ` 54 | INSERT INTO users (id, name, email) 55 | VALUES (:id, :name, :email) 56 | ` 57 | _, err = db.NamedExecContext(ctx, query, userDB) 58 | if err != nil { 59 | return nil, fmt.Errorf("failed to exec insert user query: %w", err) 60 | } 61 | 62 | return user, nil 63 | } 64 | 65 | func (r *PostgresRepo) UpdateUser( 66 | ctx context.Context, 67 | id uuid.UUID, 68 | updateFn func(*domain.User) (bool, error), 69 | ) (*domain.User, error) { 70 | db := r.cluster.Primary().DBx() 71 | user, err := r.GetUser(ctx, id) 72 | if err != nil { 73 | return nil, fmt.Errorf("failed to get user: %w", err) 74 | } 75 | updated, err := updateFn(user) 76 | if err != nil { 77 | return nil, fmt.Errorf("failed to update user: %w", err) 78 | } 79 | if !updated { 80 | return user, nil 81 | } 82 | userDB := userDB{ 83 | ID: user.ID(), 84 | Name: user.Name(), 85 | Email: user.Email(), 86 | } 87 | query := ` 88 | UPDATE users 89 | SET name = :name, email = :email 90 | WHERE id = :id 91 | ` 92 | _, err = db.NamedExecContext(ctx, query, userDB) 93 | if err != nil { 94 | return nil, fmt.Errorf("failed to exec update user query: %w", err) 95 | } 96 | return user, nil 97 | } 98 | 99 | func (r *PostgresRepo) SaveUser(ctx context.Context, entity domain.User) error { 100 | db := r.cluster.Primary().DBx() 101 | user := userDB{ 102 | ID: entity.ID(), 103 | Name: entity.Name(), 104 | Email: entity.Email(), 105 | } 106 | query := ` 107 | INSERT INTO users (id, name, email) 108 | VALUES (:id, :name, :email) 109 | ON CONFLICT (id) DO UPDATE SET name = :name, email = :email 110 | ` 111 | _, err := db.NamedExecContext(ctx, query, user) 112 | if err != nil { 113 | return fmt.Errorf("failed to upsert user: %w", err) 114 | } 115 | return nil 116 | } 117 | 118 | func (r *PostgresRepo) GetUser(ctx context.Context, id uuid.UUID) (*domain.User, error) { 119 | db := r.cluster.StandbyPreferred().DBx() 120 | var user userDB 121 | query := `SELECT id, name, email FROM users WHERE id = $1` 122 | if err := db.GetContext(ctx, &user, query, id); err != nil { 123 | return nil, fmt.Errorf("failed to select user: %w", err) 124 | } 125 | entity, err := domain.NewUser(user.ID, user.Name, user.Email) 126 | if err != nil { 127 | return nil, fmt.Errorf("failed to init user entity: %w", err) 128 | } 129 | return entity, nil 130 | } 131 | 132 | func (r *PostgresRepo) DeleteUser(ctx context.Context, id uuid.UUID) error { 133 | db := r.cluster.Primary().DBx() 134 | query := `DELETE FROM users WHERE id = $1` 135 | _, err := db.ExecContext(ctx, query, id) 136 | if err != nil { 137 | return fmt.Errorf("failed to delete user: %w", err) 138 | } 139 | return nil 140 | } 141 | 142 | func (r *PostgresRepo) checkUserExist(ctx context.Context, email string) (bool, error) { 143 | db := r.cluster.Primary().DBx() 144 | 145 | query := `SELECT EXISTS (SELECT 1 FROM users WHERE email = $1)` 146 | var exist bool 147 | err := db.GetContext(ctx, &exist, query, email) 148 | if err != nil { 149 | return false, fmt.Errorf("failed to select user exist: %w", err) 150 | } 151 | 152 | return exist, nil 153 | } 154 | -------------------------------------------------------------------------------- /internal/infrastructure/users/redis.go: -------------------------------------------------------------------------------- 1 | // Description: The file contains just the example of the Redis repository implementation and isn't used in the project. 2 | 3 | package users 4 | 5 | import ( 6 | "context" 7 | "crypto/tls" 8 | "encoding/json" 9 | "errors" 10 | "fmt" 11 | "time" 12 | 13 | "github.com/go-redis/redis/v8" 14 | "github.com/google/uuid" 15 | 16 | "go-echo-template/internal/domain/users" 17 | ) 18 | 19 | type redisUser struct { 20 | Name string `json:"name"` 21 | Email string `json:"email"` 22 | } 23 | 24 | type redisClient interface { 25 | Set(ctx context.Context, key string, value interface{}, expiration time.Duration) *redis.StatusCmd 26 | Get(ctx context.Context, key string) *redis.StringCmd 27 | Del(ctx context.Context, keys ...string) *redis.IntCmd 28 | } 29 | 30 | type RedisRepo struct { 31 | client redisClient 32 | expiration time.Duration 33 | } 34 | 35 | func NewRedisRepo(clusterMode, tlsEnabled bool, addr, username, password string, expiration time.Duration) *RedisRepo { 36 | var ( 37 | tlsConfig *tls.Config 38 | client redisClient 39 | ) 40 | if tlsEnabled { 41 | tlsConfig = &tls.Config{ 42 | InsecureSkipVerify: true, //nolint:gosec // It's okay in intranet 43 | } 44 | } 45 | 46 | if clusterMode { 47 | client = redis.NewClusterClient(&redis.ClusterOptions{ 48 | Addrs: []string{addr}, 49 | Username: username, 50 | Password: password, 51 | TLSConfig: tlsConfig, 52 | }) 53 | } else { 54 | client = redis.NewClient(&redis.Options{ 55 | Addr: addr, 56 | Username: username, 57 | Password: password, 58 | TLSConfig: tlsConfig, 59 | }) 60 | } 61 | 62 | return &RedisRepo{ 63 | client: client, 64 | expiration: expiration, 65 | } 66 | } 67 | 68 | func (r *RedisRepo) SaveUser(ctx context.Context, user users.User) error { 69 | ru := redisUser{ 70 | Name: user.Name(), 71 | Email: user.Email(), 72 | } 73 | val, err := json.Marshal(ru) 74 | if err != nil { 75 | return fmt.Errorf("failed to serialize user: %w", err) 76 | } 77 | 78 | err = r.client.Set(ctx, user.ID().String(), val, r.expiration).Err() 79 | if err != nil { 80 | return fmt.Errorf("failed to save user to redis: %w", err) 81 | } 82 | return nil 83 | } 84 | 85 | func (r *RedisRepo) GetUser(ctx context.Context, id uuid.UUID) (*users.User, error) { 86 | val, err := r.client.Get(ctx, id.String()).Result() 87 | if err != nil { 88 | if errors.Is(err, redis.Nil) { 89 | return nil, users.ErrUserNotFound 90 | } 91 | return nil, fmt.Errorf("failed to get user from redis: %w", err) 92 | } 93 | 94 | var ru redisUser 95 | err = json.Unmarshal([]byte(val), &ru) 96 | if err != nil { 97 | return nil, fmt.Errorf("failed to deserialize user: %w", err) 98 | } 99 | 100 | return users.NewUser(id, ru.Name, ru.Email) 101 | } 102 | 103 | func (r *RedisRepo) DeleteUser(ctx context.Context, id uuid.UUID) error { 104 | err := r.client.Del(ctx, id.String()).Err() 105 | if err != nil { 106 | return fmt.Errorf("failed to delete user from redis: %w", err) 107 | } 108 | return nil 109 | } 110 | -------------------------------------------------------------------------------- /internal/migrate.go: -------------------------------------------------------------------------------- 1 | package internal 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "fmt" 7 | 8 | "go-echo-template/pkg/logger" 9 | "go-echo-template/pkg/sentry" 10 | 11 | "github.com/golang-migrate/migrate/v4" 12 | _ "github.com/golang-migrate/migrate/v4/database/postgres" 13 | _ "github.com/golang-migrate/migrate/v4/source/file" 14 | 15 | "go-echo-template/pkg/postgres" 16 | ) 17 | 18 | func Migrate(cfg Config) error { 19 | if err := sentry.Init(cfg.Sentry.DSN, cfg.Sentry.Environment); err != nil { 20 | return fmt.Errorf("failed to init sentry: %w", err) 21 | } 22 | logger.Setup() 23 | 24 | connData, err := postgres.NewConnectionData( 25 | cfg.Postgres.Hosts, 26 | cfg.Postgres.Database, 27 | cfg.Postgres.User, 28 | cfg.Postgres.Password, 29 | cfg.Postgres.Port, 30 | cfg.Postgres.SSL, 31 | ) 32 | if err != nil { 33 | return fmt.Errorf("failed to init postgres connection data: %w", err) 34 | } 35 | cluster, err := postgres.InitCluster(context.Background(), connData) 36 | if err != nil { 37 | return fmt.Errorf("failed to init postgres cluster: %w", err) 38 | } 39 | 40 | masterHost := cluster.Primary().Addr() 41 | connURL := connData.URL(masterHost) 42 | 43 | m, err := migrate.New( 44 | cfg.Postgres.MigrationPath, 45 | connURL, 46 | ) 47 | if err != nil { 48 | return fmt.Errorf("failed to create migrate instance: %w", err) 49 | } 50 | if err := m.Up(); err != nil && !errors.Is(err, migrate.ErrNoChange) { 51 | return fmt.Errorf("migration failed: %w", err) 52 | } 53 | 54 | return nil 55 | } 56 | -------------------------------------------------------------------------------- /internal/run.go: -------------------------------------------------------------------------------- 1 | package internal 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "fmt" 7 | "log/slog" 8 | "net/http" 9 | _ "net/http/pprof" //nolint:gosec // pprof port is not exposed to the internet 10 | "os" 11 | "os/signal" 12 | "strings" 13 | 14 | "golang.org/x/net/http2" 15 | "golang.org/x/net/http2/h2c" 16 | "golang.org/x/sync/errgroup" 17 | hasql "golang.yandex/hasql/sqlx" 18 | 19 | "go-echo-template/internal/application" 20 | ordersInfra "go-echo-template/internal/infrastructure/orders" 21 | productsInfra "go-echo-template/internal/infrastructure/products" 22 | usersInfra "go-echo-template/internal/infrastructure/users" 23 | "go-echo-template/pkg/logger" 24 | "go-echo-template/pkg/postgres" 25 | "go-echo-template/pkg/sentry" 26 | ) 27 | 28 | func Run(cfg Config) error { 29 | if err := sentry.Init(cfg.Sentry.DSN, cfg.Sentry.Environment); err != nil { 30 | return fmt.Errorf("failed to init sentry: %w", err) 31 | } 32 | logger.Setup() 33 | 34 | connData, err := postgres.NewConnectionData( 35 | cfg.Postgres.Hosts, 36 | cfg.Postgres.Database, 37 | cfg.Postgres.User, 38 | cfg.Postgres.Password, 39 | cfg.Postgres.Port, 40 | cfg.Postgres.SSL, 41 | ) 42 | if err != nil { 43 | return fmt.Errorf("failed to init postgres connection data: %w", err) 44 | } 45 | cluster, err := postgres.InitCluster(context.Background(), connData) 46 | if err != nil { 47 | return fmt.Errorf("failed to init postgres cluster: %w", err) 48 | } 49 | 50 | g, ctx := errgroup.WithContext(context.Background()) 51 | ctx, stop := signal.NotifyContext(ctx, os.Interrupt) 52 | defer stop() 53 | 54 | startServers(ctx, g, cluster, cfg) 55 | if cfg.Server.PprofPort != "" { 56 | startPprofServer(ctx, g, cfg) 57 | } 58 | 59 | if err := g.Wait(); err != nil && !errors.Is(err, context.Canceled) { 60 | return fmt.Errorf("server exited with error: %w", err) 61 | } 62 | return nil 63 | } 64 | 65 | func startServers(ctx context.Context, g *errgroup.Group, cluster *hasql.Cluster, cfg Config) { 66 | userRepo := usersInfra.NewPostgresRepo(cluster) 67 | productRepo := productsInfra.NewPostgresRepo(cluster) 68 | orderRepo := ordersInfra.NewPostgresRepo(cluster) 69 | 70 | httpServer := application.SetupHTTPServer(userRepo, orderRepo, productRepo) 71 | grpcServer := application.SetupGRPCServer(userRepo, orderRepo, productRepo) 72 | 73 | address := "0.0.0.0:" + cfg.Server.Port 74 | server := &http.Server{ 75 | Addr: address, 76 | Handler: h2c.NewHandler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 77 | if r.ProtoMajor == 2 && strings.Contains(r.Header.Get("Content-Type"), "application/grpc") { 78 | grpcServer.ServeHTTP(w, r) 79 | } else { 80 | httpServer.ServeHTTP(w, r) 81 | } 82 | }), &http2.Server{}), 83 | ReadHeaderTimeout: cfg.Server.ReadHeaderTimeout, 84 | } 85 | 86 | g.Go(func() error { 87 | slog.Info("Starting server http and grpc server at " + address) 88 | if err := server.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) { 89 | return err 90 | } 91 | slog.Info("Http and grpc server shut down gracefully") 92 | return nil 93 | }) 94 | g.Go(func() error { 95 | <-ctx.Done() 96 | shutdownCtx, cancel := context.WithTimeout(context.Background(), cfg.Server.InterruptTimeout) 97 | defer cancel() 98 | err := server.Shutdown(shutdownCtx) 99 | if err != nil { 100 | return err 101 | } 102 | return nil 103 | }) 104 | } 105 | 106 | func startPprofServer(ctx context.Context, g *errgroup.Group, cfg Config) { 107 | pprofAddress := "0.0.0.0:" + cfg.Server.PprofPort 108 | //nolint:gosec // pprofServer is not exposed to the internet 109 | pprofServer := &http.Server{Addr: pprofAddress, Handler: http.DefaultServeMux} 110 | g.Go(func() error { 111 | slog.Info("Starting pprof server at " + pprofAddress) 112 | if err := pprofServer.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) { 113 | return err 114 | } 115 | slog.Info("Pprof server shut down gracefully") 116 | return nil 117 | }) 118 | g.Go(func() error { 119 | <-ctx.Done() 120 | shutdownCtx, cancel := context.WithTimeout(context.Background(), cfg.Server.InterruptTimeout) 121 | defer cancel() 122 | err := pprofServer.Shutdown(shutdownCtx) 123 | if err != nil { 124 | return err 125 | } 126 | return nil 127 | }) 128 | } 129 | -------------------------------------------------------------------------------- /internal/service/orders/create.go: -------------------------------------------------------------------------------- 1 | package orders 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | 7 | ordersDomain "go-echo-template/internal/domain/orders" 8 | productsDomain "go-echo-template/internal/domain/products" 9 | 10 | "github.com/google/uuid" 11 | ) 12 | 13 | type Item struct { 14 | ID uuid.UUID 15 | } 16 | 17 | func (s *OrderCreationService) CreateOrder( 18 | ctx context.Context, 19 | userID uuid.UUID, 20 | items []Item, 21 | ) (*ordersDomain.Order, error) { 22 | // check if user exists 23 | _, err := s.userRepo.GetUser(ctx, userID) 24 | if err != nil { 25 | return nil, err 26 | } 27 | 28 | itemIDs := getItemIDs(items) 29 | order, err := s.orderRepo.CreateOrder(ctx, itemIDs, func() (*ordersDomain.Order, error) { 30 | products, err := s.productRepo.GetProducts(ctx, itemIDs) 31 | if err != nil { 32 | return nil, fmt.Errorf("failed to get products: %w", err) 33 | } 34 | 35 | orderItems, err := makeOrderItems(items, products) 36 | if err != nil { 37 | return nil, fmt.Errorf("failed to create order items: %w", err) 38 | } 39 | return ordersDomain.CreateOrder(userID, orderItems) 40 | }) 41 | 42 | if err != nil { 43 | return nil, fmt.Errorf("failed to create order: %w", err) 44 | } 45 | 46 | return order, nil 47 | } 48 | 49 | func makeOrderItems(items []Item, ps []productsDomain.Product) ([]ordersDomain.Item, error) { 50 | orderItems := make([]ordersDomain.Item, 0, len(items)) 51 | for i, product := range ps { 52 | item := items[i] 53 | orderItem, err := ordersDomain.NewItem(item.ID, product.Name(), product.Price()) 54 | if err != nil { 55 | return nil, err 56 | } 57 | orderItems = append(orderItems, *orderItem) 58 | } 59 | return orderItems, nil 60 | } 61 | 62 | func getItemIDs(items []Item) []uuid.UUID { 63 | itemIDs := make([]uuid.UUID, 0, len(items)) 64 | for _, i := range items { 65 | itemIDs = append(itemIDs, i.ID) 66 | } 67 | return itemIDs 68 | } 69 | -------------------------------------------------------------------------------- /internal/service/orders/service.go: -------------------------------------------------------------------------------- 1 | package orders 2 | 3 | import ( 4 | "context" 5 | 6 | "go-echo-template/internal/domain/orders" 7 | "go-echo-template/internal/domain/products" 8 | "go-echo-template/internal/domain/users" 9 | 10 | "github.com/google/uuid" 11 | ) 12 | 13 | type OrderRepository interface { 14 | CreateOrder(ctx context.Context, itemIDs []uuid.UUID, createFn func() (*orders.Order, error)) (*orders.Order, error) 15 | GetOrder(ctx context.Context, id uuid.UUID) (*orders.Order, error) 16 | } 17 | 18 | type UserRepository interface { 19 | GetUser(ctx context.Context, id uuid.UUID) (*users.User, error) 20 | } 21 | 22 | type ProductRepository interface { 23 | GetProducts(ctx context.Context, ids []uuid.UUID) ([]products.Product, error) 24 | } 25 | 26 | type OrderCreationService struct { 27 | orderRepo OrderRepository 28 | userRepo UserRepository 29 | productRepo ProductRepository 30 | } 31 | 32 | func NewOrderCreationService(or OrderRepository, ur UserRepository, pr ProductRepository) *OrderCreationService { 33 | return &OrderCreationService{ 34 | orderRepo: or, 35 | userRepo: ur, 36 | productRepo: pr, 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /migrations/000001_add_users_table.down.sql: -------------------------------------------------------------------------------- 1 | DROP TABLE users; -------------------------------------------------------------------------------- /migrations/000001_add_users_table.up.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE users ( 2 | id UUID PRIMARY KEY, 3 | name VARCHAR(255) NOT NULL, 4 | email VARCHAR(255) UNIQUE NOT NULL, 5 | created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP 6 | ); -------------------------------------------------------------------------------- /migrations/20240919100509_add_products_table.down.sql: -------------------------------------------------------------------------------- 1 | DROP TABLE products; -------------------------------------------------------------------------------- /migrations/20240919100509_add_products_table.up.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE products ( 2 | id UUID PRIMARY KEY, 3 | name VARCHAR(255) NOT NULL, 4 | price NUMERIC(10, 2) NOT NULL, 5 | available BOOLEAN NOT NULL DEFAULT TRUE 6 | ); -------------------------------------------------------------------------------- /migrations/20240919131123_add_orders_table.down.sql: -------------------------------------------------------------------------------- 1 | DROP TABLE orders; -------------------------------------------------------------------------------- /migrations/20240919131123_add_orders_table.up.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE orders ( 2 | id UUID PRIMARY KEY, 3 | user_id UUID NOT NULL, 4 | status VARCHAR(255) NOT NULL, 5 | items JSONB NOT NULL 6 | ); -------------------------------------------------------------------------------- /pkg/contextkeys/contextkeys.go: -------------------------------------------------------------------------------- 1 | package contextkeys 2 | 3 | type CtxKey string 4 | 5 | const RequestIDCtxKey CtxKey = "request_id" 6 | const TraceIDCtxKey CtxKey = "trace_id" 7 | -------------------------------------------------------------------------------- /pkg/echomiddleware/echomiddleware_test/sloglogger_test.go: -------------------------------------------------------------------------------- 1 | package echomiddleware_test 2 | 3 | import ( 4 | "context" 5 | "log/slog" 6 | "net/http" 7 | "net/http/httptest" 8 | "testing" 9 | 10 | "github.com/labstack/echo/v4" 11 | "github.com/stretchr/testify/mock" 12 | "github.com/stretchr/testify/require" 13 | 14 | "go-echo-template/pkg/echomiddleware" 15 | ) 16 | 17 | type MockLogger struct { 18 | mock.Mock 19 | } 20 | 21 | func (m *MockLogger) LogAttrs(ctx context.Context, level slog.Level, msg string, attrs ...slog.Attr) { 22 | m.Called(ctx, level, msg, attrs) 23 | } 24 | 25 | func TestSlogLoggerMiddleware(t *testing.T) { 26 | e := echo.New() 27 | req := httptest.NewRequest(http.MethodGet, "/", nil) 28 | req.Header.Add(echomiddleware.RequestIDHeader, "some-id") 29 | req.Header.Add(echomiddleware.TraceParentHeader, "aa-bb-cc-dd") 30 | rec := httptest.NewRecorder() 31 | c := e.NewContext(req, rec) 32 | 33 | mockLogger := new(MockLogger) 34 | mockLogger.On("LogAttrs", mock.Anything, slog.LevelInfo, "REQUEST", mock.Anything) 35 | 36 | m := echomiddleware.SlogLoggerMiddleware(mockLogger) 37 | h := m(func(c echo.Context) error { 38 | return c.String(http.StatusOK, "test") 39 | }) 40 | err := h(c) 41 | 42 | require.NoError(t, err) 43 | require.Equal(t, http.StatusOK, rec.Code) 44 | mockLogger.AssertExpectations(t) 45 | } 46 | -------------------------------------------------------------------------------- /pkg/echomiddleware/requestcontext.go: -------------------------------------------------------------------------------- 1 | package echomiddleware 2 | 3 | import ( 4 | "context" 5 | 6 | "go-echo-template/pkg/contextkeys" 7 | 8 | "github.com/labstack/echo/v4" 9 | ) 10 | 11 | func PutRequestIDContext(next echo.HandlerFunc) echo.HandlerFunc { 12 | return func(c echo.Context) error { 13 | reqID := getRequestID(c.Request().Header) 14 | traceID := getTraceID(c.Request().Header) 15 | ctx := c.Request().Context() 16 | ctx = context.WithValue(ctx, contextkeys.RequestIDCtxKey, reqID) 17 | ctx = context.WithValue(ctx, contextkeys.TraceIDCtxKey, traceID) 18 | c.SetRequest(c.Request().WithContext(ctx)) 19 | return next(c) 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /pkg/echomiddleware/sentrycontext.go: -------------------------------------------------------------------------------- 1 | package echomiddleware 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/getsentry/sentry-go" 7 | sentryecho "github.com/getsentry/sentry-go/echo" 8 | "github.com/labstack/echo/v4" 9 | ) 10 | 11 | // PutSentryContext adding sentryHub to request context.Context. 12 | func PutSentryContext(next echo.HandlerFunc) echo.HandlerFunc { 13 | return func(c echo.Context) error { 14 | if hub := sentryecho.GetHubFromContext(c); hub != nil { 15 | hub.ConfigureScope(func(scope *sentry.Scope) { 16 | scope.SetRequest(c.Request()) 17 | }) 18 | ctx := c.Request().Context() 19 | ctx = context.WithValue(ctx, sentry.HubContextKey, hub) 20 | c.SetRequest(c.Request().WithContext(ctx)) 21 | } 22 | return next(c) 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /pkg/echomiddleware/shared.go: -------------------------------------------------------------------------------- 1 | package echomiddleware 2 | 3 | import ( 4 | "net/http" 5 | "strings" 6 | ) 7 | 8 | const ( 9 | AwsRequestIDHeader = "X-Amzn-Trace-Id" 10 | RequestIDHeader = "X-Request-Id" 11 | TraceParentHeader = "Traceparent" 12 | ) 13 | 14 | func getRequestID(headers map[string][]string) string { 15 | var reqID string 16 | if val, ok := headers[http.CanonicalHeaderKey(RequestIDHeader)]; ok && len(val) > 0 { 17 | reqID = val[0] 18 | } 19 | // Using aws request id header if present 20 | if val, ok := headers[AwsRequestIDHeader]; ok && len(val) > 0 { 21 | reqID = val[0] 22 | } 23 | return reqID 24 | } 25 | 26 | func getTraceID(headers map[string][]string) string { 27 | traceID := "0" 28 | if val, ok := headers[http.CanonicalHeaderKey(TraceParentHeader)]; ok && len(val) > 0 { 29 | parentVal := val[0] 30 | if strings.Count(parentVal, "-") == parentSeparatorNumber { 31 | traceID = strings.Split(parentVal, "-")[1] 32 | } 33 | } 34 | return traceID 35 | } 36 | -------------------------------------------------------------------------------- /pkg/echomiddleware/sloglogger.go: -------------------------------------------------------------------------------- 1 | package echomiddleware 2 | 3 | import ( 4 | "context" 5 | "log/slog" 6 | "net/http" 7 | 8 | "go-echo-template/pkg/contextkeys" 9 | 10 | "github.com/labstack/echo/v4" 11 | "github.com/labstack/echo/v4/middleware" 12 | ) 13 | 14 | const ( 15 | parentSeparatorNumber = 3 // https://www.w3.org/TR/trace-context/#version-format 16 | ) 17 | 18 | type logger interface { 19 | LogAttrs(ctx context.Context, level slog.Level, msg string, attrs ...slog.Attr) 20 | } 21 | 22 | func SlogLoggerMiddleware(logger logger) echo.MiddlewareFunc { 23 | return middleware.RequestLoggerWithConfig(middleware.RequestLoggerConfig{ 24 | LogStatus: true, 25 | LogURI: true, 26 | LogMethod: true, 27 | LogRemoteIP: true, 28 | LogProtocol: true, 29 | LogUserAgent: true, 30 | LogLatency: true, 31 | LogError: true, 32 | LogHeaders: []string{AwsRequestIDHeader, RequestIDHeader, TraceParentHeader}, 33 | HandleError: true, // forwards error to the global error handler, so it can decide appropriate status code 34 | LogValuesFunc: func(c echo.Context, v middleware.RequestLoggerValues) error { 35 | attrs := []any{ 36 | slog.String("path", v.URI), 37 | slog.Int("status_code", v.Status), 38 | slog.String("method", v.Method), 39 | slog.String("protocol", v.Protocol), 40 | slog.String("remote_ip", v.RemoteIP), 41 | slog.String("user_agent", v.UserAgent), 42 | slog.String("exec_time", v.Latency.String()), 43 | } 44 | level := slog.LevelInfo 45 | msg := "REQUEST" 46 | 47 | // Adding request_id and trace_id 48 | reqID := getRequestID(v.Headers) 49 | attrs = append(attrs, slog.String(string(contextkeys.RequestIDCtxKey), reqID)) 50 | traceID := getTraceID(v.Headers) 51 | attrs = append(attrs, slog.String(string(contextkeys.TraceIDCtxKey), traceID)) 52 | 53 | // Adding container_id attribute 54 | if containerID := c.Param("containerID"); containerID != "" { 55 | attrs = append(attrs, slog.String("container_id", containerID)) 56 | } 57 | 58 | respErrStr := "?" 59 | if v.Error != nil { 60 | respErrStr = v.Error.Error() 61 | attrs = append(attrs, slog.String("err", respErrStr)) 62 | } 63 | 64 | // Change level on 5xx 65 | if v.Status >= http.StatusInternalServerError { 66 | level = slog.LevelError 67 | msg = "REQUEST_ERROR: " + respErrStr 68 | } 69 | logger.LogAttrs(c.Request().Context(), level, msg, slog.Group("context", attrs...)) 70 | return nil 71 | }, 72 | }) 73 | } 74 | -------------------------------------------------------------------------------- /pkg/environment/type.go: -------------------------------------------------------------------------------- 1 | package environment 2 | 3 | type Type string 4 | 5 | const ( 6 | Production Type = "production" 7 | Testing Type = "testing" 8 | ) 9 | -------------------------------------------------------------------------------- /pkg/logger/logger_test/setup_test.go: -------------------------------------------------------------------------------- 1 | package logger_test 2 | 3 | import ( 4 | "context" 5 | "log/slog" 6 | "testing" 7 | 8 | "go-echo-template/pkg/logger" 9 | ) 10 | 11 | func TestSetup(_ *testing.T) { 12 | logger.Setup() 13 | slog.InfoContext(context.Background(), "test logging") 14 | } 15 | -------------------------------------------------------------------------------- /pkg/logger/setup.go: -------------------------------------------------------------------------------- 1 | package logger 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "io" 7 | "log/slog" 8 | "os" 9 | "runtime" 10 | "strings" 11 | 12 | "github.com/getsentry/sentry-go" 13 | 14 | "go-echo-template/pkg/contextkeys" 15 | ) 16 | 17 | func Setup() { 18 | opts := &slog.HandlerOptions{ 19 | Level: slog.LevelInfo, 20 | } 21 | logger := slog.New(newSentryJSONCtxHandler(os.Stdout, opts)) 22 | slog.SetDefault(logger) 23 | } 24 | 25 | type sentryJSONCtxHandler struct { 26 | *slog.JSONHandler 27 | } 28 | 29 | func (h *sentryJSONCtxHandler) Handle(ctx context.Context, r slog.Record) error { 30 | contextInRecord := false 31 | r.Attrs(func(a slog.Attr) bool { 32 | contextInRecord = a.Key == "context" 33 | return !contextInRecord 34 | }) 35 | 36 | requestIDKey := contextkeys.RequestIDCtxKey 37 | traceIDKey := contextkeys.TraceIDCtxKey 38 | requestID := fmt.Sprintf("%v", ctx.Value(requestIDKey)) 39 | traceID := fmt.Sprintf("%v", ctx.Value(traceIDKey)) 40 | 41 | if r.Level == slog.LevelError { 42 | stackTrace := getStackTrace() 43 | r.AddAttrs(slog.String("stacktrace", stackTrace)) 44 | 45 | // Sending event to sentry 46 | if hub, ok := ctx.Value(sentry.HubContextKey).(*sentry.Hub); ok && hub != nil { 47 | hub.WithScope(func(scope *sentry.Scope) { 48 | scope.SetLevel(sentry.LevelError) 49 | scope.SetTag(string(requestIDKey), requestID) 50 | scope.SetTag(string(traceIDKey), traceID) 51 | scope.SetExtra("stacktrace", stackTrace) 52 | hub.CaptureMessage(r.Message) 53 | }) 54 | } 55 | } 56 | 57 | // Log recored does not contain any custom 'context' key -> logging values from ctx 58 | if !contextInRecord { 59 | r.AddAttrs(slog.Group( 60 | "context", 61 | slog.String(string(requestIDKey), requestID), 62 | slog.String(string(traceIDKey), traceID), 63 | )) 64 | } 65 | 66 | return h.JSONHandler.Handle(ctx, r) 67 | } 68 | 69 | func newSentryJSONCtxHandler(w io.Writer, opts *slog.HandlerOptions) *sentryJSONCtxHandler { 70 | jsonHandler := slog.NewJSONHandler(w, opts) 71 | return &sentryJSONCtxHandler{ 72 | JSONHandler: jsonHandler, 73 | } 74 | } 75 | 76 | const stackBufSize = 4096 // 4KB 77 | 78 | func getStackTrace() string { 79 | stackBuf := make([]byte, stackBufSize) 80 | stackSize := runtime.Stack(stackBuf, false) 81 | stackTrace := string(stackBuf[:stackSize]) 82 | return strings.TrimSpace(stackTrace) 83 | } 84 | -------------------------------------------------------------------------------- /pkg/postgres/cluster.go: -------------------------------------------------------------------------------- 1 | package postgres 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "time" 7 | 8 | "github.com/jackc/pgx/v5" 9 | "github.com/jackc/pgx/v5/stdlib" 10 | "github.com/jmoiron/sqlx" 11 | "golang.yandex/hasql/checkers" 12 | hasql "golang.yandex/hasql/sqlx" 13 | ) 14 | 15 | const ( 16 | connectionTimeout = time.Second 17 | clusterUpdateInterval = 2 * time.Second 18 | ) 19 | 20 | func InitCluster(ctx context.Context, connData ConnectionData) (*hasql.Cluster, error) { 21 | nodes, err := initNodes(ctx, connData) 22 | if err != nil { 23 | return nil, fmt.Errorf("failed to init nodes: %w", err) 24 | } 25 | opts := []hasql.ClusterOption{hasql.WithUpdateInterval(clusterUpdateInterval)} 26 | cluster, _ := hasql.NewCluster(nodes, checkers.PostgreSQL, opts...) 27 | ctx2, cancel := context.WithTimeout(ctx, connectionTimeout) 28 | defer cancel() 29 | _, err = cluster.WaitForPrimary(ctx2) 30 | if err != nil { 31 | return nil, fmt.Errorf("failed to wait for cluster availability: %w", err) 32 | } 33 | 34 | return cluster, nil 35 | } 36 | 37 | func initNodes(ctx context.Context, connData ConnectionData) ([]hasql.Node, error) { 38 | nodes := make([]hasql.Node, 0, len(connData.Hosts)) 39 | for _, host := range connData.Hosts { 40 | dsn, err := pgx.ParseConfig(connData.String(host)) 41 | if err != nil { 42 | return nil, fmt.Errorf("failed to parse config: %w", err) 43 | } 44 | db := sqlx.NewDb(stdlib.OpenDB(*dsn), "pgx") 45 | if err := pingWithTimeout(ctx, db); err != nil { 46 | return nil, fmt.Errorf("failed to ping host %s: %w", host, err) 47 | } 48 | nodes = append(nodes, hasql.NewNode(host, db)) 49 | } 50 | return nodes, nil 51 | } 52 | 53 | func pingWithTimeout(ctx context.Context, db *sqlx.DB) error { 54 | ctx2, cancel := context.WithTimeout(ctx, connectionTimeout) 55 | defer cancel() 56 | return db.PingContext(ctx2) 57 | } 58 | -------------------------------------------------------------------------------- /pkg/postgres/connection.go: -------------------------------------------------------------------------------- 1 | package postgres 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "net" 7 | ) 8 | 9 | type ConnectionData struct { 10 | Hosts []string 11 | Database string 12 | User string 13 | Password string 14 | Port string 15 | SSL bool 16 | } 17 | 18 | func NewConnectionData(hosts []string, dbname, user, password, port string, ssl bool) (ConnectionData, error) { 19 | if len(hosts) == 0 { 20 | return ConnectionData{}, errors.New("no host found") 21 | } 22 | if port == "" { 23 | port = "5432" 24 | } 25 | return ConnectionData{ 26 | Database: dbname, 27 | User: user, 28 | Password: password, 29 | Port: port, 30 | SSL: ssl, 31 | Hosts: hosts, 32 | }, nil 33 | } 34 | 35 | func (c ConnectionData) String(host string) string { 36 | connStr := fmt.Sprintf( 37 | `host=%v port=%s dbname=%s user=%s password=%s`, 38 | host, 39 | c.Port, 40 | c.Database, 41 | c.User, 42 | c.Password, 43 | ) 44 | if c.SSL { 45 | connStr += " sslmode=verify-full" 46 | } 47 | return connStr 48 | } 49 | 50 | func (c ConnectionData) URL(host string) string { 51 | sslMode := "disable" 52 | if c.SSL { 53 | sslMode = "require" 54 | } 55 | return fmt.Sprintf( 56 | "postgres://%s:%s@%s/%s?sslmode=%s", 57 | c.User, 58 | c.Password, 59 | net.JoinHostPort(host, c.Port), 60 | c.Database, 61 | sslMode, 62 | ) 63 | } 64 | -------------------------------------------------------------------------------- /pkg/postgres/transaction.go: -------------------------------------------------------------------------------- 1 | package postgres 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "fmt" 7 | 8 | "github.com/jmoiron/sqlx" 9 | ) 10 | 11 | func RunInTx(ctx context.Context, db *sqlx.DB, fn func(tx *sqlx.Tx) error) error { 12 | tx, err := db.BeginTxx(ctx, nil) 13 | if err != nil { 14 | return fmt.Errorf("failed to start transaction: %w", err) 15 | } 16 | 17 | err = fn(tx) 18 | if err == nil { 19 | return tx.Commit() 20 | } 21 | 22 | rollbackErr := tx.Rollback() 23 | if rollbackErr != nil { 24 | return errors.Join(err, rollbackErr) 25 | } 26 | 27 | return err 28 | } 29 | -------------------------------------------------------------------------------- /pkg/sentry/init.go: -------------------------------------------------------------------------------- 1 | package sentry 2 | 3 | import ( 4 | "go-echo-template/pkg/environment" 5 | 6 | "github.com/getsentry/sentry-go" 7 | ) 8 | 9 | func Init(dsn string, environment environment.Type) error { 10 | if dsn == "" { 11 | return nil 12 | } 13 | tracesSampleRate := 0.7 14 | err := sentry.Init(sentry.ClientOptions{ 15 | Dsn: dsn, 16 | TracesSampleRate: tracesSampleRate, 17 | AttachStacktrace: true, 18 | Environment: string(environment), 19 | }) 20 | if err != nil { 21 | return err 22 | } 23 | return nil 24 | } 25 | -------------------------------------------------------------------------------- /profiles/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gonozov0/go-ddd-template/465380bd0ab9f8ddd2e7a975bf612b285f566cda/profiles/.gitkeep --------------------------------------------------------------------------------