From 5a8b17f0e8fa26a6c3292571cd02dd45e8e3411e Mon Sep 17 00:00:00 2001 From: kataras Date: Tue, 7 Nov 2017 19:12:30 +0200 Subject: [PATCH] Add a useful(?) introduction README.md section to some examples Former-commit-id: 14041307dc2f98810d2c20dee68d2c4a7aa63e13 --- _examples/configuration/README.md | 283 +++++++ _examples/http-listening/README.md | 251 ++++++ _examples/mvc/README.md | 970 +++++++++++++++++++++++ _examples/routing/README.md | 1141 ++++++++++++++++++++++++++++ _examples/sessions/README.md | 121 +++ _examples/websocket/README.md | 132 ++++ view/README.md | 177 +++++ 7 files changed, 3075 insertions(+) create mode 100644 _examples/configuration/README.md create mode 100644 _examples/http-listening/README.md create mode 100644 _examples/mvc/README.md create mode 100644 _examples/routing/README.md create mode 100644 _examples/sessions/README.md create mode 100644 _examples/websocket/README.md create mode 100644 view/README.md diff --git a/_examples/configuration/README.md b/_examples/configuration/README.md new file mode 100644 index 00000000..fa54b775 --- /dev/null +++ b/_examples/configuration/README.md @@ -0,0 +1,283 @@ +# Configuration + +All configuration's values have default values, things will work as you expected with `iris.New()`. + +Configuration is useless before listen functions, so it should be passed on `Application#Run/2` (second argument(s)). + +Iris has a type named `Configurator` which is a `func(*iris.Application)`, any function +which completes this can be passed at `Application#Configure` and/or `Application#Run/2`. + +`Application#ConfigurationReadOnly()` returns the configuration values. + +`.Run` **by `Configuration` struct** + +```go +package main + +import ( + "github.com/kataras/iris" +) + +func main() { + app := iris.New() + app.Get("/", func(ctx iris.Context) { + ctx.HTML("Hello!") + }) + // [...] + + // Good when you want to modify the whole configuration. + app.Run(iris.Addr(":8080"), iris.WithConfiguration(iris.Configuration{ + DisableStartupLog: false, + DisableInterruptHandler: false, + DisablePathCorrection: false, + EnablePathEscape: false, + FireMethodNotAllowed: false, + DisableBodyConsumptionOnUnmarshal: false, + DisableAutoFireStatusCode: false, + TimeFormat: "Mon, 02 Jan 2006 15:04:05 GMT", + Charset: "UTF-8", + })) +} +``` + +`.Run` **by options** + +```go +package main + +import ( + "github.com/kataras/iris" +) + +func main() { + app := iris.New() + app.Get("/", func(ctx iris.Context) { + ctx.HTML("Hello!") + }) + // [...] + + // Good when you want to change some of the configuration's field. + // Prefix: "With", code editors will help you navigate through all + // configuration options without even a glitch to the documentation. + + app.Run(iris.Addr(":8080"), iris.WithoutStartupLog, iris.WithCharset("UTF-8")) + + // or before run: + // app.Configure(iris.WithoutStartupLog, iris.WithCharset("UTF-8")) + // app.Run(iris.Addr(":8080")) +} +``` + +`.Run` **by TOML config file** + +```tml +DisablePathCorrection = false +EnablePathEscape = false +FireMethodNotAllowed = true +DisableBodyConsumptionOnUnmarshal = false +TimeFormat = "Mon, 01 Jan 2006 15:04:05 GMT" +Charset = "UTF-8" + +[Other] + MyServerName = "iris" + +``` + +```go +package main + +import ( + "github.com/kataras/iris" +) + +func main() { + app := iris.New() + + app.Get("/", func(ctx iris.Context) { + ctx.HTML("Hello!") + }) + // [...] + + // Good when you have two configurations, one for development and a different one for production use. + app.Run(iris.Addr(":8080"), iris.WithConfiguration(iris.TOML("./configs/iris.tml"))) +} +``` + + +`.Run` **by YAML config file** + +```yml +DisablePathCorrection: false +EnablePathEscape: false +FireMethodNotAllowed: true +DisableBodyConsumptionOnUnmarshal: true +TimeFormat: Mon, 01 Jan 2006 15:04:05 GMT +Charset: UTF-8 +``` + +```go +package main + +import ( + "github.com/kataras/iris" +) + +func main() { + app := iris.New() + app.Get("/", func(ctx iris.Context) { + ctx.HTML("Hello!") + }) + // [...] + + app.Run(iris.Addr(":8080"), iris.WithConfiguration(iris.YAML("./configs/iris.yml"))) +} + +``` + +## Built'n Configurators + +```go +// WithoutServerError will cause to ignore the matched "errors" +// from the main application's `Run` function. +// +// Usage: +// err := app.Run(iris.Addr(":8080"), iris.WithoutServerError(iris.ErrServerClosed)) +// will return `nil` if the server's error was `http/iris#ErrServerClosed`. +// +// See `Configuration#IgnoreServerErrors []string` too. +// +// Example: https://github.com/kataras/iris/tree/master/_examples/http-listening/listen-addr/omit-server-errors +func WithoutServerError(errors ...error) Configurator + +// WithoutStartupLog turns off the information send, once, to the terminal when the main server is open. +var WithoutStartupLog + +// WithoutInterruptHandler disables the automatic graceful server shutdown +// when control/cmd+C pressed. +var WithoutInterruptHandler + +// WithoutPathCorrection disables the PathCorrection setting. +// +// See `Configuration`. +var WithoutPathCorrection + +// WithoutBodyConsumptionOnUnmarshal disables BodyConsumptionOnUnmarshal setting. +// +// See `Configuration`. +var WithoutBodyConsumptionOnUnmarshal + +// WithoutAutoFireStatusCode disables the AutoFireStatusCode setting. +// +// See `Configuration`. +var WithoutAutoFireStatusCode + +// WithPathEscape enanbles the PathEscape setting. +// +// See `Configuration`. +var WithPathEscape + +// WithOptimizations can force the application to optimize for the best performance where is possible. +// +// See `Configuration`. +var WithOptimizations + +// WithFireMethodNotAllowed enanbles the FireMethodNotAllowed setting. +// +// See `Configuration`. +var WithFireMethodNotAllowed + +// WithTimeFormat sets the TimeFormat setting. +// +// See `Configuration`. +func WithTimeFormat(timeformat string) Configurator + +// WithCharset sets the Charset setting. +// +// See `Configuration`. +func WithCharset(charset string) Configurator + +// WithRemoteAddrHeader enables or adds a new or existing request header name +// that can be used to validate the client's real IP. +// +// Existing values are: +// "X-Real-Ip": false, +// "X-Forwarded-For": false, +// "CF-Connecting-IP": false +// +// Look `context.RemoteAddr()` for more. +func WithRemoteAddrHeader(headerName string) Configurator + +// WithoutRemoteAddrHeader disables an existing request header name +// that can be used to validate the client's real IP. +// +// Existing values are: +// "X-Real-Ip": false, +// "X-Forwarded-For": false, +// "CF-Connecting-IP": false +// +// Look `context.RemoteAddr()` for more. +func WithoutRemoteAddrHeader(headerName string) Configurator + +// WithOtherValue adds a value based on a key to the Other setting. +// +// See `Configuration`. +func WithOtherValue(key string, val interface{}) Configurator +``` + +## Custom Configurator + +With the `Configurator` developers can modularize their applications with ease. + +Example Code: + +```go +// file counter/counter.go +package counter + +import ( + "time" + + "github.com/kataras/iris" + "github.com/kataras/iris/core/host" +) + +func Configurator(app *iris.Application) { + counterValue := 0 + + go func() { + ticker := time.NewTicker(time.Second) + + for range ticker.C { + counterValue++ + } + + app.ConfigureHost(func(h *host.Supervisor) { // <- HERE: IMPORTANT + h.RegisterOnShutdown(func() { + ticker.Stop() + }) + }) + }() + + app.Get("/counter", func(ctx iris.Context) { + ctx.Writef("Counter value = %d", counterValue) + }) +} +``` + +```go +// file: main.go +package main + +import ( + "counter" + + "github.com/kataras/iris" +) + +func main() { + app := iris.New() + app.Configure(counter.Configurator) + + app.Run(iris.Addr(":8080")) +} +``` \ No newline at end of file diff --git a/_examples/http-listening/README.md b/_examples/http-listening/README.md new file mode 100644 index 00000000..c315b41d --- /dev/null +++ b/_examples/http-listening/README.md @@ -0,0 +1,251 @@ +# Hosts + +## Listen and Serve + +You can start the server(s) listening to any type of `net.Listener` or even `http.Server` instance. +The method for initialization of the server should be passed at the end, via `Run` function. + +The most common method that Go developers are use to serve their servers are +by passing a network address with form of "hostname:ip". With Iris +we use the `iris.Addr` which is an `iris.Runner` type + +```go +// Listening on tcp with network address 0.0.0.0:8080 +app.Run(iris.Addr(":8080")) +``` + +Sometimes you have created a standard net/http server somewhere else in your app and want to use that to serve the Iris web app + +```go +// Same as before but using a custom http.Server which may being used somewhere else too +app.Run(iris.Server(&http.Server{Addr:":8080"})) +``` + +The most advanced usage is to create a custom or a standard `net.Listener` and pass that to `app.Run` + +```go +// Using a custom net.Listener +l, err := net.Listen("tcp4", ":8080") +if err != nil { + panic(err) +} +app.Run(iris.Listener(l)) +``` + +A more complete example, using the unix-only socket files feature + +```go +package main + +import ( + "os" + "net" + + "github.com/kataras/iris" +) + +func main() { + app := iris.New() + + // UNIX socket + if errOs := os.Remove(socketFile); errOs != nil && !os.IsNotExist(errOs) { + app.Logger().Fatal(errOs) + } + + l, err := net.Listen("unix", socketFile) + + if err != nil { + app.Logger().Fatal(err) + } + + if err = os.Chmod(socketFile, mode); err != nil { + app.Logger().Fatal(err) + } + + app.Run(iris.Listener(l)) +} +``` + +UNIX and BSD hosts can take advandage of the reuse port feature + +```go +package main + +import ( + // Package tcplisten provides customizable TCP net.Listener with various + // performance-related options: + // + // - SO_REUSEPORT. This option allows linear scaling server performance + // on multi-CPU servers. + // See https://www.nginx.com/blog/socket-sharding-nginx-release-1-9-1/ for details. + // + // - TCP_DEFER_ACCEPT. This option expects the server reads from the accepted + // connection before writing to them. + // + // - TCP_FASTOPEN. See https://lwn.net/Articles/508865/ for details. + "github.com/valyala/tcplisten" + + "github.com/kataras/iris" +) + +// go get github.com/valyala/tcplisten +// go run main.go + +func main() { + app := iris.New() + + app.Get("/", func(ctx iris.Context) { + ctx.HTML("

Hello World!

