├── app ├── fixtures │ ├── blank.txt │ ├── view │ │ ├── index.tpl │ │ └── sample │ │ │ └── hello.tpl │ └── mvc │ │ └── app.toml ├── app_test.go └── app.go ├── fixtures ├── badconfig │ ├── app.json │ ├── app.toml │ ├── app.yml │ └── app.hcl ├── view │ ├── index.tpl │ └── sample │ │ └── hello.tpl └── config │ ├── routes.yml │ ├── routes.hcl │ ├── routes.toml │ ├── routes.json │ ├── app.yml │ ├── app.hcl │ ├── app.toml │ └── app.json ├── utron.png ├── .travis.yml ├── utron.go ├── base ├── session_test.go ├── session.go ├── context_test.go └── context.go ├── logger ├── logger_test.go └── logger.go ├── view ├── view_test.go └── view.go ├── LICENCE ├── go.mod ├── controller ├── controller_test.go └── controller.go ├── router ├── middleware.go ├── middleware_test.go ├── routes_test.go └── routes.go ├── doc.go ├── flash ├── flash_test.go └── flash.go ├── models └── models.go ├── config ├── config_test.go └── config.go ├── README.md └── go.sum /app/fixtures/blank.txt: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /fixtures/badconfig/app.json: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /fixtures/badconfig/app.toml: -------------------------------------------------------------------------------- 1 | "bad" 2 | -------------------------------------------------------------------------------- /fixtures/badconfig/app.yml: -------------------------------------------------------------------------------- 1 | "crap" 2 | -------------------------------------------------------------------------------- /fixtures/view/index.tpl: -------------------------------------------------------------------------------- 1 | hello {{.Name}} -------------------------------------------------------------------------------- /app/fixtures/view/index.tpl: -------------------------------------------------------------------------------- 1 | hello {{.Name}} -------------------------------------------------------------------------------- /fixtures/badconfig/app.hcl: -------------------------------------------------------------------------------- 1 | "bad too" 2 | -------------------------------------------------------------------------------- /fixtures/view/sample/hello.tpl: -------------------------------------------------------------------------------- 1 | hello {{.Name}} -------------------------------------------------------------------------------- /app/fixtures/view/sample/hello.tpl: -------------------------------------------------------------------------------- 1 | hello {{.Name}} -------------------------------------------------------------------------------- /utron.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gernest/utron/HEAD/utron.png -------------------------------------------------------------------------------- /fixtures/config/routes.yml: -------------------------------------------------------------------------------- 1 | routes: 2 | - "get,post;/hello;Sample.Hello" 3 | - "get,post;/about;Hello.About" 4 | -------------------------------------------------------------------------------- /fixtures/config/routes.hcl: -------------------------------------------------------------------------------- 1 | routes = [ 2 | "get,post;/hello;Sample.Hello", 3 | "get,post;/about;Hello.About" 4 | ] -------------------------------------------------------------------------------- /fixtures/config/routes.toml: -------------------------------------------------------------------------------- 1 | routes= [ 2 | "get,post;/hello;Sample.Hello", 3 | "get,post;/about;Hello.About" 4 | ] 5 | -------------------------------------------------------------------------------- /fixtures/config/routes.json: -------------------------------------------------------------------------------- 1 | { 2 | "routes": [ 3 | "get,post;/hello;Sample.Hello", 4 | "get,post;/about;Hello.About" 5 | ] 6 | } -------------------------------------------------------------------------------- /fixtures/config/app.yml: -------------------------------------------------------------------------------- 1 | app_name: utron web app 2 | base_url: http://localhost:8090 3 | port: 8090 4 | verbose: false 5 | static_dir: fixtures/static 6 | view_dir: fixtures/view 7 | database: "" 8 | database_conn: "" 9 | automigrate: true 10 | -------------------------------------------------------------------------------- /fixtures/config/app.hcl: -------------------------------------------------------------------------------- 1 | app_name = "utron web app" 2 | base_url = "http://localhost:8090" 3 | port = 8090 4 | verbose = false 5 | static_dir = "fixtures/static" 6 | view_dir = "fixtures/view" 7 | database = "" 8 | database_conn = "" 9 | automigrate = true -------------------------------------------------------------------------------- /fixtures/config/app.toml: -------------------------------------------------------------------------------- 1 | app_name = "utron web app" 2 | base_url = "http://localhost:8090" 3 | port = 8090 4 | verbose = false 5 | static_dir = "fixtures/static" 6 | view_dir = "fixtures/view" 7 | database = "" 8 | database_conn = "" 9 | automigrate = true 10 | -------------------------------------------------------------------------------- /fixtures/config/app.json: -------------------------------------------------------------------------------- 1 | { 2 | "app_name": "utron web app", 3 | "base_url": "http://localhost:8090", 4 | "port": 8090, 5 | "verbose": false, 6 | "static_dir": "fixtures/static", 7 | "view_dir": "fixtures/view", 8 | "database": "", 9 | "database_conn": "", 10 | "automigrate":true 11 | } 12 | -------------------------------------------------------------------------------- /app/fixtures/mvc/app.toml: -------------------------------------------------------------------------------- 1 | app_name = "utron web app" 2 | base_url = "http://localhost:8090" 3 | port = 8090 4 | verbose = false 5 | static_dir = "fixtures/view" 6 | view_dir = "fixtures/view" 7 | database = "postgres" 8 | database_conn = "postgres://postgres@localhost/utron?sslmode=disable" 9 | automigrate = true 10 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: go 2 | go: 3 | - 1.9.x 4 | - 1.10.x 5 | - 1.11.x 6 | - tip 7 | services: 8 | -postgresql 9 | before_script: 10 | - psql -c 'create database utron;' -U postgres 11 | before_install: 12 | - go get -t -v 13 | - go get github.com/mattn/goveralls 14 | script: 15 | - $HOME/gopath/bin/goveralls -v -service=travis-ci -repotoken=$COVERALLS 16 | -------------------------------------------------------------------------------- /utron.go: -------------------------------------------------------------------------------- 1 | package utron 2 | 3 | import "github.com/gernest/utron/app" 4 | 5 | // NewApp creates a new bare-bone utron application. To use the MVC components, you should call 6 | // the Init method before serving requests. 7 | func NewApp() *app.App { 8 | return app.NewApp() 9 | } 10 | 11 | // NewMVC creates a new MVC utron app. If cfg is passed, it should be a directory to look for 12 | // the configuration files. The App returned is initialized. 13 | func NewMVC(cfg ...string) (*app.App, error) { 14 | return app.NewMVC(cfg...) 15 | } 16 | -------------------------------------------------------------------------------- /base/session_test.go: -------------------------------------------------------------------------------- 1 | package base 2 | 3 | import "testing" 4 | 5 | func TestContextSession(t *testing.T) { 6 | // should error when the session store is not set 7 | name := "sess" 8 | ctx := &Context{} 9 | _, err := ctx.NewSession(name) 10 | if err == nil { 11 | t.Error("expected error ", errNoStore) 12 | } 13 | _, err = ctx.GetSession(name) 14 | if err == nil { 15 | t.Error("expected error ", errNoStore) 16 | } 17 | err = ctx.SaveSession(nil) 18 | if err == nil { 19 | t.Error("expected error ", errNoStore) 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /logger/logger_test.go: -------------------------------------------------------------------------------- 1 | package logger 2 | 3 | import ( 4 | "bytes" 5 | "strings" 6 | "testing" 7 | ) 8 | 9 | func TestDefaultLogger(t *testing.T) { 10 | 11 | logData := []string{ 12 | "INFO", "SUCC", "ERR", "WARN", 13 | } 14 | buf := &bytes.Buffer{} 15 | l := NewDefaultLogger(buf) 16 | msg := "hello" 17 | l.Info(msg) 18 | l.Errors(msg) 19 | l.Warn(msg) 20 | l.Success(msg) 21 | 22 | out := buf.String() 23 | for _, v := range logData { 24 | if !strings.Contains(out, v) { 25 | t.Errorf("expected %s to contain %s", out, v) 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /view/view_test.go: -------------------------------------------------------------------------------- 1 | package view 2 | 3 | import ( 4 | "bytes" 5 | "strings" 6 | "testing" 7 | ) 8 | 9 | func TestSimpleView(t *testing.T) { 10 | v, err := NewSimpleView("../fixtures/view") 11 | if err != nil { 12 | t.Error(err) 13 | } 14 | 15 | out := &bytes.Buffer{} 16 | data := struct { 17 | Name string 18 | }{ 19 | "gernest", 20 | } 21 | 22 | tpls := []string{ 23 | "index", "sample/hello", 24 | } 25 | for _, tpl := range tpls { 26 | verr := v.Render(out, tpl, data) 27 | if verr != nil { 28 | t.Error(verr) 29 | } 30 | if !strings.Contains(out.String(), data.Name) { 31 | t.Errorf("expeted %s to contain %s", out.String(), data.Name) 32 | } 33 | } 34 | 35 | // file instead of a directory 36 | _, err = NewSimpleView("fixtures/view/index.tpl") 37 | if err == nil { 38 | t.Error("expected an error") 39 | } 40 | 41 | // non existing directory 42 | _, err = NewSimpleView("bogus") 43 | if err == nil { 44 | t.Error("expected an error") 45 | } 46 | 47 | } 48 | -------------------------------------------------------------------------------- /base/session.go: -------------------------------------------------------------------------------- 1 | package base 2 | 3 | import ( 4 | "errors" 5 | 6 | "github.com/gorilla/sessions" 7 | ) 8 | 9 | var errNoStore = errors.New("no session store was found") 10 | 11 | //NewSession returns a new browser session whose key is set to name. This only 12 | //works when the *Context.SessionStore is not nil. 13 | // 14 | // The session returned is from grorilla/sessions package. 15 | func (ctx *Context) NewSession(name string) (*sessions.Session, error) { 16 | if ctx.SessionStore != nil { 17 | return ctx.SessionStore.New(ctx.Request(), name) 18 | } 19 | return nil, errNoStore 20 | } 21 | 22 | //GetSession retrieves session with a given name. 23 | func (ctx *Context) GetSession(name string) (*sessions.Session, error) { 24 | if ctx.SessionStore != nil { 25 | return ctx.SessionStore.New(ctx.Request(), name) 26 | } 27 | return nil, errNoStore 28 | } 29 | 30 | //SaveSession saves the given session. 31 | func (ctx *Context) SaveSession(s *sessions.Session) error { 32 | if ctx.SessionStore != nil { 33 | return ctx.SessionStore.Save(ctx.Request(), ctx.Response(), s) 34 | } 35 | return errNoStore 36 | } 37 | -------------------------------------------------------------------------------- /LICENCE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2015 Geofrey Ernest 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in 11 | all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/gernest/utron 2 | 3 | require ( 4 | cloud.google.com/go v0.27.0 // indirect 5 | github.com/BurntSushi/toml v0.3.0 6 | github.com/cznic/ql v1.2.0 7 | github.com/denisenkom/go-mssqldb v0.0.0-20180901172138-1eb28afdf9b6 // indirect 8 | github.com/erikstmartin/go-testdb v0.0.0-20160219214506-8d10e4a1bae5 // indirect 9 | github.com/fatih/camelcase v1.0.0 10 | github.com/gernest/ita v0.0.0-20161218171910-00d04c1bb701 11 | github.com/gernest/qlstore v0.0.0-20161224085350-646d93e25ad3 12 | github.com/go-sql-driver/mysql v1.4.0 13 | github.com/google/go-cmp v0.2.0 // indirect 14 | github.com/gorilla/context v1.1.1 15 | github.com/gorilla/mux v1.6.2 16 | github.com/gorilla/securecookie v1.1.1 17 | github.com/gorilla/sessions v1.1.2 18 | github.com/hashicorp/hcl v1.0.0 19 | github.com/jinzhu/gorm v1.9.1 20 | github.com/jinzhu/inflection v0.0.0-20180308033659-04140366298a // indirect 21 | github.com/jinzhu/now v0.0.0-20180511015916-ed742868f2ae // indirect 22 | github.com/justinas/alice v0.0.0-20171023064455-03f45bd4b7da 23 | github.com/lib/pq v1.0.0 24 | github.com/mattn/go-sqlite3 v1.9.0 // indirect 25 | golang.org/x/crypto v0.0.0-20180910181607-0e37d006457b // indirect 26 | google.golang.org/appengine v1.1.0 // indirect 27 | gopkg.in/yaml.v2 v2.2.1 28 | ) 29 | -------------------------------------------------------------------------------- /logger/logger.go: -------------------------------------------------------------------------------- 1 | package logger 2 | 3 | import ( 4 | "fmt" 5 | "io" 6 | "log" 7 | "os" 8 | ) 9 | 10 | var logThis = NewDefaultLogger(os.Stdout) 11 | 12 | // Logger is an interface for utron logger 13 | type Logger interface { 14 | Info(v ...interface{}) 15 | Errors(fv ...interface{}) 16 | Warn(v ...interface{}) 17 | Success(v ...interface{}) 18 | } 19 | 20 | // DefaultLogger is the default logger 21 | type DefaultLogger struct { 22 | *log.Logger 23 | } 24 | 25 | // NewDefaultLogger returns a default logger writing to out 26 | func NewDefaultLogger(out io.Writer) Logger { 27 | d := &DefaultLogger{} 28 | d.Logger = log.New(out, "", log.LstdFlags) 29 | return d 30 | } 31 | 32 | // Info logs info messages 33 | func (d *DefaultLogger) Info(v ...interface{}) { 34 | d.Println(fmt.Sprintf(">>INFO>> %s", fmt.Sprint(v...))) 35 | } 36 | 37 | // Errors log error messages 38 | func (d *DefaultLogger) Errors(v ...interface{}) { 39 | d.Println(fmt.Sprintf(">>ERR>> %s", fmt.Sprint(v...))) 40 | } 41 | 42 | // Warn logs warning messages 43 | func (d *DefaultLogger) Warn(v ...interface{}) { 44 | d.Println(fmt.Sprintf(">>WARN>> %s", fmt.Sprint(v...))) 45 | } 46 | 47 | // Success logs success messages 48 | func (d *DefaultLogger) Success(v ...interface{}) { 49 | d.Println(fmt.Sprintf(">>SUCC>> %s", fmt.Sprint(v...))) 50 | } 51 | -------------------------------------------------------------------------------- /controller/controller_test.go: -------------------------------------------------------------------------------- 1 | package controller 2 | 3 | import ( 4 | "net/http" 5 | "net/http/httptest" 6 | "testing" 7 | 8 | "github.com/gernest/utron/base" 9 | ) 10 | 11 | func TestBaseController(t *testing.T) { 12 | req, _ := http.NewRequest("GET", "/", nil) 13 | w := httptest.NewRecorder() 14 | 15 | ctx := base.NewContext(w, req) 16 | 17 | ctrl := BaseController{} 18 | 19 | if ctrl.New(ctx); ctrl.Ctx == nil { 20 | t.Error("expected Ctx to be set") 21 | } 22 | 23 | // HTML response 24 | ctrl.HTML(http.StatusOK) 25 | cTyp := w.Header().Get(base.Content.Type) 26 | if cTyp != base.Content.TextHTML { 27 | t.Errorf("expecetd %s got %s", base.Content.TextHTML, cTyp) 28 | } 29 | 30 | // JSON response 31 | ctrl.JSON(http.StatusOK) 32 | cTyp = w.Header().Get(base.Content.Type) 33 | if cTyp != base.Content.Application.JSON { 34 | t.Errorf("expected %s got %s", base.Content.Application.JSON, cTyp) 35 | } 36 | 37 | // Plain text response 38 | ctrl.String(http.StatusOK) 39 | cTyp = w.Header().Get(base.Content.Type) 40 | if cTyp != base.Content.TextPlain { 41 | t.Errorf("expected %s got %s", base.Content.TextPlain, cTyp) 42 | } 43 | 44 | err := ctrl.Render() 45 | if err != nil { 46 | t.Errorf("expected nil got %v", err) 47 | } 48 | 49 | err = ctrl.Render() 50 | if err == nil { 51 | t.Error("expected error got nil") 52 | } 53 | 54 | } 55 | -------------------------------------------------------------------------------- /router/middleware.go: -------------------------------------------------------------------------------- 1 | package router 2 | 3 | import ( 4 | "net/http" 5 | 6 | "github.com/gernest/utron/base" 7 | ) 8 | 9 | //MiddlewareType is the kind of middleware. Utron support middleware with 10 | //variary of signatures. 11 | type MiddlewareType int 12 | 13 | const ( 14 | //PlainMiddleware is the middleware with signature 15 | // func(http.Handler)http.Handler 16 | PlainMiddleware MiddlewareType = iota 17 | 18 | //CtxMiddleware is the middlewate with signature 19 | // func(*base.Context)error 20 | CtxMiddleware 21 | ) 22 | 23 | //Middleware is the utron middleware 24 | type Middleware struct { 25 | Type MiddlewareType 26 | value interface{} 27 | } 28 | 29 | //ToHandler returns a func(http.Handler) http.Handler from the Middleware. Utron 30 | //uses alice to chain middleware. 31 | // 32 | // Use this method to get alice compatible middleware. 33 | func (m *Middleware) ToHandler(ctx *base.Context) func(http.Handler) http.Handler { 34 | switch m.Type { 35 | case PlainMiddleware: 36 | return m.value.(func(http.Handler) http.Handler) 37 | case CtxMiddleware: 38 | fn := m.value.(func(*base.Context) error) 39 | return func(h http.Handler) http.Handler { 40 | return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 41 | err := fn(ctx) 42 | if err != nil { 43 | return 44 | } 45 | h.ServeHTTP(w, r) 46 | }) 47 | } 48 | 49 | } 50 | return func(h http.Handler) http.Handler { 51 | return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 52 | }) 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /doc.go: -------------------------------------------------------------------------------- 1 | //Package utron is a lightweight MVC framework for building fast, scalable and robust web applications 2 | // 3 | // Utron is built with focus of composability. This means, you can reap all the 4 | // benefit of using utron while maintaining harmony with existing libraries and 5 | // frameworks. 6 | // 7 | // Core feaures of utron 8 | // 9 | // * Session management 10 | // 11 | // * Middleware support 12 | // 13 | // * Data acess( Relational database) 14 | // 15 | // * Logging 16 | // 17 | // * Templates (views) 18 | // 19 | // * Configuration 20 | // 21 | // * Static content serving 22 | // 23 | // Why utron 24 | // 25 | // There are many frameworks out there, you might be wondering why do we need 26 | // yet another framework?. We probably don't.. Utron is just a summary of the 27 | // tools, and techniques I use to develop web applications in Go. 28 | // 29 | // This includes the best libraries, and the best organization of the code base. 30 | // Utron has one of the very handy Controller( you will see more details in the 31 | // controller section) 32 | // 33 | // These are the common libraries I use 34 | // 35 | // * gorilla/mux: for http routing. 36 | // 37 | // * gorilla/session: for session management 38 | // 39 | // * gorm: for object relationl mapping. 40 | // 41 | // In all of my web application I noticed that, keeping global state brings a 42 | // lot of complexities to the application, so uttron avoids this. utron 43 | // applicatio is a simple struct that you can play around with in whatever way 44 | // that you find suits you. 45 | package utron 46 | -------------------------------------------------------------------------------- /flash/flash_test.go: -------------------------------------------------------------------------------- 1 | package flash 2 | 3 | import ( 4 | "fmt" 5 | "net/http" 6 | "net/http/cookiejar" 7 | "net/http/httptest" 8 | "os" 9 | "testing" 10 | 11 | "github.com/gernest/utron/controller" 12 | "github.com/gernest/utron/logger" 13 | "github.com/gernest/utron/router" 14 | "github.com/gorilla/sessions" 15 | ) 16 | 17 | type FlashTest struct { 18 | controller.BaseController 19 | Routes []string 20 | } 21 | 22 | const ( 23 | fname = "flash" 24 | fkey = "flash" 25 | ) 26 | 27 | var result Flashes 28 | 29 | func (f *FlashTest) Index() { 30 | fl := New() 31 | fl.Success("Success") 32 | fl.Err("Err") 33 | fl.Warn("Warn") 34 | fl.Save(f.Ctx, fname, fkey) 35 | } 36 | 37 | func (f FlashTest) Flash() { 38 | r, err := GetFlashes(f.Ctx, fname, fkey) 39 | if err != nil { 40 | f.Ctx.Log.Errors(err) 41 | return 42 | } 43 | result = r 44 | } 45 | 46 | func NewFlashTest() controller.Controller { 47 | return &FlashTest{ 48 | Routes: []string{ 49 | "get;/;Index", 50 | "get;/flash;Flash", 51 | }, 52 | } 53 | } 54 | 55 | func TestFlash(t *testing.T) { 56 | codecKey1 := "ePAPW9vJv7gHoftvQTyNj5VkWB52mlza" 57 | codecKey2 := "N8SmpJ00aSpepNrKoyYxmAJhwVuKEWZD" 58 | jar, err := cookiejar.New(nil) 59 | if err != nil { 60 | t.Error(err) 61 | } 62 | client := &http.Client{Jar: jar} 63 | o := &router.Options{ 64 | Log: logger.NewDefaultLogger(os.Stdout), 65 | SessionStore: sessions.NewCookieStore([]byte(codecKey1), []byte(codecKey2)), 66 | } 67 | r := router.NewRouter(o) 68 | r.Add(NewFlashTest) 69 | ts := httptest.NewServer(r) 70 | defer ts.Close() 71 | _, err = client.Get(fmt.Sprintf("%s/", ts.URL)) 72 | if err != nil { 73 | t.Error(err) 74 | } 75 | _, err = client.Get(fmt.Sprintf("%s/flash", ts.URL)) 76 | if err != nil { 77 | t.Error(err) 78 | } 79 | 80 | if len(result) != 3 { 81 | t.Errorf("expected 3 got %d", len(result)) 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /controller/controller.go: -------------------------------------------------------------------------------- 1 | package controller 2 | 3 | import ( 4 | "encoding/json" 5 | "reflect" 6 | 7 | "github.com/gernest/utron/base" 8 | ) 9 | 10 | // Controller is an interface for utron controllers 11 | type Controller interface { 12 | New(*base.Context) 13 | Render() error 14 | } 15 | 16 | // BaseController implements the Controller interface, It is recommended all 17 | // user defined Controllers should embed *BaseController. 18 | type BaseController struct { 19 | Ctx *base.Context 20 | Routes []string 21 | } 22 | 23 | // New sets ctx as the active context 24 | func (b *BaseController) New(ctx *base.Context) { 25 | b.Ctx = ctx 26 | } 27 | 28 | // Render commits the changes made in the active context. 29 | func (b *BaseController) Render() error { 30 | return b.Ctx.Commit() 31 | } 32 | 33 | // HTML renders text/html with the given code as status code 34 | func (b *BaseController) HTML(code int) { 35 | b.Ctx.HTML() 36 | b.Ctx.Set(code) 37 | } 38 | 39 | // String renders text/plain with given code as status code 40 | func (b *BaseController) String(code int) { 41 | b.Ctx.TextPlain() 42 | b.Ctx.Set(code) 43 | } 44 | 45 | // JSON renders application/json with the given code 46 | func (b *BaseController) JSON(code int) { 47 | b.Ctx.JSON() 48 | b.Ctx.Set(code) 49 | } 50 | 51 | // RenderJSON encodes value into json and renders the response as JSON 52 | func (b *BaseController) RenderJSON(value interface{}, code int) { 53 | _ = json.NewEncoder(b.Ctx).Encode(value) 54 | b.JSON(code) 55 | } 56 | 57 | // GetCtrlFunc returns a new copy of the contoller everytime the function is called 58 | func GetCtrlFunc(ctrl Controller) func() Controller { 59 | v := reflect.ValueOf(ctrl) 60 | return func() Controller { 61 | e := v 62 | if e.Kind() == reflect.Ptr { 63 | e = e.Elem() 64 | return e.Addr().Interface().(Controller) 65 | } 66 | return e.Interface().(Controller) 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /base/context_test.go: -------------------------------------------------------------------------------- 1 | package base 2 | 3 | import ( 4 | "io" 5 | "net/http" 6 | "net/http/httptest" 7 | "testing" 8 | 9 | "github.com/gernest/utron/config" 10 | "github.com/gorilla/mux" 11 | ) 12 | 13 | type DummyView struct { 14 | } 15 | 16 | func (d *DummyView) Render(out io.Writer, name string, data interface{}) error { 17 | out.Write([]byte(name)) 18 | return nil 19 | } 20 | 21 | func TestContext(t *testing.T) { 22 | r := mux.NewRouter() 23 | name := "world" 24 | r.HandleFunc("/hello/{name}", testHandler(t, name)) 25 | req, _ := http.NewRequest("GET", "/hello/"+name, nil) 26 | w := httptest.NewRecorder() 27 | r.ServeHTTP(w, req) 28 | } 29 | 30 | func testHandler(t *testing.T, name string) func(http.ResponseWriter, *http.Request) { 31 | return func(w http.ResponseWriter, r *http.Request) { 32 | ctxHandler(t, name, w, r) 33 | } 34 | } 35 | 36 | func ctxHandler(t *testing.T, name string, w http.ResponseWriter, r *http.Request) { 37 | ctx := NewContext(w, r) 38 | ctx.Init() 39 | pname := ctx.Params["name"] 40 | if pname != name { 41 | t.Errorf("expected %s got %s", name, pname) 42 | } 43 | 44 | ctx.SetData("name", pname) 45 | 46 | data := ctx.GetData("name") 47 | if data == nil { 48 | t.Error("expected values to be stored in context") 49 | } 50 | ctx.JSON() 51 | h := w.Header().Get(Content.Type) 52 | if h != Content.Application.JSON { 53 | t.Errorf("expected %s got %s", Content.Application.JSON, h) 54 | } 55 | ctx.HTML() 56 | h = w.Header().Get(Content.Type) 57 | if h != Content.TextHTML { 58 | t.Errorf("expected %s got %s", Content.TextHTML, h) 59 | } 60 | ctx.TextPlain() 61 | h = w.Header().Get(Content.Type) 62 | if h != Content.TextPlain { 63 | t.Errorf("expected %s got %s", Content.TextPlain, h) 64 | } 65 | 66 | err := ctx.Commit() 67 | if err != nil { 68 | t.Error(err) 69 | } 70 | 71 | // make sure we can't commit twice 72 | err = ctx.Commit() 73 | if err == nil { 74 | t.Error("expected error") 75 | } 76 | 77 | // when there is template and view 78 | ctx.isCommited = false 79 | ctx.Template = pname 80 | ctx.Set(&DummyView{}) 81 | ctx.Cfg = &config.Config{} 82 | err = ctx.Commit() 83 | if err != nil { 84 | t.Error(err) 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /models/models.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | import ( 4 | "errors" 5 | "reflect" 6 | "strings" 7 | 8 | "github.com/gernest/utron/config" 9 | "github.com/jinzhu/gorm" 10 | 11 | // support mysql, sqlite3 and postgresql 12 | _ "github.com/go-sql-driver/mysql" 13 | _ "github.com/jinzhu/gorm/dialects/sqlite" 14 | _ "github.com/lib/pq" 15 | ) 16 | 17 | // Model facilitate database interactions, supports postgres, mysql and foundation 18 | type Model struct { 19 | models map[string]reflect.Value 20 | isOpen bool 21 | *gorm.DB 22 | } 23 | 24 | // NewModel returns a new Model without opening database connection 25 | func NewModel() *Model { 26 | return &Model{ 27 | models: make(map[string]reflect.Value), 28 | } 29 | } 30 | 31 | // IsOpen returns true if the Model has already established connection 32 | // to the database 33 | func (m *Model) IsOpen() bool { 34 | return m.isOpen 35 | } 36 | 37 | // OpenWithConfig opens database connection with the settings found in cfg 38 | func (m *Model) OpenWithConfig(cfg *config.Config) error { 39 | db, err := gorm.Open(cfg.Database, cfg.DatabaseConn) 40 | if err != nil { 41 | return err 42 | } 43 | m.DB = db 44 | m.isOpen = true 45 | return nil 46 | } 47 | 48 | // Register adds the values to the models registry 49 | func (m *Model) Register(values ...interface{}) error { 50 | 51 | // do not work on them.models first, this is like an insurance policy 52 | // whenever we encounter any error in the values nothing goes into the registry 53 | models := make(map[string]reflect.Value) 54 | if len(values) > 0 { 55 | for _, val := range values { 56 | rVal := reflect.ValueOf(val) 57 | if rVal.Kind() == reflect.Ptr { 58 | rVal = rVal.Elem() 59 | } 60 | switch rVal.Kind() { 61 | case reflect.Struct: 62 | models[getTypName(rVal.Type())] = reflect.New(rVal.Type()) 63 | default: 64 | return errors.New("utron: models must be structs") 65 | } 66 | } 67 | } 68 | for k, v := range models { 69 | m.models[k] = v 70 | } 71 | return nil 72 | } 73 | 74 | // AutoMigrateAll runs migrations for all the registered models 75 | func (m *Model) AutoMigrateAll() { 76 | for _, v := range m.models { 77 | m.AutoMigrate(v.Interface()) 78 | } 79 | } 80 | func getTypName(typ reflect.Type) string { 81 | if typ.Name() != "" { 82 | return typ.Name() 83 | } 84 | split := strings.Split(typ.String(), ".") 85 | return split[len(split)-1] 86 | } 87 | -------------------------------------------------------------------------------- /config/config_test.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "os" 5 | "testing" 6 | ) 7 | 8 | func TestConfig(t *testing.T) { 9 | cfgFiles := []string{ 10 | "../fixtures/config/app.json", 11 | "../fixtures/config/app.yml", 12 | "../fixtures/config/app.toml", 13 | "../fixtures/config/app.hcl", 14 | } 15 | badCfgFiles := []string{ 16 | "../fixtures/badconfig/app.json", 17 | "../fixtures/badconfig/app.yml", 18 | "../fixtures/badconfig/app.toml", 19 | "../fixtures/badconfig/app.hcl", 20 | } 21 | for _, f := range badCfgFiles { 22 | _, err := NewConfig(f) 23 | if err == nil { 24 | t.Fatal("expected error ", f) 25 | } 26 | 27 | } 28 | 29 | cfg := DefaultConfig() 30 | 31 | for _, f := range cfgFiles { 32 | nCfg, err := NewConfig(f) 33 | if err != nil { 34 | t.Fatal(err) 35 | } 36 | if nCfg.AppName != cfg.AppName { 37 | t.Errorf("expecetd %s got %s", cfg.AppName, nCfg.AppName) 38 | } 39 | } 40 | 41 | // no file 42 | _, err := NewConfig("nothing") 43 | if err == nil { 44 | t.Error("expected error") 45 | } 46 | 47 | //unsupporte file 48 | _, err = NewConfig("../fixtures/view/index.tpl") 49 | if err == nil { 50 | t.Error("expected error") 51 | } 52 | if err != errCfgUnsupported { 53 | t.Errorf("expected %v got %v", errCfgUnsupported, err) 54 | } 55 | 56 | } 57 | 58 | func TestConfigEnv(t *testing.T) { 59 | fields := []struct { 60 | name, env, value string 61 | }{ 62 | {"AppName", "APP_NAME", "utron"}, 63 | {"BaseURL", "BASE_URL", "http://localhost:8090"}, 64 | {"Port", "PORT", "8091"}, 65 | {"ViewsDir", "VIEWS_DIR", "fixtures/view"}, 66 | {"StaticDir", "STATIC_DIR", "fixtures/todo/static"}, 67 | {"Database", "DATABASE", "postgres"}, 68 | {"DatabaseConn", "DATABASE_CONN", "postgres://postgres@localhost/utron?sslmode=disable"}, 69 | {"Automigrate", "AUTOMIGRATE", "true"}, 70 | } 71 | for _, f := range fields { 72 | 73 | // check out env name maker 74 | cm := getEnvName(f.name) 75 | if cm != f.env { 76 | t.Errorf("expected %s got %s", f.env, cm) 77 | } 78 | } 79 | 80 | // set environment values 81 | for _, f := range fields { 82 | _ = os.Setenv(f.env, f.value) 83 | } 84 | 85 | cfg := DefaultConfig() 86 | if err := cfg.SyncEnv(); err != nil { 87 | t.Errorf("syncing env %v", err) 88 | } 89 | 90 | if cfg.Port != 8091 { 91 | t.Errorf("expected 8091 got %d instead", cfg.Port) 92 | } 93 | 94 | if cfg.AppName != "utron" { 95 | t.Errorf("expected utron got %s", cfg.AppName) 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /app/app_test.go: -------------------------------------------------------------------------------- 1 | package app 2 | 3 | import ( 4 | "net/http" 5 | "net/http/httptest" 6 | "os" 7 | "path/filepath" 8 | "strings" 9 | "testing" 10 | 11 | "github.com/gernest/utron/config" 12 | "github.com/gernest/utron/controller" 13 | ) 14 | 15 | const notFoundMsg = "nothing" 16 | 17 | func TestGetAbsPath(t *testing.T) { 18 | wd, err := os.Getwd() 19 | if err != nil { 20 | t.Error(err) 21 | } 22 | 23 | // non existing 24 | _, err = getAbsolutePath("nope") 25 | if err == nil { 26 | t.Error("expcted error got nil") 27 | } 28 | if !os.IsNotExist(err) { 29 | t.Errorf("expcetd not exist got %v", err) 30 | } 31 | 32 | absPath := filepath.Join(wd, "fixtures") 33 | 34 | // Relqtive 35 | dir, err := getAbsolutePath("fixtures") 36 | if err != nil { 37 | t.Error(err) 38 | } 39 | 40 | if dir != absPath { 41 | t.Errorf("expceted %s got %s", absPath, dir) 42 | } 43 | 44 | // Absolute 45 | dir, err = getAbsolutePath(absPath) 46 | if err != nil { 47 | t.Error(err) 48 | } 49 | 50 | if dir != absPath { 51 | t.Errorf("expceted %s got %s", absPath, dir) 52 | } 53 | 54 | } 55 | 56 | type SimpleMVC struct { 57 | controller.BaseController 58 | } 59 | 60 | func (s *SimpleMVC) Hello() { 61 | s.Ctx.Data["Name"] = "gernest" 62 | s.Ctx.Template = "index" 63 | s.String(http.StatusOK) 64 | } 65 | 66 | func TestMVC(t *testing.T) { 67 | app, err := NewMVC("fixtures/mvc") 68 | if err != nil { 69 | t.Skip(err) 70 | } 71 | app.AddController(controller.GetCtrlFunc(&SimpleMVC{})) 72 | 73 | req, _ := http.NewRequest("GET", "/simplemvc/hello", nil) 74 | w := httptest.NewRecorder() 75 | app.ServeHTTP(w, req) 76 | 77 | if w.Code != http.StatusOK { 78 | t.Errorf("expcted %d got %d", http.StatusOK, w.Code) 79 | } 80 | 81 | if !strings.Contains(w.Body.String(), "gernest") { 82 | t.Errorf("expected %s to contain gernest", w.Body.String()) 83 | } 84 | 85 | } 86 | 87 | func TestApp(t *testing.T) { 88 | app := NewApp() 89 | // Set not found handler 90 | err := app.SetNotFoundHandler(http.HandlerFunc(sampleDefault)) 91 | if err != nil { 92 | t.Error(err) 93 | } 94 | 95 | // no router 96 | app.Router = nil 97 | err = app.SetNotFoundHandler(http.HandlerFunc(sampleDefault)) 98 | if err == nil { 99 | t.Error("expected an error") 100 | } 101 | } 102 | 103 | func sampleDefault(w http.ResponseWriter, r *http.Request) { 104 | w.Write([]byte(notFoundMsg)) 105 | } 106 | 107 | func TestStaticServer(t *testing.T) { 108 | c := &config.Config{} 109 | _, ok, _ := StaticServer(c) 110 | if ok { 111 | t.Error("expected false") 112 | } 113 | c.StaticDir = "fixtures" 114 | s, ok, _ := StaticServer(c) 115 | if !ok { 116 | t.Error("expected true") 117 | } 118 | expect := "/static/" 119 | if s != expect { 120 | t.Errorf("expected %s got %s", expect, s) 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /view/view.go: -------------------------------------------------------------------------------- 1 | package view 2 | 3 | import ( 4 | "fmt" 5 | "html/template" 6 | "io" 7 | "io/ioutil" 8 | "os" 9 | "path/filepath" 10 | "strings" 11 | ) 12 | 13 | // View is an interface for rendering templates. 14 | type View interface { 15 | Render(out io.Writer, name string, data interface{}) error 16 | } 17 | 18 | // SimpleView implements View interface, but based on golang templates. 19 | type SimpleView struct { 20 | viewDir string 21 | tmpl *template.Template 22 | } 23 | 24 | //NewSimpleView returns a SimpleView with templates loaded from viewDir 25 | func NewSimpleView(viewDir string) (View, error) { 26 | info, err := os.Stat(viewDir) 27 | if err != nil { 28 | return nil, err 29 | } 30 | if !info.IsDir() { 31 | return nil, fmt.Errorf("utron: %s is not a directory", viewDir) 32 | } 33 | s := &SimpleView{ 34 | viewDir: viewDir, 35 | tmpl: template.New(filepath.Base(viewDir)), 36 | } 37 | return s.load(viewDir) 38 | } 39 | 40 | // load loads templates from dir. The templates should be valid golang templates 41 | // 42 | // Only files with extension .html, .tpl, .tmpl will be loaded. references to these templates 43 | // should be relative to the dir. That is, if dir is foo, you don't have to refer to 44 | // foo/bar.tpl, instead just use bar.tpl 45 | func (s *SimpleView) load(dir string) (View, error) { 46 | 47 | // supported is the list of file extensions that will be parsed as templates 48 | supported := map[string]bool{".tpl": true, ".html": true, ".tmpl": true} 49 | 50 | werr := filepath.Walk(dir, func(path string, info os.FileInfo, err error) error { 51 | if err != nil { 52 | return err 53 | } 54 | if info.IsDir() { 55 | return nil 56 | } 57 | 58 | extension := filepath.Ext(path) 59 | if _, ok := supported[extension]; !ok { 60 | return nil 61 | } 62 | 63 | data, err := ioutil.ReadFile(path) 64 | if err != nil { 65 | return err 66 | } 67 | 68 | // We remove the directory name from the path 69 | // this means if we have directory foo, with file bar.tpl 70 | // full path for bar file foo/bar.tpl 71 | // we trim the foo part and remain with /bar.tpl 72 | // 73 | // NOTE we don't account for the opening slash, when dir ends with /. 74 | name := path[len(dir):] 75 | 76 | name = filepath.ToSlash(name) 77 | 78 | name = strings.TrimPrefix(name, "/") // case we missed the opening slash 79 | 80 | name = strings.TrimSuffix(name, extension) // remove extension 81 | 82 | t := s.tmpl.New(name) 83 | 84 | if _, err = t.Parse(string(data)); err != nil { 85 | return err 86 | } 87 | return nil 88 | }) 89 | 90 | if werr != nil { 91 | return nil, werr 92 | } 93 | 94 | return s, nil 95 | } 96 | 97 | // Render executes template named name, passing data as context, the output is written to out. 98 | func (s *SimpleView) Render(out io.Writer, name string, data interface{}) error { 99 | return s.tmpl.ExecuteTemplate(out, name, data) 100 | } 101 | -------------------------------------------------------------------------------- /router/middleware_test.go: -------------------------------------------------------------------------------- 1 | package router 2 | 3 | import ( 4 | "fmt" 5 | "net/http" 6 | "net/http/httptest" 7 | "testing" 8 | 9 | "github.com/gernest/utron/base" 10 | "github.com/gernest/utron/controller" 11 | "github.com/gorilla/context" 12 | ) 13 | 14 | const incrementKey = "increment" 15 | 16 | func (s *Sample) Increment() { 17 | key := s.Ctx.GetData(incrementKey) 18 | fmt.Fprintf(s.Ctx, "%v", key) 19 | s.String(http.StatusOK) 20 | } 21 | 22 | func plainIncrement(n int) func(http.Handler) http.Handler { 23 | return func(h http.Handler) http.Handler { 24 | return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 25 | if key, ok := context.GetOk(r, incrementKey); ok { 26 | ikey := key.(int) 27 | ikey += n 28 | context.Set(r, incrementKey, ikey) 29 | } else { 30 | context.Set(r, incrementKey, 0) 31 | } 32 | h.ServeHTTP(w, r) 33 | }) 34 | } 35 | } 36 | 37 | func contextMiddleware(n int) func(*base.Context) error { 38 | return func(ctx *base.Context) error { 39 | key := ctx.GetData(incrementKey) 40 | if key != nil { 41 | ikey := key.(int) 42 | ikey += n 43 | ctx.SetData(incrementKey, ikey) 44 | } else { 45 | ctx.SetData(incrementKey, 0) 46 | } 47 | return nil 48 | } 49 | } 50 | 51 | func TestMiddlewarePlain(t *testing.T) { 52 | expect := "3" 53 | r := NewRouter() 54 | _ = r.Add(controller.GetCtrlFunc(&Sample{}), plainIncrement(0), plainIncrement(1), plainIncrement(2)) 55 | 56 | req, err := http.NewRequest("GET", "/sample/increment", nil) 57 | if err != nil { 58 | t.Error(err) 59 | } 60 | w := httptest.NewRecorder() 61 | r.ServeHTTP(w, req) 62 | 63 | if w.Code != http.StatusOK { 64 | t.Errorf("expected %d got %d", http.StatusOK, w.Code) 65 | } 66 | if w.Body.String() != expect { 67 | t.Errorf("expected %s got %s", expect, w.Body.String()) 68 | } 69 | } 70 | 71 | func TestMiddlewareContext(t *testing.T) { 72 | expect := "3" 73 | r := NewRouter() 74 | _ = r.Add(controller.GetCtrlFunc(&Sample{}), contextMiddleware(0), contextMiddleware(1), contextMiddleware(2)) 75 | 76 | req, err := http.NewRequest("GET", "/sample/increment", nil) 77 | if err != nil { 78 | t.Error(err) 79 | } 80 | w := httptest.NewRecorder() 81 | r.ServeHTTP(w, req) 82 | 83 | if w.Code != http.StatusOK { 84 | t.Errorf("expected %d got %d", http.StatusOK, w.Code) 85 | } 86 | if w.Body.String() != expect { 87 | t.Errorf("expected %s got %s", expect, w.Body.String()) 88 | } 89 | } 90 | 91 | func TestMiddlewareMixed(t *testing.T) { 92 | expect := "6" 93 | 94 | r := NewRouter() 95 | _ = r.Add(controller.GetCtrlFunc(&Sample{}), plainIncrement(0), contextMiddleware(1), plainIncrement(2), contextMiddleware(3)) 96 | 97 | req, err := http.NewRequest("GET", "/sample/increment", nil) 98 | if err != nil { 99 | t.Error(err) 100 | } 101 | w := httptest.NewRecorder() 102 | r.ServeHTTP(w, req) 103 | 104 | if w.Code != http.StatusOK { 105 | t.Errorf("expected %d got %d", http.StatusOK, w.Code) 106 | } 107 | if w.Body.String() != expect { 108 | t.Errorf("expected %s got %s", expect, w.Body.String()) 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /flash/flash.go: -------------------------------------------------------------------------------- 1 | package flash 2 | 3 | import ( 4 | "encoding/gob" 5 | "errors" 6 | 7 | "github.com/gernest/utron/base" 8 | ) 9 | 10 | const ( 11 | // FlashSuccess is the context key for success flash messages 12 | FlashSuccess = "FlashSuccess" 13 | 14 | // FlashWarn is a context key for warning flash messages 15 | FlashWarn = "FlashWarn" 16 | 17 | // FlashErr is a context key for flash error message 18 | FlashErr = "FlashError" 19 | ) 20 | 21 | func init() { 22 | gob.Register(&Flash{}) 23 | gob.Register(Flashes{}) 24 | } 25 | 26 | // Flash implements flash messages, like ones in gorilla/sessions 27 | type Flash struct { 28 | Kind string 29 | Message string 30 | } 31 | 32 | // Flashes is a collection of flash messages 33 | type Flashes []*Flash 34 | 35 | // GetFlashes retieves all flash messages found in a cookie session associated with ctx.. 36 | // 37 | // name is the session name which is used to store the flash messages. The flash 38 | // messages can be stored in any session, but it is a good idea to separate 39 | // session for flash messages from other sessions. 40 | // 41 | // key is the key that is used to identiry which flash messages are of interest. 42 | func GetFlashes(ctx *base.Context, name, key string) (Flashes, error) { 43 | ss, err := ctx.GetSession(name) 44 | if err != nil { 45 | return nil, err 46 | } 47 | if v, ok := ss.Values[key]; ok { 48 | delete(ss.Values, key) 49 | serr := ss.Save(ctx.Request(), ctx.Response()) 50 | if serr != nil { 51 | return nil, serr 52 | } 53 | return v.(Flashes), nil 54 | } 55 | return nil, errors.New("no flashes found") 56 | } 57 | 58 | // AddFlashToCtx takes flash messages stored in a cookie which is associated with the 59 | // request found in ctx, and puts them inside the ctx object. The flash messages can then 60 | // be retrieved by calling ctx.Get( FlashKey). 61 | // 62 | // NOTE When there are no flash messages then nothing is set. 63 | func AddFlashToCtx(ctx *base.Context, name, key string) error { 64 | f, err := GetFlashes(ctx, name, key) 65 | if err != nil { 66 | return err 67 | } 68 | ctx.SetData(key, f) 69 | return nil 70 | } 71 | 72 | //Flasher tracks flash messages 73 | type Flasher struct { 74 | f Flashes 75 | } 76 | 77 | //New creates new flasher. This alllows accumulation of lash messages. To save the flash messages 78 | //the Save method should be called explicitly. 79 | func New() *Flasher { 80 | return &Flasher{} 81 | } 82 | 83 | // Add adds the flash message 84 | func (f *Flasher) Add(kind, message string) { 85 | fl := &Flash{kind, message} 86 | f.f = append(f.f, fl) 87 | } 88 | 89 | // Success adds success flash message 90 | func (f *Flasher) Success(msg string) { 91 | f.Add(FlashSuccess, msg) 92 | } 93 | 94 | // Err adds error flash message 95 | func (f *Flasher) Err(msg string) { 96 | f.Add(FlashErr, msg) 97 | } 98 | 99 | // Warn adds warning flash message 100 | func (f *Flasher) Warn(msg string) { 101 | f.Add(FlashWarn, msg) 102 | } 103 | 104 | // Save saves flash messages to context 105 | func (f *Flasher) Save(ctx *base.Context, name, key string) error { 106 | ss, err := ctx.GetSession(name) 107 | if err != nil { 108 | return err 109 | } 110 | var flashes Flashes 111 | if v, ok := ss.Values[key]; ok { 112 | flashes = v.(Flashes) 113 | } 114 | ss.Values[key] = append(flashes, f.f...) 115 | err = ss.Save(ctx.Request(), ctx.Response()) 116 | if err != nil { 117 | return err 118 | } 119 | return nil 120 | } 121 | -------------------------------------------------------------------------------- /router/routes_test.go: -------------------------------------------------------------------------------- 1 | package router 2 | 3 | import ( 4 | "net/http" 5 | "net/http/httptest" 6 | "testing" 7 | 8 | "github.com/gernest/utron/controller" 9 | ) 10 | 11 | var msg = "gernest" 12 | 13 | type Sample struct { 14 | controller.BaseController 15 | Routes []string 16 | } 17 | 18 | func (s *Sample) Bang() { 19 | _, _ = s.Ctx.Write([]byte(msg)) 20 | s.JSON(http.StatusOK) 21 | } 22 | 23 | func (s *Sample) Hello() { 24 | _, _ = s.Ctx.Write([]byte(msg)) 25 | s.String(http.StatusOK) 26 | } 27 | 28 | func NewSample() *Sample { 29 | routes := []string{ 30 | "get,post;/hello/world;Hello", 31 | } 32 | s := &Sample{} 33 | s.Routes = routes 34 | return s 35 | } 36 | 37 | func TestRouterAdd(t *testing.T) { 38 | r := NewRouter() 39 | _ = r.Add(controller.GetCtrlFunc(&Sample{})) 40 | 41 | req, err := http.NewRequest("GET", "/sample/bang", nil) 42 | if err != nil { 43 | t.Error(err) 44 | } 45 | w := httptest.NewRecorder() 46 | r.ServeHTTP(w, req) 47 | 48 | if w.Code != http.StatusOK { 49 | t.Errorf("expected %d got %d", http.StatusOK, w.Code) 50 | } 51 | if w.Body.String() != msg { 52 | t.Errorf("expected %s got %s", msg, w.Body.String()) 53 | } 54 | } 55 | 56 | func TestRouteField(t *testing.T) { 57 | r := NewRouter() 58 | routes := []string{ 59 | "get,post;/hello/world;Hello", 60 | } 61 | s := &Sample{} 62 | s.Routes = routes 63 | err := r.Add(controller.GetCtrlFunc(s)) 64 | if err != nil { 65 | t.Error(err) 66 | } 67 | req, err := http.NewRequest("GET", "/hello/world", nil) 68 | if err != nil { 69 | t.Error(err) 70 | } 71 | w := httptest.NewRecorder() 72 | r.ServeHTTP(w, req) 73 | 74 | if w.Code != http.StatusOK { 75 | t.Errorf("expected %d got %d", http.StatusOK, w.Code) 76 | } 77 | if w.Body.String() != msg { 78 | t.Errorf("expected %s got %s", msg, w.Body.String()) 79 | } 80 | 81 | req, err = http.NewRequest("GET", "/sample/bang", nil) 82 | if err != nil { 83 | t.Error(err) 84 | } 85 | w = httptest.NewRecorder() 86 | r.ServeHTTP(w, req) 87 | 88 | if w.Code != http.StatusOK { 89 | t.Errorf("expected %d got %d", http.StatusOK, w.Code) 90 | } 91 | if w.Body.String() != msg { 92 | t.Errorf("expected %s got %s", msg, w.Body.String()) 93 | } 94 | } 95 | 96 | func TestRoutesFile(t *testing.T) { 97 | 98 | routeFiles := []string{ 99 | "../fixtures/config/routes.json", 100 | "../fixtures/config/routes.yml", 101 | "../fixtures/config/routes.toml", 102 | "../fixtures/config/routes.hcl", 103 | } 104 | 105 | for _, file := range routeFiles { 106 | r := NewRouter() 107 | 108 | err := r.LoadRoutesFile(file) 109 | if err != nil { 110 | t.Error(err) 111 | } 112 | if len(r.routes) != 2 { 113 | t.Errorf("expcted 2 got %d", len(r.routes)) 114 | } 115 | _ = r.Add(controller.GetCtrlFunc(NewSample())) 116 | 117 | req, err := http.NewRequest("GET", "/hello", nil) 118 | if err != nil { 119 | t.Error(err) 120 | } 121 | w := httptest.NewRecorder() 122 | r.ServeHTTP(w, req) 123 | 124 | if w.Code != http.StatusOK { 125 | t.Errorf("expected %d got %d", http.StatusOK, w.Code) 126 | } 127 | if w.Body.String() != msg { 128 | t.Errorf("expected %s got %s", msg, w.Body.String()) 129 | } 130 | } 131 | 132 | } 133 | 134 | func TestSplitRoutes(t *testing.T) { 135 | data := []struct { 136 | routeStr, ctrl, fn string 137 | }{ 138 | { 139 | "get,post;/;Hello.Home", "Hello", "Home", 140 | }, 141 | { 142 | "get,post;/;Home", "", "Home", 143 | }, 144 | } 145 | 146 | for _, v := range data { 147 | r, err := splitRoutes(v.routeStr) 148 | if err != nil { 149 | t.Fatal(err) 150 | } 151 | if r.ctrl != v.ctrl { 152 | t.Errorf("expected %s got %s", v.ctrl, r.ctrl) 153 | } 154 | if r.fn != v.fn { 155 | t.Errorf("extected %s got %s", v.fn, r.fn) 156 | } 157 | } 158 | } 159 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![utron logo](utron.png) 2 | 3 | # utron 4 | [![GoDoc](https://godoc.org/github.com/gernest/utron?status.svg)](https://godoc.org/github.com/gernest/utron) [![Coverage Status](https://coveralls.io/repos/gernest/utron/badge.svg?branch=master&service=github)](https://coveralls.io/github/gernest/utron?branch=master) [![Build Status](https://travis-ci.org/gernest/utron.svg)](https://travis-ci.org/gernest/utron) [![Join the chat at https://gitter.im/gernest/utron](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/gernest/utron?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) [![Go Report Card](https://goreportcard.com/badge/github.com/gernest/utron)](https://goreportcard.com/report/github.com/gernest/utron) 5 | 6 | 7 | `utron` is a lightweight MVC framework in Go ([Golang](https://golang.org)) for building fast, scalable and robust database-driven web applications. 8 | 9 | # Features 10 | * [x] Postgres, MySQL, SQLite and Foundation database support 11 | * [x] Modular (you can choose which components to use) 12 | * [x] Middleware support. All [alice](https://github.com/justinas/alice) compatible Middleware works out of the box 13 | * [x] Gopher spirit (write golang, use all the golang libraries you like) 14 | * [x] Lightweight. Only MVC 15 | * [x] Multiple configuration files support (currently json, yaml, toml and hcl) 16 | 17 | 18 | 19 | # Overview 20 | `utron` is a lightweight MVC framework. It is based on the principles of simplicity, relevance and elegance. 21 | 22 | * Simplicity. The design is simple, easy to understand, and doesn't introduce many layers between you and the standard library. It is a goal of the project that users should be able to understand the whole framework in a single day. 23 | 24 | * Relevance. `utron` doesn't assume anything. We focus on things that matter, this way we are able to ensure easy maintenance and keep the system well-organized, well-planned and sweet. 25 | 26 | * Elegance. `utron` uses golang best practises. We are not afraid of heights, it's just that we need a parachute in our backpack. The source code is heavily documented, any functionality should be well explained and well tested. 27 | 28 | ## Motivation 29 | After two years of playing with golang, I have looked on some of my projects and asked myself: "How golang is that?" 30 | 31 | So, `utron` is my reimagining of lightweight MVC, that maintains the golang spirit, and works seamlessly with the current libraries. 32 | 33 | 34 | ## Installation 35 | 36 | `utron` works with Go 1.4+ 37 | 38 | go get github.com/gernest/utron 39 | 40 | For the Old API use 41 | 42 | go get gopkg.in/gernest/utron.v1 43 | 44 | # Tutorials 45 | 46 | - [create a todo list application with utron](https://github.com/utronframework/tutorials/blob/master/create_todo_list_application_with_utron.md) 47 | - [use custom not found handler in utron](https://github.com/utronframework/tutorials/blob/master/set_not_found_handler.md) 48 | 49 | ## Sample application 50 | 51 | - [Todo list](https://github.com/utronframework/todo) 52 | - [File Upload](https://github.com/utronframework/upload) 53 | - [chat](https://github.com/utronframework/chat) 54 | 55 | 56 | # Contributing 57 | 58 | Start with clicking the star button to make the author and his neighbors happy. Then fork the repository and submit a pull request for whatever change you want to be added to this project. 59 | 60 | If you have any questions, just open an issue. 61 | 62 | # Author 63 | Geofrey Ernest 64 | 65 | Twitter : [@gernesti](https://twitter.com/gernesti) 66 | 67 | 68 | 69 | # Acknowledgements 70 | These amazing projects have made `utron` possible: 71 | 72 | * [gorilla mux](https://github.com/gorilla/mux) 73 | * [ita](https://github.com/gernest/ita) 74 | * [gorm](https://github.com/jinzhu/gorm) 75 | * [alice](https://github.com/justinas/alice) 76 | * [golang](http://golang.org) 77 | 78 | 79 | # Licence 80 | 81 | This project is released under the MIT licence. See [LICENCE](LICENCE) for more details. 82 | -------------------------------------------------------------------------------- /base/context.go: -------------------------------------------------------------------------------- 1 | // Package base is the basic building cblock of utron. The main structure here is 2 | // Context, but for some reasons to avoid confusion since there is a lot of 3 | // context packages I decided to name this package base instead. 4 | package base 5 | 6 | import ( 7 | "bytes" 8 | "errors" 9 | "io" 10 | "net/http" 11 | 12 | "github.com/gernest/utron/config" 13 | "github.com/gernest/utron/logger" 14 | "github.com/gernest/utron/models" 15 | "github.com/gernest/utron/view" 16 | "github.com/gorilla/context" 17 | "github.com/gorilla/mux" 18 | "github.com/gorilla/sessions" 19 | ) 20 | 21 | // Content holds http response content type strings 22 | var Content = struct { 23 | Type string 24 | TextPlain string 25 | TextHTML string 26 | Application struct { 27 | Form, JSON, MultipartForm string 28 | } 29 | }{ 30 | "Content-Type", "text/plain", "text/html", 31 | struct { 32 | Form, JSON, MultipartForm string 33 | }{ 34 | "application/x-www-form-urlencoded", 35 | "application/json", 36 | "multipart/form-data", 37 | }, 38 | } 39 | 40 | // Context wraps request and response. It provides methods for handling responses 41 | type Context struct { 42 | 43 | // Params are the parameters specified in the url patterns 44 | // utron uses gorilla mux for routing. So basically Params stores results 45 | // after calling mux.Vars function . 46 | // 47 | // e.g. if you have route /hello/{world} 48 | // when you make request to /hello/gernest , then 49 | // in the Params, key named world will have value gernest. meaning Params["world"]=="gernest" 50 | Params map[string]string 51 | 52 | // Data keeps values that are going to be passed to the view as context 53 | Data map[string]interface{} 54 | 55 | // Template is the name of the template to be rendered by the view 56 | Template string 57 | 58 | // Cfg is the application configuration 59 | Cfg *config.Config 60 | 61 | //DB is the database stuff, with all models registered 62 | DB *models.Model 63 | 64 | Log logger.Logger 65 | 66 | SessionStore sessions.Store 67 | 68 | request *http.Request 69 | response http.ResponseWriter 70 | out io.ReadWriter 71 | isCommited bool 72 | view view.View 73 | } 74 | 75 | // NewContext creates new context for the given w and r 76 | func NewContext(w http.ResponseWriter, r *http.Request) *Context { 77 | ctx := &Context{ 78 | Params: make(map[string]string), 79 | Data: make(map[string]interface{}), 80 | request: r, 81 | response: w, 82 | out: &bytes.Buffer{}, 83 | } 84 | ctx.Init() 85 | return ctx 86 | } 87 | 88 | // Init initializes the context 89 | func (c *Context) Init() { 90 | c.Params = mux.Vars(c.request) 91 | } 92 | 93 | // Write writes the data to the context, data is written to the http.ResponseWriter 94 | // upon calling Commit(). 95 | // 96 | // data will only be used when Template is not specified and there is no View set. You can use 97 | // this for creating APIs (which does not depend on views like JSON APIs) 98 | func (c *Context) Write(data []byte) (int, error) { 99 | return c.out.Write(data) 100 | } 101 | 102 | // TextPlain renders text/plain response 103 | func (c *Context) TextPlain() { 104 | c.SetHeader(Content.Type, Content.TextPlain) 105 | } 106 | 107 | // JSON renders JSON response 108 | func (c *Context) JSON() { 109 | c.SetHeader(Content.Type, Content.Application.JSON) 110 | } 111 | 112 | // HTML renders text/html response 113 | func (c *Context) HTML() { 114 | c.SetHeader(Content.Type, Content.TextHTML) 115 | } 116 | 117 | // Request returns the *http.Request object used by the context 118 | func (c *Context) Request() *http.Request { 119 | return c.request 120 | } 121 | 122 | // Response returns the http.ResponseWriter object used by the context 123 | func (c *Context) Response() http.ResponseWriter { 124 | return c.response 125 | } 126 | 127 | // GetData retrievess any data stored in the request using 128 | // gorilla.Context package 129 | func (c *Context) GetData(key interface{}) interface{} { 130 | return context.Get(c.Request(), key) 131 | } 132 | 133 | //SetData stores key value into the request object attached with the context. 134 | //this is a helper method, wraping gorilla/context 135 | func (c *Context) SetData(key, value interface{}) { 136 | context.Set(c.Request(), key, value) 137 | } 138 | 139 | // Set sets value in the context object. You can use this to change the following 140 | // 141 | // * Request by passing *http.Request 142 | // * ResponseWriter by passing http.ResponseVritter 143 | // * view by passing View 144 | // * response status code by passing an int 145 | func (c *Context) Set(value interface{}) { 146 | switch value := value.(type) { 147 | case view.View: 148 | c.view = value 149 | case *http.Request: 150 | c.request = value 151 | case http.ResponseWriter: 152 | c.response = value 153 | case int: 154 | c.response.WriteHeader(value) 155 | } 156 | } 157 | 158 | // SetHeader sets response header 159 | func (c *Context) SetHeader(key, value string) { 160 | c.response.Header().Set(key, value) 161 | } 162 | 163 | // Commit writes the results on the underlying http.ResponseWriter and commits the changes. 164 | // This should be called only once, subsequent calls to this will result in an error. 165 | // 166 | // If there is a view, and the template is specified the the view is rendered and its 167 | // output is written to the response, otherwise any data written to the context is written to the 168 | // ResponseWriter. 169 | func (c *Context) Commit() error { 170 | if c.isCommited { 171 | return errors.New("already committed") 172 | } 173 | if c.Template != "" && c.view != nil { 174 | out := &bytes.Buffer{} 175 | err := c.view.Render(out, c.Template, c.Data) 176 | if err != nil { 177 | return err 178 | } 179 | _, _ = io.Copy(c.response, out) 180 | } else { 181 | _, _ = io.Copy(c.response, c.out) 182 | } 183 | c.isCommited = true 184 | return nil 185 | } 186 | 187 | // Redirect redirects request to url using code as status code. 188 | func (c *Context) Redirect(url string, code int) { 189 | http.Redirect(c.Response(), c.Request(), url, code) 190 | } 191 | -------------------------------------------------------------------------------- /config/config.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "encoding/json" 5 | "errors" 6 | "fmt" 7 | "io/ioutil" 8 | "os" 9 | "path/filepath" 10 | "reflect" 11 | "strconv" 12 | "strings" 13 | 14 | "github.com/BurntSushi/toml" 15 | "github.com/fatih/camelcase" 16 | "github.com/gorilla/securecookie" 17 | "github.com/hashicorp/hcl" 18 | "gopkg.in/yaml.v2" 19 | ) 20 | 21 | var errCfgUnsupported = errors.New("utron: config file format not supported") 22 | 23 | // Config stores configurations values 24 | type Config struct { 25 | AppName string `json:"app_name" yaml:"app_name" toml:"app_name" hcl:"app_name"` 26 | BaseURL string `json:"base_url" yaml:"base_url" toml:"base_url" hcl:"base_url"` 27 | Port int `json:"port" yaml:"port" toml:"port" hcl:"port"` 28 | Verbose bool `json:"verbose" yaml:"verbose" toml:"verbose" hcl:"verbose"` 29 | StaticDir string `json:"static_dir" yaml:"static_dir" toml:"static_dir" hcl:"static_dir"` 30 | ViewsDir string `json:"view_dir" yaml:"view_dir" toml:"view_dir" hcl:"view_dir"` 31 | Database string `json:"database" yaml:"database" toml:"database" hcl:"database"` 32 | DatabaseConn string `json:"database_conn" yaml:"database_conn" toml:"database_conn" hcl:"database_conn"` 33 | Automigrate bool `json:"automigrate" yaml:"automigrate" toml:"automigrate" hcl:"automigrate"` 34 | NoModel bool `json:"no_model" yaml:"no_model" toml:"no_model" hcl:"no_model"` 35 | 36 | // session 37 | SessionName string `json:"session_name" yaml:"session_name" toml:"session_name" hcl:"session_name"` 38 | SessionPath string `json:"session_path" yaml:"session_path" toml:"session_path" hcl:"session_path"` 39 | SessionDomain string `json:"session_domain" yaml:"session_domain" toml:"session_domain" hcl:"session_domain"` 40 | SessionMaxAge int `json:"session_max_age" yaml:"session_max_age" toml:"session_max_age" hcl:"session_max_age"` 41 | SessionSecure bool `json:"session_secure" yaml:"session_secure" toml:"session_secure" hcl:"session_secure"` 42 | SessionHTTPOnly bool `json:"session_httponly" yaml:"session_httponly" toml:"session_httponly" hcl:"session_httponly"` 43 | 44 | // The name of the session store to use 45 | // Options are 46 | // file , cookie ,ql 47 | SessionStore string `json:"session_store" yaml:"session_store" toml:"session_store" hcl:"session_store"` 48 | 49 | // Flash is the session name for flash messages 50 | Flash string `json:"flash" yaml:"flash" toml:"flash" hcl:"flash"` 51 | 52 | // KeyPair for secure cookie its a comma separates strings of keys. 53 | SessionKeyPair []string `json:"session_key_pair" yaml:"session_key_pair" toml:"session_key_pair" hcl:"session_key_pair"` 54 | 55 | // flash message 56 | FlashContextKey string `json:"flash_context_key" yaml:"flash_context_key" toml:"flash_context_key" hcl:"flash_context_key"` 57 | } 58 | 59 | // DefaultConfig returns the default configuration settings. 60 | func DefaultConfig() *Config { 61 | a := securecookie.GenerateRandomKey(32) 62 | b := securecookie.GenerateRandomKey(32) 63 | return &Config{ 64 | AppName: "utron web app", 65 | BaseURL: "http://localhost:8090", 66 | Port: 8090, 67 | Verbose: false, 68 | StaticDir: "static", 69 | ViewsDir: "views", 70 | Automigrate: true, 71 | SessionName: "_utron", 72 | SessionPath: "/", 73 | SessionMaxAge: 2592000, 74 | SessionKeyPair: []string{ 75 | string(a), string(b), 76 | }, 77 | Flash: "_flash", 78 | } 79 | } 80 | 81 | // NewConfig reads configuration from path. The format is deduced from the file extension 82 | // * .json - is decoded as json 83 | // * .yml - is decoded as yaml 84 | // * .toml - is decoded as toml 85 | // * .hcl - is decoded as hcl 86 | func NewConfig(path string) (*Config, error) { 87 | _, err := os.Stat(path) 88 | if err != nil { 89 | return nil, err 90 | } 91 | 92 | data, err := ioutil.ReadFile(path) 93 | if err != nil { 94 | return nil, err 95 | } 96 | 97 | cfg := &Config{} 98 | switch filepath.Ext(path) { 99 | case ".json": 100 | jerr := json.Unmarshal(data, cfg) 101 | if jerr != nil { 102 | return nil, jerr 103 | } 104 | case ".toml": 105 | _, terr := toml.Decode(string(data), cfg) 106 | if terr != nil { 107 | return nil, terr 108 | } 109 | case ".yml": 110 | yerr := yaml.Unmarshal(data, cfg) 111 | if yerr != nil { 112 | return nil, yerr 113 | } 114 | case ".hcl": 115 | obj, herr := hcl.Parse(string(data)) 116 | if herr != nil { 117 | return nil, herr 118 | } 119 | if herr = hcl.DecodeObject(&cfg, obj); herr != nil { 120 | return nil, herr 121 | } 122 | default: 123 | return nil, errCfgUnsupported 124 | } 125 | err = cfg.SyncEnv() 126 | if err != nil { 127 | return nil, err 128 | } 129 | 130 | // ensure the key pairs are set 131 | if cfg.SessionKeyPair == nil { 132 | a := securecookie.GenerateRandomKey(32) 133 | b := securecookie.GenerateRandomKey(32) 134 | cfg.SessionKeyPair = []string{ 135 | string(a), string(b), 136 | } 137 | } 138 | return cfg, nil 139 | } 140 | 141 | // SyncEnv overrides c field's values that are set in the environment. 142 | // 143 | // The environment variable names are derived from config fields by underscoring, and uppercasing 144 | // the name. E.g. AppName will have a corresponding environment variable APP_NAME 145 | // 146 | // NOTE only int, string and bool fields are supported and the corresponding values are set. 147 | // when the field value is not supported it is ignored. 148 | func (c *Config) SyncEnv() error { 149 | cfg := reflect.ValueOf(c).Elem() 150 | cTyp := cfg.Type() 151 | 152 | for k := range make([]struct{}, cTyp.NumField()) { 153 | field := cTyp.Field(k) 154 | 155 | cm := getEnvName(field.Name) 156 | env := os.Getenv(cm) 157 | if env == "" { 158 | continue 159 | } 160 | switch field.Type.Kind() { 161 | case reflect.String: 162 | cfg.FieldByName(field.Name).SetString(env) 163 | case reflect.Int: 164 | v, err := strconv.Atoi(env) 165 | if err != nil { 166 | return fmt.Errorf("utron: loading config field %s %v", field.Name, err) 167 | } 168 | cfg.FieldByName(field.Name).Set(reflect.ValueOf(v)) 169 | case reflect.Bool: 170 | b, err := strconv.ParseBool(env) 171 | if err != nil { 172 | return fmt.Errorf("utron: loading config field %s %v", field.Name, err) 173 | } 174 | cfg.FieldByName(field.Name).SetBool(b) 175 | } 176 | 177 | } 178 | return nil 179 | } 180 | 181 | // getEnvName returns all upper case and underscore separated string, from field. 182 | // field is a camel case string. 183 | // 184 | // example 185 | // AppName will change to APP_NAME 186 | func getEnvName(field string) string { 187 | camSplit := camelcase.Split(field) 188 | var rst string 189 | for k, v := range camSplit { 190 | if k == 0 { 191 | rst = strings.ToUpper(v) 192 | continue 193 | } 194 | rst = rst + "_" + strings.ToUpper(v) 195 | } 196 | return rst 197 | } 198 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | cloud.google.com/go v0.27.0 h1:Xa8ZWro6QYKOwDKtxfKsiE0ea2jD39nx32RxtF5RjYE= 2 | cloud.google.com/go v0.27.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= 3 | github.com/BurntSushi/toml v0.3.0 h1:e1/Ivsx3Z0FVTV0NSOv/aVgbUWyQuzj7DDnFblkRvsY= 4 | github.com/BurntSushi/toml v0.3.0/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= 5 | github.com/cznic/b v0.0.0-20180115125044-35e9bbe41f07 h1:UHFGPvSxX4C4YBApSPvmUfL8tTvWLj2ryqvT9K4Jcuk= 6 | github.com/cznic/b v0.0.0-20180115125044-35e9bbe41f07/go.mod h1:URriBxXwVq5ijiJ12C7iIZqlA69nTlI+LgI6/pwftG8= 7 | github.com/cznic/fileutil v0.0.0-20180108211300-6a051e75936f h1:7uSNgsgcarNk4oiN/nNkO0J7KAjlsF5Yv5Gf/tFdHas= 8 | github.com/cznic/fileutil v0.0.0-20180108211300-6a051e75936f/go.mod h1:8S58EK26zhXSxzv7NQFpnliaOQsmDUxvoQO3rt154Vg= 9 | github.com/cznic/golex v0.0.0-20170803123110-4ab7c5e190e4 h1:CVAqftqbj+exlab+8KJQrE+kNIVlQfJt58j4GxCMF1s= 10 | github.com/cznic/golex v0.0.0-20170803123110-4ab7c5e190e4/go.mod h1:+bmmJDNmKlhWNG+gwWCkaBoTy39Fs+bzRxVBzoTQbIc= 11 | github.com/cznic/internal v0.0.0-20180608152220-f44710a21d00 h1:FHpbUtp2K8X53/b4aFNj4my5n+i3x+CQCZWNuHWH/+E= 12 | github.com/cznic/internal v0.0.0-20180608152220-f44710a21d00/go.mod h1:olo7eAdKwJdXxb55TKGLiJ6xt1H0/tiiRCWKVLmtjY4= 13 | github.com/cznic/lldb v1.1.0 h1:AIA+ham6TSJ+XkMe8imQ/g8KPzMUVWAwqUQQdtuMsHs= 14 | github.com/cznic/lldb v1.1.0/go.mod h1:FIZVUmYUVhPwRiPzL8nD/mpFcJ/G7SSXjjXYG4uRI3A= 15 | github.com/cznic/mathutil v0.0.0-20180504122225-ca4c9f2c1369 h1:XNT/Zf5l++1Pyg08/HV04ppB0gKxAqtZQBRYiYrUuYk= 16 | github.com/cznic/mathutil v0.0.0-20180504122225-ca4c9f2c1369/go.mod h1:e6NPNENfs9mPDVNRekM7lKScauxd5kXTr1Mfyig6TDM= 17 | github.com/cznic/ql v1.2.0 h1:lcKp95ZtdF0XkWhGnVIXGF8dVD2X+ClS08tglKtf+ak= 18 | github.com/cznic/ql v1.2.0/go.mod h1:FbpzhyZrqr0PVlK6ury+PoW3T0ODUV22OeWIxcaOrSE= 19 | github.com/cznic/sortutil v0.0.0-20150617083342-4c7342852e65 h1:hxuZop6tSoOi0sxFzoGGYdRqNrPubyaIf9KoBG9tPiE= 20 | github.com/cznic/sortutil v0.0.0-20150617083342-4c7342852e65/go.mod h1:q2w6Bg5jeox1B+QkJ6Wp/+Vn0G/bo3f1uY7Fn3vivIQ= 21 | github.com/cznic/strutil v0.0.0-20171016134553-529a34b1c186 h1:0rkFMAbn5KBKNpJyHQ6Prb95vIKanmAe62KxsrN+sqA= 22 | github.com/cznic/strutil v0.0.0-20171016134553-529a34b1c186/go.mod h1:AHHPPPXTw0h6pVabbcbyGRK1DckRn7r/STdZEeIDzZc= 23 | github.com/cznic/zappy v0.0.0-20160723133515-2533cb5b45cc h1:YKKpTb2BrXN2GYyGaygIdis1vXbE7SSAG9axGWIMClg= 24 | github.com/cznic/zappy v0.0.0-20160723133515-2533cb5b45cc/go.mod h1:Y1SNZ4dRUOKXshKUbwUapqNncRrho4mkjQebgEHZLj8= 25 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 26 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 27 | github.com/denisenkom/go-mssqldb v0.0.0-20180901172138-1eb28afdf9b6 h1:BZGp1dbKFjqlGmxEpwkDpCWNxVwEYnUPoncIzLiHlPo= 28 | github.com/denisenkom/go-mssqldb v0.0.0-20180901172138-1eb28afdf9b6/go.mod h1:xN/JuLBIz4bjkxNmByTiV1IbhfnYb6oo99phBn4Eqhc= 29 | github.com/edsrzf/mmap-go v0.0.0-20170320065105-0bce6a688712 h1:aaQcKT9WumO6JEJcRyTqFVq4XUZiUcKR2/GI31TOcz8= 30 | github.com/edsrzf/mmap-go v0.0.0-20170320065105-0bce6a688712/go.mod h1:YO35OhQPt3KJa3ryjFM5Bs14WD66h8eGKpfaBNrHW5M= 31 | github.com/erikstmartin/go-testdb v0.0.0-20160219214506-8d10e4a1bae5 h1:Yzb9+7DPaBjB8zlTR87/ElzFsnQfuHnVUVqpZZIcV5Y= 32 | github.com/erikstmartin/go-testdb v0.0.0-20160219214506-8d10e4a1bae5/go.mod h1:a2zkGnVExMxdzMo3M0Hi/3sEU+cWnZpSni0O6/Yb/P0= 33 | github.com/fatih/camelcase v1.0.0 h1:hxNvNX/xYBp0ovncs8WyWZrOrpBNub/JfaMvbURyft8= 34 | github.com/fatih/camelcase v1.0.0/go.mod h1:yN2Sb0lFhZJUdVvtELVWefmrXpuZESvPmqwoZc+/fpc= 35 | github.com/gernest/ita v0.0.0-20161218171910-00d04c1bb701 h1:mTEmLCxrBnku6cZcJmjPxkh+NcM3VgVB9NGTiU5IgPQ= 36 | github.com/gernest/ita v0.0.0-20161218171910-00d04c1bb701/go.mod h1:xd2z3+moreNPluO6sb8zZB+OhuIQ35LT4+X+qJDXpa4= 37 | github.com/gernest/qlstore v0.0.0-20161224085350-646d93e25ad3 h1:Wq10nRQdXJVcFT2XJESXTvgM8CSEkG2lkl2Cb0Wk7OM= 38 | github.com/gernest/qlstore v0.0.0-20161224085350-646d93e25ad3/go.mod h1:sE2aPMPaBgyQR3fHW/wY27ypmf2WKYSzD8ekVcFR3RQ= 39 | github.com/go-sql-driver/mysql v1.4.0 h1:7LxgVwFb2hIQtMm87NdgAVfXjnt4OePseqT1tKx+opk= 40 | github.com/go-sql-driver/mysql v1.4.0/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w= 41 | github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db h1:woRePGFeVFfLKN/pOkfl+p/TAqKOfFu+7KPlMVpok/w= 42 | github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= 43 | github.com/google/go-cmp v0.2.0 h1:+dTQ8DZQJz0Mb/HjFlkptS1FeQ4cWSnN941F8aEG4SQ= 44 | github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= 45 | github.com/gorilla/context v1.1.1 h1:AWwleXJkX/nhcU9bZSnZoi3h/qGYqQAGhq6zZe/aQW8= 46 | github.com/gorilla/context v1.1.1/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51q0aT7Yg= 47 | github.com/gorilla/mux v1.6.2 h1:Pgr17XVTNXAk3q/r4CpKzC5xBM/qW1uVLV+IhRZpIIk= 48 | github.com/gorilla/mux v1.6.2/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs= 49 | github.com/gorilla/securecookie v1.1.1 h1:miw7JPhV+b/lAHSXz4qd/nN9jRiAFV5FwjeKyCS8BvQ= 50 | github.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+HVt/4epWDjd4= 51 | github.com/gorilla/sessions v1.1.2 h1:4esMHhwKLQ9Odtku/p+onvH+eRJFWjV4y3iTDVWrZNU= 52 | github.com/gorilla/sessions v1.1.2/go.mod h1:8KCfur6+4Mqcc6S0FEfKuN15Vl5MgXW92AE8ovaJD0w= 53 | github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= 54 | github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= 55 | github.com/jinzhu/gorm v1.9.1 h1:lDSDtsCt5AGGSKTs8AHlSDbbgif4G4+CKJ8ETBDVHTA= 56 | github.com/jinzhu/gorm v1.9.1/go.mod h1:Vla75njaFJ8clLU1W44h34PjIkijhjHIYnZxMqCdxqo= 57 | github.com/jinzhu/inflection v0.0.0-20180308033659-04140366298a h1:eeaG9XMUvRBYXJi4pg1ZKM7nxc5AfXfojeLLW7O5J3k= 58 | github.com/jinzhu/inflection v0.0.0-20180308033659-04140366298a/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc= 59 | github.com/jinzhu/now v0.0.0-20180511015916-ed742868f2ae h1:8bBMcboXYVuo0WYH+rPe5mB8obO89a993hdTZ3phTjc= 60 | github.com/jinzhu/now v0.0.0-20180511015916-ed742868f2ae/go.mod h1:oHTiXerJ20+SfYcrdlBO7rzZRJWGwSTQ0iUY2jI6Gfc= 61 | github.com/justinas/alice v0.0.0-20171023064455-03f45bd4b7da h1:5y58+OCjoHCYB8182mpf/dEsq0vwTKPOo4zGfH0xW9A= 62 | github.com/justinas/alice v0.0.0-20171023064455-03f45bd4b7da/go.mod h1:oLH0CmIaxCGXD67VKGR5AacGXZSMznlmeqM8RzPrcY8= 63 | github.com/lib/pq v1.0.0 h1:X5PMW56eZitiTeO7tKzZxFCSpbFZJtkMMooicw2us9A= 64 | github.com/lib/pq v1.0.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= 65 | github.com/mattn/go-sqlite3 v1.9.0 h1:pDRiWfl+++eC2FEFRy6jXmQlvp4Yh3z1MJKg4UeYM/4= 66 | github.com/mattn/go-sqlite3 v1.9.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc= 67 | golang.org/x/crypto v0.0.0-20180910181607-0e37d006457b h1:2b9XGzhjiYsYPnKXoEfL7klWZQIt8IfyRCz62gCqqlQ= 68 | golang.org/x/crypto v0.0.0-20180910181607-0e37d006457b/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= 69 | google.golang.org/appengine v1.1.0 h1:igQkv0AAhEIvTEpD5LIpAfav2eeVO9HBTjvKHVJPRSs= 70 | google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= 71 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= 72 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 73 | gopkg.in/yaml.v2 v2.2.1 h1:mUhvW9EsL+naU5Q3cakzfE91YhliOondGd6ZrsDBHQE= 74 | gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 75 | -------------------------------------------------------------------------------- /app/app.go: -------------------------------------------------------------------------------- 1 | package app 2 | 3 | import ( 4 | "database/sql" 5 | "errors" 6 | "fmt" 7 | "net/http" 8 | "os" 9 | "path/filepath" 10 | 11 | "github.com/gernest/qlstore" 12 | "github.com/gernest/utron/config" 13 | "github.com/gernest/utron/controller" 14 | "github.com/gernest/utron/logger" 15 | "github.com/gernest/utron/models" 16 | "github.com/gernest/utron/router" 17 | "github.com/gernest/utron/view" 18 | "github.com/gorilla/sessions" 19 | // load ql drier 20 | _ "github.com/cznic/ql/driver" 21 | ) 22 | 23 | //StaticServerFunc is a function that returns the static assetsfiles server. 24 | // 25 | // The first argument retrued is the path prefix for the static assets. If strp 26 | // is set to true then the prefix is going to be stripped. 27 | type StaticServerFunc func(*config.Config) (prefix string, strip bool, h http.Handler) 28 | 29 | // App is the main utron application. 30 | type App struct { 31 | Router *router.Router 32 | Config *config.Config 33 | View view.View 34 | Log logger.Logger 35 | Model *models.Model 36 | ConfigPath string 37 | StaticServer StaticServerFunc 38 | SessionStore sessions.Store 39 | isInit bool 40 | } 41 | 42 | // NewApp creates a new bare-bone utron application. To use the MVC components, you should call 43 | // the Init method before serving requests. 44 | func NewApp() *App { 45 | return &App{ 46 | Log: logger.NewDefaultLogger(os.Stdout), 47 | Router: router.NewRouter(), 48 | Model: models.NewModel(), 49 | } 50 | } 51 | 52 | // NewMVC creates a new MVC utron app. If cfg is passed, it should be a directory to look for 53 | // the configuration files. The App returned is initialized. 54 | func NewMVC(cfg ...string) (*App, error) { 55 | app := NewApp() 56 | if len(cfg) > 0 { 57 | app.SetConfigPath(cfg[0]) 58 | } 59 | if err := app.Init(); err != nil { 60 | return nil, err 61 | } 62 | return app, nil 63 | } 64 | 65 | //StaticServer implements StaticServerFunc. 66 | // 67 | // This uses the http.Fileserver to handle static assets. The routes prefixed 68 | // with /static/ are static asset routes by default. 69 | func StaticServer(cfg *config.Config) (string, bool, http.Handler) { 70 | static, _ := getAbsolutePath(cfg.StaticDir) 71 | if static != "" { 72 | return "/static/", true, http.FileServer(http.Dir(static)) 73 | } 74 | return "", false, nil 75 | } 76 | 77 | func (a *App) options() *router.Options { 78 | return &router.Options{ 79 | Model: a.Model, 80 | View: a.View, 81 | Config: a.Config, 82 | Log: a.Log, 83 | SessionStore: a.SessionStore, 84 | } 85 | } 86 | 87 | // Init initializes the MVC App. 88 | func (a *App) Init() error { 89 | if a.ConfigPath == "" { 90 | a.SetConfigPath("config") 91 | } 92 | return a.init() 93 | } 94 | 95 | // SetConfigPath sets the directory path to search for the config files. 96 | func (a *App) SetConfigPath(dir string) { 97 | a.ConfigPath = dir 98 | } 99 | 100 | // init initializes values to the app components. 101 | func (a *App) init() error { 102 | appConfig, err := loadConfig(a.ConfigPath) 103 | if err != nil { 104 | return err 105 | } 106 | a.Config = appConfig 107 | 108 | views, err := view.NewSimpleView(appConfig.ViewsDir) 109 | if err != nil { 110 | return err 111 | } 112 | a.View = views 113 | 114 | // only when mode is allowed 115 | if !appConfig.NoModel { 116 | model := models.NewModel() 117 | err = model.OpenWithConfig(appConfig) 118 | if err != nil { 119 | return err 120 | } 121 | a.Model = model 122 | } 123 | 124 | // The sessionistore s really not critical. The application can just run 125 | // without session set 126 | store, err := getSesionStore(appConfig) 127 | if err == nil { 128 | a.SessionStore = store 129 | } 130 | 131 | a.Router.Options = a.options() 132 | a.Router.LoadRoutes(a.ConfigPath) // Load a routes file if available. 133 | a.isInit = true 134 | 135 | // In case the StaticDir is specified in the Config file, register 136 | // a handler serving contents of that directory under the PathPrefix /static/. 137 | if appConfig.StaticDir != "" { 138 | static, _ := getAbsolutePath(appConfig.StaticDir) 139 | if static != "" { 140 | a.Router.PathPrefix("/static/").Handler(http.StripPrefix("/static/", http.FileServer(http.Dir(static)))) 141 | } 142 | 143 | } 144 | return nil 145 | } 146 | 147 | func getSesionStore(cfg *config.Config) (sessions.Store, error) { 148 | opts := &sessions.Options{ 149 | Path: cfg.SessionPath, 150 | Domain: cfg.SessionDomain, 151 | MaxAge: cfg.SessionMaxAge, 152 | Secure: cfg.SessionSecure, 153 | HttpOnly: cfg.SessionSecure, 154 | } 155 | db, err := sql.Open("ql-mem", "session.db") 156 | if err != nil { 157 | return nil, err 158 | } 159 | err = qlstore.Migrate(db) 160 | if err != nil { 161 | return nil, err 162 | } 163 | 164 | store := qlstore.NewQLStore(db, "/", 2592000, keyPairs(cfg.SessionKeyPair)...) 165 | store.Options = opts 166 | return store, nil 167 | } 168 | 169 | func keyPairs(src []string) [][]byte { 170 | var pairs [][]byte 171 | for _, v := range src { 172 | pairs = append(pairs, []byte(v)) 173 | } 174 | return pairs 175 | } 176 | 177 | // getAbsolutePath returns the absolute path to dir. If the dir is relative, then we add 178 | // the current working directory. Checks are made to ensure the directory exist. 179 | // In case of any error, an empty string is returned. 180 | func getAbsolutePath(dir string) (string, error) { 181 | info, err := os.Stat(dir) 182 | if err != nil { 183 | return "", err 184 | } 185 | if !info.IsDir() { 186 | return "", fmt.Errorf("untron: %s is not a directory", dir) 187 | } 188 | 189 | if filepath.IsAbs(dir) { // If dir is already absolute, return it. 190 | return dir, nil 191 | } 192 | wd, err := os.Getwd() 193 | if err != nil { 194 | return "", err 195 | } 196 | absDir := filepath.Join(wd, dir) 197 | _, err = os.Stat(absDir) 198 | if err != nil { 199 | return "", err 200 | } 201 | return absDir, nil 202 | } 203 | 204 | // loadConfig loads the configuration file. If cfg is provided, then it is used as the directory 205 | // for searching the configuration files. It defaults to the directory named config in the current 206 | // working directory. 207 | func loadConfig(cfg ...string) (*config.Config, error) { 208 | cfgDir := "config" 209 | if len(cfg) > 0 { 210 | cfgDir = cfg[0] 211 | } 212 | 213 | // Load configurations. 214 | cfgFile, err := findConfigFile(cfgDir, "app") 215 | if err != nil { 216 | return nil, err 217 | } 218 | return config.NewConfig(cfgFile) 219 | } 220 | 221 | // findConfigFile finds the configuration file name in the directory dir. 222 | func findConfigFile(dir string, name string) (file string, err error) { 223 | extensions := []string{".json", ".toml", ".yml", ".hcl"} 224 | 225 | for _, ext := range extensions { 226 | file = filepath.Join(dir, name) 227 | if info, serr := os.Stat(file); serr == nil && !info.IsDir() { 228 | return 229 | } 230 | file = file + ext 231 | if info, serr := os.Stat(file); serr == nil && !info.IsDir() { 232 | return 233 | } 234 | } 235 | return "", fmt.Errorf("utron: can't find configuration file %s in %s", name, dir) 236 | } 237 | 238 | // AddController registers a controller, and middlewares if any is provided. 239 | func (a *App) AddController(ctrlfn func() controller.Controller, middlewares ...interface{}) { 240 | _ = a.Router.Add(ctrlfn, middlewares...) 241 | } 242 | 243 | // ServeHTTP serves http requests. It can be used with other http.Handler implementations. 244 | func (a *App) ServeHTTP(w http.ResponseWriter, r *http.Request) { 245 | a.Router.ServeHTTP(w, r) 246 | } 247 | 248 | //SetNotFoundHandler this sets the hadler that is will execute when the route is 249 | //not found. 250 | func (a *App) SetNotFoundHandler(h http.Handler) error { 251 | if a.Router != nil { 252 | a.Router.NotFoundHandler = h 253 | return nil 254 | } 255 | return errors.New("untron: application router is not set") 256 | } 257 | -------------------------------------------------------------------------------- /router/routes.go: -------------------------------------------------------------------------------- 1 | package router 2 | 3 | import ( 4 | "encoding/json" 5 | "errors" 6 | "fmt" 7 | "io/ioutil" 8 | "net/http" 9 | "os" 10 | "path/filepath" 11 | "reflect" 12 | "strings" 13 | 14 | "github.com/BurntSushi/toml" 15 | "github.com/gernest/ita" 16 | "github.com/gernest/utron/base" 17 | "github.com/gernest/utron/config" 18 | "github.com/gernest/utron/controller" 19 | "github.com/gernest/utron/logger" 20 | "github.com/gernest/utron/models" 21 | "github.com/gernest/utron/view" 22 | "github.com/gorilla/mux" 23 | "github.com/gorilla/sessions" 24 | "github.com/hashicorp/hcl" 25 | "github.com/justinas/alice" 26 | "gopkg.in/yaml.v2" 27 | ) 28 | 29 | var ( 30 | 31 | // ErrRouteStringFormat is returned when the route string is of the wrong format 32 | ErrRouteStringFormat = errors.New("wrong route string, example is\" get,post;/hello/world;Hello\"") 33 | 34 | defaultLogger = logger.NewDefaultLogger(os.Stdout) 35 | ) 36 | 37 | // Router registers routes and handlers. It embeds gorilla mux Router 38 | type Router struct { 39 | *mux.Router 40 | config *config.Config 41 | routes []*route 42 | Options *Options 43 | } 44 | 45 | //Options additional settings for the router. 46 | type Options struct { 47 | Model *models.Model 48 | View view.View 49 | Config *config.Config 50 | Log logger.Logger 51 | SessionStore sessions.Store 52 | } 53 | 54 | // NewRouter returns a new Router, if app is passed then it is used 55 | func NewRouter(app ...*Options) *Router { 56 | r := &Router{ 57 | Router: mux.NewRouter(), 58 | } 59 | if len(app) > 0 { 60 | o := app[0] 61 | r.Options = o 62 | } 63 | return r 64 | } 65 | 66 | // route tracks information about http route 67 | type route struct { 68 | pattern string // url pattern e.g /home 69 | methods []string // http methods e.g GET, POST etc 70 | ctrl string // the name of the controller 71 | fn string // the name of the controller's method to be executed 72 | } 73 | 74 | // Add registers ctrl. It takes additional comma separated list of middleware. middlewares 75 | // are of type 76 | // func(http.Handler)http.Handler 77 | // or 78 | // func(*base.Context)error 79 | // 80 | // utron uses the alice package to chain middlewares, this means all alice compatible middleware 81 | // works out of the box 82 | func (r *Router) Add(ctrlfn func() controller.Controller, middlewares ...interface{}) error { 83 | var ( 84 | 85 | // routes is a slice of all routes associated 86 | // with ctrl 87 | routes = struct { 88 | inCtrl, standard []*route 89 | }{} 90 | 91 | // baseController is the name of the Struct BaseController 92 | // when users embed the BaseController, an anonymous field 93 | // BaseController is added, and here we are referring to the name of the 94 | // anonymous field 95 | baseController = "BaseController" 96 | 97 | // routePaths is the name of the field that allows uses to add Routes information 98 | routePaths = "Routes" 99 | ) 100 | 101 | baseCtr := reflect.ValueOf(&controller.BaseController{}) 102 | ctrlVal := reflect.ValueOf(ctrlfn()) 103 | 104 | bTyp := baseCtr.Type() 105 | cTyp := ctrlVal.Type() 106 | 107 | numCtr := cTyp.NumMethod() 108 | 109 | ctrlName := getTypName(cTyp) // The name of the controller 110 | 111 | for v := range make([]struct{}, numCtr) { 112 | method := cTyp.Method(v) 113 | 114 | // skip methods defined by the base controller 115 | if _, bok := bTyp.MethodByName(method.Name); bok { 116 | continue 117 | } 118 | 119 | // patt composes pattern. This can be overridden by routes defined in the Routes 120 | // field of the controller. 121 | // By default the path is of the form /:controller/:method. All http methods will be registered 122 | // for this pattern, meaning it is up to the user to filter out what he/she wants, the easier way 123 | // is to use the Routes field instead 124 | // 125 | // TODD: figure out the way of passing parameters to the method arguments? 126 | patt := "/" + strings.ToLower(ctrlName) + "/" + strings.ToLower(method.Name) 127 | 128 | r := &route{ 129 | pattern: patt, 130 | ctrl: ctrlName, 131 | fn: method.Name, 132 | } 133 | routes.standard = append(routes.standard, r) 134 | } 135 | 136 | // ultimate returns the actual value stored in rVals this means if rVals is a pointer, 137 | // then we return the value that is pointed to. We are dealing with structs, so the returned 138 | // value is of kind reflect.Struct 139 | ultimate := func(rVals reflect.Value) reflect.Value { 140 | val := rVals 141 | switch val.Kind() { 142 | case reflect.Ptr: 143 | val = val.Elem() 144 | } 145 | return val 146 | } 147 | 148 | uCtr := ultimate(ctrlVal) // actual value after dereferencing the pointer 149 | 150 | uCtrTyp := uCtr.Type() // we store the type, so we can use in the next iterations 151 | 152 | for k := range make([]struct{}, uCtr.NumField()) { 153 | // We iterate in all fields, to filter out the user defined methods. We are aware 154 | // of methods inherited from the BaseController. Since we recommend user Controllers 155 | // should embed BaseController 156 | 157 | field := uCtrTyp.Field(k) 158 | 159 | // If we find any field matching BaseController 160 | // This is already initialized , we move to the next field. 161 | if field.Name == baseController { 162 | continue 163 | } 164 | 165 | // If there is any field named Routes, and it is of signature []string 166 | // then the field's value is used to override the patterns defined earlier. 167 | // 168 | // It is not necessary for every user implementation to define method named Routes 169 | // If we can't find it then we just ignore its use and fall-back to defaults. 170 | // 171 | // Route strings, are of the form "httpMethods;path;method" 172 | // where httMethod: is a comma separated http method strings 173 | // e.g GET,POST,PUT. 174 | // The case does not matter, you can use lower case or upper case characters 175 | // or even mixed case, that is get,GET,gET and GeT will all be treated as GET 176 | // 177 | // path: Is a url path or pattern, utron uses gorilla mux package. So, everything you can do 178 | // with gorilla mux url path then you can do here. 179 | // e.g /hello/{world} 180 | // Don't worry about the params, they will be accessible via .Ctx.Params field in your 181 | // controller. 182 | // 183 | // method: The name of the user Controller method to execute for this route. 184 | if field.Name == routePaths { 185 | fieldVal := uCtr.Field(k) 186 | switch fieldVal.Kind() { 187 | case reflect.Slice: 188 | if data, ok := fieldVal.Interface().([]string); ok { 189 | for _, d := range data { 190 | rt, err := splitRoutes(d) 191 | if err != nil { 192 | continue 193 | } 194 | routes.inCtrl = append(routes.inCtrl, rt) 195 | } 196 | 197 | } 198 | } 199 | } 200 | 201 | } 202 | 203 | for _, v := range routes.standard { 204 | 205 | var found bool 206 | 207 | // use routes from the configuration file first 208 | for _, rFile := range r.routes { 209 | if rFile.ctrl == v.ctrl && rFile.fn == v.fn { 210 | if err := r.add(rFile, ctrlfn, middlewares...); err != nil { 211 | return err 212 | } 213 | found = true 214 | } 215 | } 216 | 217 | // if there is no match from the routes file, use the routes defined in the Routes field 218 | if !found { 219 | for _, rFile := range routes.inCtrl { 220 | if rFile.fn == v.fn { 221 | if err := r.add(rFile, ctrlfn, middlewares...); err != nil { 222 | return err 223 | } 224 | found = true 225 | } 226 | } 227 | } 228 | 229 | // resolve to sandard when everything else never matched 230 | if !found { 231 | if err := r.add(v, ctrlfn, middlewares...); err != nil { 232 | return err 233 | } 234 | } 235 | 236 | } 237 | return nil 238 | } 239 | 240 | // getTypName returns a string representing the name of the object typ. 241 | // if the name is defined then it is used, otherwise, the name is derived from the 242 | // Stringer interface. 243 | // 244 | // the stringer returns something like *somepkg.MyStruct, so skip 245 | // the *somepkg and return MyStruct 246 | func getTypName(typ reflect.Type) string { 247 | if typ.Name() != "" { 248 | return typ.Name() 249 | } 250 | split := strings.Split(typ.String(), ".") 251 | return split[len(split)-1] 252 | } 253 | 254 | // splitRoutes harvest the route components from routeStr. 255 | func splitRoutes(routeStr string) (*route, error) { 256 | 257 | // supported contains supported http methods 258 | supported := "GET POST PUT PATCH TRACE PATCH DELETE HEAD OPTIONS" 259 | 260 | // separator is a character used to separate route components from the routes string 261 | separator := ";" 262 | 263 | activeRoute := &route{} 264 | if routeStr != "" { 265 | s := strings.Split(routeStr, separator) 266 | if len(s) != 3 { 267 | return nil, ErrRouteStringFormat 268 | } 269 | 270 | m := strings.Split(s[0], ",") 271 | for _, v := range m { 272 | up := strings.ToUpper(v) 273 | if !strings.Contains(supported, up) { 274 | return nil, ErrRouteStringFormat 275 | } 276 | activeRoute.methods = append(activeRoute.methods, up) 277 | } 278 | p := s[1] 279 | if !strings.Contains(p, "/") { 280 | return nil, ErrRouteStringFormat 281 | } 282 | activeRoute.pattern = p 283 | 284 | fn := strings.Split(s[2], ".") 285 | switch len(fn) { 286 | case 1: 287 | activeRoute.fn = fn[0] 288 | case 2: 289 | activeRoute.ctrl = fn[0] 290 | activeRoute.fn = fn[1] 291 | default: 292 | return nil, ErrRouteStringFormat 293 | } 294 | return activeRoute, nil 295 | 296 | } 297 | return nil, ErrRouteStringFormat 298 | } 299 | 300 | // add registers controller ctrl, using activeRoute. If middlewares are provided, utron uses 301 | // alice package to chain middlewares. 302 | func (r *Router) add(activeRoute *route, ctrlfn func() controller.Controller, middlewares ...interface{}) error { 303 | var m []*Middleware 304 | if len(middlewares) > 0 { 305 | for _, v := range middlewares { 306 | switch v.(type) { 307 | case func(http.Handler) http.Handler: 308 | m = append(m, &Middleware{ 309 | Type: PlainMiddleware, 310 | value: v, 311 | }) 312 | case func(*base.Context) error: 313 | m = append(m, &Middleware{ 314 | Type: CtxMiddleware, 315 | value: v, 316 | }) 317 | 318 | default: 319 | return fmt.Errorf("unsupported middleware %v", v) 320 | } 321 | } 322 | } 323 | route := r.HandleFunc(activeRoute.pattern, func(w http.ResponseWriter, req *http.Request) { 324 | ctx := base.NewContext(w, req) 325 | r.prepareContext(ctx) 326 | chain := chainMiddleware(ctx, m...) 327 | chain.ThenFunc(r.wrapController(ctx, activeRoute.fn, ctrlfn())).ServeHTTP(w, req) 328 | }) 329 | 330 | // register methods if any 331 | if len(activeRoute.methods) > 0 { 332 | route.Methods(activeRoute.methods...) 333 | 334 | } 335 | return nil 336 | } 337 | 338 | func chainMiddleware(ctx *base.Context, wares ...*Middleware) alice.Chain { 339 | if len(wares) > 0 { 340 | var m []alice.Constructor 341 | for _, v := range wares { 342 | m = append(m, v.ToHandler(ctx)) 343 | } 344 | return alice.New(m...) 345 | } 346 | return alice.New() 347 | 348 | } 349 | 350 | // preparebase.Context sets view,config and model on the ctx. 351 | func (r *Router) prepareContext(ctx *base.Context) { 352 | if r.Options != nil { 353 | if r.Options.View != nil { 354 | ctx.Set(r.Options.View) 355 | } 356 | if r.Options.Config != nil { 357 | ctx.Cfg = r.Options.Config 358 | } 359 | if r.Options.Model != nil { 360 | ctx.DB = r.Options.Model 361 | } 362 | if r.Options.Log != nil { 363 | ctx.Log = r.Options.Log 364 | } 365 | if r.Options.SessionStore != nil { 366 | ctx.SessionStore = r.Options.SessionStore 367 | } 368 | } 369 | 370 | // It is a good idea to ensure that a well prepared context always has the 371 | // Log field set. 372 | if ctx.Log == nil { 373 | ctx.Log = defaultLogger 374 | } 375 | } 376 | 377 | // executes the method fn on Controller ctrl, it sets context. 378 | func (r *Router) handleController(ctx *base.Context, fn string, ctrl controller.Controller) { 379 | ctrl.New(ctx) 380 | // execute the method 381 | // TODO: better error handling? 382 | if x := ita.New(ctrl).Call(fn); x.Error() != nil { 383 | ctx.Set(http.StatusInternalServerError) 384 | _, _ = ctx.Write([]byte(x.Error().Error())) 385 | ctx.TextPlain() 386 | _ = ctx.Commit() 387 | return 388 | } 389 | err := ctx.Commit() 390 | if err != nil { 391 | //TODO: Log error 392 | } 393 | } 394 | 395 | // wrapController wraps a controller ctrl with method fn, and returns http.HandleFunc 396 | func (r *Router) wrapController(ctx *base.Context, fn string, ctrl controller.Controller) func(http.ResponseWriter, *http.Request) { 397 | return func(w http.ResponseWriter, req *http.Request) { 398 | r.handleController(ctx, fn, ctrl) 399 | } 400 | } 401 | 402 | type routeFile struct { 403 | Routes []string `json:"routes" toml:"routes" yaml:"routes"` 404 | } 405 | 406 | // LoadRoutesFile loads routes from a json file. Example of the routes file. 407 | // { 408 | // "routes": [ 409 | // "get,post;/hello;Sample.Hello", 410 | // "get,post;/about;Hello.About" 411 | // ] 412 | // } 413 | // 414 | // supported formats are json, toml, yaml and hcl with extension .json, .toml, .yml and .hcl respectively. 415 | // 416 | //TODO refactor the decoding part to a separate function? This part shares the same logic as the 417 | // one found in NewConfig() 418 | func (r *Router) LoadRoutesFile(file string) error { 419 | rFile := &routeFile{} 420 | data, err := ioutil.ReadFile(file) 421 | if err != nil { 422 | return err 423 | } 424 | switch filepath.Ext(file) { 425 | case ".json": 426 | err = json.Unmarshal(data, rFile) 427 | if err != nil { 428 | return err 429 | } 430 | case ".toml": 431 | _, err = toml.Decode(string(data), rFile) 432 | if err != nil { 433 | return err 434 | } 435 | case ".yml": 436 | err = yaml.Unmarshal(data, rFile) 437 | if err != nil { 438 | return err 439 | } 440 | case ".hcl": 441 | obj, err := hcl.Parse(string(data)) 442 | if err != nil { 443 | return err 444 | } 445 | if err = hcl.DecodeObject(&rFile, obj); err != nil { 446 | return err 447 | } 448 | default: 449 | return errors.New("utron: unsupported file format") 450 | } 451 | 452 | for _, v := range rFile.Routes { 453 | parsedRoute, perr := splitRoutes(v) 454 | if perr != nil { 455 | // TODO: log error? 456 | continue 457 | } 458 | r.routes = append(r.routes, parsedRoute) 459 | } 460 | return nil 461 | } 462 | 463 | // LoadRoutes searches for the route file i the cfgPath. The order of file lookup is 464 | // as follows. 465 | // * routes.json 466 | // * routes.toml 467 | // * routes.yml 468 | // * routes.hcl 469 | func (r *Router) LoadRoutes(cfgPath string) { 470 | exts := []string{".json", ".toml", ".yml", ".hcl"} 471 | rFile := "routes" 472 | for _, ext := range exts { 473 | file := filepath.Join(cfgPath, rFile+ext) 474 | _, err := os.Stat(file) 475 | if os.IsNotExist(err) { 476 | continue 477 | } 478 | _ = r.LoadRoutesFile(file) 479 | break 480 | } 481 | } 482 | 483 | // Static registers static handler for path perfix 484 | func (r *Router) Static(prefix string, h http.FileSystem) { 485 | r.PathPrefix(prefix).Handler(http.StripPrefix(prefix, http.FileServer(h))) 486 | } 487 | --------------------------------------------------------------------------------