Skip to content

Commit

Permalink
feature: add fiber rest server
Browse files Browse the repository at this point in the history
  • Loading branch information
agungdwiprasetyo committed Jul 6, 2021
1 parent dce8755 commit e4c808e
Show file tree
Hide file tree
Showing 9 changed files with 450 additions and 5 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
example/
8 changes: 8 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,3 +18,11 @@ Google Cloud PubSub
</p>

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)

<p align="center">
<img src="https://raw.githubusercontent.com/gofiber/docs/master/static/fiber_v2_logo.svg" width="150" alt="go fiber" />
</p>
120 changes: 120 additions & 0 deletions fiber_rest/README.md
Original file line number Diff line number Diff line change
@@ -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!"})
}
```
172 changes: 172 additions & 0 deletions fiber_rest/fiber_http_wrapper.go
Original file line number Diff line number Diff line change
@@ -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)
}
48 changes: 48 additions & 0 deletions fiber_rest/fiber_rest_server.go
Original file line number Diff line number Diff line change
@@ -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)
}
Loading

0 comments on commit e4c808e

Please sign in to comment.