") + }) + + listenerCfg := tcplisten.Config{ + ReusePort: true, + DeferAccept: true, + FastOpen: true, + } + + l, err := listenerCfg.NewListener("tcp", ":8080") + if err != nil { + app.Logger().Fatal(err) + } + + app.Run(iris.Listener(l)) +} +``` + +### HTTP/2 and Secure + +If you have signed file keys you can use the `iris.TLS` to serve `https` based on those certification keys + +```go +// TLS using files +app.Run(iris.TLS("127.0.0.1:443", "mycert.cert", "mykey.key")) +``` + +The method you should use when your app is ready for **production** is the `iris.AutoTLS` which starts a secure server with automated certifications provided by https://letsencrypt.org for **free** + +```go +// Automatic TLS +app.Run(iris.AutoTLS(":443", "example.com", "admin@example.com")) +``` + +### Any `iris.Runner` + +There may be times that you want something very special to listen on, which is not a type of `net.Listener`. You are able to do that by `iris.Raw`, but you're responsible of that method + +```go +// Using any func() error, +// the responsibility of starting up a listener is up to you with this way, +// for the sake of simplicity we will use the +// ListenAndServe function of the `net/http` package. +app.Run(iris.Raw(&http.Server{Addr:":8080"}).ListenAndServe) +``` + +## Host configurators + +All the above forms of listening are accepting a last, variadic argument of `func(*iris.Supervisor)`. This is used to add configurators for that specific host you passed via those functions. + +For example let's say that we want to add a callback which is fired when +the server is shutdown + +```go +app.Run(iris.Addr(":8080", func(h *iris.Supervisor) { + h.RegisterOnShutdown(func() { + println("server terminated") + }) +})) +``` + +You can even do that before `app.Run` method, but the difference is that +these host configurators will be executed to all hosts that you may use to serve your web app (via `app.NewHost` we'll see that in a minute) + +```go +app := iris.New() +app.ConfigureHost(func(h *iris.Supervisor) { + h.RegisterOnShutdown(func() { + println("server terminated") + }) +}) +app.Run(iris.Addr(":8080")) +``` + +Access to all hosts that serve your application can be provided by +the `Application#Hosts` field, after the `Run` method. + +But the most common scenario is that you may need access to the host before the `app.Run` method, +there are two ways of gain access to the host supervisor, read below. + +We have already saw how to configure all application's hosts by second argument of `app.Run` or `app.ConfigureHost`. There is one more way which suits better for simple scenarios and that is to use the `app.NewHost` to create a new host +and use one of its `Serve` or `Listen` functions +to start the application via the `iris#Raw` Runner. + +Note that this way needs an extra import of the `net/http` package. + +Example Code: + +```go +h := app.NewHost(&http.Server{Addr:":8080"}) +h.RegisterOnShutdown(func(){ + println("server terminated") +}) + +app.Run(iris.Raw(h.ListenAndServe)) +``` + +## Multi hosts + +You can serve your Iris web app using more than one server, the `iris.Router` is compatible with the `net/http/Handler` function therefore, as you can understand, it can be used to be adapted at any `net/http` server, however there is an easier way, by using the `app.NewHost` which is also copying all the host configurators and it closes all the hosts attached to the particular web app on `app.Shutdown`. + +```go +app := iris.New() +app.Get("/", indexHandler) + +// run in different goroutine in order to not block the main "goroutine". +go app.Run(iris.Addr(":8080")) +// start a second server which is listening on tcp 0.0.0.0:9090, +// without "go" keyword because we want to block at the last server-run. +app.NewHost(&http.Server{Addr:":9090"}).ListenAndServe() +``` + +## Shutdown (Gracefully) + +Let's continue by learning how to catch CONTROL+C/COMMAND+C or unix kill command and shutdown the server gracefully. + +> Gracefully Shutdown on CONTROL+C/COMMAND+C or when kill command sent is ENABLED BY-DEFAULT. + +In order to manually manage what to do when app is interrupted, +we have to disable the default behavior with the option `WithoutInterruptHandler` +and register a new interrupt handler (globally, across all possible hosts). + + +Example code: + +```go +package main + +import ( + "context" + "time" + + "github.com/kataras/iris" +) + + +func main() { + app := iris.New() + + iris.RegisterOnInterrupt(func() { + timeout := 5 * time.Second + ctx, cancel := context.WithTimeout(context.Background(), timeout) + defer cancel() + // close all hosts + app.Shutdown(ctx) + }) + + app.Get("/", func(ctx iris.Context) { + ctx.HTML("

hi, I just exist in order to see if the server is closed

