Giter Club home page Giter Club logo

go-don's Introduction

Don - Go API Framework

GoDoc Codecov Go Report Card

Don is a fast & simple API framework written in Go. It features a super-simple API and thanks to fasthttp and a custom version of httprouter it's blazing fast and has a low memory footprint.

As Don uses Go generics it requires Go 1.18 to work.
Minor version updates should be considered breaking changes.

Contents

Basic Example

package main

import (
  "context"
  "errors"
  "fmt"
  "net/http"

  "github.com/abemedia/go-don"
  _ "github.com/abemedia/go-don/encoding/json" // Enable JSON parsing & rendering.
  _ "github.com/abemedia/go-don/encoding/yaml" // Enable YAML parsing & rendering.
)

type GreetRequest struct {
  Name string `path:"name"`         // Get name from the URL path.
  Age  int    `header:"X-User-Age"` // Get age from HTTP header.
}

type GreetResponse struct {
  // Remember to add all the tags for the renderers you enable.
  Greeting string `json:"data" yaml:"data"`
}

func Greet(ctx context.Context, req GreetRequest) (*GreetResponse, error) {
  if req.Name == "" {
    return nil, don.Error(errors.New("missing name"), http.StatusBadRequest)
  }

  res := &GreetResponse{
    Greeting: fmt.Sprintf("Hello %s, you're %d years old.", req.Name, req.Age),
  }

  return res, nil
}

func Pong(context.Context, any) (string, error) {
  return "pong", nil
}

func main() {
  r := don.New(nil)
  r.Get("/ping", don.H(Pong)) // Handlers are wrapped with `don.H`.
  r.Post("/greet/:name", don.H(Greet))
  r.ListenAndServe(":8080")
}

Configuration

Don is configured by passing in the Config struct to don.New.

r := don.New(&don.Config{
  DefaultEncoding: "application/json",
  DisableNoContent: false,
})

DefaultEncoding

Set this to the format you'd like to use if no Content-Type or Accept headers are in the request.

DisableNoContent

If you return nil from your handler, Don will respond with an empty body and a 204 No Content status code. Set this to true to disable that behaviour.

Support multiple formats

Support multiple request & response formats without writing extra parsing or rendering code. The API uses the Content-Type and Accept headers to determine what input and output encoding to use.

You can mix multiple formats, for example if the Content-Type header is set to application/json, however the Accept header is set to application/x-yaml, then the request will be parsed as JSON, and the response will be YAML encoded.

If no Content-Type or Accept header is passed the default will be used.

Formats need to be explicitly imported e.g.

import _ "github.com/abemedia/go-don/encoding/yaml"

Currently supported formats

JSON

MIME: application/json

Parses JSON requests & encodes responses as JSON. Use the json tag in your request & response structs.

XML

MIME: application/xml, text/xml

Parses XML requests & encodes responses as XML. Use the xml tag in your request & response structs.

YAML

MIME: application/yaml, text/yaml, application/x-yaml, text/x-yaml, text/vnd.yaml

Parses YAML requests & encodes responses as YAML. Use the yaml tag in your request & response structs.

Form (input only)

MIME: application/x-www-form-urlencoded, multipart/form-data

Parses form data requests. Use the form tag in your request struct.

Text

MIME: text/plain

Parses non-struct requests and encodes non-struct responses e.g. string, int, bool etc.

func MyHandler(ctx context.Context, req int64) (string, error) {
  // ...
}

If the request is a struct and the Content-Type header is set to text/plain it returns a 415 Unsupported Media Type error.

MessagePack

MIME: application/msgpack, application/x-msgpack, application/vnd.msgpack

Parses MessagePack requests & encodes responses as MessagePack. Use the msgpack tag in your request & response structs.

TOML

MIME: application/toml

Parses TOML requests & encodes responses as TOML. Use the toml tag in your request & response structs.

Protocol Buffers

MIME: application/protobuf, application/x-protobuf

Parses protobuf requests & encodes responses as protobuf. Use pointer types generated with protoc as your request & response structs.

Adding custom encoding

Adding your own is easy. See encoding/json/json.go.

Request parsing

Automatically unmarshals values from headers, URL query, URL path & request body into your request struct.

type MyRequest struct {
  // Get from the URL path.
  ID int64 `path:"id"`

  // Get from the URL query.
  Filter string `query:"filter"`

  // Get from the JSON, YAML, XML or form body.
  Content float64 `form:"bar" json:"bar" yaml:"bar" xml:"bar"`

  // Get from the HTTP header.
  Lang string `header:"Accept-Language"`
}

Please note that using a pointer as the request type negatively affects performance.

Headers & response codes

Implement the StatusCoder and Headerer interfaces to customise headers and response codes.

type MyResponse struct {
  Foo  string `json:"foo"`
}

// Set a custom HTTP response code.
func (nr *MyResponse) StatusCode() int {
  return 201
}

// Add custom headers to the response.
func (nr *MyResponse) Header() http.Header {
  header := http.Header{}
  header.Set("foo", "bar")
  return header
}

Sub-routers

You can create sub-routers using the Group function:

r := don.New(nil)
sub := r.Group("/api")
sub.Get("/hello", don.H(Hello))

Middleware

Don uses the standard fasthttp middleware format of func(fasthttp.RequestHandler) fasthttp.RequestHandler.

For example:

func loggingMiddleware(next fasthttp.RequestHandler) fasthttp.RequestHandler {
  return func(ctx *fasthttp.RequestCtx) {
    log.Println(string(ctx.RequestURI()))
    next(ctx)
  }
}

It is registered on a router using Use e.g.

r := don.New(nil)
r.Post("/", don.H(handler))
r.Use(loggingMiddleware)

