Skip to content

Commit

Permalink
0.2 groudwork done -- breaks 0.1 release
Browse files Browse the repository at this point in the history
  • Loading branch information
codekidX committed May 31, 2020
1 parent 3e6ec98 commit e2687c2
Show file tree
Hide file tree
Showing 9 changed files with 272 additions and 193 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@

### Welcome to Rubik (alpha)

Rubik is a efficient, scalable micro-framework for writing REST client and server-side applications. It provides a pluggable
Rubik is a efficient, scalable web framework for writing REST client and server-side applications faster. It provides a pluggable
layer of abstraction over `net/http` and enables automation of development environment through extensive tooling.

Even though the goal of Rubik is set it'll take a lot of time to achieve it, that being said you must
Expand Down
210 changes: 154 additions & 56 deletions app.go
Original file line number Diff line number Diff line change
@@ -1,16 +1,43 @@
// Package rubik is used for accessing Rubik Framework: a minimal and efficient web framework
// for Go and it's APIs.
//
// Running an empty server:
// package main
//
// import r "github.com/rubikorg/rubik"
//
// func main() {
// // this runs Rubik server on port: 8000
// panic(r.Run())
// }
//
// Adding a route:
//
// package main
//
// import r "github.com/rubikorg/rubik"
//
// func main() {
// // this runs Rubik server on port: 8000
// index := rubik.Route{
// Path: "/",
// Controller: func (d *r.Data) { d.Respond("This is a text response") },
// }
// rubik.UseRoute(index)
// panic(r.Run())
// }
package rubik

import (
"bytes"
"encoding/json"
"fmt"
"io/ioutil"
"net/http"
"os"
"path/filepath"
"reflect"
"strconv"
"strings"
"time"

"github.com/pkg/errors"

Expand Down Expand Up @@ -68,22 +95,29 @@ func (re RestErrorMixin) Error() string {
return re.Message
}

// Middleware intercepts user request and processes it
type Middleware func(req Request) interface{}

// Controller ...
type Controller func(entity interface{}) ByteResponse
type Controller func(*Request)

// Request ...
type Request struct {
app *rubik
Entity interface{}
Writer RResponseWriter
Params httprouter.Params
Raw *http.Request
Ctx map[string]interface{}
}

// RequestContext ...
type RequestContext struct {
// HookContext ...
type HookContext struct {
Request *http.Request
Ctx map[string]interface{}
Response interface{}
Response []byte
Status int
}

// RequestHook ...
type RequestHook func(*RequestContext)
type RequestHook func(*HookContext)

// Rubik is the instance of Server which holds all the necessary information of apis
type rubik struct {
Expand All @@ -103,13 +137,12 @@ type rubik struct {
}

// Request ...
type Request struct {
Raw *http.Request
Params httprouter.Params
ResponseHeader http.Header
app *rubik
entity interface{}
}
// type RequestP struct {
// Raw *http.Request
// Params httprouter.Params
// ResponseHeader http.Header
// entity interface{}
// }

// GetRouteTree returns a list of loaded routes in rubik
func (req Request) GetRouteTree() RouteTree {
Expand Down Expand Up @@ -137,7 +170,7 @@ type Route struct {
JSON bool
Entity interface{}
Guard AuthorizationGuard
Middlewares []Middleware
Middlewares []Controller
Validation Validation
Controller Controller
}
Expand All @@ -161,19 +194,19 @@ type RouteInfo struct {
}

// FromStorage returns the file bytes of a given fileName as response
func FromStorage(fileName string) ByteResponse {
var filePath = filepath.Join(".", "storage", fileName)
if _, err := os.Stat(filePath); os.IsNotExist(err) {
return Failure(500, errors.New("FileNotFoundError: "+fileName+" does not exist."))
}
// func FromStorage(fileName string) ByteResponse {
// var filePath = filepath.Join(".", "storage", fileName)
// if _, err := os.Stat(filePath); os.IsNotExist(err) {
// return Failure(500, errors.New("FileNotFoundError: "+fileName+" does not exist."))
// }

b, err := ioutil.ReadFile(filePath)
if err != nil {
return Failure(500, err)
}
// b, err := ioutil.ReadFile(filePath)
// if err != nil {
// return Failure(500, err)
// }

return Success(b, Type.Bytes)
}
// return Success(b, Type.Bytes)
// }

// GetConfig returns the injected config from the Load method
func GetConfig() interface{} {
Expand Down Expand Up @@ -212,12 +245,16 @@ func GetBlock(symbol string) Block {
return app.blocks[strings.ToLower(symbol)]
}

// BeforeRequest ...
// BeforeRequest is used to execute the request hook h. When a request is sent on a certain route
// the hook specified as h is executed in a separate goroutine without hindering the current
// main goroutine of request.
func BeforeRequest(h RequestHook) {
beforeHooks = append(beforeHooks, h)
}

// AfterRequest ...
// AfterRequest is used to execute the request hook h after completion of the request. A
// request is said to be complete only after the response is written through http.ResponseWriter
// interface of http.Server.
func AfterRequest(h RequestHook) {
afterHooks = append(afterHooks, h)
}
Expand Down Expand Up @@ -333,6 +370,38 @@ func UseRoute(route Route) {
app.routers = append(app.routers, router)
}

// rHandler ...
type rHandler struct {
fn http.HandlerFunc
}

func (rh rHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
rh.fn(w, r)
}

// UseHandlerFunc converts any http,HandlerFunc into rubik.Controller
func UseHandlerFunc(fn http.HandlerFunc) Controller {
return func(req *Request) {
fn(&req.Writer, req.Raw)
}
}

// UseHandler converts any http,Handler into rubik.Controller
func UseHandler(handler http.Handler) Controller {
return func(req *Request) {
handler.ServeHTTP(&req.Writer, req.Raw)
}
}

// UseIntermHandler converts any func(http,Handler) http,Handler into rubik.Controller
func UseIntermHandler(intermHandler func(http.Handler) http.Handler) Controller {
return func(req *Request) {
rh := rHandler{}
rh.fn = func(w http.ResponseWriter, r *http.Request) {}
intermHandler(rh).ServeHTTP(&req.Writer, req.Raw)
}
}

// Redirect redirects your request to the given URL
func Redirect(url string) ByteResponse {
return ByteResponse{
Expand All @@ -343,16 +412,16 @@ func Redirect(url string) ByteResponse {
// Proxy does not redirect your current resource locator but
// makes an internal GET call to the specified URL to serve
// it's response as your own
func Proxy(url string) ByteResponse {
cl := NewClient(url, time.Second*30)
en := BlankRequestEntity{}
en.PointTo = "@"
resp, err := cl.Get(en)
if err != nil {
return Failure(500, err)
}
return Success(resp.StringBody, Type.Text)
}
// func Proxy(url string) ByteResponse {
// cl := NewClient(url, time.Second*30)
// en := BlankRequestEntity{}
// en.PointTo = "@"
// resp, err := cl.Get(en)
// if err != nil {
// return Failure(500, err)
// }
// return Success(resp.StringBody, Type.Text)
// }

// SetNotFoundHandler sets custom handler for 404
func SetNotFoundHandler(h http.Handler) {
Expand Down Expand Up @@ -433,32 +502,61 @@ func RestError(code int, message string) (interface{}, error) {
return nil, RestErrorMixin{Code: code, Message: message}
}

// Success is a terminal function for rubik controller that sends byte response
// Respond is a terminal function for rubik controller that sends byte response
// it wraps around your arguments for better reading
func Success(data interface{}, btype ...ByteType) ByteResponse {
strings.Contains("a", "a")
return ByteResponse{
Status: http.StatusOK,
Data: data,
OfType: defByteType(btype),
func (req *Request) Respond(data interface{}, ofType ...ByteType) {
req.Writer.WriteHeader(200)

var ty ByteType
if len(ofType) == 0 {
ty = Type.Text
} else {
ty = ofType[0]
}

switch ty {
case Type.HTML:
break
case Type.Text:
s, ok := data.(string)
if !ok {
// TODO: common function to write error
fmt.Fprint(&req.Writer, "Error: cannot be written as string")
return
}
fmt.Fprint(&req.Writer, s)
break
}
}

// Failure returns a ByteResponse type with given status code
// Throw writes an error with given status code as response
// The ByteType parameter is optional as you can convert your
// error into a JSON or plain text
// FACT: if you dont have an error object with you in the moment
//
// NOTE: if you dont have an error object with you in the moment
// you can use r.E() to quickly wrap your stirng into an error
// and pass it inside this function
func Failure(status int, err error, btype ...ByteType) ByteResponse {
return ByteResponse{
Status: status,
Error: err,
OfType: defByteType(btype),
func (req *Request) Throw(status int, err error, btype ...ByteType) {
ty := defByteType(btype)
switch ty {
case Type.Text:
writeResponse(&req.Writer, status, Content.Text, []byte(err.Error()))
break
case Type.JSON:
req.Writer.Header().Add(Content.Header, Content.JSON)
req.Writer.WriteHeader(status)
jsonErr := struct {
Code int `json:"code"`
Message string `json:"message"`
}{status, err.Error()}
json.NewEncoder(&req.Writer).Encode(&jsonErr)
break
}
}

// E returns errors.New's error object
// E wraps the message into an error interface and returns it. This method can be used in
// your controller for throwing error response.
//
// NOTE: this error is not stdlib errors package
// this is pkg/errors error wrapper
func E(msg string) error {
Expand Down
54 changes: 27 additions & 27 deletions app_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -67,31 +67,31 @@ func TestUse(t *testing.T) {
}
}

func TestFromStorage(t *testing.T) {
resp := FromStorage("noSuchFile")
if resp.Error == nil {
t.Error("FromStorage() did not throw an error when it did not find the file")
}
}

func TestFromStorageUnreadableFile(t *testing.T) {
resp := FromStorage("nonreadable")
t.Log(resp.Error.Error())
if resp.Error == nil {
t.Error("FromStorage() did not return error if the file is not readable")
}
}

func TestFromStorageBytes(t *testing.T) {
resp := FromStorage("testFile")
if resp.Error != nil {
t.Error("FromStorage should not return an error because this file exists")
}

if string(resp.Data.([]byte)) != "test,haha" {
t.Error("FromStorage() did not read file properly")
}
}
// func TestFromStorage(t *testing.T) {
// resp := FromStorage("noSuchFile")
// if resp.Error == nil {
// t.Error("FromStorage() did not throw an error when it did not find the file")
// }
// }

// func TestFromStorageUnreadableFile(t *testing.T) {
// resp := FromStorage("nonreadable")
// t.Log(resp.Error.Error())
// if resp.Error == nil {
// t.Error("FromStorage() did not return error if the file is not readable")
// }
// }

// func TestFromStorageBytes(t *testing.T) {
// resp := FromStorage("testFile")
// if resp.Error != nil {
// t.Error("FromStorage should not return an error because this file exists")
// }

// if string(resp.Data.([]byte)) != "test,haha" {
// t.Error("FromStorage() did not read file properly")
// }
// }

func TestRestError(t *testing.T) {
_, err := RestError(400, "something")
Expand Down Expand Up @@ -145,14 +145,14 @@ func TestGetBlock(t *testing.T) {
}

func TestBeforeRequest(t *testing.T) {
BeforeRequest(func(rc *RequestContext) {})
BeforeRequest(func(rc *HookContext) {})
if len(beforeHooks) == 0 {
t.Error("BeforeRequest() not attached to beforeHooks slice")
}
}

func TestAfterRequest(t *testing.T) {
AfterRequest(func(rc *RequestContext) {})
AfterRequest(func(rc *HookContext) {})
if len(afterHooks) == 0 {
t.Error("AfterRequest() not attached to beforeHooks slice")
}
Expand Down
Loading

1 comment on commit e2687c2

@codekidX
Copy link
Owner Author

@codekidX codekidX commented on e2687c2 May 31, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The changes done to 0.2 version is huge:

  • Provide interoperability with stdlib
  • Everything is a controller now -- and handlers can be casted into controller using UseHandler, UseHandlerFunc, UseIntermHandler
  • HookContext is separate from rubik.Request
  • The method signature of a controller now is:
func someCtl(req *rubik.Request) {
    req.Respond("Hello")
}
  • Success and Failure methods are now *req.Respond and *req.Throw
  • Injector now returns the pointer of the Entity for modifications in Middlewares
  • Middlewares are now rubik.Controller too

Please sign in to comment.