") + }) + + app.Run(iris.Addr(":8080"), iris.WithoutInterruptHandler) +} +``` \ No newline at end of file diff --git a/_examples/mvc/README.md b/_examples/mvc/README.md new file mode 100644 index 00000000..1593996e --- /dev/null +++ b/_examples/mvc/README.md @@ -0,0 +1,970 @@ +# MVC + +![](https://github.com/kataras/iris/raw/master/_examples/mvc/web_mvc_diagram.png) + +Iris has **first-class support for the MVC pattern**, you'll not find +these stuff anywhere else in the Go world. + +Iris supports Request data, Models, Persistence Data and Binding +with the fastest possible execution. + +## Characteristics + +All HTTP Methods are supported, for example if want to serve `GET` +then the controller should have a function named `Get()`, +you can define more than one method function to serve in the same Controller struct. + +Persistence data inside your Controller struct (share data between requests) +via `iris:"persistence"` tag right to the field or Bind using `app.Controller("/" , new(myController), theBindValue)`. + +Models inside your Controller struct (set-ed at the Method function and rendered by the View) +via `iris:"model"` tag right to the field, i.e ```User UserModel `iris:"model" name:"user"` ``` view will recognise it as `{{.user}}`. +If `name` tag is missing then it takes the field's name, in this case the `"User"`. + +Access to the request path and its parameters via the `Path and Params` fields. + +Access to the template file that should be rendered via the `Tmpl` field. + +Access to the template data that should be rendered inside +the template file via `Data` field. + +Access to the template layout via the `Layout` field. + +Access to the low-level `iris.Context` via the `Ctx` field. + +Get the relative request path by using the controller's name via `RelPath()`. + +Get the relative template path directory by using the controller's name via `RelTmpl()`. + +Flow as you used to, `Controllers` can be registered to any `Party`, +including Subdomains, the Party's begin and done handlers work as expected. + +Optional `BeginRequest(ctx)` function to perform any initialization before the method execution, +useful to call middlewares or when many methods use the same collection of data. + +Optional `EndRequest(ctx)` function to perform any finalization after any method executed. + +Inheritance, recursively, see for example our `mvc.SessionController`, it has the `iris.Controller` as an embedded field +and it adds its logic to its `BeginRequest`, [here](https://github.com/kataras/iris/blob/master/mvc/session_controller.go). + +Read access to the current route via the `Route` field. + +Register one or more relative paths and able to get path parameters, i.e + +If `app.Controller("/user", new(user.Controller))` + +- `func(*Controller) Get()` - `GET:/user` , as usual. +- `func(*Controller) Post()` - `POST:/user`, as usual. +- `func(*Controller) GetLogin()` - `GET:/user/login` +- `func(*Controller) PostLogin()` - `POST:/user/login` +- `func(*Controller) GetProfileFollowers()` - `GET:/user/profile/followers` +- `func(*Controller) PostProfileFollowers()` - `POST:/user/profile/followers` +- `func(*Controller) GetBy(id int64)` - `GET:/user/{param:long}` +- `func(*Controller) PostBy(id int64)` - `POST:/user/{param:long}` + +If `app.Controller("/profile", new(profile.Controller))` + +- `func(*Controller) GetBy(username string)` - `GET:/profile/{param:string}` + +If `app.Controller("/assets", new(file.Controller))` + +- `func(*Controller) GetByWildard(path string)` - `GET:/assets/{param:path}` + + Supported types for method functions receivers: int, int64, bool and string. + +Response via output arguments, optionally, i.e + +```go +func(c *ExampleController) Get() string | + (string, string) | + (string, int) | + int | + (int, string) | + (string, error) | + error | + (int, error) | + (any, bool) | + (customStruct, error) | + customStruct | + (customStruct, int) | + (customStruct, string) | + mvc.Result or (mvc.Result, error) +``` + +where [mvc.Result](https://github.com/kataras/iris/blob/master/mvc/method_result.go) is an interface which contains only that function: `Dispatch(ctx iris.Context)`. + +## Using Iris MVC for code reuse + +By creating components that are independent of one another, developers are able to reuse components quickly and easily in other applications. The same (or similar) view for one application can be refactored for another application with different data because the view is simply handling how the data is being displayed to the user. + +If you're new to back-end web development read about the MVC architectural pattern first, a good start is that [wikipedia article](https://en.wikipedia.org/wiki/Model%E2%80%93view%E2%80%93controller). + +## Quick MVC Tutorial Part 1 (without output result) + +```go +package main + +import ( + "github.com/kataras/iris" + "github.com/kataras/iris/mvc" +) + +func main() { + app := iris.New() + + app.Controller("/helloworld", new(HelloWorldController)) + + app.Run(iris.Addr("localhost:8080")) +} + +type HelloWorldController struct { + mvc.C + + // [ Your fields here ] + // Request lifecycle data + // Models + // Database + // Global properties +} + +// +// GET: /helloworld + +func (c *HelloWorldController) Get() string { + return "This is my default action..." +} + +// +// GET: /helloworld/{name:string} + +func (c *HelloWorldController) GetBy(name string) string { + return "Hello " + name +} + +// +// GET: /helloworld/welcome + +func (c *HelloWorldController) GetWelcome() (string, int) { + return "This is the GetWelcome action func...", iris.StatusOK +} + +// +// GET: /helloworld/welcome/{name:string}/{numTimes:int} + +func (c *HelloWorldController) GetWelcomeBy(name string, numTimes int) { + // Access to the low-level Context, + // output arguments are optional of course so we don't have to use them here. + c.Ctx.Writef("Hello %s, NumTimes is: %d", name, numTimes) +} + +/* +func (c *HelloWorldController) Post() {} handles HTTP POST method requests +func (c *HelloWorldController) Put() {} handles HTTP PUT method requests +func (c *HelloWorldController) Delete() {} handles HTTP DELETE method requests +func (c *HelloWorldController) Connect() {} handles HTTP CONNECT method requests +func (c *HelloWorldController) Head() {} handles HTTP HEAD method requests +func (c *HelloWorldController) Patch() {} handles HTTP PATCH method requests +func (c *HelloWorldController) Options() {} handles HTTP OPTIONS method requests +func (c *HelloWorldController) Trace() {} handles HTTP TRACE method requests +*/ + +/* +func (c *HelloWorldController) All() {} handles All method requests +// OR +func (c *HelloWorldController) Any() {} handles All method requests +*/ +``` + +> The [_examples/mvc](https://github.com/kataras/iris/tree/master/_examples/mvc) and [mvc/controller_test.go](https://github.com/kataras/iris/blob/master/mvc/controller_test.go) files explain each feature with simple paradigms, they show how you can take advandage of the Iris MVC Binder, Iris MVC Models and many more... + +Every `exported` func prefixed with an HTTP Method(`Get`, `Post`, `Put`, `Delete`...) in a controller is callable as an HTTP endpoint. In the sample above, all funcs writes a string to the response. Note the comments preceding each method. + +An HTTP endpoint is a targetable URL in the web application, such as `http://localhost:8080/helloworld`, and combines the protocol used: HTTP, the network location of the web server (including the TCP port): `localhost:8080` and the target URI `/helloworld`. + +The first comment states this is an [HTTP GET](https://www.w3schools.com/tags/ref_httpmethods.asp) method that is invoked by appending "/helloworld" to the base URL. The third comment specifies an [HTTP GET](https://www.w3schools.com/tags/ref_httpmethods.asp) method that is invoked by appending "/helloworld/welcome" to the URL. + +Controller knows how to handle the "name" on `GetBy` or the "name" and "numTimes" at `GetWelcomeBy`, because of the `By` keyword, and builds the dynamic route without boilerplate; the third comment specifies an [HTTP GET](https://www.w3schools.com/tags/ref_httpmethods.asp) dynamic method that is invoked by any URL that starts with "/helloworld/welcome" and followed by two more path parts, the first one can accept any value and the second can accept only numbers, i,e: "http://localhost:8080/helloworld/welcome/golang/32719", otherwise a [404 Not Found HTTP Error](https://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html#sec10.4.5) will be sent to the client instead. + +---- + +### Quick MVC Tutorial #2 + +Iris has a very powerful and **blazing [fast](_benchmarks)** MVC support, you can return any value of any type from a method function +and it will be sent to the client as expected. + +* if `string` then it's the body. +* if `string` is the second output argument then it's the content type. +* if `int` then it's the status code. +* if `error` and not nil then (any type) response will be omitted and error's text with a 400 bad request will be rendered instead. +* if `(int, error)` and error is not nil then the response result will be the error's text with the status code as `int`. +* if `bool` is false then it throws 404 not found http error by skipping everything else. +* if `custom struct` or `interface{}` or `slice` or `map` then it will be rendered as json, unless a `string` content type is following. +* if `mvc.Result` then it executes its `Dispatch` function, so good design patters can be used to split the model's logic where needed. + +The example below is not intended to be used in production but it's a good showcase of some of the return types we saw before; + +```go +package main + +import ( + "github.com/kataras/iris" + "github.com/kataras/iris/middleware/basicauth" + "github.com/kataras/iris/mvc" +) + +// Movie is our sample data structure. +type Movie struct { + Name string `json:"name"` + Year int `json:"year"` + Genre string `json:"genre"` + Poster string `json:"poster"` +} + +// movies contains our imaginary data source. +var movies = []Movie{ + { + Name: "Casablanca", + Year: 1942, + Genre: "Romance", + Poster: "https://iris-go.com/images/examples/mvc-movies/1.jpg", + }, + { + Name: "Gone with the Wind", + Year: 1939, + Genre: "Romance", + Poster: "https://iris-go.com/images/examples/mvc-movies/2.jpg", + }, + { + Name: "Citizen Kane", + Year: 1941, + Genre: "Mystery", + Poster: "https://iris-go.com/images/examples/mvc-movies/3.jpg", + }, + { + Name: "The Wizard of Oz", + Year: 1939, + Genre: "Fantasy", + Poster: "https://iris-go.com/images/examples/mvc-movies/4.jpg", + }, +} + + +var basicAuth = basicauth.New(basicauth.Config{ + Users: map[string]string{ + "admin": "password", + }, +}) + + +func main() { + app := iris.New() + + app.Use(basicAuth) + + app.Controller("/movies", new(MoviesController)) + + app.Run(iris.Addr(":8080")) +} + +// MoviesController is our /movies controller. +type MoviesController struct { + mvc.C +} + +// Get returns list of the movies +// Demo: +// curl -i http://localhost:8080/movies +func (c *MoviesController) Get() []Movie { + return movies +} + +// GetBy returns a movie +// Demo: +// curl -i http://localhost:8080/movies/1 +func (c *MoviesController) GetBy(id int) Movie { + return movies[id] +} + +// PutBy updates a movie +// Demo: +// curl -i -X PUT -F "genre=Thriller" -F "poster=@/Users/kataras/Downloads/out.gif" http://localhost:8080/movies/1 +func (c *MoviesController) PutBy(id int) Movie { + // get the movie + m := movies[id] + + // get the request data for poster and genre + file, info, err := c.Ctx.FormFile("poster") + if err != nil { + c.Ctx.StatusCode(iris.StatusInternalServerError) + return Movie{} + } + file.Close() // we don't need the file + poster := info.Filename // imagine that as the url of the uploaded file... + genre := c.Ctx.FormValue("genre") + + // update the poster + m.Poster = poster + m.Genre = genre + movies[id] = m + + return m +} + +// DeleteBy deletes a movie +// Demo: +// curl -i -X DELETE -u admin:password http://localhost:8080/movies/1 +func (c *MoviesController) DeleteBy(id int) iris.Map { + // delete the entry from the movies slice + deleted := movies[id].Name + movies = append(movies[:id], movies[id+1:]...) + // and return the deleted movie's name + return iris.Map{"deleted": deleted} +} +``` + +### Quick MVC Tutorial #3 + +Nothing stops you from using your favorite **folder structure**. Iris is a low level web framework, it has got MVC first-class support but it doesn't limit your folder structure, this is your choice. + +Structuring depends on your own needs. We can't tell you how to design your own application for sure but you're free to take a closer look to one typical example below; + +[![folder structure example](_examples/mvc/overview/folder_structure.png)](_examples/mvc/overview) + +Shhh, let's spread the code itself. + +#### Data Model Layer + +```go +// file: datamodels/movie.go + +package datamodels + +// Movie is our sample data structure. +// Keep note that the tags for public-use (for our web app) +// should be kept in other file like "web/viewmodels/movie.go" +// which could wrap by embedding the datamodels.Movie or +// declare new fields instead butwe will use this datamodel +// as the only one Movie model in our application, +// for the shake of simplicty. +type Movie struct { + ID int64 `json:"id"` + Name string `json:"name"` + Year int `json:"year"` + Genre string `json:"genre"` + Poster string `json:"poster"` +} +``` + +#### Data Source / Data Store Layer + +```go +// file: datasource/movies.go + +package datasource + +import "github.com/kataras/iris/_examples/mvc/overview/datamodels" + +// Movies is our imaginary data source. +var Movies = map[int64]datamodels.Movie{ + 1: { + ID: 1, + Name: "Casablanca", + Year: 1942, + Genre: "Romance", + Poster: "https://iris-go.com/images/examples/mvc-movies/1.jpg", + }, + 2: { + ID: 2, + Name: "Gone with the Wind", + Year: 1939, + Genre: "Romance", + Poster: "https://iris-go.com/images/examples/mvc-movies/2.jpg", + }, + 3: { + ID: 3, + Name: "Citizen Kane", + Year: 1941, + Genre: "Mystery", + Poster: "https://iris-go.com/images/examples/mvc-movies/3.jpg", + }, + 4: { + ID: 4, + Name: "The Wizard of Oz", + Year: 1939, + Genre: "Fantasy", + Poster: "https://iris-go.com/images/examples/mvc-movies/4.jpg", + }, + 5: { + ID: 5, + Name: "North by Northwest", + Year: 1959, + Genre: "Thriller", + Poster: "https://iris-go.com/images/examples/mvc-movies/5.jpg", + }, +} +``` + +#### Repositories + +The layer which has direct access to the "datasource" and can manipulate data directly. + +```go +// file: repositories/movie_repository.go + +package repositories + +import ( + "errors" + "sync" + + "github.com/kataras/iris/_examples/mvc/overview/datamodels" +) + +// Query represents the visitor and action queries. +type Query func(datamodels.Movie) bool + +// MovieRepository handles the basic operations of a movie entity/model. +// It's an interface in order to be testable, i.e a memory movie repository or +// a connected to an sql database. +type MovieRepository interface { + Exec(query Query, action Query, limit int, mode int) (ok bool) + + Select(query Query) (movie datamodels.Movie, found bool) + SelectMany(query Query, limit int) (results []datamodels.Movie) + + InsertOrUpdate(movie datamodels.Movie) (updatedMovie datamodels.Movie, err error) + Delete(query Query, limit int) (deleted bool) +} + +// NewMovieRepository returns a new movie memory-based repository, +// the one and only repository type in our example. +func NewMovieRepository(source map[int64]datamodels.Movie) MovieRepository { + return &movieMemoryRepository{source: source} +} + +// movieMemoryRepository is a "MovieRepository" +// which manages the movies using the memory data source (map). +type movieMemoryRepository struct { + source map[int64]datamodels.Movie + mu sync.RWMutex +} + +const ( + // ReadOnlyMode will RLock(read) the data . + ReadOnlyMode = iota + // ReadWriteMode will Lock(read/write) the data. + ReadWriteMode +) + +func (r *movieMemoryRepository) Exec(query Query, action Query, actionLimit int, mode int) (ok bool) { + loops := 0 + + if mode == ReadOnlyMode { + r.mu.RLock() + defer r.mu.RUnlock() + } else { + r.mu.Lock() + defer r.mu.Unlock() + } + + for _, movie := range r.source { + ok = query(movie) + if ok { + if action(movie) { + loops++ + if actionLimit >= loops { + break // break + } + } + } + } + + return +} + +// Select receives a query function +// which is fired for every single movie model inside +// our imaginary data source. +// When that function returns true then it stops the iteration. +// +// It returns the query's return last known "found" value +// and the last known movie model +// to help callers to reduce the LOC. +// +// It's actually a simple but very clever prototype function +// I'm using everywhere since I firstly think of it, +// hope you'll find it very useful as well. +func (r *movieMemoryRepository) Select(query Query) (movie datamodels.Movie, found bool) { + found = r.Exec(query, func(m datamodels.Movie) bool { + movie = m + return true + }, 1, ReadOnlyMode) + + // set an empty datamodels.Movie if not found at all. + if !found { + movie = datamodels.Movie{} + } + + return +} + +// SelectMany same as Select but returns one or more datamodels.Movie as a slice. +// If limit <=0 then it returns everything. +func (r *movieMemoryRepository) SelectMany(query Query, limit int) (results []datamodels.Movie) { + r.Exec(query, func(m datamodels.Movie) bool { + results = append(results, m) + return true + }, limit, ReadOnlyMode) + + return +} + +// InsertOrUpdate adds or updates a movie to the (memory) storage. +// +// Returns the new movie and an error if any. +func (r *movieMemoryRepository) InsertOrUpdate(movie datamodels.Movie) (datamodels.Movie, error) { + id := movie.ID + + if id == 0 { // Create new action + var lastID int64 + // find the biggest ID in order to not have duplications + // in productions apps you can use a third-party + // library to generate a UUID as string. + r.mu.RLock() + for _, item := range r.source { + if item.ID > lastID { + lastID = item.ID + } + } + r.mu.RUnlock() + + id = lastID + 1 + movie.ID = id + + // map-specific thing + r.mu.Lock() + r.source[id] = movie + r.mu.Unlock() + + return movie, nil + } + + // Update action based on the movie.ID, + // here we will allow updating the poster and genre if not empty. + // Alternatively we could do pure replace instead: + // r.source[id] = movie + // and comment the code below; + current, exists := r.Select(func(m datamodels.Movie) bool { + return m.ID == id + }) + + if !exists { // ID is not a real one, return an error. + return datamodels.Movie{}, errors.New("failed to update a nonexistent movie") + } + + // or comment these and r.source[id] = m for pure replace + if movie.Poster != "" { + current.Poster = movie.Poster + } + + if movie.Genre != "" { + current.Genre = movie.Genre + } + + // map-specific thing + r.mu.Lock() + r.source[id] = current + r.mu.Unlock() + + return movie, nil +} + +func (r *movieMemoryRepository) Delete(query Query, limit int) bool { + return r.Exec(query, func(m datamodels.Movie) bool { + delete(r.source, m.ID) + return true + }, limit, ReadWriteMode) +} +``` + +#### Services + +The layer which has access to call functions from the "repositories" and "models" (or even "datamodels" if simple application). It should contain the most of the domain logic. + +```go +// file: services/movie_service.go + +package services + +import ( + "github.com/kataras/iris/_examples/mvc/overview/datamodels" + "github.com/kataras/iris/_examples/mvc/overview/repositories" +) + +// MovieService handles some of the CRUID operations of the movie datamodel. +// It depends on a movie repository for its actions. +// It's here to decouple the data source from the higher level compoments. +// As a result a different repository type can be used with the same logic without any aditional changes. +// It's an interface and it's used as interface everywhere +// because we may need to change or try an experimental different domain logic at the future. +type MovieService interface { + GetAll() []datamodels.Movie + GetByID(id int64) (datamodels.Movie, bool) + DeleteByID(id int64) bool + UpdatePosterAndGenreByID(id int64, poster string, genre string) (datamodels.Movie, error) +} + +// NewMovieService returns the default movie service. +func NewMovieService(repo repositories.MovieRepository) MovieService { + return &movieService{ + repo: repo, + } +} + +type movieService struct { + repo repositories.MovieRepository +} + +// GetAll returns all movies. +func (s *movieService) GetAll() []datamodels.Movie { + return s.repo.SelectMany(func(_ datamodels.Movie) bool { + return true + }, -1) +} + +// GetByID returns a movie based on its id. +func (s *movieService) GetByID(id int64) (datamodels.Movie, bool) { + return s.repo.Select(func(m datamodels.Movie) bool { + return m.ID == id + }) +} + +// UpdatePosterAndGenreByID updates a movie's poster and genre. +func (s *movieService) UpdatePosterAndGenreByID(id int64, poster string, genre string) (datamodels.Movie, error) { + // update the movie and return it. + return s.repo.InsertOrUpdate(datamodels.Movie{ + ID: id, + Poster: poster, + Genre: genre, + }) +} + +// DeleteByID deletes a movie by its id. +// +// Returns true if deleted otherwise false. +func (s *movieService) DeleteByID(id int64) bool { + return s.repo.Delete(func(m datamodels.Movie) bool { + return m.ID == id + }, 1) +} +``` + +#### View Models + +There should be the view models, the structure that the client will be able to see. + +Example: + +```go +import ( + "github.com/kataras/iris/_examples/mvc/overview/datamodels" + + "github.com/kataras/iris/context" +) + +type Movie struct { + datamodels.Movie +} + +func (m Movie) IsValid() bool { + /* do some checks and return true if it's valid... */ + return m.ID > 0 +} +``` + +Iris is able to convert any custom data Structure into an HTTP Response Dispatcher, +so theoretically, something like the following is permitted if it's really necessary; + +```go +// Dispatch completes the `kataras/iris/mvc#Result` interface. +// Sends a `Movie` as a controlled http response. +// If its ID is zero or less then it returns a 404 not found error +// else it returns its json representation, +// (just like the controller's functions do for custom types by default). +// +// Don't overdo it, the application's logic should not be here. +// It's just one more step of validation before the response, +// simple checks can be added here. +// +// It's just a showcase, +// imagine the potentials this feature gives when designing a bigger application. +// +// This is called where the return value from a controller's method functions +// is type of `Movie`. +// For example the `controllers/movie_controller.go#GetBy`. +func (m Movie) Dispatch(ctx context.Context) { + if !m.IsValid() { + ctx.NotFound() + return + } + ctx.JSON(m, context.JSON{Indent: " "}) +} +``` + +However, we will use the "datamodels" as the only one models package because +Movie structure doesn't contain any sensitive data, clients are able to see all of its fields +and we don't need any extra functionality or validation inside it. + +#### Controllers + +Handles web requests, bridge between the services and the client. + +```go +// file: web/controllers/movie_controller.go + +package controllers + +import ( + "errors" + + "github.com/kataras/iris/_examples/mvc/overview/datamodels" + "github.com/kataras/iris/_examples/mvc/overview/services" + + "github.com/kataras/iris" + "github.com/kataras/iris/mvc" +) + +// MovieController is our /movies controller. +type MovieController struct { + mvc.C + + // Our MovieService, it's an interface which + // is binded from the main application. + Service services.MovieService +} + +// Get returns list of the movies. +// Demo: +// curl -i http://localhost:8080/movies +// +// The correct way if you have sensitive data: +// func (c *MovieController) Get() (results []viewmodels.Movie) { +// data := c.Service.GetAll() +// +// for _, movie := range data { +// results = append(results, viewmodels.Movie{movie}) +// } +// return +// } +// otherwise just return the datamodels. +func (c *MovieController) Get() (results []datamodels.Movie) { + return c.Service.GetAll() +} + +// GetBy returns a movie. +// Demo: +// curl -i http://localhost:8080/movies/1 +func (c *MovieController) GetBy(id int64) (movie datamodels.Movie, found bool) { + return c.Service.GetByID(id) // it will throw 404 if not found. +} + +// PutBy updates a movie. +// Demo: +// curl -i -X PUT -F "genre=Thriller" -F "poster=@/Users/kataras/Downloads/out.gif" http://localhost:8080/movies/1 +func (c *MovieController) PutBy(id int64) (datamodels.Movie, error) { + // get the request data for poster and genre + file, info, err := c.Ctx.FormFile("poster") + if err != nil { + return datamodels.Movie{}, errors.New("failed due form file 'poster' missing") + } + // we don't need the file so close it now. + file.Close() + + // imagine that is the url of the uploaded file... + poster := info.Filename + genre := c.Ctx.FormValue("genre") + + return c.Service.UpdatePosterAndGenreByID(id, poster, genre) +} + +// DeleteBy deletes a movie. +// Demo: +// curl -i -X DELETE -u admin:password http://localhost:8080/movies/1 +func (c *MovieController) DeleteBy(id int64) interface{} { + wasDel := c.Service.DeleteByID(id) + if wasDel { + // return the deleted movie's ID + return iris.Map{"deleted": id} + } + // right here we can see that a method function can return any of those two types(map or int), + // we don't have to specify the return type to a specific type. + return iris.StatusBadRequest +} +``` + +```go +// file: web/controllers/hello_controller.go + +package controllers + +import ( + "errors" + + "github.com/kataras/iris/mvc" +) + +// HelloController is our sample controller +// it handles GET: /hello and GET: /hello/{name} +type HelloController struct { + mvc.C +} + +var helloView = mvc.View{ + Name: "hello/index.html", + Data: map[string]interface{}{ + "Title": "Hello Page", + "MyMessage": "Welcome to my awesome website", + }, +} + +// Get will return a predefined view with bind data. +// +// `mvc.Result` is just an interface with a `Dispatch` function. +// `mvc.Response` and `mvc.View` are the built'n result type dispatchers +// you can even create custom response dispatchers by +// implementing the `github.com/kataras/iris/mvc#Result` interface. +func (c *HelloController) Get() mvc.Result { + return helloView +} + +// you can define a standard error in order to be re-usable anywhere in your app. +var errBadName = errors.New("bad name") + +// you can just return it as error or even better +// wrap this error with an mvc.Response to make it an mvc.Result compatible type. +var badName = mvc.Response{Err: errBadName, Code: 400} + +// GetBy returns a "Hello {name}" response. +// Demos: +// curl -i http://localhost:8080/hello/iris +// curl -i http://localhost:8080/hello/anything +func (c *HelloController) GetBy(name string) mvc.Result { + if name != "iris" { + return badName + // or + // GetBy(name string) (mvc.Result, error) { + // return nil, errBadName + // } + } + + // return mvc.Response{Text: "Hello " + name} OR: + return mvc.View{ + Name: "hello/name.html", + Data: name, + } +} +``` + +```go +// file: web/middleware/basicauth.go + +package middleware + +import "github.com/kataras/iris/middleware/basicauth" + +// BasicAuth middleware sample. +var BasicAuth = basicauth.New(basicauth.Config{ + Users: map[string]string{ + "admin": "password", + }, +}) +``` + +```html + + + + + {{.Title}} - My App + + + +

