From 38c62410551229cd0f4b0c19375237e637cec3e7 Mon Sep 17 00:00:00 2001 From: "Gerasimos (Makis) Maropoulos" Date: Sun, 1 Oct 2017 16:29:25 +0300 Subject: [PATCH] Add a third, simple, example for folder structuring as requested at https://github.com/kataras/iris/issues/748 One change to code base but it will be described at the next version: Error Handlers (`app.OnErrorCode/OnAnyErrorCode`) respect the `app.UseGlobal`'s middlewares now (not the `app.Use` for reasons we can all understand, hopefully). Former-commit-id: ec97bbb04548f9932cf4d7b950be513b70747bcb --- _examples/README.md | 1 + .../handler-based/bootstrap/bootstrapper.go | 113 ++++++++++++++++++ _examples/structuring/handler-based/main.go | 20 ++++ .../structuring/handler-based/main_test.go | 31 +++++ .../middleware/identity/identity.go | 33 +++++ .../handler-based/public/favicon.ico | Bin 0 -> 15086 bytes .../handler-based/routes/follower.go | 11 ++ .../handler-based/routes/following.go | 11 ++ .../structuring/handler-based/routes/index.go | 11 ++ .../structuring/handler-based/routes/like.go | 11 ++ .../handler-based/routes/routes.go | 14 +++ .../handler-based/views/index.html | 1 + .../handler-based/views/shared/error.html | 5 + .../handler-based/views/shared/layout.html | 23 ++++ context/context.go | 13 ++ core/router/api_builder.go | 10 ++ 16 files changed, 308 insertions(+) create mode 100644 _examples/structuring/handler-based/bootstrap/bootstrapper.go create mode 100644 _examples/structuring/handler-based/main.go create mode 100644 _examples/structuring/handler-based/main_test.go create mode 100644 _examples/structuring/handler-based/middleware/identity/identity.go create mode 100644 _examples/structuring/handler-based/public/favicon.ico create mode 100644 _examples/structuring/handler-based/routes/follower.go create mode 100644 _examples/structuring/handler-based/routes/following.go create mode 100644 _examples/structuring/handler-based/routes/index.go create mode 100644 _examples/structuring/handler-based/routes/like.go create mode 100644 _examples/structuring/handler-based/routes/routes.go create mode 100644 _examples/structuring/handler-based/views/index.html create mode 100644 _examples/structuring/handler-based/views/shared/error.html create mode 100644 _examples/structuring/handler-based/views/shared/layout.html diff --git a/_examples/README.md b/_examples/README.md index e0aa242e..7d5f71d5 100644 --- a/_examples/README.md +++ b/_examples/README.md @@ -22,6 +22,7 @@ Structuring is always depends on your needs. We can't tell you how to design you - [Example 1](mvc/login) - [Example 2](structuring/mvc) +- [Example 3](structuring/handler-based) ### HTTP Listening diff --git a/_examples/structuring/handler-based/bootstrap/bootstrapper.go b/_examples/structuring/handler-based/bootstrap/bootstrapper.go new file mode 100644 index 00000000..3257b9a0 --- /dev/null +++ b/_examples/structuring/handler-based/bootstrap/bootstrapper.go @@ -0,0 +1,113 @@ +package bootstrap + +import ( + "time" + + "github.com/gorilla/securecookie" + + "github.com/kataras/iris" + "github.com/kataras/iris/sessions" + "github.com/kataras/iris/websocket" +) + +type Configurator func(*Bootstrapper) + +type Bootstrapper struct { + *iris.Application + AppName string + AppOwner string + AppSpawnDate time.Time + + Sessions *sessions.Sessions +} + +// New returns a new Bootstrapper. +func New(appName, appOwner string, cfgs ...Configurator) *Bootstrapper { + b := &Bootstrapper{ + AppName: appName, + AppOwner: appOwner, + AppSpawnDate: time.Now(), + Application: iris.New(), + } + + for _, cfg := range cfgs { + cfg(b) + } + + return b +} + +// SetupViews loads the templates. +func (b *Bootstrapper) SetupViews(viewsDir string) { + b.RegisterView(iris.HTML(viewsDir, ".html").Layout("shared/layout.html")) +} + +// SetupSessions initializes the sessions, optionally. +func (b *Bootstrapper) SetupSessions(expires time.Duration, cookieHashKey, cookieBlockKey []byte) { + b.Sessions = sessions.New(sessions.Config{ + Cookie: "SECRET_SESS_COOKIE_" + b.AppName, + Expires: expires, + Encoding: securecookie.New(cookieHashKey, cookieBlockKey), + }) +} + +// SetupWebsockets prepares the websocket server. +func (b *Bootstrapper) SetupWebsockets(endpoint string, onConnection websocket.ConnectionFunc) { + ws := websocket.New(websocket.Config{}) + ws.OnConnection(onConnection) + + b.Get(endpoint, ws.Handler()) + b.Any("/iris-ws.js", func(ctx iris.Context) { + ctx.Write(websocket.ClientSource) + }) +} + +// SetupErrorHandlers prepares the http error handlers (>=400). +func (b *Bootstrapper) SetupErrorHandlers() { + b.OnAnyErrorCode(func(ctx iris.Context) { + err := iris.Map{ + "app": b.AppName, + "status": ctx.GetStatusCode(), + "message": ctx.Values().GetString("message"), + } + + if jsonOutput := ctx.URLParamExists("json"); jsonOutput { + ctx.JSON(err) + return + } + + ctx.ViewData("Err", err) + ctx.ViewData("Title", "Error") + ctx.View("shared/error.html") + }) +} + +const ( + // StaticAssets is the root directory for public assets like images, css, js. + StaticAssets = "./public/" + // Favicon is the relative 9to the "StaticAssets") favicon path for our app. + Favicon = "favicon.ico" +) + +// Bootstrap prepares our application. +// +// Returns itself. +func (b *Bootstrapper) Bootstrap() *Bootstrapper { + b.SetupViews("./views") + b.SetupSessions(24*time.Hour, + []byte("the-big-and-secret-fash-key-here"), + []byte("lot-secret-of-characters-big-too"), + ) + b.SetupErrorHandlers() + + // static files + b.Favicon(StaticAssets + Favicon) + b.StaticWeb(StaticAssets[1:len(StaticAssets)-1], StaticAssets) + + return b +} + +// Listen starts the http server with the specified "addr". +func (b *Bootstrapper) Listen(addr string, cfgs ...iris.Configurator) { + b.Run(iris.Addr(addr), cfgs...) +} diff --git a/_examples/structuring/handler-based/main.go b/_examples/structuring/handler-based/main.go new file mode 100644 index 00000000..06704fc5 --- /dev/null +++ b/_examples/structuring/handler-based/main.go @@ -0,0 +1,20 @@ +package main + +import ( + "github.com/kataras/iris/_examples/structuring/handler-based/bootstrap" + "github.com/kataras/iris/_examples/structuring/handler-based/middleware/identity" + "github.com/kataras/iris/_examples/structuring/handler-based/routes" +) + +var app = bootstrap.New("Awesome App", "kataras2006@hotmail.com", + identity.Configure, + routes.Configure, +) + +func init() { + app.Bootstrap() +} + +func main() { + app.Listen(":8080") +} diff --git a/_examples/structuring/handler-based/main_test.go b/_examples/structuring/handler-based/main_test.go new file mode 100644 index 00000000..ba51a081 --- /dev/null +++ b/_examples/structuring/handler-based/main_test.go @@ -0,0 +1,31 @@ +package main + +import ( + "testing" + + "github.com/kataras/iris/httptest" +) + +// TestApp runs by `$ go test -v`. +func TestApp(t *testing.T) { + e := httptest.New(t, app.Application) + + // test our routes + e.GET("/").Expect().Status(httptest.StatusOK) + e.GET("/follower/42").Expect().Status(httptest.StatusOK). + Body().Equal("from /follower/{id:long} with ID: 42") + e.GET("/following/52").Expect().Status(httptest.StatusOK). + Body().Equal("from /following/{id:long} with ID: 52") + e.GET("/like/64").Expect().Status(httptest.StatusOK). + Body().Equal("from /like/{id:long} with ID: 64") + + // test not found + e.GET("/notfound").Expect().Status(httptest.StatusNotFound) + expectedErr := map[string]interface{}{ + "app": app.AppName, + "status": httptest.StatusNotFound, + "message": "", + } + e.GET("/anotfoundwithjson").WithQuery("json", nil). + Expect().Status(httptest.StatusNotFound).JSON().Equal(expectedErr) +} diff --git a/_examples/structuring/handler-based/middleware/identity/identity.go b/_examples/structuring/handler-based/middleware/identity/identity.go new file mode 100644 index 00000000..496d0de8 --- /dev/null +++ b/_examples/structuring/handler-based/middleware/identity/identity.go @@ -0,0 +1,33 @@ +package identity + +import ( + "time" + + "github.com/kataras/iris" + + "github.com/kataras/iris/_examples/structuring/handler-based/bootstrap" +) + +// New returns a new handler which adds some headers and view data +// describing the application, i.e the owner, the startup time. +func New(b *bootstrap.Bootstrapper) iris.Handler { + return func(ctx iris.Context) { + // response headers + ctx.Header("App-Name", b.AppName) + ctx.Header("App-Owner", b.AppOwner) + ctx.Header("App-Since", time.Since(b.AppSpawnDate).String()) + + ctx.Header("Server", "Iris: https://iris-go.com") + + // view data if ctx.View or c.Tmpl = "$page.html" will be called next. + ctx.ViewData("AppName", b.AppName) + ctx.ViewData("AppOwner", b.AppOwner) + ctx.Next() + } +} + +// Configure creates a new identity middleware and registers that to the app. +func Configure(b *bootstrap.Bootstrapper) { + h := New(b) + b.UseGlobal(h) +} diff --git a/_examples/structuring/handler-based/public/favicon.ico b/_examples/structuring/handler-based/public/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..c370da518ec542579b7cc0d5d30f4778b4a96318 GIT binary patch literal 15086 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 \ No newline at end of file diff --git a/_examples/structuring/handler-based/views/shared/error.html b/_examples/structuring/handler-based/views/shared/error.html new file mode 100644 index 00000000..9fde08ad --- /dev/null +++ b/_examples/structuring/handler-based/views/shared/error.html @@ -0,0 +1,5 @@ +

