diff --git a/.gitignore b/.gitignore index f1c181e..bcaaa39 100644 --- a/.gitignore +++ b/.gitignore @@ -10,3 +10,8 @@ # Output of the go coverage tool, specifically when used with LiteIDE *.out + +# editors +.idea +.DS_Store +.vscode \ No newline at end of file diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..bbe327c --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2019 Roshan Gade + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md index 125023a..77f651a 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,16 @@ REST API framework for go lang # Framework is under development ## Status: -- Working on POC as per concept +Released alpha version +
+See examples + - Request Interceptors/Middlewares + - Routes with URL pattern + - Methods [GET, POST, PUT, DELETE, OPTIONS, HEAD, PATCH] + - Extend routes with namespace + - Error handler + - HTTP, HTTPS support + ``` var api rest.API @@ -13,22 +22,22 @@ api.Use(func(ctx *rest.Context) { }) // routes -api.GET("/", func(ctx *rest.Context) { - ctx.Send("Hello World!") +api.Get("/", func(ctx *rest.Context) { + ctx.Text("Hello World!") }) -api.GET("/foo", func(ctx *rest.Context) { +api.Get("/foo", func(ctx *rest.Context) { ctx.Status(401).Throw(errors.New("UNAUTHORIZED")) }) -api.GET("/:bar", func(ctx *rest.Context) { +api.Get("/:bar", func(ctx *rest.Context) { fmt.Println("authtoken", ctx.Get("authtoken")) - ctx.SendJSON(ctx.Params) + ctx.JSON(ctx.Params) }) // error handler api.Error("UNAUTHORIZED", func(ctx *rest.Context) { - ctx.Send("You are unauthorized") + ctx.Text("You are unauthorized") }) fmt.Println("Starting server.") diff --git a/api.go b/api.go new file mode 100644 index 0000000..cf497b8 --- /dev/null +++ b/api.go @@ -0,0 +1,188 @@ +/*! + * rest-api-framework + * Copyright(c) 2019 Roshan Gade + * MIT Licensed + */ +package rest + +import ( + "errors" + "fmt" + "github.com/go-rs/rest-api-framework/utils" + "net/http" + "regexp" +) + +type Handler func(ctx *Context) + +/** + * API - Application + */ +type API struct { + prefix string + routes []route + interceptors []interceptor + exceptions []exception + unhandled Handler +} + +/** + * Route + */ +type route struct { + method string + pattern string + regex *regexp.Regexp + params []string + handle Handler +} + +/** + * Request interceptor + */ +type interceptor struct { + handle Handler +} + +/** + * Exception Route + */ +type exception struct { + message string + handle Handler +} + +/** + * Common Route + */ +func (api *API) Route(method string, pattern string, handle Handler) { + regex, params, err := utils.Compile(pattern) + if err != nil { + fmt.Println("Error in pattern", err) + panic(1) + } + api.routes = append(api.routes, route{ + method: method, + pattern: pattern, + regex: regex, + params: params, + handle: handle, + }) +} + +/** + * Required handle for http module + */ +func (api API) ServeHTTP(res http.ResponseWriter, req *http.Request) { + + urlPath := []byte(req.URL.Path) + + ctx := Context{ + Request: req, + Response: res, + } + + // STEP 1: initialize context + ctx.init() + defer ctx.destroy() + + // STEP 2: execute all interceptors + for _, task := range api.interceptors { + if ctx.end || ctx.err != nil { + break + } + + task.handle(&ctx) + } + + // STEP 3: check routes + for _, route := range api.routes { + if ctx.end || ctx.err != nil { + break + } + + if (route.method == "" || route.method == req.Method) && route.regex.Match(urlPath) { + ctx.found = route.method != "" //? + ctx.Params = utils.Exec(route.regex, route.params, urlPath) + route.handle(&ctx) + } + } + + // STEP 4: check handled exceptions + for _, exp := range api.exceptions { + if ctx.end || ctx.err == nil { + break + } + + if exp.message == ctx.err.Error() { + exp.handle(&ctx) + } + } + + // STEP 5: unhandled exceptions + if !ctx.end { + if ctx.err == nil && !ctx.found { + ctx.err = errors.New("URL_NOT_FOUND") + } + + if api.unhandled != nil { + api.unhandled(&ctx) + } + } + + // STEP 6: system handle + if !ctx.end { + ctx.unhandledException() + } +} + +func (api *API) Use(handle Handler) { + task := interceptor{ + handle: handle, + } + api.interceptors = append(api.interceptors, task) +} + +func (api *API) All(pattern string, handle Handler) { + api.Route("", pattern, handle) +} + +func (api *API) Get(pattern string, handle Handler) { + api.Route("GET", pattern, handle) +} + +func (api *API) Post(pattern string, handle Handler) { + api.Route("POST", pattern, handle) +} + +func (api *API) Put(pattern string, handle Handler) { + api.Route("PUT", pattern, handle) +} + +func (api *API) Delete(pattern string, handle Handler) { + api.Route("DELETE", pattern, handle) +} + +func (api *API) Options(pattern string, handle Handler) { + api.Route("OPTIONS", pattern, handle) +} + +func (api *API) Head(pattern string, handle Handler) { + api.Route("HEAD", pattern, handle) +} + +func (api *API) Patch(pattern string, handle Handler) { + api.Route("PATCH", pattern, handle) +} + +func (api *API) Exception(err string, handle Handler) { + exp := exception{ + message: err, + handle: handle, + } + api.exceptions = append(api.exceptions, exp) +} + +func (api *API) UnhandledException(handle Handler) { + api.unhandled = handle +} diff --git a/context.go b/context.go new file mode 100644 index 0000000..aeee4fa --- /dev/null +++ b/context.go @@ -0,0 +1,162 @@ +/*! + * rest-api-framework + * Copyright(c) 2019 Roshan Gade + * MIT Licensed + */ +package rest + +import ( + "github.com/go-rs/rest-api-framework/render" + "net/http" +) + +/** + * Context + */ +type Context struct { + Request *http.Request + Response http.ResponseWriter + Params *map[string]string + data map[string]interface{} + err error + status int + found bool + end bool +} + +/** + * Initialization of context on every request + */ +func (ctx *Context) init() { + ctx.data = make(map[string]interface{}) + ctx.status = 200 + ctx.found = false + ctx.end = false +} + +/** + * Destroy context once request end + */ +func (ctx *Context) destroy() { + ctx.Request = nil + ctx.Response = nil + ctx.Params = nil + ctx.data = nil + ctx.err = nil + ctx.status = 0 + ctx.found = false + ctx.end = false +} + +/** + * Set request data in context + */ +func (ctx *Context) Set(key string, val interface{}) { + ctx.data[key] = val +} + +/** + * Get request data from context + */ +func (ctx *Context) Get(key string) interface{} { + return ctx.data[key] +} + +/** + * Set Status + */ +func (ctx *Context) Status(code int) *Context { + ctx.status = code + return ctx +} + +/** + * Set Header + */ +func (ctx *Context) SetHeader(key string, val string) *Context { + ctx.Response.Header().Set(key, val) + return ctx +} + +/** + * Throw error + */ +func (ctx *Context) Throw(err error) { + ctx.err = err +} + +/** + * Get error + */ +func (ctx *Context) GetError() error { + return ctx.err +} + +/** + * End + */ +func (ctx *Context) End() { + ctx.end = true +} + +/** + * Write Bytes + */ +func (ctx *Context) Write(data []byte) { + ctx.send(data, nil) +} + +/** + * Write JSON + */ +func (ctx *Context) JSON(data interface{}) { + json := render.JSON{ + Body: data, + } + body, err := json.Write(ctx.Response) + ctx.send(body, err) +} + +/** + * Write Text + */ +func (ctx *Context) Text(data string) { + txt := render.Text{ + Body: data, + } + body, err := txt.Write(ctx.Response) + ctx.send(body, err) +} + +////////////////////////////////////////////////// +/** + * Send data + */ +func (ctx *Context) send(data []byte, err error) { + if ctx.end && err != nil { + return + } + ctx.Response.WriteHeader(ctx.status) + _, err = ctx.Response.Write(data) + if err != nil { + ctx.err = err + return + } + + ctx.End() +} + +/** + * Unhandled Exception + */ +func (ctx *Context) unhandledException() { + err := ctx.GetError() + if err != nil { + msg := err.Error() + ctx.Status(500) + if msg == "URL_NOT_FOUND" { + ctx.Status(404) + } + ctx.Write([]byte(msg)) + } +} diff --git a/examples/server.go b/examples/server.go new file mode 100644 index 0000000..f440add --- /dev/null +++ b/examples/server.go @@ -0,0 +1,40 @@ +package main + +import ( + ".." + "errors" + "fmt" + "github.com/go-rs/rest-api-framework/examples/user" + "net/http" +) + +func main() { + var api rest.API + + user.APIs(&api) + + // request interceptor / middleware + // body-parser : json, raw, form-data, etc + // security + api.Use(func(ctx *rest.Context) { + ctx.Set("authtoken", "roshangade") + }) + + // routes + api.Get("/", func(ctx *rest.Context) { + ctx.JSON(`{"message": "Hello World!"}`) + }) + + api.Get("/foo", func(ctx *rest.Context) { + ctx.Throw(errors.New("UNAUTHORIZED")) + }) + + // error handler + api.Exception("UNAUTHORIZED", func(ctx *rest.Context) { + ctx.Status(401).JSON(`{"message": "You are unauthorized"}`) + }) + + fmt.Println("Starting server.") + + http.ListenAndServe(":8080", api) +} diff --git a/examples/user/user.go b/examples/user/user.go new file mode 100644 index 0000000..54af9e1 --- /dev/null +++ b/examples/user/user.go @@ -0,0 +1,24 @@ +package user + +import ( + "../.." +) + +func APIs(api *rest.API) { + + var user rest.Namespace + + user.Set("/user", api) + + user.Use(func(ctx *rest.Context) { + println("User middleware > /user/*") + }) + + user.Get("/:uid/profile", func(ctx *rest.Context) { + ctx.JSON(`{"user": "profile"}`) + }) + + user.Get("/:uid", func(ctx *rest.Context) { + ctx.JSON(ctx.Params) + }) +} diff --git a/namespace.go b/namespace.go new file mode 100644 index 0000000..fce0a96 --- /dev/null +++ b/namespace.go @@ -0,0 +1,64 @@ +/*! + * rest-api-framework + * Copyright(c) 2019 Roshan Gade + * MIT Licensed + */ +package rest + +/** + * Namespace - Application + */ +type Namespace struct { + prefix string + api *API +} + +//TODO: error handling on unset api +func (n *Namespace) Set(prefix string, api *API) { + n.prefix = prefix + n.api = api +} + +func (n *Namespace) Use(handle Handler) { + n.api.Route("", n.prefix+"/*", handle) +} + +func (n *Namespace) All(pattern string, handle Handler) { + n.api.Route("", n.prefix+pattern, handle) +} + +func (n *Namespace) Get(pattern string, handle Handler) { + n.api.Route("GET", n.prefix+pattern, handle) +} + +func (n *Namespace) Post(pattern string, handle Handler) { + n.api.Route("POST", n.prefix+pattern, handle) +} + +func (n *Namespace) Put(pattern string, handle Handler) { + n.api.Route("PUT", n.prefix+pattern, handle) +} + +func (n *Namespace) Delete(pattern string, handle Handler) { + n.api.Route("DELETE", n.prefix+pattern, handle) +} + +func (n *Namespace) Options(pattern string, handle Handler) { + n.api.Route("OPTIONS", n.prefix+pattern, handle) +} + +func (n *Namespace) Head(pattern string, handle Handler) { + n.api.Route("HEAD", n.prefix+pattern, handle) +} + +func (n *Namespace) Patch(pattern string, handle Handler) { + n.api.Route("PATCH", n.prefix+pattern, handle) +} + +func (n *Namespace) Exception(err string, handle Handler) { + exp := exception{ + message: err, + handle: handle, + } + n.api.exceptions = append(n.api.exceptions, exp) +} diff --git a/render/json.go b/render/json.go new file mode 100644 index 0000000..66c4732 --- /dev/null +++ b/render/json.go @@ -0,0 +1,40 @@ +/*! + * rest-api-framework + * Copyright(c) 2019 Roshan Gade + * MIT Licensed + */ +package render + +import ( + "encoding/json" + "net/http" + "reflect" +) + +//TODO: JSONP +type JSON struct { + Body interface{} +} + +var ( + jsonType = "application/json" +) + +/** + * JSON Write + */ +func (j JSON) Write(w http.ResponseWriter) ([]byte, error) { + var data []byte + var err error + if reflect.TypeOf(j.Body).String() == "string" { + rawIn := json.RawMessage(j.Body.(string)) + data, err = rawIn.MarshalJSON() + } else { + data, err = json.Marshal(j.Body) + } + if err != nil { + return nil, err + } + w.Header().Set("Content-Type", jsonType) + return data, nil +} diff --git a/render/text.go b/render/text.go new file mode 100644 index 0000000..8b12d34 --- /dev/null +++ b/render/text.go @@ -0,0 +1,27 @@ +/*! + * rest-api-framework + * Copyright(c) 2019 Roshan Gade + * MIT Licensed + */ +package render + +import ( + "net/http" +) + +type Text struct { + Body string +} + +var ( + plainType = "text/plain" +) + +/** + * Text Write + */ +func (j Text) Write(w http.ResponseWriter) ([]byte, error) { + data := []byte(j.Body) + w.Header().Set("Content-Type", plainType) + return data, nil +} diff --git a/utils/url.go b/utils/url.go new file mode 100644 index 0000000..7c57304 --- /dev/null +++ b/utils/url.go @@ -0,0 +1,68 @@ +/*! + * rest-api-framework + * Copyright(c) 2019 Roshan Gade + * MIT Licensed + */ +package utils + +import ( + "regexp" + "strings" +) + +const sep = "/" + +func Compile(str string) (*regexp.Regexp, []string, error) { + pattern := "" + keys := make([]string, 0) + _str := strings.Split(str, "/") + + for _, val := range _str { + if val != "" { + switch val[0] { + case 42: + pattern += "(?:/(.*))" + keys = append(keys, "*") + + case 58: + length := len(val) + lastChar := val[length-1] + if lastChar == 63 { + pattern += "(?:/([^/]+?))?" + keys = append(keys, val[1:(length-1)]) + } else { + pattern += sep + "([^/]+?)" + keys = append(keys, val[1:]) + } + + default: + pattern += sep + val + } + } + } + + // if len(keys) == 0 { + // pattern += "(?:/)?" + // } + + regex, err := regexp.Compile("^" + pattern + "/?$") + + return regex, keys, err +} + +func Exec(regex *regexp.Regexp, keys []string, uri []byte) *map[string]string { + params := make(map[string]string) + + matches := regex.FindAllSubmatch(uri, -1) + + for _, val := range matches { + for i, k := range val[1:] { + params[keys[i]] = string(k) + if keys[i] == "*" { + params["*"] = sep + params["*"] + } + } + } + + return ¶ms +} diff --git a/version.txt b/version.txt new file mode 100644 index 0000000..d75fc87 --- /dev/null +++ b/version.txt @@ -0,0 +1 @@ +0.0.1-alpha.1 \ No newline at end of file