rest

package module
v0.0.0-...-665d599 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Sep 23, 2020 License: BSD-3-Clause Imports: 15 Imported by: 0

README

Minimalist REST Framework Build Status

This package leverages the power of gorilla/mux and protobuf to provide a REST framework with the following characteristics:

  • JSON/protobuf dual formats support.
  • Automatic validation against required/optional properties.

Let's start with a simple REST user service:

func main() {
    r := mux.NewRouter()
    rest := rest.NewServer()
    r.Path("/user/{id:[a-z]+}").Handler(rest.HandlerFunc(UserHandler))
    log.Fatal(http.ListenAndServe("localhost:8080", r))
}

And then implement a handler function:

func UserHandler(s *rest.Session) interface{} {
    // handle session and return either an error, or a response, or nil
}

Either of the following could be returned in a handler function:

  • nil: Status 200 OK will be replied with nothing written to the ResponseWriter.
  • error: Status code 500 will be replied with message of err.Error(), and nothing written to the ResponseWriter.
  • A special error created by jrest.HTTPError(code int) or jrest.HTTPErrorf(code int, format string, ...): custom status code with be replied, with message created by http.StatusText(code int) or custom message.
  • Otherwise, status 200 OK will be replied, and the returned object will be encoded and written to the ResponseWriter. By default it will be encoded in JSON, unless both server and client support protobuf. However, if there was error encoding the response, status code 500 will be replied instead. So be sure to set required properties of data object in protobuf format. How the dual format works will be described later.

Before finishing the handler function, define the data structure of User and a simple database with map.

type User struct {
    Email       string `json:"email"`
    DisplayName string `json:"display_name,omitempty"`
}
var users = map[string]*User{
    "alice": &User{"alice@foo.com", "Alice"},
}

And then we can finish the handler function.

func UserHandler(s *rest.Session) interface{} {
    id := s.Var("id")
    switch s.Request.Method {
    case "GET":
        return users[id]
    case "POST":
        user := &User{}
        err := s.Decode(user)
        if err != nil {
            return err
        }
        users[id] = user
    case "DELETE":
        delete(users, id)
    }
    return nil
}

Now we have a REST user service with just few lines of code, but only JSON format was supported. So this doesn't bring obvious benefit compared will gorilla/mux. So let's move on.

How Protobuf Help JSON Validation

In the past, JSON validation is tending to be tidious and error prone. Assume that we expected a User should has a mandatory email property and an optional display_name property.

However, all the following JSON data could be decoded by json.Unmarshal() without returning error:

{"email":"alice@foo.com","name":"Alice"}
{"email":"alice@foo.com"}
{"name":"Alice"}
{}

And all decoded User can also be encoded by json.Marshal() without returning error. But the corresponding output JSON data become:

{"email":"alice@foo.com","name":"Alice"}
{"email":"alice@foo.com"}
{"email":"","name":"Alice"}
{"email":""}

It's obvious and well-known that encoding/json does nothing about data validation. Struct tag omitempty only help marshal empty properties. So data validation was left on us by checking properties one-by-one:

user := &User{}
err := json.Unmarshal(data, user)
if err != nil {
    return err
}
if user.Email == "" {
    return err.Errorf("Empty e-mail")
}
// do actual stuffs

However, if we define User in .proto and generate its golang code:

message User {
    required string email = 1;
    optional string display_name = 2;
}

We can leverage protobuf to help basic data validation against required/optional:

user := &User{}
err := json.Unmarshal(data, user)
if err == nil {
    _, err = proto.Marshal(user)
}
if err != nil {
    return err
}
// do actual stuffs

If the argument passed to Session.Decode() is an instance of proto.Message, it will do all the data validation magic. No matter the actual request was in JSON or protobuf request. As a result, it's encouraged to define data objects in .proto if the code generation overhead is acceptable.

However, required/optional was not supported by proto3 but only by proto2. Even so, protobuf still have the following benefits:

  • Help convert snake-case naming convention to camel-case, saving us from defining tons of JSON struct tags.
  • Help marshal/unmarshal for both server side and client side.
  • More data efficient compared with JSON.

How Dual Format was Supported

If all the following criteria were met, request body will be decoded as a protobuf message:

  • The argument passed to Session.Decode() is an instance of proto.Message.
  • The value of request header Content-Type matches application/protobuf or application/x-protobuf.

Otherwise, the request body will be decoded as JSON message.

If all the following criteria were met, the response will be encoded as a protobuf message:

  • The returned object is an instance of proto.Message.
  • The request was decoded as protobuf message, or values of request header Accept matches application/protobuf or application/x-protobuf.

With dual format support, a REST service can keep backward compatibility and interoperability with javascript clients, and at the same time embrace the data security and efficient of protobuf format.

Alternatives

  • grpc by Google: Also based on protobuf, but grpc is not RESTful. However, it's possible to keep backward compatibility by providing a REST broker in front of grpc service. In grpc both data object and service could be defined by .proto. And corresponding server stubs and client could be code-generated in many supported programming languages.
  • swagger: Swagger is a framework for generating REST services as well as its documents based on OpenAPI. However, AFAIK it doesn't support protobuf over REST. And in some article, you can learn that protobuf is much more efficient than JSON.

