jas

package module
v0.0.0-...-e8ccaf9 Latest Latest
Warning

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

Go to latest
Published: Apr 6, 2015 License: MIT Imports: 17 Imported by: 15

README

JAS

JAS (JSON API Server) is a simple and powerful REST API framework for Go. 中文版 README

Build Status Build Status

Requirement

Require Go 1.1+.

Features

  • No need to manually define any url routing rules, the rules are defined by your resource struct names and method names. No more inconsistencies between your url path and your method name.

  • Generate all the handled url paths seperated by "\n", so it can be used for reference or detect api changes.

  • Unobtrusive, JAS router is just a http.Handler, you can make it work with other http.Handlers as well as have multiple JAS routers on the same server.

  • Support HTTP Streaming, you can keep an connection open and send real-time data to the client, and get notified when the connection is closed on the client side.

  • Support extract parameters from json request body in any level of depth, so it can be used like JSON RPC.

  • Get and validate request parameter at the same time, support validate a integer, a string's min and max length or rune length, and match a regular expression.

  • Generate default response with the parameter name when validation failed, optionally log the error in Common Log Format.

  • Wrap all unhandled error into InternalError type, write response to the client with default message, and log the stacktraces and request infomation in Common Log Format. and provide a optional function parameter to do the extra error handlling work.

  • Provide an interface to handle errors, so you can define your own error type if the two default implementation can not meet your requirement.

  • Support gzip.

  • Highly configuarable.

Performance

JAS is a thin layer on top of net/http package, it adds about 1000ns operation time on every request, which means 99% of the performance when the qps number is around 10000.

But JAS will be faster than any regular expression routing solution. a single regular experssion match operation usually takes about 1000ns.

JAS router do not use regular expression, the routing performance would be constant as you define more resource and methods.

Install

go get github.com/coocood/jas

Only depends on a small assert package github.com/coocood/assrt for testing.

Get Started

Define a struct type and its methods, methods should have one argument of type *jas.Context, no return value.

type Hello struct {}

func (*Hello) Get (ctx *jas.Context) { // `GET /v1/hello`
	ctx.Data = "hello world"
	//response: `{"data":"hello world","error":null}`
}

func main () {
    router := jas.NewRouter(new(Hello))
    router.BasePath = "/v1/"
    fmt.Println(router.HandledPaths(true))
    //output: `GET /v1/hello`
    http.Handle(router.BasePath, router)
    http.ListenAndServe(":8080", nil)
}

Documentation

See Gowalker or godoc for complete documentation.

LICENSE

JAS is distributed under the terms of the MIT License. See LICENSE for details.

Contributiors

Jacob Olsen, doun, Olav

Documentation

Overview

To build a REST API you need to define resources.

A resource is a struct with one or more exported pointer methods that accept only one argument of type `*jas.Context`

A `*jas.Context` has everything you need to handle the http request, it embeds a anonymous *http.Request field, so you can call *http.Requst methods directly with *jas.Context.

The resource name and method name will be converted to snake case in the url path by default.(can be changed in config).

HTTP GET POST PUT DELETE should be the prefix of the method name.

Methods with no prefix will handle GET request.

Other HTTP request with methods like "HEAD", "OPTIONS" will be routed to resource "Get" method.

Examples:

	type Users struct {}

    func (*Users) Photo (ctx *jas.Context) {} // `GET /users/photo`

    func (*Users) PostPhoto (ctx *jas.Context) {} // `POST /users/photo`

    func (*Users) PostPost (ctx *jas.Context) {} // `POST /users/post`

    func (*Users) GetPost (ctx *jas.Context) {} // `GET /users/post`

    func (*Users) PutPhoto (ctx *jas.Context) {} // `PUT /users/photo`

    func (*Users) DeletePhoto (ctx *jas.Context) {} // `DELETE /users/photo`

On your application start, make a new *jas.Router with jas.NewRouter method, pass all your resources to it.