{{.MyMessage}}

+ + + +``` + +```html + + + + + {{.}}' Portfolio - My App + + + +

Hello {{.}}

+ + + +``` + +> Navigate to the [_examples/view](https://github.com/kataras/iris/tree/master/_examples/#view) for more examples +like shared layouts, tmpl funcs, reverse routing and more! + +#### Main + +This file creates any necessary component and links them together. + +```go +// file: main.go + +package main + +import ( + "github.com/kataras/iris/_examples/mvc/overview/datasource" + "github.com/kataras/iris/_examples/mvc/overview/repositories" + "github.com/kataras/iris/_examples/mvc/overview/services" + "github.com/kataras/iris/_examples/mvc/overview/web/controllers" + "github.com/kataras/iris/_examples/mvc/overview/web/middleware" + + "github.com/kataras/iris" +) + +func main() { + app := iris.New() + + // Load the template files. + app.RegisterView(iris.HTML("./web/views", ".html")) + + // Register our controllers. + app.Controller("/hello", new(controllers.HelloController)) + + // Create our movie repository with some (memory) data from the datasource. + repo := repositories.NewMovieRepository(datasource.Movies) + // Create our movie service, we will bind it to the movie controller. + movieService := services.NewMovieService(repo) + + app.Controller("/movies", new(controllers.MovieController), + // Bind the "movieService" to the MovieController's Service (interface) field. + movieService, + // Add the basic authentication(admin:password) middleware + // for the /movies based requests. + middleware.BasicAuth) + + // Start the web server at localhost:8080 + // http://localhost:8080/hello + // http://localhost:8080/hello/iris + // http://localhost:8080/movies + // http://localhost:8080/movies/1 + app.Run( + iris.Addr("localhost:8080"), + iris.WithoutVersionChecker, + iris.WithoutServerError(iris.ErrServerClosed), + iris.WithOptimizations, // enables faster json serialization and more + ) +} +``` + +More folder structure guidelines can be found at the [_examples/#structuring](https://github.com/kataras/iris/tree/master/_examples/#structuring) section. \ No newline at end of file diff --git a/_examples/routing/README.md b/_examples/routing/README.md new file mode 100644 index 00000000..f57c4849 --- /dev/null +++ b/_examples/routing/README.md @@ -0,0 +1,1141 @@ +# Routing + +## Handlers + +A Handler, as the name implies, handle requests. + +```go +// A Handler responds to an HTTP request. +// It writes reply headers and data to the +// Context.ResponseWriter() and then return. +// Returning signals that the request is finished; +// it is not valid to use the Context after or +// concurrently with the completion of the Handler call. +// +// Depending on the HTTP client software, HTTP protocol version, +// and any intermediaries between the client and the iris server, +// it may not be possible to read from the +// Context.Request().Body after writing to the context.ResponseWriter(). +// Cautious handlers should read the Context.Request().Body first, and then reply. +// +// Except for reading the body, handlers should not modify the provided Context. +// +// If Handler panics, the server (the caller of Handler) assumes that +// the effect of the panic was isolated to the active request. +// It recovers the panic, logs a stack trace to the server error log +// and hangs up the connection. +type Handler func(Context) +``` + +Once the handler is registered, we can use the returned [`Route`](https://godoc.org/github.com/kataras/iris/core/router#Route) instance to give a name to the handler registration for easier lookup in code or in templates. + +For more information, checkout the [Routing and reverse lookups](routing_reverse.md) section. + +## API + +All HTTP methods are supported, developers can also register handlers for same paths for different methods. + +The first parameter is the HTTP Method, +second parameter is the request path of the route, +third variadic parameter should contains one or more iris.Handler executed +by the registered order when a user requests for that specific resouce path from the server. + +Example code: + +```go +app := iris.New() + +app.Handle("GET", "/contact", func(ctx iris.Context) { + ctx.HTML("

Hello from /contact

