diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..5b98bb5 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,12 @@ +language: go + +go: + - 1.9.x + - 1.10.x + +before_install: + - go get github.com/mattn/goveralls + +script: + - go test -v -covermode=count -coverprofile=profile.cov . + - $HOME/gopath/bin/goveralls -coverprofile=profile.cov -service=travis-ci diff --git a/README.md b/README.md index 5a552f5..f45de2c 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,7 @@ # Hime +[![Build Status](https://travis-ci.org/acoshift/hime.svg?branch=master)](https://travis-ci.org/acoshift/hime) +[![Coverage Status](https://coveralls.io/repos/github/acoshift/hime/badge.svg?branch=master)](https://coveralls.io/github/acoshift/hime?branch=master) [![Go Report Card](https://goreportcard.com/badge/github.com/acoshift/hime)](https://goreportcard.com/report/github.com/acoshift/hime) [![GoDoc](https://godoc.org/github.com/acoshift/hime?status.svg)](https://godoc.org/github.com/acoshift/hime) @@ -26,7 +28,7 @@ Other framework don't allow this. They have built-in router, framework-specific - Compatible with net/http middlewares without code change - Use standard html/template for view - Built-in core functions for build web server -- Reduce developer bug +- Reduce developer bugs ## What is this framework DO NOT focus @@ -50,7 +52,7 @@ func main() { ListenAndServe(":8080") } -func router(app hime.App) http.Handler { +func router(app *hime.App) http.Handler { mux := http.NewServeMux() mux.Handle(app.Route("index"), hime.H(indexHandler)) return middleware.Chain( diff --git a/app.go b/app.go index c8bdfb8..46a2d16 100644 --- a/app.go +++ b/app.go @@ -2,8 +2,11 @@ package hime import ( "context" + "crypto/tls" "html/template" + "log" "mime" + "net" "net/http" "time" @@ -14,8 +17,36 @@ import ( "github.com/tdewolff/minify/js" ) -type app struct { - srv *http.Server +// App is the hime app +type App struct { + // TLSConfig overrides http.Server TLSConfig + TLSConfig *tls.Config + + // ReadTimeout overrides http.Server ReadTimeout + ReadTimeout time.Duration + + // ReadHeaderTimeout overrides http.Server ReadHeaderTimeout + ReadHeaderTimeout time.Duration + + // WriteTimeout overrides http.Server WriteTimeout + WriteTimeout time.Duration + + // IdleTimeout overrides http.Server IdleTimeout + IdleTimeout time.Duration + + // MaxHeaderBytes overrides http.Server MaxHeaderBytes + MaxHeaderBytes int + + // TLSNextProto overrides http.Server TLSNextProto + TLSNextProto map[string]func(*http.Server, *tls.Conn, http.Handler) + + // ConnState overrides http.Server ConnState + ConnState func(net.Conn, http.ConnState) + + // ErrorLog overrides http.Server ErrorLog + ErrorLog *log.Logger + + srv http.Server handler http.Handler templateFuncs []template.FuncMap templateComponents []string @@ -28,13 +59,6 @@ type app struct { beforeRender middleware.Middleware } -// consts -const ( - defTemplateRoot = "layout" - defTemplateDir = "view" - defShutdownTimeout = 30 * time.Second -) - var ( ctxKeyApp = struct{}{} ) @@ -44,36 +68,18 @@ func init() { } // New creates new app -func New() App { - app := &app{} - app.template = make(map[string]*template.Template) - app.templateRoot = defTemplateRoot - app.templateDir = defTemplateDir - app.routes = make(Routes) - app.globals = make(Globals) - return app +func New() *App { + return &App{} } -// TemplateRoot sets template root to select when load -func (app *app) TemplateRoot(name string) App { - app.templateRoot = name - return app -} - -// TemplateDir sets template dir -func (app *app) TemplateDir(path string) App { - app.templateDir = path - return app -} - -// Handler sets app handler -func (app *app) Handler(h http.Handler) App { +// Handler sets the handler +func (app *App) Handler(h http.Handler) *App { app.handler = h return app } -// Minify sets app minifier -func (app *app) Minify() App { +// Minify enables minify when render html, css, js +func (app *App) Minify() *App { app.minifier = minify.New() app.minifier.AddFunc("text/html", html.Minify) app.minifier.AddFunc("text/css", css.Minify) @@ -81,39 +87,51 @@ func (app *app) Minify() App { return app } -func (app *app) BeforeRender(m middleware.Middleware) App { +// BeforeRender runs given middleware for before render, +// ex. View, JSON, String, Bytes, CopyForm, etc +func (app *App) BeforeRender(m middleware.Middleware) *App { app.beforeRender = m return app } -func (app *app) ServeHTTP(w http.ResponseWriter, r *http.Request) { +func (app *App) ServeHTTP(w http.ResponseWriter, r *http.Request) { ctx := r.Context() ctx = context.WithValue(ctx, ctxKeyApp, app) r = r.WithContext(ctx) app.handler.ServeHTTP(w, r) } -func (app *app) Server(server *http.Server) App { - app.srv = server - return app +func (app *App) configServer(addr string) { + app.srv.TLSConfig = app.TLSConfig + app.srv.ReadTimeout = app.ReadTimeout + app.srv.ReadHeaderTimeout = app.ReadHeaderTimeout + app.srv.WriteTimeout = app.WriteTimeout + app.srv.IdleTimeout = app.IdleTimeout + app.srv.MaxHeaderBytes = app.MaxHeaderBytes + app.srv.TLSNextProto = app.TLSNextProto + app.srv.ConnState = app.ConnState + app.srv.ErrorLog = app.ErrorLog + app.srv.Handler = app + app.srv.Addr = addr } -// ListenAndServe is the shotcut for http.ListenAndServe -func (app *app) ListenAndServe(addr string) error { - if app.srv == nil { - app.srv = &http.Server{ - Addr: addr, - Handler: app, - } - } +// ListenAndServe starts web server +func (app *App) ListenAndServe(addr string) error { + app.configServer(addr) return app.srv.ListenAndServe() } -// GracefulShutdown change app to graceful mode -func (app *app) GracefulShutdown() GracefulShutdownApp { - return &gracefulShutdownApp{ - app: app, - timeout: defShutdownTimeout, +// ListenAndServeTLS starts web server in tls mode +func (app *App) ListenAndServeTLS(addr, certFile, keyFile string) error { + app.configServer(addr) + + return app.srv.ListenAndServeTLS(certFile, keyFile) +} + +// GracefulShutdown returns graceful shutdown server +func (app *App) GracefulShutdown() *GracefulShutdown { + return &GracefulShutdown{ + App: app, } } diff --git a/context.go b/context.go index 2bf14dc..19b82f3 100644 --- a/context.go +++ b/context.go @@ -11,21 +11,21 @@ func NewContext(w http.ResponseWriter, r *http.Request) Context { } func newInternalContext(w http.ResponseWriter, r *http.Request) *appContext { - app, ok := r.Context().Value(ctxKeyApp).(*app) + app, ok := r.Context().Value(ctxKeyApp).(*App) if !ok { panic(ErrAppNotFound) } return newContext(app, w, r) } -func newContext(app *app, w http.ResponseWriter, r *http.Request) *appContext { +func newContext(app *App, w http.ResponseWriter, r *http.Request) *appContext { return &appContext{r.Context(), app, r, w, 0} } type appContext struct { context.Context - app *app + app *App r *http.Request w http.ResponseWriter diff --git a/examples/net-http/main.go b/examples/net-http/main.go index 3309606..11f814b 100644 --- a/examples/net-http/main.go +++ b/examples/net-http/main.go @@ -51,7 +51,7 @@ func main() { } } -func router(app hime.App) http.Handler { +func router(app *hime.App) http.Handler { mux := http.NewServeMux() mux.Handle(app.Route("index"), hime.H(indexHandler)) mux.Handle(app.Route("about"), hime.H(aboutHandler)) diff --git a/global.go b/global.go index 31e775a..549cebb 100644 --- a/global.go +++ b/global.go @@ -1,13 +1,21 @@ package hime -func (app *app) Globals(globals Globals) App { +// Globals registers global constants +func (app *App) Globals(globals Globals) *App { + if app.globals == nil { + app.globals = make(Globals) + } for key, value := range globals { app.globals[key] = value } return app } -func (app *app) Global(key interface{}) interface{} { +// Global gets value from global storage +func (app *App) Global(key interface{}) interface{} { + if app.globals == nil { + return nil + } return app.globals[key] } diff --git a/graceful.go b/graceful.go index c804b74..5be8da5 100644 --- a/graceful.go +++ b/graceful.go @@ -9,59 +9,56 @@ import ( "time" ) -// GracefulShutdownApp - -type gracefulShutdownApp struct { - *app +// GracefulShutdown is the app in graceful shutdown mode +type GracefulShutdown struct { + App *App timeout time.Duration wait time.Duration notiFns []func() beforeFns []func() } -// ShutdownTimeout sets shutdown timeout for graceful shutdown -func (app *gracefulShutdownApp) Timeout(d time.Duration) GracefulShutdownApp { - app.timeout = d - return app +// Timeout sets shutdown timeout for graceful shutdown, +// set to 0 to disable timeout +// +// default is 0 +func (gs *GracefulShutdown) Timeout(d time.Duration) *GracefulShutdown { + gs.timeout = d + return gs } -func (app *gracefulShutdownApp) Wait(d time.Duration) GracefulShutdownApp { - app.wait = d - return app +// Wait sets wait time before shutdown +func (gs *GracefulShutdown) Wait(d time.Duration) *GracefulShutdown { + gs.wait = d + return gs } -func (app *gracefulShutdownApp) Notify(fn func()) GracefulShutdownApp { +// Notify calls fn when receive terminate signal from os +func (gs *GracefulShutdown) Notify(fn func()) *GracefulShutdown { if fn != nil { - app.notiFns = append(app.notiFns, fn) + gs.notiFns = append(gs.notiFns, fn) } - return app + return gs } -func (app *gracefulShutdownApp) Before(fn func()) GracefulShutdownApp { +// Before runs fn before start waiting to SIGTERM +func (gs *GracefulShutdown) Before(fn func()) *GracefulShutdown { if fn != nil { - app.beforeFns = append(app.beforeFns, fn) + gs.beforeFns = append(gs.beforeFns, fn) } - return app + return gs } -// ListenAndServe is the shotcut for http.ListenAndServe -func (app *gracefulShutdownApp) ListenAndServe(addr string) (err error) { - if app.srv == nil { - app.srv = &http.Server{ - Addr: addr, - Handler: app, - } - } - +func (gs *GracefulShutdown) start(listenAndServe func() error) (err error) { serverCtx, cancelServer := context.WithCancel(context.Background()) defer cancelServer() go func() { - if err = app.srv.ListenAndServe(); err != http.ErrServerClosed { + if err = listenAndServe(); err != http.ErrServerClosed { cancelServer() } }() - for _, fn := range app.beforeFns { + for _, fn := range gs.beforeFns { fn() } @@ -72,15 +69,30 @@ func (app *gracefulShutdownApp) ListenAndServe(addr string) (err error) { case <-serverCtx.Done(): return case <-stop: - for _, fn := range app.notiFns { + for _, fn := range gs.notiFns { fn() } - if app.wait > 0 { - time.Sleep(app.wait) + if gs.wait > 0 { + time.Sleep(gs.wait) + } + + if gs.timeout > 0 { + ctx, cancel := context.WithTimeout(context.Background(), gs.timeout) + defer cancel() + err = gs.App.srv.Shutdown(ctx) + } else { + err = gs.App.srv.Shutdown(context.Background()) } - ctx, cancel := context.WithTimeout(context.Background(), app.timeout) - defer cancel() - err = app.srv.Shutdown(ctx) } return } + +// ListenAndServe starts web server in graceful shutdown mode +func (gs *GracefulShutdown) ListenAndServe(addr string) error { + return gs.start(func() error { return gs.App.ListenAndServe(addr) }) +} + +// ListenAndServeTLS starts web server in graceful shutdown and tls mode +func (gs *GracefulShutdown) ListenAndServeTLS(addr, certFile, keyFile string) error { + return gs.start(func() error { return gs.App.ListenAndServeTLS(addr, certFile, keyFile) }) +} diff --git a/hime.go b/hime.go index fee2aa3..9e0cdd9 100644 --- a/hime.go +++ b/hime.go @@ -2,90 +2,12 @@ package hime import ( "context" - "html/template" "io" "mime/multipart" "net/http" "net/url" - "time" - - "github.com/acoshift/middleware" ) -// App is the hime app -type App interface { - http.Handler - - // Builder - - // TemplateDir sets directory to load template, - // default is "view" - TemplateDir(path string) App - - // TemplateRoot sets root layout using t.Lookup, - // default is "layout" - TemplateRoot(name string) App - - // TemplateFuncs adds template funcs while load template - TemplateFuncs(funcs ...template.FuncMap) App - - // Component adds given templates to every templates - Component(filename ...string) App - - // Template loads template into memory - Template(name string, filename ...string) App - - // BeforeRender runs given middleware for before render, - // ex. View, JSON, String, Bytes, CopyForm, etc. - BeforeRender(m middleware.Middleware) App - - // Minify enables minify when render html, css, js - Minify() App - - // Handler sets the handler - Handler(h http.Handler) App - - // Routes registers route name and path - Routes(routes Routes) App - - // Globals registers global constants - Globals(Globals) App - - // Server overrides server when calling ListenAndServe - Server(server *http.Server) App - - // GracefulShutdown runs server as graceful shutdown, - // can works only when start server with app.ListenAndServe - GracefulShutdown() GracefulShutdownApp - - // ListenAndServe starts web server - ListenAndServe(addr string) error - - // Route gets route path from given name - Route(name string, params ...interface{}) string - - // Global gets value from global storage - Global(key interface{}) interface{} -} - -// GracefulShutdownApp is the app in graceful shutdown mode -type GracefulShutdownApp interface { - // Timeout sets timeout - Timeout(d time.Duration) GracefulShutdownApp - - // Wait sets wait time before shutdown - Wait(d time.Duration) GracefulShutdownApp - - // Notify calls fn when receive terminate signal from os - Notify(fn func()) GracefulShutdownApp - - // Before runs fn before start waiting to SIGTERM - Before(fn func()) GracefulShutdownApp - - // ListenAndServe starts web server - ListenAndServe(addr string) error -} - // Routes is the map for route name => path type Routes map[string]string @@ -227,7 +149,4 @@ type Param struct { Value interface{} } -var ( - _ = App(&app{}) - _ = Context(&appContext{}) -) +var _ = Context(&appContext{}) diff --git a/route.go b/route.go index fbd8080..fed6b33 100644 --- a/route.go +++ b/route.go @@ -1,13 +1,21 @@ package hime -func (app *app) Routes(routes Routes) App { +// Routes registers route name and path +func (app *App) Routes(routes Routes) *App { + if app.routes == nil { + app.routes = make(Routes) + } for name, path := range routes { app.routes[name] = path } return app } -func (app *app) Route(name string, params ...interface{}) string { +// Route gets route path from given name +func (app *App) Route(name string, params ...interface{}) string { + if app.routes == nil { + panic(newErrRouteNotFound(name)) + } path, ok := app.routes[name] if !ok { panic(newErrRouteNotFound(name)) diff --git a/template.go b/template.go index 94dfa5a..013c9b2 100644 --- a/template.go +++ b/template.go @@ -5,20 +5,41 @@ import ( "path/filepath" ) -// TemplateFuncs adds template funcs -func (app *app) TemplateFuncs(funcs ...template.FuncMap) App { +// TemplateRoot calls t.Lookup(name) after load template, +// empty string won't trigger t.Lookup +// +// default is "" +func (app *App) TemplateRoot(name string) *App { + app.templateRoot = name + return app +} + +// TemplateDir sets root directory when load template +// +// default is "" +func (app *App) TemplateDir(path string) *App { + app.templateDir = path + return app +} + +// TemplateFuncs adds template funcs while load template +func (app *App) TemplateFuncs(funcs ...template.FuncMap) *App { app.templateFuncs = append(app.templateFuncs, funcs...) return app } -// Component adds global template component -func (app *app) Component(filename ...string) App { +// Component adds given templates to every templates +func (app *App) Component(filename ...string) *App { app.templateComponents = append(app.templateComponents, filename...) return app } -// Template registers new template -func (app *app) Template(name string, filename ...string) App { +// Template loads template into memory +func (app *App) Template(name string, filename ...string) *App { + if app.template == nil { + app.template = make(map[string]*template.Template) + } + if _, ok := app.template[name]; ok { panic(newErrTemplateDuplicate(name)) } @@ -45,8 +66,12 @@ func (app *app) Template(name string, filename ...string) App { fn := make([]string, len(filename)) copy(fn, filename) fn = append(fn, app.templateComponents...) + t = template.Must(t.ParseFiles(joinTemplateDir(app.templateDir, fn...)...)) - t = t.Lookup(app.templateRoot) + + if app.templateRoot != "" { + t = t.Lookup(app.templateRoot) + } app.template[name] = t