router := jas.NewRouter(new(Users), new(Posts), new(Photos)

Then you can config the router, see Config type for detail.

    router.BasePath = "/v1/"
	router.EnableGzip = true

You can get all the handled path printed. they are separated by '\n'

fmt.Println(router.HandledPaths(true)) // true for with base path. false for without base path.

Finally, set the router as http handler and Listen.

	http.Handle(router.BasePath, router)
    http.ListenAndServe(":8080", nil)

You can make it more RESTful by put an Id path between the resource name and method name.

The id value can be obtained from *jas.Context, resource name with `Id` suffix do the trick.

type UsersId struct {}

func (*UsersId) Photo (ctx *jas.Context) {// `GET /users/:id/photo`
    id := ctx.Id
    _ = id
}

If resource implements ResourceWithGap interface, the handled path will has gap segments between resource name and method name.

If method has a suffix "Id", the handled path will append an `:id` segment after the method segment.

You can obtain the Id value from *jas.Context, but it only works with none Id resource, because there is only one Id field in *jas.Context.

	type Users struct {}

	func (*Users) Gap() string {
		return ":name"
	}

    func (*Users) Photo (ctx *jas.Context) {// `GET /users/:name/photo`
    	// suppose the request path is `/users/john/photo`
        name := ctx.GapSegment("") // "john"
        _ = name
    }

    func (*Users) PhotoId (ctx *jas.Context) { `GET /users/:name/photo/:id`
    	// suppose the request path is `/users/john/photo/7`
    	id := ctx.Id // 7
        _ = id
    }

Find methods will return error if the parameter value is invalid. Require methods will stop the execution in the method and respond an error message if the parameter value is invalid.

func (*Users) Photo (ctx *jas.Context) {
    // will stop execution and response `{"data":null,"error":"nameInvalid"} if "name" parameter is not given..
    name := ctx.RequireString("name")
    age := ctx.RequirePositiveInt("age")
    grade, err := ctx.FindPositiveInt("grade")

    // 6, 60 is the min and max length, error message can be "passwordTooShort" or "passwordTooLong"
    password := ctx.RequireStringLen(6, 60, "password")

    // emailRegexp is a *regexp.Regexp instance.error message would be "emailInvalid"
    email := ctx.RequireStringMatch(emailRegexp, "email")
    _, _, _, _, _, _ = name, age, grade, err,password, email
}

Get json body parameter: Assume we have a request with json body

{
    "photo":[
        {"name":"abc"},
        {"id":200}
    ]
}

Then we can get the value with Find or Require methods. Find and Require methods accept varargs, the type can be either string or int. string argument to get value from json object, int argumnet to get value form json array.

func (*Users) Bar (ctx *jas.Context) {
    name, _ := ctx.Find("photo", 0, "name") // "abc"
    id:= ctx.RequirePositiveInt("photo", 1, "id") //200
}

If you want unmarshal json body to struct, the `DisableAutoUnmarshal` option must be set to true.

router.DisableAutoUnmarshal = true

Then you can call `Unmarshal` method to unmarshal json body:

ctx.Unmarshal(&myStruct)

HTTP streaming :

FlushData will write []byte data without any modification, other data types will be marshaled to json format.

func (*Users) Feed (ctx *jas.Context) {
    for !ctx.ClientClosed() {
        ctx.FlushData([]byte("some data"))
        time.Sleep(time.Second)
    }
}

Index

Constants

This section is empty.

Variables

View Source
var DoNotMatchError = errors.New("jas.Finder: do not match")
View Source
var EmptyMapError = errors.New("jas.Finder: empty map")
View Source
var EmptySliceError = errors.New("jas.Finder: empty slice")
View Source
var EmptyStringError = errors.New("jas.Finder: empty string")
View Source
var EntryNotExistsError = errors.New("jas.Finder: entry not exists")
View Source
var IndexOutOfBoundError = errors.New("jas.Finder: index out of bound")
View Source
var InternalErrorStatusCode = 500
View Source
var InvalidErrorFormat = "%vInvalid"
View Source
var MalformedJsonBody = "MalformedJsonBody"
View Source
var NoJsonBody = errors.New("jas.Context: no json body")
View Source
var NotFoundError = RequestError{"Not Found", 404}
View Source
var NotFoundStatusCode = 404
View Source
var NotPositiveError = errors.New("jas.Finder: not positive")
View Source
var NotPositiveErrorFormat = "%vNotPositive"
View Source
var NullValueError = errors.New("jas.Finder: null value")
View Source
var RequestErrorStatusCode = 400
View Source
var StackFormat = "%s:%d(0x%x);"

Stack trace format which formats file name, line number and program counter.

View Source
var TooLongError = errors.New("jas.Finder: string too long")
View Source
var TooLongErrorFormat = "%vTooLong"
View Source
var TooShortError = errors.New("jas.Finder: string too short")
View Source
var TooShortErrorFormat = "%vTooShort"
View Source
var UnauthorizedStatusCode = 401
View Source
var WordSeparator = "_"
View Source
var WrongTypeError = errors.New("jas.Finder: wrong type")

Functions

func AllowCORS

func AllowCORS(r *http.Request, responseHeader http.Header) bool

This is an implementation of HandleCORS function to allow all cross domain request.

func NameValuesToUrlValues

func NameValuesToUrlValues(nameValues ...interface{}) url.Values

func NewGetRequest

func NewGetRequest(baseUrlOrPath, path string, nameValues ...interface{}) *http.Request

func NewPostFormRequest

func NewPostFormRequest(baseUrlOrPath, path string, nameValues ...interface{}) *http.Request

func NewPostJsonRequest

func NewPostJsonRequest(baseUrlOrPath, path string, jsonData []byte, nameValues ...interface{}) *http.Request

Types

type AppError

type AppError interface {

	//The actual error string that will be logged.
	Error() string

	//The status code that will be written to the response header.
	Status() int

	//The error message response to the client.
	//Can be the same string as Error() for request error
	//Should be simple string like "InternalError" for internal error.
	Message() string

	//Log self, it will be called after response is written to the client.
	//It runs on its own goroutine, so long running task will not affect the response time.
	Log(*Context)
}

If RequestError and internalError is not sufficient for you application, you can implement this interface to define your own error that can log itself in different way..

type Assert

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

func NewAssert

func NewAssert(t tester) *Assert

func (*Assert) Equal

func (ast *Assert) Equal(expected, actual interface{}, logs ...interface{})

func (*Assert) MustEqual

func (ast *Assert) MustEqual(expected, actual interface{}, logs ...interface{})

func (*Assert) MustNil

func (ast *Assert) MustNil(value interface{}, logs ...interface{})

func (*Assert) MustNotEqual

func (ast *Assert) MustNotEqual(expected, actual interface{}, logs ...interface{})

func (*Assert) MustNotNil

func (ast *Assert) MustNotNil(value interface{}, logs ...interface{})

func (*Assert) MustTrue

func (ast *Assert) MustTrue(boolValue bool, logs ...interface{})

func (*Assert) Nil

func (ast *Assert) Nil(value interface{}, logs ...interface{})

func (*Assert) NotEqual

func (ast *Assert) NotEqual(expected, actual interface{}, logs ...interface{})

func (*Assert) NotNil

func (ast *Assert) NotNil(value interface{}, logs ...interface{})

func (*Assert) True

func (ast *Assert) True(boolValue bool, logs ...interface{})

type Config

type Config struct {
	//The base path of the request url.
	//If you want to make it work along with other router or http.Handler,
	//it can be used as a pattern string for `http.Handle` method
	//It must starts and ends with "/", e.g. "/api/v1/".
	//Defaults to "/".
	BasePath string

	//Handle Cross-origin Resource Sharing.
	//It accept request and response header parameter.
	//return true to go on handle the request, return false to stop handling and response with header only.
	//Defaults to nil
	//You can set it to AllowCORS function to allow all CORS request.
	HandleCORS func(*http.Request, http.Header) bool

	//gzip is disabled by default. set true to enable it
	EnableGzip bool

	//defaults to nil, if set, request error will be logged.
	RequestErrorLogger *log.Logger

	//log to standard err by default.
	InternalErrorLogger *log.Logger

	//If set, it will be called after recovered from panic.
	//Do time consuming work in the function will not increase response time because it runs in its own goroutine.
	OnAppError func(AppError, *Context)

	//If set, it will be called before calling the matched method.
	BeforeServe func(*Context)

	//If set, it will be called after calling the matched method.
	AfterServe func(*Context)

	//If set, the user id can be obtained by *Context.UserId and will be logged on error.
	//Implementations can be like decode cookie value or token parameter.
	ParseIdFunc func(*http.Request) int64

	//If set, the delimiter will be appended to the end of the data on every call to *Context.FlushData method.
	FlushDelimiter []byte

	//handler function for unhandled path request.
	//default function just send `{"data":null,"error":"NotFound"}` with 404 status code.
	OnNotFound func(http.ResponseWriter, *http.Request)

	//if you do not like the default json format `{"data":...,"error":...}`,
	//you can define your own write function here.
	//The io.Writer may be http.ResponseWriter or GzipWriter depends on if gzip is enabled.
	//The errMessage is of type string or nil, it's not AppError.
	//it should return the number of bytes has been written.
	HijackWrite func(io.Writer, *Context) int

	//If set to true, json request body will not be unmarshaled in Finder automatically.
	//Then you will be able to call `Unmarshal` to unmarshal the body to a struct.
	//If you still want to get body parameter with Finder methods in some cases, you can call `UnmarshalInFinder`
	//explicitly before you get body parameters with Finder methods.
	DisableAutoUnmarshal bool

	//By default gap only matches non-integer segment, set true to allow gap to match integer segment.
	//But then resource with gap will shadow id resource.
	//e.g "/user/123" will be resolved to "User" that has "Gap" method instead of "UserId".
	AllowIntegerGap bool
}

type Context

type Context struct {
	Finder
	*http.Request
	ResponseHeader http.Header
	Callback       string //jsonp callback
	Status         int
	Error          AppError
	Data           interface{} //The data to be written after the resource method has returned.
	UserId         int64
	Id             int64
	Extra          interface{} //Store extra data generated/used by hook functions, e.g. 'BeforeServe'.
	// contains filtered or unexported fields
}

Context contains all the information for a single request. it hides the http.ResponseWriter because directly writing to http.ResponseWriter will make Context unable to work correctly. it embeds *http.Request and Finder, so you can call methods and access fields in Finder and *http.Request directly.

func (*Context) AddCookie

func (ctx *Context) AddCookie(cookie *http.Cookie)

override *http.Request AddCookie method to add response header's cookie. Same as SetCookie.

func (*Context) ClientClosed

func (ctx *Context) ClientClosed() bool

Typically used in for loop condition.along with Flush.

func (*Context) FlushData

func (ctx *Context) FlushData(data interface{}) (written int, err error)

Write and flush the data. It can be used for http streaming or to write a portion of large amount of data. If the type of the data is not []byte, it will be marshaled to json format.

func (*Context) GapSegment

func (ctx *Context) GapSegment(key string) string

If the gap has multiple segments, the key should be the segment defined in resource Gap method. e.g. for gap ":domain/:language", use key ":domain" to get the first gap segment, use key ":language" to get the second gap segment. The first gap segment can also be gotten by empty string key "" for convenience.

func (*Context) PathSegment

func (ctx *Context) PathSegment(index int) string

the segment index starts at the resource segment

func (*Context) RequireUserId

func (ctx *Context) RequireUserId() int64

It is an convenient method to validate and get the user id.

func (*Context) SetCookie

func (ctx *Context) SetCookie(cookie *http.Cookie)

Add response header Set-Cookie.

func (*Context) Unmarshal

func (ctx *Context) Unmarshal(in interface{}) error

Unmarshal the request body into the interface. It only works when you set Config option `DisableAutoUnmarshal` to true.

func (*Context) UnmarshalInFinder

func (ctx *Context) UnmarshalInFinder()

If set Config option `DisableAutoUnmarshal` to true, you should call this method first before you can get body parameters in Finder methods..

type ContextWriter

type ContextWriter struct {
	Ctx *Context
}

func (ContextWriter) Write

func (cw ContextWriter) Write(p []byte) (n int, err error)

type Finder

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

All the "Find" methods return error, All the "Require" methods do panic with RequestError when error occured.

func FinderWithBytes

func FinderWithBytes(data []byte) Finder

Construct a Finder with json formatted data.

func FinderWithRequest

func FinderWithRequest(req *http.Request) Finder

Construct a Finder with *http.Request.

func (Finder) FindBool

func (finder Finder) FindBool(paths ...interface{}) (bool, error)

func (Finder) FindChild

func (finder Finder) FindChild(paths ...interface{}) Finder

func (Finder) FindFloat

func (finder Finder) FindFloat(paths ...interface{}) (float64, error)

func (Finder) FindInt

func (finder Finder) FindInt(paths ...interface{}) (int64, error)

func (Finder) FindMap

func (finder Finder) FindMap(paths ...interface{}) (map[string]interface{}, error)

func (Finder) FindOptionalString

func (finder Finder) FindOptionalString(val string, paths ...interface{}) (string, error)

Looks up the given path and returns a default value if not present.

func (Finder) FindPositiveInt

func (finder Finder) FindPositiveInt(paths ...interface{}) (int64, error)

func (Finder) FindSlice

func (finder Finder) FindSlice(paths ...interface{}) ([]interface{}, error)

func (Finder) FindString

func (finder Finder) FindString(paths ...interface{}) (string, error)

func (Finder) FindStringLen

func (finder Finder) FindStringLen(min, max int, paths ...interface{}) (string, error)

func (Finder) FindStringMatch

func (finder Finder) FindStringMatch(reg *regexp.Regexp, paths ...interface{}) (string, error)

func (Finder) FindStringRuneLen

func (finder Finder) FindStringRuneLen(min, max int, paths ...interface{}) (string, error)

func (Finder) Len

func (finder Finder) Len(paths ...interface{}) int

return the length of []interface or map[string]interface{} return -1 if the value not found or has wrong type.

func (Finder) RequireFloat

func (finder Finder) RequireFloat(paths ...interface{}) float64

func (Finder) RequireInt

func (finder Finder) RequireInt(paths ...interface{}) int64

func (Finder) RequireMap

func (finder Finder) RequireMap(paths ...interface{}) map[string]interface{}

func (Finder) RequirePositiveFloat

func (finder Finder) RequirePositiveFloat(paths ...interface{}) float64

func (Finder) RequirePositiveInt

func (finder Finder) RequirePositiveInt(paths ...interface{}) int64

func (Finder) RequireSlice

func (finder Finder) RequireSlice(paths ...interface{}) []interface{}

func (Finder) RequireString

func (finder Finder) RequireString(paths ...interface{}) string

func (Finder) RequireStringLen

func (finder Finder) RequireStringLen(min, max int, paths ...interface{}) string

func (Finder) RequireStringMatch

func (finder Finder) RequireStringMatch(reg *regexp.Regexp, paths ...interface{}) string

func (Finder) RequireStringRuneLen

func (finder Finder) RequireStringRuneLen(min, max int, paths ...interface{}) string

type InternalError

type InternalError struct {
	Err        error
	StatusCode int
}

InternalError is an AppError implementation which returns "InternalError" message to the client and logs the wrapped error and stack trace in Common Log Format. any panic during a session will be recovered and wrapped in InternalError and then logged.

func NewInternalError

func NewInternalError(err interface{}) InternalError

Wrap an error to InternalError

func (InternalError) Error

func (ie InternalError) Error() string

func (InternalError) Log

func (ie InternalError) Log(context *Context)

func (InternalError) Message

func (ie InternalError) Message() string

func (InternalError) Status

func (ie InternalError) Status() int

type RequestError

type RequestError struct {
	Msg        string
	StatusCode int
}

RequestError is an AppError implementation which Error() and Message() returns the same string.

func NewRequestError

func NewRequestError(message string) RequestError

Make an RequestError with message which will be sent to the client.

func (RequestError) Error

func (re RequestError) Error() string

func (RequestError) Log

func (re RequestError) Log(context *Context)

func (RequestError) Message

func (re RequestError) Message() string

func (RequestError) Status

func (re RequestError) Status() int

type ResourceWithGap

type ResourceWithGap interface {
	Gap() string
}

type Response

type Response struct {
	Data  interface{} `json:"data"`
	Error interface{} `json:"error"`
}

type Router

type Router struct {
	*Config
	// contains filtered or unexported fields
}

func NewRouter

func NewRouter(resources ...interface{}) *Router

Construct a Router instance. Then you can set the configuration fields to config the router. Configuration fields applies to a single router, there are also some package level variables you can change if needed. You can make multiple routers with different base path to handle requests to the same host. See documentation about resources at the top of the file.

func (*Router) HandledPaths

func (r *Router) HandledPaths(withBasePath bool) string

Get the paths that have been handled by resources. The paths are sorted, it can be used to detect api path changes.

func (*Router) ServeHTTP

func (router *Router) ServeHTTP(w http.ResponseWriter, r *http.Request)

Implements http.Handler interface.

Jump to

Keyboard shortcuts

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