") +}) +``` + +In order to make things easier for the end-developer, iris provides functions for all HTTP Methods. +The first parameter is the request path of the route, +second variadic parameter should contains one or more iris.Handler executed +by the registered order when a user requests for that specific resouce path from the server. + +Example code: + +```go +app := iris.New() + +// Method: "GET" +app.Get("/", handler) + +// Method: "POST" +app.Post("/", handler) + +// Method: "PUT" +app.Put("/", handler) + +// Method: "DELETE" +app.Delete("/", handler) + +// Method: "OPTIONS" +app.Options("/", handler) + +// Method: "TRACE" +app.Trace("/", handler) + +// Method: "CONNECT" +app.Connect("/", handler) + +// Method: "HEAD" +app.Head("/", handler) + +// Method: "PATCH" +app.Patch("/", handler) + +// register the route for all HTTP Methods +app.Any("/", handler) + +func handler(ctx iris.Context){ + ctx.Writef("Hello from method: %s and path: %s", ctx.Method(), ctx.Path()) +} +``` + +## Grouping Routes + +A set of routes that are being groupped by path prefix can (optionally) share the same middleware handlers and template layout. +A group can have a nested group too. + +`.Party` is being used to group routes, developers can declare an unlimited number of (nested) groups. + +Example code: + +```go +app := iris.New() + +users := app.Party("/users", myAuthMiddlewareHandler) + +// http://localhost:8080/users/42/profile +users.Get("/{id:int}/profile", userProfileHandler) +// http://localhost:8080/users/messages/1 +users.Get("/inbox/{id:int}", userMessageHandler) +``` + +The same could be also written using a function which accepts the child router(the Party). + +```go +app := iris.New() + +app.PartyFunc("/users", func(users iris.Party) { + users.Use(myAuthMiddlewareHandler) + + // http://localhost:8080/users/42/profile + users.Get("/{id:int}/profile", userProfileHandler) + // http://localhost:8080/users/messages/1 + users.Get("/inbox/{id:int}", userMessageHandler) +}) +``` + +> `id:int` is a (typed) dynamic path parameter, learn more by scrolling down. + +# Dynamic Path Parameters + +Iris has the easiest and the most powerful routing process you have ever meet. + +At the same time, +Iris has its own interpeter(yes like a programming language) +for route's path syntax and their dynamic path parameters parsing and evaluation. +We call them "macros" for shortcut. + +How? It calculates its needs and if not any special regexp needed then it just +registers the route with the low-level path syntax, +otherwise it pre-compiles the regexp and adds the necessary middleware(s). That means that you have zero performance cost compared to other routers or web frameworks. + +Standard macro types for route path parameters + +```sh ++------------------------+ +| {param:string} | ++------------------------+ +string type +anything + ++------------------------+ +| {param:int} | ++------------------------+ +int type +only numbers (0-9) + ++------------------------+ +| {param:long} | ++------------------------+ +int64 type +only numbers (0-9) + ++------------------------+ +| {param:boolean} | ++------------------------+ +bool type +only "1" or "t" or "T" or "TRUE" or "true" or "True" +or "0" or "f" or "F" or "FALSE" or "false" or "False" + ++------------------------+ +| {param:alphabetical} | ++------------------------+ +alphabetical/letter type +letters only (upper or lowercase) + ++------------------------+ +| {param:file} | ++------------------------+ +file type +letters (upper or lowercase) +numbers (0-9) +underscore (_) +dash (-) +point (.) +no spaces ! or other character + ++------------------------+ +| {param:path} | ++------------------------+ +path type +anything, should be the last part, more than one path segment, +i.e: /path1/path2/path3 , ctx.Params().Get("param") == "/path1/path2/path3" +``` + +If type is missing then parameter's type is defaulted to string, so +`{param} == {param:string}`. + +If a function not found on that type then the `string` macro type's functions are being used. + +Besides the fact that iris provides the basic types and some default "macro funcs" +you are able to register your own too!. + +Register a named path parameter function + +```go +app.Macros().Int.RegisterFunc("min", func(argument int) func(paramValue string) bool { + // [...] + return true + // -> true means valid, false means invalid fire 404 or if "else 500" is appended to the macro syntax then internal server error. +}) +``` + +At the `func(argument ...)` you can have any standard type, it will be validated before the server starts so don't care about any performance cost there, the only thing it runs at serve time is the returning `func(paramValue string) bool`. + +```go +{param:string equal(iris)} , "iris" will be the argument here: +app.Macros().String.RegisterFunc("equal", func(argument string) func(paramValue string) bool { + return func(paramValue string){ return argument == paramValue } +}) +``` + +Example Code: + +```go +app := iris.New() +// you can use the "string" type which is valid for a single path parameter that can be anything. +app.Get("/username/{name}", func(ctx iris.Context) { + ctx.Writef("Hello %s", ctx.Params().Get("name")) +}) // type is missing = {name:string} + +// Let's register our first macro attached to int macro type. +// "min" = the function +// "minValue" = the argument of the function +// func(string) bool = the macro's path parameter evaluator, this executes in serve time when +// a user requests a path which contains the :int macro type with the min(...) macro parameter function. +app.Macros().Int.RegisterFunc("min", func(minValue int) func(string) bool { + // do anything before serve here [...] + // at this case we don't need to do anything + return func(paramValue string) bool { + n, err := strconv.Atoi(paramValue) + if err != nil { + return false + } + return n >= minValue + } +}) + +// http://localhost:8080/profile/id>=1 +// this will throw 404 even if it's found as route on : /profile/0, /profile/blabla, /profile/-1 +// macro parameter functions are optional of course. +app.Get("/profile/{id:int min(1)}", func(ctx iris.Context) { + // second parameter is the error but it will always nil because we use macros, + // the validaton already happened. + id, _ := ctx.Params().GetInt("id") + ctx.Writef("Hello id: %d", id) +}) + +// to change the error code per route's macro evaluator: +app.Get("/profile/{id:int min(1)}/friends/{friendid:int min(1) else 504}", func(ctx iris.Context) { + id, _ := ctx.Params().GetInt("id") + friendid, _ := ctx.Params().GetInt("friendid") + ctx.Writef("Hello id: %d looking for friend id: ", id, friendid) +}) // this will throw e 504 error code instead of 404 if all route's macros not passed. + +// http://localhost:8080/game/a-zA-Z/level/0-9 +// remember, alphabetical is lowercase or uppercase letters only. +app.Get("/game/{name:alphabetical}/level/{level:int}", func(ctx iris.Context) { + ctx.Writef("name: %s | level: %s", ctx.Params().Get("name"), ctx.Params().Get("level")) +}) + +// let's use a trivial custom regexp that validates a single path parameter +// which its value is only lowercase letters. + +// http://localhost:8080/lowercase/anylowercase +app.Get("/lowercase/{name:string regexp(^[a-z]+)}", func(ctx iris.Context) { + ctx.Writef("name should be only lowercase, otherwise this handler will never executed: %s", ctx.Params().Get("name")) +}) + +// http://localhost:8080/single_file/app.js +app.Get("/single_file/{myfile:file}", func(ctx iris.Context) { + ctx.Writef("file type validates if the parameter value has a form of a file name, got: %s", ctx.Params().Get("myfile")) +}) + +// http://localhost:8080/myfiles/any/directory/here/ +// this is the only macro type that accepts any number of path segments. +app.Get("/myfiles/{directory:path}", func(ctx iris.Context) { + ctx.Writef("path type accepts any number of path segments, path after /myfiles/ is: %s", ctx.Params().Get("directory")) +}) + +app.Run(iris.Addr(":8080")) +} +``` + +A **path parameter name should contain only alphabetical letters. Symbols like '_' and numbers are NOT allowed**. + +Last, do not confuse `ctx.Params()` with `ctx.Values()`. +Path parameter's values goes to `ctx.Params()` and context's local storage +that can be used to communicate between handlers and middleware(s) goes to +`ctx.Values()`. + +# Routing and reverse lookups + +As mentioned in the [Handlers](handlers.md) chapter, Iris provides several handler registration methods, each of which returns a [`Route`](https://godoc.org/github.com/kataras/iris/core/router#Route) instance. + +## Route naming + +Route naming is easy, since we just call the returned `*Route` with a `Name` field to define a name: + +```go +package main + +import ( + "github.com/kataras/iris" +) + +func main() { + app := iris.New() + // define a function + h := func(ctx iris.Context) { + ctx.HTML("Hi") + } + + // handler registration and naming + home := app.Get("/", h) + home.Name = "home" + // or + app.Get("/about", h).Name = "about" + app.Get("/page/{id}", h).Name = "page" + + app.Run(iris.Addr(":8080")) +} +``` + +## Route reversing AKA generating URLs from the route name + +When we register the handlers for a specific path, we get the ability to create URLs based on the structured data we pass to Iris. In the example above, we've named three routers, one of which even takes parameters. If we're using the default `html/template` view engine, we can use a simple action to reverse the routes (and generae actual URLs): + +```sh +Home: {{ urlpath "home" }} +About: {{ urlpath "about" }} +Page 17: {{ urlpath "page" "17" }} +``` + +Above code would generate the following output: + +```sh +Home: http://localhost:8080/ +About: http://localhost:8080/about +Page 17: http://localhost:8080/page/17 +``` + +## Using route names in code + +We can use the following methods/functions to work with named routes (and their parameters): + +* [`GetRoutes`](https://godoc.org/github.com/kataras/iris/core/router#APIBuilder.GetRoutes) function to get all registered routes +* [`GetRoute(routeName string)`](https://godoc.org/github.com/kataras/iris/core/router#APIBuilder.GetRoute) method to retrieve a route by name +* [`URL(routeName string, paramValues ...interface{})`](https://godoc.org/github.com/kataras/iris/core/router#RoutePathReverser.URL) method to generate url string based on supplied parameters +* [`Path(routeName string, paramValues ...interface{}`](https://godoc.org/github.com/kataras/iris/core/router#RoutePathReverser.Path) method to generate just the path (without host and protocol) portion of the URL based on provided values + +## Examples + +Check out the [https://github.com/kataras/iris/tree/master/_examples/view/template_html_4](https://github.com/kataras/iris/tree/master/_examples/view/template_html_4) example for more details. + +# Middleware + +When we talk about Middleware in Iris we're talking about running code before and/or after our main handler code in a HTTP request lifecycle. For example, logging middleware might write the incoming request details to a log, then call the handler code, before writing details about the response to the log. One of the cool things about middleware is that these units are extremely flexible and reusable. + +A middleware is just a **Handler** form of `func(ctx iris.Context)`, the middleware is being executed when the previous middleware calls the `ctx.Next()`, this can be used for authentication, i.e: if logged in then `ctx.Next()` otherwise fire an error response. + +## Writing a middleware + +```go +package main + +import "github.com/kataras/iris" + +func main() { + app := iris.New() + app.Get("/", before, mainHandler, after) + app.Run(iris.Addr(":8080")) +} + +func before(ctx iris.Context) { + shareInformation := "this is a sharable information between handlers" + + requestPath := ctx.Path() + println("Before the mainHandler: " + requestPath) + + ctx.Values().Set("info", shareInformation) + ctx.Next() // execute the next handler, in this case the main one. +} + +func after(ctx iris.Context) { + println("After the mainHandler") +} + +func mainHandler(ctx iris.Context) { + println("Inside mainHandler") + + // take the info from the "before" handler. + info := ctx.Values().GetString("info") + + // write something to the client as a response. + ctx.HTML("

Response

") + ctx.HTML("
Info: " + info) + + ctx.Next() // execute the "after". +} +``` + +```bash +$ go run main.go # and navigate to the http://localhost:8080 +Now listening on: http://localhost:8080 +Application started. Press CTRL+C to shut down. +Before the mainHandler: / +Inside mainHandler +After the mainHandler +``` + +### Globally + +```go +package main + +import "github.com/kataras/iris" + +func main() { + app := iris.New() + // register the "before" handler as the first handler which will be executed + // on all domain's routes. + // or use the `UseGlobal` to register a middleware which will fire across subdomains. + app.Use(before) + // register the "after" handler as the last handler which will be executed + // after all domain's routes' handler(s). + app.Done(after) + + // register our routes. + app.Get("/", indexHandler) + app.Get("/contact", contactHandler) + + app.Run(iris.Addr(":8080")) +} + +func before(ctx iris.Context) { + // [...] +} + +func after(ctx iris.Context) { + // [...] +} + +func indexHandler(ctx iris.Context) { + // write something to the client as a response. + ctx.HTML("

Index

") + + ctx.Next() // execute the "after" handler registered via `Done`. +} + +func contactHandler(ctx iris.Context) { + // write something to the client as a response. + ctx.HTML("

Contact