Middleware registered on a group only applies to routes in that group and child groups.

r := don.New(nil)
r.Get("/login", don.H(loginHandler))
r.Use(loggingMiddleware) // applied to all routes

api := r.Group("/api")
api.Get("/hello", don.H(helloHandler))
api.Use(authMiddleware) // applied to routes `/api/hello` and `/api/v2/bye`


v2 := api.Group("/v2")
v2.Get("/bye", don.H(byeHandler))
v2.Use(corsMiddleware) // only applied to `/api/v2/bye`

To pass values from the middleware to the handler extend the context e.g.

func myMiddleware(next fasthttp.RequestHandler) fasthttp.RequestHandler {
  return func(ctx *fasthttp.RequestCtx) {
    ctx.SetUserValue(ContextUserKey, "my_user")
    next(ctx)
  }
}

This can now be accessed in the handler:

user := ctx.Value(ContextUserKey).(string)

Benchmarks

To give you a rough idea of Don's performance, here is a comparison with Gin.

Request Parsing

Don has extremely fast & efficient binding of request data.

Benchmark name (1) (2) (3) (4)
BenchmarkDon_BindRequest 2947474 390.3 ns/op 72 B/op 2 allocs/op
BenchmarkGin_BindRequest 265609 4377 ns/op 1193 B/op 21 allocs/op

Source: benchmarks/binding_test.go

Serving HTTP Requests

Keep in mind that the majority of time here is actually the HTTP roundtrip.

Benchmark name (1) (2) (3) (4)
BenchmarkDon_HTTP 45500 25384 ns/op 32 B/op 3 allocs/op
BenchmarkGin_HTTP 22995 49865 ns/op 2313 B/op 21 allocs/op

Source: benchmarks/http_test.go

go-don's People

Contributors

abemedia avatar dependabot[bot] avatar elara6331 avatar github-actions[bot] avatar ssoroka avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar

go-don's Issues

Context request unmarshalling

Idea to include context unmarshalling into the request struct or a simple way for custom model binding based on tags:

type WidgetPutReq struct {
	IsAuth         bool   `perm:"widget-manage"`
	UserName  string `context:"userName"`
	UrlCode      string `path:"code"`
	Widget
}

Add missing tests

  • api.ListenAndServe
  • api.Serve
  • decoder New/NewCached error handling
  • decoder cached.DecodeValue

Consider rfc7807 error response handling

It appears don will fallback to a 500 error and uses a simple format { "message": "Internal Server Error" }

It would be nice if it used a standard response format with a method of hooking in validation errors like shown in the spec.

Full example below

package settings

import (
	"context"
	"github.com/Example/go-auth/auth"
	"github.com/abemedia/go-don/problems"
)

type SettingsGetReq struct {
	Session auth.Session `context:"auth-session"`
	Code    string       `path:"code"`
}

type SettingsPutReq struct {
	TargetCode string       `path:"code"`
	Session    auth.Session `context:"auth-session"`
	Settings
}

func SettingsGet(ctx context.Context, req SettingsGetReq) (*Settings, error) {
	if !req.Session.Permit("settings-read") {
		return nil, problems.NewPermError(req.Session.Username)
	}

	s, err := get(req.Code)

	if s == nil && err == nil {
		return nil, problems.NewNotFoundError()
	} else if err != nil {
		return nil, problems.NewUnexpError(err)
	}

	return s, nil
}

func SettingsPut(ctx context.Context, req SettingsPutReq) (*Settings, error) {
	if !req.Session.Permit("settings-manage") {
		return nil, problems.NewPermError(req.Session.Username)
	}

	fieldErrors := req.validate(req.TargetCode)
	if fieldErrors != nil {
		return nil, problems.NewValidError(fieldErrors)
	}

	err := put(&req.Settings)
	if err != nil {
		return nil, problems.NewUnexpError(err)
	}

	return &req.Settings, nil
}

Set default encoding if only one encoder is registered

e.g. in api.go:

        if c.DefaultEncoding == "" {
-               c.DefaultEncoding = DefaultEncoding
+               switch len(encoders) {
+               case 0:
+                       panic("no encoders available")
+               case 1:
+                       for key := range encoders {
+                               c.DefaultEncoding = key
+                       }
+               default:
+                       c.DefaultEncoding = DefaultEncoding
+               }
+       }
+
+       if _, ok := encoders[c.DefaultEncoding]; !ok {
+               panic("no encoder available for default: " + c.DefaultEncoding)
        }

Create abstraction layer for requests

Create a don.Context interface with an implementation that calls out to fasthttp.

don.Context Methods:

  • context.Context
  • io.Reader (reads from request body)
  • io.Writer (writes to response body)
  • Body() []byte (request body)
  • Method() string
  • URL() don.URL (request URL)
  • RequestHeader() don.Header
  • ResponseHeader() don.Header
  • Query() don.Params (request URL query)
  • Path() don.Params (request path params)
  • Status(int) don.Params (sets status code)
  • Redirect(string, int)
  • Decode(any) error (decode request body, path, header & query to struct)
  • Encode(any) error (encode and write to response body)

don.Header Methods:

  • Get(string) string
  • Values(string) []string
  • Add(k,v string)
  • Set(k,v string)
  • Range(f func(k,v string))

don.Params Methods:

  • Get(string) string
  • Values(string) []string
  • Range(f func(k,v string))

Use this interface in middleware and handlers that don't use don.H. Old style handlers using the fasthttp request directly should also still be supported via an adapter.

This will allow don to also run on standard lib net/http down the line by creating a second implementation of don.Context that wraps the standard lib request & response writer.

Force content type option

Add an option to change the behaviour when the Accept or Content-Type headers are unsupported to force using the default content type.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.