Add some _examples in the main repository too.

Former-commit-id: 98895c34115ec2076b431332f0ffe9645adf7590
This commit is contained in:
Gerasimos (Makis) Maropoulos 2017-03-13 15:16:12 +02:00
parent d76d9b1ec6
commit f487cd0029
29 changed files with 1318 additions and 248 deletions

33
_examples/README.md Normal file
View File

@ -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.
<a href ="https://github.com/kataras/iris"> <img src="http://iris-go.com/assets/book/cover_4.jpg" width="300" /> </a>
## 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!

View File

@ -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, "<h1>Hello!</h1>")
}, -1))
app.Listen(":8080")
}

View File

@ -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")
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

View File

@ -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")
}

View File

@ -0,0 +1,14 @@
{{if .Success}}
<h1>Thanks for your message!</h1>
{{else}}
<h1>Contact</h1>
<form method="POST">
<label>Email:</label><br />
<input type="text" name="email"><br />
<label>Subject:</label><br />
<input type="text" name="subject"><br />
<label>Message:</label><br />
<textarea name="message"></textarea><br />
<input type="submit">
</form>
{{end}}

View File

@ -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")
}

View File

@ -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")
}

View File

@ -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())
}
})
}

View File

@ -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";
});
})();

View File

@ -0,0 +1,43 @@
<html>
<head>
<title>Online visitors example</title>
<style>
body {
margin: 0;
font-family: -apple-system, "San Francisco", "Helvetica Neue", "Noto", "Roboto", "Calibri Light", sans-serif;
color: #212121;
font-size: 1.0em;
line-height: 1.6;
}
.container {
max-width: 750px;
margin: auto;
padding: 15px;
}
#online_views {
font-weight: bold;
font-size: 18px;
}
</style>
</head>
<body>
<div class="container">
<span id="online_views">1 online view</span>
</div>
<script type="text/javascript">
/* take the page source from our passed struct on .Render */
var PAGE_SOURCE = {{ .PageID }}
</script>
<script src="/iris-ws.js"></script>
<script src="/js/visitors.js"></script>
</body>
</html>

View File

@ -0,0 +1,29 @@
<html>
<head>
<title>Different page, different results</title>
<style>
#online_views {
font-weight: bold;
font-size: 18px;
}
</style>
</head>
<body>
<span id="online_views">1 online view</span>
<script type="text/javascript">
/* take the page source from our passed struct on .Render */
var PAGE_SOURCE = {{ .PageID }}
</script>
<script src="/iris-ws.js"></script>
<script src="/js/visitors.js"></script>
</body>
</html>

View File

@ -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)
}

View File

@ -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")
}

View File

@ -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")
}

View File

@ -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")
}

View File

@ -0,0 +1,3 @@
body {
background-color: black;
}

View File

@ -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")
}

View File

@ -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")
}

View File

@ -0,0 +1,10 @@
<h1>Todos</h1>
<ul>
{{range .Todos}}
{{if .Done}}
<li><s>{{.Task}}</s></li>
{{else}}
<li>{{.Task}}</li>
{{end}}
{{end}}
</ul>

View File

@ -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: <b>"+err.Error()+"</b>")
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: <b>"+err.Error()+"</b>")
return
}
defer out.Close()
io.Copy(out, file)
})
// start the server at 127.0.0.1:8080
app.Listen(":8080")
}

View File

@ -0,0 +1,12 @@
<html>
<head>
<title>Upload file</title>
</head>
<body>
<form enctype="multipart/form-data"
action="http://127.0.0.1:8080/upload" method="post">
<input type="file" name="uploadfile" /> <input type="hidden"
name="token" value="{{.}}" /> <input type="submit" value="upload" />
</form>
</body>
</html>

View File

@ -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("<pre>Here is your short URL: <a href='" + shortenURL + "'>" + shortenURL + " </a></pre>")
}
}
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<<letterIdxBits - 1 // All 1-bits, as many as letterIdxBits
letterIdxMax = 63 / letterIdxBits // # of letter indices fitting in 63 bits
)
func randomString(n int) string {
src := rand.NewSource(time.Now().UnixNano())
b := make([]byte, n)
// A src.Int63() generates 63 random bits, enough for letterIdxMax characters!
for i, cache, remain := n-1, src.Int63(), letterIdxMax; i >= 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)
}

View File

@ -0,0 +1,3 @@
body{
background-color:silver;
}

View File

@ -0,0 +1,20 @@
<html>
<head>
<meta charset="utf-8">
<title>Golang URL Shortener</title>
<link rel="stylesheet" href="/static/css/style.css" />
</head>
<body>
<h2>Golang URL Shortener</h2>
<h3>{{ .form_result}}</h3>
<form action="/url/shorten" method="POST">
<input type="text" name="url" style="width: 35em;" />
<input type="submit" value="Shorten!" />
</form>
<p>{{ .url_count }} URLs shortened</p>
</body>
</html>

View File

@ -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")
}

View File

@ -0,0 +1,29 @@
<input id="input" type="text" />
<button onclick="send()">Send</button>
<pre id="output"></pre>
<script src="/iris-ws.js"></script>
<script>
var input = document.getElementById("input");
var output = document.getElementById("output");
// Ws comes from the auto-served '/iris-ws.js'
var socket = new Ws("ws://localhost:8080/echo");
socket.OnConnect(function () {
output.innerHTML += "Status: Connected\n";
});
socket.OnDisconnect(function () {
output.innerHTML += "Status: Disconnected\n";
});
// read events from the server
socket.On("chat", function (msg) {
output.innerHTML += "Server: " + msg + "\n";
});
function send() {
// send chat event data to the server
socket.Emit("chat", input.value);
input.value = "";
}
</script>

View File

@ -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)

View File

@ -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"))
})