diff --git a/_examples/README.md b/_examples/README.md new file mode 100644 index 00000000..a57889ba --- /dev/null +++ b/_examples/README.md @@ -0,0 +1,33 @@ +## Examples + +This folder provides easy to understand code snippets on how to get started with web development with the Go programming language using the [Iris](https://github.com/kataras/iris) web framework. + + +It doesn't contains "best ways" neither explains all its features. It's just a simple, practical cookbook for young Go developers! + +Developers should read the official [documentation](https://godoc.org/gopkg.in/kataras/iris.v6) in depth. + + + + + +## Table of Contents + +* [Hello World](examples/hello-world/main.go) +* [Routes (using httprouter)](examples/routes-using-httprouter/main.go) +* [Routes (using gorillamux)](examples/routes-using-gorillamux/main.go) +* [Templates](examples/templates/main.go) +* [Forms](examples/forms/main.go) +* [JSON](examples/json/main.go) +* [Upload Files](examples/upload-files/main.go) +* [Static Files](examples/static-files/main.go) +* [Favicon](examples/favicon/main.go) +* [Password Hashing](examples/password-hashing/main.go) +* [Sessions](examples/sessions/main.go) +* [Websockets](examples/websockets/main.go) +* [Markdown and Cache](examples/cache-markdown/main.go) +* [Online Visitors](examples/online-visitors/main.go) +* [URL Shortener](examples/url-shortener/main.go) + + +> Take look at the [community examples](https://github.com/iris-contrib/examples) too! \ No newline at end of file diff --git a/_examples/examples/cache-markdown/main.go b/_examples/examples/cache-markdown/main.go new file mode 100644 index 00000000..724d57dd --- /dev/null +++ b/_examples/examples/cache-markdown/main.go @@ -0,0 +1,82 @@ +package main + +import ( + "time" + + "gopkg.in/kataras/iris.v6" + "gopkg.in/kataras/iris.v6/adaptors/httprouter" +) + +var testMarkdownContents = `## Hello Markdown + +This is a sample of Markdown contents + + + +Features +-------- + +All features of Sundown are supported, including: + +* **Compatibility**. The Markdown v1.0.3 test suite passes with + the --tidy option. Without --tidy, the differences are + mostly in whitespace and entity escaping, where blackfriday is + more consistent and cleaner. + +* **Common extensions**, including table support, fenced code + blocks, autolinks, strikethroughs, non-strict emphasis, etc. + +* **Safety**. Blackfriday is paranoid when parsing, making it safe + to feed untrusted user input without fear of bad things + happening. The test suite stress tests this and there are no + known inputs that make it crash. If you find one, please let me + know and send me the input that does it. + + NOTE: "safety" in this context means *runtime safety only*. In order to + protect yourself against JavaScript injection in untrusted content, see + [this example](https://github.com/russross/blackfriday#sanitize-untrusted-content). + +* **Fast processing**. It is fast enough to render on-demand in + most web applications without having to cache the output. + +* **Thread safety**. You can run multiple parsers in different + goroutines without ill effect. There is no dependence on global + shared state. + +* **Minimal dependencies**. Blackfriday only depends on standard + library packages in Go. The source code is pretty + self-contained, so it is easy to add to any project, including + Google App Engine projects. + +* **Standards compliant**. Output successfully validates using the + W3C validation tool for HTML 4.01 and XHTML 1.0 Transitional. + + [this is a link](https://github.com/kataras/iris) ` + +func main() { + app := iris.New() + // output startup banner and error logs on os.Stdout + app.Adapt(iris.DevLogger()) + // set the router, you can choose gorillamux too + app.Adapt(httprouter.New()) + + app.Get("/hi", app.Cache(func(c *iris.Context) { + c.WriteString("Hi this is a big content, do not try cache on small content it will not make any significant difference!") + }, time.Duration(10)*time.Second)) + + bodyHandler := func(ctx *iris.Context) { + ctx.Markdown(iris.StatusOK, testMarkdownContents) + } + + expiration := time.Duration(5 * time.Second) + + app.Get("/", app.Cache(bodyHandler, expiration)) + + // if expiration is <=time.Second then the cache tries to set the expiration from the "cache-control" maxage header's value(in seconds) + // // if this header doesn't founds then the default is 5 minutes + app.Get("/cache_control", app.Cache(func(ctx *iris.Context) { + ctx.HTML(iris.StatusOK, "

Hello!

") + }, -1)) + + app.Listen(":8080") +} diff --git a/_examples/examples/favicon/main.go b/_examples/examples/favicon/main.go new file mode 100644 index 00000000..14341a12 --- /dev/null +++ b/_examples/examples/favicon/main.go @@ -0,0 +1,23 @@ +package main + +import ( + "gopkg.in/kataras/iris.v6" + "gopkg.in/kataras/iris.v6/adaptors/httprouter" +) + +func main() { + app := iris.New() + app.Adapt(httprouter.New()) + // This will serve the ./static/favicons/iris_favicon_32_32.ico to: localhost:8080/favicon.ico + app.Favicon("./static/favicons/iris_favicon_32_32.ico") + + // app.Favicon("./static/favicons/iris_favicon_32_32.ico", "/favicon_32_32.ico") + // This will serve the ./static/favicons/iris_favicon_32_32.ico to: localhost:8080/favicon_32_32.ico + + app.Get("/", func(ctx *iris.Context) { + ctx.HTML(iris.StatusOK, `You should see the favicon now at the side of your browser, + if not, please refresh or clear the browser's cache.`) + }) + + app.Listen(":8080") +} diff --git a/_examples/examples/favicon/static/favicons/iris_favicon_32_32.ico b/_examples/examples/favicon/static/favicons/iris_favicon_32_32.ico new file mode 100644 index 00000000..4dd556e1 Binary files /dev/null and b/_examples/examples/favicon/static/favicons/iris_favicon_32_32.ico differ diff --git a/_examples/examples/forms/main.go b/_examples/examples/forms/main.go new file mode 100644 index 00000000..0e221703 --- /dev/null +++ b/_examples/examples/forms/main.go @@ -0,0 +1,47 @@ +package main + +import ( + "gopkg.in/kataras/iris.v6" + "gopkg.in/kataras/iris.v6/adaptors/httprouter" + "gopkg.in/kataras/iris.v6/adaptors/view" +) + +// ContactDetails the information from user +type ContactDetails struct { + Email string `form:"email"` + Subject string `form:"subject"` + Message string `form:"message"` +} + +func main() { + app := iris.New() + app.Adapt(httprouter.New()) + + // Parse all files inside `./mytemplates` directory ending with `.html` + app.Adapt(view.HTML("./mytemplates", ".html")) + + app.Get("/", func(ctx *iris.Context) { + ctx.Render("forms.html", nil) + }) + + // Equivalent with app.HandleFunc("POST", ...) + app.Post("/", func(ctx *iris.Context) { + + // details := ContactDetails{ + // Email: ctx.FormValue("email"), + // Subject: ctx.FormValue("subject"), + // Message: ctx.FormValue("message"), + // } + + // or simply: + var details ContactDetails + ctx.ReadForm(&details) + + // do something with details + _ = details + + ctx.Render("forms.html", struct{ Success bool }{true}) + }) + + app.Listen(":8080") +} diff --git a/_examples/examples/forms/mytemplates/forms.html b/_examples/examples/forms/mytemplates/forms.html new file mode 100644 index 00000000..6044d5be --- /dev/null +++ b/_examples/examples/forms/mytemplates/forms.html @@ -0,0 +1,14 @@ +{{if .Success}} +