") + + ctx.Next() // execute the "after" handler registered via `Done`. +} +``` + +## [Explore](https://github.com/kataras/iris/tree/master/middleware) + +# Wrapping the Router + +**Very rare**, you may never need that but it's here in any case you need it. + +There are times you need to override or decide whether the Router will be executed on an incoming request. If you've any previous experience with the `net/http` and other web frameworks this function will be familiar with you (it has the form of a net/http middleware, but instead of accepting the next handler it accepts the Router as a function to be executed or not). + +```go +// WrapperFunc is used as an expected input parameter signature +// for the WrapRouter. It's a "low-level" signature which is compatible +// with the net/http. +// It's being used to run or no run the router based on a custom logic. +type WrapperFunc func(w http.ResponseWriter, r *http.Request, firstNextIsTheRouter http.HandlerFunc) + +// WrapRouter adds a wrapper on the top of the main router. +// Usually it's useful for third-party middleware +// when need to wrap the entire application with a middleware like CORS. +// +// Developers can add more than one wrappers, +// those wrappers' execution comes from last to first. +// That means that the second wrapper will wrap the first, and so on. +// +// Before build. +func WrapRouter(wrapperFunc WrapperFunc) +``` + +Iris' router searches for its routes based on the `HTTP Method` a Router Wrapper can override that behavior and execute custom code. + +Example Code: + +```go +package main + +import ( + "net/http" + "strings" + + "github.com/kataras/iris" +) + +// In this example you'll just see one use case of .WrapRouter. +// You can use the .WrapRouter to add custom logic when or when not the router should +// be executed in order to execute the registered routes' handlers. +// +// To see how you can serve files on root "/" without a custom wrapper +// just navigate to the "file-server/single-page-application" example. +// +// This is just for the proof of concept, you can skip this tutorial if it's too much for you. + + +func main() { + app := iris.New() + + app.OnErrorCode(iris.StatusNotFound, func(ctx iris.Context) { + ctx.HTML("Resource Not found") + }) + + app.Get("/", func(ctx iris.Context) { + ctx.ServeFile("./public/index.html", false) + }) + + app.Get("/profile/{username}", func(ctx iris.Context) { + ctx.Writef("Hello %s", ctx.Params().Get("username")) + }) + + // serve files from the root "/", if we used .StaticWeb it could override + // all the routes because of the underline need of wildcard. + // Here we will see how you can by-pass this behavior + // by creating a new file server handler and + // setting up a wrapper for the router(like a "low-level" middleware) + // in order to manually check if we want to process with the router as normally + // or execute the file server handler instead. + + // use of the .StaticHandler + // which is the same as StaticWeb but it doesn't + // registers the route, it just returns the handler. + fileServer := app.StaticHandler("./public", false, false) + + // wrap the router with a native net/http handler. + // if url does not contain any "." (i.e: .css, .js...) + // (depends on the app , you may need to add more file-server exceptions), + // then the handler will execute the router that is responsible for the + // registered routes (look "/" and "/profile/{username}") + // if not then it will serve the files based on the root "/" path. + app.WrapRouter(func(w http.ResponseWriter, r *http.Request, router http.HandlerFunc) { + path := r.URL.Path + // Note that if path has suffix of "index.html" it will auto-permant redirect to the "/", + // so our first handler will be executed instead. + + if !strings.Contains(path, ".") { + // if it's not a resource then continue to the router as normally. <-- IMPORTANT + router(w, r) + return + } + // acquire and release a context in order to use it to execute + // our file server + // remember: we use net/http.Handler because here we are in the "low-level", before the router itself. + ctx := app.ContextPool.Acquire(w, r) + fileServer(ctx) + app.ContextPool.Release(ctx) + }) + + // http://localhost:8080 + // http://localhost:8080/index.html + // http://localhost:8080/app.js + // http://localhost:8080/css/main.css + // http://localhost:8080/profile/anyusername + app.Run(iris.Addr(":8080")) + + // Note: In this example we just saw one use case, + // you may want to .WrapRouter or .Downgrade in order to bypass the iris' default router, i.e: + // you can use that method to setup custom proxies too. + // + // If you just want to serve static files on other path than root + // you can just use the StaticWeb, i.e: + // .StaticWeb("/static", "./public") + // ________________________________requestPath, systemPath +} +``` + +There is not much to say here, it's just a function wrapper which accepts the native response writer and request and the next handler which is the Iris' Router itself, it's being or not executed whether is called or not, **it's a middleware for the whole Router**. + +# Error handlers + +You can define your own handlers when a specific http error code occurs. + +> Error codes are the http status codes that are bigger or equal to 400, like 404 not found and 500 internal server. + +Example code: + +```go +package main + +import "github.com/kataras/iris" + +func main(){ + app := iris.New() + app.OnErrorCode(iris.StatusNotFound, notFound) + app.OnErrorCode(iris.StatusInternalServerError, internalServerError) + // to register a handler for all status codes >=400: + // app.OnAnyErrorCode(handler) + app.Get("/", index) + app.Run(iris.Addr(":8080")) +} + +func notFound(ctx iris.Context) { + // when 404 then render the template $views_dir/errors/404.html + ctx.View("errors/404.html") +} + +func internalServerError(ctx iris.Context) { + ctx.WriteString("Oups something went wrong, try again") +} + +func index(ctx context.Context) { + ctx.View("index.html") +} +``` + +# Context Outline + +The `iris.Context` source code can be found [here](https://github.com/kataras/iris/blob/master/context/context.go). Keep note using an IDE/Editors with `auto-complete` feature will help you a lot. + +```go +// Context is the midle-man server's "object" for the clients. +// +// A New context is being acquired from a sync.Pool on each connection. +// The Context is the most important thing on the iris's http flow. +// +// Developers send responses to the client's request through a Context. +// Developers get request information from the client's request a Context. +// +// This context is an implementation of the context.Context sub-package. +// context.Context is very extensible and developers can override +// its methods if that is actually needed. +type Context interface { + // ResponseWriter returns an http.ResponseWriter compatible response writer, as expected. + ResponseWriter() ResponseWriter + // ResetResponseWriter should change or upgrade the Context's ResponseWriter. + ResetResponseWriter(ResponseWriter) + + // Request returns the original *http.Request, as expected. + Request() *http.Request + + // SetCurrentRouteName sets the route's name internally, + // in order to be able to find the correct current "read-only" Route when + // end-developer calls the `GetCurrentRoute()` function. + // It's being initialized by the Router, if you change that name + // manually nothing really happens except that you'll get other + // route via `GetCurrentRoute()`. + // Instead, to execute a different path + // from this context you should use the `Exec` function + // or change the handlers via `SetHandlers/AddHandler` functions. + SetCurrentRouteName(currentRouteName string) + // GetCurrentRoute returns the current registered "read-only" route that + // was being registered to this request's path. + GetCurrentRoute() RouteReadOnly + + // AddHandler can add handler(s) + // to the current request in serve-time, + // these handlers are not persistenced to the router. + // + // Router is calling this function to add the route's handler. + // If AddHandler called then the handlers will be inserted + // to the end of the already-defined route's handler. + // + AddHandler(...Handler) + // SetHandlers replaces all handlers with the new. + SetHandlers(Handlers) + // Handlers keeps tracking of the current handlers. + Handlers() Handlers + + // HandlerIndex sets the current index of the + // current context's handlers chain. + // If -1 passed then it just returns the + // current handler index without change the current index.rns that index, useless return value. + // + // Look Handlers(), Next() and StopExecution() too. + HandlerIndex(n int) (currentIndex int) + // HandlerName returns the current handler's name, helpful for debugging. + HandlerName() string + // Next calls all the next handler from the handlers chain, + // it should be used inside a middleware. + // + // Note: Custom context should override this method in order to be able to pass its own context.Context implementation. + Next() + // NextHandler returns(but it is NOT executes) the next handler from the handlers chain. + // + // Use .Skip() to skip this handler if needed to execute the next of this returning handler. + NextHandler() Handler + // Skip skips/ignores the next handler from the handlers chain, + // it should be used inside a middleware. + Skip() + // StopExecution if called then the following .Next calls are ignored. + StopExecution() + // IsStopped checks and returns true if the current position of the Context is 255, + // means that the StopExecution() was called. + IsStopped() bool + + // +------------------------------------------------------------+ + // | Current "user/request" storage | + // | and share information between the handlers - Values(). | + // | Save and get named path parameters - Params() | + // +------------------------------------------------------------+ + + // Params returns the current url's named parameters key-value storage. + // Named path parameters are being saved here. + // This storage, as the whole Context, is per-request lifetime. + Params() *RequestParams + + // Values returns the current "user" storage. + // Named path parameters and any optional data can be saved here. + // This storage, as the whole Context, is per-request lifetime. + // + // You can use this function to Set and Get local values + // that can be used to share information between handlers and middleware. + Values() *memstore.Store + // Translate is the i18n (localization) middleware's function, + // it calls the Get("translate") to return the translated value. + // + // Example: https://github.com/kataras/iris/tree/master/_examples/miscellaneous/i18n + Translate(format string, args ...interface{}) string + + // +------------------------------------------------------------+ + // | Path, Host, Subdomain, IP, Headers etc... | + // +------------------------------------------------------------+ + + // Method returns the request.Method, the client's http method to the server. + Method() string + // Path returns the full request path, + // escaped if EnablePathEscape config field is true. + Path() string + // RequestPath returns the full request path, + // based on the 'escape'. + RequestPath(escape bool) string + + // Host returns the host part of the current url. + Host() string + // Subdomain returns the subdomain of this request, if any. + // Note that this is a fast method which does not cover all cases. + Subdomain() (subdomain string) + // RemoteAddr tries to parse and return the real client's request IP. + // + // Based on allowed headers names that can be modified from Configuration.RemoteAddrHeaders. + // + // If parse based on these headers fail then it will return the Request's `RemoteAddr` field + // which is filled by the server before the HTTP handler. + // + // Look `Configuration.RemoteAddrHeaders`, + // `Configuration.WithRemoteAddrHeader(...)`, + // `Configuration.WithoutRemoteAddrHeader(...)` for more. + RemoteAddr() string + // GetHeader returns the request header's value based on its name. + GetHeader(name string) string + // IsAjax returns true if this request is an 'ajax request'( XMLHttpRequest) + // + // There is no a 100% way of knowing that a request was made via Ajax. + // You should never trust data coming from the client, they can be easily overcome by spoofing. + // + // Note that "X-Requested-With" Header can be modified by any client(because of "X-"), + // so don't rely on IsAjax for really serious stuff, + // try to find another way of detecting the type(i.e, content type), + // there are many blogs that describe these problems and provide different kind of solutions, + // it's always depending on the application you're building, + // this is the reason why this `IsAjax`` is simple enough for general purpose use. + // + // Read more at: https://developer.mozilla.org/en-US/docs/AJAX + // and https://xhr.spec.whatwg.org/ + IsAjax() bool + + // +------------------------------------------------------------+ + // | Response Headers helpers | + // +------------------------------------------------------------+ + + // Header adds a header to the response writer. + Header(name string, value string) + + // ContentType sets the response writer's header key "Content-Type" to the 'cType'. + ContentType(cType string) + // GetContentType returns the response writer's header value of "Content-Type" + // which may, setted before with the 'ContentType'. + GetContentType() string + + // StatusCode sets the status code header to the response. + // Look .GetStatusCode too. + StatusCode(statusCode int) + // GetStatusCode returns the current status code of the response. + // Look StatusCode too. + GetStatusCode() int + + // Redirect redirect sends a redirect response the client + // accepts 2 parameters string and an optional int + // first parameter is the url to redirect + // second parameter is the http status should send, default is 302 (StatusFound), + // you can set it to 301 (Permant redirect), if that's nessecery + Redirect(urlToRedirect string, statusHeader ...int) + + // +------------------------------------------------------------+ + // | Various Request and Post Data | + // +------------------------------------------------------------+ + + // URLParam returns the get parameter from a request , if any. + URLParam(name string) string + // URLParamInt returns the url query parameter as int value from a request, + // returns an error if parse failed. + URLParamInt(name string) (int, error) + // URLParamInt64 returns the url query parameter as int64 value from a request, + // returns an error if parse failed. + URLParamInt64(name string) (int64, error) + // URLParams returns a map of GET query parameters separated by comma if more than one + // it returns an empty map if nothing found. + URLParams() map[string]string + + // FormValue returns a single form value by its name/key + FormValue(name string) string + // FormValues returns all post data values with their keys + // form data, get, post & put query arguments + // + // NOTE: A check for nil is necessary. + FormValues() map[string][]string + // PostValue returns a form's only-post value by its name, + // same as Request.PostFormValue. + PostValue(name string) string + // FormFile returns the first file for the provided form key. + // FormFile calls ctx.Request.ParseMultipartForm and ParseForm if necessary. + // + // same as Request.FormFile. + FormFile(key string) (multipart.File, *multipart.FileHeader, error) + + // +------------------------------------------------------------+ + // | Custom HTTP Errors | + // +------------------------------------------------------------+ + + // NotFound emits an error 404 to the client, using the specific custom error error handler. + // Note that you may need to call ctx.StopExecution() if you don't want the next handlers + // to be executed. Next handlers are being executed on iris because you can alt the + // error code and change it to a more specific one, i.e + // users := app.Party("/users") + // users.Done(func(ctx context.Context){ if ctx.StatusCode() == 400 { /* custom error code for /users */ }}) + NotFound() + + // +------------------------------------------------------------+ + // | Body Readers | + // +------------------------------------------------------------+ + + // SetMaxRequestBodySize sets a limit to the request body size + // should be called before reading the request body from the client. + SetMaxRequestBodySize(limitOverBytes int64) + + // UnmarshalBody reads the request's body and binds it to a value or pointer of any type + // Examples of usage: context.ReadJSON, context.ReadXML. + UnmarshalBody(v interface{}, unmarshaler Unmarshaler) error + // ReadJSON reads JSON from request's body and binds it to a value of any json-valid type. + ReadJSON(jsonObject interface{}) error + // ReadXML reads XML from request's body and binds it to a value of any xml-valid type. + ReadXML(xmlObject interface{}) error + // ReadForm binds the formObject with the form data + // it supports any kind of struct. + ReadForm(formObject interface{}) error + + // +------------------------------------------------------------+ + // | Body (raw) Writers | + // +------------------------------------------------------------+ + + // Write writes the data to the connection as part of an HTTP reply. + // + // If WriteHeader has not yet been called, Write calls + // WriteHeader(http.StatusOK) before writing the data. If the Header + // does not contain a Content-Type line, Write adds a Content-Type set + // to the result of passing the initial 512 bytes of written data to + // DetectContentType. + // + // Depending on the HTTP protocol version and the client, calling + // Write or WriteHeader may prevent future reads on the + // Request.Body. For HTTP/1.x requests, handlers should read any + // needed request body data before writing the response. Once the + // headers have been flushed (due to either an explicit Flusher.Flush + // call or writing enough data to trigger a flush), the request body + // may be unavailable. For HTTP/2 requests, the Go HTTP server permits + // handlers to continue to read the request body while concurrently + // writing the response. However, such behavior may not be supported + // by all HTTP/2 clients. Handlers should read before writing if + // possible to maximize compatibility. + Write(body []byte) (int, error) + // Writef formats according to a format specifier and writes to the response. + // + // Returns the number of bytes written and any write error encountered. + Writef(format string, args ...interface{}) (int, error) + // WriteString writes a simple string to the response. + // + // Returns the number of bytes written and any write error encountered. + WriteString(body string) (int, error) + // WriteWithExpiration like Write but it sends with an expiration datetime + // which is refreshed every package-level `StaticCacheDuration` field. + WriteWithExpiration(body []byte, modtime time.Time) (int, error) + // StreamWriter registers the given stream writer for populating + // response body. + // + // Access to context's and/or its' members is forbidden from writer. + // + // This function may be used in the following cases: + // + // * if response body is too big (more than iris.LimitRequestBodySize(if setted)). + // * if response body is streamed from slow external sources. + // * if response body must be streamed to the client in chunks. + // (aka `http server push`). + // + // receives a function which receives the response writer + // and returns false when it should stop writing, otherwise true in order to continue + StreamWriter(writer func(w io.Writer) bool) + + // +------------------------------------------------------------+ + // | Body Writers with compression | + // +------------------------------------------------------------+ + // ClientSupportsGzip retruns true if the client supports gzip compression. + ClientSupportsGzip() bool + // WriteGzip accepts bytes, which are compressed to gzip format and sent to the client. + // returns the number of bytes written and an error ( if the client doesn' supports gzip compression) + // + // This function writes temporary gzip contents, the ResponseWriter is untouched. + WriteGzip(b []byte) (int, error) + // TryWriteGzip accepts bytes, which are compressed to gzip format and sent to the client. + // If client does not supprots gzip then the contents are written as they are, uncompressed. + // + // This function writes temporary gzip contents, the ResponseWriter is untouched. + TryWriteGzip(b []byte) (int, error) + // GzipResponseWriter converts the current response writer into a response writer + // which when its .Write called it compress the data to gzip and writes them to the client. + // + // Can be also disabled with its .Disable and .ResetBody to rollback to the usual response writer. + GzipResponseWriter() *GzipResponseWriter + // Gzip enables or disables (if enabled before) the gzip response writer,if the client + // supports gzip compression, so the following response data will + // be sent as compressed gzip data to the client. + Gzip(enable bool) + + // +------------------------------------------------------------+ + // | Rich Body Content Writers/Renderers | + // +------------------------------------------------------------+ + + // ViewLayout sets the "layout" option if and when .View + // is being called afterwards, in the same request. + // Useful when need to set or/and change a layout based on the previous handlers in the chain. + // + // Note that the 'layoutTmplFile' argument can be setted to iris.NoLayout || view.NoLayout + // to disable the layout for a specific view render action, + // it disables the engine's configuration's layout property. + // + // Look .ViewData and .View too. + // + // Example: https://github.com/kataras/iris/tree/master/_examples/view/context-view-data/ + ViewLayout(layoutTmplFile string) + + // ViewData saves one or more key-value pair in order to be passed if and when .View + // is being called afterwards, in the same request. + // Useful when need to set or/and change template data from previous hanadlers in the chain. + // + // If .View's "binding" argument is not nil and it's not a type of map + // then these data are being ignored, binding has the priority, so the main route's handler can still decide. + // If binding is a map or context.Map then these data are being added to the view data + // and passed to the template. + // + // After .View, the data are not destroyed, in order to be re-used if needed (again, in the same request as everything else), + // to clear the view data, developers can call: + // ctx.Set(ctx.Application().ConfigurationReadOnly().GetViewDataContextKey(), nil) + // + // If 'key' is empty then the value is added as it's (struct or map) and developer is unable to add other value. + // + // Look .ViewLayout and .View too. + // + // Example: https://github.com/kataras/iris/tree/master/_examples/view/context-view-data/ + ViewData(key string, value interface{}) + + // GetViewData returns the values registered by `context#ViewData`. + // The return value is `map[string]interface{}`, this means that + // if a custom struct registered to ViewData then this function + // will try to parse it to map, if failed then the return value is nil + // A check for nil is always a good practise if different + // kind of values or no data are registered via `ViewData`. + // + // Similarly to `viewData := ctx.Values().Get("iris.viewData")` or + // `viewData := ctx.Values().Get(ctx.Application().ConfigurationReadOnly().GetViewDataContextKey())`. + GetViewData() map[string]interface{} + + // View renders templates based on the adapted view engines. + // First argument accepts the filename, relative to the view engine's Directory, + // i.e: if directory is "./templates" and want to render the "./templates/users/index.html" + // then you pass the "users/index.html" as the filename argument. + // + // Look: .ViewData and .ViewLayout too. + // + // Examples: https://github.com/kataras/iris/tree/master/_examples/view/ + View(filename string) error + + // Binary writes out the raw bytes as binary data. + Binary(data []byte) (int, error) + // Text writes out a string as plain text. + Text(text string) (int, error) + // HTML writes out a string as text/html. + HTML(htmlContents string) (int, error) + // JSON marshals the given interface object and writes the JSON response. + JSON(v interface{}, options ...JSON) (int, error) + // JSONP marshals the given interface object and writes the JSON response. + JSONP(v interface{}, options ...JSONP) (int, error) + // XML marshals the given interface object and writes the XML response. + XML(v interface{}, options ...XML) (int, error) + // Markdown parses the markdown to html and renders to client. + Markdown(markdownB []byte, options ...Markdown) (int, error) + + // +------------------------------------------------------------+ + // | Serve files | + // +------------------------------------------------------------+ + + // ServeContent serves content, headers are autoset + // receives three parameters, it's low-level function, instead you can use .ServeFile(string,bool)/SendFile(string,string) + // + // You can define your own "Content-Type" header also, after this function call + // Doesn't implements resuming (by range), use ctx.SendFile instead + ServeContent(content io.ReadSeeker, filename string, modtime time.Time, gzipCompression bool) error + // ServeFile serves a view file, to send a file ( zip for example) to the client you should use the SendFile(serverfilename,clientfilename) + // receives two parameters + // filename/path (string) + // gzipCompression (bool) + // + // You can define your own "Content-Type" header also, after this function call + // This function doesn't implement resuming (by range), use ctx.SendFile instead + // + // Use it when you want to serve css/js/... files to the client, for bigger files and 'force-download' use the SendFile. + ServeFile(filename string, gzipCompression bool) error + // SendFile sends file for force-download to the client + // + // Use this instead of ServeFile to 'force-download' bigger files to the client. + SendFile(filename string, destinationName string) error + + // +------------------------------------------------------------+ + // | Cookies | + // +------------------------------------------------------------+ + + // SetCookie adds a cookie + SetCookie(cookie *http.Cookie) + // SetCookieKV adds a cookie, receives just a name(string) and a value(string) + // + // If you use this method, it expires at 2 hours + // use ctx.SetCookie or http.SetCookie if you want to change more fields. + SetCookieKV(name, value string) + // GetCookie returns cookie's value by it's name + // returns empty string if nothing was found. + GetCookie(name string) string + // RemoveCookie deletes a cookie by it's name. + RemoveCookie(name string) + // VisitAllCookies takes a visitor which loops + // on each (request's) cookies' name and value. + VisitAllCookies(visitor func(name string, value string)) + + // MaxAge returns the "cache-control" request header's value + // seconds as int64 + // if header not found or parse failed then it returns -1. + MaxAge() int64 + + // +------------------------------------------------------------+ + // | Advanced: Response Recorder and Transactions | + // +------------------------------------------------------------+ + + // Record transforms the context's basic and direct responseWriter to a ResponseRecorder + // which can be used to reset the body, reset headers, get the body, + // get & set the status code at any time and more. + Record() + // Recorder returns the context's ResponseRecorder + // if not recording then it starts recording and returns the new context's ResponseRecorder + Recorder() *ResponseRecorder + // IsRecording returns the response recorder and a true value + // when the response writer is recording the status code, body, headers and so on, + // else returns nil and false. + IsRecording() (*ResponseRecorder, bool) + + // BeginTransaction starts a scoped transaction. + // + // You can search third-party articles or books on how Business Transaction works (it's quite simple, especially here). + // + // Note that this is unique and new + // (=I haver never seen any other examples or code in Golang on this subject, so far, as with the most of iris features...) + // it's not covers all paths, + // such as databases, this should be managed by the libraries you use to make your database connection, + // this transaction scope is only for context's response. + // Transactions have their own middleware ecosystem also, look iris.go:UseTransaction. + // + // See https://github.com/kataras/iris/tree/master/_examples/ for more + BeginTransaction(pipe func(t *Transaction)) + // SkipTransactions if called then skip the rest of the transactions + // or all of them if called before the first transaction + SkipTransactions() + // TransactionsSkipped returns true if the transactions skipped or canceled at all. + TransactionsSkipped() bool + + // Exec calls the framewrok's ServeCtx + // based on this context but with a changed method and path + // like it was requested by the user, but it is not. + // + // Offline means that the route is registered to the iris and have all features that a normal route has + // BUT it isn't available by browsing, its handlers executed only when other handler's context call them + // it can validate paths, has sessions, path parameters and all. + // + // You can find the Route by app.GetRoute("theRouteName") + // you can set a route name as: myRoute := app.Get("/mypath", handler)("theRouteName") + // that will set a name to the route and returns its RouteInfo instance for further usage. + // + // It doesn't changes the global state, if a route was "offline" it remains offline. + // + // app.None(...) and app.GetRoutes().Offline(route)/.Online(route, method) + // + // Example: https://github.com/kataras/iris/tree/master/_examples/routing/route-state + // + // User can get the response by simple using rec := ctx.Recorder(); rec.Body()/rec.StatusCode()/rec.Header(). + // + // Context's Values and the Session are kept in order to be able to communicate via the result route. + // + // It's for extreme use cases, 99% of the times will never be useful for you. + Exec(method string, path string) + + // Application returns the iris app instance which belongs to this context. + // Worth to notice that this function returns an interface + // of the Application, which contains methods that are safe + // to be executed at serve-time. The full app's fields + // and methods are not available here for the developer's safety. + Application() Application +} +``` \ No newline at end of file diff --git a/_examples/sessions/README.md b/_examples/sessions/README.md new file mode 100644 index 00000000..10c7c564 --- /dev/null +++ b/_examples/sessions/README.md @@ -0,0 +1,121 @@ +# Sessions + +Iris provides a fast, fully featured and easy to use sessions manager. + +Iris sessions manager lives on its own [kataras/iris/sessions](https://github.com/kataras/iris/tree/master/sessions) package. + +Some trivial examples, + +- [Overview](https://github.com/kataras/iris/blob/master/_examples/sessions/overview/main.go) +- [Standalone](https://github.com/kataras/iris/blob/master/_examples/sessions/standalone/main.go) +- [Secure Cookie](https://github.com/kataras/iris/blob/master/_examples/sessions/securecookie/main.go) +- [Flash Messages](https://github.com/kataras/iris/blob/master/_examples/sessions/flash-messages/main.go) +- [Databases](https://github.com/kataras/iris/tree/master/_examples/sessions/database) + * [BadgerDB](https://github.com/kataras/iris/blob/master/_examples/sessions/database/badger/main.go) **fastest** + * [File](https://github.com/kataras/iris/blob/master/_examples/sessions/database/file/main.go) + * [BoltDB](https://github.com/kataras/iris/blob/master/_examples/sessions/database/boltdb/main.go) + * [LevelDB](https://github.com/kataras/iris/blob/master/_examples/sessions/database/leveldb/main.go) + * [Redis](https://github.com/kataras/iris/blob/master/_examples/sessions/database/redis/main.go) + +## Overview + +```go +import "github.com/kataras/iris/sessions" + +sess := sessions.Start(http.ResponseWriter, *http.Request) +sess. + ID() string + Get(string) interface{} + HasFlash() bool + GetFlash(string) interface{} + GetFlashString(string) string + GetString(key string) string + GetInt(key string) (int, error) + GetInt64(key string) (int64, error) + GetFloat32(key string) (float32, error) + GetFloat64(key string) (float64, error) + GetBoolean(key string) (bool, error) + GetAll() map[string]interface{} + GetFlashes() map[string]interface{} + VisitAll(cb func(k string, v interface{})) + Set(string, interface{}) + SetImmutable(key string, value interface{}) + SetFlash(string, interface{}) + Delete(string) + Clear() + ClearFlashes() +``` + +This example will show how to store data from a session. + +You don't need any third-party library except Iris, but if you want you can use anything, remember Iris is fully compatible with the standard library. You can find a more detailed examples by pressing [here](https://github.com/kataras/iris/tree/master/_examples/sessions). + +In this example we will only allow authenticated users to view our secret message on the `/secret` age. To get access to it, the will first have to visit `/login` to get a valid session cookie, hich logs him in. Additionally he can visit `/logout` to revoke his access to our secret message. + +```go +// sessions.go +package main + +import ( + "github.com/kataras/iris" + + "github.com/kataras/iris/sessions" +) + +var ( + cookieNameForSessionID = "mycookiesessionnameid" + sess = sessions.New(sessions.Config{Cookie: cookieNameForSessionID}) +) + +func secret(ctx iris.Context) { + // Check if user is authenticated + if auth, _ := sess.Start(ctx).GetBoolean("authenticated"); !auth { + ctx.StatusCode(iris.StatusForbidden) + return + } + + // Print secret message + ctx.WriteString("The cake is a lie!") +} + +func login(ctx iris.Context) { + session := sess.Start(ctx) + + // Authentication goes here + // ... + + // Set user as authenticated + session.Set("authenticated", true) +} + +func logout(ctx iris.Context) { + session := sess.Start(ctx) + + // Revoke users authentication + session.Set("authenticated", false) +} + +func main() { + app := iris.New() + + app.Get("/secret", secret) + app.Get("/login", login) + app.Get("/logout", logout) + + app.Run(iris.Addr(":8080")) +} + +``` + +```bash +$ go run sessions.go + +$ curl -s http://localhost:8080/secret +Forbidden + +$ curl -s -I http://localhost:8080/login +Set-Cookie: mysessionid=MTQ4NzE5Mz... + +$ curl -s --cookie "mysessionid=MTQ4NzE5Mz..." http://localhost:8080/secret +The cake is a lie! +``` \ No newline at end of file diff --git a/_examples/websocket/README.md b/_examples/websocket/README.md new file mode 100644 index 00000000..35ca1cbd --- /dev/null +++ b/_examples/websocket/README.md @@ -0,0 +1,132 @@ +# Websocket + +[WebSocket](https://wikipedia.org/wiki/WebSocket) is a protocol that enables two-way persistent communication channels over TCP connections. It is used for applications such as chat, stock tickers, games, anywhere you want real-time functionality in a web application. + +[View or download sample code](https://github.com/kataras/iris/tree/master/_examples/websocket). + +## When to use it + +Use WebSockets when you need to work directly with a socket connection. For example, you might need the best possible performance for a real-time game. + +## How to use it + +* import the `"github.com/kataras/iris/websocket"` +* Configure the websocket package. +* Accept WebSocket requests. +* Send and receive messages. + +### Import the websocket package + +```go +import "github.com/kataras/iris/websocket" +``` + +### Configure the websocket package + +```go +import "github.com/kataras/iris/websocket" + +func main() { + ws := websocket.New(websocket.Config{ + ReadBufferSize: 1024, + WriteBufferSize: 1024, + }) +} +``` + +#### Complete configuration + +```go +// Config the websocket server configuration +// all of these are optional. +type Config struct { + // IDGenerator used to create (and later on, set) + // an ID for each incoming websocket connections (clients). + // The request is an argument which you can use to generate the ID (from headers for example). + // If empty then the ID is generated by DefaultIDGenerator: randomString(64) + IDGenerator func(ctx context.Context) string + + Error func(w http.ResponseWriter, r *http.Request, status int, reason error) + CheckOrigin func(r *http.Request) bool + // HandshakeTimeout specifies the duration for the handshake to complete. + HandshakeTimeout time.Duration + // WriteTimeout time allowed to write a message to the connection. + // 0 means no timeout. + // Default value is 0 + WriteTimeout time.Duration + // ReadTimeout time allowed to read a message from the connection. + // 0 means no timeout. + // Default value is 0 + ReadTimeout time.Duration + // PongTimeout allowed to read the next pong message from the connection. + // Default value is 60 * time.Second + PongTimeout time.Duration + // PingPeriod send ping messages to the connection with this period. Must be less than PongTimeout. + // Default value is 60 *time.Second + PingPeriod time.Duration + // MaxMessageSize max message size allowed from connection. + // Default value is 1024 + MaxMessageSize int64 + // BinaryMessages set it to true in order to denotes binary data messages instead of utf-8 text + // compatible if you wanna use the Connection's EmitMessage to send a custom binary data to the client, like a native server-client communication. + // defaults to false + BinaryMessages bool + // ReadBufferSize is the buffer size for the underline reader + // Default value is 4096 + ReadBufferSize int + // WriteBufferSize is the buffer size for the underline writer + // Default value is 4096 + WriteBufferSize int + // EnableCompression specify if the server should attempt to negotiate per + // message compression (RFC 7692). Setting this value to true does not + // guarantee that compression will be supported. Currently only "no context + // takeover" modes are supported. + EnableCompression bool + + // Subprotocols specifies the server's supported protocols in order of + // preference. If this field is set, then the Upgrade method negotiates a + // subprotocol by selecting the first match in this list with a protocol + // requested by the client. + Subprotocols []string +} +``` + +### Accept WebSocket requests & send & receive messages + +```go +import ( + "github.com/kataras/iris" + "github.com/kataras/iris/websocket" +) + +func main() { + ws := websocket.New(websocket.Config{ + ReadBufferSize: 1024, + WriteBufferSize: 1024, + }) + + ws.OnConnection(handleConnection) + + app := iris.New() + // register the server on an endpoint. + // see the inline javascript code in the websockets.html, this endpoint is used to connect to the server. + app.Get("/echo", ws.Handler()) + + // serve the javascript built'n client-side library, + // see weboskcets.html script tags, this path is used. + app.Any("/iris-ws.js", func(ctx iris.Context) { + ctx.Write(websocket.ClientSource) + }) +} + +func handleConnection(c websocket.Connection) { + // Read events from browser + c.On("chat", func(msg string) { + // Print the message to the console, c.Context() is the iris's http context. + fmt.Printf("%s sent: %s\n", c.Context().RemoteAddr(), msg) + // Write message back to the client message owner: + // c.Emit("chat", msg) + c.To(websocket.Broadcast).Emit("chat", msg) + }) +} +``` \ No newline at end of file diff --git a/view/README.md b/view/README.md new file mode 100644 index 00000000..e054a77e --- /dev/null +++ b/view/README.md @@ -0,0 +1,177 @@ +# View + +Iris supports 5 template engines out-of-the-box, developers can still use any external golang template engine, +as `context/context#ResponseWriter()` is an `io.Writer`. + +All of these five template engines have common features with common API, +like Layout, Template Funcs, Party-specific layout, partial rendering and more. + +- The standard html, its template parser is the [golang.org/pkg/html/template/](https://golang.org/pkg/html/template/) +- Django, its template parser is the [github.com/flosch/pongo2](https://github.com/flosch/pongo2) +- Pug(Jade), its template parser is the [github.com/Joker/jade](https://github.com/Joker/jade) +- Handlebars, its template parser is the [github.com/aymerick/raymond](https://github.com/aymerick/raymond) +- Amber, its template parser is the [github.com/eknkc/amber](https://github.com/eknkc/amber) + +## Overview + +```go +// file: main.go +package main + +import "github.com/kataras/iris" + +func main() { + app := iris.New() + // Load all templates from the "./views" folder + // where extension is ".html" and parse them + // using the standard `html/template` package. + app.RegisterView(iris.HTML("./views", ".html")) + + // Method: GET + // Resource: http://localhost:8080 + app.Get("/", func(ctx iris.Context) { + // Bind: {{.message}} with "Hello world!" + ctx.ViewData("message", "Hello world!") + // Render template file: ./views/hello.html + ctx.View("hello.html") + }) + + // Method: GET + // Resource: http://localhost:8080/user/42 + app.Get("/user/{id:long}", func(ctx iris.Context) { + userID, _ := ctx.Params().GetInt64("id") + ctx.Writef("User ID: %d", userID) + }) + + // Start the server using a network address. + app.Run(iris.Addr(":8080")) +} +``` + +```html + + + + Hello Page + + +

