From eb899390a2ae8ae64e8b6ea266a7ea860d2e1fdb Mon Sep 17 00:00:00 2001 From: Jonathan VUILLEMIN Date: Fri, 12 Jan 2024 12:53:58 +0100 Subject: [PATCH] feat(fxhttpserver): Provided module (#40) --- .github/workflows/coverage.yml | 1 + .github/workflows/fxhttpserver-ci.yml | 31 + README.md | 1 + fxhttpserver/.golangci.yml | 62 ++ fxhttpserver/README.md | 684 ++++++++++++++++ fxhttpserver/define.go | 124 +++ fxhttpserver/define_test.go | 59 ++ fxhttpserver/enum.go | 24 + fxhttpserver/enum_test.go | 32 + fxhttpserver/go.mod | 84 ++ fxhttpserver/go.sum | 573 +++++++++++++ fxhttpserver/info.go | 57 ++ fxhttpserver/info_test.go | 40 + fxhttpserver/module.go | 252 ++++++ fxhttpserver/module_test.go | 844 ++++++++++++++++++++ fxhttpserver/reflect.go | 27 + fxhttpserver/reflect_test.go | 105 +++ fxhttpserver/register.go | 293 +++++++ fxhttpserver/register_test.go | 124 +++ fxhttpserver/registry.go | 220 +++++ fxhttpserver/registry_test.go | 470 +++++++++++ fxhttpserver/resolve.go | 114 +++ fxhttpserver/resolve_test.go | 107 +++ fxhttpserver/testdata/config/config.yaml | 38 + fxhttpserver/testdata/factory/factory.go | 19 + fxhttpserver/testdata/handler/bar.go | 32 + fxhttpserver/testdata/handler/baz.go | 32 + fxhttpserver/testdata/handler/metrics.go | 41 + fxhttpserver/testdata/handler/template.go | 28 + fxhttpserver/testdata/middleware/global.go | 29 + fxhttpserver/testdata/middleware/group.go | 29 + fxhttpserver/testdata/middleware/handler.go | 29 + fxhttpserver/testdata/service/service.go | 19 + fxhttpserver/testdata/templates/test.html | 1 + release-please-config.json | 5 + 35 files changed, 4630 insertions(+) create mode 100644 .github/workflows/fxhttpserver-ci.yml create mode 100644 fxhttpserver/.golangci.yml create mode 100644 fxhttpserver/README.md create mode 100644 fxhttpserver/define.go create mode 100644 fxhttpserver/define_test.go create mode 100644 fxhttpserver/enum.go create mode 100644 fxhttpserver/enum_test.go create mode 100644 fxhttpserver/go.mod create mode 100644 fxhttpserver/go.sum create mode 100644 fxhttpserver/info.go create mode 100644 fxhttpserver/info_test.go create mode 100644 fxhttpserver/module.go create mode 100644 fxhttpserver/module_test.go create mode 100644 fxhttpserver/reflect.go create mode 100644 fxhttpserver/reflect_test.go create mode 100644 fxhttpserver/register.go create mode 100644 fxhttpserver/register_test.go create mode 100644 fxhttpserver/registry.go create mode 100644 fxhttpserver/registry_test.go create mode 100644 fxhttpserver/resolve.go create mode 100644 fxhttpserver/resolve_test.go create mode 100644 fxhttpserver/testdata/config/config.yaml create mode 100644 fxhttpserver/testdata/factory/factory.go create mode 100644 fxhttpserver/testdata/handler/bar.go create mode 100644 fxhttpserver/testdata/handler/baz.go create mode 100644 fxhttpserver/testdata/handler/metrics.go create mode 100644 fxhttpserver/testdata/handler/template.go create mode 100644 fxhttpserver/testdata/middleware/global.go create mode 100644 fxhttpserver/testdata/middleware/group.go create mode 100644 fxhttpserver/testdata/middleware/handler.go create mode 100644 fxhttpserver/testdata/service/service.go create mode 100644 fxhttpserver/testdata/templates/test.html diff --git a/.github/workflows/coverage.yml b/.github/workflows/coverage.yml index f67b42f..c845cd1 100644 --- a/.github/workflows/coverage.yml +++ b/.github/workflows/coverage.yml @@ -33,6 +33,7 @@ jobs: - "fxgenerate" - "fxhealthcheck" - "fxhttpclient" + - "fxhttpserver" - "fxlog" - "fxmetrics" - "fxorm" diff --git a/.github/workflows/fxhttpserver-ci.yml b/.github/workflows/fxhttpserver-ci.yml new file mode 100644 index 0000000..ecc1ca9 --- /dev/null +++ b/.github/workflows/fxhttpserver-ci.yml @@ -0,0 +1,31 @@ +name: "fxhttpserver-ci" + +on: + push: + branches: + - "feat**" + - "fix**" + - "hotfix**" + - "chore**" + paths: + - "fxhttpserver/**.go" + - "fxhttpserver/go.mod" + - "fxhttpserver/go.sum" + pull_request: + types: + - opened + - synchronize + - reopened + branches: + - main + paths: + - "fxhttpserver/**.go" + - "fxhttpserver/go.mod" + - "fxhttpserver/go.sum" + +jobs: + ci: + uses: ./.github/workflows/common-ci.yml + secrets: inherit + with: + module: "fxhttpserver" diff --git a/README.md b/README.md index 94f62a1..0fefe56 100644 --- a/README.md +++ b/README.md @@ -26,6 +26,7 @@ Yokai's `Fx modules` are the plugins for your Yokai application. | [fxgenerate](fxgenerate) | Fx module for [generate](generate) | | [fxhealthcheck](fxhealthcheck) | Fx module for [healthcheck](healthcheck) | | [fxhttpclient](fxhttpclient) | Fx module for [httpclient](httpclient) | +| [fxhttpserver](fxhttpserver) | Fx module for [httpserver](httpserver) | | [fxlog](fxlog) | Fx module for [log](log) | | [fxmetrics](fxmetrics) | Fx module for [prometheus](https://github.com/prometheus/client_golang) | | [fxorm](fxorm) | Fx module for [orm](orm) | diff --git a/fxhttpserver/.golangci.yml b/fxhttpserver/.golangci.yml new file mode 100644 index 0000000..6812f58 --- /dev/null +++ b/fxhttpserver/.golangci.yml @@ -0,0 +1,62 @@ +run: + timeout: 5m + concurrency: 8 + +linters: + enable: + - asasalint + - asciicheck + - bidichk + - bodyclose + - containedctx + - contextcheck + - cyclop + - decorder + - dogsled + - durationcheck + - errcheck + - errchkjson + - errname + - errorlint + - exhaustive + - forbidigo + - forcetypeassert + - gocognit + - goconst + - gocritic + - gocyclo + - godox + - gofmt + - goheader + - gomoddirectives + - gomodguard + - goprintffuncname + - gosec + - gosimple + - govet + - grouper + - importas + - ineffassign + - interfacebloat + - logrlint + - maintidx + - makezero + - misspell + - nilerr + - nilnil + - nlreturn + - nolintlint + - nosprintfhostport + - predeclared + - promlinter + - reassign + - staticcheck + - tenv + - thelper + - tparallel + - typecheck + - unconvert + - unparam + - unused + - usestdlibvars + - whitespace diff --git a/fxhttpserver/README.md b/fxhttpserver/README.md new file mode 100644 index 0000000..15f3573 --- /dev/null +++ b/fxhttpserver/README.md @@ -0,0 +1,684 @@ +# Fx Http Server Module + +[![ci](https://github.com/ankorstore/yokai/actions/workflows/fxhttpserver-ci.yml/badge.svg)](https://github.com/ankorstore/yokai/actions/workflows/fxhttpserver-ci.yml) +[![go report](https://goreportcard.com/badge/github.com/ankorstore/yokai/fxhttpserver)](https://goreportcard.com/report/github.com/ankorstore/yokai/fxhttpserver) +[![codecov](https://codecov.io/gh/ankorstore/yokai/graph/badge.svg?token=ghUBlFsjhR&flag=fxhttpserver)](https://app.codecov.io/gh/ankorstore/yokai/tree/main/fxhttpserver) +[![Deps](https://img.shields.io/badge/osi-deps-blue)](https://deps.dev/go/github.com%2Fankorstore%2Fyokai%2Ffxhttpserver) +[![PkgGoDev](https://pkg.go.dev/badge/github.com/ankorstore/yokai/fxhttpserver)](https://pkg.go.dev/github.com/ankorstore/yokai/fxhttpserver) + +> [Fx](https://uber-go.github.io/fx/) module for [httpserver](https://github.com/ankorstore/yokai/tree/main/httpserver). + + +* [Installation](#installation) +* [Features](#features) +* [Documentation](#documentation) + * [Dependencies](#dependencies) + * [Loading](#loading) + * [Configuration](#configuration) + * [Registration](#registration) + * [Middlewares](#middlewares) + * [Handlers](#handlers) + * [Handlers groups](#handlers-groups) + * [Templates](#templates) + * [Override](#override) + * [Testing](#testing) + + +## Installation + +```shell +go get github.com/ankorstore/yokai/fxhttpserver +``` + +## Features + +This module provides a http server to your Fx application with: + +- automatic panic recovery +- automatic requests logging and tracing (method, path, duration, ...) +- automatic requests metrics (count and duration) +- possibility to register handlers, groups and middlewares +- possibility to render HTML templates + +## Documentation + +### Dependencies + +This module is intended to be used alongside: + +- the [fxconfig](https://github.com/ankorstore/yokai/tree/main/fxconfig) module +- the [fxlog](https://github.com/ankorstore/yokai/tree/main/fxlog) module +- the [fxtrace](https://github.com/ankorstore/yokai/tree/main/fxtrace) module +- the [fxmetrics](https://github.com/ankorstore/yokai/tree/main/fxmetrics) module +- the [fxgenerate](https://github.com/ankorstore/yokai/tree/main/fxgenerate) module + +### Loading + +To load the module in your Fx application: + +```go +package main + +import ( + "github.com/ankorstore/yokai/fxconfig" + "github.com/ankorstore/yokai/fxgenerate" + "github.com/ankorstore/yokai/fxhttpserver" + "github.com/ankorstore/yokai/fxlog" + "github.com/ankorstore/yokai/fxmetrics" + "github.com/ankorstore/yokai/fxtrace" + "go.uber.org/fx" +) + +func main() { + fx.New( + fxconfig.FxConfigModule, // load the module dependencies + fxlog.FxLogModule, + fxtrace.FxTraceModule, + fxtrace.FxTraceModule, + fxmetrics.FxMetricsModule, + fxgenerate.FxGenerateModule, + fxhttpserver.FxHttpServerModule, // load the module + ).Run() +} +``` + +### Configuration + +Configuration reference: + +```yaml +# ./configs/config.yaml +app: + name: app + env: dev + version: 0.1.0 + debug: true +modules: + log: + level: info + output: stdout + trace: + processor: + type: stdout + core: + server: + port: 8080 # http server port (default 8080) + errors: + obfuscate: false # to obfuscate error messages on the http server responses + stack: false # to add error stack trace to error response of the http server + log: + headers: # to log incoming request headers on the http server + x-foo: foo # to log for example the header x-foo in the log field foo + x-bar: bar + exclude: # to exclude specific routes from logging + - /foo + - /bar + level_from_response: true # to use response status code for log level (ex: 500=error) + trace: + enabled: true # to trace incoming request headers on the http server + exclude: # to exclude specific routes from tracing + - /foo + - /bar + metrics: + collect: + enabled: true # to collect http server metrics + namespace: app # http server metrics namespace (default app.name value) + subsystem: fx-httpserver # http server metrics subsystem (default fx-httpserver) + buckets: 0.1, 1, 10 # to override default request duration buckets + normalize: true # to normalize http status code (2xx, 3xx, ...) + templates: + enabled: true # disabled by default + path: templates/*.html # templates path lookup pattern +``` + +Notes: + +- the http server requests logging will be based on the [fxlog](https://github.com/ankorstore/yokai/tree/main/fxlog) + module configuration +- the http server requests tracing will be based on the [fxtrace](https://github.com/ankorstore/yokai/tree/main/fxtrace) + module configuration +- if `app.debug=true` (or env var `APP_DEBUG=true`), error responses will not be obfuscated and stack trace will be + added + +### Registration + +This module offers the possibility to easily register handlers, groups and middlewares. + +#### Middlewares + +You can use the `AsMiddleware()` function to register global middlewares on your http server: + +- you can provide any [Middleware](registry.go) interface implementation (will be autowired from Fx container) +- or any `echo.MiddlewareFunc`, for example any + built-in [Echo middleware](https://echo.labstack.com/docs/category/middleware) + +```go +package main + +import ( + "github.com/ankorstore/yokai/config" + "github.com/ankorstore/yokai/fxconfig" + "github.com/ankorstore/yokai/fxgenerate" + "github.com/ankorstore/yokai/fxhttpserver" + "github.com/ankorstore/yokai/fxlog" + "github.com/ankorstore/yokai/fxmetrics" + "github.com/ankorstore/yokai/fxtrace" + "github.com/ankorstore/yokai/httpserver" + "github.com/labstack/echo/v4" + "github.com/labstack/echo/v4/middleware" + "go.uber.org/fx" +) + +type SomeMiddleware struct { + config *config.Config +} + +func NewSomeMiddleware(config *config.Config) *SomeMiddleware { + return &SomeMiddleware{ + config: config, + } +} + +func (m *SomeMiddleware) Handle() echo.MiddlewareFunc { + return func(next echo.HandlerFunc) echo.HandlerFunc { + return func(c echo.Context) error { + // request correlated log + httpserver.CtxLogger(c).Info().Msg("in some middleware") + + // use injected dependency + c.Response().Header().Add("app-name", m.config.AppName()) + + return next(c) + } + } +} + +func main() { + fx.New( + fxconfig.FxConfigModule, // load the module dependencies + fxlog.FxLogModule, + fxtrace.FxTraceModule, + fxmetrics.FxMetricsModule, + fxgenerate.FxGenerateModule, + fxhttpserver.FxHttpServerModule, // load the module + fx.Provide( + fxhttpserver.AsMiddleware(middleware.CORS(), fxhttpserver.GlobalUse), // register echo CORS middleware via echo.Use() + fxhttpserver.AsMiddleware(NewSomeMiddleware, fxhttpserver.GlobalPre), // register and autowire the SomeMiddleware via echo.Pre() + ), + ).Run() +} +``` + +#### Handlers + +You can use the `AsHandler()` function to register handlers and their middlewares on your http server: + +- you can provide any [Handler](registry.go) interface implementation (will be autowired from Fx container) +- or any `echo.HandlerFunc` + +```go +package main + +import ( + "fmt" + "net/http" + + "github.com/ankorstore/yokai/fxconfig" + "github.com/ankorstore/yokai/fxgenerate" + "github.com/ankorstore/yokai/fxhttpserver" + "github.com/ankorstore/yokai/fxlog" + "github.com/ankorstore/yokai/fxmetrics" + "github.com/ankorstore/yokai/fxtrace" + "github.com/ankorstore/yokai/config" + "github.com/ankorstore/yokai/httpserver" + "github.com/ankorstore/yokai/log" + "github.com/labstack/echo/v4" + "github.com/labstack/echo/v4/middleware" + "go.uber.org/fx" +) + +type SomeMiddleware struct { + config *config.Config +} + +func NewSomeMiddleware(config *config.Config) *SomeMiddleware { + return &SomeMiddleware{ + config: config, + } +} + +func (m *SomeMiddleware) Handle() echo.MiddlewareFunc { + return func(next echo.HandlerFunc) echo.HandlerFunc { + return func(c echo.Context) error { + // request correlated log + httpserver.CtxLogger(c).Info().Msg("in some middleware") + + // use injected dependency + c.Response().Header().Add("app-name", m.config.AppName()) + + return next(c) + } + } +} + +type SomeHandler struct { + config *config.Config +} + +func NewSomeHandler(config *config.Config) *SomeHandler { + return &SomeHandler{ + config: config, + } +} + +func (h *SomeHandler) Handle() echo.HandlerFunc { + return func(c echo.Context) error { + // request correlated trace span + ctx, span := httpserver.CtxTracer(c).Start(c.Request().Context(), "some span") + defer span.End() + + // request correlated log + log.CtxLogger(ctx).Info().Msg("in some handler") + + // use injected dependency + return c.String(http.StatusOK, fmt.Sprintf("app name: %s", h.config.AppName())) + } +} + +func main() { + fx.New( + fxconfig.FxConfigModule, // load the module dependencies + fxlog.FxLogModule, + fxtrace.FxTraceModule, + fxmetrics.FxMetricsModule, + fxgenerate.FxGenerateModule, + fxhttpserver.FxHttpServerModule, // load the module + fx.Provide( + // register and autowire the SomeHandler handler for [GET] /some-path, with the SomeMiddleware and echo CORS() middlewares + fxhttpserver.AsHandler("GET", "/some-path", NewSomeHandler, NewSomeMiddleware, middleware.CORS()), + ), + ).Run() +} +``` + +#### Handlers groups + +You can use the `AsHandlersGroup()` function to register handlers groups and their middlewares on your http +server: + +- you can provide any [Handler](registry.go) interface implementation (will be autowired from Fx container) or + any `echo.HandlerFunc`, with their middlewares +- and group them + - under a common route `prefix` + - with common [Middleware](registry.go) interface implementation (will be autowired from Fx container) or + any `echo.MiddlewareFunc` + +```go +package main + +import ( + "fmt" + "net/http" + + "github.com/ankorstore/yokai/config" + "github.com/ankorstore/yokai/fxconfig" + "github.com/ankorstore/yokai/fxgenerate" + "github.com/ankorstore/yokai/fxhttpserver" + "github.com/ankorstore/yokai/fxlog" + "github.com/ankorstore/yokai/fxmetrics" + "github.com/ankorstore/yokai/fxtrace" + "github.com/ankorstore/yokai/httpserver" + "github.com/ankorstore/yokai/log" + "github.com/labstack/echo/v4" + "github.com/labstack/echo/v4/middleware" + "go.uber.org/fx" +) + +type SomeMiddleware struct { + config *config.Config +} + +func NewSomeMiddleware(config *config.Config) *SomeMiddleware { + return &SomeMiddleware{ + config: config, + } +} + +func (m *SomeMiddleware) Handle() echo.MiddlewareFunc { + return func(next echo.HandlerFunc) echo.HandlerFunc { + return func(c echo.Context) error { + // request correlated log + httpserver.CtxLogger(c).Info().Msg("in some middleware") + + // use injected dependency + c.Response().Header().Add("app-name", m.config.AppName()) + + return next(c) + } + } +} + +type SomeHandler struct { + config *config.Config +} + +func NewSomeHandler(config *config.Config) *SomeHandler { + return &SomeHandler{ + config: config, + } +} + +func (h *SomeHandler) Handle() echo.HandlerFunc { + return func(c echo.Context) error { + // request correlated trace span + ctx, span := httpserver.CtxTracer(c).Start(c.Request().Context(), "some span") + defer span.End() + + // request correlated log + log.CtxLogger(ctx).Info().Msg("in some handler") + + // use injected dependency + return c.String(http.StatusOK, fmt.Sprintf("app name: %s", h.config.AppName())) + } +} + +type OtherHandler struct { + config *config.Config +} + +func NewOtherHandler(config *config.Config) *OtherHandler { + return &OtherHandler{ + config: config, + } +} + +func (h *OtherHandler) Handle() echo.HandlerFunc { + return func(c echo.Context) error { + // use injected dependency + return c.String(http.StatusOK, fmt.Sprintf("app version: %s", h.config.AppVersion())) + } +} + +func main() { + fx.New( + fxconfig.FxConfigModule, // load the module dependencies + fxlog.FxLogModule, + fxtrace.FxTraceModule, + fxmetrics.FxMetricsModule, + fxgenerate.FxGenerateModule, + fxhttpserver.FxHttpServerModule, // load the module + fx.Provide( + // register and autowire the SomeHandler handler with NewSomeMiddleware middleware for [GET] /group/some-path + // register and autowire the OtherHandler handler with echo CORS middleware for [POST] /group/other-path + // register the echo CSRF middleware for all handlers of this group + fxhttpserver.AsHandlersGroup( + "/group", + []*fxhttpserver.HandlerRegistration{ + fxhttpserver.NewHandlerRegistration("GET", "/some-path", NewSomeHandler, NewSomeMiddleware), + fxhttpserver.NewHandlerRegistration("POST", "/other-path", NewOtherHandler, middleware.CORS()), + }, + middleware.CSRF(), + ), + ), + ).Run() +} +``` + +### Templates + +The module will look up HTML templates to render if `modules.http.server.templates.enabled=true`. + +The HTML templates will be loaded from a path matching the pattern specified in `modules.http.server.templates.path`. + +Considering the configuration: + +```yaml +# ./configs/config.yaml +app: + name: app +modules: + http: + server: + templates: + enabled: true + path: templates/*.html +``` + +And the template: + +```html + + + +

App name is {{index . "name"}}

+ + +``` + +To render it: + +```go +package main + +import ( + "net/http" + + "github.com/ankorstore/yokai/config" + "github.com/ankorstore/yokai/fxconfig" + "github.com/ankorstore/yokai/fxgenerate" + "github.com/ankorstore/yokai/fxhttpserver" + "github.com/ankorstore/yokai/fxlog" + "github.com/ankorstore/yokai/fxmetrics" + "github.com/ankorstore/yokai/fxtrace" + "github.com/ankorstore/yokai/httpserver" + "github.com/labstack/echo/v4" + "go.uber.org/fx" +) + +type TemplateHandler struct { + config *config.Config +} + +func NewTemplateHandler(cfg *config.Config) *TemplateHandler { + return &TemplateHandler{ + config: cfg, + } +} + +func (h *TemplateHandler) Handle() echo.HandlerFunc { + return func(c echo.Context) error { + // output: "App name is app" + return c.Render(http.StatusOK, "app.html", map[string]interface{}{ + "name": h.config.AppName(), + }) + } +} + +func main() { + fx.New( + fxconfig.FxConfigModule, // load the module dependencies + fxlog.FxLogModule, + fxtrace.FxTraceModule, + fxmetrics.FxMetricsModule, + fxgenerate.FxGenerateModule, + fxhttpserver.FxHttpServerModule, // load the module + fx.Provide( + fxhttpserver.AsHandler("GET", "/app", NewTemplateHandler), + ), + ).Run() +} +``` + +### Override + +By default, the `echo.Echo` is created by +the [DefaultHttpServerFactory](https://github.com/ankorstore/yokai/blob/main/httpserver/factory.go). + +If needed, you can provide your own factory and override the module: + +```go +package main + +import ( + "github.com/ankorstore/yokai/fxconfig" + "github.com/ankorstore/yokai/fxgenerate" + "github.com/ankorstore/yokai/fxhttpserver" + "github.com/ankorstore/yokai/fxlog" + "github.com/ankorstore/yokai/fxmetrics" + "github.com/ankorstore/yokai/fxtrace" + "github.com/ankorstore/yokai/httpserver" + "github.com/labstack/echo/v4" + "go.uber.org/fx" +) + +type CustomHttpServerFactory struct{} + +func NewCustomHttpServerFactory() httpserver.HttpServerFactory { + return &CustomHttpServerFactory{} +} + +func (f *CustomHttpServerFactory) Create(options ...httpserver.HttpServerOption) (*echo.Echo, error) { + return echo.New(), nil +} + +func main() { + fx.New( + fxconfig.FxConfigModule, // load the module dependencies + fxlog.FxLogModule, + fxtrace.FxTraceModule, + fxmetrics.FxMetricsModule, + fxgenerate.FxGenerateModule, + fxhttpserver.FxHttpServerModule, // load the module + fx.Decorate(NewCustomHttpServerFactory), // override the module with a custom factory + fx.Invoke(func(httpServer *echo.Echo) { // invoke the custom http server + // ... + }), + ).Run() +} +``` + +### Testing + +This module allows you to easily provide `functional` tests for your handlers. + +For example, considering this handler: + +```go +package handler + +import ( + "net/http" + + "github.com/labstack/echo/v4" +) + +type SomeHandler struct{} + +func NewSomeHandler() *SomeHandler { + return &SomeHandler{} +} + +func (h *SomeHandler) Handle() echo.HandlerFunc { + return func(c echo.Context) error { + return c.String(http.StatusOK, "ok") + } +} +``` + +You can then test it, considering logs, traces and metrics are enabled: + +```go +package handler_test + +import ( + "net/http" + "net/http/httptest" + "strings" + "testing" + + "github.com/ankorstore/yokai/fxconfig" + "github.com/ankorstore/yokai/fxgenerate" + "github.com/ankorstore/yokai/fxhttpserver" + "github.com/ankorstore/yokai/fxlog" + "github.com/ankorstore/yokai/fxmetrics" + "github.com/ankorstore/yokai/fxtrace" + "github.com/ankorstore/yokai/log/logtest" + "github.com/ankorstore/yokai/trace/tracetest" + "github.com/labstack/echo/v4" + "github.com/prometheus/client_golang/prometheus" + "github.com/prometheus/client_golang/prometheus/testutil" + "github.com/stretchr/testify/assert" + semconv "go.opentelemetry.io/otel/semconv/v1.17.0" + "go.uber.org/fx" + "go.uber.org/fx/fxtest" + "handler" +) + +func TestSomeHandler(t *testing.T) { + var httpServer *echo.Echo + var logBuffer logtest.TestLogBuffer + var traceExporter tracetest.TestTraceExporter + var metricsRegistry *prometheus.Registry + + fxtest.New( + t, + fx.NopLogger, + fxconfig.FxConfigModule, + fxlog.FxLogModule, + fxtrace.FxTraceModule, + fxmetrics.FxMetricsModule, + fxgenerate.FxGenerateModule, + fxhttpserver.FxHttpServerModule, + fx.Options( + fxhttpserver.AsHandler("GET", "/test", handler.NewSomeHandler), + ), + fx.Populate(&httpServer, &logBuffer, &traceExporter, &metricsRegistry), // extract components + ).RequireStart().RequireStop() + + // http call [GET] /test on the server + req := httptest.NewRequest(http.MethodGet, "/test", nil) + rec := httptest.NewRecorder() + httpServer.ServeHTTP(rec, req) + + // assertions on http response + assert.Equal(t, http.StatusOK, rec.Code) + assert.Equal(t, rec.Body.String(), "ok") + + // assertion on the logs buffer + logtest.AssertHasLogRecord(t, logBuffer, map[string]interface{}{ + "level": "info", + "service": "test", + "system": "fx-httpserver", + "method": "GET", + "uri": "/test", + "status": 200, + "message": "request logger", + }) + + // assertion on the traces exporter + tracetest.AssertHasTraceSpan( + t, + traceExporter, + "GET /test", + semconv.HTTPRoute("/test"), + semconv.HTTPMethod(http.MethodGet), + semconv.HTTPStatusCode(http.StatusOK), + ) + + // assertion on the metrics registry + expectedMetric := ` + # HELP app_fx_httpserver_requests_total Number of processed HTTP requests + # TYPE app_fx_httpserver_requests_total counter + app_fx_httpserver_requests_total{handler="/test",method="GET",status="2xx"} 1 + ` + + err := testutil.GatherAndCompare( + metricsRegistry, + strings.NewReader(expectedMetric), + "app_fx_httpserver_requests_total", + ) + assert.NoError(t, err) +} +``` + +You can find more tests examples in this module own [tests](module_test.go). diff --git a/fxhttpserver/define.go b/fxhttpserver/define.go new file mode 100644 index 0000000..23e47bd --- /dev/null +++ b/fxhttpserver/define.go @@ -0,0 +1,124 @@ +package fxhttpserver + +// MiddlewareDefinition is the interface for middlewares definitions. +type MiddlewareDefinition interface { + Concrete() bool + Middleware() any + Kind() MiddlewareKind +} + +type middlewareDefinition struct { + middleware any + kind MiddlewareKind +} + +// NewMiddlewareDefinition returns a new [MiddlewareDefinition]. +func NewMiddlewareDefinition(middleware any, kind MiddlewareKind) MiddlewareDefinition { + return &middlewareDefinition{ + middleware: middleware, + kind: kind, + } +} + +// Concrete returns true if the middleware is a [echo.MiddlewareFunc] concrete implementation. +func (d *middlewareDefinition) Concrete() bool { + return IsConcreteMiddleware(d.middleware) +} + +// Middleware returns the middleware. +func (d *middlewareDefinition) Middleware() any { + return d.middleware +} + +// Kind returns the middleware kind. +func (d *middlewareDefinition) Kind() MiddlewareKind { + return d.kind +} + +// HandlerDefinition is the interface for handlers definitions. +type HandlerDefinition interface { + Concrete() bool + Method() string + Path() string + Handler() any + Middlewares() []MiddlewareDefinition +} + +type handlerDefinition struct { + method string + path string + handler any + middlewares []MiddlewareDefinition +} + +// NewHandlerDefinition returns a new [HandlerDefinition]. +func NewHandlerDefinition(method string, path string, handler any, middlewares []MiddlewareDefinition) HandlerDefinition { + return &handlerDefinition{ + method: method, + path: path, + handler: handler, + middlewares: middlewares, + } +} + +// Concrete returns true if the handler is a [echo.HandlerFunc] concrete implementation. +func (d *handlerDefinition) Concrete() bool { + return IsConcreteHandler(d.handler) +} + +// Method returns the handler http method. +func (d *handlerDefinition) Method() string { + return d.method +} + +// Path returns the handler http path. +func (d *handlerDefinition) Path() string { + return d.path +} + +// Handler returns the handler. +func (d *handlerDefinition) Handler() any { + return d.handler +} + +// Middlewares returns the handler associated middlewares. +func (d *handlerDefinition) Middlewares() []MiddlewareDefinition { + return d.middlewares +} + +// HandlersGroupDefinition is the interface for handlers groups definitions. +type HandlersGroupDefinition interface { + Prefix() string + Handlers() []HandlerDefinition + Middlewares() []MiddlewareDefinition +} + +type handlersGroupDefinition struct { + prefix string + handlers []HandlerDefinition + middlewares []MiddlewareDefinition +} + +// NewHandlersGroupDefinition returns a new [HandlersGroupDefinition]. +func NewHandlersGroupDefinition(prefix string, handlers []HandlerDefinition, middlewares []MiddlewareDefinition) HandlersGroupDefinition { + return &handlersGroupDefinition{ + prefix: prefix, + handlers: handlers, + middlewares: middlewares, + } +} + +// Prefix returns the handlers group http path prefix. +func (h *handlersGroupDefinition) Prefix() string { + return h.prefix +} + +// Handlers returns the handlers group associated handlers. +func (h *handlersGroupDefinition) Handlers() []HandlerDefinition { + return h.handlers +} + +// Middlewares returns the handlers group associated middlewares. +func (h *handlersGroupDefinition) Middlewares() []MiddlewareDefinition { + return h.middlewares +} diff --git a/fxhttpserver/define_test.go b/fxhttpserver/define_test.go new file mode 100644 index 0000000..94442f2 --- /dev/null +++ b/fxhttpserver/define_test.go @@ -0,0 +1,59 @@ +package fxhttpserver_test + +import ( + "net/http" + "testing" + + "github.com/ankorstore/yokai/fxhttpserver" + "github.com/ankorstore/yokai/fxhttpserver/testdata/handler" + "github.com/ankorstore/yokai/fxhttpserver/testdata/middleware" + "github.com/stretchr/testify/assert" +) + +func TestMiddlewareDefinition(t *testing.T) { + t.Parallel() + + kind := fxhttpserver.GlobalUse + + md := fxhttpserver.NewMiddlewareDefinition(middleware.NewTestGlobalMiddleware, kind) + + assert.False(t, md.Concrete()) + assert.Equal(t, kind, md.Kind()) +} + +func TestHandlerDefinition(t *testing.T) { + t.Parallel() + + method := http.MethodGet + path := "/test" + hand := handler.NewTestBarHandler + middlewares := []fxhttpserver.MiddlewareDefinition{ + fxhttpserver.NewMiddlewareDefinition(middleware.NewTestGlobalMiddleware, fxhttpserver.GlobalUse), + } + + hd := fxhttpserver.NewHandlerDefinition(method, path, hand, middlewares) + + assert.False(t, hd.Concrete()) + assert.Equal(t, method, hd.Method()) + assert.Equal(t, path, hd.Path()) + assert.Equal(t, middlewares, hd.Middlewares()) +} + +func TestHandlersGroupDefinition(t *testing.T) { + t.Parallel() + + prefix := "/group" + hand := handler.NewTestBarHandler + handlers := []fxhttpserver.HandlerDefinition{ + fxhttpserver.NewHandlerDefinition(http.MethodGet, "/test", hand, nil), + } + middlewares := []fxhttpserver.MiddlewareDefinition{ + fxhttpserver.NewMiddlewareDefinition(middleware.NewTestGlobalMiddleware, fxhttpserver.GlobalUse), + } + + hgd := fxhttpserver.NewHandlersGroupDefinition(prefix, handlers, middlewares) + + assert.Equal(t, prefix, hgd.Prefix()) + assert.Equal(t, handlers, hgd.Handlers()) + assert.Equal(t, middlewares, hgd.Middlewares()) +} diff --git a/fxhttpserver/enum.go b/fxhttpserver/enum.go new file mode 100644 index 0000000..9e4b8aa --- /dev/null +++ b/fxhttpserver/enum.go @@ -0,0 +1,24 @@ +package fxhttpserver + +// MiddlewareKind is an enum for the middleware kinds (global, pre, post). +type MiddlewareKind int + +const ( + GlobalUse MiddlewareKind = iota + GlobalPre + Attached +) + +// String returns a string representation of a [MiddlewareKind]. +func (k MiddlewareKind) String() string { + switch k { + case GlobalUse: + return "global-use" + case GlobalPre: + return "global-pre" + case Attached: + return "attached" + default: + return "global-use" + } +} diff --git a/fxhttpserver/enum_test.go b/fxhttpserver/enum_test.go new file mode 100644 index 0000000..9451346 --- /dev/null +++ b/fxhttpserver/enum_test.go @@ -0,0 +1,32 @@ +package fxhttpserver_test + +import ( + "testing" + + "github.com/ankorstore/yokai/fxhttpserver" + "github.com/stretchr/testify/assert" +) + +func TestMiddlewareKindAsString(t *testing.T) { + t.Parallel() + + tests := []struct { + input fxhttpserver.MiddlewareKind + expected string + }{ + {fxhttpserver.GlobalUse, "global-use"}, + {fxhttpserver.GlobalPre, "global-pre"}, + {fxhttpserver.Attached, "attached"}, + {fxhttpserver.MiddlewareKind(1000), "global-use"}, + } + + for _, tt := range tests { + tt := tt + t.Run(tt.expected, func(t *testing.T) { + t.Parallel() + + actual := tt.input.String() + assert.Equal(t, tt.expected, actual) + }) + } +} diff --git a/fxhttpserver/go.mod b/fxhttpserver/go.mod new file mode 100644 index 0000000..dacf246 --- /dev/null +++ b/fxhttpserver/go.mod @@ -0,0 +1,84 @@ +module github.com/ankorstore/yokai/fxhttpserver + +go 1.20 + +require ( + github.com/ankorstore/yokai/config v1.1.0 + github.com/ankorstore/yokai/fxconfig v1.0.0 + github.com/ankorstore/yokai/fxgenerate v1.0.0 + github.com/ankorstore/yokai/fxlog v1.0.0 + github.com/ankorstore/yokai/fxmetrics v1.0.0 + github.com/ankorstore/yokai/fxtrace v1.1.0 + github.com/ankorstore/yokai/generate v1.0.0 + github.com/ankorstore/yokai/httpserver v1.0.0 + github.com/ankorstore/yokai/log v1.0.0 + github.com/ankorstore/yokai/trace v1.0.0 + github.com/labstack/echo/v4 v4.11.1 + github.com/prometheus/client_golang v1.18.0 + github.com/stretchr/testify v1.8.4 + go.opentelemetry.io/otel v1.16.0 + go.opentelemetry.io/otel/trace v1.16.0 + go.uber.org/fx v1.20.1 +) + +require ( + github.com/beorn7/perks v1.0.1 // indirect + github.com/cenkalti/backoff/v4 v4.2.1 // indirect + github.com/cespare/xxhash/v2 v2.2.0 // indirect + github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect + github.com/fsnotify/fsnotify v1.7.0 // indirect + github.com/go-errors/errors v1.4.2 // indirect + github.com/go-logr/logr v1.2.4 // indirect + github.com/go-logr/stdr v1.2.2 // indirect + github.com/golang-jwt/jwt v3.2.2+incompatible // indirect + github.com/golang/protobuf v1.5.3 // indirect + github.com/google/uuid v1.5.0 // indirect + github.com/grpc-ecosystem/grpc-gateway/v2 v2.7.0 // indirect + github.com/hashicorp/hcl v1.0.0 // indirect + github.com/labstack/gommon v0.4.2 // indirect + github.com/magiconair/properties v1.8.7 // indirect + github.com/mattn/go-colorable v0.1.13 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect + github.com/matttproud/golang_protobuf_extensions/v2 v2.0.0 // indirect + github.com/mitchellh/mapstructure v1.5.0 // indirect + github.com/pelletier/go-toml/v2 v2.1.1 // indirect + github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect + github.com/prometheus/client_model v0.5.0 // indirect + github.com/prometheus/common v0.45.0 // indirect + github.com/prometheus/procfs v0.12.0 // indirect + github.com/rs/zerolog v1.31.0 // indirect + github.com/sagikazarmark/locafero v0.4.0 // indirect + github.com/sagikazarmark/slog-shim v0.1.0 // indirect + github.com/sourcegraph/conc v0.3.0 // indirect + github.com/spf13/afero v1.11.0 // indirect + github.com/spf13/cast v1.6.0 // indirect + github.com/spf13/pflag v1.0.5 // indirect + github.com/spf13/viper v1.18.2 // indirect + github.com/stretchr/objx v0.5.0 // indirect + github.com/subosito/gotenv v1.6.0 // indirect + github.com/valyala/bytebufferpool v1.0.0 // indirect + github.com/valyala/fasttemplate v1.2.2 // indirect + go.opentelemetry.io/otel/exporters/otlp/internal/retry v1.16.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.16.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.16.0 // indirect + go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.16.0 // indirect + go.opentelemetry.io/otel/metric v1.16.0 // indirect + go.opentelemetry.io/otel/sdk v1.16.0 // indirect + go.opentelemetry.io/proto/otlp v0.19.0 // indirect + go.uber.org/dig v1.17.1 // indirect + go.uber.org/multierr v1.11.0 // indirect + go.uber.org/zap v1.26.0 // indirect + golang.org/x/crypto v0.16.0 // indirect + golang.org/x/exp v0.0.0-20240110193028-0dcbfd608b1e // indirect + golang.org/x/net v0.19.0 // indirect + golang.org/x/sys v0.16.0 // indirect + golang.org/x/text v0.14.0 // indirect + golang.org/x/time v0.5.0 // indirect + google.golang.org/genproto v0.0.0-20231106174013-bbf56f31fb17 // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20231106174013-bbf56f31fb17 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20231120223509-83a465c0220f // indirect + google.golang.org/grpc v1.59.0 // indirect + google.golang.org/protobuf v1.31.0 // indirect + gopkg.in/ini.v1 v1.67.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect +) diff --git a/fxhttpserver/go.sum b/fxhttpserver/go.sum new file mode 100644 index 0000000..c2be765 --- /dev/null +++ b/fxhttpserver/go.sum @@ -0,0 +1,573 @@ +cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU= +cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU= +cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY= +cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc= +cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0= +cloud.google.com/go v0.50.0/go.mod h1:r9sluTvynVuxRIOHXQEHMFffphuXHOMZMycpNR5e6To= +cloud.google.com/go v0.52.0/go.mod h1:pXajvRH/6o3+F9jDHZWQ5PbGhn+o8w9qiu/CffaVdO4= +cloud.google.com/go v0.53.0/go.mod h1:fp/UouUEsRkN6ryDKNW/Upv/JBKnv6WDthjR6+vze6M= +cloud.google.com/go v0.54.0/go.mod h1:1rq2OEkV3YMf6n/9ZvGWI3GWw0VoqH/1x2nd8Is/bPc= +cloud.google.com/go v0.56.0/go.mod h1:jr7tqZxxKOVYizybht9+26Z/gUq7tiRzu+ACVAMbKVk= +cloud.google.com/go v0.57.0/go.mod h1:oXiQ6Rzq3RAkkY7N6t3TcE6jE+CIBBbA36lwQ1JyzZs= +cloud.google.com/go v0.62.0/go.mod h1:jmCYTdRCQuc1PHIIJ/maLInMho30T/Y0M4hTdTShOYc= +cloud.google.com/go v0.65.0/go.mod h1:O5N8zS7uWy9vkA9vayVHs65eM1ubvY4h553ofrNHObY= +cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o= +cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE= +cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc= +cloud.google.com/go/bigquery v1.5.0/go.mod h1:snEHRnqQbz117VIFhE8bmtwIDY80NLUZUMb4Nv6dBIg= +cloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4gLoIoXIAPc= +cloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ= +cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE= +cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk= +cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I= +cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw= +cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA= +cloud.google.com/go/pubsub v1.3.1/go.mod h1:i+ucay31+CNRpDW4Lu78I4xXG+O1r/MAHgjpRVR+TSU= +cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw= +cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos= +cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk= +cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs= +cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0= +dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= +github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= +github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= +github.com/ankorstore/yokai/config v1.1.0 h1:z6xnsVXAbWhhjcb5kqVaw0VlaGZziGc7Di1bJlt5rf0= +github.com/ankorstore/yokai/config v1.1.0/go.mod h1:yDANaMWIOfAUkAMClG22Q4bzQk91NLwWK3WbL5IFnbg= +github.com/ankorstore/yokai/fxconfig v1.0.0 h1:zaYOLfpurqFJuS/IHeAXPOzrCNuttsFjJWNOK39opR4= +github.com/ankorstore/yokai/fxconfig v1.0.0/go.mod h1:p+x6Jp8aLv1+uE1qO42KF+yahBK+VJdPP1/YReBjJ7M= +github.com/ankorstore/yokai/fxgenerate v1.0.0 h1:jLe6FVnUqTkHZINK/LmjoD3C+CaZuNlMlQ/JJp0T1Cg= +github.com/ankorstore/yokai/fxgenerate v1.0.0/go.mod h1:o6ICl0t3DRC3xUUm/z11EIA53BA8dHwZkJJaMVMgnGk= +github.com/ankorstore/yokai/fxlog v1.0.0 h1:ujq/XxgCK0uwKCNSt86XEYR2vqYbXZX2/lA/pQHZX4A= +github.com/ankorstore/yokai/fxlog v1.0.0/go.mod h1:juQnBYNddDVOa7Ukhw8axLYWyibDDMJAwG7MDpluKnk= +github.com/ankorstore/yokai/fxmetrics v1.0.0 h1:jA1MnIRzRqBk4JsdCcQxPZ6Jvmpd+uyoBwO7c0vUCMc= +github.com/ankorstore/yokai/fxmetrics v1.0.0/go.mod h1:No9z3tnPxAyjYiXfHcpGjZzBwYB/OSs80L7w0oiXXmM= +github.com/ankorstore/yokai/fxtrace v1.1.0 h1:UBzz5mo0kvfbp2fEaY/2Mamy4lkWoJiWe8iz2bDl+Vw= +github.com/ankorstore/yokai/fxtrace v1.1.0/go.mod h1:DP/aNn65I+LU1QoBVvCLhFVr2djFUNFnclITmUxjQmc= +github.com/ankorstore/yokai/generate v1.0.0 h1:kHpbl8cet9qklUamMqSTJy3h6aiybKMgnAK6dDI42p8= +github.com/ankorstore/yokai/generate v1.0.0/go.mod h1:7/gebXdxAOmqeDG54RcguC0a+f3JtqEKVKtSy8f2dlk= +github.com/ankorstore/yokai/httpserver v1.0.0 h1:ROCsM1L/tCSA9zcOpSwrpecQv8twbs3hYtrZ5rFkRF8= +github.com/ankorstore/yokai/httpserver v1.0.0/go.mod h1:W72H3+ok6sUY41Qj5TdhjFqyDlQ9nC4JFwKVQIT6+1A= +github.com/ankorstore/yokai/log v1.0.0 h1:9NsM0J+1O028WuNDW7vr0yeUdWDX1JKYTkuz7hiYCSs= +github.com/ankorstore/yokai/log v1.0.0/go.mod h1:lyBRVA8VkrmlNjaR2jVTH9XjV06ioolWTuDVN6wF0vk= +github.com/ankorstore/yokai/trace v1.0.0 h1:EKWXyg2W8v3xszIiB5JfiDwU2OUfSDOo8LXJMDxlSrw= +github.com/ankorstore/yokai/trace v1.0.0/go.mod h1:OhCIJouVmBD7je1dIynqR1mhMEFCBzidy16a624lwBw= +github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY= +github.com/benbjohnson/clock v1.3.0 h1:ip6w0uFQkncKQ979AypyG0ER7mqUSBdKLOgAle/AT8A= +github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= +github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= +github.com/cenkalti/backoff/v4 v4.2.1 h1:y4OZtCnogmCPw98Zjyt5a6+QwPLGkiQsYW5oUqylYbM= +github.com/cenkalti/backoff/v4 v4.2.1/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= +github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= +github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc= +github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44= +github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= +github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= +github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= +github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= +github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= +github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= +github.com/cncf/udpa/go v0.0.0-20210930031921-04548b0d99d4/go.mod h1:6pvJx4me5XPnfI9Z40ddWsdw2W/uZgQLFXToKeRcDiI= +github.com/cncf/xds/go v0.0.0-20210312221358-fbca930ec8ed/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= +github.com/cncf/xds/go v0.0.0-20210805033703-aa0b78936158/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= +github.com/cncf/xds/go v0.0.0-20210922020428-25de7278fc84/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= +github.com/cncf/xds/go v0.0.0-20211011173535-cb28da3451f1/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= +github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= +github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= +github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= +github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= +github.com/envoyproxy/go-control-plane v0.9.9-0.20210512163311-63b5d3c536b0/go.mod h1:hliV/p42l8fGbc6Y9bQ70uLwIvmJyVE5k4iMKlh8wCQ= +github.com/envoyproxy/go-control-plane v0.9.10-0.20210907150352-cf90f659a021/go.mod h1:AFq3mo9L8Lqqiid3OhADV3RfLJnjiw63cSpi+fDTRC0= +github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= +github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= +github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA= +github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM= +github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= +github.com/go-errors/errors v1.4.2 h1:J6MZopCL4uSllY1OfXM374weqZFFItUbrImctkmUxIA= +github.com/go-errors/errors v1.4.2/go.mod h1:sIVyrIiJhuEF+Pj9Ebtd6P/rEYROXFi3BopGUQ5a5Og= +github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= +github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= +github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= +github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/go-logr/logr v1.2.4 h1:g01GSCwiDw2xSZfjJ2/T9M+S6pFdcNtFYsp+Y43HYDQ= +github.com/go-logr/logr v1.2.4/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= +github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= +github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= +github.com/golang-jwt/jwt v3.2.2+incompatible h1:IfV12K8xAKAnZqdXVzCZ+TOjboZ2keLg81eXfW3O+oY= +github.com/golang-jwt/jwt v3.2.2+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I= +github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= +github.com/golang/glog v1.0.0/go.mod h1:EWib/APOK0SL3dFbYqvxE3UYd8E6s1ouQ7iEp/0LWV4= +github.com/golang/glog v1.1.2 h1:DVjP2PbBOzHyzA+dn3WhHIq4NdVu3Q+pvivFICf/7fo= +github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y= +github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= +github.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= +github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= +github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4= +github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= +github.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= +github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk= +github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= +github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= +github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= +github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= +github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= +github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= +github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= +github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= +github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= +github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= +github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg= +github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= +github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= +github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= +github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= +github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.4.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= +github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= +github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= +github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= +github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= +github.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20200430221834-fc25d7d30c6d/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= +github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/google/uuid v1.5.0 h1:1p67kYwdtXjb0gL0BPiP1Av9wiZPo5A8z2cWkTZ+eyU= +github.com/google/uuid v1.5.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= +github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= +github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.7.0 h1:BZHcxBETFHIdVyhyEfOvn/RdU/QGdLI4y34qQGjGWO0= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.7.0/go.mod h1:hgWBS7lorOAVIJEQMi4ZsPv9hVvWI6+ch50m39Pf2Ks= +github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= +github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= +github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= +github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= +github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= +github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= +github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk= +github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/labstack/echo/v4 v4.11.1 h1:dEpLU2FLg4UVmvCGPuk/APjlH6GDpbEPti61srUUUs4= +github.com/labstack/echo/v4 v4.11.1/go.mod h1:YuYRTSM3CHs2ybfrL8Px48bO6BAnYIN4l8wSTMP6BDQ= +github.com/labstack/gommon v0.4.2 h1:F8qTUNXgG1+6WQmqoUWnz8WiEU60mXVVw0P4ht1WRA0= +github.com/labstack/gommon v0.4.2/go.mod h1:QlUFxVM+SNXhDL/Z7YhocGIBYOiwB0mXm1+1bAPHPyU= +github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY= +github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0= +github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= +github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= +github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= +github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/matttproud/golang_protobuf_extensions/v2 v2.0.0 h1:jWpvCLoY8Z/e3VKvlsiIGKtc+UG6U5vzxaoagmhXfyg= +github.com/matttproud/golang_protobuf_extensions/v2 v2.0.0/go.mod h1:QUyp042oQthUoa9bqDv0ER0wrtXnBruoNd7aNjkbP+k= +github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= +github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= +github.com/pelletier/go-toml/v2 v2.1.1 h1:LWAJwfNvjQZCFIDKWYQaM62NcYeYViCmWIwmOStowAI= +github.com/pelletier/go-toml/v2 v2.1.1/go.mod h1:tJU2Z3ZkXwnxa4DPO899bsyIoywizdUvyaeZurnPPDc= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/prometheus/client_golang v1.18.0 h1:HzFfmkOzH5Q8L8G+kSJKUx5dtG87sewO+FoDDqP5Tbk= +github.com/prometheus/client_golang v1.18.0/go.mod h1:T+GXkCk5wSJyOqMIzVgvvjFDlkOQntgjkJWKrN5txjA= +github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/prometheus/client_model v0.5.0 h1:VQw1hfvPvk3Uv6Qf29VrPF32JB6rtbgI6cYPYQjL0Qw= +github.com/prometheus/client_model v0.5.0/go.mod h1:dTiFglRmd66nLR9Pv9f0mZi7B7fk5Pm3gvsjB5tr+kI= +github.com/prometheus/common v0.45.0 h1:2BGz0eBc2hdMDLnO/8n0jeB3oPrt2D08CekT0lneoxM= +github.com/prometheus/common v0.45.0/go.mod h1:YJmSTw9BoKxJplESWWxlbyttQR4uaEcGyv9MZjVOJsY= +github.com/prometheus/procfs v0.12.0 h1:jluTpSng7V9hY0O2R9DzzJHYb2xULk9VTR1V1R/k6Bo= +github.com/prometheus/procfs v0.12.0/go.mod h1:pcuDEFsWDnvcgNzo4EEweacyhjeA9Zk3cnaOZAZEfOo= +github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ= +github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= +github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ= +github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg= +github.com/rs/zerolog v1.31.0 h1:FcTR3NnLWW+NnTwwhFWiJSZr4ECLpqCm6QsEnyvbV4A= +github.com/rs/zerolog v1.31.0/go.mod h1:/7mN4D5sKwJLZQ2b/znpjC3/GQWY/xaDXUM0kKWRHss= +github.com/sagikazarmark/locafero v0.4.0 h1:HApY1R9zGo4DBgr7dqsTH/JJxLTTsOt7u6keLGt6kNQ= +github.com/sagikazarmark/locafero v0.4.0/go.mod h1:Pe1W6UlPYUk/+wc/6KFhbORCfqzgYEpgQ3O5fPuL3H4= +github.com/sagikazarmark/slog-shim v0.1.0 h1:diDBnUNK9N/354PgrxMywXnAwEr1QZcOr6gto+ugjYE= +github.com/sagikazarmark/slog-shim v0.1.0/go.mod h1:SrcSrq8aKtyuqEI1uvTDTK1arOWRIczQRv+GVI1AkeQ= +github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo= +github.com/sourcegraph/conc v0.3.0/go.mod h1:Sdozi7LEKbFPqYX2/J+iBAM6HpqSLTASQIKqDmF7Mt0= +github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= +github.com/spf13/afero v1.11.0 h1:WJQKhtpdm3v2IzqG8VMqrr6Rf3UYpEF239Jy9wNepM8= +github.com/spf13/afero v1.11.0/go.mod h1:GH9Y3pIexgf1MTIWtNGyogA5MwRIDXGUr+hbWNoBjkY= +github.com/spf13/cast v1.6.0 h1:GEiTHELF+vaR5dhz3VqZfFSzZjYbgeKDpBxQVS4GYJ0= +github.com/spf13/cast v1.6.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo= +github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= +github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/spf13/viper v1.18.2 h1:LUXCnvUvSM6FXAsj6nnfc8Q2tp1dIgUfY9Kc8GsSOiQ= +github.com/spf13/viper v1.18.2/go.mod h1:EKmWIqdnk5lOcmR72yw6hS+8OPYcwD0jteitLMVB+yk= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0 h1:1zr/of2m5FGMsad5YfcqgdqdWrIhu+EBEJRhR1U7z/c= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8= +github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU= +github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= +github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= +github.com/valyala/fasttemplate v1.2.2 h1:lxLXG0uE3Qnshl9QyaK6XJxMXlQZELvChBOCmQD0Loo= +github.com/valyala/fasttemplate v1.2.2/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ= +github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= +go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= +go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= +go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= +go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= +go.opentelemetry.io/otel v1.16.0 h1:Z7GVAX/UkAXPKsy94IU+i6thsQS4nb7LviLpnaNeW8s= +go.opentelemetry.io/otel v1.16.0/go.mod h1:vl0h9NUa1D5s1nv3A5vZOYWn8av4K8Ml6JDeHrT/bx4= +go.opentelemetry.io/otel/exporters/otlp/internal/retry v1.16.0 h1:t4ZwRPU+emrcvM2e9DHd0Fsf0JTPVcbfa/BhTDF03d0= +go.opentelemetry.io/otel/exporters/otlp/internal/retry v1.16.0/go.mod h1:vLarbg68dH2Wa77g71zmKQqlQ8+8Rq3GRG31uc0WcWI= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.16.0 h1:cbsD4cUcviQGXdw8+bo5x2wazq10SKz8hEbtCRPcU78= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.16.0/go.mod h1:JgXSGah17croqhJfhByOLVY719k1emAXC8MVhCIJlRs= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.16.0 h1:TVQp/bboR4mhZSav+MdgXB8FaRho1RC8UwVn3T0vjVc= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.16.0/go.mod h1:I33vtIe0sR96wfrUcilIzLoA3mLHhRmz9S9Te0S3gDo= +go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.16.0 h1:+XWJd3jf75RXJq29mxbuXhCXFDG3S3R4vBUeSI2P7tE= +go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.16.0/go.mod h1:hqgzBPTf4yONMFgdZvL/bK42R/iinTyVQtiWihs3SZc= +go.opentelemetry.io/otel/metric v1.16.0 h1:RbrpwVG1Hfv85LgnZ7+txXioPDoh6EdbZHo26Q3hqOo= +go.opentelemetry.io/otel/metric v1.16.0/go.mod h1:QE47cpOmkwipPiefDwo2wDzwJrlfxxNYodqc4xnGCo4= +go.opentelemetry.io/otel/sdk v1.16.0 h1:Z1Ok1YsijYL0CSJpHt4cS3wDDh7p572grzNrBMiMWgE= +go.opentelemetry.io/otel/sdk v1.16.0/go.mod h1:tMsIuKXuuIWPBAOrH+eHtvhTL+SntFtXF9QD68aP6p4= +go.opentelemetry.io/otel/trace v1.16.0 h1:8JRpaObFoW0pxuVPapkgH8UhHQj+bJW8jJsCZEu5MQs= +go.opentelemetry.io/otel/trace v1.16.0/go.mod h1:Yt9vYq1SdNz3xdjZZK7wcXv1qv2pwLkqr2QVwea0ef0= +go.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqeYNgFYFoEGnI= +go.opentelemetry.io/proto/otlp v0.19.0 h1:IVN6GR+mhC4s5yfcTbmzHYODqvWAp3ZedA2SJPI1Nnw= +go.opentelemetry.io/proto/otlp v0.19.0/go.mod h1:H7XAot3MsfNsj7EXtrA2q5xSNQ10UqI405h3+duxN4U= +go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE= +go.uber.org/dig v1.17.1 h1:Tga8Lz8PcYNsWsyHMZ1Vm0OQOUaJNDyvPImgbAu9YSc= +go.uber.org/dig v1.17.1/go.mod h1:Us0rSJiThwCv2GteUN0Q7OKvU7n5J4dxZ9JKUXozFdE= +go.uber.org/fx v1.20.1 h1:zVwVQGS8zYvhh9Xxcu4w1M6ESyeMzebzj2NbSayZ4Mk= +go.uber.org/fx v1.20.1/go.mod h1:iSYNbHf2y55acNCwCXKx7LbWb5WG1Bnue5RDXz1OREg= +go.uber.org/goleak v1.2.1 h1:NBol2c7O1ZokfZ0LEU9K6Whx/KnwvepVetCUhtKja4A= +go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= +go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= +go.uber.org/zap v1.26.0 h1:sI7k6L95XOKS281NhVKOFCUNIvv9e0w4BF8N3u+tCRo= +go.uber.org/zap v1.26.0/go.mod h1:dtElttAiwGvoJ/vj4IwHBS/gXsEu/pZ50mUIRWuG0so= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.16.0 h1:mMMrFzRSCF0GvB7Ne27XVtVAaXLrPmgPC7/v0tkwHaY= +golang.org/x/crypto v0.16.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4= +golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= +golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek= +golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY= +golang.org/x/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= +golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= +golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= +golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM= +golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU= +golang.org/x/exp v0.0.0-20240110193028-0dcbfd608b1e h1:723BNChdd0c2Wk6WOE320qGBiPtYx0F0Bbm1kriShfE= +golang.org/x/exp v0.0.0-20240110193028-0dcbfd608b1e/go.mod h1:iRJReGqOEeBhDZGkGbynYwcHlctCvnjTYIamk7uXpHI= +golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= +golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= +golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= +golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs= +golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= +golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= +golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE= +golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o= +golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= +golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY= +golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= +golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= +golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200222125558-5a598a2470a0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200506145744-7e3656a0809f/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= +golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= +golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= +golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= +golang.org/x/net v0.19.0 h1:zTwKpTd2XuCqf8huc7Fo2iSy+4RHPd10s4KzeTnVr1c= +golang.org/x/net v0.19.0/go.mod h1:CfAk/cbD4CthTvqiEl8NpboMuiuOYsAr/7NOjZJtv1U= +golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= +golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20211104180415-d3ed0bb246c8/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200331124033-c3d80250170d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200501052902-10377860bb8e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200511232937-7e40ca221e25/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.16.0 h1:xWw16ngr6ZMtmxDyKyIgsE93KNKz5HKmMa3b8ALHidU= +golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= +golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk= +golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= +golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191130070609-6e064ea0cf2d/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191216173652-a0e659d51361/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20191227053925-7b8e75db28f4/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200117161641-43d50277825c/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200122220014-bf1340f18c4a/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200204074204-1cc6d1ef6c74/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200212150539-ea181f53ac56/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200224181240-023911ca70b2/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200227222343-706bc42d1f0d/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200304193943-95d2e580d8eb/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= +golang.org/x/tools v0.0.0-20200312045724-11d5b4c81c7d/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= +golang.org/x/tools v0.0.0-20200331025713-a30bf2db82d4/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8= +golang.org/x/tools v0.0.0-20200501065659-ab2804fb9c9d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200512131952-2bc93b1c0c88/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200515010526-7d3b6ebf133d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200618134242-20370b0cb4b2/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= +golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= +golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= +google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M= +google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= +google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= +google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= +google.golang.org/api v0.14.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= +google.golang.org/api v0.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= +google.golang.org/api v0.17.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.18.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.19.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.20.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.22.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.24.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= +google.golang.org/api v0.28.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= +google.golang.org/api v0.29.0/go.mod h1:Lcubydp8VUV7KeIHD9z2Bys/sm/vGKnG1UHuDBSrHWM= +google.golang.org/api v0.30.0/go.mod h1:QGmEvQ87FHZNiUVJkT14jQNYJ4ZJjdRF23ZXz5138Fc= +google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= +google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0= +google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= +google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= +google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= +google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= +google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= +google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8= +google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20191115194625-c23dd37a84c9/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20191216164720-4f79533eabd1/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20200115191322-ca5a22157cba/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20200122232147-0452cf42e150/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20200204135345-fa8e72b47b90/go.mod h1:GmwEX6Z4W5gMy59cAlVYjN9JhxgbQH6Gn+gFDQe2lzA= +google.golang.org/genproto v0.0.0-20200212174721-66ed5ce911ce/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200224152610-e50cd9704f63/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200228133532-8c2c7df3a383/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200305110556-506484158171/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200312145019-da6875a35672/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200331122359-1ee6d9798940/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200430143042-b979b6f78d84/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200511104702-f5ebc3bea380/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200513103714-09dca8ec2884/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200515170657-fc4c6c6a6587/go.mod h1:YsZOwe1myG/8QRHRsmBRE1LrgQY60beZKjly0O1fX9U= +google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= +google.golang.org/genproto v0.0.0-20200618031413-b414f8b61790/go.mod h1:jDfRM7FcilCzHH/e9qn6dsT145K34l5v+OpcnNgKAAA= +google.golang.org/genproto v0.0.0-20200729003335-053ba62fc06f/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20200804131852-c06518451d9c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20200825200019-8632dd797987/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20211118181313-81c1377c94b1/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= +google.golang.org/genproto v0.0.0-20231106174013-bbf56f31fb17 h1:wpZ8pe2x1Q3f2KyT5f8oP/fa9rHAKgFPr/HZdNuS+PQ= +google.golang.org/genproto v0.0.0-20231106174013-bbf56f31fb17/go.mod h1:J7XzRzVy1+IPwWHZUzoD0IccYZIrXILAQpc+Qy9CMhY= +google.golang.org/genproto/googleapis/api v0.0.0-20231106174013-bbf56f31fb17 h1:JpwMPBpFN3uKhdaekDpiNlImDdkUAyiJ6ez/uxGaUSo= +google.golang.org/genproto/googleapis/api v0.0.0-20231106174013-bbf56f31fb17/go.mod h1:0xJLfVdJqpAPl8tDg1ujOCGzx6LFLttXT5NhllGOXY4= +google.golang.org/genproto/googleapis/rpc v0.0.0-20231120223509-83a465c0220f h1:ultW7fxlIvee4HYrtnaRPon9HpEgFk5zYpmfMgtKB5I= +google.golang.org/genproto/googleapis/rpc v0.0.0-20231120223509-83a465c0220f/go.mod h1:L9KNLi232K1/xB6f7AlSX692koaRnKaWSR0stBki0Yc= +google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= +google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= +google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= +google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= +google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= +google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.28.0/go.mod h1:rpkK4SK4GF4Ach/+MFLZUBavHOvF2JJB5uozKKal+60= +google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk= +google.golang.org/grpc v1.30.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= +google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= +google.golang.org/grpc v1.33.1/go.mod h1:fr5YgcSWrqhRRxogOsw7RzIpsmvOZ6IcH4kBYTpR3n0= +google.golang.org/grpc v1.36.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= +google.golang.org/grpc v1.40.0/go.mod h1:ogyxbiOoUXAkP+4+xa6PZSE9DZgIHtSpzjDTB9KAK34= +google.golang.org/grpc v1.42.0/go.mod h1:k+4IHHFw41K8+bbowsex27ge2rCb65oeWqe4jJ590SU= +google.golang.org/grpc v1.59.0 h1:Z5Iec2pjwb+LEOqzpB2MR12/eKFhDPhuqW91O+4bwUk= +google.golang.org/grpc v1.59.0/go.mod h1:aUPDwccQo6OTjy7Hct4AfBPD1GptF4fyUjIkQ9YtF98= +google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= +google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= +google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= +google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= +google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= +google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4= +google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= +google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= +google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= +google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= +google.golang.org/protobuf v1.31.0 h1:g0LDEJHgrBl9N9r17Ru3sqWhkIx2NB67okBHPwC7hs8= +google.golang.org/protobuf v1.31.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= +gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA= +gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= +honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= +honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= +rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= +rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0= +rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA= diff --git a/fxhttpserver/info.go b/fxhttpserver/info.go new file mode 100644 index 0000000..0a705a0 --- /dev/null +++ b/fxhttpserver/info.go @@ -0,0 +1,57 @@ +package fxhttpserver + +import ( + "fmt" + + "github.com/ankorstore/yokai/config" + "github.com/labstack/echo/v4" +) + +// FxHttpServerModuleInfo is a module info collector for fxcore. +type FxHttpServerModuleInfo struct { + Port int + Debug bool + Logger string + Binder string + Serializer string + Renderer string + ErrorHandler string + Routes []*echo.Route +} + +// NewFxHttpServerModuleInfo returns a new [FxHttpServerModuleInfo]. +func NewFxHttpServerModuleInfo(httpServer *echo.Echo, cfg *config.Config) *FxHttpServerModuleInfo { + port := cfg.GetInt("modules.http.server.port") + if port == 0 { + port = DefaultPort + } + + return &FxHttpServerModuleInfo{ + Port: port, + Debug: httpServer.Debug, + Logger: fmt.Sprintf("%T", httpServer.Logger), + Binder: fmt.Sprintf("%T", httpServer.Binder), + Serializer: fmt.Sprintf("%T", httpServer.JSONSerializer), + Renderer: fmt.Sprintf("%T", httpServer.Renderer), + ErrorHandler: fmt.Sprintf("%T", httpServer.HTTPErrorHandler), + Routes: httpServer.Routes(), + } +} + +// Name return the name of the module info. +func (i *FxHttpServerModuleInfo) Name() string { + return ModuleName +} + +// Data return the data of the module info. +func (i *FxHttpServerModuleInfo) Data() map[string]interface{} { + return map[string]interface{}{ + "port": i.Port, + "debug": i.Debug, + "binder": i.Binder, + "serializer": i.Serializer, + "renderer": i.Renderer, + "errorHandler": i.ErrorHandler, + "routes": i.Routes, + } +} diff --git a/fxhttpserver/info_test.go b/fxhttpserver/info_test.go new file mode 100644 index 0000000..0539a86 --- /dev/null +++ b/fxhttpserver/info_test.go @@ -0,0 +1,40 @@ +package fxhttpserver_test + +import ( + "github.com/labstack/echo/v4" + "testing" + + "github.com/ankorstore/yokai/config" + "github.com/ankorstore/yokai/fxhttpserver" + "github.com/stretchr/testify/assert" +) + +func TestNewFxHttpServerModuleInfo(t *testing.T) { + t.Parallel() + + cfg, err := config.NewDefaultConfigFactory().Create( + config.WithFilePaths("./testdata/config"), + ) + assert.NoError(t, err) + + httpServer := echo.New() + httpServer.Debug = true + + info := fxhttpserver.NewFxHttpServerModuleInfo(httpServer, cfg) + assert.IsType(t, &fxhttpserver.FxHttpServerModuleInfo{}, info) + + assert.Equal(t, fxhttpserver.ModuleName, info.Name()) + assert.Equal( + t, + map[string]interface{}{ + "port": fxhttpserver.DefaultPort, + "debug": true, + "binder": "*echo.DefaultBinder", + "serializer": "*echo.DefaultJSONSerializer", + "renderer": "", + "errorHandler": "echo.HTTPErrorHandler", + "routes": []*echo.Route{}, + }, + info.Data(), + ) +} diff --git a/fxhttpserver/module.go b/fxhttpserver/module.go new file mode 100644 index 0000000..e662c5e --- /dev/null +++ b/fxhttpserver/module.go @@ -0,0 +1,252 @@ +package fxhttpserver + +import ( + "context" + "fmt" + "strconv" + "strings" + + "github.com/ankorstore/yokai/config" + "github.com/ankorstore/yokai/generate/uuid" + "github.com/ankorstore/yokai/httpserver" + httpservermiddleware "github.com/ankorstore/yokai/httpserver/middleware" + "github.com/ankorstore/yokai/log" + "github.com/labstack/echo/v4" + "github.com/prometheus/client_golang/prometheus" + "go.opentelemetry.io/otel/trace" + "go.uber.org/fx" +) + +const ( + ModuleName = "fx-httpserver" + DefaultPort = 8080 +) + +// FxHttpServerModule is the [Fx] httpserver module. +// +// [Fx]: https://github.com/uber-go/fx +var FxHttpServerModule = fx.Module( + ModuleName, + fx.Provide( + httpserver.NewDefaultHttpServerFactory, + NewFxHttpServerRegistry, + NewFxHttpServer, + fx.Annotate( + NewFxHttpServerModuleInfo, + fx.As(new(interface{})), + fx.ResultTags(`group:"core-module-infos"`), + ), + ), +) + +// FxHttpServerParam allows injection of the required dependencies in [NewFxHttpServer]. +type FxHttpServerParam struct { + fx.In + LifeCycle fx.Lifecycle + Factory httpserver.HttpServerFactory + Generator uuid.UuidGenerator + Registry *HttpServerRegistry + Config *config.Config + Logger *log.Logger + TracerProvider trace.TracerProvider + MetricsRegistry *prometheus.Registry +} + +// NewFxHttpServer returns a new [echo.Echo]. +func NewFxHttpServer(p FxHttpServerParam) (*echo.Echo, error) { + appDebug := p.Config.AppDebug() + + // logger + echoLogger := httpserver.NewEchoLogger( + log.FromZerolog(p.Logger.ToZerolog().With().Str("system", ModuleName).Logger()), + ) + + // renderer + var renderer echo.Renderer + if p.Config.GetBool("modules.http.server.templates.enabled") { + renderer = httpserver.NewHtmlTemplateRenderer(p.Config.GetString("modules.http.server.templates.path")) + } + + // server + httpServer, err := p.Factory.Create( + httpserver.WithDebug(appDebug), + httpserver.WithBanner(false), + httpserver.WithRecovery(true), + httpserver.WithLogger(echoLogger), + httpserver.WithRenderer(renderer), + httpserver.WithHttpErrorHandler( + httpserver.JsonErrorHandler( + p.Config.GetBool("modules.http.server.errors.obfuscate") || !appDebug, + p.Config.GetBool("modules.http.server.errors.stack") || appDebug, + ), + ), + ) + if err != nil { + return nil, fmt.Errorf("failed to create http server: %w", err) + } + + // middlewares + httpServer = withDefaultMiddlewares(httpServer, p) + + // groups, handlers & middlewares registrations + httpServer = withRegisteredResources(httpServer, p) + + // lifecycles + p.LifeCycle.Append(fx.Hook{ + OnStart: func(ctx context.Context) error { + if !p.Config.IsTestEnv() { + port := p.Config.GetInt("modules.http.server.port") + if port == 0 { + port = DefaultPort + } + + //nolint:errcheck + go httpServer.Start(fmt.Sprintf(":%d", port)) + } + + return nil + }, + OnStop: func(ctx context.Context) error { + if !p.Config.IsTestEnv() { + return httpServer.Shutdown(ctx) + } + + return nil + }, + }) + + return httpServer, nil +} + +func withDefaultMiddlewares(httpServer *echo.Echo, p FxHttpServerParam) *echo.Echo { + // request id middleware + httpServer.Use(httpservermiddleware.RequestIdMiddlewareWithConfig( + httpservermiddleware.RequestIdMiddlewareConfig{ + Generator: p.Generator, + }, + )) + + // request tracer middleware + if p.Config.GetBool("modules.http.server.trace.enabled") { + httpServer.Use(httpservermiddleware.RequestTracerMiddlewareWithConfig( + p.Config.AppName(), + httpservermiddleware.RequestTracerMiddlewareConfig{ + TracerProvider: p.TracerProvider, + RequestUriPrefixesToExclude: p.Config.GetStringSlice("modules.http.server.trace.exclude"), + }, + )) + } + + // request logger middleware + requestHeadersToLog := map[string]string{ + httpservermiddleware.HeaderXRequestId: httpservermiddleware.LogFieldRequestId, + } + + for headerName, fieldName := range p.Config.GetStringMapString("modules.http.server.log.headers") { + requestHeadersToLog[headerName] = fieldName + } + + httpServer.Use(httpservermiddleware.RequestLoggerMiddlewareWithConfig( + httpservermiddleware.RequestLoggerMiddlewareConfig{ + RequestHeadersToLog: requestHeadersToLog, + RequestUriPrefixesToExclude: p.Config.GetStringSlice("modules.http.server.log.exclude"), + LogLevelFromResponseOrErrorCode: p.Config.GetBool("modules.http.server.log.level_from_response"), + }, + )) + + // request metrics middleware + if p.Config.GetBool("modules.http.server.metrics.collect.enabled") { + namespace := p.Config.GetString("modules.http.server.metrics.collect.namespace") + if namespace == "" { + namespace = p.Config.AppName() + } + + subsystem := p.Config.GetString("modules.http.server.metrics.collect.subsystem") + if subsystem == "" { + subsystem = ModuleName + } + + var buckets []float64 + if bucketsConfig := p.Config.GetString("modules.http.server.metrics.buckets"); bucketsConfig != "" { + for _, s := range strings.Split(strings.ReplaceAll(bucketsConfig, " ", ""), ",") { + f, err := strconv.ParseFloat(s, 64) + if err == nil { + buckets = append(buckets, f) + } + } + } + + metricsMiddlewareConfig := httpservermiddleware.RequestMetricsMiddlewareConfig{ + Registry: p.MetricsRegistry, + Namespace: strings.ReplaceAll(namespace, "-", "_"), + Subsystem: strings.ReplaceAll(subsystem, "-", "_"), + Buckets: buckets, + NormalizeHTTPStatus: p.Config.GetBool("modules.http.server.metrics.normalize"), + } + + httpServer.Use(httpservermiddleware.RequestMetricsMiddlewareWithConfig(metricsMiddlewareConfig)) + } + + return httpServer +} + +func withRegisteredResources(httpServer *echo.Echo, p FxHttpServerParam) *echo.Echo { + // register handler groups + resolvedHandlersGroups, err := p.Registry.ResolveHandlersGroups() + if err != nil { + httpServer.Logger.Errorf("cannot resolve router handlers groups: %v", err) + } + + for _, g := range resolvedHandlersGroups { + group := httpServer.Group(g.Prefix(), g.Middlewares()...) + + for _, h := range g.Handlers() { + group.Add( + strings.ToUpper(h.Method()), + h.Path(), + h.Handler(), + h.Middlewares()..., + ) + httpServer.Logger.Debugf("registering handler in group for [%s]%s%s", h.Method(), g.Prefix(), h.Path()) + } + + httpServer.Logger.Debugf("registered handlers group for prefix %s", g.Prefix()) + } + + // register middlewares + resolvedMiddlewares, err := p.Registry.ResolveMiddlewares() + if err != nil { + httpServer.Logger.Errorf("cannot resolve router middlewares: %v", err) + } + + for _, m := range resolvedMiddlewares { + if m.Kind() == GlobalPre { + httpServer.Pre(m.Middleware()) + } + + if m.Kind() == GlobalUse { + httpServer.Use(m.Middleware()) + } + + httpServer.Logger.Debugf("registered %s middleware %T", m.Kind().String(), m.Middleware()) + } + + // register handlers + resolvedHandlers, err := p.Registry.ResolveHandlers() + if err != nil { + httpServer.Logger.Errorf("cannot resolve router handlers: %v", err) + } + + for _, h := range resolvedHandlers { + httpServer.Add( + strings.ToUpper(h.Method()), + h.Path(), + h.Handler(), + h.Middlewares()..., + ) + + httpServer.Logger.Debugf("registered handler for [%s]%s", h.Method(), h.Path()) + } + + return httpServer +} diff --git a/fxhttpserver/module_test.go b/fxhttpserver/module_test.go new file mode 100644 index 0000000..abc987b --- /dev/null +++ b/fxhttpserver/module_test.go @@ -0,0 +1,844 @@ +package fxhttpserver_test + +import ( + "fmt" + "net/http" + "net/http/httptest" + "strings" + "testing" + + "github.com/ankorstore/yokai/fxconfig" + "github.com/ankorstore/yokai/fxgenerate" + "github.com/ankorstore/yokai/fxhttpserver" + "github.com/ankorstore/yokai/fxhttpserver/testdata/factory" + "github.com/ankorstore/yokai/fxhttpserver/testdata/handler" + "github.com/ankorstore/yokai/fxhttpserver/testdata/middleware" + "github.com/ankorstore/yokai/fxhttpserver/testdata/service" + "github.com/ankorstore/yokai/fxlog" + "github.com/ankorstore/yokai/fxmetrics" + "github.com/ankorstore/yokai/fxtrace" + "github.com/ankorstore/yokai/httpserver" + "github.com/ankorstore/yokai/log" + "github.com/ankorstore/yokai/log/logtest" + "github.com/ankorstore/yokai/trace/tracetest" + "github.com/labstack/echo/v4" + echomiddleware "github.com/labstack/echo/v4/middleware" + "github.com/prometheus/client_golang/prometheus" + "github.com/prometheus/client_golang/prometheus/testutil" + "github.com/stretchr/testify/assert" + "go.opentelemetry.io/otel/attribute" + semconv "go.opentelemetry.io/otel/semconv/v1.17.0" + "go.uber.org/fx" + "go.uber.org/fx/fxtest" +) + +var ( + // request headers parts + testRequestId = "33084b3e-9b90-926c-af19-3859d70bd296" + testTraceId = "c4ca71e03e42c2c3d54293a6e2608bfa" + testSpanId = "8d0fdc8a74baaaea" + testTraceParent = fmt.Sprintf("00-%s-%s-01", testTraceId, testSpanId) + + // resources + concreteGlobalMiddleware = func(next echo.HandlerFunc) echo.HandlerFunc { + return func(c echo.Context) error { + log.CtxLogger(c.Request().Context()).Info().Msg("CONCRETE GLOBAL middleware") + + return next(c) + } + } + + concreteGroupMiddleware = func(next echo.HandlerFunc) echo.HandlerFunc { + return func(c echo.Context) error { + log.CtxLogger(c.Request().Context()).Info().Msg("CONCRETE GROUP middleware") + + return next(c) + } + } + + concreteHandlerMiddleware = func(next echo.HandlerFunc) echo.HandlerFunc { + return func(c echo.Context) error { + log.CtxLogger(c.Request().Context()).Info().Msg("CONCRETE HANDLER middleware") + + return next(c) + } + } + + concreteHandler = func(c echo.Context) error { + log.CtxLogger(c.Request().Context()).Info().Msg("in concrete handler") + + return c.JSON(http.StatusOK, "concrete") + } +) + +//nolint:maintidx +func TestModuleWithAutowiredResources(t *testing.T) { + t.Setenv("APP_CONFIG_PATH", "testdata/config") + + var httpServer *echo.Echo + var logBuffer logtest.TestLogBuffer + var traceExporter tracetest.TestTraceExporter + + fxtest.New( + t, + fx.NopLogger, + fxconfig.FxConfigModule, + fxlog.FxLogModule, + fxtrace.FxTraceModule, + fxmetrics.FxMetricsModule, + fxgenerate.FxGenerateModule, + fxhttpserver.FxHttpServerModule, + fx.Provide(service.NewTestService), + fx.Options( + fxhttpserver.AsMiddleware(middleware.NewTestGlobalMiddleware, fxhttpserver.GlobalUse), + fxhttpserver.AsHandler("GET", "/bar", handler.NewTestBarHandler, middleware.NewTestHandlerMiddleware), + fxhttpserver.AsHandler("GET", "/baz", handler.NewTestBazHandler, middleware.NewTestHandlerMiddleware), + fxhttpserver.AsHandlersGroup( + "/foo", + []*fxhttpserver.HandlerRegistration{ + fxhttpserver.NewHandlerRegistration("GET", "/bar", handler.NewTestBarHandler, middleware.NewTestHandlerMiddleware), + fxhttpserver.NewHandlerRegistration("GET", "/baz", handler.NewTestBazHandler, middleware.NewTestHandlerMiddleware), + }, + middleware.NewTestGroupMiddleware, + ), + ), + fx.Populate(&httpServer, &logBuffer, &traceExporter), + ).RequireStart().RequireStop() + + // [GET] /bar + req := httptest.NewRequest(http.MethodGet, "/bar", nil) + req.Header.Add("x-request-id", testRequestId) + req.Header.Add("traceparent", testTraceParent) + req.Header.Add("x-foo", "foo") + req.Header.Add("x-bar", "bar") + rec := httptest.NewRecorder() + httpServer.ServeHTTP(rec, req) + + assert.Equal(t, http.StatusOK, rec.Code) + assert.Contains(t, rec.Body.String(), "bar: test") + assert.Equal(t, "true", rec.Header().Get("global-middleware")) + assert.Equal(t, "", rec.Header().Get("group-middleware")) + assert.Equal(t, "true", rec.Header().Get("handler-middleware")) + + logtest.AssertHasLogRecord(t, logBuffer, map[string]interface{}{ + "level": "info", + "service": "test", + "system": "fx-httpserver", + "requestID": testRequestId, + "traceID": testTraceId, + "foo": "foo", + "bar": "bar", + "message": "GLOBAL middleware for app: test", + }) + logtest.AssertHasLogRecord(t, logBuffer, map[string]interface{}{ + "level": "info", + "service": "test", + "system": "fx-httpserver", + "requestID": testRequestId, + "traceID": testTraceId, + "foo": "foo", + "bar": "bar", + "message": "HANDLER middleware for app: test", + }) + logtest.AssertHasLogRecord(t, logBuffer, map[string]interface{}{ + "level": "info", + "service": "test", + "system": "fx-httpserver", + "requestID": testRequestId, + "traceID": testTraceId, + "foo": "foo", + "bar": "bar", + "message": "in bar handler", + }) + logtest.AssertHasLogRecord(t, logBuffer, map[string]interface{}{ + "level": "info", + "service": "test", + "system": "fx-httpserver", + "method": "GET", + "uri": "/bar", + "status": 200, + "requestID": testRequestId, + "traceID": testTraceId, + "foo": "foo", + "bar": "bar", + "message": "request logger", + }) + + tracetest.AssertHasTraceSpan( + t, + traceExporter, + "bar span", + attribute.String(httpserver.TraceSpanAttributeHttpRequestId, testRequestId), + ) + tracetest.AssertHasTraceSpan( + t, + traceExporter, + "GET /bar", + semconv.HTTPMethod(http.MethodGet), + semconv.HTTPRoute("/bar"), + semconv.HTTPStatusCode(http.StatusOK), + attribute.String(httpserver.TraceSpanAttributeHttpRequestId, testRequestId), + ) + + // [GET] /baz + req = httptest.NewRequest(http.MethodGet, "/baz", nil) + req.Header.Add("x-request-id", testRequestId) + req.Header.Add("traceparent", testTraceParent) + req.Header.Add("x-foo", "foo") + req.Header.Add("x-bar", "bar") + rec = httptest.NewRecorder() + httpServer.ServeHTTP(rec, req) + + assert.Equal(t, http.StatusOK, rec.Code) + assert.Contains(t, rec.Body.String(), "baz: test") + assert.Equal(t, "true", rec.Header().Get("global-middleware")) + assert.Equal(t, "", rec.Header().Get("group-middleware")) + assert.Equal(t, "true", rec.Header().Get("handler-middleware")) + + logtest.AssertHasLogRecord(t, logBuffer, map[string]interface{}{ + "level": "info", + "service": "test", + "system": "fx-httpserver", + "requestID": testRequestId, + "traceID": testTraceId, + "foo": "foo", + "bar": "bar", + "message": "GLOBAL middleware for app: test", + }) + logtest.AssertHasLogRecord(t, logBuffer, map[string]interface{}{ + "level": "info", + "service": "test", + "system": "fx-httpserver", + "requestID": testRequestId, + "traceID": testTraceId, + "foo": "foo", + "bar": "bar", + "message": "HANDLER middleware for app: test", + }) + logtest.AssertHasLogRecord(t, logBuffer, map[string]interface{}{ + "level": "info", + "service": "test", + "system": "fx-httpserver", + "requestID": testRequestId, + "traceID": testTraceId, + "foo": "foo", + "bar": "bar", + "message": "in baz handler", + }) + logtest.AssertHasLogRecord(t, logBuffer, map[string]interface{}{ + "level": "info", + "service": "test", + "system": "fx-httpserver", + "method": "GET", + "uri": "/baz", + "status": 200, + "requestID": testRequestId, + "traceID": testTraceId, + "foo": "foo", + "bar": "bar", + "message": "request logger", + }) + + tracetest.AssertHasTraceSpan( + t, + traceExporter, + "baz span", + attribute.String(httpserver.TraceSpanAttributeHttpRequestId, testRequestId), + ) + tracetest.AssertHasTraceSpan( + t, + traceExporter, + "GET /baz", + semconv.HTTPMethod(http.MethodGet), + semconv.HTTPRoute("/baz"), + semconv.HTTPStatusCode(http.StatusOK), + attribute.String(httpserver.TraceSpanAttributeHttpRequestId, testRequestId), + ) + + // [GET] /foo/bar + req = httptest.NewRequest(http.MethodGet, "/foo/bar", nil) + req.Header.Add("x-request-id", testRequestId) + req.Header.Add("traceparent", testTraceParent) + req.Header.Add("x-foo", "foo") + req.Header.Add("x-bar", "bar") + rec = httptest.NewRecorder() + httpServer.ServeHTTP(rec, req) + + assert.Equal(t, http.StatusOK, rec.Code) + assert.Contains(t, rec.Body.String(), "bar: test") + assert.Equal(t, "true", rec.Header().Get("global-middleware")) + assert.Equal(t, "true", rec.Header().Get("group-middleware")) + assert.Equal(t, "true", rec.Header().Get("handler-middleware")) + + logtest.AssertHasLogRecord(t, logBuffer, map[string]interface{}{ + "level": "info", + "service": "test", + "system": "fx-httpserver", + "requestID": testRequestId, + "traceID": testTraceId, + "foo": "foo", + "bar": "bar", + "message": "GLOBAL middleware for app: test", + }) + logtest.AssertHasLogRecord(t, logBuffer, map[string]interface{}{ + "level": "info", + "service": "test", + "system": "fx-httpserver", + "requestID": testRequestId, + "traceID": testTraceId, + "foo": "foo", + "bar": "bar", + "message": "GROUP middleware for app: test", + }) + logtest.AssertHasLogRecord(t, logBuffer, map[string]interface{}{ + "level": "info", + "service": "test", + "system": "fx-httpserver", + "requestID": testRequestId, + "traceID": testTraceId, + "foo": "foo", + "bar": "bar", + "message": "HANDLER middleware for app: test", + }) + logtest.AssertHasLogRecord(t, logBuffer, map[string]interface{}{ + "level": "info", + "service": "test", + "system": "fx-httpserver", + "requestID": testRequestId, + "traceID": testTraceId, + "foo": "foo", + "bar": "bar", + "message": "in bar handler", + }) + logtest.AssertHasNotLogRecord(t, logBuffer, map[string]interface{}{ + "level": "info", + "service": "test", + "system": "fx-httpserver", + "method": "GET", + "uri": "/foo/bar", + "status": 200, + "requestID": testRequestId, + "traceID": testTraceId, + "foo": "foo", + "bar": "bar", + "message": "request logger", + }) + + tracetest.AssertHasTraceSpan( + t, + traceExporter, + "bar span", + attribute.String(httpserver.TraceSpanAttributeHttpRequestId, testRequestId), + ) + assert.False( + t, + traceExporter.HasSpan( + "GET /foo/bar", + semconv.HTTPMethod(http.MethodGet), + semconv.HTTPRoute("/foo/bar"), + semconv.HTTPStatusCode(http.StatusOK), + attribute.String(httpserver.TraceSpanAttributeHttpRequestId, testRequestId), + ), + ) + + // [GET] /foo/baz + req = httptest.NewRequest(http.MethodGet, "/foo/baz", nil) + req.Header.Add("x-request-id", testRequestId) + req.Header.Add("traceparent", testTraceParent) + req.Header.Add("x-foo", "foo") + req.Header.Add("x-bar", "bar") + rec = httptest.NewRecorder() + httpServer.ServeHTTP(rec, req) + + assert.Equal(t, http.StatusOK, rec.Code) + assert.Contains(t, rec.Body.String(), "baz: test") + assert.Equal(t, "true", rec.Header().Get("global-middleware")) + assert.Equal(t, "true", rec.Header().Get("group-middleware")) + assert.Equal(t, "true", rec.Header().Get("handler-middleware")) + + logtest.AssertHasLogRecord(t, logBuffer, map[string]interface{}{ + "level": "info", + "service": "test", + "system": "fx-httpserver", + "requestID": testRequestId, + "traceID": testTraceId, + "foo": "foo", + "bar": "bar", + "message": "GLOBAL middleware for app: test", + }) + logtest.AssertHasLogRecord(t, logBuffer, map[string]interface{}{ + "level": "info", + "service": "test", + "system": "fx-httpserver", + "requestID": testRequestId, + "traceID": testTraceId, + "foo": "foo", + "bar": "bar", + "message": "GROUP middleware for app: test", + }) + logtest.AssertHasLogRecord(t, logBuffer, map[string]interface{}{ + "level": "info", + "service": "test", + "system": "fx-httpserver", + "requestID": testRequestId, + "traceID": testTraceId, + "foo": "foo", + "bar": "bar", + "message": "HANDLER middleware for app: test", + }) + logtest.AssertHasLogRecord(t, logBuffer, map[string]interface{}{ + "level": "info", + "service": "test", + "system": "fx-httpserver", + "requestID": testRequestId, + "traceID": testTraceId, + "foo": "foo", + "bar": "bar", + "message": "in baz handler", + }) + logtest.AssertHasNotLogRecord(t, logBuffer, map[string]interface{}{ + "level": "info", + "service": "test", + "system": "fx-httpserver", + "method": "GET", + "uri": "/foo/baz", + "status": 200, + "requestID": testRequestId, + "traceID": testTraceId, + "foo": "foo", + "bar": "bar", + "message": "request logger", + }) + + tracetest.AssertHasTraceSpan( + t, + traceExporter, + "baz span", + attribute.String(httpserver.TraceSpanAttributeHttpRequestId, testRequestId), + ) + assert.False( + t, + traceExporter.HasSpan( + "GET /foo/baz", + semconv.HTTPMethod(http.MethodGet), + semconv.HTTPRoute("/foo/baz"), + semconv.HTTPStatusCode(http.StatusOK), + attribute.String(httpserver.TraceSpanAttributeHttpRequestId, testRequestId), + ), + ) + + // [GET] /invalid + req = httptest.NewRequest(http.MethodGet, "/invalid", nil) + req.Header.Add("x-request-id", testRequestId) + req.Header.Add("traceparent", testTraceParent) + rec = httptest.NewRecorder() + httpServer.ServeHTTP(rec, req) + + assert.Equal(t, http.StatusNotFound, rec.Code) + + logtest.AssertHasLogRecord(t, logBuffer, map[string]interface{}{ + "level": "warn", + "service": "test", + "system": "fx-httpserver", + "method": "GET", + "uri": "/invalid", + "status": 404, + "requestID": testRequestId, + "traceID": testTraceId, + "error": "code=404, message=Not Found", + "message": "request logger", + }) + + tracetest.AssertHasTraceSpan( + t, + traceExporter, + "GET /invalid", + semconv.HTTPMethod(http.MethodGet), + semconv.HTTPStatusCode(http.StatusNotFound), + attribute.String(httpserver.TraceSpanAttributeHttpRequestId, testRequestId), + ) +} + +func TestModuleWithConcreteResources(t *testing.T) { + t.Setenv("APP_CONFIG_PATH", "testdata/config") + + var httpServer *echo.Echo + var logBuffer logtest.TestLogBuffer + var traceExporter tracetest.TestTraceExporter + + fxtest.New( + t, + fx.NopLogger, + fxconfig.FxConfigModule, + fxlog.FxLogModule, + fxtrace.FxTraceModule, + fxmetrics.FxMetricsModule, + fxgenerate.FxGenerateModule, + fxhttpserver.FxHttpServerModule, + fx.Provide(service.NewTestService), + fx.Options( + fxhttpserver.AsMiddleware(concreteGlobalMiddleware, fxhttpserver.GlobalUse), + fxhttpserver.AsHandler("GET", "/concrete", concreteHandler, concreteHandlerMiddleware), + fxhttpserver.AsHandlersGroup( + "/group", + []*fxhttpserver.HandlerRegistration{ + fxhttpserver.NewHandlerRegistration("GET", "/concrete", concreteHandler, concreteHandlerMiddleware), + }, + concreteGroupMiddleware, + ), + ), + fx.Populate(&httpServer, &logBuffer, &traceExporter), + ).RequireStart().RequireStop() + + // [GET] /concrete + req := httptest.NewRequest(http.MethodGet, "/concrete", nil) + req.Header.Add("x-request-id", testRequestId) + req.Header.Add("traceparent", testTraceParent) + rec := httptest.NewRecorder() + httpServer.ServeHTTP(rec, req) + + assert.Equal(t, http.StatusOK, rec.Code) + assert.Contains(t, rec.Body.String(), "concrete") + + logtest.AssertHasLogRecord(t, logBuffer, map[string]interface{}{ + "level": "info", + "service": "test", + "requestID": testRequestId, + "traceID": testTraceId, + "message": "CONCRETE GLOBAL middleware", + }) + logtest.AssertHasLogRecord(t, logBuffer, map[string]interface{}{ + "level": "info", + "service": "test", + "requestID": testRequestId, + "traceID": testTraceId, + "message": "CONCRETE HANDLER middleware", + }) + logtest.AssertHasLogRecord(t, logBuffer, map[string]interface{}{ + "level": "info", + "service": "test", + "requestID": testRequestId, + "traceID": testTraceId, + "message": "in concrete handler", + }) + logtest.AssertHasLogRecord(t, logBuffer, map[string]interface{}{ + "level": "info", + "service": "test", + "method": "GET", + "uri": "/concrete", + "status": 200, + "message": "request logger", + "requestID": testRequestId, + "traceID": testTraceId, + }) + + tracetest.AssertHasTraceSpan( + t, + traceExporter, + "GET /concrete", + semconv.HTTPMethod(http.MethodGet), + semconv.HTTPRoute("/concrete"), + semconv.HTTPStatusCode(http.StatusOK), + attribute.String(httpserver.TraceSpanAttributeHttpRequestId, testRequestId), + ) + + // [GET] /group/concrete + req = httptest.NewRequest(http.MethodGet, "/group/concrete", nil) + req.Header.Add("x-request-id", testRequestId) + req.Header.Add("traceparent", testTraceParent) + rec = httptest.NewRecorder() + httpServer.ServeHTTP(rec, req) + + assert.Equal(t, http.StatusOK, rec.Code) + assert.Contains(t, rec.Body.String(), "concrete") + + logtest.AssertHasLogRecord(t, logBuffer, map[string]interface{}{ + "level": "info", + "service": "test", + "requestID": testRequestId, + "traceID": testTraceId, + "message": "CONCRETE GLOBAL middleware", + }) + logtest.AssertHasLogRecord(t, logBuffer, map[string]interface{}{ + "level": "info", + "service": "test", + "requestID": testRequestId, + "traceID": testTraceId, + "message": "CONCRETE GROUP middleware", + }) + logtest.AssertHasLogRecord(t, logBuffer, map[string]interface{}{ + "level": "info", + "service": "test", + "requestID": testRequestId, + "traceID": testTraceId, + "message": "CONCRETE HANDLER middleware", + }) + logtest.AssertHasLogRecord(t, logBuffer, map[string]interface{}{ + "level": "info", + "service": "test", + "requestID": testRequestId, + "traceID": testTraceId, + "message": "in concrete handler", + }) + logtest.AssertHasLogRecord(t, logBuffer, map[string]interface{}{ + "level": "info", + "service": "test", + "method": "GET", + "uri": "/group/concrete", + "status": 200, + "message": "request logger", + "requestID": testRequestId, + "traceID": testTraceId, + }) + + tracetest.AssertHasTraceSpan( + t, + traceExporter, + "GET /group/concrete", + semconv.HTTPMethod(http.MethodGet), + semconv.HTTPRoute("/group/concrete"), + semconv.HTTPStatusCode(http.StatusOK), + attribute.String(httpserver.TraceSpanAttributeHttpRequestId, testRequestId), + ) + + // [GET] /invalid + req = httptest.NewRequest(http.MethodGet, "/invalid", nil) + req.Header.Add("x-request-id", testRequestId) + req.Header.Add("traceparent", testTraceParent) + rec = httptest.NewRecorder() + httpServer.ServeHTTP(rec, req) + + assert.Equal(t, http.StatusNotFound, rec.Code) + + logtest.AssertHasLogRecord(t, logBuffer, map[string]interface{}{ + "level": "warn", + "service": "test", + "method": "GET", + "uri": "/invalid", + "error": "code=404, message=Not Found", + "status": 404, + "message": "request logger", + "requestID": testRequestId, + "traceID": testTraceId, + }) + + tracetest.AssertHasTraceSpan( + t, + traceExporter, + "GET /invalid", + semconv.HTTPMethod(http.MethodGet), + semconv.HTTPStatusCode(http.StatusNotFound), + attribute.String(httpserver.TraceSpanAttributeHttpRequestId, testRequestId), + ) +} + +func TestModuleWithEchoResources(t *testing.T) { + t.Setenv("APP_CONFIG_PATH", "testdata/config") + + var httpServer *echo.Echo + + fxtest.New( + t, + fx.NopLogger, + fxconfig.FxConfigModule, + fxlog.FxLogModule, + fxtrace.FxTraceModule, + fxmetrics.FxMetricsModule, + fxgenerate.FxGenerateModule, + fxhttpserver.FxHttpServerModule, + fx.Provide(service.NewTestService), + fx.Options( + fxhttpserver.AsMiddleware( + echomiddleware.Rewrite(map[string]string{"/abstract": "/concrete"}), + fxhttpserver.GlobalPre, + ), + fxhttpserver.AsHandler("GET", "/concrete", concreteHandler, echomiddleware.CORS()), + fxhttpserver.AsHandlersGroup( + "/group", + []*fxhttpserver.HandlerRegistration{ + fxhttpserver.NewHandlerRegistration("GET", "/concrete", concreteHandler, echomiddleware.CORS()), + }, + echomiddleware.Secure(), + ), + ), + fx.Populate(&httpServer), + ).RequireStart().RequireStop() + + // [GET] /abstract + req := httptest.NewRequest(http.MethodGet, "/abstract", nil) + rec := httptest.NewRecorder() + httpServer.ServeHTTP(rec, req) + + assert.Contains(t, rec.Body.String(), "concrete") + assert.Equal(t, "Origin", rec.Header().Get(echo.HeaderVary)) // CORS middleware + + // [GET] /concrete + req = httptest.NewRequest(http.MethodGet, "/concrete", nil) + rec = httptest.NewRecorder() + httpServer.ServeHTTP(rec, req) + + assert.Contains(t, rec.Body.String(), "concrete") + assert.Equal(t, "Origin", rec.Header().Get(echo.HeaderVary)) // CORS middleware + + // [GET] /group/concrete + req = httptest.NewRequest(http.MethodGet, "/group/concrete", nil) + rec = httptest.NewRecorder() + httpServer.ServeHTTP(rec, req) + + assert.Contains(t, rec.Body.String(), "concrete") + assert.Equal(t, "Origin", rec.Header().Get(echo.HeaderVary)) // CORS middleware + assert.Equal(t, "SAMEORIGIN", rec.Header().Get(echo.HeaderXFrameOptions)) // Secure middleware +} + +func TestModuleWithMetrics(t *testing.T) { + t.Setenv("APP_CONFIG_PATH", "testdata/config") + t.Setenv("APP_DEBUG", "true") + + var httpServer *echo.Echo + var logBuffer logtest.TestLogBuffer + var traceExporter tracetest.TestTraceExporter + var metricsRegistry *prometheus.Registry + + fxtest.New( + t, + fx.NopLogger, + fxconfig.FxConfigModule, + fxlog.FxLogModule, + fxtrace.FxTraceModule, + fxmetrics.FxMetricsModule, + fxgenerate.FxGenerateModule, + fxhttpserver.FxHttpServerModule, + fx.Provide(service.NewTestService), + fx.Options( + fxhttpserver.AsHandler("GET", "/bar", handler.NewTestBarHandler), + ), + fx.Populate(&httpServer, &logBuffer, &traceExporter, &metricsRegistry), + ).RequireStart().RequireStop() + + // [GET] /bar + req := httptest.NewRequest(http.MethodGet, "/bar", nil) + rec := httptest.NewRecorder() + httpServer.ServeHTTP(rec, req) + + assert.Equal(t, http.StatusOK, rec.Code) + assert.Contains(t, rec.Body.String(), "bar: test") + + logtest.AssertHasLogRecord(t, logBuffer, map[string]interface{}{ + "level": "info", + "service": "test", + "message": "in bar handler", + }) + + logtest.AssertHasLogRecord(t, logBuffer, map[string]interface{}{ + "level": "info", + "service": "test", + "method": "GET", + "uri": "/bar", + "status": 200, + "message": "request logger", + }) + + tracetest.AssertHasTraceSpan( + t, + traceExporter, + "GET /bar", + semconv.HTTPMethod(http.MethodGet), + semconv.HTTPRoute("/bar"), + semconv.HTTPStatusCode(http.StatusOK), + ) + + expectedHelp := ` + # HELP foo_bar_requests_total Number of processed HTTP requests + # TYPE foo_bar_requests_total counter + ` + expectedMetric := ` + foo_bar_requests_total{handler="/bar",method="GET",status="2xx"} 1 + ` + + err := testutil.GatherAndCompare( + metricsRegistry, + strings.NewReader(expectedHelp+expectedMetric), + "foo_bar_requests_total", + ) + assert.NoError(t, err) +} + +func TestModuleWithTemplates(t *testing.T) { + t.Setenv("APP_CONFIG_PATH", "testdata/config") + t.Setenv("APP_DEBUG", "true") + t.Setenv("TEMPLATES_ENABLED", "true") + t.Setenv("TEMPLATES_PATH", "testdata/templates/*.html") + + var httpServer *echo.Echo + var logBuffer logtest.TestLogBuffer + var traceExporter tracetest.TestTraceExporter + + fxtest.New( + t, + fx.NopLogger, + fxconfig.FxConfigModule, + fxlog.FxLogModule, + fxtrace.FxTraceModule, + fxmetrics.FxMetricsModule, + fxgenerate.FxGenerateModule, + fxhttpserver.FxHttpServerModule, + fx.Provide(service.NewTestService), + fx.Options( + fxhttpserver.AsHandler("GET", "/template", handler.NewTestTemplateHandler), + ), + fx.Populate(&httpServer, &logBuffer, &traceExporter), + ).RequireStart().RequireStop() + + // [GET] /template + req := httptest.NewRequest(http.MethodGet, "/template", nil) + rec := httptest.NewRecorder() + httpServer.ServeHTTP(rec, req) + + assert.Equal(t, http.StatusOK, rec.Code) + assert.Contains(t, rec.Body.String(), "App name: test") + + logtest.AssertHasLogRecord(t, logBuffer, map[string]interface{}{ + "level": "info", + "service": "test", + "message": "in template handler", + }) + + logtest.AssertHasLogRecord(t, logBuffer, map[string]interface{}{ + "level": "info", + "service": "test", + "method": "GET", + "uri": "/template", + "status": 200, + "message": "request logger", + }) + + tracetest.AssertHasTraceSpan( + t, + traceExporter, + "GET /template", + semconv.HTTPMethod(http.MethodGet), + semconv.HTTPRoute("/template"), + semconv.HTTPStatusCode(http.StatusOK), + ) +} + +func TestModuleDecoration(t *testing.T) { + t.Setenv("APP_CONFIG_PATH", "testdata/config") + + var httpServer *echo.Echo + + fxtest.New( + t, + fx.NopLogger, + fxconfig.FxConfigModule, + fxlog.FxLogModule, + fxtrace.FxTraceModule, + fxmetrics.FxMetricsModule, + fxgenerate.FxGenerateModule, + fxhttpserver.FxHttpServerModule, + fx.Decorate(factory.NewTestHttpServerFactory), + fx.Populate(&httpServer), + ).RequireStart().RequireStop() + + assert.False(t, httpServer.HideBanner) +} diff --git a/fxhttpserver/reflect.go b/fxhttpserver/reflect.go new file mode 100644 index 0000000..6d032b5 --- /dev/null +++ b/fxhttpserver/reflect.go @@ -0,0 +1,27 @@ +package fxhttpserver + +import ( + "reflect" + + "github.com/labstack/echo/v4" +) + +// GetType returns the type of a target. +func GetType(target any) string { + return reflect.TypeOf(target).String() +} + +// GetReturnType returns the return type of a target. +func GetReturnType(target any) string { + return reflect.TypeOf(target).Out(0).String() +} + +// IsConcreteMiddleware returns true if the middleware is a concrete [echo.MiddlewareFunc] implementation. +func IsConcreteMiddleware(middleware any) bool { + return reflect.TypeOf(middleware).ConvertibleTo(reflect.TypeOf(echo.MiddlewareFunc(nil))) +} + +// IsConcreteHandler returns true if the handler is a concrete [echo.HandlerFunc] implementation. +func IsConcreteHandler(handler any) bool { + return reflect.TypeOf(handler).ConvertibleTo(reflect.TypeOf(echo.HandlerFunc(nil))) +} diff --git a/fxhttpserver/reflect_test.go b/fxhttpserver/reflect_test.go new file mode 100644 index 0000000..672710f --- /dev/null +++ b/fxhttpserver/reflect_test.go @@ -0,0 +1,105 @@ +package fxhttpserver_test + +import ( + "testing" + + "github.com/ankorstore/yokai/fxhttpserver" + "github.com/labstack/echo/v4" + "github.com/stretchr/testify/assert" +) + +func TestGetType(t *testing.T) { + t.Parallel() + + tests := []struct { + target any + expected string + }{ + {123, "int"}, + {"test", "string"}, + {echo.MiddlewareFunc(func(next echo.HandlerFunc) echo.HandlerFunc { + return next + }), "echo.MiddlewareFunc"}, + } + + for _, tt := range tests { + tt := tt + + t.Run(tt.expected, func(t *testing.T) { + t.Parallel() + + got := fxhttpserver.GetType(tt.target) + assert.Equal(t, tt.expected, got) + }) + } +} + +func TestGetReturnType(t *testing.T) { + t.Parallel() + + tests := []struct { + target any + expected string + }{ + {func() string { return "test" }, "string"}, + {func() int { return 123 }, "int"}, + } + + for _, tt := range tests { + tt := tt + t.Run(tt.expected, func(t *testing.T) { + t.Parallel() + + got := fxhttpserver.GetReturnType(tt.target) + assert.Equal(t, tt.expected, got) + }) + } +} + +func TestIsConcreteMiddleware(t *testing.T) { + t.Parallel() + + tests := []struct { + middleware any + expected bool + }{ + {echo.MiddlewareFunc(func(next echo.HandlerFunc) echo.HandlerFunc { return next }), true}, + {123, false}, + {"test", false}, + } + + for _, tt := range tests { + tt := tt + + t.Run(fxhttpserver.GetType(tt.middleware), func(t *testing.T) { + t.Parallel() + + got := fxhttpserver.IsConcreteMiddleware(tt.middleware) + assert.Equal(t, tt.expected, got) + }) + } +} + +func TestIsConcreteHandler(t *testing.T) { + t.Parallel() + + tests := []struct { + handler any + expected bool + }{ + {echo.HandlerFunc(func(c echo.Context) error { return nil }), true}, + {123, false}, + {"test", false}, + } + + for _, tt := range tests { + tt := tt + + t.Run(fxhttpserver.GetType(tt.handler), func(t *testing.T) { + t.Parallel() + + got := fxhttpserver.IsConcreteHandler(tt.handler) + assert.Equal(t, tt.expected, got) + }) + } +} diff --git a/fxhttpserver/register.go b/fxhttpserver/register.go new file mode 100644 index 0000000..4356818 --- /dev/null +++ b/fxhttpserver/register.go @@ -0,0 +1,293 @@ +package fxhttpserver + +import ( + "go.uber.org/fx" +) + +// MiddlewareRegistration is a middleware registration. +type MiddlewareRegistration struct { + middleware any + kind MiddlewareKind +} + +// NewMiddlewareRegistration returns a new [MiddlewareRegistration]. +func NewMiddlewareRegistration(middleware any, kind MiddlewareKind) *MiddlewareRegistration { + return &MiddlewareRegistration{ + middleware: middleware, + kind: kind, + } +} + +// Middleware returns the middleware. +func (m *MiddlewareRegistration) Middleware() any { + return m.middleware +} + +// Kind returns the middleware kind. +func (m *MiddlewareRegistration) Kind() MiddlewareKind { + return m.kind +} + +// AsMiddleware registers a middleware into Fx. +func AsMiddleware(middleware any, kind MiddlewareKind) fx.Option { + return RegisterMiddleware(NewMiddlewareRegistration(middleware, kind)) +} + +// RegisterMiddleware registers a middleware registration into Fx. +func RegisterMiddleware(middlewareRegistration *MiddlewareRegistration) fx.Option { + var providers []any + + var middlewareDef MiddlewareDefinition + if !IsConcreteMiddleware(middlewareRegistration.Middleware()) { + providers = append( + providers, + fx.Annotate( + middlewareRegistration.Middleware(), + fx.As(new(Middleware)), + fx.ResultTags(`group:"httpserver-middlewares"`), + ), + ) + + middlewareDef = NewMiddlewareDefinition(GetReturnType(middlewareRegistration.Middleware()), middlewareRegistration.kind) + } else { + middlewareDef = NewMiddlewareDefinition(middlewareRegistration.Middleware(), middlewareRegistration.kind) + } + + return fx.Options( + fx.Provide(providers...), + fx.Supply( + fx.Annotate( + middlewareDef, + fx.As(new(MiddlewareDefinition)), + fx.ResultTags(`group:"httpserver-middleware-definitions"`), + ), + ), + ) +} + +// HandlerRegistration is a handler registration. +type HandlerRegistration struct { + method string + path string + handler any + middlewares []any +} + +// NewHandlerRegistration returns a new [HandlerRegistration]. +func NewHandlerRegistration(method string, path string, handler any, middlewares ...any) *HandlerRegistration { + return &HandlerRegistration{ + method: method, + path: path, + handler: handler, + middlewares: middlewares, + } +} + +// Method returns the handler http method. +func (h *HandlerRegistration) Method() string { + return h.method +} + +// Path returns the handler http path. +func (h *HandlerRegistration) Path() string { + return h.path +} + +// Handler returns the handler. +func (h *HandlerRegistration) Handler() any { + return h.handler +} + +// Middlewares returns the handler associated middlewares. +func (h *HandlerRegistration) Middlewares() []any { + return h.middlewares +} + +// AsHandler registers a handler into Fx. +func AsHandler(method string, path string, handler any, middlewares ...any) fx.Option { + return RegisterHandler(NewHandlerRegistration(method, path, handler, middlewares...)) +} + +// RegisterHandler registers a handler registration into Fx. +func RegisterHandler(handlerRegistration *HandlerRegistration) fx.Option { + var providers []any + + var middlewareDefs []MiddlewareDefinition + for _, middleware := range handlerRegistration.Middlewares() { + if !IsConcreteMiddleware(middleware) { + providers = append( + providers, + fx.Annotate( + middleware, + fx.As(new(Middleware)), + fx.ResultTags(`group:"httpserver-middlewares"`), + ), + ) + + middlewareDefs = append(middlewareDefs, NewMiddlewareDefinition(GetReturnType(middleware), Attached)) + } else { + middlewareDefs = append(middlewareDefs, NewMiddlewareDefinition(middleware, Attached)) + } + } + + var handlerDef HandlerDefinition + if !IsConcreteHandler(handlerRegistration.Handler()) { + providers = append( + providers, + fx.Annotate( + handlerRegistration.Handler(), + fx.As(new(Handler)), + fx.ResultTags(`group:"httpserver-handlers"`), + ), + ) + handlerDef = NewHandlerDefinition( + handlerRegistration.Method(), + handlerRegistration.Path(), + GetReturnType(handlerRegistration.Handler()), + middlewareDefs, + ) + } else { + handlerDef = NewHandlerDefinition( + handlerRegistration.Method(), + handlerRegistration.Path(), + handlerRegistration.Handler(), + middlewareDefs, + ) + } + + return fx.Options( + fx.Provide(providers...), + fx.Supply( + fx.Annotate( + handlerDef, + fx.As(new(HandlerDefinition)), + fx.ResultTags(`group:"httpserver-handler-definitions"`), + ), + ), + ) +} + +// HandlersGroupRegistration is a handlers group registration. +type HandlersGroupRegistration struct { + prefix string + handlersRegistrations []*HandlerRegistration + middlewares []any +} + +// NewHandlersGroupRegistration returns a new [HandlersGroupRegistration]. +func NewHandlersGroupRegistration(prefix string, handlersRegistrations []*HandlerRegistration, middlewares ...any) *HandlersGroupRegistration { + return &HandlersGroupRegistration{ + prefix: prefix, + handlersRegistrations: handlersRegistrations, + middlewares: middlewares, + } +} + +// Prefix returns the handlers group http path prefix. +func (h *HandlersGroupRegistration) Prefix() string { + return h.prefix +} + +// HandlersRegistrations returns the handlers group associated handlers registrations. +func (h *HandlersGroupRegistration) HandlersRegistrations() []*HandlerRegistration { + return h.handlersRegistrations +} + +// Middlewares returns the handlers group associated middlewares. +func (h *HandlersGroupRegistration) Middlewares() []any { + return h.middlewares +} + +// AsHandlersGroup registers a handlers group into Fx. +func AsHandlersGroup(prefix string, handlersRegistrations []*HandlerRegistration, middlewares ...any) fx.Option { + return RegisterHandlersGroup(NewHandlersGroupRegistration(prefix, handlersRegistrations, middlewares...)) +} + +// RegisterHandlersGroup registers a handlers group registration into Fx. +func RegisterHandlersGroup(handlersGroupRegistration *HandlersGroupRegistration) fx.Option { + var providers []any + + var groupMiddlewareDefs []MiddlewareDefinition + for _, middleware := range handlersGroupRegistration.Middlewares() { + if !IsConcreteMiddleware(middleware) { + providers = append( + providers, + fx.Annotate( + middleware, + fx.As(new(Middleware)), + fx.ResultTags(`group:"httpserver-middlewares"`), + ), + ) + + groupMiddlewareDefs = append(groupMiddlewareDefs, NewMiddlewareDefinition(GetReturnType(middleware), Attached)) + } else { + groupMiddlewareDefs = append(groupMiddlewareDefs, NewMiddlewareDefinition(middleware, Attached)) + } + } + + var groupHandlerDefs []HandlerDefinition + for _, handlerRegistration := range handlersGroupRegistration.HandlersRegistrations() { + var handlerDef HandlerDefinition + var middlewareDefs []MiddlewareDefinition + + for _, middleware := range handlerRegistration.Middlewares() { + if !IsConcreteMiddleware(middleware) { + providers = append( + providers, + fx.Annotate( + middleware, + fx.As(new(Middleware)), + fx.ResultTags(`group:"httpserver-middlewares"`), + ), + ) + + middlewareDefs = append(middlewareDefs, NewMiddlewareDefinition(GetReturnType(middleware), Attached)) + } else { + middlewareDefs = append(middlewareDefs, NewMiddlewareDefinition(middleware, Attached)) + } + } + + if !IsConcreteHandler(handlerRegistration.Handler()) { + providers = append( + providers, + fx.Annotate( + handlerRegistration.Handler(), + fx.As(new(Handler)), + fx.ResultTags(`group:"httpserver-handlers"`), + ), + ) + handlerDef = NewHandlerDefinition( + handlerRegistration.Method(), + handlerRegistration.Path(), + GetReturnType(handlerRegistration.Handler()), + middlewareDefs, + ) + } else { + handlerDef = NewHandlerDefinition( + handlerRegistration.Method(), + handlerRegistration.Path(), + handlerRegistration.Handler(), + middlewareDefs, + ) + } + + groupHandlerDefs = append(groupHandlerDefs, handlerDef) + } + + handlersGroupDef := NewHandlersGroupDefinition( + handlersGroupRegistration.Prefix(), + groupHandlerDefs, + groupMiddlewareDefs, + ) + + return fx.Options( + fx.Provide(providers...), + fx.Supply( + fx.Annotate( + handlersGroupDef, + fx.As(new(HandlersGroupDefinition)), + fx.ResultTags(`group:"httpserver-handlers-group-definitions"`), + ), + ), + ) +} diff --git a/fxhttpserver/register_test.go b/fxhttpserver/register_test.go new file mode 100644 index 0000000..dd3fe01 --- /dev/null +++ b/fxhttpserver/register_test.go @@ -0,0 +1,124 @@ +package fxhttpserver_test + +import ( + "testing" + + "github.com/ankorstore/yokai/fxhttpserver" + "github.com/stretchr/testify/assert" +) + +func TestMiddlewareRegistration(t *testing.T) { + t.Parallel() + + type exampleMiddleware struct { + name string + } + + mw := exampleMiddleware{name: "test"} + + tests := []struct { + middleware any + kind fxhttpserver.MiddlewareKind + }{ + {mw, fxhttpserver.GlobalUse}, + {mw, fxhttpserver.GlobalPre}, + {mw, fxhttpserver.Attached}, + } + + for _, tt := range tests { + tt := tt + + t.Run(tt.kind.String(), func(t *testing.T) { + t.Parallel() + + mr := fxhttpserver.NewMiddlewareRegistration(tt.middleware, tt.kind) + + assert.Equal(t, tt.middleware, mr.Middleware()) + assert.Equal(t, tt.kind, mr.Kind()) + }) + } +} + +func TestHandlerRegistration(t *testing.T) { + t.Parallel() + + type exampleHandler struct { + name string + } + type exampleMiddleware struct { + name string + } + + handler := exampleHandler{name: "handler-test"} + mw1 := exampleMiddleware{name: "middleware1"} + mw2 := exampleMiddleware{name: "middleware2"} + + tests := []struct { + method string + path string + handler any + middlewares []any + }{ + {"GET", "/path1", handler, nil}, + {"POST", "/path2", handler, []any{mw1}}, + {"PUT", "/path3", handler, []any{mw1, mw2}}, + } + + for _, tt := range tests { + tt := tt + + t.Run(tt.method+tt.path, func(t *testing.T) { + t.Parallel() + + hr := fxhttpserver.NewHandlerRegistration(tt.method, tt.path, tt.handler, tt.middlewares...) + + assert.Equal(t, tt.method, hr.Method()) + assert.Equal(t, tt.path, hr.Path()) + assert.Equal(t, tt.handler, hr.Handler()) + assert.Equal(t, tt.middlewares, hr.Middlewares()) + }) + } +} + +func TestHandlersGroupRegistration(t *testing.T) { + t.Parallel() + + type exampleHandler struct { + name string + } + type exampleMiddleware struct { + name string + } + + handler1 := exampleHandler{name: "handler-test1"} + handler2 := exampleHandler{name: "handler-test2"} + mw1 := exampleMiddleware{name: "middleware1"} + mw2 := exampleMiddleware{name: "middleware2"} + + hr1 := fxhttpserver.NewHandlerRegistration("GET", "/path1", handler1) + hr2 := fxhttpserver.NewHandlerRegistration("POST", "/path2", handler2) + + tests := []struct { + prefix string + handlersRegistrations []*fxhttpserver.HandlerRegistration + middlewares []any + }{ + {"/group/1", []*fxhttpserver.HandlerRegistration{hr1}, nil}, + {"/group/2", []*fxhttpserver.HandlerRegistration{hr1, hr2}, []any{mw1}}, + {"/group/3", []*fxhttpserver.HandlerRegistration{hr1}, []any{mw1, mw2}}, + } + + for _, tt := range tests { + tt := tt + + t.Run(tt.prefix, func(t *testing.T) { + t.Parallel() + + hgr := fxhttpserver.NewHandlersGroupRegistration(tt.prefix, tt.handlersRegistrations, tt.middlewares...) + + assert.Equal(t, tt.prefix, hgr.Prefix()) + assert.Equal(t, tt.handlersRegistrations, hgr.HandlersRegistrations()) + assert.Equal(t, tt.middlewares, hgr.Middlewares()) + }) + } +} diff --git a/fxhttpserver/registry.go b/fxhttpserver/registry.go new file mode 100644 index 0000000..f2ab004 --- /dev/null +++ b/fxhttpserver/registry.go @@ -0,0 +1,220 @@ +package fxhttpserver + +import ( + "fmt" + + "github.com/labstack/echo/v4" + "go.uber.org/fx" +) + +// Middleware is the interface for middlewares. +type Middleware interface { + Handle() echo.MiddlewareFunc +} + +// Handler is the interface for handlers. +type Handler interface { + Handle() echo.HandlerFunc +} + +// HttpServerRegistry is the registry collecting middlewares, handlers, handlers groups and their definitions. +type HttpServerRegistry struct { + middlewares []Middleware + middlewareDefinitions []MiddlewareDefinition + handlers []Handler + handlerDefinitions []HandlerDefinition + handlersGroupDefinitions []HandlersGroupDefinition +} + +// FxHttpServerRegistryParam allows injection of the required dependencies in [NewFxHttpServerRegistry]. +type FxHttpServerRegistryParam struct { + fx.In + Middlewares []Middleware `group:"httpserver-middlewares"` + MiddlewareDefinitions []MiddlewareDefinition `group:"httpserver-middleware-definitions"` + Handlers []Handler `group:"httpserver-handlers"` + HandlerDefinitions []HandlerDefinition `group:"httpserver-handler-definitions"` + HandlersGroupDefinitions []HandlersGroupDefinition `group:"httpserver-handlers-group-definitions"` +} + +// NewFxHttpServerRegistry returns as new [HttpServerRegistry]. +func NewFxHttpServerRegistry(p FxHttpServerRegistryParam) *HttpServerRegistry { + return &HttpServerRegistry{ + middlewares: p.Middlewares, + middlewareDefinitions: p.MiddlewareDefinitions, + handlers: p.Handlers, + handlerDefinitions: p.HandlerDefinitions, + handlersGroupDefinitions: p.HandlersGroupDefinitions, + } +} + +// ResolveMiddlewares resolves a list of [ResolvedMiddleware] from their definitions. +func (r *HttpServerRegistry) ResolveMiddlewares() ([]ResolvedMiddleware, error) { + var resolvedMiddlewares []ResolvedMiddleware + + for _, middlewareDef := range r.middlewareDefinitions { + if middlewareDef.Kind() != Attached { + resMiddleware, err := r.resolveMiddlewareDefinition(middlewareDef) + if err != nil { + return nil, err + } + + resolvedMiddlewares = append(resolvedMiddlewares, resMiddleware) + } + } + + return resolvedMiddlewares, nil +} + +// ResolveHandlers resolves a list of [ResolvedHandler] from their definitions. +func (r *HttpServerRegistry) ResolveHandlers() ([]ResolvedHandler, error) { + var resolvedHandlers []ResolvedHandler + + for _, handlerDef := range r.handlerDefinitions { + var handlerMiddlewares []echo.MiddlewareFunc + + for _, middlewareDef := range handlerDef.Middlewares() { + handlerMiddleware, err := r.resolveMiddlewareDefinition(middlewareDef) + if err != nil { + return nil, err + } + + handlerMiddlewares = append(handlerMiddlewares, handlerMiddleware.Middleware()) + } + + resHandler, err := r.resolveHandlerDefinition(handlerDef, handlerMiddlewares) + if err != nil { + return nil, err + } + + resolvedHandlers = append(resolvedHandlers, resHandler) + } + + return resolvedHandlers, nil +} + +// ResolveHandlersGroups resolves a list of [ResolvedHandlersGroup] from their definitions. +func (r *HttpServerRegistry) ResolveHandlersGroups() ([]ResolvedHandlersGroup, error) { + var resolvedHandlersGroups []ResolvedHandlersGroup + + for _, handlerGroupDef := range r.handlersGroupDefinitions { + var groupMiddlewares []echo.MiddlewareFunc + + for _, middlewareDef := range handlerGroupDef.Middlewares() { + groupMiddleware, err := r.resolveMiddlewareDefinition(middlewareDef) + if err != nil { + return nil, err + } + + groupMiddlewares = append(groupMiddlewares, groupMiddleware.Middleware()) + } + + var groupHandlers []ResolvedHandler + + for _, handlerDef := range handlerGroupDef.Handlers() { + var resolvedHandlerMiddlewares []echo.MiddlewareFunc + + for _, middlewareDef := range handlerDef.Middlewares() { + resolvedHandlerMiddleware, err := r.resolveMiddlewareDefinition(middlewareDef) + if err != nil { + return nil, err + } + + resolvedHandlerMiddlewares = append(resolvedHandlerMiddlewares, resolvedHandlerMiddleware.Middleware()) + } + + groupHandler, err := r.resolveHandlerDefinition(handlerDef, resolvedHandlerMiddlewares) + if err != nil { + return nil, err + } + + groupHandlers = append(groupHandlers, groupHandler) + } + + resolvedHandlersGroups = append( + resolvedHandlersGroups, + NewResolvedHandlersGroup( + handlerGroupDef.Prefix(), + groupHandlers, + groupMiddlewares..., + ), + ) + } + + return resolvedHandlersGroups, nil +} + +func (r *HttpServerRegistry) resolveMiddlewareDefinition(middlewareDefinition MiddlewareDefinition) (ResolvedMiddleware, error) { + if middlewareDefinition.Concrete() { + if castMiddleware, ok := middlewareDefinition.Middleware().(func(echo.HandlerFunc) echo.HandlerFunc); ok { + return NewResolvedMiddleware(castMiddleware, middlewareDefinition.Kind()), nil + } else if castMiddleware, ok = middlewareDefinition.Middleware().(echo.MiddlewareFunc); ok { + return NewResolvedMiddleware(castMiddleware, middlewareDefinition.Kind()), nil + } else { + return nil, fmt.Errorf("cannot cast middleware definition as MiddlewareFunc") + } + } + + registeredMiddleware, err := r.lookupRegisteredMiddleware(middlewareDefinition.Middleware().(string)) + if err != nil { + return nil, fmt.Errorf("cannot lookup registered middleware") + } + + return NewResolvedMiddleware( + registeredMiddleware.Handle(), + middlewareDefinition.Kind(), + ), nil +} + +func (r *HttpServerRegistry) resolveHandlerDefinition(handlerDefinition HandlerDefinition, handlerMiddlewares []echo.MiddlewareFunc) (ResolvedHandler, error) { + if handlerDefinition.Concrete() { + if castHandler, ok := handlerDefinition.Handler().(func(echo.Context) error); ok { + return NewResolvedHandler( + handlerDefinition.Method(), + handlerDefinition.Path(), + castHandler, + handlerMiddlewares..., + ), nil + } else if castHandler, ok = handlerDefinition.Handler().(echo.HandlerFunc); ok { + return NewResolvedHandler( + handlerDefinition.Method(), + handlerDefinition.Path(), + castHandler, + handlerMiddlewares..., + ), nil + } else { + return nil, fmt.Errorf("cannot cast handler definition as HandlerFunc") + } + } + + registeredHandler, err := r.lookupRegisteredHandler(handlerDefinition.Handler().(string)) + if err != nil { + return nil, fmt.Errorf("cannot lookup registered handler") + } + + return NewResolvedHandler( + handlerDefinition.Method(), + handlerDefinition.Path(), + registeredHandler.Handle(), + handlerMiddlewares..., + ), nil +} + +func (r *HttpServerRegistry) lookupRegisteredMiddleware(middleware string) (Middleware, error) { + for _, m := range r.middlewares { + if GetType(m) == middleware { + return m, nil + } + } + + return nil, fmt.Errorf("cannot find middleware for type %s", middleware) +} + +func (r *HttpServerRegistry) lookupRegisteredHandler(handler string) (Handler, error) { + for _, h := range r.handlers { + if GetType(h) == handler { + return h, nil + } + } + + return nil, fmt.Errorf("cannot find handler for type %s", handler) +} diff --git a/fxhttpserver/registry_test.go b/fxhttpserver/registry_test.go new file mode 100644 index 0000000..adbde22 --- /dev/null +++ b/fxhttpserver/registry_test.go @@ -0,0 +1,470 @@ +package fxhttpserver_test + +import ( + "testing" + + "github.com/ankorstore/yokai/fxhttpserver" + "github.com/labstack/echo/v4" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" +) + +var testMiddleware = func(next echo.HandlerFunc) echo.HandlerFunc { + return next +} + +var testHandler = func(c echo.Context) error { + return nil +} + +type testMiddlewareDefinitionMock struct { + mock.Mock +} + +func (m *testMiddlewareDefinitionMock) Concrete() bool { + args := m.Called() + + return args.Bool(0) +} + +func (m *testMiddlewareDefinitionMock) Middleware() any { + args := m.Called() + + return args.Get(0) +} + +func (m *testMiddlewareDefinitionMock) Kind() fxhttpserver.MiddlewareKind { + args := m.Called() + + //nolint:forcetypeassert + return args.Get(0).(fxhttpserver.MiddlewareKind) +} + +type testHandlerDefinitionMock struct { + mock.Mock +} + +func (m *testHandlerDefinitionMock) Concrete() bool { + args := m.Called() + + return args.Bool(0) +} + +func (m *testHandlerDefinitionMock) Method() string { + args := m.Called() + + return args.String(0) +} + +func (m *testHandlerDefinitionMock) Path() string { + args := m.Called() + + return args.String(0) +} + +func (m *testHandlerDefinitionMock) Handler() any { + args := m.Called() + + return args.Get(0) +} + +func (m *testHandlerDefinitionMock) Middlewares() []fxhttpserver.MiddlewareDefinition { + args := m.Called() + + //nolint:forcetypeassert + return args.Get(0).([]fxhttpserver.MiddlewareDefinition) +} + +type testMiddlewareImplementation struct{} + +func (m testMiddlewareImplementation) Handle() echo.MiddlewareFunc { + return testMiddleware +} + +type testHandlerImplementation struct{} + +func (h testHandlerImplementation) Handle() echo.HandlerFunc { + return testHandler +} + +func TestNewFxHttpServerRegistry(t *testing.T) { + t.Parallel() + + param := fxhttpserver.FxHttpServerRegistryParam{ + Middlewares: []fxhttpserver.Middleware{testMiddlewareImplementation{}}, + Handlers: []fxhttpserver.Handler{testHandlerImplementation{}}, + } + registry := fxhttpserver.NewFxHttpServerRegistry(param) + + assert.IsType(t, &fxhttpserver.HttpServerRegistry{}, registry) +} + +func TestResolveMiddlewaresSuccessWithAnonFuncType(t *testing.T) { + t.Parallel() + + param := fxhttpserver.FxHttpServerRegistryParam{ + Middlewares: []fxhttpserver.Middleware{ + testMiddlewareImplementation{}, + }, + MiddlewareDefinitions: []fxhttpserver.MiddlewareDefinition{ + fxhttpserver.NewMiddlewareDefinition(testMiddleware, fxhttpserver.GlobalUse), + }, + } + registry := fxhttpserver.NewFxHttpServerRegistry(param) + + resolvedMiddlewares, err := registry.ResolveMiddlewares() + assert.NoError(t, err) + + assert.Len(t, resolvedMiddlewares, 1) + assert.Equal(t, fxhttpserver.GlobalUse, resolvedMiddlewares[0].Kind()) + assert.Equal(t, testMiddleware(testHandler)(nil), resolvedMiddlewares[0].Middleware()(testHandler)(nil)) +} + +func TestResolveMiddlewaresFailureOnInvalidImplementation(t *testing.T) { + t.Parallel() + + middlewareDefinitionMock := new(testMiddlewareDefinitionMock) + middlewareDefinitionMock.On("Concrete").Return(true) + middlewareDefinitionMock.On("Middleware").Return(nil) + middlewareDefinitionMock.On("Kind").Return(fxhttpserver.GlobalUse) + + param := fxhttpserver.FxHttpServerRegistryParam{ + Middlewares: []fxhttpserver.Middleware{ + testMiddlewareImplementation{}, + }, + MiddlewareDefinitions: []fxhttpserver.MiddlewareDefinition{ + middlewareDefinitionMock, + }, + } + registry := fxhttpserver.NewFxHttpServerRegistry(param) + + _, err := registry.ResolveMiddlewares() + assert.Error(t, err) + assert.Equal(t, "cannot cast middleware definition as MiddlewareFunc", err.Error()) +} + +func TestResolveMiddlewaresFailureOnMissingImplementation(t *testing.T) { + t.Parallel() + + middlewareDefinitionMock := new(testMiddlewareDefinitionMock) + middlewareDefinitionMock.On("Concrete").Return(false) + middlewareDefinitionMock.On("Middleware").Return("invalid") + middlewareDefinitionMock.On("Kind").Return(fxhttpserver.GlobalUse) + + param := fxhttpserver.FxHttpServerRegistryParam{ + Middlewares: []fxhttpserver.Middleware{}, + MiddlewareDefinitions: []fxhttpserver.MiddlewareDefinition{ + middlewareDefinitionMock, + }, + } + registry := fxhttpserver.NewFxHttpServerRegistry(param) + + _, err := registry.ResolveMiddlewares() + assert.Error(t, err) + assert.Equal(t, "cannot lookup registered middleware", err.Error()) +} + +func TestResolveHandlersSuccessWithAnonFuncType(t *testing.T) { + t.Parallel() + + param := fxhttpserver.FxHttpServerRegistryParam{ + Handlers: []fxhttpserver.Handler{ + testHandlerImplementation{}, + }, + HandlerDefinitions: []fxhttpserver.HandlerDefinition{ + fxhttpserver.NewHandlerDefinition( + "GET", + "/path", + testHandler, + []fxhttpserver.MiddlewareDefinition{ + fxhttpserver.NewMiddlewareDefinition(testMiddleware, fxhttpserver.GlobalUse), + }, + ), + }, + } + registry := fxhttpserver.NewFxHttpServerRegistry(param) + + resolvedHandlers, err := registry.ResolveHandlers() + assert.NoError(t, err) + + assert.Len(t, resolvedHandlers, 1) + assert.Equal(t, "GET", resolvedHandlers[0].Method()) + assert.Equal(t, "/path", resolvedHandlers[0].Path()) + assert.Equal(t, testHandler(nil), resolvedHandlers[0].Handler()(nil)) +} + +func TestResolveHandlersSuccessWithHandlerFuncType(t *testing.T) { + t.Parallel() + + var h echo.HandlerFunc + + handlerDefinitionMock := new(testHandlerDefinitionMock) + handlerDefinitionMock.On("Concrete").Return(true) + handlerDefinitionMock.On("Method").Return("GET") + handlerDefinitionMock.On("Path").Return("/path") + handlerDefinitionMock.On("Handler").Return(h) + handlerDefinitionMock.On("Middlewares").Return([]fxhttpserver.MiddlewareDefinition{}) + + param := fxhttpserver.FxHttpServerRegistryParam{ + Handlers: []fxhttpserver.Handler{ + testHandlerImplementation{}, + }, + HandlerDefinitions: []fxhttpserver.HandlerDefinition{ + handlerDefinitionMock, + }, + } + registry := fxhttpserver.NewFxHttpServerRegistry(param) + + resolvedHandlers, err := registry.ResolveHandlers() + assert.NoError(t, err) + + assert.Len(t, resolvedHandlers, 1) + assert.Equal(t, "GET", resolvedHandlers[0].Method()) + assert.Equal(t, "/path", resolvedHandlers[0].Path()) +} + +func TestResolveHandlersFailureOnMissingHandlerImplementation(t *testing.T) { + t.Parallel() + + handlerDefinitionMock := new(testHandlerDefinitionMock) + handlerDefinitionMock.On("Concrete").Return(false) + handlerDefinitionMock.On("Method").Return("GET") + handlerDefinitionMock.On("Path").Return("/path") + handlerDefinitionMock.On("Handler").Return("invalid") + handlerDefinitionMock.On("Middlewares").Return([]fxhttpserver.MiddlewareDefinition{}) + + param := fxhttpserver.FxHttpServerRegistryParam{ + Handlers: []fxhttpserver.Handler{}, + HandlerDefinitions: []fxhttpserver.HandlerDefinition{ + handlerDefinitionMock, + }, + } + registry := fxhttpserver.NewFxHttpServerRegistry(param) + + _, err := registry.ResolveHandlers() + assert.Error(t, err) + assert.Equal(t, "cannot lookup registered handler", err.Error()) +} + +func TestResolveHandlersFailureOnInvalidHandlerImplementation(t *testing.T) { + t.Parallel() + + handlerDefinitionMock := new(testHandlerDefinitionMock) + handlerDefinitionMock.On("Concrete").Return(true) + handlerDefinitionMock.On("Method").Return("GET") + handlerDefinitionMock.On("Path").Return("/path") + handlerDefinitionMock.On("Handler").Return("invalid") + handlerDefinitionMock.On("Middlewares").Return([]fxhttpserver.MiddlewareDefinition{}) + + param := fxhttpserver.FxHttpServerRegistryParam{ + Handlers: []fxhttpserver.Handler{ + testHandlerImplementation{}, + }, + HandlerDefinitions: []fxhttpserver.HandlerDefinition{ + handlerDefinitionMock, + }, + } + registry := fxhttpserver.NewFxHttpServerRegistry(param) + + _, err := registry.ResolveHandlers() + assert.Error(t, err) + assert.Equal(t, "cannot cast handler definition as HandlerFunc", err.Error()) +} + +func TestResolveHandlersFailureOnInvalidMiddlewareImplementation(t *testing.T) { + t.Parallel() + + middlewareDefinitionMock := new(testMiddlewareDefinitionMock) + middlewareDefinitionMock.On("Concrete").Return(true) + middlewareDefinitionMock.On("Middleware").Return("invalid") + middlewareDefinitionMock.On("Kind").Return(fxhttpserver.GlobalUse) + + handlerDefinitionMock := new(testHandlerDefinitionMock) + handlerDefinitionMock.On("Concrete").Return(true) + handlerDefinitionMock.On("Method").Return("GET") + handlerDefinitionMock.On("Path").Return("/path") + handlerDefinitionMock.On("Handler").Return("invalid") + handlerDefinitionMock.On("Middlewares").Return([]fxhttpserver.MiddlewareDefinition{middlewareDefinitionMock}) + + param := fxhttpserver.FxHttpServerRegistryParam{ + Handlers: []fxhttpserver.Handler{ + testHandlerImplementation{}, + }, + HandlerDefinitions: []fxhttpserver.HandlerDefinition{ + handlerDefinitionMock, + }, + } + registry := fxhttpserver.NewFxHttpServerRegistry(param) + + _, err := registry.ResolveHandlers() + assert.Error(t, err) + assert.Equal(t, "cannot cast middleware definition as MiddlewareFunc", err.Error()) +} + +func TestResolveHandlersFailureOnMissingMiddlewareImplementation(t *testing.T) { + t.Parallel() + + middlewareDefinitionMock := new(testMiddlewareDefinitionMock) + middlewareDefinitionMock.On("Concrete").Return(false) + middlewareDefinitionMock.On("Middleware").Return("invalid") + middlewareDefinitionMock.On("Kind").Return(fxhttpserver.GlobalUse) + + handlerDefinitionMock := new(testHandlerDefinitionMock) + handlerDefinitionMock.On("Concrete").Return(true) + handlerDefinitionMock.On("Method").Return("GET") + handlerDefinitionMock.On("Path").Return("/path") + handlerDefinitionMock.On("Handler").Return("invalid") + handlerDefinitionMock.On("Middlewares").Return([]fxhttpserver.MiddlewareDefinition{middlewareDefinitionMock}) + + param := fxhttpserver.FxHttpServerRegistryParam{ + Handlers: []fxhttpserver.Handler{ + testHandlerImplementation{}, + }, + HandlerDefinitions: []fxhttpserver.HandlerDefinition{ + handlerDefinitionMock, + }, + } + registry := fxhttpserver.NewFxHttpServerRegistry(param) + + _, err := registry.ResolveHandlers() + assert.Error(t, err) + assert.Equal(t, "cannot lookup registered middleware", err.Error()) +} + +func TestResolveHandlersGroupsSuccess(t *testing.T) { + t.Parallel() + + param := fxhttpserver.FxHttpServerRegistryParam{ + Handlers: []fxhttpserver.Handler{ + testHandlerImplementation{}, + }, + HandlersGroupDefinitions: []fxhttpserver.HandlersGroupDefinition{ + fxhttpserver.NewHandlersGroupDefinition( + "/group", + []fxhttpserver.HandlerDefinition{ + fxhttpserver.NewHandlerDefinition( + "GET", + "/path", + testHandler, + []fxhttpserver.MiddlewareDefinition{ + fxhttpserver.NewMiddlewareDefinition(testMiddleware, fxhttpserver.GlobalUse), + }, + ), + }, + []fxhttpserver.MiddlewareDefinition{}, + ), + }, + } + registry := fxhttpserver.NewFxHttpServerRegistry(param) + + resolvedGroups, err := registry.ResolveHandlersGroups() + assert.NoError(t, err) + + assert.Len(t, resolvedGroups, 1) + assert.Equal(t, "/group", resolvedGroups[0].Prefix()) +} + +func TestResolveHandlersGroupFailureOnMissingGroupMiddlewareImplementation(t *testing.T) { + t.Parallel() + + middlewareDefinitionMock := new(testMiddlewareDefinitionMock) + middlewareDefinitionMock.On("Concrete").Return(false) + middlewareDefinitionMock.On("Middleware").Return("invalid") + middlewareDefinitionMock.On("Kind").Return(fxhttpserver.GlobalUse) + + handlerDefinitionMock := new(testHandlerDefinitionMock) + handlerDefinitionMock.On("Concrete").Return(true) + handlerDefinitionMock.On("Method").Return("GET") + handlerDefinitionMock.On("Path").Return("/path") + handlerDefinitionMock.On("Handler").Return("invalid") + handlerDefinitionMock.On("Middlewares").Return([]fxhttpserver.MiddlewareDefinition{middlewareDefinitionMock}) + + param := fxhttpserver.FxHttpServerRegistryParam{ + Handlers: []fxhttpserver.Handler{ + testHandlerImplementation{}, + }, + HandlersGroupDefinitions: []fxhttpserver.HandlersGroupDefinition{ + fxhttpserver.NewHandlersGroupDefinition( + "/group", + []fxhttpserver.HandlerDefinition{ + handlerDefinitionMock, + }, + []fxhttpserver.MiddlewareDefinition{ + middlewareDefinitionMock, + }, + ), + }, + } + registry := fxhttpserver.NewFxHttpServerRegistry(param) + + _, err := registry.ResolveHandlersGroups() + assert.Error(t, err) + assert.Equal(t, "cannot lookup registered middleware", err.Error()) +} + +func TestResolveHandlersGroupFailureOnInvalidHandlerImplementation(t *testing.T) { + t.Parallel() + + handlerDefinitionMock := new(testHandlerDefinitionMock) + handlerDefinitionMock.On("Concrete").Return(true) + handlerDefinitionMock.On("Method").Return("GET") + handlerDefinitionMock.On("Path").Return("/path") + handlerDefinitionMock.On("Handler").Return("invalid") + handlerDefinitionMock.On("Middlewares").Return([]fxhttpserver.MiddlewareDefinition{}) + + param := fxhttpserver.FxHttpServerRegistryParam{ + Handlers: []fxhttpserver.Handler{ + testHandlerImplementation{}, + }, + HandlersGroupDefinitions: []fxhttpserver.HandlersGroupDefinition{ + fxhttpserver.NewHandlersGroupDefinition( + "/group", + []fxhttpserver.HandlerDefinition{ + handlerDefinitionMock, + }, + []fxhttpserver.MiddlewareDefinition{}, + ), + }, + } + registry := fxhttpserver.NewFxHttpServerRegistry(param) + + _, err := registry.ResolveHandlersGroups() + assert.Error(t, err) + assert.Equal(t, "cannot cast handler definition as HandlerFunc", err.Error()) +} + +func TestResolveHandlersGroupFailureOnInvalidHandlerMiddlewareImplementation(t *testing.T) { + t.Parallel() + + middlewareDefinitionMock := new(testMiddlewareDefinitionMock) + middlewareDefinitionMock.On("Concrete").Return(true) + middlewareDefinitionMock.On("Middleware").Return("invalid") + middlewareDefinitionMock.On("Kind").Return(fxhttpserver.GlobalUse) + + handlerDefinitionMock := new(testHandlerDefinitionMock) + handlerDefinitionMock.On("Concrete").Return(true) + handlerDefinitionMock.On("Method").Return("GET") + handlerDefinitionMock.On("Path").Return("/path") + handlerDefinitionMock.On("Handler").Return("invalid") + handlerDefinitionMock.On("Middlewares").Return([]fxhttpserver.MiddlewareDefinition{middlewareDefinitionMock}) + + param := fxhttpserver.FxHttpServerRegistryParam{ + Handlers: []fxhttpserver.Handler{ + testHandlerImplementation{}, + }, + HandlersGroupDefinitions: []fxhttpserver.HandlersGroupDefinition{ + fxhttpserver.NewHandlersGroupDefinition( + "/group", + []fxhttpserver.HandlerDefinition{ + handlerDefinitionMock, + }, + []fxhttpserver.MiddlewareDefinition{}, + ), + }, + } + registry := fxhttpserver.NewFxHttpServerRegistry(param) + + _, err := registry.ResolveHandlersGroups() + assert.Error(t, err) + assert.Equal(t, "cannot cast middleware definition as MiddlewareFunc", err.Error()) +} diff --git a/fxhttpserver/resolve.go b/fxhttpserver/resolve.go new file mode 100644 index 0000000..aac415e --- /dev/null +++ b/fxhttpserver/resolve.go @@ -0,0 +1,114 @@ +package fxhttpserver + +import "github.com/labstack/echo/v4" + +// ResolvedMiddleware is an interface for the resolved middlewares. +type ResolvedMiddleware interface { + Middleware() echo.MiddlewareFunc + Kind() MiddlewareKind +} + +type resolvedMiddleware struct { + middleware echo.MiddlewareFunc + kind MiddlewareKind +} + +// NewResolvedMiddleware returns a new [ResolvedMiddleware]. +func NewResolvedMiddleware(middleware echo.MiddlewareFunc, kind MiddlewareKind) ResolvedMiddleware { + return &resolvedMiddleware{ + middleware: middleware, + kind: kind, + } +} + +// Middleware return the resolved middleware as [echo.MiddlewareFunc]. +func (r *resolvedMiddleware) Middleware() echo.MiddlewareFunc { + return r.middleware +} + +// Kind return the resolved middleware kind. +func (r *resolvedMiddleware) Kind() MiddlewareKind { + return r.kind +} + +// ResolvedHandler is an interface for the resolved handlers. +type ResolvedHandler interface { + Method() string + Path() string + Handler() echo.HandlerFunc + Middlewares() []echo.MiddlewareFunc +} + +type resolvedHandler struct { + method string + path string + handler echo.HandlerFunc + middlewares []echo.MiddlewareFunc +} + +// NewResolvedHandler returns a new [ResolvedHandler]. +func NewResolvedHandler(method string, path string, handler echo.HandlerFunc, middlewares ...echo.MiddlewareFunc) ResolvedHandler { + return &resolvedHandler{ + method: method, + path: path, + handler: handler, + middlewares: middlewares, + } +} + +// Method return the resolved handler http method. +func (r *resolvedHandler) Method() string { + return r.method +} + +// Path return the resolved handler http path. +func (r *resolvedHandler) Path() string { + return r.path +} + +// Handler return the resolved handler as [echo.HandlerFunc]. +func (r *resolvedHandler) Handler() echo.HandlerFunc { + return r.handler +} + +// Middlewares return the resolved handler associated middlewares as a list of [echo.MiddlewareFunc]. +func (r *resolvedHandler) Middlewares() []echo.MiddlewareFunc { + return r.middlewares +} + +// ResolvedHandlersGroup is an interface for the resolved handlers groups. +type ResolvedHandlersGroup interface { + Prefix() string + Handlers() []ResolvedHandler + Middlewares() []echo.MiddlewareFunc +} + +type resolvedHandlersGroup struct { + prefix string + handlers []ResolvedHandler + middlewares []echo.MiddlewareFunc +} + +// NewResolvedHandlersGroup returns a new [ResolvedHandlersGroup]. +func NewResolvedHandlersGroup(prefix string, handlers []ResolvedHandler, middlewares ...echo.MiddlewareFunc) ResolvedHandlersGroup { + return &resolvedHandlersGroup{ + prefix: prefix, + handlers: handlers, + middlewares: middlewares, + } +} + +// Prefix return the resolved handlers group http path prefix. +func (r *resolvedHandlersGroup) Prefix() string { + return r.prefix +} + +// Handlers return the resolved handlers group associated handlers as a list of [ResolvedHandler]. +func (r *resolvedHandlersGroup) Handlers() []ResolvedHandler { + return r.handlers +} + +// Middlewares return the resolved handlers group associated middlewares as a list of [echo.MiddlewareFunc]. +func (r *resolvedHandlersGroup) Middlewares() []echo.MiddlewareFunc { + return r.middlewares +} diff --git a/fxhttpserver/resolve_test.go b/fxhttpserver/resolve_test.go new file mode 100644 index 0000000..434fa0e --- /dev/null +++ b/fxhttpserver/resolve_test.go @@ -0,0 +1,107 @@ +package fxhttpserver_test + +import ( + "fmt" + "testing" + + "github.com/ankorstore/yokai/fxhttpserver" + "github.com/labstack/echo/v4" + "github.com/stretchr/testify/assert" +) + +var testMiddlewareFunc = func(next echo.HandlerFunc) echo.HandlerFunc { + return func(c echo.Context) error { + return next(c) + } +} + +var testHandlerFunc = func(next echo.Context) error { + return fmt.Errorf("custom error") +} + +func TestResolvedMiddleware(t *testing.T) { + t.Parallel() + + tests := []struct { + middleware echo.MiddlewareFunc + kind fxhttpserver.MiddlewareKind + }{ + {testMiddlewareFunc, fxhttpserver.GlobalUse}, + {testMiddlewareFunc, fxhttpserver.GlobalPre}, + {testMiddlewareFunc, fxhttpserver.Attached}, + } + + for _, tt := range tests { + tt := tt + + t.Run(tt.kind.String(), func(t *testing.T) { + t.Parallel() + + rm := fxhttpserver.NewResolvedMiddleware(tt.middleware, tt.kind) + + assert.Equal(t, "custom error", rm.Middleware()(testHandlerFunc)(nil).Error()) + assert.Equal(t, tt.kind, rm.Kind()) + }) + } +} + +func TestResolvedHandler(t *testing.T) { + t.Parallel() + + tests := []struct { + method string + path string + handler echo.HandlerFunc + middlewares []echo.MiddlewareFunc + }{ + {"GET", "/path1", testHandlerFunc, nil}, + {"POST", "/path2", testHandlerFunc, []echo.MiddlewareFunc{testMiddlewareFunc}}, + {"PUT", "/path3", testHandlerFunc, []echo.MiddlewareFunc{testMiddlewareFunc, testMiddlewareFunc}}, + } + + for _, tt := range tests { + tt := tt + + t.Run(tt.method+tt.path, func(t *testing.T) { + t.Parallel() + + rh := fxhttpserver.NewResolvedHandler(tt.method, tt.path, tt.handler, tt.middlewares...) + + assert.Equal(t, tt.method, rh.Method()) + assert.Equal(t, tt.path, rh.Path()) + assert.Equal(t, "custom error", rh.Handler()(nil).Error()) + assert.Equal(t, tt.middlewares, rh.Middlewares()) + }) + } +} + +func TestResolvedHandlersGroup(t *testing.T) { + t.Parallel() + + rh1 := fxhttpserver.NewResolvedHandler("GET", "/path1", testHandlerFunc) + rh2 := fxhttpserver.NewResolvedHandler("POST", "/path2", testHandlerFunc, testMiddlewareFunc) + + tests := []struct { + prefix string + handlers []fxhttpserver.ResolvedHandler + middlewares []echo.MiddlewareFunc + }{ + {"/group/1", []fxhttpserver.ResolvedHandler{rh1}, nil}, + {"/group/2", []fxhttpserver.ResolvedHandler{rh1, rh2}, []echo.MiddlewareFunc{testMiddlewareFunc}}, + {"/group/3", []fxhttpserver.ResolvedHandler{rh1}, []echo.MiddlewareFunc{testMiddlewareFunc, testMiddlewareFunc}}, + } + + for _, tt := range tests { + tt := tt + + t.Run(tt.prefix, func(t *testing.T) { + t.Parallel() + + rg := fxhttpserver.NewResolvedHandlersGroup(tt.prefix, tt.handlers, tt.middlewares...) + + assert.Equal(t, tt.prefix, rg.Prefix()) + assert.Equal(t, tt.handlers, rg.Handlers()) + assert.Equal(t, tt.middlewares, rg.Middlewares()) + }) + } +} diff --git a/fxhttpserver/testdata/config/config.yaml b/fxhttpserver/testdata/config/config.yaml new file mode 100644 index 0000000..5812d8c --- /dev/null +++ b/fxhttpserver/testdata/config/config.yaml @@ -0,0 +1,38 @@ +app: + name: test + version: 0.1.0 +modules: + log: + level: info + output: test + trace: + processor: + type: test + http: + server: + errors: + obfuscate: false + stack: false + log: + headers: + x-foo: foo + x-bar: bar + exclude: + - /foo/bar + - /foo/baz + level_from_response: true + trace: + enabled: true + exclude: + - /foo/bar + - /foo/baz + metrics: + collect: + enabled: true + namespace: foo + subsystem: bar + buckets: 0.1, 1, 10 + normalize: true + templates: + enabled: ${TEMPLATES_ENABLED} + path: ${TEMPLATES_PATH} diff --git a/fxhttpserver/testdata/factory/factory.go b/fxhttpserver/testdata/factory/factory.go new file mode 100644 index 0000000..4be08cf --- /dev/null +++ b/fxhttpserver/testdata/factory/factory.go @@ -0,0 +1,19 @@ +package factory + +import ( + "github.com/ankorstore/yokai/httpserver" + "github.com/labstack/echo/v4" +) + +type TestHttpServerFactory struct{} + +func NewTestHttpServerFactory() httpserver.HttpServerFactory { + return &TestHttpServerFactory{} +} + +func (f *TestHttpServerFactory) Create(options ...httpserver.HttpServerOption) (*echo.Echo, error) { + e := echo.New() + e.HideBanner = false + + return e, nil +} diff --git a/fxhttpserver/testdata/handler/bar.go b/fxhttpserver/testdata/handler/bar.go new file mode 100644 index 0000000..1d869ba --- /dev/null +++ b/fxhttpserver/testdata/handler/bar.go @@ -0,0 +1,32 @@ +package handler + +import ( + "fmt" + "github.com/labstack/echo/v4" + "net/http" + + "github.com/ankorstore/yokai/fxhttpserver/testdata/service" + "github.com/ankorstore/yokai/httpserver" + "github.com/ankorstore/yokai/log" +) + +type TestBarHandler struct { + service *service.TestService +} + +func NewTestBarHandler(service *service.TestService) *TestBarHandler { + return &TestBarHandler{ + service: service, + } +} + +func (h *TestBarHandler) Handle() echo.HandlerFunc { + return func(c echo.Context) error { + ctx, span := httpserver.CtxTracer(c).Start(c.Request().Context(), "bar span") + defer span.End() + + log.CtxLogger(ctx).Info().Msg("in bar handler") + + return c.String(http.StatusOK, fmt.Sprintf("bar: %s", h.service.GetAppName())) + } +} diff --git a/fxhttpserver/testdata/handler/baz.go b/fxhttpserver/testdata/handler/baz.go new file mode 100644 index 0000000..0c1ee4d --- /dev/null +++ b/fxhttpserver/testdata/handler/baz.go @@ -0,0 +1,32 @@ +package handler + +import ( + "fmt" + "github.com/labstack/echo/v4" + "net/http" + + "github.com/ankorstore/yokai/fxhttpserver/testdata/service" + "github.com/ankorstore/yokai/httpserver" + "github.com/ankorstore/yokai/log" +) + +type TestBazHandler struct { + service *service.TestService +} + +func NewTestBazHandler(service *service.TestService) *TestBazHandler { + return &TestBazHandler{ + service: service, + } +} + +func (h *TestBazHandler) Handle() echo.HandlerFunc { + return func(c echo.Context) error { + ctx, span := httpserver.CtxTracer(c).Start(c.Request().Context(), "baz span") + defer span.End() + + log.CtxLogger(ctx).Info().Msg("in baz handler") + + return c.String(http.StatusOK, fmt.Sprintf("baz: %s", h.service.GetAppName())) + } +} diff --git a/fxhttpserver/testdata/handler/metrics.go b/fxhttpserver/testdata/handler/metrics.go new file mode 100644 index 0000000..71848fc --- /dev/null +++ b/fxhttpserver/testdata/handler/metrics.go @@ -0,0 +1,41 @@ +package handler + +import ( + "fmt" + "github.com/labstack/echo/v4" + "net/http" + "time" + + "github.com/ankorstore/yokai/fxhttpserver/testdata/service" + "github.com/prometheus/client_golang/prometheus" + "github.com/prometheus/client_golang/prometheus/promauto" +) + +var testMetricsHandlerHistogram = promauto.NewHistogram(prometheus.HistogramOpts{ + Name: "test_metrics_handler_duration_seconds", + Help: "The duration of the TestMetricsHandler", + Buckets: prometheus.ExponentialBuckets(0.001, 2, 10), +}) + +type TestMetricsHandler struct { + service *service.TestService +} + +func NewTestMetricsHandler(service *service.TestService) *TestMetricsHandler { + return &TestMetricsHandler{ + service: service, + } +} + +func (h *TestMetricsHandler) Handle() echo.HandlerFunc { + return func(c echo.Context) error { + c.Logger().Info("in metrics handler") + + start := time.Now() + defer func() { + testMetricsHandlerHistogram.Observe(time.Since(start).Seconds()) + }() + + return c.String(http.StatusOK, fmt.Sprintf("name: %s", h.service.GetAppName())) + } +} diff --git a/fxhttpserver/testdata/handler/template.go b/fxhttpserver/testdata/handler/template.go new file mode 100644 index 0000000..4cb0b72 --- /dev/null +++ b/fxhttpserver/testdata/handler/template.go @@ -0,0 +1,28 @@ +package handler + +import ( + "github.com/labstack/echo/v4" + "net/http" + + "github.com/ankorstore/yokai/fxhttpserver/testdata/service" +) + +type TestTemplateHandler struct { + service *service.TestService +} + +func NewTestTemplateHandler(service *service.TestService) *TestTemplateHandler { + return &TestTemplateHandler{ + service: service, + } +} + +func (h *TestTemplateHandler) Handle() echo.HandlerFunc { + return func(c echo.Context) error { + c.Logger().Info("in template handler") + + return c.Render(http.StatusOK, "test.html", map[string]interface{}{ + "name": h.service.GetAppName(), + }) + } +} diff --git a/fxhttpserver/testdata/middleware/global.go b/fxhttpserver/testdata/middleware/global.go new file mode 100644 index 0000000..5fe5621 --- /dev/null +++ b/fxhttpserver/testdata/middleware/global.go @@ -0,0 +1,29 @@ +package middleware + +import ( + "github.com/ankorstore/yokai/config" + "github.com/ankorstore/yokai/log" + "github.com/labstack/echo/v4" +) + +type TestGlobalMiddleware struct { + config *config.Config +} + +func NewTestGlobalMiddleware(config *config.Config) *TestGlobalMiddleware { + return &TestGlobalMiddleware{ + config: config, + } +} + +func (m *TestGlobalMiddleware) Handle() echo.MiddlewareFunc { + return func(next echo.HandlerFunc) echo.HandlerFunc { + return func(c echo.Context) error { + log.CtxLogger(c.Request().Context()).Info().Msgf("GLOBAL middleware for app: %s", m.config.AppName()) + + c.Response().Header().Add("global-middleware", "true") + + return next(c) + } + } +} diff --git a/fxhttpserver/testdata/middleware/group.go b/fxhttpserver/testdata/middleware/group.go new file mode 100644 index 0000000..d1a7a38 --- /dev/null +++ b/fxhttpserver/testdata/middleware/group.go @@ -0,0 +1,29 @@ +package middleware + +import ( + "github.com/ankorstore/yokai/config" + "github.com/ankorstore/yokai/log" + "github.com/labstack/echo/v4" +) + +type TestGroupMiddleware struct { + config *config.Config +} + +func NewTestGroupMiddleware(config *config.Config) *TestGroupMiddleware { + return &TestGroupMiddleware{ + config: config, + } +} + +func (m *TestGroupMiddleware) Handle() echo.MiddlewareFunc { + return func(next echo.HandlerFunc) echo.HandlerFunc { + return func(c echo.Context) error { + log.CtxLogger(c.Request().Context()).Info().Msgf("GROUP middleware for app: %s", m.config.AppName()) + + c.Response().Header().Add("group-middleware", "true") + + return next(c) + } + } +} diff --git a/fxhttpserver/testdata/middleware/handler.go b/fxhttpserver/testdata/middleware/handler.go new file mode 100644 index 0000000..3576606 --- /dev/null +++ b/fxhttpserver/testdata/middleware/handler.go @@ -0,0 +1,29 @@ +package middleware + +import ( + "github.com/ankorstore/yokai/config" + "github.com/ankorstore/yokai/log" + "github.com/labstack/echo/v4" +) + +type TestHandlerMiddleware struct { + config *config.Config +} + +func NewTestHandlerMiddleware(config *config.Config) *TestHandlerMiddleware { + return &TestHandlerMiddleware{ + config: config, + } +} + +func (m *TestHandlerMiddleware) Handle() echo.MiddlewareFunc { + return func(next echo.HandlerFunc) echo.HandlerFunc { + return func(c echo.Context) error { + log.CtxLogger(c.Request().Context()).Info().Msgf("HANDLER middleware for app: %s", m.config.AppName()) + + c.Response().Header().Add("handler-middleware", "true") + + return next(c) + } + } +} diff --git a/fxhttpserver/testdata/service/service.go b/fxhttpserver/testdata/service/service.go new file mode 100644 index 0000000..1b8cb98 --- /dev/null +++ b/fxhttpserver/testdata/service/service.go @@ -0,0 +1,19 @@ +package service + +import ( + "github.com/ankorstore/yokai/config" +) + +type TestService struct { + config *config.Config +} + +func NewTestService(config *config.Config) *TestService { + return &TestService{ + config: config, + } +} + +func (s *TestService) GetAppName() string { + return s.config.AppName() +} diff --git a/fxhttpserver/testdata/templates/test.html b/fxhttpserver/testdata/templates/test.html new file mode 100644 index 0000000..0ee9aa3 --- /dev/null +++ b/fxhttpserver/testdata/templates/test.html @@ -0,0 +1 @@ +App name: {{index . "name"}} \ No newline at end of file diff --git a/release-please-config.json b/release-please-config.json index 93be219..fee0356 100644 --- a/release-please-config.json +++ b/release-please-config.json @@ -61,6 +61,11 @@ "component": "fxhttpclient", "tag-separator": "/" }, + "fxhttpserver": { + "release-type": "go", + "component": "fxhttpserver", + "tag-separator": "/" + }, "fxlog": { "release-type": "go", "component": "fxlog",