From e4c808e6a5cba553172a9d07cd41705e405b0b30 Mon Sep 17 00:00:00 2001 From: agungdwiprasetyo Date: Tue, 6 Jul 2021 11:54:22 +0700 Subject: [PATCH] feature: add fiber rest server --- .gitignore | 1 + README.md | 8 ++ fiber_rest/README.md | 120 +++++++++++++++++++++ fiber_rest/fiber_http_wrapper.go | 172 +++++++++++++++++++++++++++++++ fiber_rest/fiber_rest_server.go | 48 +++++++++ fiber_rest/middleware.go | 67 ++++++++++++ fiber_rest/types.go | 16 +++ go.mod | 5 +- go.sum | 18 +++- 9 files changed, 450 insertions(+), 5 deletions(-) create mode 100644 .gitignore create mode 100644 fiber_rest/README.md create mode 100644 fiber_rest/fiber_http_wrapper.go create mode 100644 fiber_rest/fiber_rest_server.go create mode 100644 fiber_rest/middleware.go create mode 100644 fiber_rest/types.go diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..4949f09 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +example/ \ No newline at end of file diff --git a/README.md b/README.md index 3cc08d1..7c99378 100644 --- a/README.md +++ b/README.md @@ -18,3 +18,11 @@ Google Cloud PubSub

Can be used for AMQ Consumer + +## [Fiber REST Server](https://github.com/agungdwiprasetyo/candi-plugin/tree/master/fiber_rest) + +Fiber web Framework (https://gofiber.io) + +

+go fiber +