{{.message}}

+ + +``` + +## Template functions + +```go +package main + +import "github.com/kataras/iris" + +func main() { + app := iris.New() + + // - standard html | iris.HTML(...) + // - django | iris.Django(...) + // - pug(jade) | iris.Pug(...) + // - handlebars | iris.Handlebars(...) + // - amber | iris.Amber(...) + tmpl := iris.HTML("./templates", ".html") + + // built'n template funcs are: + // + // - {{ urlpath "mynamedroute" "pathParameter_ifneeded" }} + // - {{ render "header.html" }} + // - {{ render_r "header.html" }} // partial relative path to current page + // - {{ yield }} + // - {{ current }} + + // register a custom template func. + tmpl.AddFunc("greet", func(s string) string { + return "Greetings " + s + "!" + }) + + // register the view engine to the views, this will load the templates. + app.RegisterView(tmpl) + + app.Get("/", hi) + + // http://localhost:8080 + app.Run(iris.Addr(":8080")) +} + +func hi(ctx iris.Context) { + // render the template file "./templates/hi.html" + ctx.View("hi.html") +} +``` + +```html + +{{greet "kataras"}} +``` + +## Embedded + +View engine supports bundled(https://github.com/jteeuwen/go-bindata) template files too. +`go-bindata` gives you two functions, `Assset` and `AssetNames`, +these can be setted to each of the template engines using the `.Binary` function. + +Example code: + +```go +package main + +import "github.com/kataras/iris" + +func main() { + app := iris.New() + // $ go get -u github.com/jteeuwen/go-bindata/... + // $ go-bindata ./templates/... + // $ go build + // $ ./embedding-templates-into-app + // html files are not used, you can delete the folder and run the example + app.RegisterView(iris.HTML("./templates", ".html").Binary(Asset, AssetNames)) + app.Get("/", hi) + + // http://localhost:8080 + app.Run(iris.Addr(":8080")) +} + +type page struct { + Title, Name string +} + +func hi(ctx iris.Context) { + // {{.Page.Title}} and {{Page.Name}} + ctx.ViewData("Page", page{Title: "Hi Page", Name: "iris"}) + ctx.View("hi.html") +} +``` + +A real example can be found here: https://github.com/kataras/iris/tree/master/_examples/view/embedding-templates-into-app. + +## Reload + +Enable auto-reloading of templates on each request. Useful while developers are in dev mode +as they no neeed to restart their app on every template edit. + +Example code: + +```go +pugEngine := iris.Pug("./templates", ".jade") +pugEngine.Reload(true) // <--- set to true to re-build the templates on each request. +app.RegisterView(pugEngine) +``` + +## Examples + +- [Overview](https://github.com/kataras/iris/blob/master/_examples/view/overview/main.go) +- [Hi](https://github.com/kataras/iris/blob/master/_examples/view/template_html_0/main.go) +- [A simple Layout](https://github.com/kataras/iris/blob/master/_examples/view/template_html_1/main.go) +- [Layouts: `yield` and `render` tmpl funcs](https://github.com/kataras/iris/blob/master/_examples/view/template_html_2/main.go) +- [The `urlpath` tmpl func](https://github.com/kataras/iris/blob/master/_examples/view/template_html_3/main.go) +- [The `url` tmpl func](https://github.com/kataras/iris/blob/master/_examples/view/template_html_4/main.go) +- [Inject Data Between Handlers](https://github.com/kataras/iris/blob/master/_examples/view/context-view-data/main.go) +- [Embedding Templates Into App Executable File](https://github.com/kataras/iris/blob/master/_examples/view/embedding-templates-into-app/main.go) + +You can serve [quicktemplate](https://github.com/valyala/quicktemplate) files too, simply by using the `context#ResponseWriter`, take a look at the [iris/_examples/http_responsewriter/quicktemplate](https://github.com/kataras/iris/tree/master/_examples/http_responsewriter/quicktemplate) example. \ No newline at end of file