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 ¶
- Variables
- func AllowCORS(r *http.Request, responseHeader http.Header) bool
- func NameValuesToUrlValues(nameValues ...interface{}) url.Values
- func NewGetRequest(baseUrlOrPath, path string, nameValues ...interface{}) *http.Request
- func NewPostFormRequest(baseUrlOrPath, path string, nameValues ...interface{}) *http.Request
- func NewPostJsonRequest(baseUrlOrPath, path string, jsonData []byte, nameValues ...interface{}) *http.Request
- type AppError
- type Assert
- func (ast *Assert) Equal(expected, actual interface{}, logs ...interface{})
- func (ast *Assert) MustEqual(expected, actual interface{}, logs ...interface{})
- func (ast *Assert) MustNil(value interface{}, logs ...interface{})
- func (ast *Assert) MustNotEqual(expected, actual interface{}, logs ...interface{})
- func (ast *Assert) MustNotNil(value interface{}, logs ...interface{})
- func (ast *Assert) MustTrue(boolValue bool, logs ...interface{})
- func (ast *Assert) Nil(value interface{}, logs ...interface{})
- func (ast *Assert) NotEqual(expected, actual interface{}, logs ...interface{})
- func (ast *Assert) NotNil(value interface{}, logs ...interface{})
- func (ast *Assert) True(boolValue bool, logs ...interface{})
- type Config
- type Context
- func (ctx *Context) AddCookie(cookie *http.Cookie)
- func (ctx *Context) ClientClosed() bool
- func (ctx *Context) FlushData(data interface{}) (written int, err error)
- func (ctx *Context) GapSegment(key string) string
- func (ctx *Context) PathSegment(index int) string
- func (ctx *Context) RequireUserId() int64
- func (ctx *Context) SetCookie(cookie *http.Cookie)
- func (ctx *Context) Unmarshal(in interface{}) error
- func (ctx *Context) UnmarshalInFinder()
- type ContextWriter
- type Finder
- func (finder Finder) FindBool(paths ...interface{}) (bool, error)
- func (finder Finder) FindChild(paths ...interface{}) Finder
- func (finder Finder) FindFloat(paths ...interface{}) (float64, error)
- func (finder Finder) FindInt(paths ...interface{}) (int64, error)
- func (finder Finder) FindMap(paths ...interface{}) (map[string]interface{}, error)
- func (finder Finder) FindOptionalString(val string, paths ...interface{}) (string, error)
- func (finder Finder) FindPositiveInt(paths ...interface{}) (int64, error)
- func (finder Finder) FindSlice(paths ...interface{}) ([]interface{}, error)
- func (finder Finder) FindString(paths ...interface{}) (string, error)
- func (finder Finder) FindStringLen(min, max int, paths ...interface{}) (string, error)
- func (finder Finder) FindStringMatch(reg *regexp.Regexp, paths ...interface{}) (string, error)
- func (finder Finder) FindStringRuneLen(min, max int, paths ...interface{}) (string, error)
- func (finder Finder) Len(paths ...interface{}) int
- func (finder Finder) RequireFloat(paths ...interface{}) float64
- func (finder Finder) RequireInt(paths ...interface{}) int64
- func (finder Finder) RequireMap(paths ...interface{}) map[string]interface{}
- func (finder Finder) RequirePositiveFloat(paths ...interface{}) float64
- func (finder Finder) RequirePositiveInt(paths ...interface{}) int64
- func (finder Finder) RequireSlice(paths ...interface{}) []interface{}
- func (finder Finder) RequireString(paths ...interface{}) string
- func (finder Finder) RequireStringLen(min, max int, paths ...interface{}) string
- func (finder Finder) RequireStringMatch(reg *regexp.Regexp, paths ...interface{}) string
- func (finder Finder) RequireStringRuneLen(min, max int, paths ...interface{}) string
- type InternalError
- type RequestError
- type ResourceWithGap
- type Response
- type Router
Constants ¶
This section is empty.
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 WordSeparator = "_"
var WrongTypeError = errors.New("jas.Finder: wrong type")
Functions ¶
func AllowCORS ¶
This is an implementation of HandleCORS function to allow all cross domain request.
func NameValuesToUrlValues ¶
func NewGetRequest ¶
func NewPostFormRequest ¶
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 (*Assert) MustEqual ¶
func (ast *Assert) MustEqual(expected, actual 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{})
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 ¶
override *http.Request AddCookie method to add response header's cookie. Same as SetCookie.
func (*Context) ClientClosed ¶
Typically used in for loop condition.along with Flush.
func (*Context) FlushData ¶
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 ¶
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 ¶
the segment index starts at the resource segment
func (*Context) RequireUserId ¶
It is an convenient method to validate and get the user id.
func (*Context) Unmarshal ¶
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
}
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 ¶
Construct a Finder with json formatted data.
func FinderWithRequest ¶
Construct a Finder with *http.Request.
func (Finder) FindOptionalString ¶
Looks up the given path and returns a default value if not present.
func (Finder) FindPositiveInt ¶
func (Finder) FindString ¶
func (Finder) FindStringLen ¶
func (Finder) FindStringMatch ¶
func (Finder) FindStringRuneLen ¶
func (Finder) Len ¶
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) RequireInt ¶
func (Finder) RequireMap ¶
func (Finder) RequirePositiveFloat ¶
func (Finder) RequirePositiveInt ¶
func (Finder) RequireSlice ¶
func (finder Finder) RequireSlice(paths ...interface{}) []interface{}
func (Finder) RequireString ¶
func (Finder) RequireStringLen ¶
func (Finder) RequireStringMatch ¶
func (Finder) RequireStringRuneLen ¶
type InternalError ¶
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 ¶
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 ¶
Get the paths that have been handled by resources. The paths are sorted, it can be used to detect api path changes.