diff --git a/fiber_rest/README.md b/fiber_rest/README.md new file mode 100644 index 0000000..f5b56a2 --- /dev/null +++ b/fiber_rest/README.md @@ -0,0 +1,120 @@ +# Fiber REST Server + +https://github.com/gofiber/fiber + +## Install this plugin in your `candi` service + +### Add in service.go + +```go +package service + +import ( + fiberrest "github.com/agungdwiprasetyo/candi-plugin/fiber_rest" +... + +// Service model +type Service struct { + applications []factory.AppServerFactory +... + +// NewService in this service +func NewService(cfg *config.Config) factory.ServiceFactory { + ... + + s := &Service{ + ... + + // Add custom application runner, must implement `factory.AppServerFactory` methods + s.applications = append(s.applications, []factory.AppServerFactory{ + // customApplication + fiberrest.NewFiberServer(s, "[http port]", fiberrest.JaegerTracingMiddleware),, + }...) + + ... +} +... +``` + +### Register in module.go + +```go +package examplemodule + +import ( + "example.service/internal/modules/examplemodule/delivery/workerhandler" + + fiberrest "github.com/agungdwiprasetyo/candi-plugin/fiber_rest" + + "pkg.agungdp.dev/candi/codebase/factory/dependency" + "pkg.agungdp.dev/candi/codebase/factory/types" + "pkg.agungdp.dev/candi/codebase/interfaces" +) + +type Module struct { + // ...another delivery handler + serverHandlers map[types.Server]interfaces.ServerHandler +} + +func NewModules(deps dependency.Dependency) *Module { + return &Module{ + serverHandlers: map[types.Server]interfaces.ServerHandler{ + // ...another server handler + // ... + fiberrest.FiberREST: resthandler.NewFiberHandler(usecaseUOW.User(), dependency.GetMiddleware(), dependency.GetValidator()), + }, + } +} + +// ...another method +``` + +### Create delivery handler + +```go +package workerhandler + +import ( + "context" + "encoding/json" + + fiberrest "github.com/agungdwiprasetyo/candi-plugin/fiber_rest" + "github.com/gofiber/fiber/v2" + + "pkg.agungdp.dev/candi/candishared" + "pkg.agungdp.dev/candi/codebase/interfaces" + "pkg.agungdp.dev/candi/tracer" +) + +// FiberHandler struct +type FiberHandler struct { + mw interfaces.Middleware + uc usecase.UserUsecase + validator interfaces.Validator +} + +// NewFiberHandler constructor +func NewFiberHandler(uc usecase.UserUsecase, mw interfaces.Middleware, validator interfaces.Validator) *FiberHandler { + return &FiberHandler{ + uc: uc, + mw: mw, + validator: validator, + } +} + +// MountHandlers mount handler group +func (h *FiberHandler) MountHandlers(i interface{}) { + group := fiberrest.ParseGroupHandler(i) + + group.Get("/hello", fiberrest.WrapFiberHTTPMiddleware(h.mw.HTTPBearerAuth), helloHandler) +} + +func helloHandler(c *fiber.Ctx) error { + trace, ctx := tracer.StartTraceWithContext(fiberrest.FastHTTPParseContext(c.Context()), "DeliveryHandler") + defer trace.Finish() + + claim := candishared.ParseTokenClaimFromContext(ctx) + log.Println(claim) + return c.JSON(fiber.Map{"message": "Hello world!"}) +} +``` diff --git a/fiber_rest/fiber_http_wrapper.go b/fiber_rest/fiber_http_wrapper.go new file mode 100644 index 0000000..697da7e --- /dev/null +++ b/fiber_rest/fiber_http_wrapper.go @@ -0,0 +1,172 @@ +package fiberrest + +import ( + "context" + "io" + "net/http" + "net/url" + + "github.com/gofiber/fiber/v2" + "github.com/valyala/fasthttp" +) + +const ( + contextKey = "fibercontext" +) + +// WrapFiberHTTPMiddleware wraps net/http middleware to fiber middleware +func WrapFiberHTTPMiddleware(mw func(http.Handler) http.Handler) fiber.Handler { + return func(c *fiber.Ctx) error { + var next bool + netHTTPHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + next = true + c.Request().Header.SetMethod(r.Method) + c.Request().SetRequestURI(r.RequestURI) + c.Request().SetHost(r.Host) + c.Locals(contextKey, r.Context()) + for key, val := range r.Header { + for _, v := range val { + c.Request().Header.Set(key, v) + } + } + }) + fiberHandler := func(ctx context.Context, h http.Handler) fiber.Handler { + return func(c *fiber.Ctx) error { + WrapNetHTTPToFastHTTPHandler(ctx, h)(c.Context()) + return nil + } + } + + fiberHandler(FastHTTPParseContext(c.Context()), mw(netHTTPHandler))(c) + if next { + return c.Next() + } + return nil + } +} + +// WrapNetHTTPToFastHTTPHandler custom from https://github.com/valyala/fasthttp/blob/master/fasthttpadaptor/adaptor.go#L49 +// with additional context.Context param for use standard Go context to net/http request context +func WrapNetHTTPToFastHTTPHandler(ctx context.Context, h http.Handler) fasthttp.RequestHandler { + return func(c *fasthttp.RequestCtx) { + var r http.Request + + body := c.PostBody() + r.Method = string(c.Method()) + r.Proto = "HTTP/1.1" + r.ProtoMajor = 1 + r.ProtoMinor = 1 + r.RequestURI = string(c.RequestURI()) + r.ContentLength = int64(len(body)) + r.Host = string(c.Host()) + r.RemoteAddr = c.RemoteAddr().String() + + hdr := make(http.Header) + c.Request.Header.VisitAll(func(k, v []byte) { + sk := string(k) + sv := string(v) + switch sk { + case "Transfer-Encoding": + r.TransferEncoding = append(r.TransferEncoding, sv) + default: + hdr.Set(sk, sv) + } + }) + r.Header = hdr + r.Body = &netHTTPRequestBody{body} + rURL, err := url.ParseRequestURI(r.RequestURI) + if err != nil { + c.Logger().Printf("cannot parse requestURI %q: %s", r.RequestURI, err) + c.Error("Internal Server Error", fasthttp.StatusInternalServerError) + return + } + r.URL = rURL + + var w netHTTPResponseWriter + h.ServeHTTP(&w, r.WithContext(ctx)) + + c.SetStatusCode(w.StatusCode()) + haveContentType := false + for k, vv := range w.Header() { + if k == fasthttp.HeaderContentType { + haveContentType = true + } + + for _, v := range vv { + c.Response.Header.Set(k, v) + } + } + if !haveContentType { + // From net/http.ResponseWriter.Write: + // If the Header does not contain a Content-Type line, Write adds a Content-Type set + // to the result of passing the initial 512 bytes of written data to DetectContentType. + l := 512 + if len(w.body) < 512 { + l = len(w.body) + } + c.Response.Header.Set(fasthttp.HeaderContentType, http.DetectContentType(w.body[:l])) + } + c.Write(w.body) //nolint:errcheck + } +} + +type netHTTPResponseWriter struct { + statusCode int + h http.Header + body []byte +} + +func (w *netHTTPResponseWriter) StatusCode() int { + if w.statusCode == 0 { + return http.StatusOK + } + return w.statusCode +} + +func (w *netHTTPResponseWriter) Header() http.Header { + if w.h == nil { + w.h = make(http.Header) + } + return w.h +} + +func (w *netHTTPResponseWriter) WriteHeader(statusCode int) { + w.statusCode = statusCode +} + +func (w *netHTTPResponseWriter) Write(p []byte) (int, error) { + w.body = append(w.body, p...) + return len(p), nil +} + +type netHTTPRequestBody struct { + b []byte +} + +func (r *netHTTPRequestBody) Read(p []byte) (int, error) { + if len(r.b) == 0 { + return 0, io.EOF + } + n := copy(p, r.b) + r.b = r.b[n:] + return n, nil +} + +func (r *netHTTPRequestBody) Close() error { + r.b = r.b[:0] + return nil +} + +// FastHTTPParseContext get standard Go context from fasthttp request context +func FastHTTPParseContext(c *fasthttp.RequestCtx) context.Context { + ctx, ok := c.UserValue(contextKey).(context.Context) + if !ok { + return c + } + return ctx +} + +// FastHTTPSetContext set standard Go context to fasthttp request context +func FastHTTPSetContext(ctx context.Context, c *fasthttp.RequestCtx) { + c.SetUserValue(contextKey, ctx) +} diff --git a/fiber_rest/fiber_rest_server.go b/fiber_rest/fiber_rest_server.go new file mode 100644 index 0000000..45bf6bd --- /dev/null +++ b/fiber_rest/fiber_rest_server.go @@ -0,0 +1,48 @@ +package fiberrest + +import ( + "context" + "fmt" + + "github.com/gofiber/fiber/v2" + "pkg.agungdp.dev/candi/codebase/factory" +) + +type fiberREST struct { + serverEngine *fiber.App + service factory.ServiceFactory + httpPort string +} + +// NewFiberServer create new REST server +func NewFiberServer(service factory.ServiceFactory, httpPort string, rootMiddleware ...func(*fiber.Ctx) error) factory.AppServerFactory { + server := &fiberREST{ + serverEngine: fiber.New(), + service: service, + httpPort: httpPort, + } + + root := server.serverEngine.Group("/", rootMiddleware...) + for _, m := range service.GetModules() { + if h := m.ServerHandler(FiberREST); h != nil { + h.MountHandlers(root) + } + } + + fmt.Printf("\x1b[34;1m⇨ Fiber HTTP server run at port [::]%s\x1b[0m\n\n", server.httpPort) + return server +} + +func (s *fiberREST) Serve() { + if err := s.serverEngine.Listen(s.httpPort); err != nil { + panic(err) + } +} + +func (s *fiberREST) Shutdown(ctx context.Context) { + // h.serverEngine.Shutdown() +} + +func (s *fiberREST) Name() string { + return string(FiberREST) +} diff --git a/fiber_rest/middleware.go b/fiber_rest/middleware.go new file mode 100644 index 0000000..1fcd8a7 --- /dev/null +++ b/fiber_rest/middleware.go @@ -0,0 +1,67 @@ +package fiberrest + +import ( + "bytes" + "context" + "fmt" + "net/http" + + "github.com/gofiber/fiber/v2" + "github.com/opentracing/opentracing-go" + "github.com/opentracing/opentracing-go/ext" + "pkg.agungdp.dev/candi/config/env" + "pkg.agungdp.dev/candi/logger" + "pkg.agungdp.dev/candi/tracer" +) + +// JaegerTracingMiddleware use jaeger tracing middleware +func JaegerTracingMiddleware(c *fiber.Ctx) error { + globalTracer := opentracing.GlobalTracer() + operationName := fmt.Sprintf("%s %s%s", c.Method(), c.BaseURL(), c.Path()) + + netHTTPHeader := make(http.Header) + c.Request().Header.VisitAll(func(key, value []byte) { + netHTTPHeader.Set(string(key), string(value)) + }) + + var span opentracing.Span + var ctx context.Context + if spanCtx, err := globalTracer.Extract(opentracing.HTTPHeaders, opentracing.HTTPHeadersCarrier(netHTTPHeader)); err != nil { + span, ctx = opentracing.StartSpanFromContext(c.Context(), operationName) + ext.SpanKindRPCServer.Set(span) + } else { + span = globalTracer.StartSpan(operationName, opentracing.ChildOf(spanCtx), ext.SpanKindRPCClient) + ctx = opentracing.ContextWithSpan(c.Context(), span) + } + + body := c.Body() + if len(body) < env.BaseEnv().JaegerMaxPacketSize { // limit request body size to 65000 bytes (if higher tracer cannot show root span) + span.LogKV("request.body", string(body)) + } else { + span.LogKV("request.body.size", len(body)) + } + + span.SetTag("http.engine", "fiber (fasthttp) version "+fiber.Version) + span.SetTag("http.method", c.Method()) + span.SetTag("http.raw_url", c.OriginalURL()) + span.SetTag("http.request_header", string(c.Request().Header.RawHeaders())) + + defer func() { + span.SetTag("http.response_header", string(c.Response().Header.Header())) + span.SetTag("http.response_code", c.Response().StatusCode()) + resBody := new(bytes.Buffer) + c.Response().BodyWriteTo(resBody) + + if resBody.Len() < env.BaseEnv().JaegerMaxPacketSize { // limit response body size to 65000 bytes (if higher tracer cannot show root span) + span.LogKV("response.body", resBody.String()) + } else { + span.LogKV("response.body.size", resBody.Len()) + } + span.Finish() + + logger.LogGreen("fiber_rest_api > trace_url: " + tracer.GetTraceURL(ctx)) + }() + + FastHTTPSetContext(ctx, c.Context()) + return c.Next() +} diff --git a/fiber_rest/types.go b/fiber_rest/types.go new file mode 100644 index 0000000..644a926 --- /dev/null +++ b/fiber_rest/types.go @@ -0,0 +1,16 @@ +package fiberrest + +import ( + "github.com/gofiber/fiber/v2" + "pkg.agungdp.dev/candi/codebase/factory/types" +) + +const ( + // FiberREST types + FiberREST types.Server = "fiber_rest" +) + +// ParseGroupHandler parse mount handler param to fiber group +func ParseGroupHandler(i interface{}) *fiber.Group { + return i.(*fiber.Group) +} diff --git a/go.mod b/go.mod index f87d12b..0d496ef 100644 --- a/go.mod +++ b/go.mod @@ -5,6 +5,9 @@ go 1.16 require ( cloud.google.com/go/pubsub v1.11.0 github.com/go-stomp/stomp/v3 v3.0.1 + github.com/gofiber/fiber/v2 v2.14.0 + github.com/opentracing/opentracing-go v1.2.0 + github.com/valyala/fasthttp v1.26.0 google.golang.org/api v0.49.0 - pkg.agungdp.dev/candi v1.5.32 + pkg.agungdp.dev/candi v1.6.1 ) diff --git a/go.sum b/go.sum index 8dffaa0..89d5d11 100644 --- a/go.sum +++ b/go.sum @@ -53,6 +53,8 @@ github.com/Shopify/toxiproxy v2.1.4+incompatible/go.mod h1:OXgGpZ6Cli1/URJOF1DMx github.com/afex/hystrix-go v0.0.0-20180209013831-27fae8d30f1a/go.mod h1:SkGFH1ia65gfNATL8TAiHDNxPzPdmEL5uirI2Uyuz6c= github.com/agungdwiprasetyo/task-queue-worker-dashboard/external v0.0.0-20210508234331-1ec42b053c46/go.mod h1:oyuOHk50y8AZ4m9U55feZjFzJ1xFxFJwaoHIh4w6ZLA= github.com/ajstarks/svgo v0.0.0-20180226025133-644b8db467af/go.mod h1:K08gAheRH3/J6wwsYMMT4xOr94bZjxIelGM0+d/wbFw= +github.com/andybalholm/brotli v1.0.2 h1:JKnhI/XQ75uFBTiuzXpzFrUriDPiZjlOSzh6wXogP0E= +github.com/andybalholm/brotli v1.0.2/go.mod h1:loMXtMfwqflxFJPmdbJO0a3KNoPuLBgiu3qAvBg8x/Y= github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o= github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY= github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= @@ -126,6 +128,8 @@ github.com/gobuffalo/packd v0.1.0/go.mod h1:M2Juc+hhDXf/PnmBANFCqx4DM3wRbgDvnVWe github.com/gobuffalo/packr/v2 v2.0.9/go.mod h1:emmyGweYTm6Kdper+iywB6YK5YzuKchGtJQZ0Odn4pQ= github.com/gobuffalo/packr/v2 v2.2.0/go.mod h1:CaAwI0GPIAv+5wKLtv8Afwl+Cm78K/I/VCm/3ptBN+0= github.com/gobuffalo/syncx v0.0.0-20190224160051-33c29581e754/go.mod h1:HhnNqWY95UYwwW3uSASeV7vtgYkT2t16hJgV3AEPUpw= +github.com/gofiber/fiber/v2 v2.14.0 h1:oAUxouH4RWBE9r/3aZbucFefjdMmDF8rUsAIbyWkctY= +github.com/gofiber/fiber/v2 v2.14.0/go.mod h1:oZTLWqYnqpMMuF922SjGbsYZsdpE1MCfh416HNdweIM= github.com/gojektech/heimdall/v6 v6.1.0/go.mod h1:8g/ohsh0GXn8fzOf+qVrjX5pQLf7qQy8vEBjBUJ/9L4= github.com/gojektech/valkyrie v0.0.0-20180215180059-6aee720afcdf/go.mod h1:tDYRk1s5Pms6XJjj5m2PxAzmQvaDU8GqDf1u6x7yxKw= github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGwJL78qG/PmXZO1EjYhfJinVAhrmmHX6Z8B9k= @@ -347,9 +351,13 @@ github.com/uber/jaeger-lib v2.4.1+incompatible h1:td4jdvLcExb4cBISKIpHuGoVXh+dVK github.com/uber/jaeger-lib v2.4.1+incompatible/go.mod h1:ComeNDZlWwrWnDv8aPp0Ba6+uUTzImX/AauajbLI56U= 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/fasthttp v1.26.0 h1:k5Tooi31zPG/g8yS6o2RffRO2C9B9Kah9SY8j/S7058= +github.com/valyala/fasthttp v1.26.0/go.mod h1:cmWIqlu99AO/RKcp1HWaViTqc57FswJOfYYdPJBl8BA= github.com/valyala/fasttemplate v1.0.1/go.mod h1:UQGH1tvbgY+Nz5t2n7tXsz52dQxojPUpymEIMZ47gx8= github.com/valyala/fasttemplate v1.2.1 h1:TVEnxayobAdVkhQfrfes2IzOB6o+z4roRkPF52WA1u4= github.com/valyala/fasttemplate v1.2.1/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ= +github.com/valyala/tcplisten v1.0.0 h1:rBHj/Xf+E1tRGZyWIWwJDiRY0zc1Js+CV5DqwacVSA8= +github.com/valyala/tcplisten v1.0.0/go.mod h1:T0xQ8SeCZGxckz9qRXTfG43PvQ/mcWh7FwZEA7Ioqkc= github.com/xdg-go/pbkdf2 v1.0.0 h1:Su7DPu48wXMwC3bs7MCNG+z4FhcyEuz5dlvchbq0B0c= github.com/xdg-go/pbkdf2 v1.0.0/go.mod h1:jrpuAogTd400dnrH08LKmI/xc1MbPOebTwRqcT5RDeI= github.com/xdg-go/scram v1.0.2 h1:akYIkZ28e6A96dkWNJQu3nmCzH3YfwMPQExUYDaRv7w= @@ -397,8 +405,9 @@ golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8U golang.org/x/crypto v0.0.0-20200302210943-78000ba7a073/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20201112155050-0c6587e931a9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b h1:7mWr3k41Qtv8XlltBkDkl8LoP3mpSgBW8BUoxtEdbXg= golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4= +golang.org/x/crypto v0.0.0-20210513164829-c07d793c2f9a h1:kr2P4QFmQr29mSLA43kwrOcgcReGTfbE9N577tCTuBc= +golang.org/x/crypto v0.0.0-20210513164829-c07d793c2f9a/go.mod h1:P+XmwS30IXTQdn5tA2iutPOUgjI07+tq3H3K9MVA1s8= golang.org/x/exp v0.0.0-20180321215751-8460e604b9de/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20180807140117-3d87b88a115f/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= @@ -478,8 +487,9 @@ golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v golang.org/x/net v0.0.0-20210316092652-d523dce5a7f4/go.mod h1:RBQZq4jEuRlivfhVLdyRGr576XBO4/greRjx4P4O3yc= golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= golang.org/x/net v0.0.0-20210427231257-85d9c07bbe3a/go.mod h1:OJAsFXCWl8Ukc7SiCT/9KSuxbyM7479/AVlXFRxuMCk= -golang.org/x/net v0.0.0-20210503060351-7fd8e65b6420 h1:a8jGStKg0XqKDlKqjLrXn0ioF5MH36pT7Z0BRTqLhbk= golang.org/x/net v0.0.0-20210503060351-7fd8e65b6420/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.0.0-20210510120150-4163338589ed h1:p9UgmWI9wKpfYmgaV/IZKGdXc5qEK45tDwwwDyjS26I= +golang.org/x/net v0.0.0-20210510120150-4163338589ed/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= 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= @@ -789,8 +799,8 @@ honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt 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 h1:UoveltGrhghAA7ePc+e+QYDHXrBps2PqFZiHkGR/xK8= honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= -pkg.agungdp.dev/candi v1.5.32 h1:lQyu/341/ovSGgsIXP9kkgJBhiik4/zYn3OF7ld169M= -pkg.agungdp.dev/candi v1.5.32/go.mod h1:6EEuosKyrgtKF33KFBVP37t1xj7kQpcuDQWrYvEuhYM= +pkg.agungdp.dev/candi v1.6.1 h1:SvqFvsG1GRhn4FB+Hkx0AGsHv3tV8Qnd9QzevVL77iI= +pkg.agungdp.dev/candi v1.6.1/go.mod h1:6EEuosKyrgtKF33KFBVP37t1xj7kQpcuDQWrYvEuhYM= rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4= rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0=