jas: github.com/coocood/jas Index | Files

package jas

import "github.com/coocood/jas"

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

Package Files

assert.go context.go doc.go error.go finder.go request.go router.go

Variables

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

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

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

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

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) 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.

Package jas imports 17 packages (graph) and is imported by 6 packages. Updated 2014-01-15. Refresh now. Tools for package owners.