Thanks for your message!

+{{else}} +

Contact

+
+
+
+
+
+
+
+ +
+{{end}} diff --git a/_examples/examples/hello-world/main.go b/_examples/examples/hello-world/main.go new file mode 100644 index 00000000..8ab1af6e --- /dev/null +++ b/_examples/examples/hello-world/main.go @@ -0,0 +1,20 @@ +package main + +import ( + "gopkg.in/kataras/iris.v6" + "gopkg.in/kataras/iris.v6/adaptors/httprouter" +) + +func main() { + app := iris.New() + // Adapt the "httprouter", faster, + // but it has limits on named path parameters' validation, + // you can adapt "gorillamux" if you need regexp path validation! + app.Adapt(httprouter.New()) + + app.HandleFunc("GET", "/", func(ctx *iris.Context) { + ctx.Writef("hello world\n") + }) + + app.Listen(":8080") +} diff --git a/_examples/examples/json/main.go b/_examples/examples/json/main.go new file mode 100644 index 00000000..b39ee48a --- /dev/null +++ b/_examples/examples/json/main.go @@ -0,0 +1,37 @@ +package main + +import ( + "gopkg.in/kataras/iris.v6" + "gopkg.in/kataras/iris.v6/adaptors/httprouter" +) + +// User bind struct +type User struct { + Firstname string `json:"firstname"` + Lastname string `json:"lastname"` + Age int `json:"age"` +} + +func main() { + app := iris.New() + app.Adapt(httprouter.New()) + + app.Post("/decode", func(ctx *iris.Context) { + var user User + ctx.ReadJSON(&user) + + ctx.Writef("%s %s is %d years old!", user.Firstname, user.Lastname, user.Age) + }) + + app.Get("/encode", func(ctx *iris.Context) { + peter := User{ + Firstname: "John", + Lastname: "Doe", + Age: 25, + } + + ctx.JSON(iris.StatusOK, peter) + }) + + app.Listen(":8080") +} diff --git a/_examples/examples/online-visitors/main.go b/_examples/examples/online-visitors/main.go new file mode 100644 index 00000000..786c5c35 --- /dev/null +++ b/_examples/examples/online-visitors/main.go @@ -0,0 +1,170 @@ +package main + +import ( + "sync/atomic" + + "gopkg.in/kataras/iris.v6" + "gopkg.in/kataras/iris.v6/adaptors/httprouter" + "gopkg.in/kataras/iris.v6/adaptors/view" + "gopkg.in/kataras/iris.v6/adaptors/websocket" +) + +var ( + app *iris.Framework + ws websocket.Server +) + +func init() { + // init the server instance + app = iris.New() + // adapt a logger in dev mode + app.Adapt(iris.DevLogger()) + // adapt router + app.Adapt(httprouter.New()) + // adapt templaes + app.Adapt(view.HTML("./templates", ".html").Reload(true)) + // adapt websocket + ws = websocket.New(websocket.Config{Endpoint: "/my_endpoint"}) + ws.OnConnection(HandleWebsocketConnection) + app.Adapt(ws) +} + +type page struct { + PageID string +} + +func main() { + app.StaticWeb("/js", "./static/assets/js") + + h := func(ctx *iris.Context) { + ctx.Render("index.html", page{PageID: "index page"}) + } + + h2 := func(ctx *iris.Context) { + ctx.Render("other.html", page{PageID: "other page"}) + } + + // Open some browser tabs/or windows + // and navigate to + // http://localhost:8080/ and http://localhost:8080/other + // Each page has its own online-visitors counter. + app.Get("/", h) + app.Get("/other", h2) + app.Listen(":8080") +} + +type pageView struct { + source string + count uint64 +} + +func (v *pageView) increment() { + atomic.AddUint64(&v.count, 1) +} + +func (v *pageView) decrement() { + oldCount := v.count + if oldCount > 0 { + atomic.StoreUint64(&v.count, oldCount-1) + } +} + +func (v *pageView) getCount() uint64 { + val := atomic.LoadUint64(&v.count) + return val +} + +type ( + pageViews []pageView +) + +func (v *pageViews) Add(source string) { + args := *v + n := len(args) + for i := 0; i < n; i++ { + kv := &args[i] + if kv.source == source { + kv.increment() + return + } + } + + c := cap(args) + if c > n { + args = args[:n+1] + kv := &args[n] + kv.source = source + kv.count = 1 + *v = args + return + } + + kv := pageView{} + kv.source = source + kv.count = 1 + *v = append(args, kv) +} + +func (v *pageViews) Get(source string) *pageView { + args := *v + n := len(args) + for i := 0; i < n; i++ { + kv := &args[i] + if kv.source == source { + return kv + } + } + return nil +} + +func (v *pageViews) Reset() { + *v = (*v)[:0] +} + +var v pageViews + +// HandleWebsocketConnection handles the online viewers per example(gist source) +func HandleWebsocketConnection(c websocket.Connection) { + + c.On("watch", func(pageSource string) { + v.Add(pageSource) + // join the socket to a room linked with the page source + c.Join(pageSource) + + viewsCount := v.Get(pageSource).getCount() + if viewsCount == 0 { + viewsCount++ // count should be always > 0 here + } + c.To(pageSource).Emit("watch", viewsCount) + }) + + c.OnLeave(func(roomName string) { + if roomName != c.ID() { // if the roomName it's not the connection iself + // the roomName here is the source, this is the only room(except the connection's ID room) which we join the users to. + pageV := v.Get(roomName) + if pageV == nil { + return // for any case that this room is not a pageView source + } + // decrement -1 the specific counter for this page source. + pageV.decrement() + // 1. open 30 tabs. + // 2. close the browser. + // 3. re-open the browser + // 4. should be v.getCount() = 1 + // in order to achieve the previous flow we should decrement exactly when the user disconnects + // but emit the result a little after, on a goroutine + // getting all connections within this room and emit the online views one by one. + // note: + // we can also add a time.Sleep(2-3 seconds) inside the goroutine at the future if we don't need 'real-time' updates. + go func(currentConnID string) { + for _, conn := range ws.GetConnectionsByRoom(roomName) { + if conn.ID() != currentConnID { + conn.Emit("watch", pageV.getCount()) + } + + } + }(c.ID()) + } + + }) +} diff --git a/_examples/examples/online-visitors/static/assets/js/visitors.js b/_examples/examples/online-visitors/static/assets/js/visitors.js new file mode 100644 index 00000000..bfc0a9a0 --- /dev/null +++ b/_examples/examples/online-visitors/static/assets/js/visitors.js @@ -0,0 +1,21 @@ +(function() { + var socket = new Ws("ws://localhost:8080/my_endpoint"); + + socket.OnConnect(function () { + socket.Emit("watch", PAGE_SOURCE); + }); + + + socket.On("watch", function (onlineViews) { + var text = "1 online view"; + if (onlineViews > 1) { + text = onlineViews + " online views"; + } + document.getElementById("online_views").innerHTML = text; + }); + + socket.OnDisconnect(function () { + document.getElementById("online_views").innerHTML = "you've been disconnected"; + }); + +})(); diff --git a/_examples/examples/online-visitors/templates/index.html b/_examples/examples/online-visitors/templates/index.html new file mode 100644 index 00000000..92028ff5 --- /dev/null +++ b/_examples/examples/online-visitors/templates/index.html @@ -0,0 +1,43 @@ + + + + Online visitors example + + + + +
+ 1 online view +
+ + + + + + + + + + diff --git a/_examples/examples/online-visitors/templates/other.html b/_examples/examples/online-visitors/templates/other.html new file mode 100644 index 00000000..3c9d0de0 --- /dev/null +++ b/_examples/examples/online-visitors/templates/other.html @@ -0,0 +1,29 @@ + + + + Different page, different results + + + + + + 1 online view + + + + + + + + + + + diff --git a/_examples/examples/password-hashing/main.go b/_examples/examples/password-hashing/main.go new file mode 100644 index 00000000..9954565b --- /dev/null +++ b/_examples/examples/password-hashing/main.go @@ -0,0 +1,28 @@ +package main + +import ( + "fmt" + + "golang.org/x/crypto/bcrypt" +) + +func HashPassword(password string) (string, error) { + bytes, err := bcrypt.GenerateFromPassword([]byte(password), 14) + return string(bytes), err +} + +func CheckPasswordHash(password, hash string) bool { + err := bcrypt.CompareHashAndPassword([]byte(hash), []byte(password)) + return err == nil +} + +func main() { + password := "secret" + hash, _ := HashPassword(password) // ignore error for the sake of simplicity + + fmt.Println("Password:", password) + fmt.Println("Hash: ", hash) + + match := CheckPasswordHash(password, hash) + fmt.Println("Match: ", match) +} diff --git a/_examples/examples/routes-using-gorillamux/main.go b/_examples/examples/routes-using-gorillamux/main.go new file mode 100644 index 00000000..5c10b983 --- /dev/null +++ b/_examples/examples/routes-using-gorillamux/main.go @@ -0,0 +1,28 @@ +package main + +import ( + "gopkg.in/kataras/iris.v6" + "gopkg.in/kataras/iris.v6/adaptors/gorillamux" +) + +func main() { + app := iris.New() + // Adapt the "httprouter", you can use "gorillamux" too. + app.Adapt(gorillamux.New()) + + userAges := map[string]int{ + "Alice": 25, + "Bob": 30, + "Claire": 29, + } + + // Equivalent with app.HandleFunc("GET", ...) + app.Get("/users/{name}", func(ctx *iris.Context) { + name := ctx.Param("name") + age := userAges[name] + + ctx.Writef("%s is %d years old!", name, age) + }) + + app.Listen(":8080") +} diff --git a/_examples/examples/routes-using-httprouter/main.go b/_examples/examples/routes-using-httprouter/main.go new file mode 100644 index 00000000..78b39137 --- /dev/null +++ b/_examples/examples/routes-using-httprouter/main.go @@ -0,0 +1,27 @@ +package main + +import ( + "gopkg.in/kataras/iris.v6" + "gopkg.in/kataras/iris.v6/adaptors/httprouter" +) + +func main() { + app := iris.New() + app.Adapt(httprouter.New()) + + userAges := map[string]int{ + "Alice": 25, + "Bob": 30, + "Claire": 29, + } + + // Equivalent with app.HandleFunc("GET", ...) + app.Get("/users/:name", func(ctx *iris.Context) { + name := ctx.Param("name") + age := userAges[name] + + ctx.Writef("%s is %d years old!", name, age) + }) + + app.Listen(":8080") +} diff --git a/_examples/examples/sessions/main.go b/_examples/examples/sessions/main.go new file mode 100644 index 00000000..f0c470aa --- /dev/null +++ b/_examples/examples/sessions/main.go @@ -0,0 +1,54 @@ +package main + +import ( + "gopkg.in/kataras/iris.v6" + "gopkg.in/kataras/iris.v6/adaptors/httprouter" + "gopkg.in/kataras/iris.v6/adaptors/sessions" +) + +var ( + key = "my_sessionid" +) + +func secret(ctx *iris.Context) { + + // Check if user is authenticated + if auth, _ := ctx.Session().GetBoolean("authenticated"); !auth { + ctx.EmitError(iris.StatusForbidden) + return + } + + // Print secret message + ctx.WriteString("The cake is a lie!") +} + +func login(ctx *iris.Context) { + session := ctx.Session() + + // Authentication goes here + // ... + + // Set user as authenticated + session.Set("authenticated", true) +} + +func logout(ctx *iris.Context) { + session := ctx.Session() + + // Revoke users authentication + session.Set("authenticated", false) +} + +func main() { + app := iris.New() + app.Adapt(httprouter.New()) + + sess := sessions.New(sessions.Config{Cookie: key}) + app.Adapt(sess) + + app.Get("/secret", secret) + app.Get("/login", login) + app.Get("/logout", logout) + + app.Listen(":8080") +} diff --git a/_examples/examples/static-files/assets/css/styles.css b/_examples/examples/static-files/assets/css/styles.css new file mode 100644 index 00000000..7db3df1d --- /dev/null +++ b/_examples/examples/static-files/assets/css/styles.css @@ -0,0 +1,3 @@ +body { + background-color: black; +} diff --git a/_examples/examples/static-files/main.go b/_examples/examples/static-files/main.go new file mode 100644 index 00000000..08d51648 --- /dev/null +++ b/_examples/examples/static-files/main.go @@ -0,0 +1,16 @@ +package main + +import ( + "gopkg.in/kataras/iris.v6" + "gopkg.in/kataras/iris.v6/adaptors/httprouter" +) + +func main() { + app := iris.New() + app.Adapt(httprouter.New()) + // first parameter is the request path + // second is the operating system directory + app.StaticWeb("/static", "./assets") + + app.Listen(":8080") +} diff --git a/_examples/examples/templates/main.go b/_examples/examples/templates/main.go new file mode 100644 index 00000000..297eb67e --- /dev/null +++ b/_examples/examples/templates/main.go @@ -0,0 +1,39 @@ +package main + +import ( + "gopkg.in/kataras/iris.v6" + "gopkg.in/kataras/iris.v6/adaptors/httprouter" + "gopkg.in/kataras/iris.v6/adaptors/view" +) + +// Todo bind struct +type Todo struct { + Task string + Done bool +} + +func main() { + // Configuration is optional + app := iris.New(iris.Configuration{Gzip: false, Charset: "UTF-8"}) + + // Adapt a logger which will print all errors to os.Stdout + app.Adapt(iris.DevLogger()) + + // Adapt the httprouter (we will use that on all examples) + app.Adapt(httprouter.New()) + + // Parse all files inside `./mytemplates` directory ending with `.html` + app.Adapt(view.HTML("./mytemplates", ".html")) + + todos := []Todo{ + {"Learn Go", true}, + {"Read GopherBOOk", true}, + {"Create a web app in Go", false}, + } + + app.Get("/", func(ctx *iris.Context) { + ctx.Render("todos.html", struct{ Todos []Todo }{todos}) + }) + + app.Listen(":8080") +} diff --git a/_examples/examples/templates/mytemplates/todos.html b/_examples/examples/templates/mytemplates/todos.html new file mode 100644 index 00000000..a602ed70 --- /dev/null +++ b/_examples/examples/templates/mytemplates/todos.html @@ -0,0 +1,10 @@ +

Todos

+ diff --git a/_examples/examples/upload-files/main.go b/_examples/examples/upload-files/main.go new file mode 100644 index 00000000..bfb12863 --- /dev/null +++ b/_examples/examples/upload-files/main.go @@ -0,0 +1,69 @@ +package main + +import ( + "crypto/md5" + "fmt" + "io" + "os" + "strconv" + "time" + + "gopkg.in/kataras/iris.v6" + "gopkg.in/kataras/iris.v6/adaptors/httprouter" + "gopkg.in/kataras/iris.v6/adaptors/view" +) + +func main() { + app := iris.New() + app.Adapt(iris.DevLogger()) + app.Adapt(httprouter.New()) + app.Adapt(view.HTML("./templates", ".html")) + + // Serve the form.html to the user + app.Get("/upload", func(ctx *iris.Context) { + //create a token (optionally) + + now := time.Now().Unix() + h := md5.New() + io.WriteString(h, strconv.FormatInt(now, 10)) + token := fmt.Sprintf("%x", h.Sum(nil)) + //render the form with the token for any use you like + ctx.Render("upload_form.html", token) + }) + + // Handle the post request from the upload_form.html to the server + app.Post("/upload", iris.LimitRequestBodySize(10<<20), + func(ctx *iris.Context) { + // or use ctx.SetMaxRequestBodySize(10 << 20) + //to limit the uploaded file(s) size. + + // Get the file from the request + file, info, err := ctx.FormFile("uploadfile") + + if err != nil { + ctx.HTML(iris.StatusInternalServerError, + "Error while uploading: "+err.Error()+"") + return + } + + defer file.Close() + fname := info.Filename + + // Create a file with the same name + // assuming that you have a folder named 'uploads' + out, err := os.OpenFile("./uploads/"+fname, + os.O_WRONLY|os.O_CREATE, 0666) + + if err != nil { + ctx.HTML(iris.StatusInternalServerError, + "Error while uploading: "+err.Error()+"") + return + } + defer out.Close() + + io.Copy(out, file) + }) + + // start the server at 127.0.0.1:8080 + app.Listen(":8080") +} diff --git a/_examples/examples/upload-files/templates/upload_form.html b/_examples/examples/upload-files/templates/upload_form.html new file mode 100644 index 00000000..55350ff8 --- /dev/null +++ b/_examples/examples/upload-files/templates/upload_form.html @@ -0,0 +1,12 @@ + + +Upload file + + +
+ +
+ + diff --git a/_examples/examples/url-shortener/main.go b/_examples/examples/url-shortener/main.go new file mode 100644 index 00000000..7ac86cf4 --- /dev/null +++ b/_examples/examples/url-shortener/main.go @@ -0,0 +1,132 @@ +package main + +import ( + "html/template" + "math/rand" + "net/url" + "sync" + "time" + + "gopkg.in/kataras/iris.v6" + "gopkg.in/kataras/iris.v6/adaptors/httprouter" + "gopkg.in/kataras/iris.v6/adaptors/view" +) + +func main() { + app := iris.New() + app.Adapt( + iris.DevLogger(), + httprouter.New(), + view.HTML("./templates", ".html").Reload(true), + ) + + // Serve static files (css) + app.StaticWeb("/static", "./static_files") + + var mu sync.Mutex + var urls = map[string]string{ + "iris": "http://support.iris-go.com", + } + + app.Get("/", func(ctx *iris.Context) { + ctx.Render("index.html", iris.Map{"url_count": len(urls)}) + }) + + // find and execute a short url by its key + // used on http://localhost:8080/url/dsaoj41u321dsa + execShortURL := func(ctx *iris.Context, key string) { + if key == "" { + ctx.EmitError(iris.StatusBadRequest) + return + } + + value, found := urls[key] + if !found { + ctx.SetStatusCode(iris.StatusNotFound) + ctx.Writef("Short URL for key: '%s' not found", key) + return + } + + ctx.Redirect(value, iris.StatusTemporaryRedirect) + } + app.Get("/url/:shortkey", func(ctx *iris.Context) { + execShortURL(ctx, ctx.Param("shortkey")) + }) + + // for wildcard subdomain (yeah.. cool) http://dsaoj41u321dsa.localhost:8080 + // Note: + // if you want subdomains (chrome doesn't works on localhost, so you have to define other hostname on app.Listen) + // app.Party("*.", func(ctx *iris.Context) { + // execShortURL(ctx, ctx.Subdomain()) + // }) + + app.Post("/url/shorten", func(ctx *iris.Context) { + data := make(map[string]interface{}, 0) + data["url_count"] = len(urls) + value := ctx.FormValue("url") + if value == "" { + data["form_result"] = "You need to a enter a URL." + } else { + urlValue, err := url.ParseRequestURI(value) + if err != nil { + // ctx.JSON(iris.StatusInternalServerError, + // iris.Map{"status": iris.StatusInternalServerError, + // "error": err.Error(), + // "reason": "Invalid URL", + // }) + data["form_result"] = "Invalid URL." + } else { + key := randomString(12) + // Make sure that the key is unique + for { + if _, exists := urls[key]; !exists { + break + } + key = randomString(8) + } + mu.Lock() + urls[key] = urlValue.String() + mu.Unlock() + ctx.SetStatusCode(iris.StatusOK) + shortenURL := "http://" + app.Config.VHost + "/url/" + key + data["form_result"] = template.HTML("
Here is your short URL: " + shortenURL + " 
") + } + + } + ctx.Render("index.html", data) + }) + + app.Listen("localhost:8080") +} + +// +------------------------------------------------------------+ +// | | +// | Random String | +// | | +// +------------------------------------------------------------+ + +const ( + letterBytes = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ" + letterIdxBits = 6 // 6 bits to represent a letter index + letterIdxMask = 1<= 0; { + if remain == 0 { + cache, remain = src.Int63(), letterIdxMax + } + if idx := int(cache & letterIdxMask); idx < len(letterBytes) { + b[i] = letterBytes[idx] + i-- + } + cache >>= letterIdxBits + remain-- + } + + return string(b) +} diff --git a/_examples/examples/url-shortener/static_files/style.css b/_examples/examples/url-shortener/static_files/style.css new file mode 100644 index 00000000..10574f0e --- /dev/null +++ b/_examples/examples/url-shortener/static_files/style.css @@ -0,0 +1,3 @@ +body{ + background-color:silver; +} \ No newline at end of file diff --git a/_examples/examples/url-shortener/templates/index.html b/_examples/examples/url-shortener/templates/index.html new file mode 100644 index 00000000..136f5152 --- /dev/null +++ b/_examples/examples/url-shortener/templates/index.html @@ -0,0 +1,20 @@ + + + + + Golang URL Shortener + + + + +

Golang URL Shortener

+

{{ .form_result}}

+
+ + +
+ +

{{ .url_count }} URLs shortened

+ + + \ No newline at end of file diff --git a/_examples/examples/websockets/main.go b/_examples/examples/websockets/main.go new file mode 100644 index 00000000..4671701c --- /dev/null +++ b/_examples/examples/websockets/main.go @@ -0,0 +1,48 @@ +package main + +import ( + "fmt" + + "gopkg.in/kataras/iris.v6" + "gopkg.in/kataras/iris.v6/adaptors/httprouter" + "gopkg.in/kataras/iris.v6/adaptors/websocket" +) + +func handleConnection(c websocket.Connection) { + + // Read events from browser + c.On("chat", func(msg string) { + + // Print the message to the console + fmt.Printf("%s sent: %s\n", c.Context().RemoteAddr(), msg) + + // Write message back to browser + c.Emit("chat", msg) + }) + +} + +func main() { + app := iris.New() + app.Adapt(iris.DevLogger()) + app.Adapt(httprouter.New()) + + // create our echo websocket server + ws := websocket.New(websocket.Config{ + ReadBufferSize: 1024, + WriteBufferSize: 1024, + Endpoint: "/echo", + }) + + ws.OnConnection(handleConnection) + + // Adapt the websocket server. + // you can adapt more than one of course. + app.Adapt(ws) + + app.Get("/", func(ctx *iris.Context) { + ctx.ServeFile("websockets.html", false) // second parameter: enable gzip? + }) + + app.Listen(":8080") +} diff --git a/_examples/examples/websockets/websockets.html b/_examples/examples/websockets/websockets.html new file mode 100644 index 00000000..4e9098fe --- /dev/null +++ b/_examples/examples/websockets/websockets.html @@ -0,0 +1,29 @@ + + +

+
+
diff --git a/adaptors/httprouter/httprouter.go b/adaptors/httprouter/httprouter.go
index ee929992..01d0ec0a 100644
--- a/adaptors/httprouter/httprouter.go
+++ b/adaptors/httprouter/httprouter.go
@@ -42,64 +42,19 @@ const (
 	slash = "/"
 	// matchEverythingByte is just a byte of '*" rune/char
 	matchEverythingByte = byte('*')
-
-	isRoot entryCase = iota
-	hasParams
-	matchEverything
 )
 
-type (
-	// entryCase is the type which the type of muxEntryusing in order to determinate what type (parameterized, anything, static...) is the perticular node
-	entryCase uint8
-
-	// muxEntry is the node of a tree of the routes,
-	// in order to learn how this is working, google 'trie' or watch this lecture: https://www.youtube.com/watch?v=uhAUk63tLRM
-	// this method is used by the BSD's kernel also
-	muxEntry struct {
-		part        string
-		entryCase   entryCase
-		hasWildNode bool
-		tokens      string
-		nodes       []*muxEntry
-		middleware  iris.Middleware
-		precedence  uint64
-		paramsLen   uint8
+func min(a, b int) int {
+	if a <= b {
+		return a
 	}
-)
+	return b
+}
 
-var (
-	errMuxEntryConflictsWildcard = errors.New(`
-			httprouter: '%s' in new path '%s'
-							conflicts with existing wildcarded route with path: '%s'
-							in existing prefix of'%s' `)
-
-	errMuxEntryMiddlewareAlreadyExists = errors.New(`
-		httprouter: Middleware were already registered for the path: '%s'`)
-
-	errMuxEntryInvalidWildcard = errors.New(`
-		httprouter: More than one wildcard found in the path part: '%s' in route's path: '%s'`)
-
-	errMuxEntryConflictsExistingWildcard = errors.New(`
-		httprouter: Wildcard for route path: '%s' conflicts with existing children in route path: '%s'`)
-
-	errMuxEntryWildcardUnnamed = errors.New(`
-		httprouter: Unnamed wildcard found in path: '%s'`)
-
-	errMuxEntryWildcardInvalidPlace = errors.New(`
-		httprouter: Wildcard is only allowed at the end of the path, in the route path: '%s'`)
-
-	errMuxEntryWildcardConflictsMiddleware = errors.New(`
-		httprouter: Wildcard  conflicts with existing middleware for the route path: '%s'`)
-
-	errMuxEntryWildcardMissingSlash = errors.New(`
-		httprouter: No slash(/) were found before wildcard in the route path: '%s'`)
-)
-
-// getParamsLen returns the parameters length from a given path
-func getParamsLen(path string) uint8 {
+func countParams(path string) uint8 {
 	var n uint
 	for i := 0; i < len(path); i++ {
-		if path[i] != ':' && path[i] != '*' { // ParameterStartByte & MatchEverythingByte
+		if path[i] != ':' && path[i] != '*' {
 			continue
 		}
 		n++
@@ -110,304 +65,391 @@ func getParamsLen(path string) uint8 {
 	return uint8(n)
 }
 
-// findLower returns the smaller number between a and b
-func findLower(a, b int) int {
-	if a <= b {
-		return a
-	}
-	return b
+type nodeType uint8
+
+const (
+	static nodeType = iota // default
+	root
+	param
+	catchAll
+)
+
+type node struct {
+	path      string
+	wildChild bool
+	nType     nodeType
+	maxParams uint8
+	indices   string
+	children  []*node
+	handle    iris.Middleware
+	priority  uint32
 }
 
-// add adds a muxEntry to the existing muxEntry or to the tree if no muxEntry has the prefix of
-func (e *muxEntry) add(path string, middleware iris.Middleware) error {
-	fullPath := path
-	e.precedence++
-	numParams := getParamsLen(path)
+// increments priority of the given child and reorders if necessary
+func (n *node) incrementChildPrio(pos int) int {
+	n.children[pos].priority++
+	prio := n.children[pos].priority
 
-	if len(e.part) > 0 || len(e.nodes) > 0 {
-	loop:
+	// adjust position (move to front)
+	newPos := pos
+	for newPos > 0 && n.children[newPos-1].priority < prio {
+		// swap node positions
+		n.children[newPos-1], n.children[newPos] = n.children[newPos], n.children[newPos-1]
+
+		newPos--
+	}
+
+	// build new index char string
+	if newPos != pos {
+		n.indices = n.indices[:newPos] + // unchanged prefix, might be empty
+			n.indices[pos:pos+1] + // the index char we move
+			n.indices[newPos:pos] + n.indices[pos+1:] // rest without char at 'pos'
+	}
+
+	return newPos
+}
+
+// addRoute adds a node with the given handle to the path.
+// Not concurrency-safe!
+func (n *node) addRoute(path string, handle iris.Middleware) error {
+	fullPath := path
+	n.priority++
+	numParams := countParams(path)
+
+	// non-empty tree
+	if len(n.path) > 0 || len(n.children) > 0 {
+	walk:
 		for {
-			if numParams > e.paramsLen {
-				e.paramsLen = numParams
+			// Update maxParams of the current node
+			if numParams > n.maxParams {
+				n.maxParams = numParams
 			}
 
+			// Find the longest common prefix.
+			// This also implies that the common prefix contains no ':' or '*'
+			// since the existing key can't contain those chars.
 			i := 0
-			max := findLower(len(path), len(e.part))
-			for i < max && path[i] == e.part[i] {
+			max := min(len(path), len(n.path))
+			for i < max && path[i] == n.path[i] {
 				i++
 			}
 
-			if i < len(e.part) {
-				node := muxEntry{
-					part:        e.part[i:],
-					hasWildNode: e.hasWildNode,
-					tokens:      e.tokens,
-					nodes:       e.nodes,
-					middleware:  e.middleware,
-					precedence:  e.precedence - 1,
+			// Split edge
+			if i < len(n.path) {
+				child := node{
+					path:      n.path[i:],
+					wildChild: n.wildChild,
+					nType:     static,
+					indices:   n.indices,
+					children:  n.children,
+					handle:    n.handle,
+					priority:  n.priority - 1,
 				}
 
-				for i := range node.nodes {
-					if node.nodes[i].paramsLen > node.paramsLen {
-						node.paramsLen = node.nodes[i].paramsLen
+				// Update maxParams (max of all children)
+				for i := range child.children {
+					if child.children[i].maxParams > child.maxParams {
+						child.maxParams = child.children[i].maxParams
 					}
 				}
 
-				e.nodes = []*muxEntry{&node}
-				e.tokens = string([]byte{e.part[i]})
-				e.part = path[:i]
-				e.middleware = nil
-				e.hasWildNode = false
+				n.children = []*node{&child}
+				// []byte for proper unicode char conversion, see #65
+				n.indices = string([]byte{n.path[i]})
+				n.path = path[:i]
+				n.handle = nil
+				n.wildChild = false
 			}
 
+			// Make new node a child of this node
 			if i < len(path) {
 				path = path[i:]
 
-				if e.hasWildNode {
-					e = e.nodes[0]
-					e.precedence++
+				if n.wildChild {
+					n = n.children[0]
+					n.priority++
 
-					if numParams > e.paramsLen {
-						e.paramsLen = numParams
+					// Update maxParams of the child node
+					if numParams > n.maxParams {
+						n.maxParams = numParams
 					}
 					numParams--
 
-					if len(path) >= len(e.part) && e.part == path[:len(e.part)] &&
+					// Check if the wildcard matches
+					if len(path) >= len(n.path) && n.path == path[:len(n.path)] &&
 						// Check for longer wildcard, e.g. :name and :names
-						(len(e.part) >= len(path) || path[len(e.part)] == '/') {
-						continue loop
+						(len(n.path) >= len(path) || path[len(n.path)] == '/') {
+						continue walk
 					} else {
 						// Wildcard conflict
-						part := strings.SplitN(path, "/", 2)[0]
-						prefix := fullPath[:strings.Index(fullPath, part)] + e.part
-						return errMuxEntryConflictsWildcard.Format(fullPath, e.part, prefix)
-
+						pathSeg := strings.SplitN(path, "/", 2)[0]
+						prefix := fullPath[:strings.Index(fullPath, pathSeg)] + n.path
+						return errors.New("'" + pathSeg +
+							"' in new path '" + fullPath +
+							"' conflicts with existing wildcard '" + n.path +
+							"' in existing prefix '" + prefix +
+							"'")
 					}
-
 				}
 
 				c := path[0]
 
-				if e.entryCase == hasParams && c == slashByte && len(e.nodes) == 1 {
-					e = e.nodes[0]
-					e.precedence++
-					continue loop
+				// slash after param
+				if n.nType == param && c == '/' && len(n.children) == 1 {
+					n = n.children[0]
+					n.priority++
+					continue walk
 				}
-				for i := range e.tokens {
-					if c == e.tokens[i] {
-						i = e.precedenceTo(i)
-						e = e.nodes[i]
-						continue loop
+
+				// Check if a child with the next path byte exists
+				for i := 0; i < len(n.indices); i++ {
+					if c == n.indices[i] {
+						i = n.incrementChildPrio(i)
+						n = n.children[i]
+						continue walk
 					}
 				}
 
-				if c != parameterStartByte && c != matchEverythingByte {
-
-					e.tokens += string([]byte{c})
-					node := &muxEntry{
-						paramsLen: numParams,
+				// Otherwise insert it
+				if c != ':' && c != '*' {
+					// []byte for proper unicode char conversion, see #65
+					n.indices += string([]byte{c})
+					child := &node{
+						maxParams: numParams,
 					}
-					e.nodes = append(e.nodes, node)
-					e.precedenceTo(len(e.tokens) - 1)
-					e = node
+					n.children = append(n.children, child)
+					n.incrementChildPrio(len(n.indices) - 1)
+					n = child
 				}
-				return e.addNode(numParams, path, fullPath, middleware)
+				return n.insertChild(numParams, path, fullPath, handle)
 
-			} else if i == len(path) {
-				if e.middleware != nil {
-					return errMuxEntryMiddlewareAlreadyExists.Format(fullPath)
+			} else if i == len(path) { // Make node a (in-path) leaf
+				if n.handle != nil {
+					return errors.New("a handle is already registered for path '" + fullPath + "'")
 				}
-				e.middleware = middleware
+				n.handle = handle
 			}
 			return nil
 		}
-	} else {
-		if err := e.addNode(numParams, path, fullPath, middleware); err != nil {
-			return err
-		}
-		e.entryCase = isRoot
+	} else { // Empty tree
+		n.insertChild(numParams, path, fullPath, handle)
+		n.nType = root
 	}
 	return nil
 }
 
-// addNode adds a muxEntry as children to other muxEntry
-func (e *muxEntry) addNode(numParams uint8, path string, fullPath string, middleware iris.Middleware) error {
-	var offset int
+func (n *node) insertChild(numParams uint8, path, fullPath string, handle iris.Middleware) error {
+	var offset int // already handled bytes of the path
 
+	// find prefix until first wildcard (beginning with ':'' or '*'')
 	for i, max := 0, len(path); numParams > 0; i++ {
 		c := path[i]
-		if c != parameterStartByte && c != matchEverythingByte {
+		if c != ':' && c != '*' {
 			continue
 		}
 
+		// find wildcard end (either '/' or path end)
 		end := i + 1
-		for end < max && path[end] != slashByte {
+		for end < max && path[end] != '/' {
 			switch path[end] {
-			case parameterStartByte, matchEverythingByte:
-				return errMuxEntryInvalidWildcard.Format(path[i:], fullPath)
+			// the wildcard name must not contain ':' and '*'
+			case ':', '*':
+				return errors.New("only one wildcard per path segment is allowed, has: '" +
+					path[i:] + "' in path '" + fullPath + "'")
 			default:
 				end++
 			}
 		}
 
-		if len(e.nodes) > 0 {
-			return errMuxEntryConflictsExistingWildcard.Format(path[i:end], fullPath)
+		// check if this Node existing children which would be
+		// unreachable if we insert the wildcard here
+		if len(n.children) > 0 {
+			return errors.New("wildcard route '" + path[i:end] +
+				"' conflicts with existing children in path '" + fullPath + "'")
 		}
 
+		// check if the wildcard has a name
 		if end-i < 2 {
-			return errMuxEntryWildcardUnnamed.Format(fullPath)
+			return errors.New("wildcards must be named with a non-empty name in path '" + fullPath + "'")
 		}
 
-		if c == parameterStartByte {
-
+		if c == ':' { // param
+			// split path at the beginning of the wildcard
 			if i > 0 {
-				e.part = path[offset:i]
+				n.path = path[offset:i]
 				offset = i
 			}
 
-			child := &muxEntry{
-				entryCase: hasParams,
-				paramsLen: numParams,
+			child := &node{
+				nType:     param,
+				maxParams: numParams,
 			}
-			e.nodes = []*muxEntry{child}
-			e.hasWildNode = true
-			e = child
-			e.precedence++
+			n.children = []*node{child}
+			n.wildChild = true
+			n = child
+			n.priority++
 			numParams--
 
+			// if the path doesn't end with the wildcard, then there
+			// will be another non-wildcard subpath starting with '/'
 			if end < max {
-				e.part = path[offset:end]
+				n.path = path[offset:end]
 				offset = end
 
-				child := &muxEntry{
-					paramsLen:  numParams,
-					precedence: 1,
+				child := &node{
+					maxParams: numParams,
+					priority:  1,
 				}
-				e.nodes = []*muxEntry{child}
-				e = child
+				n.children = []*node{child}
+				n = child
 			}
 
-		} else {
+		} else { // catchAll
 			if end != max || numParams > 1 {
-				return errMuxEntryWildcardInvalidPlace.Format(fullPath)
+				return errors.New("catch-all routes are only allowed at the end of the path in path '" + fullPath + "'")
 			}
 
-			if len(e.part) > 0 && e.part[len(e.part)-1] == '/' {
-				return errMuxEntryWildcardConflictsMiddleware.Format(fullPath)
+			if len(n.path) > 0 && n.path[len(n.path)-1] == '/' {
+				return errors.New("catch-all conflicts with existing handle for the path segment root in path '" + fullPath + "'")
 			}
 
+			// currently fixed width 1 for '/'
 			i--
-			if path[i] != slashByte {
-				return errMuxEntryWildcardMissingSlash.Format(fullPath)
+			if path[i] != '/' {
+				return errors.New("no / before catch-all in path '" + fullPath + "'")
 			}
 
-			e.part = path[offset:i]
+			n.path = path[offset:i]
 
-			child := &muxEntry{
-				hasWildNode: true,
-				entryCase:   matchEverything,
-				paramsLen:   1,
+			// first node: catchAll node with empty path
+			child := &node{
+				wildChild: true,
+				nType:     catchAll,
+				maxParams: 1,
 			}
-			e.nodes = []*muxEntry{child}
-			e.tokens = string(path[i])
-			e = child
-			e.precedence++
+			n.children = []*node{child}
+			n.indices = string(path[i])
+			n = child
+			n.priority++
 
-			child = &muxEntry{
-				part:       path[i:],
-				entryCase:  matchEverything,
-				paramsLen:  1,
-				middleware: middleware,
-				precedence: 1,
+			// second node: node holding the variable
+			child = &node{
+				path:      path[i:],
+				nType:     catchAll,
+				maxParams: 1,
+				handle:    handle,
+				priority:  1,
 			}
-			e.nodes = []*muxEntry{child}
+			n.children = []*node{child}
 
 			return nil
 		}
 	}
 
-	e.part = path[offset:]
-	e.middleware = middleware
-
+	// insert remaining path part and handle to the leaf
+	n.path = path[offset:]
+	n.handle = handle
 	return nil
 }
 
-// get is used by the Router, it finds and returns the correct muxEntry for a path
-func (e *muxEntry) get(path string, ctx *iris.Context) (mustRedirect bool) {
-loop:
+// Returns the handle registered with the given path (key). The values of
+// wildcards are saved to a map.
+// If no handle can be found, a TSR (trailing slash redirect) recommendation is
+// made if a handle exists with an extra (without the) trailing slash for the
+// given path.
+func (n *node) getValue(path string, ctx *iris.Context) (tsr bool) {
+walk: // outer loop for walking the tree
 	for {
-		if len(path) > len(e.part) {
-			if path[:len(e.part)] == e.part {
-				path = path[len(e.part):]
-
-				if !e.hasWildNode {
+		if len(path) > len(n.path) {
+			if path[:len(n.path)] == n.path {
+				path = path[len(n.path):]
+				// If this node does not have a wildcard (param or catchAll)
+				// child,  we can just look up the next child node and continue
+				// to walk down the tree
+				if !n.wildChild {
 					c := path[0]
-					for i := range e.tokens {
-						if c == e.tokens[i] {
-							e = e.nodes[i]
-							continue loop
+					for i := 0; i < len(n.indices); i++ {
+						if c == n.indices[i] {
+							n = n.children[i]
+							continue walk
 						}
 					}
 
-					mustRedirect = (path == slash && e.middleware != nil)
+					// Nothing found.
+					// We can recommend to redirect to the same URL without a
+					// trailing slash if a leaf exists for that path.
+					tsr = (path == "/" && n.handle != nil)
 					return
+
 				}
 
-				e = e.nodes[0]
-				switch e.entryCase {
-				case hasParams:
-
+				// handle wildcard child
+				n = n.children[0]
+				switch n.nType {
+				case param:
+					// find param end (either '/' or path end)
 					end := 0
 					for end < len(path) && path[end] != '/' {
 						end++
 					}
 
-					ctx.Set(e.part[1:], path[:end])
+					// save param value
+					ctx.Set(n.path[1:], path[:end])
 
+					// we need to go deeper!
 					if end < len(path) {
-						if len(e.nodes) > 0 {
+						if len(n.children) > 0 {
 							path = path[end:]
-							e = e.nodes[0]
-							continue loop
+							n = n.children[0]
+							continue walk
 						}
 
-						mustRedirect = (len(path) == end+1)
+						// ... but we can't
+						tsr = (len(path) == end+1)
 						return
 					}
-					if ctx.Middleware = e.middleware; ctx.Middleware != nil {
+
+					if ctx.Middleware = n.handle; ctx.Middleware != nil {
 						return
-					} else if len(e.nodes) == 1 {
-						e = e.nodes[0]
-						mustRedirect = (e.part == slash && e.middleware != nil)
+					} else if len(n.children) == 1 {
+						// No handle found. Check if a handle for this path + a
+						// trailing slash exists for TSR recommendation
+						n = n.children[0]
+						tsr = (n.path == "/" && n.handle != nil)
 					}
 
 					return
 
-				case matchEverything:
+				case catchAll:
+					// save param value
+					ctx.Set(n.path[2:], path)
 
-					ctx.Set(e.part[2:], path)
-					ctx.Middleware = e.middleware
+					ctx.Middleware = n.handle
 					return
 
 				default:
-					return
+					panic("invalid node type")
 				}
 			}
-		} else if path == e.part {
-			if ctx.Middleware = e.middleware; ctx.Middleware != nil {
+		} else if path == n.path {
+			// We should have reached the node containing the handle.
+			// Check if this node has a handle registered.
+			if ctx.Middleware = n.handle; ctx.Middleware != nil {
 				return
 			}
 
-			if path == slash && e.hasWildNode && e.entryCase != isRoot {
-				mustRedirect = true
+			if path == "/" && n.wildChild && n.nType != root {
+				tsr = true
 				return
 			}
 
-			for i := range e.tokens {
-				if e.tokens[i] == slashByte {
-					e = e.nodes[i]
-					mustRedirect = (len(e.part) == 1 && e.middleware != nil) ||
-						(e.entryCase == matchEverything && e.nodes[0].middleware != nil)
+			// No handle found. Check if a handle for this path + a
+			// trailing slash exists for trailing slash recommendation
+			for i := 0; i < len(n.indices); i++ {
+				if n.indices[i] == '/' {
+					n = n.children[i]
+					tsr = (len(n.path) == 1 && n.handle != nil) ||
+						(n.nType == catchAll && n.children[0].handle != nil)
 					return
 				}
 			}
@@ -415,34 +457,29 @@ loop:
 			return
 		}
 
-		mustRedirect = (path == slash) ||
-			(len(e.part) == len(path)+1 && e.part[len(path)] == slashByte &&
-				path == e.part[:len(e.part)-1] && e.middleware != nil)
+		// Nothing found. We can recommend to redirect to the same URL with an
+		// extra trailing slash if a leaf exists for that path
+		tsr = (path == "/") ||
+			(len(n.path) == len(path)+1 && n.path[len(path)] == '/' &&
+				path == n.path[:len(n.path)-1] && n.handle != nil)
 		return
 	}
 }
 
-// precedenceTo just adds the priority of this muxEntry by an index
-func (e *muxEntry) precedenceTo(index int) int {
-	e.nodes[index].precedence++
-	_precedence := e.nodes[index].precedence
-
-	newindex := index
-	for newindex > 0 && e.nodes[newindex-1].precedence < _precedence {
-		tmpN := e.nodes[newindex-1]
-		e.nodes[newindex-1] = e.nodes[newindex]
-		e.nodes[newindex] = tmpN
-
-		newindex--
+// shift bytes in array by n bytes left
+func shiftNRuneBytes(rb [4]byte, n int) [4]byte {
+	switch n {
+	case 0:
+		return rb
+	case 1:
+		return [4]byte{rb[1], rb[2], rb[3], 0}
+	case 2:
+		return [4]byte{rb[2], rb[3]}
+	case 3:
+		return [4]byte{rb[3]}
+	default:
+		return [4]byte{}
 	}
-
-	if newindex != index {
-		e.tokens = e.tokens[:newindex] +
-			e.tokens[index:index+1] +
-			e.tokens[newindex:index] + e.tokens[index+1:]
-	}
-
-	return newindex
 }
 
 type (
@@ -451,7 +488,7 @@ type (
 		// subdomain is empty for default-hostname routes,
 		// ex: mysubdomain.
 		subdomain string
-		entry     *muxEntry
+		entry     *node
 	}
 
 	serveMux struct {
@@ -599,20 +636,20 @@ func New() iris.Policies {
 				tree := mux.getTree(method, subdomain)
 				if tree == nil {
 					//first time we register a route to this method with this domain
-					tree = &muxTree{method: method, subdomain: subdomain, entry: &muxEntry{}}
+					tree = &muxTree{method: method, subdomain: subdomain, entry: &node{}}
 					mux.garden = append(mux.garden, tree)
 				}
 				// I decide that it's better to explicit give subdomain and a path to it than registeredPath(mysubdomain./something) now its: subdomain: mysubdomain., path: /something
 				// we have different tree for each of subdomains, now you can use everything you can use with the normal paths ( before you couldn't set /any/*path)
-				if err := tree.entry.add(path, middleware); err != nil {
+				if err := tree.entry.addRoute(path, middleware); err != nil {
 					// while ProdMode means that the iris should not continue running
 					// by-default it panics on these errors, but to make sure let's introduce the fatalErr to stop visiting
 					fatalErr = true
-					logger(iris.ProdMode, err.Error())
+					logger(iris.ProdMode, Name+" "+err.Error())
 					return
 				}
 
-				if mp := tree.entry.paramsLen; mp > mux.maxParameters {
+				if mp := tree.entry.maxParams; mp > mux.maxParameters {
 					mux.maxParameters = mp
 				}
 
@@ -677,7 +714,7 @@ func (mux *serveMux) buildHandler(pool iris.ContextPool) http.Handler {
 					}
 				}
 
-				mustRedirect := tree.entry.get(routePath, context) // pass the parameters here for 0 allocation
+				mustRedirect := tree.entry.getValue(routePath, context) // pass the parameters here for 0 allocation
 				if context.Middleware != nil {
 					// ok we found the correct route, serve it and exit entirely from here
 					//ctx.Request.Header.SetUserAgentBytes(DefaultUserAgent)
diff --git a/policy_routerwrapper_test.go b/policy_routerwrapper_test.go
index 1d4b0bbc..d004c9aa 100644
--- a/policy_routerwrapper_test.go
+++ b/policy_routerwrapper_test.go
@@ -32,10 +32,6 @@ func TestRouterWrapperPolicySimple(t *testing.T) {
 		// w2 -> w1 -> httprouter -> handler
 	)
 
-	app.OnError(StatusNotFound, func(ctx *Context) {
-		ctx.Writef("not found")
-	})
-
 	app.Get("/", func(ctx *Context) {
 		ctx.Write([]byte("OK"))
 	})