Error.

+

An error occurred while processing your request.

+ +

{{.Err.status}}

+

{{.Err.message}}

\ No newline at end of file diff --git a/_examples/structuring/handler-based/views/shared/layout.html b/_examples/structuring/handler-based/views/shared/layout.html new file mode 100644 index 00000000..cf920444 --- /dev/null +++ b/_examples/structuring/handler-based/views/shared/layout.html @@ -0,0 +1,23 @@ + + + + + + + + {{.Title}} - {{.AppName}} + + + + +
+ + {{ yield }} +
+ +
+ + + \ No newline at end of file diff --git a/context/context.go b/context/context.go index 80e25378..2bfa84c5 100644 --- a/context/context.go +++ b/context/context.go @@ -364,6 +364,8 @@ type Context interface { // | Various Request and Post Data | // +------------------------------------------------------------+ + // URLParam returns true if the url parameter exists, otherwise false. + URLParamExists(name string) bool // URLParamDefault returns the get parameter from a request, if not found then "def" is returned. URLParamDefault(name string, def string) string // URLParam returns the get parameter from a request , if any. @@ -1236,6 +1238,7 @@ func (ctx *context) ContentType(cType string) { cType += "; charset=" + charset } } + ctx.writer.Header().Set(contentTypeHeaderKey, cType) } @@ -1273,6 +1276,16 @@ func (ctx *context) GetStatusCode() int { // | Various Request and Post Data | // +------------------------------------------------------------+ +// URLParam returns true if the url parameter exists, otherwise false. +func (ctx *context) URLParamExists(name string) bool { + if q := ctx.request.URL.Query(); q != nil { + _, exists := q[name] + return exists + } + + return false +} + // URLParamDefault returns the get parameter from a request, if not found then "def" is returned. func (ctx *context) URLParamDefault(name string, def string) string { v := ctx.request.URL.Query().Get(name) diff --git a/core/router/api_builder.go b/core/router/api_builder.go index c3289bcf..a67f683c 100644 --- a/core/router/api_builder.go +++ b/core/router/api_builder.go @@ -534,6 +534,12 @@ func (api *APIBuilder) Controller(relativePath string, controller activator.Base api.reporter.Add("%v for path: '%s'", err, relativePath) } + if cInit, ok := controller.(interface { + Init(activator.RegisterFunc) + }); ok { + cInit.Init(registerFunc) + } + return } @@ -800,6 +806,10 @@ func (api *APIBuilder) StaticWeb(requestPath string, systemPath string) *Route { // and/or disable the gzip if gzip response recorder // was active. func (api *APIBuilder) OnErrorCode(statusCode int, handlers ...context.Handler) { + if len(api.beginGlobalHandlers) > 0 { + handlers = joinHandlers(api.beginGlobalHandlers, handlers) + } + api.errorCodeHandlers.Register(statusCode, handlers...) }