jsonapi

package module
v0.2.0 Latest Latest
Warning

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

Go to latest
Published: Jun 17, 2024 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 Go Report Card

Notable changes before v1

v0.2.x

There're 2 breaking changes:

  • logging middleware is removed.
  • session middleware is removed.

And 3 tools are deprecated:

  • apitool.Client
  • apitool.Call
  • apitool.ParseResponse

Deprecated components will be removed at v0.3.0

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 Go
var result MyResult
param := MyParam { ... }
err := callapi.EP(http.MethodPost, uri).Call(ctx, param, &result)
var (
    eClient callapi.EClient
    eFormat callapi.EFormat
)
if err != nil {
    switch {
    case errors.As(err, &eClient):
        log.Fatal("failed to send request:", err)
    case errors.As(err, &eFormat):
        log.Fatal("failed to parse response:", err)
    default:
        log.Fatal("API returns error:", err)
    }
}
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

Mozilla Public License Version 2.0

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.E(err, jsonapi.E400.SetData("invalid parameter"))
}
if err := param.IsValid(); err != nil {
    return jsonapi.E(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.

As Go1.21, http.ServeMux is more feature-rich: you can bind http handler to specified http method. But it's too complicated to support this good cool feature here.

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.
callapi
Package callapi provides simple way to call remote api server which is written with jsonapi package.
Package callapi provides simple way to call remote api server which is written with jsonapi package.

Jump to

Keyboard shortcuts

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