Besides, there is an interesting project grpc-gateway, which could be integrated into protoc code-gen process to generate a gateway in front of grpc, and a generated REST service conforming to swagger.

However IMHO, REST is resource-oriented, but RPC is operation-oriented. They are of different paradigms. The code generation approach result in either a strange grpc service or a REST service which is not backward-compatible.

Sample Code

Check out for our minimalist sample code.

Documentation

Index

Constants

View Source
const (
	Accept          = "Accept"
	ContentType     = "Content-Type"
	JsonContentType = "application/json"
)

Variables

View Source
var (
	ProtobufContentTypes = []string{"application/protobuf", "application/x-protobuf"}
)

Functions

This section is empty.

Types

type Auth

type Auth interface {
	Authorize(req *http.Request) error
	Validate(res *Response) (bool, error)
	Invalidate() error
}

type Client

type Client struct {
	log.Sugar
	Client *http.Client
	URL    string
	// contains filtered or unexported fields
}

func NewClient

func NewClient(logger log.Logger, timeout time.Duration) *Client

func (*Client) Auth

func (c *Client) Auth(auth Auth) *Client

func (*Client) Delete

func (c *Client) Delete(url string) (*Response, error)

func (*Client) ExpectStatus

func (c *Client) ExpectStatus(status int) *Client

func (*Client) Get

func (c *Client) Get(url string) (*Response, error)

func (*Client) New

func (c *Client) New(u string) *Request

func (*Client) Post

func (c *Client) Post(url string, req interface{}) (*Response, error)

func (*Client) Protobuf

func (c *Client) Protobuf() *Client

func (*Client) Put

func (c *Client) Put(url string, req interface{}) (*Response, error)

func (*Client) Request

func (c *Client) Request(method, url string, r interface{}) (*Response, error)

type HandlerFunc

type HandlerFunc func(s *Session)

type MiddlewareFunc

type MiddlewareFunc func(handler HandlerFunc) HandlerFunc

type Request

type Request struct {
	// contains filtered or unexported fields
}

func (*Request) Delete

func (r *Request) Delete() (*Response, error)

func (*Request) Do

func (r *Request) Do(method string, v interface{}) (*Response, error)

func (*Request) Get

func (r *Request) Get() (*Response, error)

func (*Request) Header

func (r *Request) Header(name, value string) *Request

func (*Request) Join

func (r *Request) Join(path string) *Request

func (*Request) Param

func (r *Request) Param(name, value string) *Request

func (*Request) Post

func (r *Request) Post(v interface{}) (*Response, error)

func (*Request) Put

func (r *Request) Put(v interface{}) (*Response, error)

type Response

type Response struct {
	log.Sugar
	*http.Response
	Body []byte
	// contains filtered or unexported fields
}

func (*Response) Decode

func (r *Response) Decode(val interface{}) error

type Server

type Server struct {
	log.Sugar
	// contains filtered or unexported fields
}

func NewServer

func NewServer(logger log.Logger) *Server

func (*Server) Delete

func (s *Server) Delete(r *mux.Route, handler HandlerFunc) *mux.Route

func (*Server) Get

func (s *Server) Get(r *mux.Route, handler HandlerFunc) *mux.Route

func (*Server) HandlerFunc

func (s *Server) HandlerFunc(handler HandlerFunc) http.Handler

func (*Server) JSONIndent

func (s *Server) JSONIndent(prefix, indent string)

func (*Server) Post

func (s *Server) Post(r *mux.Route, handler HandlerFunc) *mux.Route

func (*Server) Put

func (s *Server) Put(r *mux.Route, handler HandlerFunc) *mux.Route

func (*Server) Use

func (s *Server) Use(middlewares ...MiddlewareFunc) *Server

type Session

type Session struct {
	log.Context

	Data           map[interface{}]interface{}
	Request        *http.Request
	ResponseWriter http.ResponseWriter
	// contains filtered or unexported fields
}

func (*Session) Decode

func (s *Session) Decode(val interface{}) error

func (*Session) RemoteHost

func (s *Session) RemoteHost() (net.IP, error)

func (*Session) RequestHeader

func (s *Session) RequestHeader() http.Header

func (*Session) ResponseHeader

func (s *Session) ResponseHeader() http.Header

func (*Session) Status

func (s *Session) Status(status int, v interface{})

func (*Session) StatusCode

func (s *Session) StatusCode(code int)

func (*Session) Statusf

func (s *Session) Statusf(code int, format string, args ...interface{})

func (*Session) Var

func (s *Session) Var(key, preset string) string

func (*Session) Vars

func (s *Session) Vars() map[string]string

type URL

type URL struct {
	// contains filtered or unexported fields
}

func NewURL

func NewURL(u string) *URL

func (*URL) Encode

func (r *URL) Encode() string

func (*URL) Join

func (r *URL) Join(path string) *URL

func (*URL) Param

func (r *URL) Param(name, value string) *URL

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL