jsonapi

package module
v0.1.1-0...-d0f436e Latest Latest
Warning

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

Go to latest
Published: Feb 18, 2021 License: MPL-2.0 Imports: 9 Imported by: 0

README

Package jsonapi is simple wrapper for buildin net/http package. It aims to let developers build json-based web api easier.

GoDoc Build Status Go Report Card

Usage

Create an api handler is so easy:

// HelloArgs is data structure for arguments passed by POST body.
type HelloArgs struct {
        Name string
        Title string
}

// HelloReply defines data structure this api will return.
type HelloReply struct {
        Message string
}

// HelloHandler greets user with hello
func HelloHandler(q jsonapi.Request) (res interface{}, err error) {
        // Read json objct from request.
        var args HelloArgs
        if err = q.Decode(&args); err != nil {
                // The arguments are not passed in JSON format, returns http
                // status 400 with {"errors": [{"detail": "invalid param"}]}
                err = jsonapi.E400.SetOrigin(err).SetData("invalid param")
                return
        }

        res = HelloReply{fmt.Sprintf("Hello, %s %s", args,Title, args.Name)}
        return
}

And this is how we do in main function:

// Suggested usage
apis := []jsonapi.API{
    {"/api/hello", HelloHandler},
}
jsonapi.Register(http.DefaultMux, apis)

// old-school
http.Handle("/api/hello", jsonapi.Handler(HelloHandler))

Generated response is a subset of jsonapi specs. Refer to handler_test.go for examples.

Call API with TypeScript

There's a fetch.ts providing grab<T>() as simple wrapping around fetch(). With following Go code:

type MyStruct struct {
    X int  `json:"x"`
	Y bool `json:"y"
}

func MyAPI(q jsonapi.Request) (ret interface{}, err error) {
    return []MyStruct{
	    {X: 1, Y: true},
		{X: 2},
	}, nil
}

function main() {
    apis := []jsonapi.API{
	    {"/my-api", MyAPI},
    }
	jsonapi.Register(http.DefaultMux, apis)
	http.ListenAndServe(":80", nil)
}

You might write TypeScript code like this:

export interface MyStruct {
  x?: number;
  y?: boolean;
}

export function getMyApi(): Promise<MyStruct[]> {
  return grab<MyStruct[]>('/my-api');
}

export function postMyApi(): Promise<MyStruct[]> {
  return grab<MyStruct[]>('/my-api', {
    method: 'POST',
	headers: {'Content-Type': 'application/json'},
	body: JSON.stringify('my data')
  });
}

Middleware

func runtimeLog(h jsonapi.Handler) jsonapi.Handler {
    return func(r jsonapi.Request) (data interface{}, err error) {
        log.Printf("entering path %s", r.R().URL.Path)
        begin := time.Now().Unix()
        data, err = h(r)
        log.Printf("processed path %s in %d seconds", r.R().URL.Path, time.Now().Unix()-begin)
        return
    }
}

func main() {
    jsonapi.With(runtimeLog).Register(http.DefaultMux, myapis)
    http.ListenAndServe(":80", nil)
}

There're few pre-defined middlewares in package apitool, see godoc.

License

See LICENSE.txt

Copyright 2019- Ronmi Ren ronmi.ren@gmail.com

Documentation

Overview

Package jsonapi is simple wrapper for buildin net/http package. It aims to let developers build json-based web api easier.

Usage

Create an api handler is so easy:

// HelloArgs is data structure for arguments passed by POST body.
type HelloArgs struct {
        Name string
        Title string
}

// HelloReply defines data structure this api will return.
type HelloReply struct {
        Message string
}

// HelloHandler greets user with hello
func HelloHandler(q jsonapi.Request) (res interface{}, err error) {
        // Read json objct from request.
        var args HelloArgs
        if err = q.Decode(&args); err != nil {
                // The arguments are not passed in JSON format, returns http
                // status 400 with {"errors": [{"detail": "invalid param"}]}
                err = jsonapi.E400.SetOrigin(err).SetData("invalid param")
                return
        }

        res = HelloReply{fmt.Sprintf("Hello, %s %s", args,Title, args.Name)}
        return
}

And this is how we do in main function:

// Suggested usage
apis := []jsonapi.API{
    {"/api/hello", HelloHandler},
}
jsonapi.Register(http.DefaultMux, apis)

// old-school
http.Handle("/api/hello", jsonapi.Handler(HelloHandler))

Generated response is a subset of specs in https://jsonapi.org. Refer to `handler_test.go` for examples.

Call API with TypeScript

There's a `fetch.ts` providing `grab<T>()` as simple wrapping around `fetch()`. With following Go code:

type MyStruct struct {
    X int  `json:"x"`
	Y bool `json:"y"
}

func MyAPI(q jsonapi.Request) (ret interface{}, err error) {
    return []MyStruct{
	    {X: 1, Y: true},
		{X: 2},
	}, nil
}

function main() {
    apis := []jsonapi.API{
	    {"/my-api", MyAPI},
    }
	jsonapi.Register(http.DefaultMux, apis)
	http.ListenAndServe(":80", nil)
}

You might write TypeScript code like this:

export interface MyStruct {
  x?: number;
  y?: boolean;
}

export function getMyApi(): Promise<MyStruct[]> {
  return grab<MyStruct[]>('/my-api');
}

export function postMyApi(): Promise<MyStruct[]> {
  return grab<MyStruct[]>('/my-api', {
    method: 'POST',
	headers: {'Content-Type': 'application/json'},
	body: JSON.stringify('my data')
  });
}

Index

Constants

This section is empty.

Variables

View Source
var (
	EUnknown = Error{Code: 0, /* contains filtered or unexported fields */}
	E301     = Error{Code: 301, /* contains filtered or unexported fields */}
	E302     = Error{Code: 302, /* contains filtered or unexported fields */}
	E303     = Error{Code: 303, /* contains filtered or unexported fields */}
	E304     = Error{Code: 304, /* contains filtered or unexported fields */}
	E307     = Error{Code: 307, /* contains filtered or unexported fields */}
	E400     = Error{Code: 400, /* contains filtered or unexported fields */}
	E401     = Error{Code: 401, /* contains filtered or unexported fields */}
	E403     = Error{Code: 403, /* contains filtered or unexported fields */}
	E404     = Error{Code: 404, /* contains filtered or unexported fields */}
	E408     = Error{Code: 408, /* contains filtered or unexported fields */}
	E409     = Error{Code: 409, /* contains filtered or unexported fields */}
	E410     = Error{Code: 410, /* contains filtered or unexported fields */}
	E413     = Error{Code: 413, /* contains filtered or unexported fields */}
	E415     = Error{Code: 415, /* contains filtered or unexported fields */}
	E418     = Error{Code: 418, /* contains filtered or unexported fields */}
	E426     = Error{Code: 426, /* contains filtered or unexported fields */}
	E429     = Error{Code: 429, /* contains filtered or unexported fields */}
	E500     = Error{Code: 500, /* contains filtered or unexported fields */}
	E501     = Error{Code: 501, /* contains filtered or unexported fields */}
	E502     = Error{Code: 502, /* contains filtered or unexported fields */}
	E503     = Error{Code: 503, /* contains filtered or unexported fields */}
	E504     = Error{Code: 504, /* contains filtered or unexported fields */}

	// application-defined error
	APPERR = Error{Code: 200}

	// special error, preventing ServeHTTP method to encode the returned data
	//
	// For string, []byte or anything implements fmt.Stringer returned, we will
	// write it to response as-is.
	//
	// For other type, we use fmt.FPrintf(responseWriter, "%v", returnedData).
	//
	// You will also have to:
	//    - Set HTTP status code manually.
	//    - Set necessary response headers manually.
	//    - Take care not to be overwritten by middleware.
	ASIS = Error{Code: -1}
)

here are predefined error instances, you should call SetData before use it like

return nil, E404.SetData("User not found")

You might noticed that here's no 500 error. You should just return a normal error instance instead.

return nil, errors.New("internal server error")

Functions

func ConvertCamelToSlash

func ConvertCamelToSlash(name string) string

ConvertCamelToSlash is a helper to convert CamelCase to camel/case

func ConvertCamelToSnake

func ConvertCamelToSnake(name string) string

ConvertCamelToSnake is a helper to convert CamelCase to camel_case

func Failed

func Failed(e1 error, e2 Error) (data interface{}, err error)

Failed wraps you error object and prepares suitable return type to be used in controller

Here's a common usage:

if err := req.Decode(&param); err != nil {
    return jsonapi.Failed(err, jsonapi.E400.SetData("invalid parameter"))
}
if err := param.IsValid(); err != nil {
    return jsonapi.Failed(err, jsonapi.E400.SetData("invalid parameter"))
}

func Register

func Register(mux HTTPMux, apis []API)

Register helps you to register many APIHandlers to a http.ServeHTTPMux

func RegisterAll

func RegisterAll(
	mux HTTPMux, prefix string, handlers interface{},
	converter func(string) string,
)

RegisterAll helps you to register all handler methods

As using reflection to do the job, only exported methods with correct signature are registered.

converter is used to convert from method name to url pattern, see CovertCamelToSnake for example.

If converter is nil, name will leave unchanged.

Types

type API

type API struct {
	Pattern string
	Handler func(Request) (interface{}, error)
}

API denotes how a json api handler registers to a servemux

type ErrObj

type ErrObj struct {
	Code   string `json:"code,omitempty"`
	Detail string `json:"detail,omitempty"`
}

ErrObj defines how an error is exported to client

For jsonapi.Error, Code will contains result of SetCode; Detail will be SetData

For other error types, only Detail is set, as error.Error()

func (*ErrObj) AsError

func (o *ErrObj) AsError() error

AsError creates an error object represents this error

If Code is set, an Error instance will be returned. errors.New(Detail) otherwise.

type Error

type Error struct {
	Code   int
	Origin error // prepared for application errors
	// contains filtered or unexported fields
}

Error represents an error status of the HTTP request. Used with APIHandler.

func (Error) Data

func (h Error) Data() string

Data retrieves user defined error message

func (Error) EqualTo

func (h Error) EqualTo(e Error) bool

EqualTo tells if two Error instances represents same kind of error

It compares all fields no matter exported or not, excepts Origin

func (Error) ErrCode

func (h Error) ErrCode() string

ErrCode retrieves user defined error code

func (Error) Error

func (h Error) Error() string

func (Error) SetCode

func (h Error) SetCode(code string) Error

SetCode forks a new instance with application-defined error code

func (Error) SetData

func (h Error) SetData(data string) Error

SetData creates a new Error instance and set the error message or url according to the error code

func (Error) SetOrigin

func (h Error) SetOrigin(err error) Error

SetOrigin creates a new Error instance to preserve original error

func (Error) String

func (h Error) String() string

type FakeRequest

type FakeRequest struct {
	// this is used to implement Request.Decode()
	Decoder *json.Decoder
	// this is used to implement Request.R() and Request.WithValue()
	Req *http.Request
	// this is used to implement Request.W()
	Resp http.ResponseWriter
}

FakeRequest implements a Request and let you do some magic in it

func (*FakeRequest) Decode

func (r *FakeRequest) Decode(data interface{}) error

Decode implements Request

func (*FakeRequest) R

func (r *FakeRequest) R() *http.Request

R implements Request

func (*FakeRequest) W

W implements Request

func (*FakeRequest) WithValue

func (r *FakeRequest) WithValue(key, val interface{}) (ret Request)

WithValue implements Request

type HTTPMux

type HTTPMux interface {
	Handle(pattern string, handler http.Handler)
}

HTTPMux abstracts http.ServeHTTPMux, so it will be easier to write tests

Only needed methods are added here.

type Handler

type Handler func(r Request) (interface{}, error)

Handler is easy to use entry for API developer.

Just return something, and it will be encoded to JSON format and send to client. Or return an Error to specify http status code and error string.

func myHandler(dec *json.Decoder, httpData *HTTP) (interface{}, error) {
    var param paramType
    if err := dec.Decode(&param); err != nil {
        return nil, jsonapi.E400.SetData("You must send parameters in JSON format.")
    }
    return doSomething(param), nil
}

To redirect clients, return 301~303 status code and set Data property

return nil, jsonapi.E301.SetData("http://google.com")

Redirecting depends on http.Redirect(). The data returned from handler will never write to ResponseWriter.

This basically obey the http://jsonapi.org rules:

  • Return {"data": your_data} if error == nil
  • Return {"errors": [{"code": application-defined-error-code, "detail": message}]} if error returned

func (Handler) ServeHTTP

func (h Handler) ServeHTTP(w http.ResponseWriter, r *http.Request)

ServeHTTP implements net/http.Handler

type Middleware

type Middleware func(Handler) Handler

Middleware is a wrapper for handler

type Registerer

type Registerer interface {
	Register(mux HTTPMux, apis []API)
	RegisterAll(mux HTTPMux, prefix string, handlers interface{},
		conv func(string) string)
	With(m Middleware) Registerer
}

Registerer represents a chain of middleware

With(
    myMiddleware
).With(
    apitool.LogIn(apitool.JSONFormat(
        log.New(os.Stdout, "myapp", log.LstdFlags),
    )),
).RegisterAll(mux, "/api", myHandler)

Request processing flow will be:

  1. mux.ServeHTTP
  2. myMiddleWare
  3. Logging middleware
  4. myHandler
  5. Logging middleware
  6. myMiddleWare

func With

func With(m Middleware) Registerer

With creates a new Registerer

type Request

type Request interface {
	// Decode() helps you to read parameters in request body
	Decode(interface{}) error
	// R() retrieves original http request
	R() *http.Request
	// W() retrieves original http response writer
	W() http.ResponseWriter
	// WithValue() adds a new key-value pair in context of http request
	WithValue(key, val interface{}) Request
}

Request represents most used data a handler need

func FromHTTP

func FromHTTP(w http.ResponseWriter, r *http.Request) Request

FromHTTP creates a Request instance from http request and response

func WrapRequest

func WrapRequest(q Request, r *http.Request) Request

WrapRequest creates a new Request, with http request replaced

func WrapResponse

func WrapResponse(q Request, w http.ResponseWriter) Request

WrapResponse creates a new Request, with http response replaced

Directories

Path Synopsis
Package apitest provides few tools helping you write tests
Package apitest provides few tools helping you write tests
Package apitool provides few middlewares helping you create your app
Package apitool provides few middlewares helping you create your app
gorsess
Package gorsess wraps gorilla session to SessionProvider
Package gorsess wraps gorilla session to SessionProvider
sessez
Package sessez provides an easy to use session to work with jsonapi
Package sessez provides an easy to use session to work with jsonapi
sessez/ezmemstore
Package ezmemstore stores session data in memory You have to call GC periodically to release memory.
Package ezmemstore stores session data in memory You have to call GC periodically to release memory.
sessez/ezpgxstore
Package ezpgxstore stores session data in Postgres using pgx You have to call GC() periodically to release db storage space.
Package ezpgxstore stores session data in Postgres using pgx You have to call GC() periodically to release db storage space.

Jump to

Keyboard shortcuts

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