diff --git a/.gitattributes b/.gitattributes index 158482ee13..574feb3509 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1,11 +1,11 @@ -*.go linguist-language=Go -vendor/* linguist-vendored -_examples/* linguist-documentation -_benchmarks/* linguist-documentation -# Set the default behavior, in case people don't have core.autocrlf set. -# if from windows: -# git config --global core.autocrlf true -# if from unix: -# git config --global core.autocrlf input -# https://help.github.com/articles/dealing-with-line-endings/#per-repository-settings +*.go linguist-language=Go +vendor/* linguist-vendored +_examples/* linguist-documentation +_benchmarks/* linguist-documentation +# Set the default behavior, in case people don't have core.autocrlf set. +# if from windows: +# git config --global core.autocrlf true +# if from unix: +# git config --global core.autocrlf input +# https://help.github.com/articles/dealing-with-line-endings/#per-repository-settings * text=auto \ No newline at end of file diff --git a/.travis.yml b/.travis.yml index 351c56a19a..fc1c42725b 100644 --- a/.travis.yml +++ b/.travis.yml @@ -3,11 +3,7 @@ os: - linux - osx go: -# - go1.8 works of course but -# we must encourage users to update to the latest go version, -# so examples are running on go 1.9 mode. - go1.9 -# - tip go_import_path: github.com/kataras/iris install: - go get ./... # for iris-contrib/httpexpect, kataras/golog diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index b2bac0247c..dbcda5e0e8 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -47,6 +47,7 @@ Instructions can be found at: https://github.com/kataras/iris/issues/796 Write an article about Iris in https://medium.com , https://dev.to or if you're being a hackathon at https://hackernoon.com, some examples: +* [A Hasura starter project with a ready to deploy Golang hello-world web app with IRIS](bit.ly/2lmKaAZ) * [Top 6 web frameworks for Go as of 2017](https://blog.usejournal.com/top-6-web-frameworks-for-go-as-of-2017-23270e059c4b) * [Iris Go Framework + MongoDB](https://medium.com/go-language/iris-go-framework-mongodb-552e349eab9c) * [How to build a file upload form using DropzoneJS and Go](https://hackernoon.com/how-to-build-a-file-upload-form-using-dropzonejs-and-go-8fb9f258a991) diff --git a/FAQ.md b/FAQ.md index 26c0bcf16b..1d018dd67b 100644 --- a/FAQ.md +++ b/FAQ.md @@ -60,17 +60,30 @@ Iris may have reached version 8, but we're not stopping there. We have many feat Yes, not only because you will learn Golang in the same time, but there are some positions open for Iris-specific developers the time we speak. -- https://glints.id/opportunities/jobs/5553 +Go to our facebook page, like it and receive notifications about new job offers, we already have couple of them stay at the top of the page: https://www.facebook.com/iris.framework + + ## Do we have a community Chat? -Yes, https://kataras.rocket.chat/channel/iris. +Yes, https://chat.iris-go.com https://github.com/kataras/iris/issues/646 @@ -78,4 +91,4 @@ https://github.com/kataras/iris/issues/646 By normal people like you, who help us by donating small or larger amounts of money. -Help this project to continue deliver awesome and unique features with the higher code quality as possible by donating any amount via [PayPal](https://www.paypal.me/kataras)! \ No newline at end of file +Help this project to continue deliver awesome and unique features with the highest possible code quality as possible by donating any amount via [PayPal](https://www.paypal.me/kataras). Your name will be published [here](https://iris-go.com/donate) after your approval via e-mail. \ No newline at end of file diff --git a/Gopkg.lock b/Gopkg.lock index 0ce0f79bd3..2bf5a8ef55 100644 --- a/Gopkg.lock +++ b/Gopkg.lock @@ -121,12 +121,6 @@ packages = ["."] revision = "20e139a6d2469769ae88e0a3579ba5df71839ca7" -[[projects]] - branch = "master" - name = "github.com/skratchdot/open-golang" - packages = ["."] - revision = "75fb7ed4208cf72d323d7d02fd1a5964a7a9073c" - [[projects]] name = "github.com/klauspost/compress" packages = ["flate","gzip"] diff --git a/Gopkg.toml b/Gopkg.toml index 83a3a289f4..59883cd866 100644 --- a/Gopkg.toml +++ b/Gopkg.toml @@ -46,10 +46,6 @@ branch = "master" name = "github.com/kataras/survey" -[[constraint]] - branch = "master" - name = "github.com/skratchdot/open-golang" - [[constraint]] name = "github.com/klauspost/compress" version = "1.2.1" diff --git a/HISTORY.md b/HISTORY.md index e11170edc5..16c39980d8 100644 --- a/HISTORY.md +++ b/HISTORY.md @@ -1,4 +1,4 @@ -# FAQ +# History/Changelog ### Looking for free and real-time support? @@ -17,1826 +17,169 @@ Developers are not forced to upgrade if they don't really need it. Upgrade whene **How to upgrade**: Open your command-line and execute this command: `go get -u github.com/kataras/iris` or let the automatic updater do that for you. -# Th, 09 November 2017 | v8.5.8 +# Mo, 01 Jenuary 2018 | v10.0.0 -- **IMPROVE** the `Single Page Application builder`[*](https://github.com/kataras/blob/master/core/router/router_spa_wrapper.go) and fix https://github.com/kataras/iris/issues/803 reported by @ionutvilie, a new example is located [here](_examples/file-server/embedded-single-page-application-with-other-routes). - * `app.SPA` now returns the `*SPABuilder` and you can change some of its fields manually, i.e; - * `IndexNames` defaulted to empty but can be seted to `[]{"index.html"}` or call the **new** `AddIndexName` from the `app.SPA` manually if dynamic view on root has registered, see [here](_examples/file-server/single-page-application/embedded-single-page-application) how. - * `AssetValidator` exists as it was and it's checked before the spa file server but it allows everything by-default because the real validation happens internally; if body was written or not, if not then reset the context's response writer and execute the router, as previously, otherwise release the context and send the response to the client. - -# Tu, 07 November 2017 | v8.5.7 +We must thanks [Mrs. Diana](https://www.instagram.com/merry.dii/) for our awesome new [logo](https://iris-go.com/images/icon.svg)! -Nothing crazy here, just one addition which may help some people; +You can [contact](mailto:Kovalenkodiana8@gmail.com) her for any design-related enquiries or explore and send a direct message via [instagram](https://www.instagram.com/merry.dii/). -Able to share configuration between multiple Iris instances based on the `$home_path+iris.yml` configuration file with the **new** [iris.WithGlobalConfiguration](https://github.com/kataras/iris/blob/master/configuration.go#L202) configurator[*](https://github.com/kataras/iris/blob/master/configuration.go#L191). +

+ +

-Example: +At this version we have many internal improvements but just two major changes and one big feature, called **hero**. -```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 share configuration between multiple iris instances. - // This configuration file lives in your $HOME/iris.yml for unix hosts - // or %HOMEDRIVE%+%HOMEPATH%/iris.yml for windows hosts, and you can modify it. - app.Run(iris.Addr(":8080"), iris.WithGlobalConfiguration) - // or before run: - // app.Configure(iris.WithGlobalConfiguration) - // app.Run(iris.Addr(":8080")) -} -``` - -# Su, 05 November 2017 | v8.5.6 - -- **DEPRECATE** the `app.StaticServe`, use `app.StaticWeb` which does the same thing but better or `iris/app.StaticHandler` which gives you more options to work on. -- add some debug messages for route registrations, to be aligned with the mvc debug messages. -- improve the https://iris-go.com/v8/recipe -- now you can see other files like assets as well -- lexical order of categories instead of "level". -- add [8 more examples](_examples/experimental-handlers) to this repository, originally lived at https://github.com/iris-contrib/middleware and https://github.com/iris-contrib/examples/tree/master/experimental-handlers. - -_TODO;_ - -- [ ] give the ability to customize the mvc path-method-and path parameters mapping, -- [ ] make a github bot which will post the monthly usage and even earnings statistics in a public github markdown file, hope that users will love that type of transparency we will introduce here. - -# Th, 02 November 2017 | v8.5.5 - -- fix [audio/mpeg3 does not appear to be a valid registered mime type#798](https://github.com/kataras/iris/issues/798]) reported by @kryptodev, -- improve the updater's performance and moved that into the framework itself, - - ask for authentication only when a new version is released. -- sessiondb's `.Async` functions do nothing now, all session databases(back-end persistence storage) should run in-sync, @speedwheel helped to find a misbehavior because of that setting, -- the [configuration](configuration.go) now has `json` fields tag like `yaml` and `toml` did in order to be able to be fetched from a json file directly using the `encoding/json` package, -- fix the `context#GetFloat64`, -- we are on [opencollective](http://opencollective.com/iris) and sponsored by [codesponsor](https://codesponsor.io/) now. - -_TODO;_ +> The new version adds 75 plus new commits, the PR is located [here](https://github.com/kataras/iris/pull/849) read the internal changes if you are developing a web framework based on Iris. Why 9 was skipped? Because. -- give the ability to customize the mvc path-method-and path parameters mapping, -- make a github bot which will post the monthly usage and even earnings statistics in a public github markdown file, hope that users will love that type of transparency we will introduce here. +## Hero -# Th, 26 October 2017 | v8.5.4 +The new package [hero](hero) contains features for binding any object or function that `handlers` may use, these are called dependencies. Hero funcs can also return any type of values, these values will be dispatched to the client. -This version is part of the [releases](https://github.com/kataras/iris/releases). +> You may saw binding before but you didn't have code editor's support, with Iris you get truly safe binding thanks to the new `hero` package. It's also fast, near to raw handlers performance because Iris calculates everything before server ran! -## Version Updater +Below you will see some screenshots we prepared for you in order to be easier to understand: -Not any new features or fixes (all reported bugs are fixed) in this version, just a tiny improvement. +### 1. Path Parameters - Built'n Dependencies -**More friendly version checker!** +![](https://github.com/kataras/explore/raw/master/iris/hero/hero-1-monokai.png) -> Remember: If you don't want to keep the version checker and you're pretty sure that you will be able to keep your server up-to-date manually, then you can disable the auto updater via; `app.Run(..., iris.WithoutVersionChecker)`. +### 2. Services - Static Dependencies -## We need your help with translations into your native language +![](https://github.com/kataras/explore/raw/master/iris/hero/hero-2-monokai.png) -Iris needs your help, please think about contributing to the translation of the [README](README.md) and https://iris-go.com, you will be rewarded. +### 3. Per-Request - Dynamic Dependencies -Instructions can be found at: https://github.com/kataras/iris/issues/796 +![](https://github.com/kataras/explore/raw/master/iris/hero/hero-3-monokai.png) -# Su, 22 October 2017 | v8.5.3 +`hero funcs` are very easy to understand and when you start using them **you never go back**. -- FIX: [Websocket: memory leak on startPinger](https://github.com/kataras/iris/issues/787) by @jerson with PR: https://github.com/kataras/iris/pull/788 -- FIX: [Websocket: time.Ticker cause memory leak](https://github.com/kataras/iris/issues/791) by @jerson with PR: https://github.com/kataras/iris/pull/792 -- NEW: [Websocket: total connections](https://github.com/kataras/iris/issues/793) by @jerson with PR: https://github.com/kataras/iris/pull/795 -- NEW: Add a `raven` middleware inside [iris-contrib/middleware/raven](https://github.com/iris-contrib/middleware/tree/master/raven) as requested at "[Can I use iris with sentry?](https://github.com/kataras/iris/issues/785)" +Examples: -### 🎗️ People that you should follow - -Help this project to continue deliver awesome and unique features with the higher code quality as possible by donating any amount via [PayPal](https://www.paypal.me/kataras) or [BTC](https://iris-go.com/v8/donate)! - -| Name | Amount | Membership | -| -----------|--------|--------| -| [Juan Sebastián Suárez Valencia](https://github.com/Juanses) | 20 EUR | Bronze | -| [Bob Lee](https://github.com/li3p) | 20 EUR | Bronze | -| [Celso Luiz](https://github.com/celsosz) | 50 EUR | **Silver** | -| [Ankur Srivastava](https://github.com/ansrivas) | 20 EUR | Bronze | -| [Damon Zhao](https://github.com/se77en) | 20 EUR | Bronze | -| [Exponity - Tech Company](https://github.com/exponity) | 30 EUR | Bronze | -| [Thomas Fritz](https://github.com/thomasfr) | 25 EUR | Bronze | -| [Thanos V.](http://mykonosbiennale.com/) | 20 EUR | Bronze | -| [George Opritescu](https://github.com/International) | 20 EUR | Bronze | -| [Lex Tang](https://github.com/lexrus) | 20 EUR | Bronze | -| [Bill Q.](https://github.com/hiveminded) | 600 EUR | **Gold** | -| [Conrad Steenberg](https://github.com/hengestone) | 25 EUR | Bronze | - -# Th, 12 October 2017 | v8.5.2 - -This version is part of the [releases](https://github.com/kataras/iris/releases). - -## MVC - -Add `bool` as a supported return value, if false then skips everything else and fires 404 not found. - -New example which covers the Service and Repository layers side-by-side with the MVC Architectural pattern, clean and simple: [_examples/mvc/overview](_examples/mvc/overview). - -## Websocket - -Fix(?) https://github.com/kataras/iris/issues/782 by @jerson with PR: https://github.com/kataras/iris/pull/783. - -## Minor - -Add some minor comments for the view/django's origin type getters-- as pushed at PR: [#765](https://github.com/kataras/iris/pull/765). - -[sessions/sessiondb/badger](sessions/sessiondb/badger) vendored with: https://github.com/kataras/iris/commit/e7517ec79b45673e7cad353e52023ebd7237cf38. - -# Tu, 10 October 2017 | v8.5.1 +- [Basic](_examples/hero/basic/main.go) +- [Overview](_examples/hero/overview) ## MVC -- fix any manual or before middleware's `ctx.ViewData(key, value)` gets overridden by setting `mvc.Controller.Data` or `return mvc.View {Data: ...}`. See the [test case](mvc/method_result_test.go#L226). +You have to understand the `hero` package in order to use the `mvc`, because `mvc` uses the `hero` internally for the controller's methods you use as routes, the same rules applied to those controller's methods of yours as well. -# Mo, 09 October 2017 | v8.5.0 - -## MVC +With this version you can register **any controller's methods as routes manually**, you can **get a route based on a method name and change its `Name` (useful for reverse routing inside templates)**, you can use any **dependencies** registered from `hero.Register` or `mvc.New(iris.Party).Register` per mvc application or per-controller, **you can still use `BeginRequest` and `EndRequest`**, you can catch **`BeforeActivation(b mvc.BeforeActivation)` to add dependencies per controller and `AfterActivation(a mvc.AfterActivation)` to make any post-validations**, **singleton controllers when no dynamic dependencies are used**, **Websocket controller, as simple as a `websocket.Connection` dependency** and more... -Great news for our **MVC** Fans or if you're not you may want to use that powerful feature today, because of the smart coding and decisions the performance is quite the same to the pure handlers, see [_benchmarks](_benchmarks). +Examples: -A Controller's field that is an interface can now be binded to any type that implements that interface. +**If you used MVC before then read very carefully: MVC CONTAINS SOME BREAKING CHANGES BUT YOU CAN DO A LOT MORE AND EVEN FASTER THAN BEFORE** -Ability to send HTTP responses based on the Controller's method function's output values, see below; +**PLEASE READ THE EXAMPLES CAREFULLY, WE'VE MADE THEM FOR YOU** -Iris now gives you the ability to render a response based on the **output values** returned from the controller's method functions! +Old examples are here as well. Compare the two different versions of each example to understand what you win if you upgrade now. -You can return any value of any type from a method function -and it will be sent to the client as expected. +| NEW | OLD | +| -----------|-------------| +| [Hello world](_examples/mvc/hello-world/main.go) | [OLD Hello world](https://github.com/kataras/iris/blob/v8/_examples/mvc/hello-world/main.go) | +| [Session Controller](_examples/mvc/session-controller/main.go) | [OLD Session Controller](https://github.com/kataras/iris/blob/v8/_examples/mvc/session-controller/main.go) | +| [Overview - Plus Repository and Service layers](_examples/mvc/overview) | [OLD Overview - Plus Repository and Service layers](https://github.com/kataras/iris/tree/v8/_examples/mvc/overview) | +| [Login showcase - Plus Repository and Service layers](_examples/mvc/login) | [OLD Login showcase - Plus Repository and Service layers](https://github.com/kataras/iris/tree/v8/_examples/mvc/login) | +| [Singleton](_examples/mvc/singleton) | **NEW** | +| [Websocket Controller](_examples/mvc/websocket) | **NEW** | +| [Vue.js Todo MVC](_examples/tutorial/vuejs-todo-mvc) | **NEW** | -* 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 `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. +## context#PostMaxMemory -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; +Remove the old static variable `context.DefaultMaxMemory` and replace it with the configuration `WithPostMaxMemory`. ```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 is just a lightweight lightweight alternative - // to the "mvc.Controller" controller type, - // use it when you don't need mvc.Controller's fields - // (you don't need those fields when you return values from the method functions). - 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} -} +// WithPostMaxMemory sets the maximum post data size +// that a client can send to the server, this differs +// from the overral request body size which can be modified +// by the `context#SetMaxRequestBodySize` or `iris#LimitRequestBodySize`. +// +// Defaults to 32MB or 32 << 20 if you prefer. +func WithPostMaxMemory(limit int64) Configurator ``` -Another good example with a typical folder structure, that many developers are used to work, is located at the new [README.md](README.md) under the [Quick MVC Tutorial #3](README.md#quick-mvc-tutorial-3) section. - -# Fr, 06 October 2017 | v8.4.5 - -- Badger team added support for transactions [yesterday](https://github.com/dgraph-io/badger/commit/06242925c2f2a5e73dc688e9049004029dd7f9f7), therefore the [badger session database](sessions/sessiondb/badger) is updated via https://github.com/kataras/iris/commit/0b48927562a2202809a7674ebedb738dc3da57e8. -- MVC: Support more than one path parameters with a single `By`, when the `By` keyword is the last word and the func's input arguments are more than one i.e `GetBy(name string, age int)`, note that you can still use the older way of doing this; `GetByBy(string, int)`. It's an enhancement of the https://github.com/kataras/iris/issues/751 feature request. -- MVC: Give controllers the ability to auto-initialize themselves by `OnActivate` func derives from the new [ActivateListener](mvc/activator/activate_listener.go) interface, this can be used to perform any custom actions when the app registers the supported Controllers. See [mvc/session_controller.go](mvc/session_controller.go) for a good use case. -- errors.Reporter.AddErr returns true if the error is added to the stack, otherwise false. -- @ZaniaDeveloper fixed https://github.com/kataras/iris/issues/778 with PR: https://github.com/kataras/iris/pull/779. -- Add `StatusSeeOther` at [mvc login example](https://github.com/kataras/iris/blob/master/_examples/mvc/login/user/controller.go#L53) for Redirection, reported by @motecshine at https://github.com/kataras/iris/issues/777. -- Fix `DisableVersionChecker` configuration field is not being passed correctly when it was true via `app.Run(..., iris.WithConfiguration{DisableVersionChecker:true, ...})` call. - -# Su, 01 October 2017 | v8.4.4 +If you used that old static field you will have to change that single line. -- Fix https://github.com/kataras/iris/issues/762 reported by @xkylsoft -- Fix https://github.com/kataras/iris/issues/771 reported by @cdren -- Improvements to the memstore's `GetInt`, `GetInt64`, `GetFloat64`, `GetBool` and remove the `golang/net/context`'s interface completion from Context, read the [changes](https://github.com/kataras/iris/commit/caff55748eca4ecb4aa5a770995265b9b3aee544) for more -- Add two examples for folder structuring as requested at https://github.com/kataras/iris/issues/748 - * [Example 1](_examples/mvc/login) - * [Example 2](_examples/structuring/mvc) -- Add node.js express [benchmarks](_benchmarks) similar to iris and netcore - -# We, 27 September 2017 | v8.4.3 - -- MVC: Support for `ByBy` syntax as requested at https://github.com/kataras/iris/issues/751 -- Fix https://github.com/kataras/iris/issues/760 -- @itcrow fixed https://github.com/kataras/iris/issues/757 with PR: https://github.com/kataras/iris/pull/758 -- @balthild fixed https://github.com/kataras/iris/issues/764 with PR: https://github.com/kataras/iris/pull/765 -- Add a new session database(back-end storage) supported by the [badger](github.com/dgraph-io/badger) key-value file-based storage, example [here](https://github.com/kataras/iris/commit/204f8474687bad1178d5108b501c6f0c7d927b9a#diff-26d58a00b7f90165fb32043676ed17a5) - -# Fr, 15 September 2017 | v8.4.2 - -## MVC - -Support more than one dynamic method function receivers. +Usage: ```go -package main - import "github.com/kataras/iris" func main() { app := iris.New() - app.Controller("/user", new(UserController)) - app.Run(iris.Addr("localhost:8080")) -} - -type UserController struct { iris.Controller } - -// Maps to GET /user -// Request example: http://localhost:8080/user -// as usual. -func (c *UserController) Get() { - c.Text = "hello from /user" -} - -// Maps to GET /user/{paramfirst:long} -// Request example: http://localhost:8080/user/42 -// as usual. -func (c *UserController) GetBy(userID int64) { - c.Ctx.Writef("hello user with id: %d", userID) -} - -// NEW: -// Maps to GET /user/{paramfirst:long}/business/{paramsecond:long} -// Request example: http://localhost:8080/user/42/business/93 -func (c *UserController) GetByBusinessBy(userID int64, businessID int64) { - c.Ctx.Writef("fetch a business id: %d that user with id: %d owns, may make your db query faster", - businessID, userID) -} -``` - -# Th, 07 September 2017 | v8.4.1 - -## Routing - -Add a macro type for booleans: `app.Get("/mypath/{paramName:boolean}", myHandler)`. - -```sh -+------------------------+ -| {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" -``` - -Add `context.Params().GetBool(paramName string) (bool, error)` respectfully. - -```go -app := iris.New() -app.Get("/mypath/{has:boolean}", func(ctx iris.Context) { // <-- - // boolean first return value - // error as second return value - // - // error will be always nil here because - // we use the {has:boolean} so router - // makes sure that the parameter is a boolean - // otherwise it will return a 404 not found http error code - // skipping the call of this handler. - has, _ := ctx.Params().GetBool("has") // <-- - if has { - ctx.HTML("it's true") - }else { - ctx.HTML("it's false") - } -}) -// [...] -``` - -## MVC - -Support for boolean method receivers, i.e `GetBy(bool), PostBy(bool)...`. - - -```go -app := iris.New() - -app.Controller("/equality", new(Controller)) -``` - -```go -type Controller struct { - iris.Controller -} - -// handles the "/equality" path. -func (c *Controller) Get() { - -} - -// registers and handles the path: "/equality/{param:boolean}". -func (c *Controller) GetBy(is bool) { // <-- // [...] -} -``` - -> Supported types for method functions receivers are: int, int64, bool and string. - -# Su, 27 August 2017 | v8.4.0 - -## Miscellaneous - -- Update `vendor blackfriday` package to its latest version, 2.0.0 -- Update [documentation](https://godoc.org/github.com/kataras/iris) for go 1.9 -- Update [_examples](_examples) folder for go 1.9 -- Update examples inside https://github.com/iris-contrib/middleware for go 1.9 -- Update https://github.com/kataras/iris-contrib/examples for go 1.9 -- Update https://iris-go.com/v8/recipe for go 1.9 - -## Router - -Add a new macro type for path parameters, `long`, it's the go type `int64`. - -```go -app.Get("/user/{id:long}", func(ctx context.Context) { - userID, _ := ctx.Params().GetInt64("id") -}) -``` - -## MVC - -The ability to pre-calculate, register and map different (relative) paths inside a single controller -with zero performance cost. - -Meaning that after a `go get -u github.com/kataras/iris` you will be able to use things like these: - -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}` - - -**Example** can be found at: [_examples/mvc/login/user/controller.go](_examples/mvc/login/user/controller.go). -## Pretty [awesome](https://github.com/kataras/iris/stargazers), right? - -# We, 23 August 2017 | v8.3.4 - -Give read access to the current request context's route, a feature that many of you asked a lot. - -```go -func(ctx context.Context) { - _ = ctx.GetCurrentRoute().Name() - // .Method() returns string, same as ctx.Method(). - // .Subdomain() returns string, the registered subdomain. - // .Path() returns string, the registered path. - // .IsOnline() returns boolean. -} -``` - -```go -type MyController struct { - mvc.Controller -} - -func (c *MyController) Get(){ - _ = c.Route().Name() // same as `c.Ctx.GetCurrentRoute().Name()`. - // [...] + app.Run(iris.Addr(":8080"), iris.WithPostMaxMemory(10 << 20)) } ``` -**Updated: 24 August 2017** - -This evening, on the next version 8.3.5: - -Able to pre-calculate, register and map different (relative) paths inside a single controller -with zero performance cost. - -Meaning that in the future you will be able to use something like these: - -If `app.Controller("/user", new(user.Controller))` - -- `func(c *Controller) Get()` - `GET:/user` , as usual. -- `func(c *Controller) Post()` - `POST:/user`, as usual. -- `func(c *Controller) GetLogin()` - `GET:/user/login` -- `func(c *Controller) PostLogin()` - `POST:/user/login` -- `func(c *Controller) GetProfileFollowers()` - `GET:/user/profile/followers` -- `func(c *Controller) PostProfileFollowers()` - `POST:/user/profile/followers` -- `func(c *Controller) GetBy()` - `GET:/user/{param}` -- `func(c *Controller) GetByName(name string)` - `GET:/user/{name}` -- `func(c *Controller) PostByName(name string)` - `POST:/user/{name}` -- `func(c *Controller) GetByID(id int64 || int)` - `GET:/user/{id:int}` -- `func(c *Controller) PostByID(id int64 || int)` - `POST:/user/{id:int}` - -Watch and stay tuned my friends. - -# We, 23 August 2017 | v8.3.3 +## context#UploadFormFiles -Better debug messages when using MVC. - -Add support for recursively binding and **custom controllers embedded to other custom controller**, that's the new feature. That simply means that Iris users are able to use "shared" controllers everywhere; when binding, using models, get/set persistence data, adding middleware, intercept request flow. - -This will help web authors to split the logic at different controllers. Those controllers can be also used as "standalone" to serve a page somewhere else in the application as well. - -My personal advice to you is to always organize and split your code nicely and wisely in order to avoid using such as an advanced MVC feature, at least any time soon. - -I'm aware that this is not always an easy task to do, therefore is here if you ever need it :) - -A ridiculous simple example of this feature can be found at the [mvc/controller_test.go](https://github.com/kataras/iris/blob/master/mvc/controller_test.go#L424) file. - - -# Tu, 22 August 2017 | v8.3.2 - -### MVC - -When one or more values of handler type (`func(ctx context.Context)`) are passed -right to the controller initialization then they will be recognised and act as middleware(s) -that ran even before the controller activation, there is no reason to load -the whole controller if the main handler or its `BeginRequest` are not "allowed" to be executed. - -Example Code +New method to upload multiple files, should be used for common upload actions, it's just a helper function. ```go -func checkLogin(ctx context.Context) { - if !myCustomAuthMethodPassed { - // [set a status or redirect, you know what to do] - ctx.StatusCode(iris.StatusForbidden) - return - } - - // [continue to the next handler, at this example is our controller itself] - ctx.Next() -} - -// [...] -app.Controller(new(ProfileController), checkLogin) -// [...] +// UploadFormFiles uploads any received file(s) from the client +// to the system physical location "destDirectory". +// +// The second optional argument "before" gives caller the chance to +// modify the *miltipart.FileHeader before saving to the disk, +// it can be used to change a file's name based on the current request, +// all FileHeader's options can be changed. You can ignore it if +// you don't need to use this capability before saving a file to the disk. +// +// Note that it doesn't check if request body streamed. +// +// Returns the copied length as int64 and +// a not nil error if at least one new file +// can't be created due to the operating system's permissions or +// http.ErrMissingFile if no file received. +// +// If you want to receive & accept files and manage them manually you can use the `context#FormFile` +// instead and create a copy function that suits your needs, the below is for generic usage. +// +// The default form's memory maximum size is 32MB, it can be changed by the +// `iris#WithPostMaxMemory` configurator at main configuration passed on `app.Run`'s second argument. +// +// See `FormFile` to a more controlled to receive a file. +func (ctx *context) UploadFormFiles( + destDirectory string, + before ...func(string, string), + ) (int64, error) ``` -Usage of these kind of MVC features could be found at the [mvc/controller_test.go](https://github.com/kataras/iris/blob/master/mvc/controller_test.go#L174) file. - -### Other minor enhancements - -- fix issue [#726](https://github.com/kataras/iris/issues/726)[*](https://github.com/kataras/iris/commit/5e435fc54fe3dbf95308327c2180d1b444ef7e0d) -- fix redis sessiondb expiration[*](https://github.com/kataras/iris/commit/85cfc91544c981e87e09c5aa86bad4b85d0b96d3) -- update recursively when new version is available[*](https://github.com/kataras/iris/commit/cd3c223536c6a33653a7fcf1f0648123f2b968fd) -- some minor session enhancements[*](https://github.com/kataras/iris/commit/2830f3b50ee9c526ac792c3ce1ec1c08c24ea024) - - -# Sa, 19 August 2017 | v8.3.1 - -First of all I want to thank you for the 100% green feedback you gratefully sent me you about -my latest article `Go vs .NET Core in terms of HTTP performance`, published at [medium's hackernoon.com](https://hackernoon.com/go-vs-net-core-in-terms-of-http-performance-7535a61b67b8) and [dev.to](https://dev.to/kataras/go-vsnet-core-in-terms-of-http-performance). I really appreciate it💓 - -No API Changes. - -However two more methods added to the `Controller`. - -- `RelPath() string`, returns the relative path based on the controller's name and the request path. -- `RelTmpl() string`, returns the relative template directory based on the controller's name. - -These are useful when dealing with big `controllers`, they help you to keep align with any -future changes inside your application. - -Want to learn more about these functions? Go to the [mvc/controller_test.go](mvc/controller_test.go) file and scroll to the bottom! - -# Fr, 18 August 2017 | v8.3.0 - -Good news for devs that are used to write their web apps using the `MVC` architecture pattern. - -Implement a whole new `mvc` package with additional support for models and easy binding. +Example can be found [here](_examples/http_request/upload-files/main.go). -@kataras started to develop that feature by version 8.2.5, back then it didn't seem -to be a large feature and maybe a game-changer, so it lived inside the `kataras/iris/core/router/controller.go` file. -However with this version, so many things are implemented for the MVC and we needed a new whole package, -this new package is the `kataras/iris/mvc`, but if you used go 1.9 to build then you don't have to do any refactor, you could use the `iris.Controller` type alias. +## context#View -People who used the mvc from its baby steps(v8.2.5) the only syntactic change you'll have to do is to rename the `router.Controller` to `mvc.Controller`: +Just a minor addition, add a second optional variadic argument to the `context#view` method to accept a single value for template binding. +When you just want one value and not key-value pairs, you used to use an empty string on the `ViewData`, which is fine, especially if you preload these from a previous handler/middleware in the request handlers chain. -Before: ```go -import "github.com/kataras/iris/core/router" -type MyController struct { - router.Controller +func(ctx iris.Context) { + ctx.ViewData("", myItem{Name: "iris" }) + ctx.View("item.html") } ``` -Now: -```go -import "github.com/kataras/iris/mvc" -type MyController struct { - mvc.Controller - // if you build with go1.9 you can omit the import of mvc package - // and just use `iris.Controller` instead. -} -``` - -### MVC (Model View Controller) - -![](_examples/mvc/web_mvc_diagram.png) - -From version 8.3 and after Iris has **first-class support for the MVC pattern**, you'll not find -these stuff anywhere else in the Go world. - - -Example Code +Same as: ```go -package main - -import ( - "sync" - - "github.com/kataras/iris" - "github.com/kataras/iris/mvc" -) - -func main() { - app := iris.New() - app.RegisterView(iris.HTML("./views", ".html")) - - // when we have a path separated by spaces - // then the Controller is registered to all of them one by one. - // - // myDB is binded to the controller's `*DB` field: use only structs and pointers. - app.Controller("/profile /profile/browse /profile/{id:int} /profile/me", - new(ProfileController), myDB) // IMPORTANT - - app.Run(iris.Addr(":8080")) -} - -// UserModel our example model which will render on the template. -type UserModel struct { - ID int64 - Username string +func(ctx iris.Context) { + ctx.View("item.html", myItem{Name: "iris" }) } - -// DB is our example database. -type DB struct { - usersTable map[int64]UserModel - mu sync.RWMutex -} - -// GetUserByID imaginary database lookup based on user id. -func (db *DB) GetUserByID(id int64) (u UserModel, found bool) { - db.mu.RLock() - u, found = db.usersTable[id] - db.mu.RUnlock() - return -} - -var myDB = &DB{ - usersTable: map[int64]UserModel{ - 1: {1, "kataras"}, - 2: {2, "makis"}, - 42: {42, "jdoe"}, - }, -} - -// ProfileController our example user controller which controls -// the paths of "/profile" "/profile/{id:int}" and "/profile/me". -type ProfileController struct { - mvc.Controller // IMPORTANT - - User UserModel `iris:"model"` - // we will bind it but you can also tag it with`iris:"persistence"` - // and init the controller with manual &PorifleController{DB: myDB}. - DB *DB -} - -// Get method handles all "GET" HTTP Method requests of the controller's paths. -func (pc *ProfileController) Get() { // IMPORTANT - path := pc.Path - - // requested: /profile path - if path == "/profile" { - pc.Tmpl = "profile/index.html" - return - } - // requested: /profile/browse - // this exists only to proof the concept of changing the path: - // it will result to a redirection. - if path == "/profile/browse" { - pc.Path = "/profile" - return - } - - // requested: /profile/me path - if path == "/profile/me" { - pc.Tmpl = "profile/me.html" - return - } - - // requested: /profile/$ID - id, _ := pc.Params.GetInt64("id") - - user, found := pc.DB.GetUserByID(id) - if !found { - pc.Status = iris.StatusNotFound - pc.Tmpl = "profile/notfound.html" - pc.Data["ID"] = id - return - } - - pc.Tmpl = "profile/profile.html" - pc.User = user -} - - -/* -func (pc *ProfileController) Post() {} -func (pc *ProfileController) Put() {} -func (pc *ProfileController) Delete() {} -func (pc *ProfileController) Connect() {} -func (pc *ProfileController) Head() {} -func (pc *ProfileController) Patch() {} -func (pc *ProfileController) Options() {} -func (pc *ProfileController) Trace() {} -*/ - -/* -func (pc *ProfileController) All() {} -// OR -func (pc *ProfileController) Any() {} -*/ ``` -Iris web framework 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 `context.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 `mvc.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. - -**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). - - -Follow the examples below, - -https://github.com/kataras/iris/tree/master/_examples/#mvc - -### Bugs - -Fix [#723](https://github.com/kataras/iris/issues/723) reported by @speedwheel. - - -# Mo, 14 August 2017 | v8.2.6 - -Able to call done/end handlers inside a `Controller`, via optional `EndRequest(ctx context.Context)` function inside the controller struct. - -```go -// it's called after t.Get()/Post()/Put()/Delete()/Connect()/Head()/Patch()/Options()/Trace(). -func (t *testControllerEndRequestFunc) EndRequest(ctx context.Context) { - // 2. - // [your code goes here...] -} - -// will handle "GET" request HTTP method only. -func (t *testControllerEndRequestFunc) Get() { - // 1. - // [your code goes here...] -} +```html +Item's name: {{.Name}} ``` -Look at the [v8.2.5 changelog](#su-13-august-2017--v825) to learn more about the new Iris Controllers feature. - -# Su, 13 August 2017 | v8.2.5 - -Good news for devs that are used to write their web apps using the `MVC-style` app architecture. - -Yesterday I wrote a [tutorial](tutorial/mvc-from-scratch) on how you can transform your raw `Handlers` to `Controllers` using the existing tools only ([Iris is the most modular web framework out there](https://medium.com/@corebreaker/iris-web-cd684b4685c7), we all have no doubt about this). - -Today, I did implement the `Controller` idea as **built'n feature inside Iris**. -Our `Controller` supports many things among them are: - -- 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 -- optional `BeginRequest(ctx)` function to perform any initialization before the methods, useful to call middlewares or when many methods use the same collection of data -- optional `EndRequest(ctx)` function to perform any finalization after the methods executed -- access to the request path parameters via the `Params` field -- 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 `context.Context` via the `Ctx` field -- flow as you used to, `Controllers` can be registered to any `Party`, including Subdomains, the Party's begin and done handlers work as expected. +## context#YAML -It's very easy to get started, the only function you need to call instead of `app.Get/Post/Put/Delete/Connect/Head/Patch/Options/Trace` is the `app.Controller`. - -Example Code: +Add a new `context#YAML` function, it renders a yaml from a structured value. ```go -// file: main.go - -package main - -import ( - "github.com/kataras/iris" - - "controllers" -) - -func main() { - app := iris.New() - app.RegisterView(iris.HTML("./views", ".html")) - - app.Controller("/", new(controllers.Index)) - - // http://localhost:8080/ - app.Run(iris.Addr(":8080")) -} - -``` - -```go -// file: controllers/index.go - -package controllers - -import ( - "github.com/kataras/iris/core/router" -) - -// Index is our index example controller. -type Index struct { - mvc.Controller - // if you're using go1.9: - // you can omit the /core/router import statement - // and just use the `iris.Controller` instead. -} - -// will handle GET method on http://localhost:8080/ -func (c *Index) Get() { - c.Tmpl = "index.html" - c.Data["title"] = "Index page" - c.Data["message"] = "Hello world!" -} - -// will handle POST method on http://localhost:8080/ -func (c *Index) Post() {} - -``` - -> Tip: declare a func(c *Index) All() {} or Any() to register all HTTP Methods. - -A full example can be found at the [_examples/mvc](_examples/mvc) folder. - - -# Sa, 12 August 2017 | v8.2.4 - -No API Changes. - -Fix https://github.com/kataras/iris/issues/717, users are welcomed to follow the thread for any questions or reports about Gzip and Static Files Handlers **only**. - -# Th, 10 August 2017 | v8.2.3 - -No API Changes. - -Fix https://github.com/kataras/iris/issues/714 - -Continue to v8.2.2 for more... - -# Th, 10 August 2017 | v8.2.2 - -No API Changes. - -- Implement [Google reCAPTCHA](middleware/recaptcha) middleware, example [here](_examples/miscellaneous/recaptcha/main.go) -- Fix [kataras/golog](https://github.com/kataras/golog) prints with colors on windows server 2012 while it shouldn't because its command line tool does not support 256bit colors -- Improve the updater by a custom self-updated back-end version checker, can be disabled by: - -```go -app.Run(iris.Addr(":8080"), iris.WithoutVersionChecker) -``` -Or -```go -app.Configure(iris.WithoutVersionChecker) -``` -Or -```go -app.Configure(iris.WithConfiguration(iris.Configuration{DisableVersionChecker:true})) -``` - -# Tu, 08 August 2017 | v8.2.1 - -No API Changes. Great news for the unique iris sessions library, once again. - -**NEW**: [LevelDB-based](https://github.com/google/leveldb) session database implemented, example [here](_examples/sessions/database/leveldb/main.go). - -[Redis-based sessiondb](sessions/sessiondb/redis) has no longer the `MaxAgeSeconds` config field, -this is passed automatically by the session manager, now. - -All [sessions databases](sessions/sessiondb) have an `Async(bool)` function, if turned on -then all synchronization between the memory store and the back-end database will happen -inside different go routines. By-default async is false but it's recommended to turn it on, it will make sessions to be stored faster, at most. - -All reported issues have been fixed, the API is simplified by `v8.2.0` so everyone can -create and use any back-end storage for application's sessions persistence. - -# Mo, 07 August 2017 | v8.2.0 - -No Common-API Changes. - -Good news for [iris sessions back-end databases](_examples/sessions) users. - -
-Info for session database authors -Session Database API Changed to: - -```go -type Database interface { - Load(sid string) RemoteStore - Sync(p SyncPayload) -} - -// SyncPayload reports the state of the session inside a database sync action. -type SyncPayload struct { - SessionID string - - Action Action - // on insert it contains the new key and the value - // on update it contains the existing key and the new value - // on delete it contains the key (the value is nil) - // on clear it contains nothing (empty key, value is nil) - // on destroy it contains nothing (empty key, value is nil) - Value memstore.Entry - // Store contains the whole memory store, this store - // contains the current, updated from memory calls, - // session data (keys and values). This way - // the database has access to the whole session's data - // every time. - Store RemoteStore -} - - -// RemoteStore is a helper which is a wrapper -// for the store, it can be used as the session "table" which will be -// saved to the session database. -type RemoteStore struct { - // Values contains the whole memory store, this store - // contains the current, updated from memory calls, - // session data (keys and values). This way - // the database has access to the whole session's data - // every time. - Values memstore.Store - // on insert it contains the expiration datetime - // on update it contains the new expiration datetime(if updated or the old one) - // on delete it will be zero - // on clear it will be zero - // on destroy it will be zero - Lifetime LifeTime -} -``` - -Read more at [sessions/database.go](sessions/database.go), view how three built'n session databases are being implemented [here](sessions/sessiondb). -
- -All sessions databases are updated and they performant even faster than before. - -- **NEW** raw file-based session database implemented, example [here](_examples/sessions/database/file) -- **NEW** [boltdb-based](https://github.com/boltdb/bolt) session database implemented, example [here](_examples/sessions/database/boltdb) (recommended as it's safer and faster) -- [redis sessiondb](_examples/sessions/database/redis) updated to the latest api - -Under the cover, session database works entirely differently than before but nothing changed from the user's perspective, so upgrade with `go get -u github.com/kataras/iris` and sleep well. - -# Tu, 01 August 2017 | v8.1.3 - -- Add `Option` function to the `html view engine`: https://github.com/kataras/iris/issues/694 -- Fix sessions backend databases restore expiration: https://github.com/kataras/iris/issues/692 by @corebreaker -- Add `PartyFunc`, same as `Party` but receives a function with the sub router as its argument instead [GO1.9 Users-ONLY] - -# Mo, 31 July 2017 | v8.1.2 - -Add a `ConfigureHost` function as an alternative way to customize the hosts via `host.Configurator`. -The first way was to pass `host.Configurator` as optional arguments on `iris.Runner`s built'n functions (`iris#Server, iris#Listener, iris#Addr, iris#TLS, iris#AutoTLS`), example of this can be found [there](https://github.com/kataras/iris/blob/master/_examples/http-listening/notify-on-shutdown). - -Example Code: - -```go -package main - -import ( - stdContext "context" - "time" - - "github.com/kataras/iris" - "github.com/kataras/iris/context" - "github.com/kataras/iris/core/host" -) - -func main() { - app := iris.New() - - app.Get("/", func(ctx context.Context) { - ctx.HTML("

Hello, try to refresh the page after ~10 secs

") - }) - - app.ConfigureHost(configureHost) // or pass "configureHost" as `app.Addr` argument, same result. - - app.Logger().Info("Wait 10 seconds and check your terminal again") - // simulate a shutdown action here... - go func() { - <-time.After(10 * time.Second) - timeout := 5 * time.Second - ctx, cancel := stdContext.WithTimeout(stdContext.Background(), timeout) - defer cancel() - // close all hosts, this will notify the callback we had register - // inside the `configureHost` func. - app.Shutdown(ctx) - }() - - // http://localhost:8080 - // wait 10 seconds and check your terminal. - app.Run(iris.Addr(":8080"), iris.WithoutServerError(iris.ErrServerClosed)) -} - -func configureHost(su *host.Supervisor) { - // here we have full access to the host that will be created - // inside the `app.Run` or `app.NewHost` function . - // - // we're registering a shutdown "event" callback here: - su.RegisterOnShutdown(func() { - println("server is closed") - }) - // su.RegisterOnError - // su.RegisterOnServe -} -``` - -# Su, 30 July 2017 - -Greetings my friends, nothing special today, no version number yet. - -We just improve the, external, Iris Logging library and the `Columns` config field from `middleware/logger` defaults to `false` now. Upgrade with `go get -u github.com/kataras/iris` and have fun! - -# Sa, 29 July 2017 | v8.1.1 - -No breaking changes, just an addition to make your life easier. - -This feature has been implemented after @corebreaker 's request, posted at: https://github.com/kataras/iris/issues/688. He was also tried to fix that by a [PR](https://github.com/kataras/iris/pull/689), we thanks him but the problem with that PR was the duplication and the separation of concepts, however we thanks him for pushing for a solution. The current feature's implementation gives a permant solution to host supervisor access issues. - -Optional host configurators added to all common serve and listen functions. - -Below you'll find how to gain access to the host, **the second way is the new feature.** - -### Hosts - -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 `Run` method, -there are two ways of gain access to the host supervisor, read below. - -First way 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 was closed!") -}) - -app.Run(iris.Raw(h.ListenAndServe)) -``` - -Second, and probably easier way is to use the `host.Configurator`. - -Note that this method requires an extra import statement of -"github.com/kataras/iris/core/host" when using go < 1.9, -if you're targeting on go1.9 then you can use the `iris#Supervisor` -and omit the extra host import. - -All common `Runners` we saw earlier (`iris#Addr, iris#Listener, iris#Server, iris#TLS, iris#AutoTLS`) -accept a variadic argument of `host.Configurator`, there are just `func(*host.Supervisor)`. -Therefore the `Application` gives you the rights to modify the auto-created host supervisor through these. - - -Example Code: - -```go -package main - -import ( - stdContext "context" - "time" - - "github.com/kataras/iris" - "github.com/kataras/iris/context" - "github.com/kataras/iris/core/host" -) - -func main() { - app := iris.New() - - app.Get("/", func(ctx context.Context) { - ctx.HTML("

Hello, try to refresh the page after ~10 secs

") - }) - - app.Logger().Info("Wait 10 seconds and check your terminal again") - // simulate a shutdown action here... - go func() { - <-time.After(10 * time.Second) - timeout := 5 * time.Second - ctx, cancel := stdContext.WithTimeout(stdContext.Background(), timeout) - defer cancel() - // close all hosts, this will notify the callback we had register - // inside the `configureHost` func. - app.Shutdown(ctx) - }() - - // start the server as usual, the only difference is that - // we're adding a second (optional) function - // to configure the just-created host supervisor. - // - // http://localhost:8080 - // wait 10 seconds and check your terminal. - app.Run(iris.Addr(":8080", configureHost), iris.WithoutServerError(iris.ErrServerClosed)) - -} - -func configureHost(su *host.Supervisor) { - // here we have full access to the host that will be created - // inside the `Run` function. - // - // we register a shutdown "event" callback - su.RegisterOnShutdown(func() { - println("server is closed") - }) - // su.RegisterOnError - // su.RegisterOnServe -} -``` - -Read more about listening and gracefully shutdown by navigating to: https://github.com/kataras/iris/tree/master/_examples/#http-listening - -# We, 26 July 2017 | v8.1.0 - -The `app.Logger() *logrus.Logger` was replaced with a custom implementation [[golog](https://github.com/kataras/golog)], it's compatible with the [logrus](https://github.com/sirupsen/logrus) package and other open-source golang loggers as well, because of that: https://github.com/kataras/iris/issues/680#issuecomment-316184570. - -The API didn't change much except these: - -- the new implementation does not recognise `Fatal` and `Panic` because, actually, iris never panics -- the old `app.Logger().Out = io.Writer` should be written as `app.Logger().SetOutput(io.Writer)` - -The new implementation, [golog](https://github.com/kataras/golog) is featured, **[three times faster than logrus](https://github.com/kataras/golog/tree/master/_benchmarks)** -and it completes every common usage. - -### Integration - -I understand that many of you may use logrus outside of Iris too. To integrate an external `logrus` logger just -`Install` it-- all print operations will be handled by the provided `logrus instance`. - -```go -import ( - "github.com/kataras/iris" - "github.com/sirupsen/logrus" -) - -package main(){ - app := iris.New() - app.Logger().Install(logrus.StandardLogger()) // the package-level logrus instance - // [...] -} -``` - -For more information about our new logger please navigate to: https://github.com/kataras/golog - contributions are welcomed as well! - -# Sa, 23 July 2017 | v8.0.7 - -Fix [It's true that with UseGlobal the "/path1.txt" route call the middleware but cause the prepend, the order is inversed](https://github.com/kataras/iris/issues/683#issuecomment-317229068) - -# Sa, 22 July 2017 | v8.0.5 & v8.0.6 - -No API Changes. - -### Performance - -Add an experimental [Configuration#EnableOptimizations](https://github.com/kataras/iris/blob/master/configuration.go#L170) option. - -```go -type Configuration { - // [...] - - // EnableOptimization when this field is true - // then the application tries to optimize for the best performance where is possible. - // - // Defaults to false. - EnableOptimizations bool `yaml:"EnableOptimizations" toml:"EnableOptimizations"` - - // [...] -} -``` - -Usage: - -```go -app.Run(iris.Addr(":8080"), iris.WithOptimizations) -``` - -### Django view engine - -@corebreaker pushed a [PR](https://github.com/kataras/iris/pull/682) to solve the [Problem for {%extends%} in Django Engine with embedded files](https://github.com/kataras/iris/issues/681). - -### Logger - -Remove the `vendor/github.com/sirupsen/logrus` folder, as a temporary solution for the https://github.com/kataras/iris/issues/680#issuecomment-316196126. - -#### Future versions - -The logrus will be replaced with a custom implementation, because of that: https://github.com/kataras/iris/issues/680#issuecomment-316184570. - -As far as we know, @kataras is working on this new implementation, see [here](https://github.com/kataras/iris/issues/680#issuecomment-316544906), -which will be compatible with the logrus package and other open-source golang loggers as well. - - -# Mo, 17 July 2017 | v8.0.4 - -No API changes. - -### HTTP Errors - -Fix a rare behavior: error handlers are not executed correctly -when a before-handler by-passes the order of execution, relative to the [previous feature](https://github.com/kataras/iris/blob/master/HISTORY.md#su-16-july-2017--v803). - -### Request Logger - -Add `Configuration#MessageContextKey`. Example can be found at [_examples/http_request/request-logger/main.go](https://github.com/kataras/iris/blob/master/_examples/http_request/request-logger/main.go#L48). - -# Su, 16 July 2017 | v8.0.3 - -No API changes. - -Relative issues: - -- https://github.com/kataras/iris/issues/674 -- https://github.com/kataras/iris/issues/675 -- https://github.com/kataras/iris/issues/676 - -### HTTP Errors - -Able to register a chain of Handlers (and middleware with `ctx.Next()` support like routes) for a specific error code, read more at [issues/674](https://github.com/kataras/iris/issues/674). Usage example can be found at [_examples/http_request/request-logger/main.go](https://github.com/kataras/iris/blob/master/_examples/http_request/request-logger/main.go#L41). - - -New function to register a Handler or a chain of Handlers for all official http error codes, by calling the new `app.OnAnyErrorCode(func(ctx context.Context){})`, read more at [issues/675](https://github.com/kataras/iris/issues/675). Usage example can be found at [_examples/http_request/request-logger/main.go](https://github.com/kataras/iris/blob/master/_examples/http_request/request-logger/main.go#L42). - -### Request Logger - -Add `Configuration#LogFunc` and `Configuration#Columns` fields, read more at [issues/676](https://github.com/kataras/iris/issues/676). Example can be found at [_examples/http_request/request-logger/request-logger-file/main.go](https://github.com/kataras/iris/blob/master/_examples/http_request/request-logger/request-logger-file/main.go). - - -Have fun and don't forget to [star](https://github.com/kataras/iris/stargazers) the github repository, it gives me power to continue publishing my work! - -# Sa, 15 July 2017 | v8.0.2 - -Okay my friends, this is a good time to upgrade, I did implement a feature that you were asking many times at the past. - -Iris' router can now handle root-level wildcard paths `app.Get("/{paramName:path})`. - -In case you're wondering: no it does not conflict with other static or dynamic routes, meaning that you can code something like this: - -```go -// it isn't conflicts with the rest of the static routes or dynamic routes with a path prefix. -app.Get("/{pathParamName:path}", myHandler) +// YAML marshals the "v" using the yaml marshaler and renders its result to the client. +func YAML(v interface{}) (int, error) ``` -Or even like this: - -```go -package main - -import ( - "github.com/kataras/iris" - "github.com/kataras/iris/context" -) - -func main() { - app := iris.New() - - // this works as expected now, - // will handle all GET requests - // except: - // / -> because of app.Get("/", ...) - // /other/anything/here -> because of app.Get("/other/{paramother:path}", ...) - // /other2/anything/here -> because of app.Get("/other2/{paramothersecond:path}", ...) - // /other2/static -> because of app.Get("/other2/static", ...) - // - // It isn't conflicts with the rest of the routes, without routing performance cost! - // - // i.e /something/here/that/cannot/be/found/by/other/registered/routes/order/not/matters - app.Get("/{p:path}", h) - - // this will handle only GET / - app.Get("/", staticPath) - - // this will handle all GET requests starting with "/other/" - // - // i.e /other/more/than/one/path/parts - app.Get("/other/{paramother:path}", other) - - // this will handle all GET requests starting with "/other2/" - // except /other2/static (because of the next static route) - // - // i.e /other2/more/than/one/path/parts - app.Get("/other2/{paramothersecond:path}", other2) - - // this will handle only GET /other2/static - app.Get("/other2/static", staticPath) - - app.Run(iris.Addr(":8080"), iris.WithoutServerError(iris.ErrServerClosed)) -} - -func h(ctx context.Context) { - param := ctx.Params().Get("p") - ctx.WriteString(param) -} - -func other(ctx context.Context) { - param := ctx.Params().Get("paramother") - ctx.Writef("from other: %s", param) -} - -func other2(ctx context.Context) { - param := ctx.Params().Get("paramothersecond") - ctx.Writef("from other2: %s", param) -} - -func staticPath(ctx context.Context) { - ctx.Writef("from the static path: %s", ctx.Path()) -} -``` - -If you find any bugs with this change please send me a [chat message](https://kataras.rocket.chat/channel/iris) in order to investigate it, I'm totally free at weekends. - -Have fun and don't forget to [star](https://github.com/kataras/iris/stargazers) the github repository, it gives me power to continue publishing my work! - -# Th, 13 July 2017 | v8.0.1 - -Nothing tremendous at this minor version. - -We've just added a configuration field in order to ignore errors received by the `Run` function, see below. - -[Configuration#IgnoreServerErrors](https://github.com/kataras/iris/blob/master/configuration.go#L255) -```go -type Configuration struct { - // [...] - - // IgnoreServerErrors will cause to ignore the matched "errors" - // from the main application's `Run` function. - // This is a slice of string, not a slice of error - // users can register these errors using yaml or toml configuration file - // like the rest of the configuration fields. - // - // See `WithoutServerError(...)` function too. - // - // Defaults to an empty slice. - IgnoreServerErrors []string `yaml:"IgnoreServerErrors" toml:"IgnoreServerErrors"` - - // [...] -} -``` -[Configuration#WithoutServerError](https://github.com/kataras/iris/blob/master/configuration.go#L106) -```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. -WithoutServerError(errors ...error) Configurator -``` - -By default no error is being ignored, of course. - -Example code: -[_examples/http-listening/listen-addr/omit-server-errors](https://github.com/kataras/iris/tree/master/_examples/http-listening/listen-addr/omit-server-errors) -```go -package main - -import ( - "github.com/kataras/iris" - "github.com/kataras/iris/context" -) - -func main() { - app := iris.New() - - app.Get("/", func(ctx context.Context) { - ctx.HTML("

Hello World!/

") - }) - - err := app.Run(iris.Addr(":8080"), iris.WithoutServerError(iris.ErrServerClosed)) - if err != nil { - // do something - } - // same as: - // err := app.Run(iris.Addr(":8080")) - // if err != nil && (err != iris.ErrServerClosed || err.Error() != iris.ErrServerClosed.Error()) { - // [...] - // } -} -``` - -At first we didn't want to implement something like that because it's ridiculous easy to do it manually but a second thought came to us, -that many applications are based on configuration, therefore it would be nice to have something to ignore errors -by simply string values that can be passed to the application's configuration via `toml` or `yaml` files too. - -This feature has been implemented after a request of ignoring the `iris/http#ErrServerClosed` from the `Run` function: -https://github.com/kataras/iris/issues/668 - -# Mo, 10 July 2017 | v8.0.0 - -## 📈 One and a half years with Iris and You... - -Despite the deflamations, the clickbait articles, the removed posts of mine at reddit/r/golang, the unexpected and inadequate ban from the gophers slack room by @dlsniper alone the previous week without any reason or inform, Iris is still here and will be. - -- 7070 github stars -- 749 github forks -- 1m total views at its documentation -- ~800$ at donations (there're a lot for a golang open-source project, thanks to you) -- ~550 reported bugs fixed -- ~30 community feature requests have been implemented - -## 🔥 Reborn - -As you may have heard I have huge responsibilities on my new position at Dubai nowadays, therefore I don't have the needed time to work on this project anymore. - -After a month of negotiations and searching I succeed to find a decent software engineer to continue my work on the open source community. - -The leadership of this, open-source, repository was transferred to [hiveminded](https://github.com/hiveminded), the author of iris-based [get-ion/ion](https://github.com/get-ion/ion), he actually did an excellent job on the framework, he kept the code as minimal as possible and at the same time added more features, examples and middleware(s). - -UPDATE: - -> [I am](https://github.com/hiveminded) voluntarily quiting this responsibility because I've been re-positioned as the new Lead Product Manager of the company I'm working for many years, which I accepted with honor. That's a very time consuming position I'm responsible to accomplish and deliver, therefore, I transfer all my rights back to @kataras and I resign from any copyrights over this project. My contribution clearly didn't make the difference but I owe a big "thank you" to Gerasimos for the chance he gave me and I hope the bests for him and iris. Thank you all. - -These types of projects need heart and sacrifices to continue offer the best developer experience like a paid software, please do support him as you did with me! - -## 📰 Changelog - -> app. = `app := iris.New();` **app.** - -> ctx. = `func(ctx context.Context) {` **ctx.** `}` - -### Docker - -Docker and kubernetes integration showcase, see the [iris-contrib/cloud-native-go](https://github.com/iris-contrib/cloud-native-go) repository as an example. - -### Logger - -* Logger which was an `io.Writer` was replaced with the pluggable `logrus`. - * which you still attach an `io.Writer` with `app.Logger().Out = an io.Writer`. - * iris as always logs only critical errors, you can disable them with `app.Logger().Level = iris.NoLog` - * the request logger outputs the incoming requests as INFO level. - -### Sessions - -Remove `ctx.Session()` and `app.AttachSessionManager`, devs should import and use the `sessions` package as standalone, it's totally optional, devs can use any other session manager too. [Examples here](sessions#table-of-contents). - -### Websockets - -The `github.com/kataras/iris/websocket` package does not handle the endpoint and client side automatically anymore. Example code: - -```go -func setupWebsocket(app *iris.Application) { - // create our echo websocket server - ws := websocket.New(websocket.Config{ - ReadBufferSize: 1024, - WriteBufferSize: 1024, - }) - ws.OnConnection(handleConnection) - // serve the javascript built'n client-side library, - // see weboskcets.html script tags, this path is used. - app.Any("/iris-ws.js", func(ctx context.Context) { - ctx.Write(websocket.ClientSource) - }) - - // 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()) -} -``` - -> More examples [here](websocket#table-of-contents) - -### View - -Rename `app.AttachView(...)` to `app.RegisterView(...)`. - -Users can omit the import of `github.com/kataras/iris/view` and use the `github.com/kataras/iris` package to -refer to the view engines, i.e: `app.RegisterView(iris.HTML("./templates", ".html"))` is the same as `import "github.com/kataras/iris/view" [...] app.RegisterView(view.HTML("./templates" ,".html"))`. - -> Examples [here](_examples/#view) - -### Security - -At previous versions, when you called `ctx.Remoteaddr()` Iris could parse and return the client's IP from the "X-Real-IP", "X-Forwarded-For" headers. This was a security leak as you can imagine, because the user can modify them. So we've disabled these headers by-default and add an option to add/remove request headers that are responsible to parse and return the client's real IP. - -```go -// 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. -WithRemoteAddrHeader(headerName string) Configurator // enables a header. -WithoutRemoteAddrHeader(headerName string) Configurator // disables a header. -``` -For example, if you want to enable the "CF-Connecting-IP" header (cloudflare) -you have to add the `WithRemoteAddrHeader` option to the `app.Run` function, at the end of your program. - -```go -app.Run(iris.Addr(":8080"), iris.WithRemoteAddrHeader("CF-Connecting-IP")) -// This header name will be checked when ctx.RemoteAddr() called and if exists -// it will return the client's IP, otherwise it will return the default *http.Request's `RemoteAddr` field. -``` - -### Miscellaneous - -Fix [typescript tools](typescript). - -[_examples](_examples/) folder has been ordered by feature and usage: - - contains tests on some examples - - new examples added, one of them shows how the `reuseport` feature on UNIX and BSD systems can be used to listen for incoming connections, [see here](_examples/#http-listening) - - -Replace supervisor's tasks with events, like `RegisterOnShutdown`, `RegisterOnError`, `RegisterOnServe` and fix the (unharmful) race condition when output the banner to the console. Global notifier for interrupt signals which can be disabled via `app.Run([...], iris.WithoutInterruptHandler)`, look [graceful-shutdown](_examples/http-listening/graceful-shutdown/main.go) example for more. - - -More handlers are ported to Iris (they can be used as they are without `iris.FromStd`), these handlers can be found at [iris-contrib/middleware](https://github.com/iris-contrib/middleware). Feel free to put your own there. - - -| Middleware | Description | Example | -| -----------|--------|-------------| -| [jwt](https://github.com/iris-contrib/middleware/tree/master/jwt) | Middleware checks for a JWT on the `Authorization` header on incoming requests and decodes it. | [iris-contrib/middleware/jwt/_example](https://github.com/iris-contrib/middleware/tree/master/jwt/_example) | -| [cors](https://github.com/iris-contrib/middleware/tree/master/cors) | HTTP Access Control. | [iris-contrib/middleware/cors/_example](https://github.com/iris-contrib/middleware/tree/master/cors/_example) | -| [secure](https://github.com/iris-contrib/middleware/tree/master/secure) | Middleware that implements a few quick security wins. | [iris-contrib/middleware/secure/_example](https://github.com/iris-contrib/middleware/tree/master/secure/_example/main.go) | -| [tollbooth](https://github.com/iris-contrib/middleware/tree/master/tollboothic) | Generic middleware to rate-limit HTTP requests. | [iris-contrib/middleware/tollbooth/_examples/limit-handler](https://github.com/iris-contrib/middleware/tree/master/tollbooth/_examples/limit-handler) | -| [cloudwatch](https://github.com/iris-contrib/middleware/tree/master/cloudwatch) | AWS cloudwatch metrics middleware. |[iris-contrib/middleware/cloudwatch/_example](https://github.com/iris-contrib/middleware/tree/master/cloudwatch/_example) | -| [new relic](https://github.com/iris-contrib/middleware/tree/master/newrelic) | Official [New Relic Go Agent](https://github.com/newrelic/go-agent). | [iris-contrib/middleware/newrelic/_example](https://github.com/iris-contrib/middleware/tree/master/newrelic/_example) | -| [prometheus](https://github.com/iris-contrib/middleware/tree/master/prometheus)| Easily create metrics endpoint for the [prometheus](http://prometheus.io) instrumentation tool | [iris-contrib/middleware/prometheus/_example](https://github.com/iris-contrib/middleware/tree/master/prometheus/_example) | - - -v7.x is deprecated because it sold as it is and it is not part of the public, stable `gopkg.in` iris versions. Developers/users of this library should upgrade their apps to v8.x, the refactor process will cost nothing for most of you, as the most common API remains as it was. The changelog history from that are being presented below. - - -# Th, 15 June 2017 | v7.2.0 - -### About our new home page - https://iris-go.com - -Thanks to [Santosh Anand](https://github.com/santoshanand) the https://iris-go.com has been upgraded and it's really awesome! - -[Santosh](https://github.com/santoshanand) is a freelancer, he has a great knowledge of nodejs and express js, Android, iOS, React Native, Vue.js etc, if you need a developer to find or create a solution for your problem or task, please contact with him. - - -The amount of the next two or three donations you'll send they will be immediately transferred to his own account balance, so be generous please! - -### Cache - -Declare the `iris.Cache alias` to the new, improved and most-suited for common usage, `cache.Handler function`. - -`iris.Cache` be used as middleware in the chain now, example [here](_examples/intermediate/cache-markdown/main.go). However [you can still use the cache as a wrapper](cache/cache_test.go) by importing the `github.com/kataras/iris/cache` package. - - -### File server - -- **Fix** [that](https://github.com/iris-contrib/community-board/issues/12). - -- `app.StaticHandler(requestPath string, systemPath string, showList bool, gzip bool)` -> `app.StaticHandler(systemPath,showList bool, gzip bool)` - -- **New** feature for Single Page Applications, `app.SPA(assetHandler context.Handler)` implemented. - -- **New** `app.StaticEmbeddedHandler(vdir string, assetFn func(name string) ([]byte, error), namesFn func() []string)` added in order to be able to pass that on `app.SPA(app.StaticEmbeddedHandler("./public", Asset, AssetNames))`. - -- **Fix** `app.StaticEmbedded(requestPath string, vdir string, assetFn func(name string) ([]byte, error), namesFn func() []string)`. - -Examples: -- [Embedding Files Into Executable App](_examples/file-server/embedding-files-into-app) -- [Single Page Application](_examples/file-server/single-page-application) -- [Embedding Single Page Application](_examples/file-server/embedding-single-page-application) - -> [app.StaticWeb](_examples/file-server/basic/main.go) doesn't works for root request path "/" anymore, use the new `app.SPA` instead. - -### WWW subdomain entry - -- [Example](_examples/subdomains/www/main.go) added to copy all application's routes, including parties, to the `www.mydomain.com` - - -### Wrapping the Router - -- [Example](_examples/routing/custom-wrapper/main.go) added to show you how you can use the `app.WrapRouter` -to implement a similar to `app.SPA` functionality, don't panic, it's easier than it sounds. - - -### Testing - -- `httptest.New(app *iris.Application, t *testing.T)` -> `httptest.New(t *testing.T, app *iris.Application)`. - -- **New** `httptest.NewLocalListener() net.Listener` added. -- **New** `httptest.NewLocalTLSListener(tcpListener net.Listener) net.Listener` added. - -Useful for testing tls-enabled servers: - -Proxies are trying to understand local addresses in order to allow `InsecureSkipVerify`. - -- `host.ProxyHandler(target *url.URL) *httputil.ReverseProxy`. -- `host.NewProxy(hostAddr string, target *url.URL) *Supervisor`. - - Tests [here](core/host/proxy_test.go). - -# Tu, 13 June 2017 | v7.1.1 - -Fix [that](https://github.com/iris-contrib/community-board/issues/11). - -# Mo, 12 June 2017 | v7.1.0 - -Fix [that](https://github.com/iris-contrib/community-board/issues/10). - - -# Su, 11 June 2017 | v7.0.5 - -Iris now supports static paths and dynamic paths for the same path prefix with zero performance cost: - -`app.Get("/profile/{id:int}", handler)` and `app.Get("/profile/create", createHandler)` are not in conflict anymore. - - -The rest of the special Iris' routing features, including static & wildcard subdomains are still work like a charm. - -> This was one of the most popular community's feature requests. Click [here](https://github.com/kataras/iris/blob/master/_examples/beginner/routing/overview/main.go) to see a trivial example. - -# Sa, 10 June 2017 | v7.0.4 - -- Simplify and add a test for the [basicauth middleware](https://github.com/kataras/iris/tree/master/middleware/basicauth), no need to be -stored inside the Context anymore, developers can get the validated user(username and password) via `context.Request().BasicAuth()`. `basicauth.Config.ContextKey` was removed, just remove that field from your configuration, it's useless now. - -# Sa, 10 June 2017 | v7.0.3 - -- New `context.Session().PeekFlash("key")` added, unlike `GetFlash` this will return the flash value but keep the message valid for the next requests too. -- Complete the [httptest example](https://github.com/iris-contrib/examples/tree/master/httptest). -- Fix the (marked as deprecated) `ListenLETSENCRYPT` function. -- Upgrade the [iris-contrib/middleware](https://github.com/iris-contrib/middleware) including JWT, CORS and Secure handlers. -- Add [OAuth2 example](https://github.com/iris-contrib/examples/tree/master/oauth2) -- showcases the third-party package [goth](https://github.com/markbates/goth) integration with Iris. - -### Community - - - Add github integration on https://kataras.rocket.chat/channel/iris , so users can login with their github accounts instead of creating new for the chat only. - -# Th, 08 June 2017 | v7.0.2 - -- Able to set **immutable** data on sessions and context's storage. Aligned to fix an issue on slices and maps as reported [here](https://github.com/iris-contrib/community-board/issues/5). - -# We, 07 June 2017 | v7.0.1 - -- Proof of concept of an internal release generator, navigate [here](https://github.com/iris-contrib/community-board/issues/2) to read more. -- Remove tray icon "feature", click [here](https://github.com/iris-contrib/community-board/issues/1) to learn why. - -# Sa, 03 June 2017 - -After 2+ months of hard work and collaborations, Iris [version 7](https://github.com/kataras/iris) was published earlier today. - -If you're new to Iris you don't have to read all these, just navigate to the [updated examples](https://github.com/kataras/iris/tree/master/_examples) and you should be fine:) - -Note that this section will not -cover the internal changes, the difference is so big that anybody can see them with a glimpse, even the code structure itself. - - -## Changes from [v6](https://github.com/kataras/iris/tree/v6) - -The whole framework was re-written from zero but I tried to keep the most common public API that iris developers use. - -Vendoring /w update - -The previous vendor action for v6 was done by-hand, now I'm using the [go dep](https://github.com/golang/dep) tool, I had to do -some small steps: - -- remove files like testdata to reduce the folder size -- rollback some of the "golang/x/net/ipv4" and "ipv6" source files because they are downloaded to their latest versions -by go dep, but they had lines with the `typealias` feature, which is not ready by current golang version (it will be on August) -- fix "cannot use internal package" at golang/x/net/ipv4 and ipv6 packages - - rename the interal folder to was-internal, everywhere and fix its references. -- fix "main redeclared in this block" - - remove all examples folders. -- remove main.go files on jsondiff lib, used by gavv/httpexpect, produces errors on `test -v ./...` while jd and jp folders are not used at all. - -The go dep tool does what is says, as expected, don't be afraid of it now. -I am totally recommending this tool for package authors, even if it's in its alpha state. -I remember when Iris was in its alpha state and it had 4k stars on its first weeks/or month and that helped me a lot to fix reported bugs by users and make the framework even better, so give love to go dep from today! - -General - -- Several enhancements for the typescript transpiler, view engine, websocket server and sessions manager -- All `Listen` methods replaced with a single `Run` method, see [here](https://github.com/kataras/iris/tree/master/_examples/beginner/listening) -- Configuration, easier to modify the defaults, see [here](https://github.com/kataras/iris/tree/master/_examples/beginner/cofiguration) -- `HandlerFunc` removed, just `Handler` of `func(context.Context)` where context.Context derives from `import "github.com/kataras/iris/context"` (**NEW**: this import path is optional, use `iris.Context` if you've installed Go 1.9) - - Simplify API, i.e: instead of `Handle,HandleFunc,Use,UseFunc,Done,DoneFunc,UseGlobal,UseGlobalFunc` use `Handle,Use,Done,UseGlobal`. -- Response time decreased even more (9-35%, depends on the application) -- The `Adaptors` idea replaced with a more structural design pattern, but you have to apply these changes: - - `app.Adapt(view.HTML/Pug/Amber/Django/Handlebars...)` -> `app.AttachView(view.HTML/Pug/Amber/Django/Handlebars...)` - - `app.Adapt(sessions.New(...))` -> `app.AttachSessionManager(sessions.New(...))` - - `app.Adapt(iris.LoggerPolicy(...))` -> `app.AttachLogger(io.Writer)` - - `app.Adapt(iris.RenderPolicy(...))` -> removed and replaced with the ability to replace the whole context with a custom one or override some methods of it, see below. - -Routing -- Remove of multiple routers, now we have the fresh Iris router which is based on top of the julien's [httprouter](https://github.com/julienschmidt/httprouter). - > Update 11 June 2017: As of 7.0.5 this is changed, read [here](https://github.com/kataras/iris/blob/master/HISTORY.md#su-11-june-2017--v705). -- Subdomains routing algorithm has been improved. -- Iris router is using a custom interpreter with parser and path evaluator to achieve the best expressiveness, with zero performance loss, you ever seen so far, i.e: - - `app.Get("/", "/users/{userid:int min(1)}", handler)`, - - `{username:string}` or just `{username}` - - `{asset:path}`, - - `{firstname:alphabetical}`, - - `{requestfile:file}` , - - `{mylowercaseParam regexp([a-z]+)}`. - - The previous syntax of `:param` and `*param` still working as expected. Previous rules for paths confliction remain as they were. - - Also, path parameter names should be only alphabetical now, numbers and symbols are not allowed (for your own good, I have seen a lot the last year...). - -Click [here](https://github.com/kataras/iris/tree/master/_examples/beginner/routing) for details. -> It was my first attempt/experience on the interpreters field, so be good with it :) - -Context -- `iris.Context pointer` replaced with `context.Context interface` as we already mention - - in order to be able to use a custom context and/or catch lifetime like `BeginRequest` and `EndRequest` from context itself, see below -- `context.JSON, context.JSONP, context.XML, context.Markdown, context.HTML` work faster -- `context.Render("filename.ext", bindingViewData{}, options) ` -> `context.View("filename.ext")` - - `View` renders only templates, it will not try to search if you have a restful renderer adapted, because, now, you can do it via method overriding using a custom Context. - - Able to set `context.ViewData` and `context.ViewLayout` via middleware when executing a template. -- `context.SetStatusCode(statusCode)` -> `context.StatusCode(statusCode)` - - which is equivalent with the old `EmitError` too: - - if status code >=400 given can automatically fire a custom http error handler if response wasn't written already. - - `context.StatusCode()` -> `context.GetStatusCode()` - - `app.OnError` -> `app.OnErrorCode` - - Errors per party are removed by-default, you can just use one global error handler with logic like "if path starts with 'prefix' fire this error handler, else...". -- Easy way to change Iris' default `Context` with a custom one, see [here](https://github.com/kataras/iris/tree/master/_examples/intermediate/custom-context) -- `context.ResponseWriter().SetBeforeFlush(...)` works for Flush and HTTP/2 Push, respectfully -- Several improvements under the `Request transactions` -- Remember that you had to set a status code on each of the render-relative methods? Now it's not required, it just renders -with the status code that user gave with `context.StatusCode` or with `200 OK`, i.e: - -`context.JSON(iris.StatusOK, myJSON{})` -> `context.JSON(myJSON{})`. - - Each one of the context's render methods has optional per-call settings, - - **the new API is even more easier to read, understand and use.** - -Server -- Able to set custom underline *http.Server(s) with new Host (aka Server Supervisor) feature - - `Done` and `Err` channels to catch shutdown or any errors on custom hosts, - - Schedule custom tasks(with cancelation) when server is running, see [here](https://github.com/kataras/iris/tree/master/_examples/intermediate/graceful-shutdown) -- Interrupt handler task for gracefully shutdown (when `CTRL/CMD+C`) are enabled by-default, you can disable its via configuration: `app.Run(iris.Addr(":8080"), iris.WithoutInterruptHandler)` +## Session#GetString -Future plans -- Future Go1.9's [ServeTLS](https://go-review.googlesource.com/c/38114/2/src/net/http/server.go) is ready when 1.9 released -- Future Go1.9's typealias feature is ready when 1.9 released, i.e `context.Context` -> `iris.Context` just one import path instead of todays' two. +`sessions/session#GetString` can now return a filled value even if the stored value is a type of integer, just like the memstore, the context's temp store, the context's path parameters and the context's url parameters. \ No newline at end of file diff --git a/LICENSE b/LICENSE index 1645802cdc..c9eada5001 100644 --- a/LICENSE +++ b/LICENSE @@ -1,4 +1,4 @@ -Copyright (c) 2017 The Iris Authors. All rights reserved. +Copyright (c) 2017-2018 The Iris Authors. All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are diff --git a/README.md b/README.md index 15c6ac6599..95c170ae80 100644 --- a/README.md +++ b/README.md @@ -1,112 +1,27 @@ -## I'm working hard on the [dev](https://github.com/kataras/iris/tree/dev) branch for the next release of Iris. +# Iris Web Framework -Do you remember, last Christmas? I did publish the version 6 with net/http and HTTP/2 support, and you've embraced Iris with so much love, ultimately it was a successful move. + -I tend to make surprises by giving you the most unique and useful features, especially on Christmas period. +[![build status](https://img.shields.io/travis/kataras/iris/master.svg?style=flat-square)](https://travis-ci.org/kataras/iris) [![report card](https://img.shields.io/badge/report%20card-a%2B-ff3333.svg?style=flat-square)](http://goreportcard.com/report/kataras/iris) [![chat](https://img.shields.io/badge/community-%20chat-00BCD4.svg?style=flat-square)](https://kataras.rocket.chat/channel/iris) [![view examples](https://img.shields.io/badge/learn%20by-examples-0077b3.svg?style=flat-square)](_examples/) [![release](https://img.shields.io/badge/release%20-v10.0-0077b3.svg?style=flat-square)](https://github.com/kataras/iris/releases) -This year, I intend to give you more gifts. +Iris is a fast, simple yet fully featured and very efficient web framework for Go. -Don't worry, it will not contain any breaking changes, except of some MVC concepts that are re-designed. +Iris provides a beautifully expressive and easy to use foundation for your next website or API. -The new Iris' MVC Ecosystem is ready on the [dev/mvc](https://github.com/kataras/iris/tree/dev/mvc). It contains features that you've never saw before, in any programming language framework. It is also, by far, the fastest MVC implementation ever created, very close to raw handlers - it's Iris, it's superior, we couldn't expect something different after all :) Treat that with respect as it treats you :) +Finally, a real expressjs equivalent for the Go Programming Language. -I'm doing my bests to get it ready before Christmas. +Learn what [others say about Iris](#support) and [star](https://github.com/kataras/iris/stargazers) this github repository to stay [up to date](https://facebook.com/iris.framework). -Star or watch the repository to stay up to date and get ready for the most amazing features! +## Backers -Yours faithfully, [Gerasimos Maropoulos](https://twitter.com/MakisMaropoulos). +Thank you to all our backers! [Become a backer](https://opencollective.com/iris#backer) ------- - -# [![Logo created by @santoshanand](logo_white_35_24.png)](https://iris-go.com) Iris - -[![build status](https://img.shields.io/travis/kataras/iris/master.svg?style=flat-square)](https://travis-ci.org/kataras/iris)[![report card](https://img.shields.io/badge/report%20card-a%2B-ff3333.svg?style=flat-square)](http://goreportcard.com/report/kataras/iris)[![github closed issues](https://img.shields.io/github/issues-closed-raw/kataras/iris.svg?style=flat-square)](https://github.com/kataras/iris/issues?q=is%3Aissue+is%3Aclosed)[![release](https://img.shields.io/github/release/kataras/iris.svg?style=flat-square)](https://github.com/kataras/iris/releases)[![view examples](https://img.shields.io/badge/learn%20by-examples-0077b3.svg?style=flat-square)](https://github.com/kataras/iris/tree/master/_examples)[![chat](https://img.shields.io/badge/community-%20chat-00BCD4.svg?style=flat-square)](https://kataras.rocket.chat/channel/iris)[![CLA assistant](https://cla-assistant.io/readme/badge/kataras/iris?style=flat-square)](https://cla-assistant.io/kataras/iris) - -Iris is a fast, simple and efficient web framework for Go. - -Iris provides a beautifully expressive and easy to use foundation for your next website, API, or distributed app. - -Learn what [others say about Iris](https://www.youtube.com/watch?v=jGx0LkuUs4A) and [star](https://github.com/kataras/iris/stargazers) this github repository to stay [up to date](https://facebook.com/iris.framework). - -[![Iris vs .NET Core(C#) vs Node.js (Express)](https://iris-go.com/images/benchmark-new.png)](_benchmarks/README_UNIX.md) - -
-Benchmarks from third-party source over the rest web frameworks - -![Comparison with other frameworks](https://raw.githubusercontent.com/smallnest/go-web-framework-benchmark/4db507a22c964c9bc9774c5b31afdc199a0fe8b7/benchmark.png) - -_Updated at: [Tuesday, 21 November 2017](_benchmarks/README_UNIX.md)_ -
- -## Built with ♥️ - -We have no doubt you will able to find other web frameworks written in Go -and even put up a real fight to learn and use them for quite some time but -make no mistake, sooner or later you will be using Iris, not because of the ergonomic, high-performant solution that it provides but its well-documented unique features, as these will transform you to a real rockstar geek. - -No matter what you're trying to build, Iris covers -every type of application, from micro services to large monolithic web applications. -It's actually the best piece of software for back-end web developers -you can find online. - -Iris may have reached version 8, but we're not stopping there. We have many feature ideas on our board that we're anxious to add and other innovative web development solutions that we're planning to build into Iris. - -Accelerated by [KeyCDN](https://www.keycdn.com/), a simple, fast and reliable CDN. - -We are developing this project using the best code editor for Golang; [Visual Studio Code](https://code.visualstudio.com/) supported by [Microsoft](https://www.microsoft.com). - -If you're coming from [nodejs](https://nodejs.org) world, Iris is the [expressjs](https://github.com/expressjs/express) equivalent for Gophers. - -## Table Of Content - -* [Installation](#installation) -* [Latest changes](https://github.com/kataras/iris/blob/master/HISTORY.md#th-09-november-2017--v858) -* [Getting started](#getting-started) -* [Learn](_examples/) - * [MVC (Model View Controller)](_examples/#mvc) **NEW** - * [Structuring](_examples/#structuring) **NEW** - * [HTTP Listening](_examples/#http-listening) - * [Configuration](_examples/#configuration) - * [Routing, Grouping, Dynamic Path Parameters, "Macros" and Custom Context](_examples/#routing-grouping-dynamic-path-parameters-macros-and-custom-context) - * [Subdomains](_examples/#subdomains) - * [Wrap `http.Handler/HandlerFunc`](_examples/#convert-httphandlerhandlerfunc) - * [View](_examples/#view) - * [Authentication](_examples/#authentication) - * [File Server](_examples/#file-server) - * [How to Read from `context.Request() *http.Request`](_examples/#how-to-read-from-contextrequest-httprequest) - * [How to Write to `context.ResponseWriter() http.ResponseWriter`](_examples/#how-to-write-to-contextresponsewriter-httpresponsewriter) - * [Test](_examples/#testing) - * [Cache](_examples/#caching) - * [Sessions](_examples/#sessions) - * [Websockets](_examples/#websockets) - * [Miscellaneous](_examples/#miscellaneous) - * [POC: Convert the medium-sized project "Parrot" from native to Iris](https://github.com/iris-contrib/parrot) - * [POC: Isomorphic react/hot reloadable/redux/css-modules starter kit](https://github.com/kataras/iris-starter-kit) - * [Typescript Automation Tools](typescript/#table-of-contents) - * [Tutorial: A URL Shortener Service using Go, Iris and Bolt](https://medium.com/@kataras/a-url-shortener-service-using-go-iris-and-bolt-4182f0b00ae7) - * [Tutorial: Online Visitors](_examples/tutorial/online-visitors) - * [Tutorial: Caddy](_examples/tutorial/caddy) - * [Tutorial: DropzoneJS Uploader](https://hackernoon.com/how-to-build-a-file-upload-form-using-dropzonejs-and-go-8fb9f258a991) - * [Tutorial:Iris Go Framework + MongoDB](https://medium.com/go-language/iris-go-framework-mongodb-552e349eab9c) -* [Middleware](middleware/) -* [Dockerize](https://github.com/iris-contrib/cloud-native-go) -* [Contributing](CONTRIBUTING.md) -* [FAQ](FAQ.md) -* [What's next?](#now-you-are-ready-to-move-to-the-next-step-and-get-closer-to-becoming-a-pro-gopher) -* [People](#people) - -## Installation - -The only requirement is the [Go Programming Language](https://golang.org/dl/) + ```sh -$ go get -u github.com/kataras/iris +$ cat example.go ``` -Iris takes advantage of the [vendor directory](https://docs.google.com/document/d/1Bz5-UB7g2uPBdOx-rw5t9MxJwkfpx90cqG9AFL0JAYo) feature. You get truly reproducible builds, as this method guards against upstream renames and deletes. - -## Getting Started - ```go package main @@ -132,8 +47,8 @@ func main() { // Resource: http://localhost:8080/user/42 // // Need to use a custom regexp instead? - // Easy; - // Just mark the parameter's type to 'string' + // Easy, + // just mark the parameter's type to 'string' // which accepts anything and make use of // its `regexp` macro function, i.e: // app.Get("/user/{id:string regexp(^[0-9]+$)}") @@ -147,7 +62,7 @@ func main() { } ``` -> Learn more about path parameter's types by clicking [here](https://github.com/kataras/iris/blob/master/_examples/routing/dynamic-path/main.go#L31). +> Learn more about path parameter's types by clicking [here](_examples/routing/dynamic-path/main.go#L31) ```html @@ -161,869 +76,130 @@ func main() { ``` -![overview screen](https://github.com/kataras/build-a-better-web-together/raw/master/overview_screen_1.png) - -> Wanna re-start your app automatically when source code changes happens? Install the [rizla](https://github.com/kataras/rizla) tool and run `rizla main.go` instead of `go run main.go`. - -Guidelines for bootstrapping applications can be found at the [_examples/structuring](_examples/#structuring). - -### Quick MVC Tutorial - -```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) -} -``` - -> The [_examples/mvc](_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: " "}) -} +```sh +$ go run example.go +Now listening on: http://localhost:8080 +Application Started. Press CTRL+C to shut down. +_ ``` -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") +## Installation - return c.Service.UpdatePosterAndGenreByID(id, poster, genre) -} +The only requirement is the [Go Programming Language](https://golang.org/dl/) -// 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 -} +```sh +$ go get -u github.com/kataras/iris ``` -```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 - // } - } +Iris takes advantage of the [vendor directory](https://docs.google.com/document/d/1Bz5-UB7g2uPBdOx-rw5t9MxJwkfpx90cqG9AFL0JAYo) feature. You get truly reproducible builds, as this method guards against upstream renames and deletes. - // return mvc.Response{Text: "Hello " + name} OR: - return mvc.View{ - Name: "hello/name.html", - Data: name, - } -} -``` +[![Iris vs .NET Core(C#) vs Node.js (Express)](https://iris-go.com/images/benchmark-new-gray.png)](_benchmarks/README_UNIX.md) -```go -// file: web/middleware/basicauth.go +_Updated at: [Tuesday, 21 November 2017](_benchmarks/README_UNIX.md)_ -package middleware +
+Benchmarks from third-party source over the rest web frameworks -import "github.com/kataras/iris/middleware/basicauth" +![Comparison with other frameworks](https://raw.githubusercontent.com/smallnest/go-web-framework-benchmark/4db507a22c964c9bc9774c5b31afdc199a0fe8b7/benchmark.png) -// BasicAuth middleware sample. -var BasicAuth = basicauth.New(basicauth.Config{ - Users: map[string]string{ - "admin": "password", - }, -}) -``` +
-```html - - +## Support - - {{.Title}} - My App - +- [HISTORY](HISTORY.md#mo-01-jenuary-2018--v1000) file is your best friend, it contains information about the latest features and changes +- Did you happen to find a bug? Post it at [github issues](https://github.com/kataras/iris/issues) +- Do you have any questions or need to speak with someone experienced to solve a problem at real-time? Join us to the [community chat](https://chat.iris-go.com) +- Complete our form-based user experience report by clicking [here](https://docs.google.com/forms/d/e/1FAIpQLSdCxZXPANg_xHWil4kVAdhmh7EBBHQZ_4_xSZVDL-oCC_z5pA/viewform?usp=sf_link) +- Do you like the framework? Tweet something about it! The People have spoken: - -

{{.MyMessage}}

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

Hello {{.}}

- + + + - -``` + + + -> Navigate to the [_examples/view](_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 +For more information about contributing to the Iris project please check the [CONTRIBUTING.md](CONTRIBUTING.md) file. -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" +[List of all Contributors](https://github.com/kataras/iris/graphs/contributors) - "github.com/kataras/iris" -) +## Learn -func main() { - app := iris.New() +First of all, the most correct way to begin with a web framework is to learn the basics of the programming language and the standard `http` capabilities, if your web application is a very simple personal project without performance and maintainability requirements you may want to proceed just with the standard packages. After that follow the guidelines: - // Load the template files. - app.RegisterView(iris.HTML("./web/views", ".html")) +- Navigate through **100+1** **[examples](_examples)** and some [iris starter kits](#iris-starter-kits) we crafted for you +- Read the [godocs](https://godoc.org/github.com/kataras/iris) for any details +- Prepare a cup of coffee or tea, whatever pleases you the most, and read some [articles](#articles) we found for you - // Register our controllers. - app.Controller("/hello", new(controllers.HelloController)) +### Iris starter kits - // 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) +1. [A basic web app built in Iris for Go](https://github.com/gauravtiwari/go_iris_app) +2. [A mini social-network created with the awesome Iris💖💖](https://github.com/iris-contrib/Iris-Mini-Social-Network) +3. [Iris isomorphic react/hot reloadable/redux/css-modules starter kit](https://github.com/iris-contrib/iris-starter-kit) +4. [Demo project with react using typescript and Iris](https://github.com/ionutvilie/react-ts) +5. [Self-hosted Localization Management Platform built with Iris and Angular](https://github.com/iris-contrib/parrot) +6. [Iris + Docker and Kubernetes](https://github.com/iris-contrib/cloud-native-go) +7. [Quickstart for Iris with Nanobox](https://guides.nanobox.io/golang/iris/from-scratch) +8. [A Hasura starter project with a ready to deploy Golang hello-world web app with IRIS](https://hasura.io/hub/project/hasura/hello-golang-iris) - // 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 - ) -} -``` +> Did you build something similar? Let us [know](https://github.com/kataras/iris/pulls)! -More folder structure guidelines can be found at the [_examples/#structuring](_examples/#structuring) section. +### Middleware -## Now you are ready to move to the next step and get closer to becoming a pro gopher +Iris has a great collection of handlers[[1]](middleware/)[[2]](https://github.com/iris-contrib/middleware) that you can use side by side with your web apps. However you are not limited to them - you are free to use any third-party middleware that is compatible with the [net/http](https://golang.org/pkg/net/http/) package, [_examples/convert-handlers](_examples/convert-handlers) will show you the way. -Congratulations, since you've made it so far, we've crafted just for you some next level content to turn you into a real pro gopher 😃 +Iris, unlike others, is 100% compatible with the standards and that's why the majority of the big companies that adapt Go to their workflow, like a very famous US Television Network, trust Iris; it's always up-to-date and it will be aligned with the std `net/http` package which is modernized by the Go Author on each new release of the Go Programming Language forever. -> Don't forget to prepare yourself a cup of coffee, or tea, whatever enjoys you the most! +### Articles +* [A Hasura starter project with a ready to deploy Golang hello-world web app with IRIS](bit.ly/2lmKaAZ) * [Top 6 web frameworks for Go as of 2017](https://blog.usejournal.com/top-6-web-frameworks-for-go-as-of-2017-23270e059c4b) * [Iris Go Framework + MongoDB](https://medium.com/go-language/iris-go-framework-mongodb-552e349eab9c) * [How to build a file upload form using DropzoneJS and Go](https://hackernoon.com/how-to-build-a-file-upload-form-using-dropzonejs-and-go-8fb9f258a991) @@ -1035,55 +211,19 @@ Congratulations, since you've made it so far, we've crafted just for you some ne * [Deploying a Iris Golang app in hasura](https://medium.com/@HasuraHQ/deploy-an-iris-golang-app-with-backend-apis-in-minutes-25a559bf530b) * [A URL Shortener Service using Go, Iris and Bolt](https://medium.com/@kataras/a-url-shortener-service-using-go-iris-and-bolt-4182f0b00ae7) -## People - -The author of Iris is [@kataras](https://github.com/kataras), you can reach him via; - -* [Medium](https://medium.com/@kataras) -* [Twitter](https://twitter.com/makismaropoulos) -* [Dev.to](https://dev.to/@kataras) -* [Facebook](https://facebook.com/iris.framework) -* [Mail](mailto:kataras2006@hotmail.com?subject=Iris%20I%20need%20some%20help%20please) - -[List of all Authors](AUTHORS) - -[List of all Contributors](https://github.com/kataras/iris/graphs/contributors) - -Help this project to continue deliver awesome and unique features with the higher code quality as possible by donating any amount via [PayPal](https://www.paypal.me/kataras) or [BTC](https://iris-go.com/v8/donate). - -For more information about contributing to the Iris project please check the [CONTRIBUTING.md file](CONTRIBUTING.md). - -### We need your help with translations into your native language - -Iris needs your help, please think about contributing to the translation of the [README](README.md) and https://iris-go.com, you will be rewarded. - -Instructions can be found at: https://github.com/kataras/iris/issues/796 - -### 03, October 2017 | Iris User Experience Report - -Be part of the **first** Iris User Experience Report by submitting a simple form, it won't take more than **2 minutes**. - -The form contains some questions that you may need to answer in order to learn more about you; learning more about you helps us to serve you with the best possible way! +### Get hired -https://docs.google.com/forms/d/e/1FAIpQLSdCxZXPANg_xHWil4kVAdhmh7EBBHQZ_4_xSZVDL-oCC_z5pA/viewform?usp=sf_link +There are many companies and start-ups looking for Go web developers with Iris experience as requirement, we are searching for you every day and we post those information via our [facebook page](https://www.facebook.com/iris.framework), like the page to get notified, we have already posted some of them. -## Sponsors +### Sponsors -Support this project by becoming a sponsor. Your logo will show up here with a link to your website. [Become a sponsor](https://opencollective.com/iris#sponsor) +Thank you to all our sponsors! (please ask your company to also support this open source project by [becoming a sponsor](https://opencollective.com/iris#sponsor)) - - - - - - - - ## License -Iris is licensed under the 3-Clause BSD [License](LICENSE). Iris is 100% open-source software. +Iris is licensed under the [3-Clause BSD License](LICENSE). Iris is 100% free and open-source software. -For any questions regarding the license please [contact us](mailto:kataras2006@hotmail.com?subject=Iris%20License). +For any questions regarding the license please send [e-mail](mailto:kataras2006@hotmail.com?subject=Iris%20License). diff --git a/README_ZH.md b/README_ZH.md index a4718c1e28..91a5d87cab 100644 --- a/README_ZH.md +++ b/README_ZH.md @@ -1,90 +1,24 @@ -# [![Logo created by @santoshanand](logo_white_35_24.png)](https://iris-go.com) Iris +# Iris Web Framework -[![build status](https://img.shields.io/travis/kataras/iris/master.svg?style=flat-square)](https://travis-ci.org/kataras/iris)[![Backers on Open Collective](https://opencollective.com/iris/backers/badge.svg?style=flat-square)](#backers)[![Sponsors on Open Collective](https://opencollective.com/iris/sponsors/badge.svg?style=flat-square)](#sponsors)[![report card](https://img.shields.io/badge/report%20card-a%2B-ff3333.svg?style=flat-square)](http://goreportcard.com/report/kataras/iris)[![github closed issues](https://img.shields.io/github/issues-closed-raw/kataras/iris.svg?style=flat-square)](https://github.com/kataras/iris/issues?q=is%3Aissue+is%3Aclosed)[![release](https://img.shields.io/github/release/kataras/iris.svg?style=flat-square)](https://github.com/kataras/iris/releases)[![view examples](https://img.shields.io/badge/learn%20by-examples-0077b3.svg?style=flat-square)](https://github.com/kataras/iris/tree/master/_examples)[![chat](https://img.shields.io/badge/community-%20chat-00BCD4.svg?style=flat-square)](https://kataras.rocket.chat/channel/iris)[![CLA assistant](https://cla-assistant.io/readme/badge/kataras/iris?style=flat-square)](https://cla-assistant.io/kataras/iris) - - - Sponsor - + +[![build status](https://img.shields.io/travis/kataras/iris/master.svg?style=flat-square)](https://travis-ci.org/kataras/iris) [![report card](https://img.shields.io/badge/report%20card-a%2B-ff3333.svg?style=flat-square)](http://goreportcard.com/report/kataras/iris) [![chat](https://img.shields.io/badge/community-%20chat-00BCD4.svg?style=flat-square)](https://kataras.rocket.chat/channel/iris) [![view examples](https://img.shields.io/badge/learn%20by-examples-0077b3.svg?style=flat-square)](_examples/) [![release](https://img.shields.io/badge/release%20-v10.0-0077b3.svg?style=flat-square)](https://github.com/kataras/iris/releases) Iris是一个超快、简单并且高效的Go语言Web开发框架。 Iris功能很强大,使用又很简单,它将会是你下一个网站、API服务或者分布式应用基础框架的不二之选。 -看看[别人是如何评价Iris](https://www.youtube.com/watch?v=jGx0LkuUs4A),同时欢迎各位[成为Iris星探](https://github.com/kataras/iris/stargazers),或者关注[Iris facebook主页](https://facebook.com/iris.framework)。 - -[![Iris vs .NET Core(C#) vs Node.js (Express)](https://iris-go.com/images/benchmark-new-gray.png)](_benchmarks) - -
-上图是第三发机构发布的REST Web框架的基准测试 - -![Comparison with other frameworks](https://raw.githubusercontent.com/smallnest/go-web-framework-benchmark/4db507a22c964c9bc9774c5b31afdc199a0fe8b7/benchmark.png) - -更新于: [2017年9月29,星期五](_benchmarks)_ -
- -## 前言 ♥️ - -在发现Iris之前,我想你一定也看过其它Go Web开发框架。也许你已经摩拳擦掌并马上要使用了,但我会很遗憾的告诉你,将来你还是会使用Iris的。不仅仅因为Iris性能卓越和使用简单,更重要的是Iris独树一帜,他可以让你成为真正的极客界摇滚明星。 +看看[别人是如何评价Iris](#support),同时欢迎各位[成为Iris星探](https://github.com/kataras/iris/stargazers),或者关注[Iris facebook主页](https://facebook.com/iris.framework)。 -不管你是想开发微服务或者大型Web应用,Iris都能满足你的需求。Iris可能是你在网上能找到最好的Web后台开发软件之一了。 +## Backers -Iris现在已经到第8版了,但是我们从未停止开发。有很多非常棒的功能已经提上开发日程了,而且我们非常乐意加入很多有创意的想法。 +感谢所有的支持者! [成为一个支持者](https://opencollective.com/iris#backer) -如果你想用CDN加速,我推荐用[KeyCDN](https://www.keycdn.com/),因为KeyCDN简单、速度快而且稳定。 - -我们用[微软](https://www.microsoft.com)开发的[Visual Studio Code](https://code.visualstudio.com/)来开发Golang应用。 - -如果你之前使用[nodejs](https://nodejs.org)做开发,恭喜你,Iris使用基本和[expressjs](https://github.com/expressjs/express)一样。 - -## 内容列表 - -* [安装](#installation) -* [最近更新](https://github.com/kataras/iris/blob/master/HISTORY.md#tu-07-november-2017--v857) -* [快速入门](#getting-started) -* [进阶](_examples/) - * [MVC (模型 视图 控制器)](_examples/#mvc) **NEW** - * [结构](_examples/#structuring) **NEW** - * [HTTP 监听](_examples/#http-listening) - * [配置](_examples/#configuration) - * [路由,分组,动态参数,“宏定义”已经自定义Context](_examples/#routing-grouping-dynamic-path-parameters-macros-and-custom-context) - * [子域名处理](_examples/#subdomains) - * [`http.Handler/HandlerFunc` 使用](_examples/#convert-httphandlerhandlerfunc) - * [视图处理](_examples/#view) - * [认证](_examples/#authentication) - * [文件服务器](_examples/#file-server) - * [如何从`context.Request() *http.Request` 读数据](_examples/#how-to-read-from-contextrequest-httprequest) - * [如何给`context.ResponseWriter() http.ResponseWriter`写数据](_examples/#how-to-write-to-contextresponsewriter-httpresponsewriter) - * [测试](_examples/#testing) - * [缓存](_examples/#caching) - * [会话](_examples/#sessions) - * [Websockets](_examples/#websockets) - * [其它杂项](_examples/#miscellaneous) - * [将"Parrot"项目转换为Iris实现](https://github.com/iris-contrib/parrot) - * [Iris和react/hot reloadable/redux/css-modules配合使用](https://github.com/kataras/iris-starter-kit) - * [Typescript自动化操作工具](typescript/#table-of-contents) - * [指南: 用Iris和Bolt实现短连接服务](https://medium.com/@kataras/a-url-shortener-service-using-go-iris-and-bolt-4182f0b00ae7) - * [指南: 如何统计在线访问人数](_examples/tutorial/online-visitors) - * [指南: Caddy](_examples/tutorial/caddy) - * [指南: 如何用DropzoneJS上传文件](https://hackernoon.com/how-to-build-a-file-upload-form-using-dropzonejs-and-go-8fb9f258a991) - * [指南: Iris+MongoDB](https://medium.com/go-language/iris-go-framework-mongodb-552e349eab9c) -* [中间件](middleware/) -* [Docker例子](https://github.com/iris-contrib/cloud-native-go) -* [贡献](CONTRIBUTING.md) -* [常见问题](FAQ.md) -* [更新计划?](#now-you-are-ready-to-move-to-the-next-step-and-get-closer-to-becoming-a-pro-gopher) -* [开发者](#people) - -## 安装 - -仅仅依赖[Go语言](https://golang.org/dl/) + ```sh -$ go get -u github.com/kataras/iris +$ cat example.go ``` -Iris使用[vendor](https://docs.google.com/document/d/1Bz5-UB7g2uPBdOx-rw5t9MxJwkfpx90cqG9AFL0JAYo) 包依赖管理方式。vendor包管理的方式可以有效处理包依赖更新问题 - -## 入门 ```go package main @@ -93,37 +27,37 @@ import "github.com/kataras/iris" func main() { app := iris.New() -    // 从"./views"目录加载HTML模板 -    // 模板解析html后缀文件 -    // 此方式是用`html/template`标准包(Iris的模板引擎) + // 从"./views"目录加载HTML模板 + // 模板解析html后缀文件 + // 此方式是用`html/template`标准包(Iris的模板引擎) app.RegisterView(iris.HTML("./views", ".html")) -    // HTTP方法: GET -    // 路径: http://localhost:8080 + // HTTP方法: GET + // 路径: http://localhost:8080 app.Get("/", func(ctx iris.Context) { -        // {{.message}} 和 "Hello world!" 字串绑定 + // {{.message}} 和 "Hello world!" 字串绑定 ctx.ViewData("message", "Hello world!") -        // 映射HTML模板文件路径 ./views/hello.html + // 映射HTML模板文件路径 ./views/hello.html ctx.View("hello.html") }) // HTTP方法: GET // 路径: http://localhost:8080/user/42 // -    // 想在路径中用正则吗?So easy! -    // 如下所示 -    // app.Get("/user/{id:string regexp(^[0-9]+$)}") + // 想在路径中用正则吗? + // 如下所示 + // app.Get("/user/{id:string regexp(^[0-9]+$)}") app.Get("/user/{id:long}", func(ctx iris.Context) { userID, _ := ctx.Params().GetInt64("id") ctx.Writef("User ID: %d", userID) }) -    // 绑定端口并启动服务. + // 绑定端口并启动服务. app.Run(iris.Addr(":8080")) } ``` -> 想要了解更多关于路径参数配置,戳[这里](https://github.com/kataras/iris/blob/master/_examples/routing/dynamic-path/main.go#L31). +> 想要了解更多关于路径参数配置,戳[这里](_examples/routing/dynamic-path/main.go#L31)。 ```html @@ -138,920 +72,151 @@ func main() { ``` ```sh -$ go run main.go -> 在这里监听服务: http://localhost:8080 -> 应用已经启动按键 CTRL+C 停止服务 -``` - -> 想要实现当代码改变后自动重启应用吗?那就装个[rizla](https://github.com/kataras/rizla)工具,启动go文件用 `rizla main.go` 来代替 `go run main.go`。 - -Iris的一些开发约定可以看看这里[_examples/structuring](_examples/#structuring)。 - -### MVC指南 - -```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) -} - -``` -> [_examples/mvc](_examples/mvc) 和 [mvc/controller_test.go](https://github.com/kataras/iris/blob/master/mvc/controller_test.go) 两个简单的例子可以让你更好的了解 Iris MVC 的使用方式 - -每一个在controller中导出的Go方法名都和HTTP方法(`Get`, `Post`, `Put`, `Delete`...) 一一对应 - -在Web应用中一个HTTP访问的资源就是一个URL(统一资源定位符),比如`http://localhost:8080/helloworld`是由HTTP协议、Web服务网络位置(包括TCP端口):`localhost:8080`以及资源名称URI(统一资源标志符) `/helloworld`组成的。 - -上面例子第一个方法映射到[HTTP GET](https://www.w3schools.com/tags/ref_httpmethods.asp)方法,访问资源是"/helloworld",第三个方法映射到[HTTP GET](https://www.w3schools.com/tags/ref_httpmethods.asp)方法,访问资源是"/helloworld/welcome" - - -Controller在处理`GetBy`方法时可以识别路径‘name’参数,`GetWelcomeBy`方法可以识别路径‘name’和‘numTimes’参数,因为Controller在识别`By`关键字后可以动态灵活的处理路由;上面第四个方法指示使用 [HTTP GET](https://www.w3schools.com/tags/ref_httpmethods.asp)方法,而且只处理以"/helloworld/welcome"开头的资源位置路径,并且此路径还得包括两部分,第一部分类型没有限制,第二部分只能是数字类型,比如"http://localhost:8080/helloworld/welcome/golang/32719" 是合法的,其它的就会给客户端返回[404 找不到](https://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html#sec10.4.5)的提示 - - -### MVC 快速指南 2 - -Iris对MVC的支持非常**棒[看看基准测试](_benchmarks)** ,Iris通过方法的返回值,可以给客户端返回任意类型的数据: - -* 如果返回的是 `string` 类型,就直接给客户端返回字符串 -* 如果第二个返回值是 `string` 类型,那么这个值就是ContentType(HTTP header)的值 -* 如果返回的是 `int` 类型,这个值就是HTTP状态码 -* 如果返回 `error` 值不是空,Iris 将会把这个值作为HTTP 400页面的返回值内容 -*  如果返回 `(int, error)` 类型,并且error不为空,那么Iris返回error的内容,同时把 `int` 值作为HTTP状态码 -* 如果返回 `bool` 类型,并且值是 false ,Iris直接返回404页面 -* 如果返回自定义` struct` 、 `interface{}` 、 `slice` 及 `map` ,Iris 将按照JSON的方式返回,注意如果第二个返回值是 `string`,那么Iris就按照这个 `string` 值的ContentType处理了(不一定是'application/json') -*  如果 `mvc.Result` 调用了 `Dispatch` 函数, 就会按照自己的逻辑重新处理 - -下面这些例子仅供参考,生产环境谨慎使用 - -```go -package main - -import ( - "github.com/kataras/iris" - "github.com/kataras/iris/middleware/basicauth" - "github.com/kataras/iris/mvc" -) - -// Movie 是自定义数据结构 -type Movie struct { - Name string `json:"name"` - Year int `json:"year"` - Genre string `json:"genre"` - Poster string `json:"poster"` -} - -// movies 对象模拟数据源 -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 是 /movies controller. -type MoviesController struct { - mvc.C -} - -// 返回 movies列表 -// 例子: -// curl -i http://localhost:8080/movies -func (c *MoviesController) Get() []Movie { - return movies -} - -// GetBy 返回一个 movie -// 例子: -// curl -i http://localhost:8080/movies/1 -func (c *MoviesController) GetBy(id int) Movie { - return movies[id] -} - -// PutBy 更新一个 movie -// 例子: -// 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 { -    // 获取一个 movie - m := movies[id] - -    // 获取一个poster文件 -    file, info, err := c.Ctx.FormFile("poster") - if err != nil { - c.Ctx.StatusCode(iris.StatusInternalServerError) - return Movie{} - } -    file.Close()           // 我们不需要这个文件 -    poster := info.Filename // 比如这就是上传的文件url - genre := c.Ctx.FormValue("genre") - -    // 更新poster - m.Poster = poster - m.Genre = genre - movies[id] = m - - return m -} - -// DeleteBy 删除一个 movie -// 例子: -// curl -i -X DELETE -u admin:password http://localhost:8080/movies/1 -func (c *MoviesController) DeleteBy(id int) iris.Map { -    //从movies slice中删除索引 -    deleted := movies[id].Name - movies = append(movies[:id], movies[id+1:]...) -    // 返回删除movie的名称 -    return iris.Map{"deleted": deleted} -} -``` - -### MVC 快速指南 3 - -Iris是一个底层的Web开发框架,如果你喜欢按 **目录结构** 的约定方式开发,那么Iris框架对此毫无影响。 - -你可以根据自己的需求来创建目录结构,但是我建议你还是最好看看如下的目录结构例子: - -[![目录结构例子](_examples/mvc/overview/folder_structure.png)](_examples/mvc/overview) - -好了,直接上代码。 - - -#### 数据模型层 - -```go -// file: datamodels/movie.go - -package datamodels - -// Movie是我们例子数据结构 -// 此Movie可能会定义在类似"web/viewmodels/movie.go"的文件 -// Movie的数据模型在应用中只有一个,这样使用就很简单了 -type Movie struct { - ID int64 `json:"id"` - Name string `json:"name"` - Year int `json:"year"` - Genre string `json:"genre"` - Poster string `json:"poster"` -} -``` - -#### 数据层 / 数据存储层 - -```go -// file: datasource/movies.go - -package datasource - -import "github.com/kataras/iris/_examples/mvc/overview/datamodels" - -// Movies是模拟的数据源 -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", - }, -} -``` - -#### 数据仓库 - -数据仓库层直接访问数据源 - -```go -// file: repositories/movie_repository.go - -package repositories - -import ( - "errors" - "sync" - - "github.com/kataras/iris/_examples/mvc/overview/datamodels" -) - -// Query 是数据访问的集合入口 -type Query func(datamodels.Movie) bool - -// MovieRepository 中会有对movie实体的基本操作 -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 返回movie内存数据 -func NewMovieRepository(source map[int64]datamodels.Movie) MovieRepository { - return &movieMemoryRepository{source: source} -} - -// movieMemoryRepository 就是 "MovieRepository",它管理movie的内存数据 -type movieMemoryRepository struct { - source map[int64]datamodels.Movie - mu sync.RWMutex -} - -const ( -    // 只读模式 - ReadOnlyMode = iota -    // 读写模式 - 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方法返回从模拟数据源找出的一个movie数据。 -// 当找到时就返回true,并停止迭代 -// -// Select 将会返回查询到的最新找到的movie数据,这样可以减少代码量 -// -// 自从我第一次想到用这种简单的原型函数后,我就经常用它了,希望这也对你有用 -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) - -    // 如果没有找到就让datamodels.Movie为空 -    // set an empty datamodels.Movie if not found at all. - if !found { - movie = datamodels.Movie{} - } - - return -} - -// 如果要查找很多值,用法基本一致,不过会返回datamodels.Movie slice。 -// 如果limit<=0,将返回全部数据 -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 -} - -// 插入或更新数据 -// -// 返回一个新的movie对象和error对象 -func (r *movieMemoryRepository) InsertOrUpdate(movie datamodels.Movie) (datamodels.Movie, error) { - id := movie.ID - - if id == 0 { // Create new action - var lastID int64 -        // 为了数据不重复,找到最大的ID。 -        // 生产环境你可以用第三方库生成一个UUID字串 - 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 - } -    //通过movie.ID更新数据 -    //这里举个例子看如果更新非空的poster和genre -    //其实我们可以直接更新对象r.source[id] = movie -    //用Select的话如下所示 - current, exists := r.Select(func(m datamodels.Movie) bool { - return m.ID == id - }) - -    if !exists { // ID不存在,返回error ID - 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结构的处理 -    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) -} -``` - -#### 服务层 - -服务层主要调用“数据仓库”和“数据模型”的方法(即使是数据模型很简单的应用)。这一层将包含主要的数据处理逻辑。 - - -```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主要包括对movie的CRUID(增删改查)操作。 -// MovieService主要调用movie 数据仓库的方法。 -// 下面例子的数据源是更高级别的组件 -// 这样可以用同样的逻辑可以返回不同的数据仓库 -// MovieService是一个接口,任何实现的地方都能用,这样可以替换不同的业务逻辑用来测试 -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 返回一个 movie 服务. -func NewMovieService(repo repositories.MovieRepository) MovieService { - return &movieService{ - repo: repo, - } -} - -type movieService struct { - repo repositories.MovieRepository -} - -// GetAll 返回所有 movies. -func (s *movieService) GetAll() []datamodels.Movie { - return s.repo.SelectMany(func(_ datamodels.Movie) bool { - return true - }, -1) -} - -// GetByID 是通过id找到movie. -func (s *movieService) GetByID(id int64) (datamodels.Movie, bool) { - return s.repo.Select(func(m datamodels.Movie) bool { - return m.ID == id - }) -} - - -// UpdatePosterAndGenreByID 更新一个 movie的 poster 和 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 通过id删除一个movie -// -// 返回true表示成功,其它都是失败 -func (s *movieService) DeleteByID(id int64) bool { - return s.repo.Delete(func(m datamodels.Movie) bool { - return m.ID == id - }, 1) -} -``` - -#### 视图模型 - -视图模型将处理结果返回给客户端 - -例子: -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 { -    /* 做一些检测,如果ID合法就返回true */ - return m.ID > 0 -} -``` - -Iris允许在HTTP Response Dispatcher中使用任何自定义数据结构, -所以理论上来说,除非万不得已,下面的代码不建议使用 - -```go -// Dispatch实现了`kataras/iris/mvc#Result`接口。在函数最后发送了一个`Movie`对象作为http response对象。 -// 如果ID小于等于0就回返回404,或者就返回json数据。 -//(这样就像控制器的方法默认返回自定义类型一样) -// -// 不要在这里写过多的代码,应用的主要逻辑不在这里 -// 在方法返回之前可以做个简单验证处理等等; -// -// 这里只是一个小例子,想想这个优势在设计大型应用是很有作用的 -// -// 这个方法是在`Movie`类型的控制器调用的。 -// 例子在这里:`controllers/movie_controller.go#GetBy`。 -func (m Movie) Dispatch(ctx context.Context) { - if !m.IsValid() { - ctx.NotFound() - return - } - ctx.JSON(m, context.JSON{Indent: " "}) -} -``` -然而,我们仅仅用"datamodels"作为一个数据模型包,是因为Movie数据结构没有包含敏感数据,客户端可以访问到其所有字段,我们不需要再有额外的功能去做验证处理了 - - -#### 控制器 - -控制器处理Web请求,它是服务层和客户端之间的桥梁 - -```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是/movies的控制器 -type MovieController struct { - mvc.C - -    // MovieService是一个接口,主app对象会持有它 - Service services.MovieService -} - -// 获取movies列表 -// 例子: -// curl -i http://localhost:8080/movies -// -// 如果你有一些敏感的数据要处理的话,可以按照如下所示的方式: -// func (c *MovieController) Get() (results []viewmodels.Movie) { -// data := c.Service.GetAll() -// -// for _, movie := range data { -// results = append(results, viewmodels.Movie{movie}) -// } -// return -// } -//否则直接返回数据模型 -func (c *MovieController) Get() (results []datamodels.Movie) { - return c.Service.GetAll() -} - -// GetBy返回一个movie对象 -// 例子: -// curl -i http://localhost:8080/movies/1 -func (c *MovieController) GetBy(id int64) (movie datamodels.Movie, found bool) { -    return c.Service.GetByID(id) // 404 没有找到 -} - -// PutBy更新一个movie. -// 例子: -// 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) { -    // 从请求中获取poster和genre - file, info, err := c.Ctx.FormFile("poster") - if err != nil { - return datamodels.Movie{}, errors.New("failed due form file 'poster' missing") - } -    // 关闭文件 -    file.Close() - -    //想象这就是一个上传文件的url - poster := info.Filename - genre := c.Ctx.FormValue("genre") - - return c.Service.UpdatePosterAndGenreByID(id, poster, genre) -} - -// DeleteBy删除一个movie对象 -// 例子: -// 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 { -        // 返回要删除的ID - return iris.Map{"deleted": id} - } -    //现在我们可以看到这里可以返回一个有2个返回值(map或int)的函数 -    //我们并没有指定一个返回的类型 - return iris.StatusBadRequest -} +$ go run example.go +Now listening on: http://localhost:8080 +Application Started. Press CTRL+C to shut down. +_ ``` -```go -// file: web/controllers/hello_controller.go - -package controllers - -import ( - "errors" - - "github.com/kataras/iris/mvc" -) - -// HelloController是控制器的例子 -// 下面会处理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会返回预定义绑定数据的视图 -// -// `mvc.Result`是一个含有`Dispatch`方法的接口 -// `mvc.Response` 和 `mvc.View` dispatchers 内置类型 -// 你也可以通过实现`github.com/kataras/iris/mvc#Result`接口来自定义dispatchers -func (c *HelloController) Get() mvc.Result { - return helloView -} - -// 你可以定义一个标准通用的error -var errBadName = errors.New("bad name") - -//你也可以将error包裹在mvc.Response中,这样就和mvc.Result类型兼容了 -var badName = mvc.Response{Err: errBadName, Code: 400} - -// GetBy 返回 "Hello {name}" response -// 例子: -// 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 -        // 或者 -        // GetBy(name string) (mvc.Result, error) { - // return nil, errBadName - // } - } - -    // 返回 mvc.Response{Text: "Hello " + name} 或者: - 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 中间件例 -var BasicAuth = basicauth.New(basicauth.Config{ - Users: map[string]string{ - "admin": "password", - }, -}) -``` - -```html - - - - - {{.Title}} - My App - - - -

{{.MyMessage}}

- - - -``` - -```html - - - - - {{.}}' Portfolio - My App - +## 安装 - -

Hello {{.}}

- +唯一的要求是 [Go Programming Language](https://golang.org/dl/) - +```sh +$ go get -u github.com/kataras/iris ``` -> 戳[_examples/view](_examples/#view) 可以找到更多关于layouts,tmpl,routing的例子 - - -#### 程序入口 +Iris使用[vendor](https://docs.google.com/document/d/1Bz5-UB7g2uPBdOx-rw5t9MxJwkfpx90cqG9AFL0JAYo) 包依赖管理方式。vendor包管理的方式可以有效处理包依赖更新问题 -程序入口可以将任何组件包含进来 +[![Iris vs .NET Core(C#) vs Node.js (Express)](https://iris-go.com/images/benchmark-new-gray.png)](_benchmarks/README_UNIX.md) -```go -// file: main.go - -package main +_更新于: [2017年11月21日星期二](_benchmarks/README_UNIX.md)_ -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() +![Comparison with other frameworks](https://raw.githubusercontent.com/smallnest/go-web-framework-benchmark/4db507a22c964c9bc9774c5b31afdc199a0fe8b7/benchmark.png) -    // 加载模板文件 -    app.RegisterView(iris.HTML("./web/views", ".html")) +
-    // 注册控制器 -    app.Controller("/hello", new(controllers.HelloController)) +## 支持 -    // 创建movie 数据仓库,次仓库包含的是内存级的数据源 -    repo := repositories.NewMovieRepository(datasource.Movies) -    // 创建movie服务, 然后将其与控制器绑定 -    movieService := services.NewMovieService(repo) +- [HISTORY](HISTORY.md#mo-01-jenuary-2018--v1000)文件是您最好的朋友,它包含有关最新功能和更改的信息 +- 你碰巧找到了一个错误? 张贴在 [github issues](https://github.com/kataras/iris/issues) +- 您是否有任何疑问或需要与有经验的人士交谈以实时解决问题? [加入我们的聊天](https://chat.iris-go.com) +- [点击这里完成我们基于表单的用户体验报告](https://docs.google.com/forms/d/e/1FAIpQLSdCxZXPANg_xHWil4kVAdhmh7EBBHQZ_4_xSZVDL-oCC_z5pA/viewform?usp=sf_link) +- 你喜欢这个框架吗? Tweet关于它的一些事情! 人民已经说了: - app.Controller("/movies", new(controllers.MovieController), -        // 将"movieService"绑定在 MovieController的Service接口 -        movieService, -        // 为/movies请求添加basic authentication(admin:password)中间件 - middleware.BasicAuth) + + + -    // 启动应用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, // 可以启用快速json序列化等优化配置 -    ) -} -``` + + + -更多指南戳 [_examples/#structuring](_examples/#structuring) + + + + + + -## 现在你已经准备好进入下一阶段,又向专家级gopher迈进一步了 + + + -恭喜你看到这里了,我们为你准备了更高水平的内容,向真正的专家级gopher进军吧😃 + + + -> 准备好咖啡,尽情享受吧! + + + -* [Iris框架+MongoDB](https://medium.com/go-language/iris-go-framework-mongodb-552e349eab9c) -* [用DropzoneJS 和 Go来构建表单文件上传](https://hackernoon.com/how-to-build-a-file-upload-form-using-dropzonejs-and-go-8fb9f258a991) -* [用DropzoneJS 和 Go来呈现服务器上的问题](https://hackernoon.com/how-to-display-existing-files-on-server-using-dropzonejs-and-go-53e24b57ba19) -* [Iris模块化Web开发框架](https://medium.com/@corebreaker/iris-web-cd684b4685c7) -* [按照 HTTP 性能来比较Go 和 .NET Core](https://medium.com/@kataras/go-vs-net-core-in-terms-of-http-performance-7535a61b67b8) -* [按照 HTTP 性能来比较Go 和 .NET Core Kestrel](https://hackernoon.com/iris-go-vs-net-core-kestrel-in-terms-of-http-performance-806195dc93d5) -* [在Android设备上搭建Web服务器](https://twitter.com/ThePracticalDev/status/892022594031017988) -* [在hasura上部署Iris应用](https://medium.com/@HasuraHQ/deploy-an-iris-golang-app-with-backend-apis-in-minutes-25a559bf530b) -* [用Iris 和 Bolt实现短连接服务](https://medium.com/@kataras/a-url-shortener-service-using-go-iris-and-bolt-4182f0b00ae7) + + + -## 作者 + + + - Iris的作者是[@kataras](https://github.com/kataras), 你可以通过以下方式来了解作者: + + + -* [Medium](https://medium.com/@kataras) -* [Twitter](https://twitter.com/makismaropoulos) -* [Dev.to](https://dev.to/@kataras) -* [Facebook](https://facebook.com/iris.framework) -* [Mail](mailto:kataras2006@hotmail.com?subject=Iris%20I%20need%20some%20help%20please) +

-[作者](AUTHORS) +[如何贡献代码](CONTRIBUTING.md) 文件。 [贡献者列表](https://github.com/kataras/iris/graphs/contributors) -你可以通过[PayPal](https://www.paypal.me/kataras) 或 [BTC](https://iris-go.com/v8/donate)来捐赠这个项目,这样可以促进开发者们创造更棒、更优秀的Iris。 +## 学习 -[如何贡献代码](CONTRIBUTING.md) +首先,从Web框架开始的最正确的方法是学习编程语言和标准的`http`功能的基础知识,如果您的web应用程序是一个非常简单的个人项目,没有性能和可维护性要求,您可能想要 只需使用标准软件包即可。 之后,遵循指导原则: -### 我们期待你能帮助我们翻译Iris文档 +- 浏览 **100+1** **[例子](_examples)** 和一[些入门套件](#iris-starter-kits) 我们为你制作 +- 阅读 [godocs](https://godoc.org/github.com/kataras/iris) 任何细节 +- 准备一杯咖啡或茶,无论你喜欢什么,并阅读我们为你找到的一[些文章](#articles) -Iris需要你的帮助,帮助我们翻译[README](README.md)和https://iris-go.com ,同时你也会得到奖励的。 +### Iris starter kits -你可以在这里https://github.com/kataras/iris/issues/796 看到详细的有关翻译的信息 + +1. [A basic web app built in Iris for Go](https://github.com/gauravtiwari/go_iris_app) +2. [A mini social-network created with the awesome Iris💖💖](https://github.com/iris-contrib/Iris-Mini-Social-Network) +3. [Iris isomorphic react/hot reloadable/redux/css-modules starter kit](https://github.com/iris-contrib/iris-starter-kit) +4. [Demo project with react using typescript and Iris](https://github.com/ionutvilie/react-ts) +5. [Self-hosted Localization Management Platform built with Iris and Angular](https://github.com/iris-contrib/parrot) +6. [Iris + Docker and Kubernetes](https://github.com/iris-contrib/cloud-native-go) +7. [Quickstart for Iris with Nanobox](https://guides.nanobox.io/golang/iris/from-scratch) +8. [A Hasura starter project with a ready to deploy Golang hello-world web app with IRIS](https://hasura.io/hub/project/hasura/hello-golang-iris) -### Iris 用户体验反馈 | 2017年10月3号 +> 你有类似的东西吗? [让我们知道](https://github.com/kataras/iris/pulls)! -**请放心** Iris用户体验反馈就是一些简单的表单提交,**2分钟**就能搞定。 +### Middleware -这些表单里有些问题是为了更好的了解你,了解你可以让我们更好的为你服务。 +Iris拥有大量的处理程序[[1]](middleware/)[[2]](https://github.com/iris-contrib/middleware),可以与您的Web应用程序并排使用。 不过,您并不局限于此 - 您可以自由使用与[net/http](https://golang.org/pkg/net/http/)软件包兼容的任何第三方中间件,[_examples/convert-handlers](_examples/convert-handlers) 将向您显示方式。 -https://docs.google.com/forms/d/e/1FAIpQLSdCxZXPANg_xHWil4kVAdhmh7EBBHQZ_4_xSZVDL-oCC_z5pA/viewform?usp=sf_link +### 用品 -## 贡献者列表 +* [A Hasura starter project with a ready to deploy Golang hello-world web app with IRIS](bit.ly/2lmKaAZ) +* [Top 6 web frameworks for Go as of 2017](https://blog.usejournal.com/top-6-web-frameworks-for-go-as-of-2017-23270e059c4b) +* [Iris Go Framework + MongoDB](https://medium.com/go-language/iris-go-framework-mongodb-552e349eab9c) +* [How to build a file upload form using DropzoneJS and Go](https://hackernoon.com/how-to-build-a-file-upload-form-using-dropzonejs-and-go-8fb9f258a991) +* [How to display existing files on server using DropzoneJS and Go](https://hackernoon.com/how-to-display-existing-files-on-server-using-dropzonejs-and-go-53e24b57ba19) +* [Iris, a modular web framework](https://medium.com/@corebreaker/iris-web-cd684b4685c7) +* [Go vs .NET Core in terms of HTTP performance](https://medium.com/@kataras/go-vs-net-core-in-terms-of-http-performance-7535a61b67b8) +* [Iris Go vs .NET Core Kestrel in terms of HTTP performance](https://hackernoon.com/iris-go-vs-net-core-kestrel-in-terms-of-http-performance-806195dc93d5) +* [How to Turn an Android Device into a Web Server](https://twitter.com/ThePracticalDev/status/892022594031017988) +* [Deploying a Iris Golang app in hasura](https://medium.com/@HasuraHQ/deploy-an-iris-golang-app-with-backend-apis-in-minutes-25a559bf530b) +* [A URL Shortener Service using Go, Iris and Bolt](https://medium.com/@kataras/a-url-shortener-service-using-go-iris-and-bolt-4182f0b00ae7) -非常感谢所有对Iris的贡献者,没有你们就没有Iris [贡献者](CONTRIBUTING.md) - +### 受雇用 -## 资助者 +有很多公司和初创公司寻找具有虹膜经验的Go网站开发者,我们每天都在寻找你,我们通过[facebook page](https://www.facebook.com/iris.framework)发布这些信息,就像页面得到通知一样,我们已经发布了一些信息。 -万分感谢所有的资助者🙏 [成为资助者](https://opencollective.com/iris#backer) +### 赞助商 - - -## 赞助商 - -资助Iris,你将是Iris的赞助商,你的logo将会出现在下面的列表中,[成为赞助商](https://opencollective.com/iris#sponsor) +感谢所有赞助商! (请通过成为赞助商来请求贵公司支持这个开源项目) - - - - - - - - -## 开源许可证 +## 执照 + +Iris is licensed under the [3-Clause BSD License](LICENSE). 虹膜是100%免费和开源软件。 -Iris使用3-Clause BSD [许可证](LICENSE)开源许可 。Iris绝对是100%开源的。 -对这个许可有任何疑问请[联系我们](mailto:kataras2006@hotmail.com?subject=Iris%20License) +有关许可证的任何问题,[请发送电子邮件](mailto:kataras2006@hotmail.com?subject=Iris%20License)。 diff --git a/VERSION b/VERSION index 87e1dd4155..2823854aaf 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -8.5.8:https://github.com/kataras/iris/blob/master/HISTORY.md#th-09-november-2017--v858 \ No newline at end of file +10.0.0:https://github.com/kataras/iris/blob/master/HISTORY.md#mo-01-jenuary-2018--v1000 \ No newline at end of file diff --git a/_benchmarks/iris-mvc-templates/controllers/about_controller.go b/_benchmarks/iris-mvc-templates/controllers/about_controller.go index 1407bfc78b..ea5585095d 100644 --- a/_benchmarks/iris-mvc-templates/controllers/about_controller.go +++ b/_benchmarks/iris-mvc-templates/controllers/about_controller.go @@ -2,10 +2,16 @@ package controllers import "github.com/kataras/iris/mvc" -type AboutController struct{ mvc.Controller } +type AboutController struct{} -func (c *AboutController) Get() { - c.Data["Title"] = "About" - c.Data["Message"] = "Your application description page." - c.Tmpl = "about.html" +var aboutView = mvc.View{ + Name: "about.html", + Data: map[string]interface{}{ + "Title": "About", + "Message": "Your application description page..", + }, +} + +func (c *AboutController) Get() mvc.View { + return aboutView } diff --git a/_benchmarks/iris-mvc-templates/controllers/contact_controller.go b/_benchmarks/iris-mvc-templates/controllers/contact_controller.go index a48b0b59c8..6e9757e96b 100644 --- a/_benchmarks/iris-mvc-templates/controllers/contact_controller.go +++ b/_benchmarks/iris-mvc-templates/controllers/contact_controller.go @@ -2,10 +2,16 @@ package controllers import "github.com/kataras/iris/mvc" -type ContactController struct{ mvc.Controller } +type ContactController struct{} -func (c *ContactController) Get() { - c.Data["Title"] = "Contact" - c.Data["Message"] = "Your contact page." - c.Tmpl = "contact.html" +var contactView = mvc.View{ + Name: "contact.html", + Data: map[string]interface{}{ + "Title": "Contact", + "Message": "Your contact page.", + }, +} + +func (c *ContactController) Get() mvc.View { + return contactView } diff --git a/_benchmarks/iris-mvc-templates/controllers/home_controller.go b/_benchmarks/iris-mvc-templates/controllers/home_controller.go index 30e6550510..b97c60fe1c 100644 --- a/_benchmarks/iris-mvc-templates/controllers/home_controller.go +++ b/_benchmarks/iris-mvc-templates/controllers/home_controller.go @@ -2,7 +2,7 @@ package controllers import "github.com/kataras/iris/mvc" -type HomeController struct{ mvc.C } +type HomeController struct{} func (c *HomeController) Get() mvc.Result { return mvc.View{Name: "index.html"} diff --git a/_benchmarks/iris-mvc-templates/controllers/index_controller.go b/_benchmarks/iris-mvc-templates/controllers/index_controller.go index d7c9d9e48b..472c4f47a5 100644 --- a/_benchmarks/iris-mvc-templates/controllers/index_controller.go +++ b/_benchmarks/iris-mvc-templates/controllers/index_controller.go @@ -2,9 +2,13 @@ package controllers import "github.com/kataras/iris/mvc" -type IndexController struct{ mvc.Controller } +type IndexController struct{} -func (c *IndexController) Get() { - c.Data["Title"] = "Home Page" - c.Tmpl = "index.html" +var indexView = mvc.View{ + Name: "index.html", + Data: map[string]interface{}{"Title": "Home Page"}, +} + +func (c *IndexController) Get() mvc.View { + return indexView } diff --git a/_benchmarks/iris-mvc-templates/controllers/index_controller_static.go b/_benchmarks/iris-mvc-templates/controllers/index_controller_static.go deleted file mode 100644 index 84da362b78..0000000000 --- a/_benchmarks/iris-mvc-templates/controllers/index_controller_static.go +++ /dev/null @@ -1,16 +0,0 @@ -package controllers - -import "github.com/kataras/iris/mvc" - -type IndexControllerStatic struct{ mvc.C } - -var index = mvc.View{ - Name: "index.html", - Data: map[string]interface{}{ - "Title": "Home Page", - }, -} - -func (c *IndexControllerStatic) Get() mvc.View { - return index -} diff --git a/_benchmarks/iris-mvc-templates/main.go b/_benchmarks/iris-mvc-templates/main.go index bde260e4a9..5601ad146e 100644 --- a/_benchmarks/iris-mvc-templates/main.go +++ b/_benchmarks/iris-mvc-templates/main.go @@ -5,6 +5,7 @@ import ( "github.com/kataras/iris" "github.com/kataras/iris/context" + "github.com/kataras/iris/mvc" ) const ( @@ -17,21 +18,13 @@ const ( func main() { app := iris.New() - app.Configure(configure) - - // app.Controller("/", new(controllers.IndexController)) - // app.Controller("/about", new(controllers.AboutController)) - // app.Controller("/contact", new(controllers.ContactController)) - - app.Controller("/", new(controllers.HomeController)) - - app.Run(iris.Addr(":5000"), iris.WithoutVersionChecker) -} - -func configure(app *iris.Application) { app.RegisterView(iris.HTML("./views", ".html").Layout("shared/layout.html")) app.StaticWeb("/public", publicDir) app.OnAnyErrorCode(onError) + + mvc.New(app).Handle(new(controllers.HomeController)) + + app.Run(iris.Addr(":5000"), iris.WithoutVersionChecker) } type err struct { diff --git a/_benchmarks/iris-mvc/controllers/values_controller.go b/_benchmarks/iris-mvc/controllers/values_controller.go index c560a6413a..3fe3a089ac 100644 --- a/_benchmarks/iris-mvc/controllers/values_controller.go +++ b/_benchmarks/iris-mvc/controllers/values_controller.go @@ -1,19 +1,8 @@ package controllers -import "github.com/kataras/iris/mvc" - // ValuesController is the equivalent // `ValuesController` of the .net core 2.0 mvc application. -type ValuesController struct { - mvc.C -} - -/* on windows tests(older) the Get was: -func (vc *ValuesController) Get() { - // id,_ := vc.Params.GetInt("id") - // vc.Ctx.WriteString("value") -} -but as Iris is always going better, now supports return values as well*/ +type ValuesController struct{} // Get handles "GET" requests to "api/values/{id}". func (vc *ValuesController) Get() string { diff --git a/_benchmarks/iris-mvc/main.go b/_benchmarks/iris-mvc/main.go index 92e03b593b..6ea755de06 100644 --- a/_benchmarks/iris-mvc/main.go +++ b/_benchmarks/iris-mvc/main.go @@ -1,15 +1,22 @@ package main +/// TODO: remove this on the "master" branch, or even replace it +// with the "iris-mvc" (the new implementatioin is even faster, close to handlers version, +// with bindings or without). + import ( - "github.com/kataras/iris" "github.com/kataras/iris/_benchmarks/iris-mvc/controllers" + + "github.com/kataras/iris" + "github.com/kataras/iris/mvc" ) func main() { app := iris.New() - app.Controller("/api/values/{id}", new(controllers.ValuesController)) + mvc.New(app.Party("/api/values/{id}")). + Handle(new(controllers.ValuesController)) - // 24 August 2017: Iris has a built'n version updater but we don't need it - // when benchmarking... app.Run(iris.Addr(":5000"), iris.WithoutVersionChecker) } + +// +2MB/s faster than the previous implementation, 0.4MB/s difference from the raw handlers. diff --git a/_benchmarks/iris/main.go b/_benchmarks/iris/main.go index ef5d109007..7c1ee6e29d 100644 --- a/_benchmarks/iris/main.go +++ b/_benchmarks/iris/main.go @@ -1,13 +1,10 @@ package main -import ( - "github.com/kataras/iris" - "github.com/kataras/iris/context" -) +import "github.com/kataras/iris" func main() { app := iris.New() - app.Get("/api/values/{id}", func(ctx context.Context) { + app.Get("/api/values/{id}", func(ctx iris.Context) { ctx.WriteString("value") }) diff --git a/_examples/README.md b/_examples/README.md index 44bb0f2cb7..647e008d00 100644 --- a/_examples/README.md +++ b/_examples/README.md @@ -11,6 +11,7 @@ It doesn't always contain the "best ways" but it does cover each important featu - [Hello world!](hello-world/main.go) - [Glimpse](overview/main.go) - [Tutorial: Online Visitors](tutorial/online-visitors/main.go) +- [Tutorial: Vue.js Todo MVC](tutorial/vuejs-todo-mvc) - [Tutorial: URL Shortener using BoltDB](https://medium.com/@kataras/a-url-shortener-service-using-go-iris-and-bolt-4182f0b00ae7) - [Tutorial: How to turn your Android Device into a fully featured Web Server (**MUST**)](https://twitter.com/ThePracticalDev/status/892022594031017988) - [POC: Convert the medium-sized project "Parrot" from native to Iris](https://github.com/iris-contrib/parrot) @@ -111,6 +112,11 @@ Navigate through examples for a better understanding. * [per-route](routing/writing-a-middleware/per-route/main.go) * [globally](routing/writing-a-middleware/globally/main.go) +### hero + +- [Basic](hero/basic/main.go) +- [Overview](hero/overview) + ### MVC ![](mvc/web_mvc_diagram.png) @@ -125,43 +131,82 @@ with the fastest possible execution. 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. +you can define more than one method function to serve in the same Controller. -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)`. +Serve custom controller's struct's methods as handlers with custom paths(even with regex parametermized path) via the `BeforeActivation` custom event callback, per-controller. Example: -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. +```go +import ( + "github.com/kataras/iris" + "github.com/kataras/iris/mvc" +) + +func main() { + app := iris.New() + mvc.Configure(app.Party("/root"), myMVC) + app.Run(iris.Addr(":8080")) +} + +func myMVC(app *mvc.Application) { + // app.Register(...) + // app.Router.Use/UseGlobal/Done(...) + app.Handle(new(MyController)) +} + +type MyController struct {} + +func (m *MyController) BeforeActivation(b mvc.BeforeActivation) { + // b.Dependencies().Add/Remove + // b.Router().Use/UseGlobal/Done // and any standard API call you already know + + // 1-> Method + // 2-> Path + // 3-> The controller's function name to be parsed as handler + // 4-> Any handlers that should run before the MyCustomHandler + b.Handle("GET", "/something/{id:long}", "MyCustomHandler", anyMiddleware...) +} + +// GET: http://localhost:8080/root +func (m *MyController) Get() string { return "Hey" } + +// GET: http://localhost:8080/root/something/{id:long} +func (m *MyController) MyCustomHandler(id int64) string { return "MyCustomHandler says Hey" } +``` -Access to the template file that should be rendered via the `Tmpl` field. +Persistence data inside your Controller struct (share data between requests) +by defining services to the Dependencies or have a `Singleton` controller scope. -Access to the template data that should be rendered inside -the template file via `Data` field. +Share the dependencies between controllers or register them on a parent MVC Application, and ability +to modify dependencies per-controller on the `BeforeActivation` optional event callback inside a Controller, +i.e `func(c *MyController) BeforeActivation(b mvc.BeforeActivation) { b.Dependencies().Add/Remove(...) }`. -Access to the template layout via the `Layout` field. +Access to the `Context` as a controller's field(no manual binding is neede) i.e `Ctx iris.Context` or via a method's input argument, i.e `func(ctx iris.Context, otherArguments...)`. -Access to the low-level `iris.Context/context.Context` via the `Ctx` field. +Models inside your Controller struct (set-ed at the Method function and rendered by the View). +You can return models from a controller's method or set a field in the request lifecycle +and return that field to another method, in the same request lifecycle. -Flow as you used to, `Controllers` can be registered to any `Party`, -including Subdomains, the Party's begin and done handlers work as expected. +Flow as you used to, mvc application has its own `Router` which is a type of `iris/router.Party`, the standard iris api. +`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, see for example our `mvc.SessionController`, it has the `mvc.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). +Inheritance, recursively, see for example our `mvc.SessionController`, it has the `Session *sessions.Session` and `Manager *sessions.Sessions` as embedded fields +which are filled by its `BeginRequest`, [here](https://github.com/kataras/iris/blob/master/mvc/session_controller.go). +This is just an example, you could use the `sessions.Session` which returned from the manager's `Start` as a dynamic dependency to the MVC Application, i.e +`mvcApp.Register(sessions.New(sessions.Config{Cookie: "iris_session_id"}).Start)`. -Register one or more relative paths and able to get path parameters, i.e +Access to the dynamic path parameters via the controller's methods' input arguments, no binding is needed. +When you use the Iris' default syntax to parse handlers from a controller, you need to suffix the methods +with the `By` word, uppercase is a new sub path. Example: -If `app.Controller("/user", new(user.Controller))` +If `mvc.New(app.Party("/user")).Handle(new(user.Controller))` -- `func(*Controller) Get()` - `GET:/user` , as usual. -- `func(*Controller) Post()` - `POST:/user`, as usual. +- `func(*Controller) Get()` - `GET:/user`. +- `func(*Controller) Post()` - `POST:/user`. - `func(*Controller) GetLogin()` - `GET:/user/login` - `func(*Controller) PostLogin()` - `POST:/user/login` - `func(*Controller) GetProfileFollowers()` - `GET:/user/profile/followers` @@ -169,11 +214,11 @@ If `app.Controller("/user", new(user.Controller))` - `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))` +If `mvc.New(app.Party("/profile")).Handle(new(profile.Controller))` - `func(*Controller) GetBy(username string)` - `GET:/profile/{param:string}` -If `app.Controller("/assets", new(file.Controller))` +If `mvc.New(app.Party("/assets")).Handle(new(file.Controller))` - `func(*Controller) GetByWildard(path string)` - `GET:/assets/{param:path}` @@ -188,21 +233,19 @@ func(c *ExampleController) Get() string | int | (int, string) | (string, error) | - bool | - (any, bool) | - (bool, any) | error | (int, error) | + (any, bool) | (customStruct, error) | customStruct | (customStruct, int) | (customStruct, string) | - mvc.Result or (mvc.Result, error) and so on... + 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)`. +where [mvc.Result](https://github.com/kataras/iris/blob/master/mvc/func_result.go) is an interface which contains only that function: `Dispatch(ctx iris.Context)`. -**Using Iris MVC for code reuse** +## 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. @@ -212,19 +255,11 @@ Follow the examples below, - [Hello world](mvc/hello-world/main.go) **UPDATED** - [Session Controller](mvc/session-controller/main.go) **UPDATED** -- [Overview - Plus Repository and Service layers](mvc/overview) **NEW** -- [Login showcase - Plus Repository and Service layers](mvc/login) **NEW** - - +- [Overview - Plus Repository and Service layers](mvc/overview) **UPDATED** +- [Login showcase - Plus Repository and Service layers](mvc/login) **UPDATED** +- [Singleton](mvc/singleton) **NEW** +- [Websocket Controller](mvc/websocket) **NEW** +- [Vue.js Todo MVC](tutorial/vuejs-todo-mvc) **NEW** ### Subdomains @@ -259,7 +294,7 @@ convert any custom type into a response dispatcher by implementing the `mvc.Resu - [Embedding Templates Into App Executable File](view/embedding-templates-into-app/main.go) -You can serve [quicktemplate](https://github.com/valyala/quicktemplate) and [hero](https://github.com/shiyanhui/hero/hero) files too, simply by using the `context#ResponseWriter`, take a look at the [http_responsewriter/quicktemplate](http_responsewriter/quicktemplate) and [http_responsewriter/hero] examples. +You can serve [quicktemplate](https://github.com/valyala/quicktemplate) and [hero templates](https://github.com/shiyanhui/hero/hero) files too, simply by using the `context#ResponseWriter`, take a look at the [http_responsewriter/quicktemplate](http_responsewriter/quicktemplate) and [http_responsewriter/herotemplate](http_responsewriter/herotemplate) examples. ### Authentication @@ -283,14 +318,15 @@ You can serve [quicktemplate](https://github.com/valyala/quicktemplate) and [her - [Bind JSON](http_request/read-json/main.go) - [Bind Form](http_request/read-form/main.go) -- [Upload/Read Files](http_request/upload-files/main.go) +- [Upload/Read File](http_request/upload-file/main.go) +- [Upload multiple files with an easy way](http_request/upload-files/main.go) > The `context.Request()` returns the same *http.Request you already know, these examples show some places where the Context uses this object. Besides that you can use it as you did before iris. ### How to Write to `context.ResponseWriter() http.ResponseWriter` - [Write `valyala/quicktemplate` templates](http_responsewriter/quicktemplate) -- [Write `shiyanhui/hero` templates](http_responsewriter/hero) +- [Write `shiyanhui/hero` templates](http_responsewriter/herotemplate) - [Text, Markdown, HTML, JSON, JSONP, XML, Binary](http_responsewriter/write-rest/main.go) - [Write Gzip](http_responsewriter/write-gzip/main.go) - [Stream Writer](http_responsewriter/stream-writer/main.go) diff --git a/_examples/hello-world/main.go b/_examples/hello-world/main.go index 78abd5f811..f9b4bc7e9a 100644 --- a/_examples/hello-world/main.go +++ b/_examples/hello-world/main.go @@ -9,6 +9,7 @@ import ( func main() { app := iris.New() + app.Logger().SetLevel("debug") // Optionally, add two built'n handlers // that can recover from any http-relative panics // and log the requests to the terminal. diff --git a/_examples/hero/basic/README.md b/_examples/hero/basic/README.md new file mode 100644 index 0000000000..c039ce97be --- /dev/null +++ b/_examples/hero/basic/README.md @@ -0,0 +1,13 @@ +# hero: basic + +## 1. Path Parameters - Built'n Dependencies + +![](https://github.com/kataras/explore/raw/master/iris/hero/hero-1-monokai.png) + +## 2. Services - Static Dependencies + +![](https://github.com/kataras/explore/raw/master/iris/hero/hero-2-monokai.png) + +## 3. Per-Request - Dynamic Dependencies + +![](https://github.com/kataras/explore/raw/master/iris/hero/hero-3-monokai.png) \ No newline at end of file diff --git a/_examples/hero/basic/main.go b/_examples/hero/basic/main.go new file mode 100644 index 0000000000..90015ed3ed --- /dev/null +++ b/_examples/hero/basic/main.go @@ -0,0 +1,67 @@ +package main + +import ( + "github.com/kataras/iris" + "github.com/kataras/iris/hero" +) + +func main() { + + app := iris.New() + + // 1 + helloHandler := hero.Handler(hello) + app.Get("/{to:string}", helloHandler) + + // 2 + hero.Register(&myTestService{ + prefix: "Service: Hello", + }) + + helloServiceHandler := hero.Handler(helloService) + app.Get("/service/{to:string}", helloServiceHandler) + + // 3 + hero.Register(func(ctx iris.Context) (form LoginForm) { + // it binds the "form" with a + // x-www-form-urlencoded form data and returns it. + ctx.ReadForm(&form) + return + }) + + loginHandler := hero.Handler(login) + app.Post("/login", loginHandler) + + // http://localhost:8080/your_name + // http://localhost:8080/service/your_name + app.Run(iris.Addr(":8080")) +} + +func hello(to string) string { + return "Hello " + to +} + +type Service interface { + SayHello(to string) string +} + +type myTestService struct { + prefix string +} + +func (s *myTestService) SayHello(to string) string { + return s.prefix + " " + to +} + +func helloService(to string, service Service) string { + return service.SayHello(to) +} + +type LoginForm struct { + Username string `form:"username"` + Password string `form:"password"` +} + +func login(form LoginForm) string { + return "Hello " + form.Username +} diff --git a/_examples/hero/overview/datamodels/movie.go b/_examples/hero/overview/datamodels/movie.go new file mode 100644 index 0000000000..7649d487e7 --- /dev/null +++ b/_examples/hero/overview/datamodels/movie.go @@ -0,0 +1,18 @@ +// 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"` +} diff --git a/_examples/hero/overview/datasource/movies.go b/_examples/hero/overview/datasource/movies.go new file mode 100644 index 0000000000..dbbe4fdfe9 --- /dev/null +++ b/_examples/hero/overview/datasource/movies.go @@ -0,0 +1,44 @@ +// file: datasource/movies.go + +package datasource + +import "github.com/kataras/iris/_examples/hero/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", + }, +} diff --git a/_examples/hero/overview/main.go b/_examples/hero/overview/main.go new file mode 100644 index 0000000000..ac7221072b --- /dev/null +++ b/_examples/hero/overview/main.go @@ -0,0 +1,60 @@ +// file: main.go + +package main + +import ( + "github.com/kataras/iris/_examples/hero/overview/datasource" + "github.com/kataras/iris/_examples/hero/overview/repositories" + "github.com/kataras/iris/_examples/hero/overview/services" + "github.com/kataras/iris/_examples/hero/overview/web/middleware" + "github.com/kataras/iris/_examples/hero/overview/web/routes" + + "github.com/kataras/iris" + "github.com/kataras/iris/hero" +) + +func main() { + app := iris.New() + app.Logger().SetLevel("debug") + + // Load the template files. + app.RegisterView(iris.HTML("./web/views", ".html")) + + // 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 app's dependencies. + movieService := services.NewMovieService(repo) + hero.Register(movieService) + + // Serve our routes with hero handlers. + app.PartyFunc("/hello", func(r iris.Party) { + r.Get("/", hero.Handler(routes.Hello)) + r.Get("/{name}", hero.Handler(routes.HelloName)) + }) + + app.PartyFunc("/movies", func(r iris.Party) { + // Add the basic authentication(admin:password) middleware + // for the /movies based requests. + r.Use(middleware.BasicAuth) + + r.Get("/", hero.Handler(routes.Movies)) + r.Get("/{id:long}", hero.Handler(routes.MovieByID)) + r.Put("/{id:long}", hero.Handler(routes.UpdateMovieByID)) + r.Delete("/{id:long}", hero.Handler(routes.DeleteMovieByID)) + }) + + // http://localhost:8080/hello + // http://localhost:8080/hello/iris + // http://localhost:8080/movies + // http://localhost:8080/movies/1 + app.Run( + // Start the web server at localhost:8080 + iris.Addr("localhost:8080"), + // disables updates: + iris.WithoutVersionChecker, + // skip err server closed when CTRL/CMD+C pressed: + iris.WithoutServerError(iris.ErrServerClosed), + // enables faster json serialization and more: + iris.WithOptimizations, + ) +} diff --git a/_examples/hero/overview/repositories/movie_repository.go b/_examples/hero/overview/repositories/movie_repository.go new file mode 100644 index 0000000000..de3275acce --- /dev/null +++ b/_examples/hero/overview/repositories/movie_repository.go @@ -0,0 +1,176 @@ +// file: repositories/movie_repository.go + +package repositories + +import ( + "errors" + "sync" + + "github.com/kataras/iris/_examples/hero/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) +} diff --git a/_examples/hero/overview/services/movie_service.go b/_examples/hero/overview/services/movie_service.go new file mode 100644 index 0000000000..16ed641230 --- /dev/null +++ b/_examples/hero/overview/services/movie_service.go @@ -0,0 +1,65 @@ +// file: services/movie_service.go + +package services + +import ( + "github.com/kataras/iris/_examples/hero/overview/datamodels" + "github.com/kataras/iris/_examples/hero/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) +} diff --git a/_examples/hero/overview/web/middleware/basicauth.go b/_examples/hero/overview/web/middleware/basicauth.go new file mode 100644 index 0000000000..c9b6eacfdd --- /dev/null +++ b/_examples/hero/overview/web/middleware/basicauth.go @@ -0,0 +1,12 @@ +// 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", + }, +}) diff --git a/_examples/hero/overview/web/routes/hello.go b/_examples/hero/overview/web/routes/hello.go new file mode 100644 index 0000000000..8feeca14fc --- /dev/null +++ b/_examples/hero/overview/web/routes/hello.go @@ -0,0 +1,50 @@ +// file: web/routes/hello.go + +package routes + +import ( + "errors" + + "github.com/kataras/iris/hero" +) + +var helloView = hero.View{ + Name: "hello/index.html", + Data: map[string]interface{}{ + "Title": "Hello Page", + "MyMessage": "Welcome to my awesome website", + }, +} + +// Hello will return a predefined view with bind data. +// +// `hero.Result` is just an interface with a `Dispatch` function. +// `hero.Response` and `hero.View` are the built'n result type dispatchers +// you can even create custom response dispatchers by +// implementing the `github.com/kataras/iris/hero#Result` interface. +func Hello() hero.Result { + return helloView +} + +// you can define a standard error in order to re-use 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 hero.Response to make it an hero.Result compatible type. +var badName = hero.Response{Err: errBadName, Code: 400} + +// HelloName returns a "Hello {name}" response. +// Demos: +// curl -i http://localhost:8080/hello/iris +// curl -i http://localhost:8080/hello/anything +func HelloName(name string) hero.Result { + if name != "iris" { + return badName + } + + // return hero.Response{Text: "Hello " + name} OR: + return hero.View{ + Name: "hello/name.html", + Data: name, + } +} diff --git a/_examples/hero/overview/web/routes/movies.go b/_examples/hero/overview/web/routes/movies.go new file mode 100644 index 0000000000..d3947d1bae --- /dev/null +++ b/_examples/hero/overview/web/routes/movies.go @@ -0,0 +1,59 @@ +// file: web/routes/movie.go + +package routes + +import ( + "errors" + + "github.com/kataras/iris/_examples/hero/overview/datamodels" + "github.com/kataras/iris/_examples/hero/overview/services" + + "github.com/kataras/iris" +) + +// Movies returns list of the movies. +// Demo: +// curl -i http://localhost:8080/movies +func Movies(service services.MovieService) (results []datamodels.Movie) { + return service.GetAll() +} + +// MovieByID returns a movie. +// Demo: +// curl -i http://localhost:8080/movies/1 +func MovieByID(service services.MovieService, id int64) (movie datamodels.Movie, found bool) { + return service.GetByID(id) // it will throw 404 if not found. +} + +// UpdateMovieByID updates a movie. +// Demo: +// curl -i -X PUT -F "genre=Thriller" -F "poster=@/Users/kataras/Downloads/out.gif" http://localhost:8080/movies/1 +func UpdateMovieByID(ctx iris.Context, service services.MovieService, id int64) (datamodels.Movie, error) { + // get the request data for poster and genre + file, info, err := 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 := ctx.FormValue("genre") + + return service.UpdatePosterAndGenreByID(id, poster, genre) +} + +// DeleteMovieByID deletes a movie. +// Demo: +// curl -i -X DELETE -u admin:password http://localhost:8080/movies/1 +func DeleteMovieByID(service services.MovieService, id int64) interface{} { + wasDel := 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 +} diff --git a/_examples/hero/overview/web/views/hello/index.html b/_examples/hero/overview/web/views/hello/index.html new file mode 100644 index 0000000000..9e7b03d6d0 --- /dev/null +++ b/_examples/hero/overview/web/views/hello/index.html @@ -0,0 +1,12 @@ + + + + + {{.Title}} - My App + + + +

{{.MyMessage}}

+ + + \ No newline at end of file diff --git a/_examples/hero/overview/web/views/hello/name.html b/_examples/hero/overview/web/views/hello/name.html new file mode 100644 index 0000000000..d6dd5ac66a --- /dev/null +++ b/_examples/hero/overview/web/views/hello/name.html @@ -0,0 +1,12 @@ + + + + + {{.}}' Portfolio - My App + + + +

Hello {{.}}

+ + + \ No newline at end of file diff --git a/_examples/http-listening/README.md b/_examples/http-listening/README.md index c315b41d2f..e395f31c49 100644 --- a/_examples/http-listening/README.md +++ b/_examples/http-listening/README.md @@ -66,7 +66,7 @@ func main() { } ``` -UNIX and BSD hosts can take advandage of the reuse port feature +UNIX and BSD hosts can take advantage of the reuse port feature ```go package main diff --git a/_examples/http_request/upload-file/main.go b/_examples/http_request/upload-file/main.go new file mode 100644 index 0000000000..38af8c0949 --- /dev/null +++ b/_examples/http_request/upload-file/main.go @@ -0,0 +1,73 @@ +package main + +import ( + "crypto/md5" + "fmt" + "io" + "os" + "strconv" + "time" + + "github.com/kataras/iris" +) + +func main() { + app := iris.New() + + app.RegisterView(iris.HTML("./templates", ".html")) + + // Serve the upload_form.html to the client. + app.Get("/upload", func(ctx iris.Context) { + // create a token (optionally). + + now := time.Now().Unix() + h := md5.New() + io.WriteString(h, strconv.FormatInt(now, 10)) + token := fmt.Sprintf("%x", h.Sum(nil)) + + // render the form with the token for any use you'd like. + // ctx.ViewData("", token) + // or add second argument to the `View` method. + // Token will be passed as {{.}} in the template. + ctx.View("upload_form.html", token) + }) + + // Handle the post request from the upload_form.html to the server + app.Post("/upload", func(ctx iris.Context) { + // iris.LimitRequestBodySize(32 <<20) as middleware to a route + // or use ctx.SetMaxRequestBodySize(32 << 20) + // to limit the whole request body size, + // + // or let the configuration option at app.Run for global setting + // for POST/PUT methods, including uploads of course. + + // Get the file from the request. + file, info, err := ctx.FormFile("uploadfile") + + if err != nil { + ctx.StatusCode(iris.StatusInternalServerError) + ctx.HTML("Error while uploading: " + err.Error() + "") + return + } + + defer file.Close() + fname := info.Filename + + // Create a file with the same name + // assuming that you have a folder named 'uploads' + out, err := os.OpenFile("./uploads/"+fname, + os.O_WRONLY|os.O_CREATE, 0666) + + if err != nil { + ctx.StatusCode(iris.StatusInternalServerError) + ctx.HTML("Error while uploading: " + err.Error() + "") + return + } + defer out.Close() + + io.Copy(out, file) + }) + + // start the server at http://localhost:8080 with post limit at 32 MB. + app.Run(iris.Addr(":8080"), iris.WithPostMaxMemory(32<<20)) +} diff --git a/_examples/http_request/upload-file/templates/upload_form.html b/_examples/http_request/upload-file/templates/upload_form.html new file mode 100644 index 0000000000..9782eadfa8 --- /dev/null +++ b/_examples/http_request/upload-file/templates/upload_form.html @@ -0,0 +1,12 @@ + + +Upload file + + +
+ +
+ + diff --git a/_examples/http_request/upload-file/uploads/.gitkeep b/_examples/http_request/upload-file/uploads/.gitkeep new file mode 100644 index 0000000000..e69de29bb2 diff --git a/_examples/http_request/upload-files/main.go b/_examples/http_request/upload-files/main.go index 77968e1f1b..25058b6efb 100644 --- a/_examples/http_request/upload-files/main.go +++ b/_examples/http_request/upload-files/main.go @@ -4,8 +4,9 @@ import ( "crypto/md5" "fmt" "io" - "os" + "mime/multipart" "strconv" + "strings" "time" "github.com/kataras/iris" @@ -26,43 +27,39 @@ func main() { token := fmt.Sprintf("%x", h.Sum(nil)) // render the form with the token for any use you'd like. - ctx.ViewData("", token) - ctx.View("upload_form.html") + ctx.View("upload_form.html", token) }) - // Handle the post request from the upload_form.html to the server - app.Post("/upload", iris.LimitRequestBodySize(10<<20), - func(ctx iris.Context) { - // or use ctx.SetMaxRequestBodySize(10 << 20) - // to limit the uploaded file(s) size. + // Handle the post request from the upload_form.html to the server. + app.Post("/upload", func(ctx iris.Context) { + // + // UploadFormFiles + // uploads any number of incoming files (multiple property on the form input). + // - // Get the file from the request - file, info, err := ctx.FormFile("uploadfile") - - if err != nil { - ctx.StatusCode(iris.StatusInternalServerError) - ctx.HTML("Error while uploading: " + err.Error() + "") - return - } - - defer file.Close() - fname := info.Filename - - // Create a file with the same name - // assuming that you have a folder named 'uploads' - out, err := os.OpenFile("./uploads/"+fname, - os.O_WRONLY|os.O_CREATE, 0666) + // second argument is totally optionally, + // it can be used to change a file's name based on the request, + // at this example we will showcase how to use it + // by prefixing the uploaded file with the current user's ip. + ctx.UploadFormFiles("./uploads", beforeSave) + }) - if err != nil { - ctx.StatusCode(iris.StatusInternalServerError) - ctx.HTML("Error while uploading: " + err.Error() + "") - return - } - defer out.Close() + // start the server at http://localhost:8080 with post limit at 32 MB. + app.Run(iris.Addr(":8080"), iris.WithPostMaxMemory(32<<20)) +} - io.Copy(out, file) - }) +func beforeSave(ctx iris.Context, file *multipart.FileHeader) { + ip := ctx.RemoteAddr() + // make sure you format the ip in a way + // that can be used for a file name (simple case): + ip = strings.Replace(ip, ".", "_", -1) + ip = strings.Replace(ip, ":", "_", -1) - // start the server at http://localhost:8080 - app.Run(iris.Addr(":8080")) + // you can use the time.Now, to prefix or suffix the files + // based on the current time as well, as an exercise. + // i.e unixTime := time.Now().Unix() + // prefix the Filename with the $IP- + // no need for more actions, internal uploader will use this + // name to save the file into the "./uploads" folder. + file.Filename = ip + "-" + file.Filename } diff --git a/_examples/http_request/upload-files/templates/upload_form.html b/_examples/http_request/upload-files/templates/upload_form.html index 17c4ae9bbb..3360741684 100644 --- a/_examples/http_request/upload-files/templates/upload_form.html +++ b/_examples/http_request/upload-files/templates/upload_form.html @@ -5,7 +5,7 @@
-
diff --git a/_examples/http_request/upload-files/uploads/.gitkeep b/_examples/http_request/upload-files/uploads/.gitkeep new file mode 100644 index 0000000000..e69de29bb2 diff --git a/_examples/http_responsewriter/hero/README.md b/_examples/http_responsewriter/herotemplate/README.md similarity index 100% rename from _examples/http_responsewriter/hero/README.md rename to _examples/http_responsewriter/herotemplate/README.md diff --git a/_examples/http_responsewriter/hero/app.go b/_examples/http_responsewriter/herotemplate/app.go similarity index 87% rename from _examples/http_responsewriter/hero/app.go rename to _examples/http_responsewriter/herotemplate/app.go index f25bb9e1da..4f1412ac89 100644 --- a/_examples/http_responsewriter/hero/app.go +++ b/_examples/http_responsewriter/herotemplate/app.go @@ -4,7 +4,7 @@ import ( "bytes" "log" - "github.com/kataras/iris/_examples/http_responsewriter/hero/template" + "github.com/kataras/iris/_examples/http_responsewriter/herotemplate/template" "github.com/kataras/iris" ) @@ -46,7 +46,7 @@ func main() { // using an io.Writer for automatic buffer management (i.e. hero built-in buffer pool), // iris context implements the io.Writer by its ResponseWriter - // which is an enhanced version of the standar http.ResponseWriter + // which is an enhanced version of the standard http.ResponseWriter // but still 100% compatible. template.UserListToWriter(userList, ctx) }) diff --git a/_examples/http_responsewriter/hero/template/index.html b/_examples/http_responsewriter/herotemplate/template/index.html similarity index 100% rename from _examples/http_responsewriter/hero/template/index.html rename to _examples/http_responsewriter/herotemplate/template/index.html diff --git a/_examples/http_responsewriter/hero/template/index.html.go b/_examples/http_responsewriter/herotemplate/template/index.html.go similarity index 100% rename from _examples/http_responsewriter/hero/template/index.html.go rename to _examples/http_responsewriter/herotemplate/template/index.html.go diff --git a/_examples/http_responsewriter/hero/template/user.html b/_examples/http_responsewriter/herotemplate/template/user.html similarity index 100% rename from _examples/http_responsewriter/hero/template/user.html rename to _examples/http_responsewriter/herotemplate/template/user.html diff --git a/_examples/http_responsewriter/hero/template/user.html.go b/_examples/http_responsewriter/herotemplate/template/user.html.go similarity index 100% rename from _examples/http_responsewriter/hero/template/user.html.go rename to _examples/http_responsewriter/herotemplate/template/user.html.go diff --git a/_examples/http_responsewriter/hero/template/userlist.html b/_examples/http_responsewriter/herotemplate/template/userlist.html similarity index 100% rename from _examples/http_responsewriter/hero/template/userlist.html rename to _examples/http_responsewriter/herotemplate/template/userlist.html diff --git a/_examples/http_responsewriter/hero/template/userlist.html.go b/_examples/http_responsewriter/herotemplate/template/userlist.html.go similarity index 100% rename from _examples/http_responsewriter/hero/template/userlist.html.go rename to _examples/http_responsewriter/herotemplate/template/userlist.html.go diff --git a/_examples/http_responsewriter/hero/template/userlistwriter.html b/_examples/http_responsewriter/herotemplate/template/userlistwriter.html similarity index 100% rename from _examples/http_responsewriter/hero/template/userlistwriter.html rename to _examples/http_responsewriter/herotemplate/template/userlistwriter.html diff --git a/_examples/http_responsewriter/hero/template/userlistwriter.html.go b/_examples/http_responsewriter/herotemplate/template/userlistwriter.html.go similarity index 100% rename from _examples/http_responsewriter/hero/template/userlistwriter.html.go rename to _examples/http_responsewriter/herotemplate/template/userlistwriter.html.go diff --git a/_examples/http_responsewriter/quicktemplate/templates/base.qtpl.go b/_examples/http_responsewriter/quicktemplate/templates/base.qtpl.go index 32a5e63892..d1e988f1ef 100644 --- a/_examples/http_responsewriter/quicktemplate/templates/base.qtpl.go +++ b/_examples/http_responsewriter/quicktemplate/templates/base.qtpl.go @@ -5,9 +5,11 @@ // //line base.qtpl:3 + package templates //line base.qtpl:3 + import ( qtio422016 "io" @@ -15,12 +17,14 @@ import ( ) //line base.qtpl:3 + var ( _ = qtio422016.Copy _ = qt422016.AcquireByteBuffer ) //line base.qtpl:4 + type Partial interface { //line base.qtpl:4 Body() string @@ -29,11 +33,13 @@ type Partial interface { //line base.qtpl:4 WriteBody(qq422016 qtio422016.Writer) //line base.qtpl:4 + } // Template writes a template implementing the Partial interface. //line base.qtpl:11 + func StreamTemplate(qw422016 *qt422016.Writer, p Partial) { //line base.qtpl:11 qw422016.N().S(` @@ -61,9 +67,11 @@ func StreamTemplate(qw422016 *qt422016.Writer, p Partial) { `) //line base.qtpl:30 + } //line base.qtpl:30 + func WriteTemplate(qq422016 qtio422016.Writer, p Partial) { //line base.qtpl:30 qw422016 := qt422016.AcquireWriter(qq422016) @@ -72,9 +80,11 @@ func WriteTemplate(qq422016 qtio422016.Writer, p Partial) { //line base.qtpl:30 qt422016.ReleaseWriter(qw422016) //line base.qtpl:30 + } //line base.qtpl:30 + func Template(p Partial) string { //line base.qtpl:30 qb422016 := qt422016.AcquireByteBuffer() @@ -87,21 +97,27 @@ func Template(p Partial) string { //line base.qtpl:30 return qs422016 //line base.qtpl:30 + } // Base template implementation. Other pages may inherit from it if they need // overriding only certain Partial methods. //line base.qtpl:35 + type Base struct{} //line base.qtpl:36 + func (b *Base) StreamBody(qw422016 *qt422016.Writer) { //line base.qtpl:36 -qw422016.N().S(`This is the base body`) } + + qw422016.N().S(`This is the base body`) +} //line base.qtpl:36 //line base.qtpl:36 + func (b *Base) WriteBody(qq422016 qtio422016.Writer) { //line base.qtpl:36 qw422016 := qt422016.AcquireWriter(qq422016) @@ -110,9 +126,11 @@ func (b *Base) WriteBody(qq422016 qtio422016.Writer) { //line base.qtpl:36 qt422016.ReleaseWriter(qw422016) //line base.qtpl:36 + } //line base.qtpl:36 + func (b *Base) Body() string { //line base.qtpl:36 qb422016 := qt422016.AcquireByteBuffer() @@ -125,4 +143,5 @@ func (b *Base) Body() string { //line base.qtpl:36 return qs422016 //line base.qtpl:36 + } diff --git a/_examples/http_responsewriter/quicktemplate/templates/hello.qtpl.go b/_examples/http_responsewriter/quicktemplate/templates/hello.qtpl.go index 5b812bfbf4..61db387a46 100644 --- a/_examples/http_responsewriter/quicktemplate/templates/hello.qtpl.go +++ b/_examples/http_responsewriter/quicktemplate/templates/hello.qtpl.go @@ -5,9 +5,11 @@ // //line hello.qtpl:3 + package templates //line hello.qtpl:3 + import ( qtio422016 "io" @@ -15,17 +17,20 @@ import ( ) //line hello.qtpl:3 + var ( _ = qtio422016.Copy _ = qt422016.AcquireByteBuffer ) //line hello.qtpl:4 + type Hello struct { Vars map[string]interface{} } //line hello.qtpl:9 + func (h *Hello) StreamBody(qw422016 *qt422016.Writer) { //line hello.qtpl:9 qw422016.N().S(` @@ -43,9 +48,11 @@ func (h *Hello) StreamBody(qw422016 *qt422016.Writer) { `) //line hello.qtpl:14 + } //line hello.qtpl:14 + func (h *Hello) WriteBody(qq422016 qtio422016.Writer) { //line hello.qtpl:14 qw422016 := qt422016.AcquireWriter(qq422016) @@ -54,9 +61,11 @@ func (h *Hello) WriteBody(qq422016 qtio422016.Writer) { //line hello.qtpl:14 qt422016.ReleaseWriter(qw422016) //line hello.qtpl:14 + } //line hello.qtpl:14 + func (h *Hello) Body() string { //line hello.qtpl:14 qb422016 := qt422016.AcquireByteBuffer() @@ -69,4 +78,5 @@ func (h *Hello) Body() string { //line hello.qtpl:14 return qs422016 //line hello.qtpl:14 + } diff --git a/_examples/http_responsewriter/quicktemplate/templates/index.qtpl.go b/_examples/http_responsewriter/quicktemplate/templates/index.qtpl.go index e5297ac40a..18b4824d5e 100644 --- a/_examples/http_responsewriter/quicktemplate/templates/index.qtpl.go +++ b/_examples/http_responsewriter/quicktemplate/templates/index.qtpl.go @@ -5,9 +5,11 @@ // //line index.qtpl:3 + package templates //line index.qtpl:3 + import ( qtio422016 "io" @@ -15,15 +17,18 @@ import ( ) //line index.qtpl:3 + var ( _ = qtio422016.Copy _ = qt422016.AcquireByteBuffer ) //line index.qtpl:4 + type Index struct{} //line index.qtpl:7 + func (i *Index) StreamBody(qw422016 *qt422016.Writer) { //line index.qtpl:7 qw422016.N().S(` @@ -33,9 +38,11 @@ func (i *Index) StreamBody(qw422016 *qt422016.Writer) { `) //line index.qtpl:12 + } //line index.qtpl:12 + func (i *Index) WriteBody(qq422016 qtio422016.Writer) { //line index.qtpl:12 qw422016 := qt422016.AcquireWriter(qq422016) @@ -44,9 +51,11 @@ func (i *Index) WriteBody(qq422016 qtio422016.Writer) { //line index.qtpl:12 qt422016.ReleaseWriter(qw422016) //line index.qtpl:12 + } //line index.qtpl:12 + func (i *Index) Body() string { //line index.qtpl:12 qb422016 := qt422016.AcquireByteBuffer() @@ -59,4 +68,5 @@ func (i *Index) Body() string { //line index.qtpl:12 return qs422016 //line index.qtpl:12 + } diff --git a/_examples/mvc/README.md b/_examples/mvc/README.md index 1593996e1e..6f10cbff46 100644 --- a/_examples/mvc/README.md +++ b/_examples/mvc/README.md @@ -12,49 +12,83 @@ with the fastest possible execution. 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. +you can define more than one method function to serve in the same Controller. -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)`. +Serve custom controller's struct's methods as handlers with custom paths(even with regex parametermized path) +via the `BeforeActivation` custom event callback, per-controller. Example: + +```go +import ( + "github.com/kataras/iris" + "github.com/kataras/iris/mvc" +) + +func main() { + app := iris.New() + mvc.Configure(app.Party("/root"), myMVC) + app.Run(iris.Addr(":8080")) +} -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"`. +func myMVC(app *mvc.Application) { + // app.Register(...) + // app.Router.Use/UseGlobal/Done(...) + app.Handle(new(MyController)) +} + +type MyController struct {} -Access to the request path and its parameters via the `Path and Params` fields. +func (m *MyController) BeforeActivation(b mvc.BeforeActivation) { + // b.Dependencies().Add/Remove + // b.Router().Use/UseGlobal/Done // and any standard API call you already know + + // 1-> Method + // 2-> Path + // 3-> The controller's function name to be parsed as handler + // 4-> Any handlers that should run before the MyCustomHandler + b.Handle("GET", "/something/{id:long}", "MyCustomHandler", anyMiddleware...) +} -Access to the template file that should be rendered via the `Tmpl` field. +// GET: http://localhost:8080/root +func (m *MyController) Get() string { return "Hey" } -Access to the template data that should be rendered inside -the template file via `Data` field. +// GET: http://localhost:8080/root/something/{id:long} +func (m *MyController) MyCustomHandler(id int64) string { return "MyCustomHandler says Hey" } +``` -Access to the template layout via the `Layout` field. +Persistence data inside your Controller struct (share data between requests) +by defining services to the Dependencies or have a `Singleton` controller scope. -Access to the low-level `iris.Context` via the `Ctx` field. +Share the dependencies between controllers or register them on a parent MVC Application, and ability +to modify dependencies per-controller on the `BeforeActivation` optional event callback inside a Controller, +i.e `func(c *MyController) BeforeActivation(b mvc.BeforeActivation) { b.Dependencies().Add/Remove(...) }`. -Get the relative request path by using the controller's name via `RelPath()`. +Access to the `Context` as a controller's field(no manual binding is neede) i.e `Ctx iris.Context` or via a method's input argument, i.e `func(ctx iris.Context, otherArguments...)`. -Get the relative template path directory by using the controller's name via `RelTmpl()`. +Models inside your Controller struct (set-ed at the Method function and rendered by the View). +You can return models from a controller's method or set a field in the request lifecycle +and return that field to another method, in the same request lifecycle. -Flow as you used to, `Controllers` can be registered to any `Party`, -including Subdomains, the Party's begin and done handlers work as expected. +Flow as you used to, mvc application has its own `Router` which is a type of `iris/router.Party`, the standard iris api. +`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). +Inheritance, recursively, see for example our `mvc.SessionController`, it has the `Session *sessions.Session` and `Manager *sessions.Sessions` as embedded fields +which are filled by its `BeginRequest`, [here](https://github.com/kataras/iris/blob/master/mvc/session_controller.go). +This is just an example, you could use the `sessions.Session` as a dependency to the MVC Application, i.e +`mvcApp.Register(sessions.New(sessions.Config{Cookie: "iris_session_id"}).Start)`. -Read access to the current route via the `Route` field. +Access to the dynamic path parameters via the controller's methods' input arguments, no binding is needed. +When you use the Iris' default syntax to parse handlers from a controller, you need to suffix the methods +with the `By` word, uppercase is a new sub path. Example: -Register one or more relative paths and able to get path parameters, i.e +If `mvc.New(app.Party("/user")).Handle(new(user.Controller))` -If `app.Controller("/user", new(user.Controller))` - -- `func(*Controller) Get()` - `GET:/user` , as usual. -- `func(*Controller) Post()` - `POST:/user`, as usual. +- `func(*Controller) Get()` - `GET:/user`. +- `func(*Controller) Post()` - `POST:/user`. - `func(*Controller) GetLogin()` - `GET:/user/login` - `func(*Controller) PostLogin()` - `POST:/user/login` - `func(*Controller) GetProfileFollowers()` - `GET:/user/profile/followers` @@ -62,11 +96,11 @@ If `app.Controller("/user", new(user.Controller))` - `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))` +If `mvc.New(app.Party("/profile")).Handle(new(profile.Controller))` - `func(*Controller) GetBy(username string)` - `GET:/profile/{param:string}` -If `app.Controller("/assets", new(file.Controller))` +If `mvc.New(app.Party("/assets")).Handle(new(file.Controller))` - `func(*Controller) GetByWildard(path string)` - `GET:/assets/{param:path}` @@ -91,7 +125,7 @@ func(c *ExampleController) Get() 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)`. +where [mvc.Result](https://github.com/kataras/iris/blob/master/mvc/func_result.go) is an interface which contains only that function: `Dispatch(ctx iris.Context)`. ## Using Iris MVC for code reuse @@ -99,872 +133,13 @@ By creating components that are independent of one another, developers are able 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. +## Examples -```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 - ) -} -``` +- [Hello world](hello-world/main.go) **UPDATED** +- [Session Controller](session-controller/main.go) **UPDATED** +- [Overview - Plus Repository and Service layers](overview) **UPDATED** +- [Login showcase - Plus Repository and Service layers](login) **UPDATED** +- [Singleton](singleton) **NEW** +- [Websocket Controller](websocket) **NEW** -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 +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/mvc/basic/main.go b/_examples/mvc/basic/main.go new file mode 100644 index 0000000000..c6dca3f51f --- /dev/null +++ b/_examples/mvc/basic/main.go @@ -0,0 +1,102 @@ +package main + +import ( + "fmt" + + "github.com/kataras/iris" + "github.com/kataras/iris/sessions" + + "github.com/kataras/iris/mvc" +) + +func main() { + app := iris.New() + app.Logger().SetLevel("debug") + mvc.Configure(app.Party("/basic"), basicMVC) + + app.Run(iris.Addr(":8080")) +} + +func basicMVC(app *mvc.Application) { + // You can use normal middlewares at MVC apps of course. + app.Router.Use(func(ctx iris.Context) { + ctx.Application().Logger().Infof("Path: %s", ctx.Path()) + ctx.Next() + }) + + // Register dependencies which will be binding to the controller(s), + // can be either a function which accepts an iris.Context and returns a single value (dynamic binding) + // or a static struct value (service). + app.Register( + sessions.New(sessions.Config{}).Start, + &prefixedLogger{prefix: "DEV"}, + ) + + // GET: http://localhost:8080/basic + // GET: http://localhost:8080/basic/custom + app.Handle(new(basicController)) + + // All dependencies of the parent *mvc.Application + // are cloned to this new child, + // thefore it has access to the same session as well. + // GET: http://localhost:8080/basic/sub + app.Party("/sub"). + Handle(new(basicSubController)) +} + +// If controller's fields (or even its functions) expecting an interface +// but a struct value is binded then it will check +// if that struct value implements +// the interface and if true then it will add this to the +// available bindings, as expected, before the server ran of course, +// remember? Iris always uses the best possible way to reduce load +// on serving web resources. + +type LoggerService interface { + Log(string) +} + +type prefixedLogger struct { + prefix string +} + +func (s *prefixedLogger) Log(msg string) { + fmt.Printf("%s: %s\n", s.prefix, msg) +} + +type basicController struct { + Logger LoggerService + + Session *sessions.Session +} + +func (c *basicController) BeforeActivation(b mvc.BeforeActivation) { + b.Handle("GET", "/custom", "Custom") +} + +func (c *basicController) AfterActivation(a mvc.AfterActivation) { + if a.Singleton() { + panic("basicController should be stateless, a request-scoped, we have a 'Session' which depends on the context.") + } +} + +func (c *basicController) Get() string { + count := c.Session.Increment("count", 1) + + body := fmt.Sprintf("Hello from basicController\nTotal visits from you: %d", count) + c.Logger.Log(body) + return body +} + +func (c *basicController) Custom() string { + return "custom" +} + +type basicSubController struct { + Session *sessions.Session +} + +func (c *basicSubController) Get() string { + count, _ := c.Session.GetIntDefault("count", 1) + return fmt.Sprintf("Hello from basicSubController.\nRead-only visits count: %d", count) +} diff --git a/_examples/mvc/hello-world/main.go b/_examples/mvc/hello-world/main.go index 5d2c759a51..6f2ec963bb 100644 --- a/_examples/mvc/hello-world/main.go +++ b/_examples/mvc/hello-world/main.go @@ -3,13 +3,6 @@ package main import ( "github.com/kataras/iris" "github.com/kataras/iris/mvc" - // auto-completion does not working well with type aliases - // when embedded fields. - // We should complete a report on golang repo for that at some point. - // - // Therefore import the "mvc" package manually - // here at "hello-world" so users can see that - // import path somewhere else than the "FAQ" section. "github.com/kataras/iris/middleware/logger" "github.com/kataras/iris/middleware/recover" @@ -35,7 +28,9 @@ import ( // what suits you best with Iris, low-level handlers: performance // or high-level controllers: easier to maintain and smaller codebase on large applications. -func main() { +// Of course you can put all these to main func, it's just a separate function +// for the main_test.go. +func newApp() *iris.Application { app := iris.New() // Optionally, add two built'n handlers // that can recover from any http-relative panics @@ -43,27 +38,23 @@ func main() { app.Use(recover.New()) app.Use(logger.New()) - app.Controller("/", new(ExampleController)) + // Serve a controller based on the root Router, "/". + mvc.New(app).Handle(new(ExampleController)) + return app +} + +func main() { + app := newApp() // http://localhost:8080 // http://localhost:8080/ping // http://localhost:8080/hello + // http://localhost:8080/custom_path app.Run(iris.Addr(":8080")) } // ExampleController serves the "/", "/ping" and "/hello". -type ExampleController struct { - // if you build with go1.8 you have to use the mvc package always, - // otherwise - // you can, optionally - // use the type alias `iris.C`, - // same for - // context.Context -> iris.Context, - // mvc.Result -> iris.Result, - // mvc.Response -> iris.Response, - // mvc.View -> iris.View - mvc.C -} +type ExampleController struct{} // Get serves // Method: GET @@ -89,6 +80,31 @@ func (c *ExampleController) GetHello() interface{} { return map[string]string{"message": "Hello Iris!"} } +// BeforeActivation called once, before the controller adapted to the main application +// and of course before the server ran. +// After version 9 you can also add custom routes for a specific controller's methods. +// Here you can register custom method's handlers +// use the standard router with `ca.Router` to do something that you can do without mvc as well, +// and add dependencies that will be binded to a controller's fields or method function's input arguments. +func (c *ExampleController) BeforeActivation(b mvc.BeforeActivation) { + anyMiddlewareHere := func(ctx iris.Context) { + ctx.Application().Logger().Warnf("Inside /custom_path") + ctx.Next() + } + b.Handle("GET", "/custom_path", "CustomHandlerWithoutFollowingTheNamingGuide", anyMiddlewareHere) + + // or even add a global middleware based on this controller's router, + // which in this example is the root "/": + // b.Router().Use(myMiddleware) +} + +// CustomHandlerWithoutFollowingTheNamingGuide serves +// Method: GET +// Resource: http://localhost:8080/custom_path +func (c *ExampleController) CustomHandlerWithoutFollowingTheNamingGuide() string { + return "hello from the custom handler without following the naming guide" +} + // GetUserBy serves // Method: GET // Resource: http://localhost:8080/user/{username:string} @@ -121,4 +137,18 @@ func (c *ExampleController) Trace() {} func (c *ExampleController) All() {} // OR func (c *ExampleController) Any() {} + + + +func (c *ExampleController) BeforeActivation(b mvc.BeforeActivation) { + // 1 -> the HTTP Method + // 2 -> the route's path + // 3 -> this controller's method name that should be handler for that route. + b.Handle("GET", "/mypath/{param}", "DoIt", optionalMiddlewareHere...) +} + +// After activation, all dependencies are set-ed - so read only access on them +// but still possible to add custom controller or simple standard handlers. +func (c *ExampleController) AfterActivation(a mvc.AfterActivation) {} + */ diff --git a/_examples/mvc/hello-world/main_test.go b/_examples/mvc/hello-world/main_test.go new file mode 100644 index 0000000000..5c8e271923 --- /dev/null +++ b/_examples/mvc/hello-world/main_test.go @@ -0,0 +1,23 @@ +package main + +import ( + "testing" + + "github.com/kataras/iris/httptest" +) + +func TestMVCHelloWorld(t *testing.T) { + e := httptest.New(t, newApp()) + + e.GET("/").Expect().Status(httptest.StatusOK). + ContentType("text/html", "utf-8").Body().Equal("

Welcome

") + + e.GET("/ping").Expect().Status(httptest.StatusOK). + Body().Equal("pong") + + e.GET("/hello").Expect().Status(httptest.StatusOK). + JSON().Object().Value("message").Equal("Hello Iris!") + + e.GET("/custom_path").Expect().Status(httptest.StatusOK). + Body().Equal("hello from the custom handler without following the naming guide") +} diff --git a/_examples/mvc/login/main.go b/_examples/mvc/login/main.go index fffe66c60e..8dd694a7f4 100644 --- a/_examples/mvc/login/main.go +++ b/_examples/mvc/login/main.go @@ -5,12 +5,14 @@ package main import ( "time" - "github.com/kataras/iris" "github.com/kataras/iris/_examples/mvc/login/datasource" "github.com/kataras/iris/_examples/mvc/login/repositories" "github.com/kataras/iris/_examples/mvc/login/services" "github.com/kataras/iris/_examples/mvc/login/web/controllers" "github.com/kataras/iris/_examples/mvc/login/web/middleware" + + "github.com/kataras/iris" + "github.com/kataras/iris/mvc" "github.com/kataras/iris/sessions" ) @@ -34,7 +36,9 @@ func main() { ctx.View("shared/error.html") }) - // Create our repositories and services. + // ---- Serve our controllers. ---- + + // Prepare our repositories and services. db, err := datasource.LoadUsers(datasource.Memory) if err != nil { app.Logger().Fatalf("error while loading the users: %v", err) @@ -43,29 +47,38 @@ func main() { repo := repositories.NewUserRepository(db) userService := services.NewUserService(repo) - // Register our controllers. - app.Controller("/users", new(controllers.UsersController), - // Add the basic authentication(admin:password) middleware - // for the /users based requests. - middleware.BasicAuth, - // Bind the "userService" to the UserController's Service (interface) field. - userService, - ) + // "/users" based mvc application. + users := mvc.New(app.Party("/users")) + // Add the basic authentication(admin:password) middleware + // for the /users based requests. + users.Router.Use(middleware.BasicAuth) + // Bind the "userService" to the UserController's Service (interface) field. + users.Register(userService) + users.Handle(new(controllers.UsersController)) + // "/user" based mvc application. sessManager := sessions.New(sessions.Config{ Cookie: "sessioncookiename", Expires: 24 * time.Hour, }) - app.Controller("/user", new(controllers.UserController), userService, sessManager) + user := mvc.New(app.Party("/user")) + user.Register( + userService, + sessManager.Start, + ) + user.Handle(new(controllers.UserController)) - // Start the web server at localhost:8080 - // http://localhost:8080/hello - // http://localhost:8080/hello/iris + // http://localhost:8080/noexist + // and all controller's methods like // http://localhost:8080/users/1 app.Run( + // Starts the web server at localhost:8080 iris.Addr("localhost:8080"), + // Disables the updater. iris.WithoutVersionChecker, + // Ignores err server closed log when CTRL/CMD+C pressed. iris.WithoutServerError(iris.ErrServerClosed), - iris.WithOptimizations, // enables faster json serialization and more + // Enables faster json serialization and more. + iris.WithOptimizations, ) } diff --git a/_examples/mvc/login/web/controllers/user_controller.go b/_examples/mvc/login/web/controllers/user_controller.go index e4bda39d94..f246595bba 100644 --- a/_examples/mvc/login/web/controllers/user_controller.go +++ b/_examples/mvc/login/web/controllers/user_controller.go @@ -6,7 +6,7 @@ import ( "github.com/kataras/iris/_examples/mvc/login/datamodels" "github.com/kataras/iris/_examples/mvc/login/services" - "github.com/kataras/iris/context" + "github.com/kataras/iris" "github.com/kataras/iris/mvc" "github.com/kataras/iris/sessions" ) @@ -20,39 +20,21 @@ import ( // GET /user/me // All HTTP Methods /user/logout type UserController struct { - // mvc.C is just a lightweight lightweight alternative - // to the "mvc.Controller" controller type, - // use it when you don't need mvc.Controller's fields - // (you don't need those fields when you return values from the method functions). - mvc.C + // context is auto-binded by Iris on each request, + // remember that on each incoming request iris creates a new UserController each time, + // so all fields are request-scoped by-default, only dependency injection is able to set + // custom fields like the Service which is the same for all requests (static binding) + // and the Session which depends on the current context (dynamic binding). + Ctx iris.Context // Our UserService, it's an interface which // is binded from the main application. Service services.UserService - // Session-relative things. - Manager *sessions.Sessions + // Session, binded using dependency injection from the main.go. Session *sessions.Session } -// BeginRequest will set the current session to the controller. -// -// Remember: iris.Context and context.Context is exactly the same thing, -// iris.Context is just a type alias for go 1.9 users. -// We use context.Context here because we don't need all iris' root functions, -// when we see the import paths, we make it visible to ourselves that this file is using only the context. -func (c *UserController) BeginRequest(ctx context.Context) { - c.C.BeginRequest(ctx) - - if c.Manager == nil { - ctx.Application().Logger().Errorf(`UserController: sessions manager is nil, you should bind it`) - ctx.StopExecution() // dont run the main method handler and any "done" handlers. - return - } - - c.Session = c.Manager.Start(ctx) -} - const userIDKey = "UserID" func (c *UserController) getCurrentUserID() int64 { @@ -65,12 +47,12 @@ func (c *UserController) isLoggedIn() bool { } func (c *UserController) logout() { - c.Manager.DestroyByID(c.Session.ID()) + c.Session.Destroy() } var registerStaticView = mvc.View{ Name: "user/register.html", - Data: context.Map{"Title": "User Registration"}, + Data: iris.Map{"Title": "User Registration"}, } // GetRegister handles GET: http://localhost:8080/user/register. @@ -119,7 +101,7 @@ func (c *UserController) PostRegister() mvc.Result { var loginStaticView = mvc.View{ Name: "user/login.html", - Data: context.Map{"Title": "User Login"}, + Data: iris.Map{"Title": "User Login"}, } // GetLogin handles GET: http://localhost:8080/user/login. @@ -172,7 +154,7 @@ func (c *UserController) GetMe() mvc.Result { return mvc.View{ Name: "user/me.html", - Data: context.Map{ + Data: iris.Map{ "Title": "Profile of " + u.Username, "User": u, }, diff --git a/_examples/mvc/login/web/controllers/users_controller.go b/_examples/mvc/login/web/controllers/users_controller.go index f3f7b80f0b..ed11170630 100644 --- a/_examples/mvc/login/web/controllers/users_controller.go +++ b/_examples/mvc/login/web/controllers/users_controller.go @@ -4,7 +4,7 @@ import ( "github.com/kataras/iris/_examples/mvc/login/datamodels" "github.com/kataras/iris/_examples/mvc/login/services" - "github.com/kataras/iris/mvc" + "github.com/kataras/iris" ) // UsersController is our /users API controller. @@ -14,30 +14,17 @@ import ( // DELETE /users/{id:long} | delete by id // Requires basic authentication. type UsersController struct { - mvc.C + // Optionally: context is auto-binded by Iris on each request, + // remember that on each incoming request iris creates a new UserController each time, + // so all fields are request-scoped by-default, only dependency injection is able to set + // custom fields like the Service which is the same for all requests (static binding). + Ctx iris.Context + // Our UserService, it's an interface which + // is binded from the main application. Service services.UserService } -// This could be possible but we should not call handlers inside the `BeginRequest`. -// Because `BeginRequest` was introduced to set common, shared variables between all method handlers -// before their execution. -// We will add this middleware from our `app.Controller` call. -// -// var authMiddleware = basicauth.New(basicauth.Config{ -// Users: map[string]string{ -// "admin": "password", -// }, -// }) -// -// func (c *UsersController) BeginRequest(ctx iris.Context) { -// c.C.BeginRequest(ctx) -// -// if !ctx.Proceed(authMiddleware) { -// ctx.StopExecution() -// } -// } - // Get returns list of the users. // Demo: // curl -i -u admin:password http://localhost:8080/users @@ -97,5 +84,5 @@ func (c *UsersController) DeleteBy(id int64) interface{} { // 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 400 // same as `iris.StatusBadRequest`. + return iris.StatusBadRequest // same as 400. } diff --git a/_examples/mvc/overview/main.go b/_examples/mvc/overview/main.go index 2d0befc568..9d7d9cf6d7 100644 --- a/_examples/mvc/overview/main.go +++ b/_examples/mvc/overview/main.go @@ -10,38 +10,53 @@ import ( "github.com/kataras/iris/_examples/mvc/overview/web/middleware" "github.com/kataras/iris" + "github.com/kataras/iris/mvc" ) func main() { app := iris.New() + app.Logger().SetLevel("debug") // Load the template files. app.RegisterView(iris.HTML("./web/views", ".html")) - // Register our controllers. - app.Controller("/hello", new(controllers.HelloController)) + // Serve our controllers. + mvc.New(app.Party("/hello")).Handle(new(controllers.HelloController)) + // You can also split the code you write to configure an mvc.Application + // using the `mvc.Configure` method, as shown below. + mvc.Configure(app.Party("/movies"), movies) - // 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( + // Start the web server at localhost:8080 iris.Addr("localhost:8080"), + // disables updates: iris.WithoutVersionChecker, + // skip err server closed when CTRL/CMD+C pressed: iris.WithoutServerError(iris.ErrServerClosed), - iris.WithOptimizations, // enables faster json serialization and more + // enables faster json serialization and more: + iris.WithOptimizations, ) } + +// note the mvc.Application, it's not iris.Application. +func movies(app *mvc.Application) { + // Add the basic authentication(admin:password) middleware + // for the /movies based requests. + app.Router.Use(middleware.BasicAuth) + + // 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 app's dependencies. + movieService := services.NewMovieService(repo) + app.Register(movieService) + + // serve our movies controller. + // Note that you can serve more than one controller + // you can also create child mvc apps using the `movies.Party(relativePath)` or `movies.Clone(app.Party(...))` + // if you want. + app.Handle(new(controllers.MovieController)) +} diff --git a/_examples/mvc/overview/web/controllers/hello_controller.go b/_examples/mvc/overview/web/controllers/hello_controller.go index 56518d5eb4..d1ff76ae63 100644 --- a/_examples/mvc/overview/web/controllers/hello_controller.go +++ b/_examples/mvc/overview/web/controllers/hello_controller.go @@ -10,9 +10,7 @@ import ( // HelloController is our sample controller // it handles GET: /hello and GET: /hello/{name} -type HelloController struct { - mvc.C -} +type HelloController struct{} var helloView = mvc.View{ Name: "hello/index.html", @@ -27,12 +25,12 @@ var helloView = mvc.View{ // `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. +// implementing the `github.com/kataras/iris/hero#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. +// you can define a standard error in order to re-use anywhere in your app. var errBadName = errors.New("bad name") // you can just return it as error or even better diff --git a/_examples/mvc/overview/web/controllers/movie_controller.go b/_examples/mvc/overview/web/controllers/movie_controller.go index 10542d1a3e..0555bf437e 100644 --- a/_examples/mvc/overview/web/controllers/movie_controller.go +++ b/_examples/mvc/overview/web/controllers/movie_controller.go @@ -9,17 +9,10 @@ import ( "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 is just a lightweight lightweight alternative - // to the "mvc.Controller" controller type, - // use it when you don't need mvc.Controller's fields - // (you don't need those fields when you return values from the method functions). - mvc.C - // Our MovieService, it's an interface which // is binded from the main application. Service services.MovieService @@ -53,9 +46,9 @@ func (c *MovieController) GetBy(id int64) (movie datamodels.Movie, found bool) { // 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) { +func (c *MovieController) PutBy(ctx iris.Context, id int64) (datamodels.Movie, error) { // get the request data for poster and genre - file, info, err := c.Ctx.FormFile("poster") + file, info, err := ctx.FormFile("poster") if err != nil { return datamodels.Movie{}, errors.New("failed due form file 'poster' missing") } @@ -64,7 +57,7 @@ func (c *MovieController) PutBy(id int64) (datamodels.Movie, error) { // imagine that is the url of the uploaded file... poster := info.Filename - genre := c.Ctx.FormValue("genre") + genre := ctx.FormValue("genre") return c.Service.UpdatePosterAndGenreByID(id, poster, genre) } diff --git a/_examples/mvc/session-controller/main.go b/_examples/mvc/session-controller/main.go index b768da6819..5c1bb2ae3e 100644 --- a/_examples/mvc/session-controller/main.go +++ b/_examples/mvc/session-controller/main.go @@ -7,72 +7,61 @@ import ( "time" "github.com/kataras/iris" + "github.com/kataras/iris/mvc" "github.com/kataras/iris/sessions" ) // VisitController handles the root route. type VisitController struct { - iris.C - - // the sessions manager, we need that to set `Session`. - // It's binded from `app.Controller`. - Manager *sessions.Sessions // the current request session, - // its initialization happens at the `BeginRequest`. + // its initialization happens by the dependency function that we've added to the `visitApp`. Session *sessions.Session - // A time.time which is binded from the `app.Controller`, + // A time.time which is binded from the MVC, // order of binded fields doesn't matter. StartTime time.Time } -// BeginRequest is executed for each Get, Post, Put requests, -// can be used to share context, common data -// or to cancel the request via `ctx.StopExecution()`. -func (c *VisitController) BeginRequest(ctx iris.Context) { - // always call the embedded `BeginRequest` before everything else. - c.C.BeginRequest(ctx) - - if c.Manager == nil { - ctx.Application().Logger().Errorf(`VisitController: sessions manager is nil, you should bind it`) - // dont run the main method handler and any "done" handlers. - ctx.StopExecution() - return - } - - // set the `c.Session` we will use that in our Get method. - c.Session = c.Manager.Start(ctx) -} - // Get handles // Method: GET // Path: http://localhost:8080 func (c *VisitController) Get() string { - // get the visits, before calcuate this new one. - visits, _ := c.Session.GetIntDefault("visits", 0) - - // increment the visits and store to the session. - visits++ - c.Session.Set("visits", visits) - + // it increments a "visits" value of integer by one, + // if the entry with key 'visits' doesn't exist it will create it for you. + visits := c.Session.Increment("visits", 1) // write the current, updated visits. since := time.Now().Sub(c.StartTime).Seconds() return fmt.Sprintf("%d visit from my current session in %0.1f seconds of server's up-time", visits, since) } -var ( - manager = sessions.New(sessions.Config{Cookie: "mysession_cookie_name"}) -) - -func main() { +func newApp() *iris.Application { app := iris.New() + sess := sessions.New(sessions.Config{Cookie: "mysession_cookie_name"}) - // bind our session manager, which is required, to the `VisitController.Manager` + visitApp := mvc.New(app.Party("/")) + // bind the current *session.Session, which is required, to the `VisitController.Session` // and the time.Now() to the `VisitController.StartTime`. - app.Controller("/", new(VisitController), - manager, - time.Now()) + visitApp.Register( + // if dependency is a function which accepts + // a Context and returns a single value + // then the result type of this function is resolved by the controller + // and on each request it will call the function with its Context + // and set the result(the *sessions.Session here) to the controller's field. + // + // If dependencies are registered without field or function's input arguments as + // consumers then those dependencies are being ignored before the server ran, + // so you can bind many dependecies and use them in different controllers. + sess.Start, + time.Now(), + ) + visitApp.Handle(new(VisitController)) + + return app +} + +func main() { + app := newApp() // 1. open the browser (no in private mode) // 2. navigate to http://localhost:8080 diff --git a/_examples/mvc/session-controller/main_test.go b/_examples/mvc/session-controller/main_test.go new file mode 100644 index 0000000000..f89005014b --- /dev/null +++ b/_examples/mvc/session-controller/main_test.go @@ -0,0 +1,21 @@ +package main + +import ( + "testing" + + "github.com/kataras/iris/httptest" +) + +func TestMVCSession(t *testing.T) { + e := httptest.New(t, newApp(), httptest.URL("http://example.com")) + + e1 := e.GET("/").Expect().Status(httptest.StatusOK) + e1.Cookies().NotEmpty() + e1.Body().Contains("1 visit") + + e.GET("/").Expect().Status(httptest.StatusOK). + Body().Contains("2 visit") + + e.GET("/").Expect().Status(httptest.StatusOK). + Body().Contains("3 visit") +} diff --git a/_examples/mvc/singleton/main.go b/_examples/mvc/singleton/main.go new file mode 100644 index 0000000000..2d087b5828 --- /dev/null +++ b/_examples/mvc/singleton/main.go @@ -0,0 +1,36 @@ +package main + +import ( + "fmt" + "sync/atomic" + + "github.com/kataras/iris" + "github.com/kataras/iris/mvc" +) + +func main() { + app := iris.New() + mvc.New(app.Party("/")).Handle(&globalVisitorsController{visits: 0}) + + // http://localhost:8080 + app.Run(iris.Addr(":8080")) +} + +type globalVisitorsController struct { + // When a singleton controller is used then concurent safe access is up to the developers, because + // all clients share the same controller instance instead. + // Note that any controller's methods + // are per-client, but the struct's field can be shared across multiple clients if the structure + // does not have any dynamic struct field dependencies that depend on the iris.Context + // and ALL field's values are NOT zero, at this case we use uint64 which it's no zero (even if we didn't set it + // manually ease-of-understand reasons) because it's a value of &{0}. + // All the above declares a Singleton, note that you don't have to write a single line of code to do this, Iris is smart enough. + // + // see `Get`. + visits uint64 +} + +func (c *globalVisitorsController) Get() string { + count := atomic.AddUint64(&c.visits, 1) + return fmt.Sprintf("Total visitors: %d", count) +} diff --git a/_examples/mvc/websocket/main.go b/_examples/mvc/websocket/main.go new file mode 100644 index 0000000000..7b4b8dcdfa --- /dev/null +++ b/_examples/mvc/websocket/main.go @@ -0,0 +1,82 @@ +package main + +import ( + "sync/atomic" + + "github.com/kataras/iris" + "github.com/kataras/iris/mvc" + "github.com/kataras/iris/websocket" +) + +func main() { + app := iris.New() + // load templaes. + app.RegisterView(iris.HTML("./views", ".html")) + + // render the ./views/index.html. + app.Get("/", func(ctx iris.Context) { + ctx.View("index.html") + }) + + mvc.Configure(app.Party("/websocket"), configureMVC) + // Or mvc.New(app.Party(...)).Configure(configureMVC) + + // http://localhost:8080 + app.Run(iris.Addr(":8080")) +} + +func configureMVC(m *mvc.Application) { + ws := websocket.New(websocket.Config{}) + // http://localhost:8080/websocket/iris-ws.js + m.Router.Any("/iris-ws.js", websocket.ClientHandler()) + + // This will bind the result of ws.Upgrade which is a websocket.Connection + // to the controller(s) served by the `m.Handle`. + m.Register(ws.Upgrade) + m.Handle(new(websocketController)) +} + +var visits uint64 + +func increment() uint64 { + return atomic.AddUint64(&visits, 1) +} + +func decrement() uint64 { + return atomic.AddUint64(&visits, ^uint64(0)) +} + +type websocketController struct { + // Note that you could use an anonymous field as well, it doesn't matter, binder will find it. + // + // This is the current websocket connection, each client has its own instance of the *websocketController. + Conn websocket.Connection +} + +func (c *websocketController) onLeave(roomName string) { + // visits-- + newCount := decrement() + // This will call the "visit" event on all clients, except the current one, + // (it can't because it's left but for any case use this type of design) + c.Conn.To(websocket.Broadcast).Emit("visit", newCount) +} + +func (c *websocketController) update() { + // visits++ + newCount := increment() + + // This will call the "visit" event on all clients, including the current + // with the 'newCount' variable. + // + // There are many ways that u can do it and faster, for example u can just send a new visitor + // and client can increment itself, but here we are just "showcasing" the websocket controller. + c.Conn.To(websocket.All).Emit("visit", newCount) +} + +func (c *websocketController) Get( /* websocket.Connection could be lived here as well, it doesn't matter */ ) { + c.Conn.OnLeave(c.onLeave) + c.Conn.On("visit", c.update) + + // call it after all event callbacks registration. + c.Conn.Wait() +} diff --git a/_examples/mvc/websocket/views/index.html b/_examples/mvc/websocket/views/index.html new file mode 100644 index 0000000000..ecedcff2a5 --- /dev/null +++ b/_examples/mvc/websocket/views/index.html @@ -0,0 +1,63 @@ + + + + Online visitors MVC example + + + + +
+ 1 online visitor +
+ + + + + + + + \ No newline at end of file diff --git a/_examples/sessions/README.md b/_examples/sessions/README.md index 10c7c564c7..a28d7ead3d 100644 --- a/_examples/sessions/README.md +++ b/_examples/sessions/README.md @@ -22,8 +22,8 @@ Some trivial examples, ```go import "github.com/kataras/iris/sessions" -sess := sessions.Start(http.ResponseWriter, *http.Request) -sess. +manager := sessions.Start(iris.Context) +manager. ID() string Get(string) interface{} HasFlash() bool diff --git a/_examples/sessions/database/badger/main.go b/_examples/sessions/database/badger/main.go index a0192dd847..0011f03c1a 100644 --- a/_examples/sessions/database/badger/main.go +++ b/_examples/sessions/database/badger/main.go @@ -89,5 +89,5 @@ func main() { sess.ShiftExpiration(ctx) }) - app.Run(iris.Addr(":8080"), iris.WithoutServerError(iris.ErrServerClosed), iris.WithoutVersionChecker) + app.Run(iris.Addr(":8080")) } diff --git a/_examples/sessions/standalone/main.go b/_examples/sessions/standalone/main.go index 4841f8f26d..9126d2f673 100644 --- a/_examples/sessions/standalone/main.go +++ b/_examples/sessions/standalone/main.go @@ -4,7 +4,6 @@ import ( "time" "github.com/kataras/iris" - "github.com/kataras/iris/sessions" ) @@ -25,21 +24,20 @@ func main() { // or set a value, like 2 hours: Expires: time.Hour * 2, // if you want to invalid cookies on different subdomains - // of the same host, then enable it - DisableSubdomainPersistence: false, - // want to be crazy safe? Take a look at the "securecookie" example folder. + // of the same host, then enable it. + // Defaults to false. + DisableSubdomainPersistence: true, }) app.Get("/", func(ctx iris.Context) { ctx.Writef("You should navigate to the /set, /get, /delete, /clear,/destroy instead") }) app.Get("/set", func(ctx iris.Context) { - //set session values. s := sess.Start(ctx) s.Set("name", "iris") - //test if setted here + //test if setted here. ctx.Writef("All ok session setted to: %s", s.GetString("name")) // Set will set the value as-it-is, @@ -52,7 +50,8 @@ func main() { }) app.Get("/get", func(ctx iris.Context) { - // get a specific value, as string, if no found returns just an empty string + // get a specific value, as string, + // if not found then it returns just an empty string. name := sess.Start(ctx).GetString("name") ctx.Writef("The name on the /set was: %s", name) @@ -64,17 +63,16 @@ func main() { }) app.Get("/clear", func(ctx iris.Context) { - // removes all entries + // removes all entries. sess.Start(ctx).Clear() }) app.Get("/update", func(ctx iris.Context) { - // updates expire date + // updates expire date. sess.ShiftExpiration(ctx) }) app.Get("/destroy", func(ctx iris.Context) { - //destroy, removes the entire session data and cookie sess.Destroy(ctx) }) diff --git a/_examples/structuring/login-mvc-single-responsibility-package/main.go b/_examples/structuring/login-mvc-single-responsibility-package/main.go index 154ec39a05..9f5e0911bd 100644 --- a/_examples/structuring/login-mvc-single-responsibility-package/main.go +++ b/_examples/structuring/login-mvc-single-responsibility-package/main.go @@ -6,6 +6,7 @@ import ( "github.com/kataras/iris/_examples/structuring/login-mvc-single-responsibility-package/user" "github.com/kataras/iris" + "github.com/kataras/iris/mvc" "github.com/kataras/iris/sessions" ) @@ -19,13 +20,7 @@ func main() { app.StaticWeb("/public", "./public") - manager := sessions.New(sessions.Config{ - Cookie: "sessioncookiename", - Expires: 24 * time.Hour, - }) - users := user.NewDataSource() - - app.Controller("/user", new(user.Controller), manager, users) + mvc.Configure(app, configureMVC) // http://localhost:8080/user/register // http://localhost:8080/user/login @@ -35,9 +30,22 @@ func main() { app.Run(iris.Addr(":8080"), configure) } +func configureMVC(app *mvc.Application) { + manager := sessions.New(sessions.Config{ + Cookie: "sessioncookiename", + Expires: 24 * time.Hour, + }) + + userApp := app.Party("/user") + userApp.Register( + user.NewDataSource(), + manager.Start, + ) + userApp.Handle(new(user.Controller)) +} + func configure(app *iris.Application) { app.Configure( iris.WithoutServerError(iris.ErrServerClosed), - iris.WithCharset("UTF-8"), ) } diff --git a/_examples/structuring/login-mvc-single-responsibility-package/user/auth.go b/_examples/structuring/login-mvc-single-responsibility-package/user/auth.go index cda9d3ddf1..0300f03506 100644 --- a/_examples/structuring/login-mvc-single-responsibility-package/user/auth.go +++ b/_examples/structuring/login-mvc-single-responsibility-package/user/auth.go @@ -6,51 +6,52 @@ import ( "strings" "github.com/kataras/iris" + "github.com/kataras/iris/mvc" + "github.com/kataras/iris/sessions" ) -// paths -const ( - PathLogin = "/user/login" - PathLogout = "/user/logout" -) +const sessionIDKey = "UserID" -// the session key for the user id comes from the Session. -const ( - sessionIDKey = "UserID" +// paths +var ( + PathLogin = mvc.Response{Path: "/user/login"} + PathLogout = mvc.Response{Path: "/user/logout"} ) // AuthController is the user authentication controller, a custom shared controller. type AuthController struct { - iris.SessionController + // context is auto-binded if struct depends on this, + // in this controller we don't we do everything with mvc-style, + // and that's neither the 30% of its features. + // Ctx iris.Context - Source *DataSource - User Model `iris:"model"` + Source *DataSource + Session *sessions.Session + + // the whole controller is request-scoped because we already depend on Session, so + // this will be new for each new incoming request, BeginRequest sets that based on the session. + UserID int64 } // BeginRequest saves login state to the context, the user id. func (c *AuthController) BeginRequest(ctx iris.Context) { - c.SessionController.BeginRequest(ctx) - - if userID := c.Session.Get(sessionIDKey); userID != nil { - ctx.Values().Set(sessionIDKey, userID) - } + c.UserID, _ = c.Session.GetInt64(sessionIDKey) } -func (c *AuthController) fireError(err error) { - if err != nil { - c.Ctx.Application().Logger().Debug(err.Error()) +// EndRequest is here just to complete the BaseController +// in order to be tell iris to call the `BeginRequest` before the main method. +func (c *AuthController) EndRequest(ctx iris.Context) {} - c.Status = 400 - c.Data["Title"] = "User Error" - c.Data["Message"] = strings.ToUpper(err.Error()) - c.Tmpl = "shared/error.html" +func (c *AuthController) fireError(err error) mvc.View { + return mvc.View{ + Code: iris.StatusBadRequest, + Name: "shared/error.html", + Data: iris.Map{"Title": "User Error", "Message": strings.ToUpper(err.Error())}, } } -func (c *AuthController) redirectTo(id int64) { - if id > 0 { - c.Path = "/user/" + strconv.Itoa(int(id)) - } +func (c *AuthController) redirectTo(id int64) mvc.Response { + return mvc.Response{Path: "/user/" + strconv.Itoa(int(id))} } func (c *AuthController) createOrUpdate(firstname, username, password string) (user Model, err error) { @@ -75,8 +76,8 @@ func (c *AuthController) createOrUpdate(firstname, username, password string) (u func (c *AuthController) isLoggedIn() bool { // we don't search by session, we have the user id - // already by the `SaveState` middleware. - return c.Values.Get(sessionIDKey) != nil + // already by the `BeginRequest` middleware. + return c.UserID > 0 } func (c *AuthController) verify(username, password string) (user Model, err error) { @@ -101,24 +102,9 @@ func (c *AuthController) verify(username, password string) (user Model, err erro // if logged in then destroy the session // and redirect to the login page // otherwise redirect to the registration page. -func (c *AuthController) logout() { +func (c *AuthController) logout() mvc.Response { if c.isLoggedIn() { - // c.Manager is the Sessions manager created - // by the embedded SessionController, automatically. - c.Manager.DestroyByID(c.Session.ID()) - return - } - - c.Path = PathLogin -} - -// AllowUser will check if this client is a logged user, -// if not then it will redirect that guest to the login page -// otherwise it will allow the execution of the next handler. -func AllowUser(ctx iris.Context) { - if ctx.Values().Get(sessionIDKey) != nil { - ctx.Next() - return + c.Session.Destroy() } - ctx.Redirect(PathLogin) + return PathLogin } diff --git a/_examples/structuring/login-mvc-single-responsibility-package/user/controller.go b/_examples/structuring/login-mvc-single-responsibility-package/user/controller.go index 628fc63f27..38330c7d17 100644 --- a/_examples/structuring/login-mvc-single-responsibility-package/user/controller.go +++ b/_examples/structuring/login-mvc-single-responsibility-package/user/controller.go @@ -1,8 +1,18 @@ package user -const ( - pathMyProfile = "/user/me" - pathRegister = "/user/register" +import ( + "github.com/kataras/iris" + "github.com/kataras/iris/mvc" +) + +var ( + // About Code: iris.StatusSeeOther -> + // When redirecting from POST to GET request you -should- use this HTTP status code, + // however there're some (complicated) alternatives if you + // search online or even the HTTP RFC. + // "See Other" RFC 7231 + pathMyProfile = mvc.Response{Path: "/user/me", Code: iris.StatusSeeOther} + pathRegister = mvc.Response{Path: "/user/register"} ) // Controller is responsible to handle the following requests: @@ -17,71 +27,89 @@ type Controller struct { AuthController } +type formValue func(string) string + +// BeforeActivation called once before the server start +// and before the controller's registration, here you can add +// dependencies, to this controller and only, that the main caller may skip. +func (c *Controller) BeforeActivation(b mvc.BeforeActivation) { + // bind the context's `FormValue` as well in order to be + // acceptable on the controller or its methods' input arguments (NEW feature as well). + b.Dependencies().Add(func(ctx iris.Context) formValue { return ctx.FormValue }) +} + +type page struct { + Title string +} + // GetRegister handles GET:/user/register. -func (c *Controller) GetRegister() { +// mvc.Result can accept any struct which contains a `Dispatch(ctx iris.Context)` method. +// Both mvc.Response and mvc.View are mvc.Result. +func (c *Controller) GetRegister() mvc.Result { if c.isLoggedIn() { - c.logout() - return + return c.logout() } - c.Data["Title"] = "User Registration" - c.Tmpl = pathRegister + ".html" + // You could just use it as a variable to win some time in serve-time, + // this is an exersise for you :) + return mvc.View{ + Name: pathRegister.Path + ".html", + Data: page{"User Registration"}, + } } // PostRegister handles POST:/user/register. -func (c *Controller) PostRegister() { +func (c *Controller) PostRegister(form formValue) mvc.Result { // we can either use the `c.Ctx.ReadForm` or read values one by one. var ( - firstname = c.Ctx.FormValue("firstname") - username = c.Ctx.FormValue("username") - password = c.Ctx.FormValue("password") + firstname = form("firstname") + username = form("username") + password = form("password") ) user, err := c.createOrUpdate(firstname, username, password) if err != nil { - c.fireError(err) - return + return c.fireError(err) } // setting a session value was never easier. c.Session.Set(sessionIDKey, user.ID) // succeed, nothing more to do here, just redirect to the /user/me. + return pathMyProfile +} - // When redirecting from POST to GET request you -should- use this HTTP status code, - // however there're some (complicated) alternatives if you - // search online or even the HTTP RFC. - c.Status = 303 // "See Other" RFC 7231 - - // Redirect to GET: /user/me - // by changing the Path (and the status code because we're in POST request at this case). - c.Path = pathMyProfile +// with these static views, +// you can use variables-- that are initialized before server start +// so you can win some time on serving. +// You can do it else where as well but I let them as pracise for you, +// essentially you can understand by just looking below. +var userLoginView = mvc.View{ + Name: PathLogin.Path + ".html", + Data: page{"User Login"}, } // GetLogin handles GET:/user/login. -func (c *Controller) GetLogin() { +func (c *Controller) GetLogin() mvc.Result { if c.isLoggedIn() { - c.logout() - return + return c.logout() } - c.Data["Title"] = "User Login" - c.Tmpl = PathLogin + ".html" + return userLoginView } // PostLogin handles POST:/user/login. -func (c *Controller) PostLogin() { +func (c *Controller) PostLogin(form formValue) mvc.Result { var ( - username = c.Ctx.FormValue("username") - password = c.Ctx.FormValue("password") + username = form("username") + password = form("password") ) user, err := c.verify(username, password) if err != nil { - c.fireError(err) - return + return c.fireError(err) } c.Session.Set(sessionIDKey, user.ID) - c.Path = pathMyProfile + return pathMyProfile } // AnyLogout handles any method on path /user/logout. @@ -90,44 +118,72 @@ func (c *Controller) AnyLogout() { } // GetMe handles GET:/user/me. -func (c *Controller) GetMe() { +func (c *Controller) GetMe() mvc.Result { id, err := c.Session.GetInt64(sessionIDKey) if err != nil || id <= 0 { - // when not already logged in. - c.Path = PathLogin - return + // when not already logged in, redirect to login. + return PathLogin } u, found := c.Source.GetByID(id) if !found { // if the session exists but for some reason the user doesn't exist in the "database" // then logout him and redirect to the register page. - c.logout() - return + return c.logout() } // set the model and render the view template. - c.User = u - c.Data["Title"] = "Profile of " + u.Username - c.Tmpl = pathMyProfile + ".html" + return mvc.View{ + Name: pathMyProfile.Path + ".html", + Data: iris.Map{ + "Title": "Profile of " + u.Username, + "User": u, + }, + } } -func (c *Controller) renderNotFound(id int64) { - c.Status = 404 - c.Data["Title"] = "User Not Found" - c.Data["ID"] = id - c.Tmpl = "user/notfound.html" +func (c *Controller) renderNotFound(id int64) mvc.View { + return mvc.View{ + Code: iris.StatusNotFound, + Name: "user/notfound.html", + Data: iris.Map{ + "Title": "User Not Found", + "ID": id, + }, + } +} + +// Dispatch completes the `mvc.Result` interface +// in order to be able to return a type of `Model` +// as mvc.Result. +// If this function didn't exist then +// we should explicit set the output result to that Model or to an interface{}. +func (u Model) Dispatch(ctx iris.Context) { + ctx.JSON(u) } // GetBy handles GET:/user/{id:long}, // i.e http://localhost:8080/user/1 -func (c *Controller) GetBy(userID int64) { +func (c *Controller) GetBy(userID int64) mvc.Result { // we have /user/{id} // fetch and render user json. - if user, found := c.Source.GetByID(userID); !found { + user, found := c.Source.GetByID(userID) + if !found { // not user found with that ID. - c.renderNotFound(userID) - } else { - c.Ctx.JSON(user) + return c.renderNotFound(userID) } + + // Q: how the hell Model can be return as mvc.Result? + // A: I told you before on some comments and the docs, + // any struct that has a `Dispatch(ctx iris.Context)` + // can be returned as an mvc.Result(see ~20 lines above), + // therefore we are able to combine many type of results in the same method. + // For example, here, we return either an mvc.View to render a not found custom template + // either a user which returns the Model as JSON via its Dispatch. + // + // We could also return just a struct value that is not an mvc.Result, + // if the output result of the `GetBy` was that struct's type or an interface{} + // and iris would render that with JSON as well, but here we can't do that without complete the `Dispatch` + // function, because we may return an mvc.View which is an mvc.Result. + return user } diff --git a/_examples/tutorial/caddy/server1/main.go b/_examples/tutorial/caddy/server1/main.go index be821797a6..3f67458081 100644 --- a/_examples/tutorial/caddy/server1/main.go +++ b/_examples/tutorial/caddy/server1/main.go @@ -2,6 +2,7 @@ package main import ( "github.com/kataras/iris" + "github.com/kataras/iris/mvc" ) func main() { @@ -10,7 +11,7 @@ func main() { templates := iris.HTML("./views", ".html").Layout("shared/layout.html") app.RegisterView(templates) - app.Controller("/", new(Controller)) + mvc.New(app).Handle(new(Controller)) // http://localhost:9091 app.Run(iris.Addr(":9091")) @@ -21,22 +22,28 @@ type Layout struct { Title string } -// Controller is our example controller. +// Controller is our example controller, request-scoped, each request has its own instance. type Controller struct { - iris.Controller - - Layout Layout `iris:"model"` + Layout Layout } -// BeginRequest is the first method fires when client requests from this Controller's path. +// BeginRequest is the first method fired when client requests from this Controller's root path. func (c *Controller) BeginRequest(ctx iris.Context) { - c.Controller.BeginRequest(ctx) - c.Layout.Title = "Home Page" } +// EndRequest is the last method fired. +// It's here just to complete the BaseController +// in order to be tell iris to call the `BeginRequest` before the main method. +func (c *Controller) EndRequest(ctx iris.Context) {} + // Get handles GET http://localhost:9091 -func (c *Controller) Get() { - c.Tmpl = "index.html" - c.Data["Message"] = "Welcome to my website!" +func (c *Controller) Get() mvc.View { + return mvc.View{ + Name: "index.html", + Data: iris.Map{ + "Layout": c.Layout, + "Message": "Welcome to my website!", + }, + } } diff --git a/_examples/tutorial/caddy/server2/main.go b/_examples/tutorial/caddy/server2/main.go index 72d9e2fd94..260df11254 100644 --- a/_examples/tutorial/caddy/server2/main.go +++ b/_examples/tutorial/caddy/server2/main.go @@ -2,13 +2,18 @@ package main import ( "github.com/kataras/iris" + "github.com/kataras/iris/mvc" ) -func main() { +type postValue func(string) string +func main() { app := iris.New() - app.Controller("/user", new(UserController)) + mvc.New(app.Party("/user")).Register( + func(ctx iris.Context) postValue { + return ctx.PostValue + }).Handle(new(UserController)) // GET http://localhost:9092/user // GET http://localhost:9092/user/42 @@ -20,37 +25,44 @@ func main() { } // UserController is our user example controller. -type UserController struct { - iris.Controller -} +type UserController struct{} // Get handles GET /user -func (c *UserController) Get() { - c.Ctx.Writef("Select all users") +func (c *UserController) Get() string { + return "Select all users" } -// GetBy handles GET /user/42 -func (c *UserController) GetBy(id int) { - c.Ctx.Writef("Select user by ID: %d", id) +// User is our test User model, nothing tremendous here. +type User struct{ ID int64 } + +// GetBy handles GET /user/42, equal to .Get("/user/{id:long}") +func (c *UserController) GetBy(id int64) User { + // Select User by ID == $id. + return User{id} } // Post handles POST /user -func (c *UserController) Post() { - username := c.Ctx.PostValue("username") - c.Ctx.Writef("Create by user with username: %s", username) +func (c *UserController) Post(post postValue) string { + username := post("username") + return "Create by user with username: " + username } // PutBy handles PUT /user/42 -func (c *UserController) PutBy(id int) { - c.Ctx.Writef("Update user by ID: %d", id) +func (c *UserController) PutBy(id int) string { + // Update user by ID == $id + return "User updated" } // DeleteBy handles DELETE /user/42 -func (c *UserController) DeleteBy(id int) { - c.Ctx.Writef("Delete user by ID: %d", id) +func (c *UserController) DeleteBy(id int) bool { + // Delete user by ID == %id + // + // when boolean then true = iris.StatusOK, false = iris.StatusNotFound + return true } // GetFollowersBy handles GET /user/followers/42 -func (c *UserController) GetFollowersBy(id int) { - c.Ctx.Writef("Select all followers by user ID: %d", id) +func (c *UserController) GetFollowersBy(id int) []User { + // Select all followers by user ID == $id + return []User{ /* ... */ } } diff --git a/_examples/tutorial/vuejs-todo-mvc/README.md b/_examples/tutorial/vuejs-todo-mvc/README.md new file mode 100644 index 0000000000..00c912fa50 --- /dev/null +++ b/_examples/tutorial/vuejs-todo-mvc/README.md @@ -0,0 +1,567 @@ +# A Todo MVC Application using Iris and Vue.js + +Vue.js is a front-end framework for building web applications using javascript. It has a blazing fast Virtual DOM renderer. + +Iris is a back-end framework for building web applications using The Go Programming Language (disclaimer: author here). It's one of the fastest and featured web frameworks out there. We wanna use this to serve our "todo service". + +## The Tools + +Programming Languages are just tools for us, but we need a safe, fast and “cross-platform” programming language to power our service. + +[Go](https://golang.org) is a [rapidly growing](https://www.tiobe.com/tiobe-index/) open source programming language designed for building simple, fast, and reliable software. Take a look [here](https://github.com/golang/go/wiki/GoUsers) which great companies use Go to power their services. + +### Install the Go Programming Language + +Extensive information about downloading & installing Go can be found [here](https://golang.org/dl/). + +[![](https://i3.ytimg.com/vi/9x-pG3lvLi0/hqdefault.jpg)](https://youtu.be/9x-pG3lvLi0) + +> Maybe [Windows](https://www.youtube.com/watch?v=WT5mTznJBS0) or [Mac OS X](https://www.youtube.com/watch?v=5qI8z_lB5Lw) user? + +> The article does not contain an introduction to the language itself, if you’re a newcomer I recommend you to bookmark this article, [learn](https://github.com/golang/go/wiki/Learn) the language’s fundamentals and come back later on. + +## The Dependencies + +Many articles have been written, in the past, that lead developers not to use a web framework because they are useless and "bad". I have to tell you that there is no such thing, it always depends on the (web) framework that you’re going to use. At production environment, we don’t have the time or the experience to code everything that we wanna use in the applications, and if we could are we sure that we can do better and safely than others? In short term: **Good frameworks are helpful tools for any developer, company or startup and "bad" frameworks are waste of time, crystal clear.** + +You’ll need two dependencies: + +1. Vue.js, for our client-side requirements. Download it from [here](https://vuejs.org/), latest v2. +2. The Iris Web Framework, for our server-side requirements. Can be found [here](https://github.com/kataras/iris), latest v10. + +> If you have Go already installed then just execute `go get -u github.com/kataras/iris` to install the Iris Web Framework. + +## Start + +If we are all in the same page, it’s time to learn how we can create a live todo application that will be easy to deploy and extend even more! + +We're going to use a vue.js todo application which uses browser' +s local storage and doesn't have any user-specified features like live sync between browser's tabs, you can find the original version inside the vue's [docs](https://vuejs.org/v2/examples/todomvc.html). + +Assuming that you know how %GOPATH% works, create an empty folder, i.e "vuejs-todo-mvc" in the %GOPATH%/src directory, there you will create those files: + +- web/public/js/app.js +- web/public/index.html +- todo/item.go +- todo/service.go +- web/controllers/todo_controller.go +- web/main.go + +_Read the comments in the source code, they may be very helpful_ + +### The client-side (vue.js) + +```js +/* file: vuejs-todo-mvc/web/public/js/app.js */ +// Full spec-compliant TodoMVC with Iris +// and hash-based routing in ~200 effective lines of JavaScript. + +var socket = new Ws("ws://localhost:8080/todos/sync"); + +socket.On("saved", function () { + // console.log("receive: on saved"); + fetchTodos(function (items) { + app.todos = items + }); +}); + + +function fetchTodos(onComplete) { + axios.get("/todos").then(response => { + if (response.data === null) { + return; + } + + onComplete(response.data); + }); +} + +var todoStorage = { + fetch: function () { + var todos = []; + fetchTodos(function (items) { + for (var i = 0; i < items.length; i++) { + todos.push(items[i]); + } + }); + return todos; + }, + save: function (todos) { + axios.post("/todos", JSON.stringify(todos)).then(response => { + if (!response.data.success) { + window.alert("saving had a failure"); + return; + } + // console.log("send: save"); + socket.Emit("save") + }); + } +} + +// visibility filters +var filters = { + all: function (todos) { + return todos + }, + active: function (todos) { + return todos.filter(function (todo) { + return !todo.completed + }) + }, + completed: function (todos) { + return todos.filter(function (todo) { + return todo.completed + }) + } +} + +// app Vue instance +var app = new Vue({ + // app initial state + data: { + todos: todoStorage.fetch(), + newTodo: '', + editedTodo: null, + visibility: 'all' + }, + + // we will not use the "watch" as it works with the fields like "hasChanges" + // and callbacks to make it true but let's keep things very simple as it's just a small getting started. + // // watch todos change for persistence + // watch: { + // todos: { + // handler: function (todos) { + // if (app.hasChanges) { + // todoStorage.save(todos); + // app.hasChanges = false; + // } + + // }, + // deep: true + // } + // }, + + // computed properties + // http://vuejs.org/guide/computed.html + computed: { + filteredTodos: function () { + return filters[this.visibility](this.todos) + }, + remaining: function () { + return filters.active(this.todos).length + }, + allDone: { + get: function () { + return this.remaining === 0 + }, + set: function (value) { + this.todos.forEach(function (todo) { + todo.completed = value + }) + this.notifyChange(); + } + } + }, + + filters: { + pluralize: function (n) { + return n === 1 ? 'item' : 'items' + } + }, + + // methods that implement data logic. + // note there's no DOM manipulation here at all. + methods: { + notifyChange: function () { + todoStorage.save(this.todos) + }, + addTodo: function () { + var value = this.newTodo && this.newTodo.trim() + if (!value) { + return + } + this.todos.push({ + id: this.todos.length + 1, // just for the client-side. + title: value, + completed: false + }) + this.newTodo = '' + this.notifyChange(); + }, + + completeTodo: function (todo) { + if (todo.completed) { + todo.completed = false; + } else { + todo.completed = true; + } + this.notifyChange(); + }, + removeTodo: function (todo) { + this.todos.splice(this.todos.indexOf(todo), 1) + this.notifyChange(); + }, + + editTodo: function (todo) { + this.beforeEditCache = todo.title + this.editedTodo = todo + }, + + doneEdit: function (todo) { + if (!this.editedTodo) { + return + } + this.editedTodo = null + todo.title = todo.title.trim(); + if (!todo.title) { + this.removeTodo(todo); + } + this.notifyChange(); + }, + + cancelEdit: function (todo) { + this.editedTodo = null + todo.title = this.beforeEditCache + }, + + removeCompleted: function () { + this.todos = filters.active(this.todos); + this.notifyChange(); + } + }, + + // a custom directive to wait for the DOM to be updated + // before focusing on the input field. + // http://vuejs.org/guide/custom-directive.html + directives: { + 'todo-focus': function (el, binding) { + if (binding.value) { + el.focus() + } + } + } +}) + +// handle routing +function onHashChange() { + var visibility = window.location.hash.replace(/#\/?/, '') + if (filters[visibility]) { + app.visibility = visibility + } else { + window.location.hash = '' + app.visibility = 'all' + } +} + +window.addEventListener('hashchange', onHashChange) +onHashChange() + +// mount +app.$mount('.todoapp') +``` + +Let's add our view, the static html. + +```html + + + + + + + Iris + Vue.js • TodoMVC + + + + + + + + + + + + + + +
+
+

todos

+ +
+
+ +
    +
  • +
    + + + + +
    + +
  • +
+
+
+ + {{ remaining }} {{ remaining | pluralize }} left + + + +
+
+ + + + + + +``` + +### The server-side (iris) + +Our view model. + +```go +// file: vuejs-todo-mvc/todo/item.go +package todo + +type Item struct { + SessionID string `json:"-"` + ID int64 `json:"id,omitempty"` + Title string `json:"title"` + Completed bool `json:"completed"` +} +``` + +Our service. + +```go +// file: vuejs-todo-mvc/todo/service.go +package todo + +import ( + "sync" +) + +type Service interface { + Get(owner string) []Item + Save(owner string, newItems []Item) error +} + +type MemoryService struct { + // key = session id, value the list of todo items that this session id has. + items map[string][]Item + // protected by locker for concurrent access. + mu sync.RWMutex +} + +func NewMemoryService() *MemoryService { + return &MemoryService{ + items: make(map[string][]Item, 0), + } +} + +func (s *MemoryService) Get(sessionOwner string) []Item { + s.mu.RLock() + items := s.items[sessionOwner] + s.mu.RUnlock() + + return items +} + +func (s *MemoryService) Save(sessionOwner string, newItems []Item) error { + var prevID int64 + for i := range newItems { + if newItems[i].ID == 0 { + newItems[i].ID = prevID + prevID++ + } + } + + s.mu.Lock() + s.items[sessionOwner] = newItems + s.mu.Unlock() + return nil +} +``` + +We are going to use some of the MVC functionalities of the iris web framework here but you can do the same with the standard API as well. + +```go +// file: vuejs-todo-mvc/controllers/todo_controller.go +package controllers + +import ( + "vuejs-todo-mvc/todo" + + "github.com/kataras/iris" + "github.com/kataras/iris/mvc" + "github.com/kataras/iris/sessions" + "github.com/kataras/iris/websocket" +) + +// TodoController is our TODO app's web controller. +type TodoController struct { + Service todo.Service + + Session *sessions.Session +} + +// BeforeActivation called once before the server ran, and before +// the routes and dependencies binded. +// You can bind custom things to the controller, add new methods, add middleware, +// add dependencies to the struct or the method(s) and more. +func (c *TodoController) BeforeActivation(b mvc.BeforeActivation) { + // this could be binded to a controller's function input argument + // if any, or struct field if any: + b.Dependencies().Add(func(ctx iris.Context) (items []todo.Item) { + ctx.ReadJSON(&items) + return + }) +} + +// Get handles the GET: /todos route. +func (c *TodoController) Get() []todo.Item { + return c.Service.Get(c.Session.ID()) +} + +// PostItemResponse the response data that will be returned as json +// after a post save action of all todo items. +type PostItemResponse struct { + Success bool `json:"success"` +} + +var emptyResponse = PostItemResponse{Success: false} + +// Post handles the POST: /todos route. +func (c *TodoController) Post(newItems []todo.Item) PostItemResponse { + if err := c.Service.Save(c.Session.ID(), newItems); err != nil { + return emptyResponse + } + + return PostItemResponse{Success: true} +} + +func (c *TodoController) GetSync(conn websocket.Connection) { + conn.Join(c.Session.ID()) + conn.On("save", func() { // "save" event from client. + conn.To(c.Session.ID()).Emit("saved", nil) // fire a "saved" event to the rest of the clients w. + }) + + conn.Wait() +} +``` + +And finally our main application's endpoint. + +```go +// file: web/main.go +package main + +import ( + "vuejs-todo-mvc/todo" + "vuejs-todo-mvc/web/controllers" + + "github.com/kataras/iris" + "github.com/kataras/iris/sessions" + "github.com/kataras/iris/websocket" + + "github.com/kataras/iris/mvc" +) + +func main() { + app := iris.New() + + // serve our app in public, public folder + // contains the client-side vue.js application, + // no need for any server-side template here, + // actually if you're going to just use vue without any + // back-end services, you can just stop afer this line and start the server. + app.StaticWeb("/", "./public") + + // configure the http sessions. + sess := sessions.New(sessions.Config{ + Cookie: "iris_session", + }) + + // configure the websocket server. + ws := websocket.New(websocket.Config{}) + + // create a sub router and register the client-side library for the iris websockets, + // you could skip it but iris websockets supports socket.io-like API. + todosRouter := app.Party("/todos") + // http://localhost:8080/todos/iris-ws.js + // serve the javascript client library to communicate with + // the iris high level websocket event system. + todosRouter.Any("/iris-ws.js", websocket.ClientHandler()) + + // create our mvc application targeted to /todos relative sub path. + todosApp := mvc.New(todosRouter) + + // any dependencies bindings here... + todosApp.Register( + todo.NewMemoryService(), + sess.Start, + ws.Upgrade, + ) + + // controllers registration here... + todosApp.Handle(new(controllers.TodoController)) + + // start the web server at http://localhost:8080 + app.Run(iris.Addr(":8080"), iris.WithoutVersionChecker) +} +``` + +Run the Iris web server you've just created by executing `go run main.go` from your current path (%GOPATH%/src/%your_folder%/web/). + +```sh +$ go run main.go +Now listening on: http://localhost:8080 +Application Started. Press CTRL+C to shut down. +_ +``` + +Open one or more browser tabs at: http://localhost:8080 and have fun! + +![](screen.png) + +### Download the Source Code + +The whole project, all the files you saw in this article are located at: https://github.com/kataras/iris/tree/master/_examples/tutorial/vuejs-todo-mvc + +## References + +https://vuejs.org/v2/examples/todomvc.html (using browser's local storage) + +https://github.com/kataras/iris/tree/master/_examples/mvc (mvc examples and features overview repository) + +## Thank you, once again + +Happy new year and thank you for your pattience, once again:) Don't hesitate to post any questions and provide feedback(I'm very active dev therefore you will be heard here!) + +Don't forget to check out my medium profile and twitter as well, I'm posting some (useful) stuff there too:) + +- https://medium.com/@kataras +- https://twitter.com/MakisMaropoulos diff --git a/_examples/tutorial/vuejs-todo-mvc/screen.png b/_examples/tutorial/vuejs-todo-mvc/screen.png new file mode 100644 index 0000000000..41df240d7f Binary files /dev/null and b/_examples/tutorial/vuejs-todo-mvc/screen.png differ diff --git a/_examples/tutorial/vuejs-todo-mvc/src/todo/item.go b/_examples/tutorial/vuejs-todo-mvc/src/todo/item.go new file mode 100644 index 0000000000..eb464be25c --- /dev/null +++ b/_examples/tutorial/vuejs-todo-mvc/src/todo/item.go @@ -0,0 +1,8 @@ +package todo + +type Item struct { + SessionID string `json:"-"` + ID int64 `json:"id,omitempty"` + Title string `json:"title"` + Completed bool `json:"completed"` +} diff --git a/_examples/tutorial/vuejs-todo-mvc/src/todo/service.go b/_examples/tutorial/vuejs-todo-mvc/src/todo/service.go new file mode 100644 index 0000000000..d9af2854b1 --- /dev/null +++ b/_examples/tutorial/vuejs-todo-mvc/src/todo/service.go @@ -0,0 +1,46 @@ +package todo + +import ( + "sync" +) + +type Service interface { + Get(owner string) []Item + Save(owner string, newItems []Item) error +} + +type MemoryService struct { + // key = session id, value the list of todo items that this session id has. + items map[string][]Item + // protected by locker for concurrent access. + mu sync.RWMutex +} + +func NewMemoryService() *MemoryService { + return &MemoryService{ + items: make(map[string][]Item, 0), + } +} + +func (s *MemoryService) Get(sessionOwner string) []Item { + s.mu.RLock() + items := s.items[sessionOwner] + s.mu.RUnlock() + + return items +} + +func (s *MemoryService) Save(sessionOwner string, newItems []Item) error { + var prevID int64 + for i := range newItems { + if newItems[i].ID == 0 { + newItems[i].ID = prevID + prevID++ + } + } + + s.mu.Lock() + s.items[sessionOwner] = newItems + s.mu.Unlock() + return nil +} diff --git a/_examples/tutorial/vuejs-todo-mvc/src/web/controllers/todo_controller.go b/_examples/tutorial/vuejs-todo-mvc/src/web/controllers/todo_controller.go new file mode 100644 index 0000000000..c3fd0724f1 --- /dev/null +++ b/_examples/tutorial/vuejs-todo-mvc/src/web/controllers/todo_controller.go @@ -0,0 +1,65 @@ +package controllers + +import ( + "github.com/kataras/iris/_examples/tutorial/vuejs-todo-mvc/src/todo" + + "github.com/kataras/iris" + "github.com/kataras/iris/mvc" + "github.com/kataras/iris/sessions" + "github.com/kataras/iris/websocket" +) + +// TodoController is our TODO app's web controller. +type TodoController struct { + Service todo.Service + + Session *sessions.Session +} + +// BeforeActivation called once before the server ran, and before +// the routes and dependencies binded. +// You can bind custom things to the controller, add new methods, add middleware, +// add dependencies to the struct or the method(s) and more. +func (c *TodoController) BeforeActivation(b mvc.BeforeActivation) { + // this could be binded to a controller's function input argument + // if any, or struct field if any: + b.Dependencies().Add(func(ctx iris.Context) (items []todo.Item) { + ctx.ReadJSON(&items) + return + }) +} + +// Get handles the GET: /todos route. +func (c *TodoController) Get() []todo.Item { + return c.Service.Get(c.Session.ID()) +} + +// PostItemResponse the response data that will be returned as json +// after a post save action of all todo items. +type PostItemResponse struct { + Success bool `json:"success"` +} + +var emptyResponse = PostItemResponse{Success: false} + +// Post handles the POST: /todos route. +func (c *TodoController) Post(newItems []todo.Item) PostItemResponse { + if err := c.Service.Save(c.Session.ID(), newItems); err != nil { + return emptyResponse + } + + return PostItemResponse{Success: true} +} + +func (c *TodoController) GetSync(conn websocket.Connection) { + // join to the session in order to send "saved" + // events only to a single user, that means + // that if user has opened more than one browser window/tab + // of the same session then the changes will be reflected to one another. + conn.Join(c.Session.ID()) + conn.On("save", func() { // "save" event from client. + conn.To(c.Session.ID()).Emit("saved", nil) // fire a "saved" event to the rest of the clients w. + }) + + conn.Wait() +} diff --git a/_examples/tutorial/vuejs-todo-mvc/src/web/main.go b/_examples/tutorial/vuejs-todo-mvc/src/web/main.go new file mode 100644 index 0000000000..c571e42630 --- /dev/null +++ b/_examples/tutorial/vuejs-todo-mvc/src/web/main.go @@ -0,0 +1,55 @@ +package main + +import ( + "github.com/kataras/iris/_examples/tutorial/vuejs-todo-mvc/src/todo" + "github.com/kataras/iris/_examples/tutorial/vuejs-todo-mvc/src/web/controllers" + + "github.com/kataras/iris" + "github.com/kataras/iris/sessions" + "github.com/kataras/iris/websocket" + + "github.com/kataras/iris/mvc" +) + +func main() { + app := iris.New() + + // serve our app in public, public folder + // contains the client-side vue.js application, + // no need for any server-side template here, + // actually if you're going to just use vue without any + // back-end services, you can just stop afer this line and start the server. + app.StaticWeb("/", "./public") + + // configure the http sessions. + sess := sessions.New(sessions.Config{ + Cookie: "iris_session", + }) + + // configure the websocket server. + ws := websocket.New(websocket.Config{}) + + // create a sub router and register the client-side library for the iris websockets, + // you could skip it but iris websockets supports socket.io-like API. + todosRouter := app.Party("/todos") + // http://localhost:8080/todos/iris-ws.js + // serve the javascript client library to communicate with + // the iris high level websocket event system. + todosRouter.Any("/iris-ws.js", websocket.ClientHandler()) + + // create our mvc application targeted to /todos relative sub path. + todosApp := mvc.New(todosRouter) + + // any dependencies bindings here... + todosApp.Register( + todo.NewMemoryService(), + sess.Start, + ws.Upgrade, + ) + + // controllers registration here... + todosApp.Handle(new(controllers.TodoController)) + + // start the web server at http://localhost:8080 + app.Run(iris.Addr(":8080"), iris.WithoutVersionChecker) +} diff --git a/_examples/tutorial/vuejs-todo-mvc/src/web/public/css/index b/_examples/tutorial/vuejs-todo-mvc/src/web/public/css/index new file mode 100644 index 0000000000..3aea0097d7 --- /dev/null +++ b/_examples/tutorial/vuejs-todo-mvc/src/web/public/css/index @@ -0,0 +1,2 @@ +index.css is not here to reduce the disk space for the examples. +https://unpkg.com/todomvc-app-css@2.0.4/index.css is used instead. \ No newline at end of file diff --git a/_examples/tutorial/vuejs-todo-mvc/src/web/public/index.html b/_examples/tutorial/vuejs-todo-mvc/src/web/public/index.html new file mode 100644 index 0000000000..38cca166ae --- /dev/null +++ b/_examples/tutorial/vuejs-todo-mvc/src/web/public/index.html @@ -0,0 +1,72 @@ + + + + + + Iris + Vue.js • TodoMVC + + + + + + + + + + + + + + +
+
+

todos

+ +
+
+ +
    +
  • +
    + + + + +
    + +
  • +
+
+
+ + {{ remaining }} {{ remaining | pluralize }} left + + + +
+
+ + + + + + \ No newline at end of file diff --git a/_examples/tutorial/vuejs-todo-mvc/src/web/public/js/app.js b/_examples/tutorial/vuejs-todo-mvc/src/web/public/js/app.js new file mode 100644 index 0000000000..ab30eee85a --- /dev/null +++ b/_examples/tutorial/vuejs-todo-mvc/src/web/public/js/app.js @@ -0,0 +1,205 @@ +// Full spec-compliant TodoMVC with Iris +// and hash-based routing in ~200 effective lines of JavaScript. + +var socket = new Ws("ws://localhost:8080/todos/sync"); + +socket.On("saved", function () { + // console.log("receive: on saved"); + fetchTodos(function (items) { + app.todos = items + }); +}); + + +function fetchTodos(onComplete) { + axios.get("/todos").then(response => { + if (response.data === null) { + return; + } + + onComplete(response.data); + }); +} + +var todoStorage = { + fetch: function () { + var todos = []; + fetchTodos(function (items) { + for (var i = 0; i < items.length; i++) { + todos.push(items[i]); + } + }); + return todos; + }, + save: function (todos) { + axios.post("/todos", JSON.stringify(todos)).then(response => { + if (!response.data.success) { + window.alert("saving had a failure"); + return; + } + // console.log("send: save"); + socket.Emit("save") + }); + } +} + +// visibility filters +var filters = { + all: function (todos) { + return todos + }, + active: function (todos) { + return todos.filter(function (todo) { + return !todo.completed + }) + }, + completed: function (todos) { + return todos.filter(function (todo) { + return todo.completed + }) + } +} + +// app Vue instance +var app = new Vue({ + // app initial state + data: { + todos: todoStorage.fetch(), + newTodo: '', + editedTodo: null, + visibility: 'all' + }, + + // we will not use the "watch" as it works with the fields like "hasChanges" + // and callbacks to make it true but let's keep things very simple as it's just a small getting started. + // // watch todos change for persistence + // watch: { + // todos: { + // handler: function (todos) { + // if (app.hasChanges) { + // todoStorage.save(todos); + // app.hasChanges = false; + // } + + // }, + // deep: true + // } + // }, + + // computed properties + // http://vuejs.org/guide/computed.html + computed: { + filteredTodos: function () { + return filters[this.visibility](this.todos) + }, + remaining: function () { + return filters.active(this.todos).length + }, + allDone: { + get: function () { + return this.remaining === 0 + }, + set: function (value) { + this.todos.forEach(function (todo) { + todo.completed = value + }) + this.notifyChange(); + } + } + }, + + filters: { + pluralize: function (n) { + return n === 1 ? 'item' : 'items' + } + }, + + // methods that implement data logic. + // note there's no DOM manipulation here at all. + methods: { + notifyChange: function () { + todoStorage.save(this.todos) + }, + addTodo: function () { + var value = this.newTodo && this.newTodo.trim() + if (!value) { + return + } + this.todos.push({ + id: this.todos.length + 1, // just for the client-side. + title: value, + completed: false + }) + this.newTodo = '' + this.notifyChange(); + }, + + completeTodo: function (todo) { + if (todo.completed) { + todo.completed = false; + } else { + todo.completed = true; + } + this.notifyChange(); + }, + removeTodo: function (todo) { + this.todos.splice(this.todos.indexOf(todo), 1) + this.notifyChange(); + }, + + editTodo: function (todo) { + this.beforeEditCache = todo.title + this.editedTodo = todo + }, + + doneEdit: function (todo) { + if (!this.editedTodo) { + return + } + this.editedTodo = null + todo.title = todo.title.trim(); + if (!todo.title) { + this.removeTodo(todo); + } + this.notifyChange(); + }, + + cancelEdit: function (todo) { + this.editedTodo = null + todo.title = this.beforeEditCache + }, + + removeCompleted: function () { + this.todos = filters.active(this.todos); + this.notifyChange(); + } + }, + + // a custom directive to wait for the DOM to be updated + // before focusing on the input field. + // http://vuejs.org/guide/custom-directive.html + directives: { + 'todo-focus': function (el, binding) { + if (binding.value) { + el.focus() + } + } + } +}) + +// handle routing +function onHashChange() { + var visibility = window.location.hash.replace(/#\/?/, '') + if (filters[visibility]) { + app.visibility = visibility + } else { + window.location.hash = '' + app.visibility = 'all' + } +} + +window.addEventListener('hashchange', onHashChange) +onHashChange() + +// mount +app.$mount('.todoapp') \ No newline at end of file diff --git a/_examples/tutorial/vuejs-todo-mvc/src/web/public/js/lib/vue b/_examples/tutorial/vuejs-todo-mvc/src/web/public/js/lib/vue new file mode 100644 index 0000000000..8da03f4896 --- /dev/null +++ b/_examples/tutorial/vuejs-todo-mvc/src/web/public/js/lib/vue @@ -0,0 +1,2 @@ +vue.js is not here to reduce the disk space for the examples. +Instead https://vuejs.org/js/vue.js is used instead. \ No newline at end of file diff --git a/_examples/websocket/README.md b/_examples/websocket/README.md index 35ca1cbd74..c7d9c68f4e 100644 --- a/_examples/websocket/README.md +++ b/_examples/websocket/README.md @@ -45,8 +45,11 @@ type Config struct { // 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) + // Error is the function that will be fired if any client couldn't upgrade the HTTP connection + // to a websocket connection, a handshake error. + Error func(w http.ResponseWriter, r *http.Request, status int, reason error) + // CheckOrigin a function that is called right before the handshake, + // if returns false then that client is not allowed to connect with the websocket server. CheckOrigin func(r *http.Request) bool // HandshakeTimeout specifies the duration for the handshake to complete. HandshakeTimeout time.Duration diff --git a/_examples/websocket/chat/main.go b/_examples/websocket/chat/main.go index d3b33a0db6..d1adeacb53 100644 --- a/_examples/websocket/chat/main.go +++ b/_examples/websocket/chat/main.go @@ -47,8 +47,9 @@ func handleConnection(c websocket.Connection) { 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: + // Write message back to the client message owner with: // c.Emit("chat", msg) + // Write message to all except this client with: c.To(websocket.Broadcast).Emit("chat", msg) }) } diff --git a/_examples/websocket/chat/websockets.html b/_examples/websocket/chat/websockets.html index bd594cc3b2..3604bfd05a 100644 --- a/_examples/websocket/chat/websockets.html +++ b/_examples/websocket/chat/websockets.html @@ -1,39 +1,45 @@ + + + + +

+
 
+
 
\ No newline at end of file
diff --git a/_examples/websocket/custom-go-client/main.go b/_examples/websocket/custom-go-client/main.go
index d2965aff04..dde7560a59 100644
--- a/_examples/websocket/custom-go-client/main.go
+++ b/_examples/websocket/custom-go-client/main.go
@@ -85,7 +85,7 @@ func SendMessage(serverID, to, method, message string) error {
 
 // SendtBytes broadcast a message to server
 func SendtBytes(serverID, to, method string, message []byte) error {
-	// look https://github.com/kataras/iris/tree/master/adaptors/websocket/message.go , client.go and client.js
+	// look https://github.com/kataras/iris/blob/master/websocket/message.go , client.go and client.js
 	// to understand the buffer line:
 	buffer := []byte(fmt.Sprintf("iris-websocket-message:%v;0;%v;%v;", method, serverID, to))
 	buffer = append(buffer, message...)
diff --git a/configuration.go b/configuration.go
index 885555f2e2..02526963b8 100644
--- a/configuration.go
+++ b/configuration.go
@@ -315,6 +315,18 @@ func WithCharset(charset string) Configurator {
 	}
 }
 
+// WithPostMaxMemory sets the maximum post data size
+// that a client can send to the server, this differs
+// from the overral request body size which can be modified
+// by the `context#SetMaxRequestBodySize` or `iris#LimitRequestBodySize`.
+//
+// Defaults to 32MB or 32 << 20 if you prefer.
+func WithPostMaxMemory(limit int64) Configurator {
+	return func(app *Application) {
+		app.config.PostMaxMemory = limit
+	}
+}
+
 // WithRemoteAddrHeader enables or adds a new or existing request header name
 // that can be used to validate the client's real IP.
 //
@@ -463,6 +475,13 @@ type Configuration struct {
 	// Defaults to "UTF-8".
 	Charset string `json:"charset,omitempty" yaml:"Charset" toml:"Charset"`
 
+	// PostMaxMemory sets the maximum post data size
+	// that a client can send to the server, this differs
+	// from the overral request body size which can be modified
+	// by the `context#SetMaxRequestBodySize` or `iris#LimitRequestBodySize`.
+	//
+	// Defaults to 32MB or 32 << 20 if you prefer.
+	PostMaxMemory int64 `json:"postMaxMemory" yaml:"PostMaxMemory" toml:"PostMaxMemory"`
 	//  +----------------------------------------------------+
 	//  | Context's keys for values used on various featuers |
 	//  +----------------------------------------------------+
@@ -504,8 +523,8 @@ type Configuration struct {
 	RemoteAddrHeaders map[string]bool `json:"remoteAddrHeaders,omitempty" yaml:"RemoteAddrHeaders" toml:"RemoteAddrHeaders"`
 
 	// Other are the custom, dynamic options, can be empty.
-	// This field used only by you to set any app's options you want
-	// or by custom adaptors, it's a way to simple communicate between your adaptors (if any)
+	// This field used only by you to set any app's options you want.
+	//
 	// Defaults to a non-nil empty map.
 	Other map[string]interface{} `json:"other,omitempty" yaml:"Other" toml:"Other"`
 }
@@ -579,6 +598,16 @@ func (c Configuration) GetCharset() string {
 	return c.Charset
 }
 
+// GetPostMaxMemory returns the maximum configured post data size
+// that a client can send to the server, this differs
+// from the overral request body size which can be modified
+// by the `context#SetMaxRequestBodySize` or `iris#LimitRequestBodySize`.
+//
+// Defaults to 32MB or 32 << 20 if you prefer.
+func (c Configuration) GetPostMaxMemory() int64 {
+	return c.PostMaxMemory
+}
+
 // GetTranslateFunctionContextKey returns the configuration's TranslateFunctionContextKey value,
 // used for i18n.
 func (c Configuration) GetTranslateFunctionContextKey() string {
@@ -684,6 +713,10 @@ func WithConfiguration(c Configuration) Configurator {
 			main.Charset = v
 		}
 
+		if v := c.PostMaxMemory; v > 0 {
+			main.PostMaxMemory = v
+		}
+
 		if v := c.TranslateFunctionContextKey; v != "" {
 			main.TranslateFunctionContextKey = v
 		}
@@ -733,10 +766,17 @@ func DefaultConfiguration() Configuration {
 		DisableAutoFireStatusCode:         false,
 		TimeFormat:                        "Mon, Jan 02 2006 15:04:05 GMT",
 		Charset:                           "UTF-8",
-		TranslateFunctionContextKey:       "iris.translate",
-		TranslateLanguageContextKey:       "iris.language",
-		ViewLayoutContextKey:              "iris.viewLayout",
-		ViewDataContextKey:                "iris.viewData",
+
+		// PostMaxMemory is for post body max memory.
+		//
+		// The request body the size limit
+		// can be set by the middleware `LimitRequestBodySize`
+		// or `context#SetMaxRequestBodySize`.
+		PostMaxMemory:               32 << 20, // 32MB
+		TranslateFunctionContextKey: "iris.translate",
+		TranslateLanguageContextKey: "iris.language",
+		ViewLayoutContextKey:        "iris.viewLayout",
+		ViewDataContextKey:          "iris.viewData",
 		RemoteAddrHeaders: map[string]bool{
 			"X-Real-Ip":        false,
 			"X-Forwarded-For":  false,
diff --git a/context/configuration.go b/context/configuration.go
index 4ad53ef850..20ff827fb8 100644
--- a/context/configuration.go
+++ b/context/configuration.go
@@ -54,6 +54,14 @@ type ConfigurationReadOnly interface {
 	// used for templates and the rest of the responses.
 	GetCharset() string
 
+	// GetPostMaxMemory returns the maximum configured post data size
+	// that a client can send to the server, this differs
+	// from the overral request body size which can be modified
+	// by the `context#SetMaxRequestBodySize` or `iris#LimitRequestBodySize`.
+	//
+	// Defaults to 32MB or 32 << 20 if you prefer.
+	GetPostMaxMemory() int64
+
 	// GetTranslateLanguageContextKey returns the configuration's TranslateFunctionContextKey value,
 	// used for i18n.
 	GetTranslateFunctionContextKey() string
diff --git a/context/context.go b/context/context.go
index 3b50f6a377..19a38df455 100644
--- a/context/context.go
+++ b/context/context.go
@@ -19,6 +19,7 @@ import (
 	"regexp"
 	"strconv"
 	"strings"
+	"sync/atomic"
 	"time"
 
 	"github.com/fatih/structs"
@@ -26,6 +27,7 @@ import (
 	"github.com/json-iterator/go"
 	"github.com/microcosm-cc/bluemonday"
 	"github.com/russross/blackfriday"
+	"gopkg.in/yaml.v2"
 
 	"github.com/kataras/iris/core/errors"
 	"github.com/kataras/iris/core/memstore"
@@ -91,15 +93,27 @@ func (r *RequestParams) Visit(visitor func(key string, value string)) {
 	})
 }
 
-// GetEntry returns the internal Entry of the memstore, as value
-// if not found then it returns a zero Entry and false.
+var emptyEntry memstore.Entry
+
+// GetEntryAt returns the internal Entry of the memstore based on its index,
+// the stored index by the router.
+// If not found then it returns a zero Entry and false.
+func (r RequestParams) GetEntryAt(index int) (memstore.Entry, bool) {
+	if len(r.store) > index {
+		return r.store[index], true
+	}
+	return emptyEntry, false
+}
+
+// GetEntry returns the internal Entry of the memstore based on its "key".
+// If not found then it returns a zero Entry and false.
 func (r RequestParams) GetEntry(key string) (memstore.Entry, bool) {
 	// we don't return the pointer here, we don't want to give the end-developer
 	// the strength to change the entry that way.
 	if e := r.store.GetEntry(key); e != nil {
 		return *e, true
 	}
-	return memstore.Entry{}, false
+	return emptyEntry, false
 }
 
 // Get returns a path parameter's value based on its route's dynamic path key.
@@ -266,8 +280,7 @@ type Context interface {
 	// Although `BeginRequest` should NOT be used to call other handlers,
 	// the `BeginRequest` has been introduced to be able to set
 	// common data to all method handlers before their execution.
-	// Controllers can accept middleware(s) from the `app.Controller`
-	// function.
+	// Controllers can accept middleware(s) from the MVC's Application's Router as normally.
 	//
 	// That said let's see an example of `ctx.Proceed`:
 	//
@@ -278,7 +291,6 @@ type Context interface {
 	// })
 	//
 	// func (c *UsersController) BeginRequest(ctx iris.Context) {
-	//	c.C.BeginRequest(ctx) // call the parent's base controller BeginRequest first.
 	// 	if !ctx.Proceed(authMiddleware) {
 	// 		ctx.StopExecution()
 	// 	}
@@ -463,52 +475,104 @@ type Context interface {
 	// it returns an empty map if nothing found.
 	URLParams() map[string]string
 
-	// FormValue returns a single form value by its name/key
+	// FormValueDefault returns a single parsed form value by its "name",
+	// including both the URL field's query parameters and the POST or PUT form data.
+	//
+	// Returns the "def" if not found.
+	FormValueDefault(name string, def string) string
+	// FormValue returns a single parsed form value by its "name",
+	// including both the URL field's query parameters and the POST or PUT form data.
 	FormValue(name string) string
-	// FormValues returns all post data values with their keys
-	// form data, get, post & put query arguments
+	// FormValues returns the parsed form data, including both the URL
+	// field's query parameters and the POST or PUT form data.
+	//
+	// The default form's memory maximum size is 32MB, it can be changed by the
+	// `iris#WithPostMaxMemory` configurator at main configuration passed on `app.Run`'s second argument.
 	//
 	// NOTE: A check for nil is necessary.
 	FormValues() map[string][]string
 
-	// PostValueDefault returns a form's only-post value by its name,
-	// if not found then "def" is returned,
-	// same as Request.PostFormValue.
+	// PostValueDefault returns the parsed form data from POST, PATCH,
+	// or PUT body parameters based on a "name".
+	//
+	// If not found then "def" is returned instead.
 	PostValueDefault(name string, def string) string
-	// PostValue returns a form's only-post value by its name,
-	// same as Request.PostFormValue.
+	// PostValue returns the parsed form data from POST, PATCH,
+	// or PUT body parameters based on a "name"
 	PostValue(name string) string
-	// PostValueTrim returns a form's only-post value without trailing spaces by its name.
+	// PostValueTrim returns the parsed form data from POST, PATCH,
+	// or PUT body parameters based on a "name",  without trailing spaces.
 	PostValueTrim(name string) string
-	// PostValueEscape returns a form's only-post escaped value by its name.
-	PostValueEscape(name string) string
-	// PostValueIntDefault returns a form's only-post value as int by its name.
-	// If not found returns "def".
+	// PostValueIntDefault returns the parsed form data from POST, PATCH,
+	// or PUT body parameters based on a "name", as int.
+	//
+	// If not found returns the "def".
 	PostValueIntDefault(name string, def int) (int, error)
-	// PostValueInt returns a form's only-post value as int by its name.
+	// PostValueInt returns the parsed form data from POST, PATCH,
+	// or PUT body parameters based on a "name", as int.
+	//
+	// If not found returns 0.
 	PostValueInt(name string) (int, error)
-	// PostValueInt64Default returns a form's only-post value as int64 by its name.
-	// If not found returns "def".
+	// PostValueInt64Default returns the parsed form data from POST, PATCH,
+	// or PUT body parameters based on a "name", as int64.
+	//
+	// If not found returns the "def".
 	PostValueInt64Default(name string, def int64) (int64, error)
-	// PostValueInt64 returns a form's only-post value as int64 by its name.
+	// PostValueInt64 returns the parsed form data from POST, PATCH,
+	// or PUT body parameters based on a "name", as float64.
+	//
+	// If not found returns 0.0.
 	PostValueInt64(name string) (int64, error)
-	// PostValueFloat64Default returns a form's only-post value as float64 by its name.
-	// If not found returns "def".
+	// PostValueInt64Default returns the parsed form data from POST, PATCH,
+	// or PUT body parameters based on a "name", as float64.
+	//
+	// If not found returns the "def".
 	PostValueFloat64Default(name string, def float64) (float64, error)
-	// PostValueFloat64 returns a form's only-post value as float64 by its name.
+	/// PostValueInt64Default returns the parsed form data from POST, PATCH,
+	// or PUT body parameters based on a "name", as float64.
+	//
+	// If not found returns 0.0.
 	PostValueFloat64(name string) (float64, error)
-	// PostValue returns a form's only-post value as boolean by its name.
+	// PostValueInt64Default returns the parsed form data from POST, PATCH,
+	// or PUT body parameters based on a "name", as bool.
+	//
+	// If not found or value is false, then it returns false, otherwise true.
 	PostValueBool(name string) (bool, error)
-	// PostValues returns a form's only-post values.
-	// PostValues calls ParseMultipartForm and ParseForm if necessary and ignores
-	// any errors returned by these functions.
+	// PostValues returns all the parsed form data from POST, PATCH,
+	// or PUT body parameters based on a "name" as a string slice.
+	//
+	// The default form's memory maximum size is 32MB, it can be changed by the
+	// `iris#WithPostMaxMemory` configurator at main configuration passed on `app.Run`'s second argument.
 	PostValues(name string) []string
-
-	// FormFile returns the first file for the provided form key.
-	// FormFile calls ctx.Request.ParseMultipartForm and ParseForm if necessary.
+	// FormFile returns the first uploaded file that received from the client.
 	//
-	// same as Request.FormFile.
+	// The default form's memory maximum size is 32MB, it can be changed by the
+	//  `iris#WithPostMaxMemory` configurator at main configuration passed on `app.Run`'s second argument.
 	FormFile(key string) (multipart.File, *multipart.FileHeader, error)
+	// UploadFormFiles uploads any received file(s) from the client
+	// to the system physical location "destDirectory".
+	//
+	// The second optional argument "before" gives caller the chance to
+	// modify the *miltipart.FileHeader before saving to the disk,
+	// it can be used to change a file's name based on the current request,
+	// all FileHeader's options can be changed. You can ignore it if
+	// you don't need to use this capability before saving a file to the disk.
+	//
+	// Note that it doesn't check if request body streamed.
+	//
+	// Returns the copied length as int64 and
+	// a not nil error if at least one new file
+	// can't be created due to the operating system's permissions or
+	// http.ErrMissingFile if no file received.
+	//
+	// If you want to receive & accept files and manage them manually you can use the `context#FormFile`
+	// instead and create a copy function that suits your needs, the below is for generic usage.
+	//
+	// The default form's memory maximum size is 32MB, it can be changed by the
+	//  `iris#WithPostMaxMemory` configurator at main configuration passed on `app.Run`'s second argument.
+	//
+	// See `FormFile` to a more controlled to receive a file.
+	UploadFormFiles(destDirectory string, before ...func(Context, *multipart.FileHeader)) (n int64, err error)
 
 	//  +------------------------------------------------------------+
 	//  | Custom HTTP Errors                                         |
@@ -631,7 +695,6 @@ type Context interface {
 	//
 	// 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.
@@ -651,7 +714,6 @@ type Context interface {
 	//
 	// 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
@@ -662,16 +724,20 @@ type Context interface {
 	// 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,
+	// View renders a template based on the registered view engine(s).
+	// First argument accepts the filename, relative to the view engine's Directory and Extension,
 	// 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.
+	// The second optional argument can receive a single "view model"
+	// that will be binded to the view template if it's not nil,
+	// otherwise it will check for previous view data stored by the `ViewData`
+	// even if stored at any previous handler(middleware) for the same request.
+	//
+	// Look .ViewData` and .ViewLayout too.
 	//
-	// Examples: https://github.com/kataras/iris/tree/master/_examples/view/
-	View(filename string) error
+	// Examples: https://github.com/kataras/iris/tree/master/_examples/view
+	View(filename string, optionalViewModel ...interface{}) error
 
 	// Binary writes out the raw bytes as binary data.
 	Binary(data []byte) (int, error)
@@ -685,9 +751,10 @@ type Context interface {
 	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 parses the markdown to html and renders its result to the client.
 	Markdown(markdownB []byte, options ...Markdown) (int, error)
-
+	// YAML parses the "v" using the yaml parser and renders its result to the client.
+	YAML(v interface{}) (int, error)
 	//  +------------------------------------------------------------+
 	//  | Serve files                                                |
 	//  +------------------------------------------------------------+
@@ -695,18 +762,23 @@ type Context interface {
 	// 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
+	//
+	// You can define your own "Content-Type" with `context#ContentType`, before this function call.
+	//
+	// This function doesn't support resuming (by range),
+	// use ctx.SendFile or router's `StaticWeb` 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)
+	// ServeFile serves a file (to send a file, a zip for example to the client you should use the `SendFile` instead)
 	// 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
+	// You can define your own "Content-Type" with `context#ContentType`, before this function call.
+	//
+	// This function doesn't support resuming (by range),
+	// use ctx.SendFile or router's `StaticWeb` instead.
 	//
-	// Use it when you want to serve css/js/... files to the client, for bigger files and 'force-download' use the SendFile.
+	// Use it when you want to serve dynamic files to the client.
 	ServeFile(filename string, gzipCompression bool) error
 	// SendFile sends file for force-download to the client
 	//
@@ -804,8 +876,19 @@ type Context interface {
 	// to be executed at serve-time. The full app's fields
 	// and methods are not available here for the developer's safety.
 	Application() Application
+
+	// String returns the string representation of this request.
+	// Each context has a unique string representation.
+	// It can be used for simple debugging scenarios, i.e print context as string.
+	//
+	// What it returns? A number which declares the length of the
+	// total `String` calls per executable application, followed
+	// by the remote IP (the client) and finally the method:url.
+	String() string
 }
 
+var _ Context = (*context)(nil)
+
 // Next calls all the next handler from the handlers chain,
 // it should be used inside a middleware.
 func Next(ctx Context) {
@@ -855,7 +938,11 @@ type Map map[string]interface{}
 //  +------------------------------------------------------------+
 
 type context struct {
-	// the http.ResponseWriter wrapped by custom writer
+	// the unique id, it's zero until `String` function is called,
+	// it's here to cache the random, unique context's id, although `String`
+	// returns more than this.
+	id uint64
+	// the http.ResponseWriter wrapped by custom writer.
 	writer ResponseWriter
 	// the original http.Request
 	request *http.Request
@@ -863,10 +950,10 @@ type context struct {
 	currentRouteName string
 
 	// the local key-value storage
-	params RequestParams  // url named parameters
-	values memstore.Store // generic storage, middleware communication
+	params RequestParams  // url named parameters.
+	values memstore.Store // generic storage, middleware communication.
 
-	// the underline application app
+	// the underline application app.
 	app Application
 	// the route's handlers
 	handlers Handlers
@@ -1034,8 +1121,7 @@ func (ctx *context) HandlerIndex(n int) (currentIndex int) {
 // Although `BeginRequest` should NOT be used to call other handlers,
 // the `BeginRequest` has been introduced to be able to set
 // common data to all method handlers before their execution.
-// Controllers can accept middleware(s) from the `app.Controller`
-// function.
+// Controllers can accept middleware(s) from the MVC's Application's Router as normally.
 //
 // That said let's see an example of `ctx.Proceed`:
 //
@@ -1046,7 +1132,6 @@ func (ctx *context) HandlerIndex(n int) (currentIndex int) {
 // })
 //
 // func (c *UsersController) BeginRequest(ctx iris.Context) {
-//	c.C.BeginRequest(ctx) // call the parent's base controller BeginRequest first.
 // 	if !ctx.Proceed(authMiddleware) {
 // 		ctx.StopExecution()
 // 	}
@@ -1512,62 +1597,107 @@ func (ctx *context) URLParams() map[string]string {
 	return values
 }
 
-func (ctx *context) askParseForm() error {
-	if ctx.request.Form == nil {
-		if err := ctx.request.ParseForm(); err != nil {
-			return err
+// No need anymore, net/http checks for the Form already.
+// func (ctx *context) askParseForm() error {
+// 	if ctx.request.Form == nil {
+// 		if err := ctx.request.ParseForm(); err != nil {
+// 			return err
+// 		}
+// 	}
+// 	return nil
+// }
+
+// FormValueDefault returns a single parsed form value by its "name",
+// including both the URL field's query parameters and the POST or PUT form data.
+//
+// Returns the "def" if not found.
+func (ctx *context) FormValueDefault(name string, def string) string {
+	if form, has := ctx.form(); has {
+		if v := form[name]; len(v) > 0 {
+			return v[0]
 		}
 	}
-	return nil
+	return def
 }
 
-// FormValue returns a single form value by its name/key
+// FormValue returns a single parsed form value by its "name",
+// including both the URL field's query parameters and the POST or PUT form data.
 func (ctx *context) FormValue(name string) string {
-	return ctx.request.FormValue(name)
+	return ctx.FormValueDefault(name, "")
 }
 
-// FormValues returns all post data values with their keys
-// form data, get, post & put query arguments
+// FormValues returns the parsed form data, including both the URL
+// field's query parameters and the POST or PUT form data.
 //
+// The default form's memory maximum size is 32MB, it can be changed by the
+// `iris#WithPostMaxMemory` configurator at main configuration passed on `app.Run`'s second argument.
 // NOTE: A check for nil is necessary.
 func (ctx *context) FormValues() map[string][]string {
-	//  we skip the check of multipart form, takes too much memory, if user wants it can do manually now.
-	if err := ctx.askParseForm(); err != nil {
-		return nil
+	form, _ := ctx.form()
+	return form
+}
+
+// Form contains the parsed form data, including both the URL
+// field's query parameters and the POST or PUT form data.
+func (ctx *context) form() (form map[string][]string, found bool) {
+	/*
+		net/http/request.go#1219
+		for k, v := range f.Value {
+			r.Form[k] = append(r.Form[k], v...)
+			// r.PostForm should also be populated. See Issue 9305.
+			r.PostForm[k] = append(r.PostForm[k], v...)
+		}
+	*/
+
+	// ParseMultipartForm calls `request.ParseForm` automatically
+	// therefore we don't need to call it here, although it doesn't hurt.
+	// After one call to ParseMultipartForm or ParseForm,
+	// subsequent calls have no effect, are idempotent.
+	ctx.request.ParseMultipartForm(ctx.Application().ConfigurationReadOnly().GetPostMaxMemory())
+
+	if form := ctx.request.Form; len(form) > 0 {
+		return form, true
 	}
-	return ctx.request.Form // nothing more to do, it's already contains both query and post & put args.
 
+	if form := ctx.request.PostForm; len(form) > 0 {
+		return form, true
+	}
+
+	if form := ctx.request.MultipartForm.Value; len(form) > 0 {
+		return form, true
+	}
+
+	return nil, false
 }
 
-// PostValueDefault returns a form's only-post value by its name,
-// if not found then "def" is returned,
-// same as Request.PostFormValue.
+// PostValueDefault returns the parsed form data from POST, PATCH,
+// or PUT body parameters based on a "name".
+//
+// If not found then "def" is returned instead.
 func (ctx *context) PostValueDefault(name string, def string) string {
-	v := ctx.request.PostFormValue(name)
-	if v == "" {
-		return def
+	ctx.form()
+	if v := ctx.request.PostForm[name]; len(v) > 0 {
+		return v[0]
 	}
-	return v
+	return def
 }
 
-// PostValue returns a form's only-post value by its name,
-// same as Request.PostFormValue.
+// PostValue returns the parsed form data from POST, PATCH,
+// or PUT body parameters based on a "name"
 func (ctx *context) PostValue(name string) string {
 	return ctx.PostValueDefault(name, "")
 }
 
-// PostValueTrim returns a form's only-post value without trailing spaces by its name.
+// PostValueTrim returns the parsed form data from POST, PATCH,
+// or PUT body parameters based on a "name",  without trailing spaces.
 func (ctx *context) PostValueTrim(name string) string {
 	return strings.TrimSpace(ctx.PostValue(name))
 }
 
-// PostValueEscape returns a form's only-post escaped value by its name.
-func (ctx *context) PostValueEscape(name string) string {
-	return DecodeQuery(ctx.PostValue(name))
-}
-
-// PostValueIntDefault returns a form's only-post value as int by its name.
-// If not found returns "def".
+// PostValueIntDefault returns the parsed form data from POST, PATCH,
+// or PUT body parameters based on a "name", as int.
+//
+// If not found returns the "def".
 func (ctx *context) PostValueIntDefault(name string, def int) (int, error) {
 	v := ctx.PostValue(name)
 	if v == "" {
@@ -1576,14 +1706,18 @@ func (ctx *context) PostValueIntDefault(name string, def int) (int, error) {
 	return strconv.Atoi(v)
 }
 
-// PostValueInt returns a form's only-post value as int by its name.
+// PostValueInt returns the parsed form data from POST, PATCH,
+// or PUT body parameters based on a "name", as int.
+//
 // If not found returns 0.
 func (ctx *context) PostValueInt(name string) (int, error) {
 	return ctx.PostValueIntDefault(name, 0)
 }
 
-// PostValueInt64Default returns a form's only-post value as int64 by its name.
-// If not found returns "def".
+// PostValueInt64Default returns the parsed form data from POST, PATCH,
+// or PUT body parameters based on a "name", as int64.
+//
+// If not found returns the "def".
 func (ctx *context) PostValueInt64Default(name string, def int64) (int64, error) {
 	v := ctx.PostValue(name)
 	if v == "" {
@@ -1592,14 +1726,18 @@ func (ctx *context) PostValueInt64Default(name string, def int64) (int64, error)
 	return strconv.ParseInt(v, 10, 64)
 }
 
-// PostValueInt64 returns a form's only-post value as int64 by its name.
+// PostValueInt64 returns the parsed form data from POST, PATCH,
+// or PUT body parameters based on a "name", as float64.
+//
 // If not found returns 0.0.
 func (ctx *context) PostValueInt64(name string) (int64, error) {
 	return ctx.PostValueInt64Default(name, 0.0)
 }
 
-// PostValueFloat64Default returns a form's only-post value as float64 by its name.
-// If not found returns "def".
+// PostValueInt64Default returns the parsed form data from POST, PATCH,
+// or PUT body parameters based on a "name", as float64.
+//
+// If not found returns the "def".
 func (ctx *context) PostValueFloat64Default(name string, def float64) (float64, error) {
 	v := ctx.PostValue(name)
 	if v == "" {
@@ -1608,46 +1746,156 @@ func (ctx *context) PostValueFloat64Default(name string, def float64) (float64,
 	return strconv.ParseFloat(v, 64)
 }
 
-// PostValueFloat64 returns a form's only-post value as float64 by its name.
+// PostValueInt64Default returns the parsed form data from POST, PATCH,
+// or PUT body parameters based on a "name", as float64.
+//
 // If not found returns 0.0.
 func (ctx *context) PostValueFloat64(name string) (float64, error) {
 	return ctx.PostValueFloat64Default(name, 0.0)
 }
 
-// PostValue returns a form's only-post value as boolean by its name.
+// PostValueInt64Default returns the parsed form data from POST, PATCH,
+// or PUT body parameters based on a "name", as bool.
+//
+// If not found or value is false, then it returns false, otherwise true.
 func (ctx *context) PostValueBool(name string) (bool, error) {
 	return strconv.ParseBool(ctx.PostValue(name))
 }
 
-const (
-	// DefaultMaxMemory is the default value
-	// for post values' max memory, defaults to
-	// 32MB.
-	// Can be also changed by the middleware `LimitRequestBodySize`
-	// or `context#SetMaxRequestBodySize`.
-	DefaultMaxMemory = 32 << 20 // 32 MB
-)
-
-// PostValues returns a form's only-post values.
-// PostValues calls ParseMultipartForm and ParseForm if necessary and ignores
-// any errors returned by these functions.
+// PostValues returns all the parsed form data from POST, PATCH,
+// or PUT body parameters based on a "name" as a string slice.
+//
+// The default form's memory maximum size is 32MB, it can be changed by the
+// `iris#WithPostMaxMemory` configurator at main configuration passed on `app.Run`'s second argument.
 func (ctx *context) PostValues(name string) []string {
-	r := ctx.request
-	if r.PostForm == nil {
-		r.ParseMultipartForm(DefaultMaxMemory)
-	}
-
-	return r.PostForm[name]
+	ctx.form()
+	return ctx.request.PostForm[name]
 }
 
-// FormFile returns the first file for the provided form key.
-// FormFile calls ctx.request.ParseMultipartForm and ParseForm if necessary.
+// FormFile returns the first uploaded file that received from the client.
 //
-// same as Request.FormFile.
+//
+// The default form's memory maximum size is 32MB, it can be changed by the
+// `iris#WithPostMaxMemory` configurator at main configuration passed on `app.Run`'s second argument.
 func (ctx *context) FormFile(key string) (multipart.File, *multipart.FileHeader, error) {
+	// we don't have access to see if the request is body stream
+	// and then the ParseMultipartForm can be useless
+	// here but do it in order to apply the post limit,
+	// the internal request.FormFile will not do it if that's filled
+	// and it's not a stream body.
+	ctx.request.ParseMultipartForm(ctx.Application().ConfigurationReadOnly().GetPostMaxMemory())
 	return ctx.request.FormFile(key)
 }
 
+// UploadFormFiles uploads any received file(s) from the client
+// to the system physical location "destDirectory".
+//
+// The second optional argument "before" gives caller the chance to
+// modify the *miltipart.FileHeader before saving to the disk,
+// it can be used to change a file's name based on the current request,
+// all FileHeader's options can be changed. You can ignore it if
+// you don't need to use this capability before saving a file to the disk.
+//
+// Note that it doesn't check if request body streamed.
+//
+// Returns the copied length as int64 and
+// a not nil error if at least one new file
+// can't be created due to the operating system's permissions or
+// http.ErrMissingFile if no file received.
+//
+// If you want to receive & accept files and manage them manually you can use the `context#FormFile`
+// instead and create a copy function that suits your needs, the below is for generic usage.
+//
+// The default form's memory maximum size is 32MB, it can be changed by the
+//  `iris#WithPostMaxMemory` configurator at main configuration passed on `app.Run`'s second argument.
+//
+// See `FormFile` to a more controlled to receive a file.
+func (ctx *context) UploadFormFiles(destDirectory string, before ...func(Context, *multipart.FileHeader)) (n int64, err error) {
+	err = ctx.request.ParseMultipartForm(ctx.Application().ConfigurationReadOnly().GetPostMaxMemory())
+	if err != nil {
+		return 0, err
+	}
+
+	if ctx.request.MultipartForm != nil {
+		if fhs := ctx.request.MultipartForm.File; fhs != nil {
+			for _, files := range fhs {
+				for _, file := range files {
+
+					for _, b := range before {
+						b(ctx, file)
+					}
+
+					n0, err0 := uploadTo(file, destDirectory)
+					if err0 != nil {
+						return 0, err0
+					}
+					n += n0
+				}
+			}
+		}
+	}
+
+	return 0, http.ErrMissingFile
+}
+
+func uploadTo(fh *multipart.FileHeader, destDirectory string) (int64, error) {
+	src, err := fh.Open()
+	if err != nil {
+		return 0, err
+	}
+	defer src.Close()
+
+	out, err := os.OpenFile(filepath.Join(destDirectory, fh.Filename),
+		os.O_WRONLY|os.O_CREATE, os.FileMode(0666))
+
+	if err != nil {
+		return 0, err
+	}
+	defer out.Close()
+
+	return io.Copy(out, src)
+}
+
+/* Good idea of mine but it doesn't work of course...
+// Go can't use `io.ReadCloser` function return value the same
+// as with other function that returns a `multipart.File`, even if
+// multipart.File is an `io.ReadCloser`.
+// So comment all those and implement a function inside the context itself.
+//
+// Copiable is the interface which should be completed
+// by files or not that intend to be used inside the `context#CopyFile`.
+// This interface allows testing file uploads to your http test as well.
+//
+// See `CopyFile` for more.
+// type Copiable interface {
+// 	Open() (io.ReadCloser, error)
+// }
+//
+
+
+// CopyFile copies a `context#Copiable` "file", that can be acquired by the second argument of
+// a `context.FormFile` (*multipart.FileHeader) as well, to the "dest".
+//
+// Returns the copied length as int64 and
+// an error if file is not exist, or new file can't be created or closed at the end.
+func CopyFile(file Copiable, dest string) (int64, error) {
+	src, err := fileOpen()
+	if err != nil {
+		return 0, err
+	}
+	defer src.Close()
+
+	out, err := os.Create(dest)
+	if err != nil {
+		return 0, err
+	}
+	defer out.Close()
+
+	return io.Copy(out, src)
+}
+
+*/
+
 // Redirect sends a redirect response to the client
 // to a specific url or relative path.
 // accepts 2 parameters string and an optional int
@@ -2053,20 +2301,32 @@ func (ctx *context) GetViewData() map[string]interface{} {
 	return nil
 }
 
-// View renders templates based on the adapted view engines.
-// First argument accepts the filename, relative to the view engine's Directory,
+// View renders a template based on the registered view engine(s).
+// First argument accepts the filename, relative to the view engine's Directory and Extension,
 // 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.
+// The second optional argument can receive a single "view model"
+// that will be binded to the view template if it's not nil,
+// otherwise it will check for previous view data stored by the `ViewData`
+// even if stored at any previous handler(middleware) for the same request.
 //
-// Examples: https://github.com/kataras/iris/tree/master/_examples/view/
-func (ctx *context) View(filename string) error {
+// Look .ViewData and .ViewLayout too.
+//
+// Examples: https://github.com/kataras/iris/tree/master/_examples/view
+func (ctx *context) View(filename string, optionalViewModel ...interface{}) error {
 	ctx.ContentType(ContentHTMLHeaderValue)
 	cfg := ctx.Application().ConfigurationReadOnly()
 
 	layout := ctx.values.GetString(cfg.GetViewLayoutContextKey())
-	bindingData := ctx.values.Get(cfg.GetViewDataContextKey())
+
+	var bindingData interface{}
+	if len(optionalViewModel) > 0 {
+		// a nil can override the existing data or model sent by `ViewData`.
+		bindingData = optionalViewModel[0]
+	} else {
+		bindingData = ctx.values.Get(cfg.GetViewDataContextKey())
+	}
 
 	err := ctx.Application().View(ctx.writer, filename, layout, bindingData)
 	if err != nil {
@@ -2090,9 +2350,10 @@ const (
 	ContentTextHeaderValue = "text/plain"
 	// ContentXMLHeaderValue header value for XML data.
 	ContentXMLHeaderValue = "text/xml"
-
 	// ContentMarkdownHeaderValue custom key/content type, the real is the text/html.
 	ContentMarkdownHeaderValue = "text/markdown"
+	// ContentYAMLHeaderValue header value for YAML data.
+	ContentYAMLHeaderValue = "application/x-yaml"
 )
 
 // Binary writes out the raw bytes as binary data.
@@ -2353,7 +2614,7 @@ func (ctx *context) XML(v interface{}, opts ...XML) (int, error) {
 	return n, err
 }
 
-// WriteMarkdown parses the markdown to html and renders these contents to the writer.
+// WriteMarkdown parses the markdown to html and writes these contents to the writer.
 func WriteMarkdown(writer io.Writer, markdownB []byte, options Markdown) (int, error) {
 	buf := blackfriday.Run(markdownB)
 	if options.Sanitize {
@@ -2366,7 +2627,7 @@ func WriteMarkdown(writer io.Writer, markdownB []byte, options Markdown) (int, e
 // from `WriteMarkdown` and `ctx.Markdown`.
 var DefaultMarkdownOptions = Markdown{}
 
-// Markdown parses the markdown to html and renders to the client.
+// Markdown parses the markdown to html and renders its result to the client.
 func (ctx *context) Markdown(markdownB []byte, opts ...Markdown) (int, error) {
 	options := DefaultMarkdownOptions
 
@@ -2385,6 +2646,18 @@ func (ctx *context) Markdown(markdownB []byte, opts ...Markdown) (int, error) {
 	return n, err
 }
 
+// YAML marshals the "v" using the yaml marshaler and renders its result to the client.
+func (ctx *context) YAML(v interface{}) (int, error) {
+	out, err := yaml.Marshal(v)
+	if err != nil {
+		ctx.StatusCode(http.StatusInternalServerError)
+		return 0, err
+	}
+
+	ctx.ContentType(ContentYAMLHeaderValue)
+	return ctx.Write(out)
+}
+
 //  +------------------------------------------------------------+
 //  | Serve files                                                |
 //  +------------------------------------------------------------+
@@ -2482,7 +2755,7 @@ var (
 func (ctx *context) SetCookieKV(name, value string) {
 	c := &http.Cookie{}
 	c.Name = name
-	c.Value = value
+	c.Value = url.QueryEscape(value)
 	c.HttpOnly = true
 	c.Expires = time.Now().Add(SetCookieKVExpiration)
 	c.MaxAge = int(SetCookieKVExpiration.Seconds())
@@ -2496,7 +2769,8 @@ func (ctx *context) GetCookie(name string) string {
 	if err != nil {
 		return ""
 	}
-	return cookie.Value
+	value, _ := url.QueryUnescape(cookie.Value)
+	return value
 }
 
 // RemoveCookie deletes a cookie by it's name.
@@ -2717,3 +2991,28 @@ func (ctx *context) Exec(method string, path string) {
 func (ctx *context) Application() Application {
 	return ctx.app
 }
+
+var lastCapturedContextID uint64
+
+// LastCapturedContextID returns the total number of `context#String` calls.
+func LastCapturedContextID() uint64 {
+	return atomic.LoadUint64(&lastCapturedContextID)
+}
+
+// String returns the string representation of this request.
+// Each context has a unique string representation.
+// It can be used for simple debugging scenarios, i.e print context as string.
+//
+// What it returns? A number which declares the length of the
+// total `String` calls per executable application, followed
+// by the remote IP (the client) and finally the method:url.
+func (ctx *context) String() string {
+	if ctx.id == 0 {
+		// set the id here.
+		forward := atomic.AddUint64(&lastCapturedContextID, 1)
+		ctx.id = forward
+	}
+
+	return fmt.Sprintf("[%d] %s ▶ %s:%s",
+		ctx.id, ctx.RemoteAddr(), ctx.Method(), ctx.Request().RequestURI)
+}
diff --git a/context/gzip_response_writer.go b/context/gzip_response_writer.go
index f9aa234118..05ee0c657c 100644
--- a/context/gzip_response_writer.go
+++ b/context/gzip_response_writer.go
@@ -83,7 +83,7 @@ type GzipResponseWriter struct {
 	disabled bool
 }
 
-var _ ResponseWriter = &GzipResponseWriter{}
+var _ ResponseWriter = (*GzipResponseWriter)(nil)
 
 // BeginGzipResponse accepts a ResponseWriter
 // and prepares the new gzip response writer.
diff --git a/context/response_recorder.go b/context/response_recorder.go
index 76e7b777fd..c469dc255e 100644
--- a/context/response_recorder.go
+++ b/context/response_recorder.go
@@ -39,7 +39,7 @@ type ResponseRecorder struct {
 	headers http.Header
 }
 
-var _ ResponseWriter = &ResponseRecorder{}
+var _ ResponseWriter = (*ResponseRecorder)(nil)
 
 // Naive returns the simple, underline and original http.ResponseWriter
 // that backends this response writer.
diff --git a/context/response_writer.go b/context/response_writer.go
index 04875723b5..baab5d226d 100644
--- a/context/response_writer.go
+++ b/context/response_writer.go
@@ -115,7 +115,7 @@ type responseWriter struct {
 	beforeFlush func()
 }
 
-var _ ResponseWriter = &responseWriter{}
+var _ ResponseWriter = (*responseWriter)(nil)
 
 const (
 	defaultStatusCode = http.StatusOK
diff --git a/core/maintenance/version.go b/core/maintenance/version.go
index 2523b3d844..ffed1c9cb7 100644
--- a/core/maintenance/version.go
+++ b/core/maintenance/version.go
@@ -13,7 +13,7 @@ import (
 
 const (
 	// Version is the string representation of the current local Iris Web Framework version.
-	Version = "8.5.8"
+	Version = "10.0.0"
 )
 
 // CheckForUpdates checks for any available updates
diff --git a/core/router/api_builder.go b/core/router/api_builder.go
index dc1438b7d8..ebc278c108 100644
--- a/core/router/api_builder.go
+++ b/core/router/api_builder.go
@@ -10,7 +10,6 @@ import (
 	"github.com/kataras/iris/context"
 	"github.com/kataras/iris/core/errors"
 	"github.com/kataras/iris/core/router/macro"
-	"github.com/kataras/iris/mvc/activator"
 )
 
 const (
@@ -190,11 +189,11 @@ func (api *APIBuilder) Handle(method string, relativePath string, handlers ...co
 // otherwise use `Party` which can handle many paths with different handlers and middlewares.
 //
 // Usage:
-// 	app.HandleMany(iris.MethodGet, "/user /user/{id:int} /user/me", userHandler)
+// 	app.HandleMany("GET", "/user /user/{id:int} /user/me", genericUserHandler)
 // At the other side, with `Handle` we've had to write:
-// 	app.Handle(iris.MethodGet, "/user", userHandler)
-// 	app.Handle(iris.MethodGet, "/user/{id:int}", userHandler)
-// 	app.Handle(iris.MethodGet, "/user/me", userHandler)
+// 	app.Handle("GET", "/user", userHandler)
+// 	app.Handle("GET", "/user/{id:int}", userByIDHandler)
+// 	app.Handle("GET", "/user/me", userMeHandler)
 //
 // This method is used behind the scenes at the `Controller` function
 // in order to handle more than one paths for the same controller instance.
@@ -202,7 +201,6 @@ func (api *APIBuilder) HandleMany(methodOrMulti string, relativePathorMulti stri
 	// at least slash
 	// a space
 	// at least one other slash for the next path
-	// app.Controller("/user /user{id}", new(UserController))
 	paths := splitPath(relativePathorMulti)
 	methods := splitMethod(methodOrMulti)
 	for _, p := range paths {
@@ -476,85 +474,6 @@ func (api *APIBuilder) Any(relativePath string, handlers ...context.Handler) (ro
 	return
 }
 
-// Controller registers a `Controller` instance and returns the registered Routes.
-// The "controller" receiver should embed a field of `Controller` in order
-// to be compatible Iris `Controller`.
-//
-// It's just an alternative way of building an API for a specific
-// path, the controller can register all type of http methods.
-//
-// Keep note that controllers are bit slow
-// because of the reflection use however it's as fast as possible because
-// it does preparation before the serve-time handler but still
-// remains slower than the low-level handlers
-// such as `Handle, Get, Post, Put, Delete, Connect, Head, Trace, Patch`.
-//
-//
-// All fields that are tagged with iris:"persistence"` or binded
-// are being persistence and kept the same between the different requests.
-//
-// An Example Controller can be:
-//
-// type IndexController struct {
-// 	Controller
-// }
-//
-// func (c *IndexController) Get() {
-// 	c.Tmpl = "index.html"
-// 	c.Data["title"] = "Index page"
-// 	c.Data["message"] = "Hello world!"
-// }
-//
-// Usage: app.Controller("/", new(IndexController))
-//
-//
-// Another example with bind:
-//
-// type UserController struct {
-// 	Controller
-//
-// 	DB        *DB
-// 	CreatedAt time.Time
-//
-// }
-//
-// // Get serves using the User controller when HTTP Method is "GET".
-// func (c *UserController) Get() {
-// 	c.Tmpl = "user/index.html"
-// 	c.Data["title"] = "User Page"
-// 	c.Data["username"] = "kataras " + c.Params.Get("userid")
-// 	c.Data["connstring"] = c.DB.Connstring
-// 	c.Data["uptime"] = time.Now().Sub(c.CreatedAt).Seconds()
-// }
-//
-// Usage: app.Controller("/user/{id:int}", new(UserController), db, time.Now())
-// Note: Binded values of context.Handler type are being recognised as middlewares by the router.
-//
-// Read more at `/mvc#Controller`.
-func (api *APIBuilder) Controller(relativePath string, controller activator.BaseController,
-	bindValues ...interface{}) (routes []*Route) {
-
-	registerFunc := func(ifRelPath string, method string, handlers ...context.Handler) {
-		relPath := relativePath + ifRelPath
-		r := api.HandleMany(method, relPath, handlers...)
-		routes = append(routes, r...)
-	}
-
-	// bind any values to the controller's relative fields
-	// and set them on each new request controller,
-	// binder is an alternative method
-	// of the persistence data control which requires the
-	// user already set the values manually to controller's fields
-	// and tag them with `iris:"persistence"`.
-	//
-	// don't worry it will never be handled if empty values.
-	if err := activator.Register(controller, bindValues, registerFunc); err != nil {
-		api.reporter.Add("%v for path: '%s'", err, relativePath)
-	}
-
-	return
-}
-
 // StaticCacheDuration expiration duration for INACTIVE file handlers, it's the only one global configuration
 // which can be changed.
 var StaticCacheDuration = 20 * time.Second
diff --git a/core/router/handler.go b/core/router/handler.go
index 0114f9daa6..bb00eb3aa4 100644
--- a/core/router/handler.go
+++ b/core/router/handler.go
@@ -1,14 +1,14 @@
 package router
 
 import (
-	"github.com/kataras/golog"
 	"html"
 	"net/http"
 	"sort"
 	"strings"
 
-	"github.com/kataras/iris/context"
+	"github.com/kataras/golog"
 
+	"github.com/kataras/iris/context"
 	"github.com/kataras/iris/core/errors"
 	"github.com/kataras/iris/core/netutil"
 	"github.com/kataras/iris/core/router/node"
diff --git a/core/router/macro/interpreter/ast/ast.go b/core/router/macro/interpreter/ast/ast.go
index 1e39bc8557..23e1723628 100644
--- a/core/router/macro/interpreter/ast/ast.go
+++ b/core/router/macro/interpreter/ast/ast.go
@@ -51,21 +51,14 @@ const (
 	ParamTypePath
 )
 
-// ValidKind will return true if at least one param type is supported
-// for this std kind.
-func ValidKind(k reflect.Kind) bool {
-	switch k {
-	case reflect.String:
-		fallthrough
-	case reflect.Int:
-		fallthrough
-	case reflect.Int64:
-		fallthrough
-	case reflect.Bool:
-		return true
-	default:
-		return false
+func (pt ParamType) String() string {
+	for k, v := range paramTypes {
+		if v == pt {
+			return k
+		}
 	}
+
+	return "unexpected"
 }
 
 // Not because for a single reason
@@ -96,6 +89,23 @@ func (pt ParamType) Kind() reflect.Kind {
 	return reflect.Invalid // 0
 }
 
+// ValidKind will return true if at least one param type is supported
+// for this std kind.
+func ValidKind(k reflect.Kind) bool {
+	switch k {
+	case reflect.String:
+		fallthrough
+	case reflect.Int:
+		fallthrough
+	case reflect.Int64:
+		fallthrough
+	case reflect.Bool:
+		return true
+	default:
+		return false
+	}
+}
+
 // Assignable returns true if the "k" standard type
 // is assignabled to this ParamType.
 func (pt ParamType) Assignable(k reflect.Kind) bool {
@@ -133,6 +143,30 @@ func LookupParamType(ident string) ParamType {
 	return ParamTypeUnExpected
 }
 
+// LookupParamTypeFromStd accepts the string representation of a standard go type.
+// It returns a ParamType, but it may differs for example
+// the alphabetical, file, path and string are all string go types, so
+// make sure that caller resolves these types before this call.
+//
+// string matches to string
+// int matches to int
+// int64 matches to long
+// bool matches to boolean
+func LookupParamTypeFromStd(goType string) ParamType {
+	switch goType {
+	case "string":
+		return ParamTypeString
+	case "int":
+		return ParamTypeInt
+	case "int64":
+		return ParamTypeLong
+	case "bool":
+		return ParamTypeBoolean
+	default:
+		return ParamTypeUnExpected
+	}
+}
+
 // ParamStatement is a struct
 // which holds all the necessary information about a macro parameter.
 // It holds its type (string, int, alphabetical, file, path),
diff --git a/core/router/party.go b/core/router/party.go
index 1683446820..f1a1a9d04d 100644
--- a/core/router/party.go
+++ b/core/router/party.go
@@ -2,7 +2,8 @@ package router
 
 import (
 	"github.com/kataras/iris/context"
-	"github.com/kataras/iris/mvc/activator"
+	"github.com/kataras/iris/core/errors"
+	"github.com/kataras/iris/core/router/macro"
 )
 
 // Party is here to separate the concept of
@@ -13,6 +14,14 @@ import (
 //
 // Look the "APIBuilder" for its implementation.
 type Party interface {
+	// GetReporter returns the reporter for adding errors
+	GetReporter() *errors.Reporter
+	// Macros returns the macro map which is responsible
+	// to register custom macro functions for all routes.
+	//
+	// Learn more at:  https://github.com/kataras/iris/tree/master/_examples/routing/dynamic-path
+	Macros() *macro.Map
+
 	// Party groups routes which may have the same prefix and share same handlers,
 	// returns that new rich subrouter.
 	//
@@ -120,63 +129,6 @@ type Party interface {
 	// (Get,Post,Put,Head,Patch,Options,Connect,Delete).
 	Any(registeredPath string, handlers ...context.Handler) []*Route
 
-	// Controller registers a `Controller` instance and returns the registered Routes.
-	// The "controller" receiver should embed a field of `Controller` in order
-	// to be compatible Iris `Controller`.
-	//
-	// It's just an alternative way of building an API for a specific
-	// path, the controller can register all type of http methods.
-	//
-	// Keep note that controllers are bit slow
-	// because of the reflection use however it's as fast as possible because
-	// it does preparation before the serve-time handler but still
-	// remains slower than the low-level handlers
-	// such as `Handle, Get, Post, Put, Delete, Connect, Head, Trace, Patch`.
-	//
-	//
-	// All fields that are tagged with iris:"persistence"` or binded
-	// are being persistence and kept the same between the different requests.
-	//
-	// An Example Controller can be:
-	//
-	// type IndexController struct {
-	// 	Controller
-	// }
-	//
-	// func (c *IndexController) Get() {
-	// 	c.Tmpl = "index.html"
-	// 	c.Data["title"] = "Index page"
-	// 	c.Data["message"] = "Hello world!"
-	// }
-	//
-	// Usage: app.Controller("/", new(IndexController))
-	//
-	//
-	// Another example with bind:
-	//
-	// type UserController struct {
-	// 	Controller
-	//
-	// 	DB        *DB
-	// 	CreatedAt time.Time
-	//
-	// }
-	//
-	// // Get serves using the User controller when HTTP Method is "GET".
-	// func (c *UserController) Get() {
-	// 	c.Tmpl = "user/index.html"
-	// 	c.Data["title"] = "User Page"
-	// 	c.Data["username"] = "kataras " + c.Params.Get("userid")
-	// 	c.Data["connstring"] = c.DB.Connstring
-	// 	c.Data["uptime"] = time.Now().Sub(c.CreatedAt).Seconds()
-	// }
-	//
-	// Usage: app.Controller("/user/{id:int}", new(UserController), db, time.Now())
-	// Note: Binded values of context.Handler type are being recognised as middlewares by the router.
-	//
-	// Read more at `/mvc#Controller`.
-	Controller(relativePath string, controller activator.BaseController, bindValues ...interface{}) []*Route
-
 	// StaticHandler returns a new Handler which is ready
 	// to serve all kind of static files.
 	//
diff --git a/core/router/route.go b/core/router/route.go
index 755e66a646..205687c151 100644
--- a/core/router/route.go
+++ b/core/router/route.go
@@ -23,7 +23,7 @@ type Route struct {
 	// Handlers are the main route's handlers, executed by order.
 	// Cannot be empty.
 	Handlers        context.Handlers
-	mainHandlerName string
+	MainHandlerName string
 	// temp storage, they're appended to the Handlers on build.
 	// Execution happens after Begin and main Handler(s), can be empty.
 	doneHandlers context.Handlers
@@ -61,7 +61,7 @@ func NewRoute(method, subdomain, unparsedPath, mainHandlerName string,
 		tmpl:            tmpl,
 		Path:            path,
 		Handlers:        handlers,
-		mainHandlerName: mainHandlerName,
+		MainHandlerName: mainHandlerName,
 		FormattedPath:   formattedPath,
 	}
 	return route, nil
@@ -214,12 +214,12 @@ func (r Route) Trace() string {
 	}
 	printfmt += fmt.Sprintf(" %s ", r.Tmpl().Src)
 	if l := len(r.Handlers); l > 1 {
-		printfmt += fmt.Sprintf("-> %s() and %d more", r.mainHandlerName, l-1)
+		printfmt += fmt.Sprintf("-> %s() and %d more", r.MainHandlerName, l-1)
 	} else {
-		printfmt += fmt.Sprintf("-> %s()", r.mainHandlerName)
+		printfmt += fmt.Sprintf("-> %s()", r.MainHandlerName)
 	}
 
-	// printfmt := fmt.Sprintf("%s: %s >> %s", r.Method, r.Subdomain+r.Tmpl().Src, r.mainHandlerName)
+	// printfmt := fmt.Sprintf("%s: %s >> %s", r.Method, r.Subdomain+r.Tmpl().Src, r.MainHandlerName)
 	// if l := len(r.Handlers); l > 0 {
 	// 	printfmt += fmt.Sprintf(" and %d more", l)
 	// }
diff --git a/deprecated.go b/deprecated.go
new file mode 100644
index 0000000000..a5b7a821a4
--- /dev/null
+++ b/deprecated.go
@@ -0,0 +1,55 @@
+package iris
+
+import (
+	"fmt"
+
+	"github.com/kataras/iris/core/router"
+	"github.com/kataras/iris/mvc"
+)
+
+// Controller method is DEPRECATED, use the "mvc" subpackage instead, i.e
+// import "github.com/kataras/iris/mvc" and read its docs among with its new features at:
+// https://github.com/kataras/iris/blob/master/HISTORY.md#mo-01-jenuary-2018--v10
+func (app *Application) Controller(relPath string, c interface{}, _ ...interface{}) []*router.Route {
+	name := mvc.NameOf(c)
+
+	panic(fmt.Errorf(`"Controller" method is DEPRECATED, use the "mvc" subpackage instead.
+
+        PREVIOUSLY YOU USED TO CODE IT LIKE THIS:
+        
+            import (
+                "github.com/kataras/iris"
+                // ...
+            )
+        
+            app.Controller("%s", new(%s), Struct_Values_Binded_To_The_Fields_Or_And_Any_Middleware)
+        
+        NOW YOU SHOULD CODE IT LIKE THIS:
+        
+            import (
+                "github.com/kataras/iris"
+                "github.com/kataras/iris/mvc"
+                // ...
+            )
+        
+            // or use it like this:          ).Register(...).Handle(new(%s))
+            mvc.Configure(app.Party("%s"), myMVC)
+        
+            func myMVC(mvcApp *mvc.Application) {
+                mvcApp.Register(
+                    Struct_Values_Dependencies_Binded_To_The_Fields_Or_And_To_Methods,
+                    Or_And_Func_Values_Dependencies_Binded_To_The_Fields_Or_And_To_Methods,
+                )
+        
+                mvcApp.Router.Use(Any_Middleware)
+        
+                mvcApp.Handle(new(%s))
+            }
+        
+        The new MVC implementation contains a lot more than the above,
+        this is the reason you see more lines for a simple controller.
+        
+        Please read more about the newest, amazing, features by navigating below
+        https://github.com/kataras/iris/blob/master/HISTORY.md#mo-01-jenuary-2018--v1000`, // v10.0.0, we skip the number 9.
+		relPath, name, name, relPath, name))
+}
diff --git a/doc.go b/doc.go
index 3eaed501d5..4ae3be38f7 100644
--- a/doc.go
+++ b/doc.go
@@ -1,4 +1,4 @@
-// Copyright (c) 2017 The Iris Authors. All rights reserved.
+// Copyright (c) 2017-2018 The Iris Authors. All rights reserved.
 //
 // Redistribution and use in source and binary forms, with or without
 // modification, are permitted provided that the following conditions are
@@ -35,7 +35,7 @@ Source code and other details for the project are available at GitHub:
 
 Current Version
 
-8.5.8
+10.0.0
 
 Installation
 
@@ -222,7 +222,7 @@ Below you'll see some useful examples:
     // ListenAndServe function of the `net/http` package.
     app.Run(iris.Raw(&http.Server{Addr:":8080"}).ListenAndServe)
 
-UNIX and BSD hosts can take advandage of the reuse port feature.
+UNIX and BSD hosts can take advantage of the reuse port feature.
 
 Example code:
 
@@ -683,365 +683,6 @@ Example code:
     }
 
 
-MVC - Model View Controller
-
-Iris has first-class support for the MVC pattern, you'll not find
-these stuff anywhere else in the Go world.
-
-Example Code:
-
-    package main
-
-    import (
-        "github.com/kataras/iris"
-
-        "github.com/kataras/iris/middleware/logger"
-        "github.com/kataras/iris/middleware/recover"
-    )
-
-    // This example is equivalent to the
-    // https://github.com/kataras/iris/blob/master/_examples/hello-world/main.go
-    //
-    // It seems that additional code you
-    // have to write doesn't worth it
-    // but remember that, this example
-    // does not make use of iris mvc features like
-    // the Model, Persistence or the View engine neither the Session,
-    // it's very simple for learning purposes,
-    // probably you'll never use such
-    // as simple controller anywhere in your app.
-
-    func main() {
-        app := iris.New()
-        // Optionally, add two built'n handlers
-        // that can recover from any http-relative panics
-        // and log the requests to the terminal.
-        app.Use(recover.New())
-        app.Use(logger.New())
-
-        app.Controller("/", new(IndexController))
-        app.Controller("/ping", new(PingController))
-        app.Controller("/hello", new(HelloController))
-
-        // http://localhost:8080
-        // http://localhost:8080/ping
-        // http://localhost:8080/hello
-        app.Run(iris.Addr(":8080"))
-    }
-
-    // IndexController serves the "/".
-    type IndexController struct {
-        // if you build with go1.8 you have to use the mvc package, `mvc.Controller` instead.
-        iris.Controller
-    }
-
-    // Get serves
-    // Method:   GET
-    // Resource: http://localhost:8080/
-    func (c *IndexController) Get() {
-        c.Ctx.HTML("Welcome!")
-    }
-
-    // PingController serves the "/ping".
-    type PingController struct {
-        iris.Controller
-    }
-
-    // Get serves
-    // Method:   GET
-    // Resource: http://localhost:8080/ping
-    func (c *PingController) Get() {
-        c.Ctx.WriteString("pong")
-    }
-
-    // HelloController serves the "/hello".
-    type HelloController struct {
-        iris.Controller
-    }
-
-    // Get serves
-    // Method:   GET
-    // Resource: http://localhost:8080/hello
-    func (c *HelloController) Get() {
-        c.Ctx.JSON(iris.Map{"message": "Hello iris web framework."})
-    }
-
-    // Can use more than one, the factory will make sure
-    // that the correct http methods are being registered for each route
-    // for this controller, uncomment these if you want:
-
-    // func (c *HelloController) Post() {}
-    // func (c *HelloController) Put() {}
-    // func (c *HelloController) Delete() {}
-    // func (c *HelloController) Connect() {}
-    // func (c *HelloController) Head() {}
-    // func (c *HelloController) Patch() {}
-    // func (c *HelloController) Options() {}
-    // func (c *HelloController) Trace() {}
-    // or All() or Any() to catch all http methods.
-
-
-Iris web framework 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/iris.SessionController`, it has the `mvc.Controller/iris.Controller` as an embedded field
-and it adds its logic to its `BeginRequest`. Source file: https://github.com/kataras/iris/blob/master/mvc/session_controller.go.
-
-Read access to the current route  via the `Route` field.
-
-Support for more than one input arguments (map to dynamic request path parameters).
-
-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}`
-
-    If `app.Controller("/equality", new(profile.Equality))`
-
-    - `func(*Controller) GetBy(is bool)` - `GET:/equality/{param:boolean}`
-    - `func(*Controller) GetByOtherBy(is bool, otherID int64)` - `GET:/equality/{paramfirst:boolean}/other/{paramsecond:long}`
-
-    Supported types for method functions receivers: int, int64, bool and string.
-
-Response via output arguments, optionally, i.e
-
-    func(c *ExampleController) Get() string |
-    (string, string) |
-    (string, int) |
-    (string, error) |
-    int |
-    (int, string) |
-    (any, int) |
-    error |
-    (int, error) |
-    (customStruct, error) |
-    (any, error) |
-    bool |
-    (any, bool)
-    customStruct |
-    (customStruct, int) |
-    (customStruct, string) |
-    `Result` or (`Result`, error)
-
-Where `any` means everything, from custom structs to standard language's types-.
-`Result` is an interface which contains only that function: Dispatch(ctx iris.Context)
-and Get where HTTP Method function(Post, Put, Delete...).
-
-
-Iris MVC Method Result
-
-Iris has a very powerful and blazing fast 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 `bool` is false then it throws 404 not found http error by skipping everything else.
-* 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  `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;
-
-    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 {
-        // if you build with go1.8 you have to use the mvc package always,
-        // otherwise
-        // you can, optionally
-        // use the type alias `iris.C`,
-        // same for
-        // context.Context -> iris.Context,
-        // mvc.Result -> iris.Result,
-        // mvc.Response -> iris.Response,
-        // mvc.View -> iris.View
-        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}
-    }
-
-
-Another good example with a typical folder structure,
-that many developers are used to work, can be found at:
-https://github.com/kataras/iris/tree/master/_examples/mvc/overview.
-
-
-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.
-
-Follow the examples at: https://github.com/kataras/iris/tree/master/_examples/#mvc
-
-
 Parameterized Path
 
 At the previous example,
@@ -1838,6 +1479,283 @@ Running the example:
     $ start http://localhost:8080
 
 
+MVC - Model View Controller
+
+Iris has first-class support for the MVC pattern, you'll not find
+these stuff anywhere else in the Go world.
+
+Example Code:
+
+    package main
+
+    import (
+        "github.com/kataras/iris"
+        "github.com/kataras/iris/mvc"
+
+        "github.com/kataras/iris/middleware/logger"
+        "github.com/kataras/iris/middleware/recover"
+    )
+
+    func main() {
+        app := iris.New()
+        // Optionally, add two built'n handlers
+        // that can recover from any http-relative panics
+        // and log the requests to the terminal.
+        app.Use(recover.New())
+        app.Use(logger.New())
+
+        // Serve a controller based on the root Router, "/".
+        mvc.New(app).Handle(new(ExampleController))
+
+        // http://localhost:8080
+        // http://localhost:8080/ping
+        // http://localhost:8080/hello
+        // http://localhost:8080/custom_path
+        app.Run(iris.Addr(":8080"))
+    }
+
+    // ExampleController serves the "/", "/ping" and "/hello".
+    type ExampleController struct{}
+
+    // Get serves
+    // Method:   GET
+    // Resource: http://localhost:8080
+    func (c *ExampleController) Get() mvc.Result {
+        return mvc.Response{
+            ContentType: "text/html",
+            Text:        "

Welcome

", + } + } + + // GetPing serves + // Method: GET + // Resource: http://localhost:8080/ping + func (c *ExampleController) GetPing() string { + return "pong" + } + + // GetHello serves + // Method: GET + // Resource: http://localhost:8080/hello + func (c *ExampleController) GetHello() interface{} { + return map[string]string{"message": "Hello Iris!"} + } + + +// GetUserBy serves +// Method: GET +// Resource: http://localhost:8080/user/{username:string} +// By is a reserved "keyword" to tell the framework that you're going to +// bind path parameters in the function's input arguments, and it also +// helps to have "Get" and "GetBy" in the same controller. +// +// func (c *ExampleController) GetUserBy(username string) mvc.Result { +// return mvc.View{ +// Name: "user/username.html", +// Data: username, +// } +// } + +Can use more than one, the factory will make sure +that the correct http methods are being registered for each route +for this controller, uncomment these if you want: + + func (c *ExampleController) Post() {} + func (c *ExampleController) Put() {} + func (c *ExampleController) Delete() {} + func (c *ExampleController) Connect() {} + func (c *ExampleController) Head() {} + func (c *ExampleController) Patch() {} + func (c *ExampleController) Options() {} + func (c *ExampleController) Trace() {} +*/ +// +/* + func (c *ExampleController) All() {} + // OR + func (c *ExampleController) Any() {} + + func (c *ExampleController) BeforeActivation(b mvc.BeforeActivation) { + // 1 -> the HTTP Method + // 2 -> the route's path + // 3 -> this controller's method name that should be handler for that route. + b.Handle("GET", "/mypath/{param}", "DoIt", optionalMiddlewareHere...) + } + + // After activation, all dependencies are set-ed - so read only access on them + // but still possible to add custom controller or simple standard handlers. + func (c *ExampleController) AfterActivation(a mvc.AfterActivation) {} + + +Iris web framework 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. + +Register custom controller's struct's methods as handlers with custom paths(even with regex parametermized path) +via the `BeforeActivation` custom event callback, per-controller. Example: + + package main + + import ( + "github.com/kataras/iris" + "github.com/kataras/iris/mvc" + ) + + func main() { + app := iris.New() + mvc.Configure(app.Party("/root"), myMVC) + app.Run(iris.Addr(":8080")) + } + + func myMVC(app *mvc.Application) { + // app.Register(...) + // app.Router.Use/UseGlobal/Done(...) + app.Handle(new(MyController)) + } + + type MyController struct {} + + func (m *MyController) BeforeActivation(b mvc.BeforeActivation) { + // b.Dependencies().Add/Remove + // b.Router().Use/UseGlobal/Done // and any standard API call you already know + + // 1-> Method + // 2-> Path + // 3-> The controller's function name to be parsed as handler + // 4-> Any handlers that should run before the MyCustomHandler + b.Handle("GET", "/something/{id:long}", "MyCustomHandler", anyMiddleware...) + } + + // GET: http://localhost:8080/root + func (m *MyController) Get() string { return "Hey" } + + // GET: http://localhost:8080/root/something/{id:long} + func (m *MyController) MyCustomHandler(id int64) string { return "MyCustomHandler says Hey" } + + +Persistence data inside your Controller struct (share data between requests) +by defining services to the Dependencies or have a `Singleton` controller scope. + +Share the dependencies between controllers or register them on a parent MVC Application, and ability +to modify dependencies per-controller on the `BeforeActivation` optional event callback inside a Controller, +i.e + + func(c *MyController) BeforeActivation(b mvc.BeforeActivation) { b.Dependencies().Add/Remove(...) } + +Access to the `Context` as a controller's field(no manual binding is neede) i.e `Ctx iris.Context` or via a method's input argument, +i.e + func(ctx iris.Context, otherArguments...) + +Models inside your Controller struct (set-ed at the Method function and rendered by the View). +You can return models from a controller's method or set a field in the request lifecycle +and return that field to another method, in the same request lifecycle. + +Flow as you used to, mvc application has its own `Router` which is a type of `iris/router.Party`, the standard iris api. +`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. + +Session dynamic dependency via manager's `Start` to the MVC Application, i.e + + mvcApp.Register(sessions.New(sessions.Config{Cookie: "iris_session_id"}).Start) + +Inheritance, recursively. + +Access to the dynamic path parameters via the controller's methods' input arguments, no binding is needed. +When you use the Iris' default syntax to parse handlers from a controller, you need to suffix the methods +with the `By` word, uppercase is a new sub path. Example: + +Register one or more relative paths and able to get path parameters, i.e + + If `mvc.New(app.Party("/user")).Handle(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 `mvc.New(app.Party("/profile")).Handle(new(profile.Controller))` + + - `func(*Controller) GetBy(username string)` - `GET:/profile/{param:string}` + + If `mvc.New(app.Party("/assets")).Handle(new(file.Controller))` + + - `func(*Controller) GetByWildard(path string)` - `GET:/assets/{param:path}` + + If `mvc.New(app.Party("/equality")).Handle(new(profile.Equality))` + + - `func(*Controller) GetBy(is bool)` - `GET:/equality/{param:boolean}` + - `func(*Controller) GetByOtherBy(is bool, otherID int64)` - `GET:/equality/{paramfirst:boolean}/other/{paramsecond:long}` + + Supported types for method functions receivers: int, int64, bool and string. + +Response via output arguments, optionally, i.e + + func(c *ExampleController) Get() string | + (string, string) | + (string, int) | + (string, error) | + int | + (int, string) | + (any, int) | + error | + (int, error) | + (customStruct, error) | + (any, error) | + bool | + (any, bool) + customStruct | + (customStruct, int) | + (customStruct, string) | + `Result` or (`Result`, error) + +Where `any` means everything, from custom structs to standard language's types-. +`Result` is an interface which contains only that function: Dispatch(ctx iris.Context) +and Get where HTTP Method function(Post, Put, Delete...). + + +Iris MVC Method Result + +Iris has a very powerful and blazing fast 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 `bool` is false then it throws 404 not found http error by skipping everything else. +* 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 `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. + +Examples with good patterns to follow but not intend to be used in production of course can be found at: +https://github.com/kataras/iris/tree/master/_examples/#mvc. + + +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. + + + That's the basics But you should have a basic idea of the framework by now, we just scratched the surface. diff --git a/go19.go b/go19.go index 4df8568135..8a16665ea8 100644 --- a/go19.go +++ b/go19.go @@ -6,7 +6,6 @@ import ( "github.com/kataras/iris/context" "github.com/kataras/iris/core/host" "github.com/kataras/iris/core/router" - "github.com/kataras/iris/mvc" ) type ( @@ -48,132 +47,4 @@ type ( // // A shortcut for the `core/router#Party`, useful when `PartyFunc` is being used. Party = router.Party - - // Controller is the base controller for the high level controllers instances. - // - // This base controller is used as an alternative way of building - // APIs, the controller can register all type of http methods. - // - // Keep note that controllers are bit slow - // because of the reflection use however it's as fast as possible because - // it does preparation before the serve-time handler but still - // remains slower than the low-level handlers - // such as `Handle, Get, Post, Put, Delete, Connect, Head, Trace, Patch`. - // - // - // All fields that are tagged with iris:"persistence"` - // are being persistence and kept between the different requests, - // meaning that these data will not be reset-ed on each new request, - // they will be the same for all requests. - // - // An Example Controller can be: - // - // type IndexController struct { - // iris.Controller - // } - // - // func (c *IndexController) Get() { - // c.Tmpl = "index.html" - // c.Data["title"] = "Index page" - // c.Data["message"] = "Hello world!" - // } - // - // Usage: app.Controller("/", new(IndexController)) - // - // - // Another example with persistence data: - // - // type UserController struct { - // iris.Controller - // - // CreatedAt time.Time `iris:"persistence"` - // Title string `iris:"persistence"` - // DB *DB `iris:"persistence"` - // } - // - // // Get serves using the User controller when HTTP Method is "GET". - // func (c *UserController) Get() { - // c.Tmpl = "user/index.html" - // c.Data["title"] = c.Title - // c.Data["username"] = "kataras " + c.Params.Get("userid") - // c.Data["connstring"] = c.DB.Connstring - // c.Data["uptime"] = time.Now().Sub(c.CreatedAt).Seconds() - // } - // - // Usage: app.Controller("/user/{id:int}", &UserController{ - // CreatedAt: time.Now(), - // Title: "User page", - // DB: yourDB, - // }) - // - // Look `core/router#APIBuilder#Controller` method too. - // - // A shortcut for the `mvc#Controller`, - // useful when `app.Controller` method is being used. - // - // A Controller can be declared by importing - // the "github.com/kataras/iris/mvc" - // package for machines that have not installed go1.9 yet. - Controller = mvc.Controller - // SessionController is a simple `Controller` implementation - // which requires a binded session manager in order to give - // direct access to the current client's session via its `Session` field. - SessionController = mvc.SessionController - // C is the lightweight BaseController type as an alternative of the `Controller` struct type. - // It contains only the Name of the controller and the Context, it's the best option - // to balance the performance cost reflection uses - // if your controller uses the new func output values dispatcher feature; - // func(c *ExampleController) Get() string | - // (string, string) | - // (string, int) | - // int | - // (int, string | - // (string, error) | - // error | - // (int, error) | - // (customStruct, error) | - // customStruct | - // (customStruct, int) | - // (customStruct, string) | - // Result or (Result, error) - // where Get is an HTTP Method func. - // - // Look `core/router#APIBuilder#Controller` method too. - // - // A shortcut for the `mvc#C`, - // useful when `app.Controller` method is being used. - // - // A C controller can be declared by importing - // the "github.com/kataras/iris/mvc" as well. - C = mvc.C - // Response completes the `mvc/activator/methodfunc.Result` interface. - // It's being used as an alternative return value which - // wraps the status code, the content type, a content as bytes or as string - // and an error, it's smart enough to complete the request and send the correct response to the client. - // - // A shortcut for the `mvc#Response`, - // useful when return values from method functions, i.e - // GetHelloworld() iris.Response { iris.Response{ Text:"Hello World!", Code: 200 }} - Response = mvc.Response - // View completes the `mvc/activator/methodfunc.Result` interface. - // It's being used as an alternative return value which - // wraps the template file name, layout, (any) view data, status code and error. - // It's smart enough to complete the request and send the correct response to the client. - // - // A shortcut for the `mvc#View`, - // useful when return values from method functions, i.e - // GetUser() iris.View { iris.View{ Name:"user.html", Data: currentUser } } - View = mvc.View - // Result is a response dispatcher. - // All types that complete this interface - // can be returned as values from the method functions. - // A shortcut for the `mvc#Result` which is a shortcut for `mvc/activator/methodfunc#Result`, - // useful when return values from method functions, i.e - // GetUser() iris.Result { iris.Response{} or a custom iris.Result } - // Can be also used for the TryResult function. - Result = mvc.Result ) - -// Try is a shortcut for the function `mvc.Try` result. -// See more at `mvc#Try` documentation. -var Try = mvc.Try diff --git a/hero/AUTHORS b/hero/AUTHORS new file mode 100644 index 0000000000..848245bb12 --- /dev/null +++ b/hero/AUTHORS @@ -0,0 +1 @@ +Gerasimos Maropoulos diff --git a/hero/LICENSE b/hero/LICENSE new file mode 100644 index 0000000000..a0b2d92fe0 --- /dev/null +++ b/hero/LICENSE @@ -0,0 +1,27 @@ +Copyright (c) 2018 Gerasimos Maropoulos. All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + + * Redistributions of source code must retain the above copyright +notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above +copyright notice, this list of conditions and the following disclaimer +in the documentation and/or other materials provided with the +distribution. + * Neither the name of Iris nor the name of Iris Hero, nor the names of its +contributor, Gerasimos Maropoulos, may be used to endorse or promote products +derived from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. \ No newline at end of file diff --git a/hero/di.go b/hero/di.go new file mode 100644 index 0000000000..4244cfa0a9 --- /dev/null +++ b/hero/di.go @@ -0,0 +1,31 @@ +package hero + +import ( + "reflect" + + "github.com/kataras/iris/hero/di" +) + +func init() { + di.DefaultHijacker = func(fieldOrFuncInput reflect.Type) (*di.BindObject, bool) { + if !IsContext(fieldOrFuncInput) { + return nil, false + } + // this is being used on both func injector and struct injector. + // if the func's input argument or the struct's field is a type of Context + // then we can do a fast binding using the ctxValue + // which is used as slice of reflect.Value, because of the final method's `Call`. + return &di.BindObject{ + Type: contextTyp, + BindType: di.Dynamic, + ReturnValue: func(ctxValue []reflect.Value) reflect.Value { + return ctxValue[0] + }, + }, true + } + + di.DefaultTypeChecker = func(fn reflect.Type) bool { + // valid if that single input arg is a typeof context.Context. + return fn.NumIn() == 1 && IsContext(fn.In(0)) + } +} diff --git a/hero/di/di.go b/hero/di/di.go new file mode 100644 index 0000000000..e41447934c --- /dev/null +++ b/hero/di/di.go @@ -0,0 +1,129 @@ +package di + +import "reflect" + +type ( + // Hijacker is a type which is used to catch fields or function's input argument + // to bind a custom object based on their type. + Hijacker func(reflect.Type) (*BindObject, bool) + // TypeChecker checks if a specific field's or function input argument's + // is valid to be binded. + TypeChecker func(reflect.Type) bool +) + +var ( + // DefaultHijacker is the hijacker used on the package-level Struct & Func functions. + DefaultHijacker Hijacker + // DefaultTypeChecker is the typechecker used on the package-level Struct & Func functions. + DefaultTypeChecker TypeChecker +) + +// Struct is being used to return a new injector based on +// a struct value instance, if it contains fields that the types of those +// are matching with one or more of the `Values` then they are binded +// with the injector's `Inject` and `InjectElem` methods. +func Struct(s interface{}, values ...reflect.Value) *StructInjector { + if s == nil { + return &StructInjector{Has: false} + } + + return MakeStructInjector( + ValueOf(s), + DefaultHijacker, + DefaultTypeChecker, + Values(values).CloneWithFieldsOf(s)..., + ) +} + +// Func is being used to return a new injector based on +// a function, if it contains input arguments that the types of those +// are matching with one or more of the `Values` then they are binded +// to the function's input argument when called +// with the injector's `Inject` method. +func Func(fn interface{}, values ...reflect.Value) *FuncInjector { + if fn == nil { + return &FuncInjector{Has: false} + } + + return MakeFuncInjector( + ValueOf(fn), + DefaultHijacker, + DefaultTypeChecker, + values..., + ) +} + +// D is the Dependency Injection container, +// it contains the Values that can be changed before the injectors. +// `Struct` and the `Func` methods returns an injector for specific +// struct instance-value or function. +type D struct { + Values + + hijacker Hijacker + goodFunc TypeChecker +} + +// New creates and returns a new Dependency Injection container. +// See `Values` field and `Func` and `Struct` methods for more. +func New() *D { + return &D{} +} + +// Hijack sets a hijacker function, read the `Hijacker` type for more explanation. +func (d *D) Hijack(fn Hijacker) *D { + d.hijacker = fn + return d +} + +// GoodFunc sets a type checker for a valid function that can be binded, +// read the `TypeChecker` type for more explanation. +func (d *D) GoodFunc(fn TypeChecker) *D { + d.goodFunc = fn + return d +} + +// Clone returns a new Dependency Injection container, it adopts the +// parent's (current "D") hijacker, good func type checker and all dependencies values. +func (d *D) Clone() *D { + return &D{ + Values: d.Values.Clone(), + hijacker: d.hijacker, + goodFunc: d.goodFunc, + } +} + +// Struct is being used to return a new injector based on +// a struct value instance, if it contains fields that the types of those +// are matching with one or more of the `Values` then they are binded +// with the injector's `Inject` and `InjectElem` methods. +func (d *D) Struct(s interface{}) *StructInjector { + if s == nil { + return &StructInjector{Has: false} + } + + return MakeStructInjector( + ValueOf(s), + d.hijacker, + d.goodFunc, + d.Values.CloneWithFieldsOf(s)..., + ) +} + +// Func is being used to return a new injector based on +// a function, if it contains input arguments that the types of those +// are matching with one or more of the `Values` then they are binded +// to the function's input argument when called +// with the injector's `Inject` method. +func (d *D) Func(fn interface{}) *FuncInjector { + if fn == nil { + return &FuncInjector{Has: false} + } + + return MakeFuncInjector( + ValueOf(fn), + d.hijacker, + d.goodFunc, + d.Values..., + ) +} diff --git a/hero/di/func.go b/hero/di/func.go new file mode 100644 index 0000000000..fe69dfb0f0 --- /dev/null +++ b/hero/di/func.go @@ -0,0 +1,215 @@ +package di + +import ( + "fmt" + "reflect" +) + +type ( + targetFuncInput struct { + Object *BindObject + InputIndex int + } + + // FuncInjector keeps the data that are needed in order to do the binding injection + // as fast as possible and with the best possible and safest way. + FuncInjector struct { + // the original function, is being used + // only the .Call, which is referring to the same function, always. + fn reflect.Value + typ reflect.Type + goodFunc TypeChecker + + inputs []*targetFuncInput + // Length is the number of the valid, final binded input arguments. + Length int + // Valid is True when `Length` is > 0, it's statically set-ed for + // performance reasons. + Has bool + + trace string // for debug info. + + lost []*missingInput // Author's note: don't change this to a map. + } +) + +type missingInput struct { + index int // the function's input argument's index. + found bool +} + +func (s *FuncInjector) miss(index int) { + s.lost = append(s.lost, &missingInput{ + index: index, + }) +} + +// MakeFuncInjector returns a new func injector, which will be the object +// that the caller should use to bind input arguments of the "fn" function. +// +// The hijack and the goodFunc are optional, the "values" is the dependencies collection. +func MakeFuncInjector(fn reflect.Value, hijack Hijacker, goodFunc TypeChecker, values ...reflect.Value) *FuncInjector { + typ := IndirectType(fn.Type()) + s := &FuncInjector{ + fn: fn, + typ: typ, + goodFunc: goodFunc, + } + + if !IsFunc(typ) { + return s + } + + defer s.refresh() + + n := typ.NumIn() + + for i := 0; i < n; i++ { + inTyp := typ.In(i) + + if hijack != nil { + b, ok := hijack(inTyp) + + if ok && b != nil { + s.inputs = append(s.inputs, &targetFuncInput{ + InputIndex: i, + Object: b, + }) + continue + } + } + + matched := false + + for j, v := range values { + if s.addValue(i, v) { + matched = true + // remove this value, so it will not try to get binded + // again, a next value even with the same type is able to be + // used to other input arg. One value per input argument, order + // matters if same type of course. + //if len(values) > j+1 { + values = append(values[:j], values[j+1:]...) + //} + + break + } + } + + if !matched { + // if no binding for this input argument, + // this will make the func injector invalid state, + // but before this let's make a list of failed + // inputs, so they can be used for a re-try + // with different set of binding "values". + s.miss(i) + } + + } + + return s +} + +func (s *FuncInjector) refresh() { + s.Length = len(s.inputs) + s.Has = s.Length > 0 +} + +func (s *FuncInjector) addValue(inputIndex int, value reflect.Value) bool { + defer s.refresh() + + if s.typ.NumIn() < inputIndex { + return false + } + + inTyp := s.typ.In(inputIndex) + + // the binded values to the func's inputs. + b, err := MakeBindObject(value, s.goodFunc) + + if err != nil { + return false + } + + if b.IsAssignable(inTyp) { + // println(inTyp.String() + " is assignable to " + val.Type().String()) + // fmt.Printf("binded input index: %d for type: %s and value: %v with pointer: %v\n", + // i, b.Type.String(), value.String(), val.Pointer()) + s.inputs = append(s.inputs, &targetFuncInput{ + InputIndex: inputIndex, + Object: &b, + }) + return true + } + + return false +} + +func (s *FuncInjector) Retry(retryFn func(inIndex int, inTyp reflect.Type) (reflect.Value, bool)) bool { + for _, missing := range s.lost { + if missing.found { + continue + } + + invalidIndex := missing.index + + inTyp := s.typ.In(invalidIndex) + v, ok := retryFn(invalidIndex, inTyp) + if !ok { + continue + } + + if !s.addValue(invalidIndex, v) { + continue + } + + // if this value completes an invalid index + // then remove this from the invalid input indexes. + missing.found = true + } + + return s.Length == s.typ.NumIn() +} + +// String returns a debug trace text. +func (s *FuncInjector) String() (trace string) { + for i, in := range s.inputs { + bindmethodTyp := bindTypeString(in.Object.BindType) + typIn := s.typ.In(in.InputIndex) + // remember: on methods that are part of a struct (i.e controller) + // the input index = 1 is the begggining instead of the 0, + // because the 0 is the controller receiver pointer of the method. + trace += fmt.Sprintf("[%d] %s binding: '%s' for input position: %d and type: '%s'\n", + i+1, bindmethodTyp, in.Object.Type.String(), in.InputIndex, typIn.String()) + } + return +} + +// Inject accepts an already created slice of input arguments +// and fills them, the "ctx" is optional and it's used +// on the dependencies that depends on one or more input arguments, these are the "ctx". +func (s *FuncInjector) Inject(in *[]reflect.Value, ctx ...reflect.Value) { + args := *in + for _, input := range s.inputs { + input.Object.Assign(ctx, func(v reflect.Value) { + // fmt.Printf("assign input index: %d for value: %v\n", + // input.InputIndex, v.String()) + args[input.InputIndex] = v + }) + + } + + *in = args +} + +// Call calls the "Inject" with a new slice of input arguments +// that are computed by the length of the input argument from the MakeFuncInjector's "fn" function. +// +// If the function needs a receiver, so +// the caller should be able to in[0] = receiver before injection, +// then the `Inject` method should be used instead. +func (s *FuncInjector) Call(ctx ...reflect.Value) []reflect.Value { + in := make([]reflect.Value, s.Length, s.Length) + s.Inject(&in, ctx...) + return s.fn.Call(in) +} diff --git a/hero/di/object.go b/hero/di/object.go new file mode 100644 index 0000000000..392abcc587 --- /dev/null +++ b/hero/di/object.go @@ -0,0 +1,123 @@ +package di + +import ( + "errors" + "reflect" +) + +// BindType is the type of a binded object/value, it's being used to +// check if the value is accessible after a function call with a "ctx" when needed ( Dynamic type) +// or it's just a struct value (a service | Static type). +type BindType uint32 + +const ( + // Static is the simple assignable value, a static value. + Static BindType = iota + // Dynamic returns a value but it depends on some input arguments from the caller, + // on serve time. + Dynamic +) + +func bindTypeString(typ BindType) string { + switch typ { + case Dynamic: + return "Dynamic" + default: + return "Static" + } +} + +// BindObject contains the dependency value's read-only information. +// FuncInjector and StructInjector keeps information about their +// input arguments/or fields, these properties contain a `BindObject` inside them. +type BindObject struct { + Type reflect.Type // the Type of 'Value' or the type of the returned 'ReturnValue' . + Value reflect.Value + + BindType BindType + ReturnValue func([]reflect.Value) reflect.Value +} + +// MakeBindObject accepts any "v" value, struct, pointer or a function +// and a type checker that is used to check if the fields (if "v.elem()" is struct) +// or the input arguments (if "v.elem()" is func) +// are valid to be included as the final object's dependencies, even if the caller added more +// the "di" is smart enough to select what each "v" needs and what not before serve time. +func MakeBindObject(v reflect.Value, goodFunc TypeChecker) (b BindObject, err error) { + if IsFunc(v) { + b.BindType = Dynamic + b.ReturnValue, b.Type, err = MakeReturnValue(v, goodFunc) + } else { + b.BindType = Static + b.Type = v.Type() + b.Value = v + } + + return +} + +var errBad = errors.New("bad") + +// MakeReturnValue takes any function +// that accept custom values and returns something, +// it returns a binder function, which accepts a slice of reflect.Value +// and returns a single one reflect.Value for that. +// It's being used to resolve the input parameters on a "x" consumer faster. +// +// The "fn" can have the following form: +// `func(myService) MyViewModel`. +// +// The return type of the "fn" should be a value instance, not a pointer, for your own protection. +// The binder function should return only one value. +func MakeReturnValue(fn reflect.Value, goodFunc TypeChecker) (func([]reflect.Value) reflect.Value, reflect.Type, error) { + typ := IndirectType(fn.Type()) + + // invalid if not a func. + if typ.Kind() != reflect.Func { + return nil, typ, errBad + } + + // invalid if not returns one single value. + if typ.NumOut() != 1 { + return nil, typ, errBad + } + + if goodFunc != nil { + if !goodFunc(typ) { + return nil, typ, errBad + } + } + + outTyp := typ.Out(0) + zeroOutVal := reflect.New(outTyp).Elem() + + bf := func(ctxValue []reflect.Value) reflect.Value { + results := fn.Call(ctxValue) + if len(results) == 0 { + return zeroOutVal + } + + v := results[0] + if !v.IsValid() { + return zeroOutVal + } + return v + } + + return bf, outTyp, nil +} + +// IsAssignable checks if "to" type can be used as "b.Value/ReturnValue". +func (b *BindObject) IsAssignable(to reflect.Type) bool { + return equalTypes(b.Type, to) +} + +// Assign sets the values to a setter, "toSetter" contains the setter, so the caller +// can use it for multiple and different structs/functions as well. +func (b *BindObject) Assign(ctx []reflect.Value, toSetter func(reflect.Value)) { + if b.BindType == Dynamic { + toSetter(b.ReturnValue(ctx)) + return + } + toSetter(b.Value) +} diff --git a/hero/di/reflect.go b/hero/di/reflect.go new file mode 100644 index 0000000000..9713ddb8ce --- /dev/null +++ b/hero/di/reflect.go @@ -0,0 +1,202 @@ +package di + +import "reflect" + +// EmptyIn is just an empty slice of reflect.Value. +var EmptyIn = []reflect.Value{} + +// IsZero returns true if a value is nil. +// Remember; fields to be checked should be exported otherwise it returns false. +// Notes for users: +// Boolean's zero value is false, even if not set-ed. +// UintXX are not zero on 0 because they are pointers to. +func IsZero(v reflect.Value) bool { + switch v.Kind() { + case reflect.Struct: + zero := true + for i := 0; i < v.NumField(); i++ { + zero = zero && IsZero(v.Field(i)) + } + + if typ := v.Type(); typ != nil && v.IsValid() { + f, ok := typ.MethodByName("IsZero") + // if not found + // if has input arguments (1 is for the value receiver, so > 1 for the actual input args) + // if output argument is not boolean + // then skip this IsZero user-defined function. + if !ok || f.Type.NumIn() > 1 || f.Type.NumOut() != 1 && f.Type.Out(0).Kind() != reflect.Bool { + return zero + } + + method := v.Method(f.Index) + // no needed check but: + if method.IsValid() && !method.IsNil() { + // it shouldn't panic here. + zero = method.Call(EmptyIn)[0].Interface().(bool) + } + } + + return zero + case reflect.Func, reflect.Map, reflect.Slice: + return v.IsNil() + case reflect.Array: + zero := true + for i := 0; i < v.Len(); i++ { + zero = zero && IsZero(v.Index(i)) + } + return zero + } + // if not any special type then use the reflect's .Zero + // usually for fields, but remember if it's boolean and it's false + // then it's zero, even if set-ed. + + if !v.CanInterface() { + // if can't interface, i.e return value from unexported field or method then return false + return false + } + zero := reflect.Zero(v.Type()) + return v.Interface() == zero.Interface() +} + +func IndirectValue(v reflect.Value) reflect.Value { + return reflect.Indirect(v) +} + +func ValueOf(o interface{}) reflect.Value { + if v, ok := o.(reflect.Value); ok { + return v + } + + return reflect.ValueOf(o) +} + +func ValuesOf(valuesAsInterface []interface{}) (values []reflect.Value) { + for _, v := range valuesAsInterface { + values = append(values, ValueOf(v)) + } + return +} + +func IndirectType(typ reflect.Type) reflect.Type { + switch typ.Kind() { + case reflect.Ptr, reflect.Array, reflect.Chan, reflect.Map, reflect.Slice: + return typ.Elem() + } + return typ +} + +func goodVal(v reflect.Value) bool { + switch v.Kind() { + case reflect.Chan, reflect.Func, reflect.Map, reflect.Ptr, reflect.Interface, reflect.Slice: + if v.IsNil() { + return false + } + } + + return v.IsValid() +} + +// IsFunc returns true if the passed type is function. +func IsFunc(kindable interface { + Kind() reflect.Kind +}) bool { + return kindable.Kind() == reflect.Func +} + +func equalTypes(got reflect.Type, expected reflect.Type) bool { + if got == expected { + return true + } + // if accepts an interface, check if the given "got" type does + // implement this "expected" user handler's input argument. + if expected.Kind() == reflect.Interface { + // fmt.Printf("expected interface = %s and got to set on the arg is: %s\n", expected.String(), got.String()) + return got.Implements(expected) + } + return false +} + +// for controller's fields only. +func structFieldIgnored(f reflect.StructField) bool { + if !f.Anonymous { + return true // if not anonymous(embedded), ignore it. + } + + s := f.Tag.Get("ignore") + return s == "true" // if has an ignore tag then ignore it. +} + +type field struct { + Type reflect.Type + Name string // the actual name. + Index []int // the index of the field, slice if it's part of a embedded struct + CanSet bool // is true if it's exported. + + // this could be empty, but in our cases it's not, + // it's filled with the bind object (as service which means as static value) + // and it's filled from the lookupFields' caller. + AnyValue reflect.Value +} + +// NumFields returns the total number of fields, and the embedded, even if the embedded struct is not exported, +// it will check for its exported fields. +func NumFields(elemTyp reflect.Type, skipUnexported bool) int { + return len(lookupFields(elemTyp, skipUnexported, nil)) +} + +func lookupFields(elemTyp reflect.Type, skipUnexported bool, parentIndex []int) (fields []field) { + if elemTyp.Kind() != reflect.Struct { + return + } + + for i, n := 0, elemTyp.NumField(); i < n; i++ { + f := elemTyp.Field(i) + + if IndirectType(f.Type).Kind() == reflect.Struct && + !structFieldIgnored(f) { + fields = append(fields, lookupFields(f.Type, skipUnexported, append(parentIndex, i))...) + continue + } + + // skip unexported fields here, + // after the check for embedded structs, these can be binded if their + // fields are exported. + isExported := f.PkgPath == "" + if skipUnexported && !isExported { + continue + } + + index := []int{i} + if len(parentIndex) > 0 { + index = append(parentIndex, i) + } + + field := field{ + Type: f.Type, + Name: f.Name, + Index: index, + CanSet: isExported, + } + + fields = append(fields, field) + } + + return +} + +// LookupNonZeroFieldsValues lookup for filled fields based on the "v" struct value instance. +// It returns a slice of reflect.Value (same type as `Values`) that can be binded, +// like the end-developer's custom values. +func LookupNonZeroFieldsValues(v reflect.Value, skipUnexported bool) (bindValues []reflect.Value) { + elem := IndirectValue(v) + fields := lookupFields(IndirectType(v.Type()), skipUnexported, nil) + + for _, f := range fields { + if fieldVal := elem.FieldByIndex(f.Index); /*f.Type.Kind() == reflect.Ptr &&*/ + !IsZero(fieldVal) { + bindValues = append(bindValues, fieldVal) + } + } + + return +} diff --git a/hero/di/struct.go b/hero/di/struct.go new file mode 100644 index 0000000000..cc48b1e45e --- /dev/null +++ b/hero/di/struct.go @@ -0,0 +1,201 @@ +package di + +import ( + "fmt" + "reflect" +) + +type Scope uint8 + +const ( + Stateless Scope = iota + Singleton +) + +type ( + targetStructField struct { + Object *BindObject + FieldIndex []int + } + + // StructInjector keeps the data that are needed in order to do the binding injection + // as fast as possible and with the best possible and safest way. + StructInjector struct { + initRef reflect.Value + initRefAsSlice []reflect.Value // useful when the struct is passed on a func as input args via reflection. + elemType reflect.Type + // + fields []*targetStructField + // is true when contains bindable fields and it's a valid target struct, + // it maybe 0 but struct may contain unexported fields or exported but no bindable (Stateless) + // see `setState`. + Has bool + CanInject bool // if any bindable fields when the state is NOT singleton. + Scope Scope + } +) + +func (s *StructInjector) countBindType(typ BindType) (n int) { + for _, f := range s.fields { + if f.Object.BindType == typ { + n++ + } + } + return +} + +// MakeStructInjector returns a new struct injector, which will be the object +// that the caller should use to bind exported fields or +// embedded unexported fields that contain exported fields +// of the "v" struct value or pointer. +// +// The hijack and the goodFunc are optional, the "values" is the dependencies collection. +func MakeStructInjector(v reflect.Value, hijack Hijacker, goodFunc TypeChecker, values ...reflect.Value) *StructInjector { + s := &StructInjector{ + initRef: v, + initRefAsSlice: []reflect.Value{v}, + elemType: IndirectType(v.Type()), + } + + fields := lookupFields(s.elemType, true, nil) + for _, f := range fields { + if hijack != nil { + if b, ok := hijack(f.Type); ok && b != nil { + s.fields = append(s.fields, &targetStructField{ + FieldIndex: f.Index, + Object: b, + }) + + continue + } + } + + for _, val := range values { + // the binded values to the struct's fields. + b, err := MakeBindObject(val, goodFunc) + + if err != nil { + return s // if error stop here. + } + + if b.IsAssignable(f.Type) { + // fmt.Printf("bind the object to the field: %s at index: %#v and type: %s\n", f.Name, f.Index, f.Type.String()) + s.fields = append(s.fields, &targetStructField{ + FieldIndex: f.Index, + Object: &b, + }) + break + } + } + } + + s.Has = len(s.fields) > 0 + // set the overall state of this injector. + s.fillStruct() + s.setState() + + return s +} + +// set the state, once. +// Here the "initRef" have already the static bindings and the manually-filled fields. +func (s *StructInjector) setState() { + // note for zero length of struct's fields: + // if struct doesn't contain any field + // so both of the below variables will be 0, + // so it's a singleton. + // At the other hand the `s.HasFields` maybe false + // but the struct may contain UNEXPORTED fields or non-bindable fields (request-scoped on both cases) + // so a new controller/struct at the caller side should be initialized on each request, + // we should not depend on the `HasFields` for singleton or no, this is the reason I + // added the `.State` now. + + staticBindingsFieldsLength := s.countBindType(Static) + allStructFieldsLength := NumFields(s.elemType, false) + // check if unexported(and exported) fields are set-ed manually or via binding (at this time we have all fields set-ed inside the "initRef") + // i.e &Controller{unexportedField: "my value"} + // or dependencies values = "my value" and Controller struct {Field string} + // if so then set the temp staticBindingsFieldsLength to that number, so for example: + // if static binding length is 0 + // but an unexported field is set-ed then act that as singleton. + if allStructFieldsLength > staticBindingsFieldsLength { + structFieldsUnexportedNonZero := LookupNonZeroFieldsValues(s.initRef, false) + staticBindingsFieldsLength = len(structFieldsUnexportedNonZero) + } + + // println("staticBindingsFieldsLength: ", staticBindingsFieldsLength) + // println("allStructFieldsLength: ", allStructFieldsLength) + + // if the number of static values binded is equal to the + // total struct's fields(including unexported fields this time) then set as singleton. + if staticBindingsFieldsLength == allStructFieldsLength { + s.Scope = Singleton + // the default is `Stateless`, which means that a new instance should be created + // on each inject action by the caller. + return + } + + s.CanInject = s.Scope == Stateless && s.Has +} + +// fill the static bindings values once. +func (s *StructInjector) fillStruct() { + if !s.Has { + return + } + // if field is Static then set it to the value that passed by the caller, + // so will have the static bindings already and we can just use that value instead + // of creating new instance. + destElem := IndirectValue(s.initRef) + for _, f := range s.fields { + // if field is Static then set it to the value that passed by the caller, + // so will have the static bindings already and we can just use that value instead + // of creating new instance. + if f.Object.BindType == Static { + destElem.FieldByIndex(f.FieldIndex).Set(f.Object.Value) + } + } +} + +// String returns a debug trace message. +func (s *StructInjector) String() (trace string) { + for i, f := range s.fields { + elemField := s.elemType.FieldByIndex(f.FieldIndex) + trace += fmt.Sprintf("[%d] %s binding: '%s' for field '%s %s'\n", + i+1, bindTypeString(f.Object.BindType), f.Object.Type.String(), + elemField.Name, elemField.Type.String()) + } + + return +} + +func (s *StructInjector) Inject(dest interface{}, ctx ...reflect.Value) { + if dest == nil { + return + } + + v := IndirectValue(ValueOf(dest)) + s.InjectElem(v, ctx...) +} + +func (s *StructInjector) InjectElem(destElem reflect.Value, ctx ...reflect.Value) { + for _, f := range s.fields { + f.Object.Assign(ctx, func(v reflect.Value) { + destElem.FieldByIndex(f.FieldIndex).Set(v) + }) + } +} + +func (s *StructInjector) Acquire() reflect.Value { + if s.Scope == Singleton { + return s.initRef + } + return reflect.New(s.elemType) +} + +func (s *StructInjector) AcquireSlice() []reflect.Value { + if s.Scope == Singleton { + return s.initRefAsSlice + } + return []reflect.Value{reflect.New(s.elemType)} +} diff --git a/hero/di/values.go b/hero/di/values.go new file mode 100644 index 0000000000..1033b95752 --- /dev/null +++ b/hero/di/values.go @@ -0,0 +1,126 @@ +package di + +import "reflect" + +// Values is a shortcut of []reflect.Value, +// it makes easier to remove and add dependencies. +type Values []reflect.Value + +// NewValues returns new empty (dependencies) values. +func NewValues() Values { + return Values{} +} + +// Clone returns a copy of the current values. +func (bv Values) Clone() Values { + if n := len(bv); n > 0 { + values := make(Values, n, n) + copy(values, bv) + return values + } + + return NewValues() +} + +// CloneWithFieldsOf will return a copy of the current values +// plus the "s" struct's fields that are filled(non-zero) by the caller. +func (bv Values) CloneWithFieldsOf(s interface{}) Values { + values := bv.Clone() + + // add the manual filled fields to the dependencies. + filledFieldValues := LookupNonZeroFieldsValues(ValueOf(s), true) + values = append(values, filledFieldValues...) + return values +} + +// Len returns the length of the current "bv" values slice. +func (bv Values) Len() int { + return len(bv) +} + +// Add adds values as dependencies, if the struct's fields +// or the function's input arguments needs them, they will be defined as +// bindings (at build-time) and they will be used (at serve-time). +func (bv *Values) Add(values ...interface{}) { + bv.AddValues(ValuesOf(values)...) +} + +// AddValues same as `Add` but accepts reflect.Value dependencies instead of interface{} +// and appends them to the list if they pass some checks. +func (bv *Values) AddValues(values ...reflect.Value) { + for _, v := range values { + if !goodVal(v) { + continue + } + *bv = append(*bv, v) + } +} + +// Remove unbinds a binding value based on the type, +// it returns true if at least one field is not binded anymore. +// +// The "n" indicates the number of elements to remove, if <=0 then it's 1, +// this is useful because you may have bind more than one value to two or more fields +// with the same type. +func (bv *Values) Remove(value interface{}, n int) bool { + return bv.remove(reflect.TypeOf(value), n) +} + +func (bv *Values) remove(typ reflect.Type, n int) (ok bool) { + input := *bv + for i, in := range input { + if equalTypes(in.Type(), typ) { + ok = true + input = input[:i+copy(input[i:], input[i+1:])] + if n > 1 { + continue + } + break + } + } + + *bv = input + + return +} + +// Has returns true if a binder responsible to +// bind and return a type of "typ" is already registered to this controller. +func (bv Values) Has(value interface{}) bool { + return bv.valueTypeExists(reflect.TypeOf(value)) +} + +func (bv Values) valueTypeExists(typ reflect.Type) bool { + for _, in := range bv { + if equalTypes(in.Type(), typ) { + return true + } + } + return false +} + +// AddOnce binds a value to the controller's field with the same type, +// if it's not binded already. +// +// Returns false if binded already or the value is not the proper one for binding, +// otherwise true. +func (bv *Values) AddOnce(value interface{}) bool { + return bv.addIfNotExists(reflect.ValueOf(value)) +} + +func (bv *Values) addIfNotExists(v reflect.Value) bool { + var ( + typ = v.Type() // no element, raw things here. + ) + + if !goodVal(v) { + return false + } + + if bv.valueTypeExists(typ) { + return false + } + + bv.Add(v) + return true +} diff --git a/hero/func_result.go b/hero/func_result.go new file mode 100644 index 0000000000..86c286c1bc --- /dev/null +++ b/hero/func_result.go @@ -0,0 +1,474 @@ +package hero + +import ( + "reflect" + "strings" + + "github.com/kataras/iris/context" + "github.com/kataras/iris/hero/di" + + "github.com/fatih/structs" +) + +// Result is a response dispatcher. +// All types that complete this interface +// can be returned as values from the method functions. +// +// Example at: https://github.com/kataras/iris/tree/master/_examples/hero/overview. +type Result interface { + // Dispatch should sends the response to the context's response writer. + Dispatch(ctx context.Context) +} + +var defaultFailureResponse = Response{Code: DefaultErrStatusCode} + +// Try will check if "fn" ran without any panics, +// using recovery, +// and return its result as the final response +// otherwise it returns the "failure" response if any, +// if not then a 400 bad request is being sent. +// +// Example usage at: https://github.com/kataras/iris/blob/master/hero/func_result_test.go. +func Try(fn func() Result, failure ...Result) Result { + var failed bool + var actionResponse Result + + func() { + defer func() { + if rec := recover(); rec != nil { + failed = true + } + }() + actionResponse = fn() + }() + + if failed { + if len(failure) > 0 { + return failure[0] + } + return defaultFailureResponse + } + + return actionResponse +} + +const slashB byte = '/' + +type compatibleErr interface { + Error() string +} + +// DefaultErrStatusCode is the default error status code (400) +// when the response contains an error which is not nil. +var DefaultErrStatusCode = 400 + +// DispatchErr writes the error to the response. +func DispatchErr(ctx context.Context, status int, err error) { + if status < 400 { + status = DefaultErrStatusCode + } + ctx.StatusCode(status) + if text := err.Error(); text != "" { + ctx.WriteString(text) + ctx.StopExecution() + } +} + +// DispatchCommon is being used internally to send +// commonly used data to the response writer with a smart way. +func DispatchCommon(ctx context.Context, + statusCode int, contentType string, content []byte, v interface{}, err error, found bool) { + + // if we have a false boolean as a return value + // then skip everything and fire a not found, + // we even don't care about the given status code or the object or the content. + if !found { + ctx.NotFound() + return + } + + status := statusCode + if status == 0 { + status = 200 + } + + if err != nil { + DispatchErr(ctx, status, err) + return + } + + // write the status code, the rest will need that before any write ofc. + ctx.StatusCode(status) + if contentType == "" { + // to respect any ctx.ContentType(...) call + // especially if v is not nil. + contentType = ctx.GetContentType() + } + + if v != nil { + if d, ok := v.(Result); ok { + // write the content type now (internal check for empty value) + ctx.ContentType(contentType) + d.Dispatch(ctx) + return + } + + if strings.HasPrefix(contentType, context.ContentJavascriptHeaderValue) { + _, err = ctx.JSONP(v) + } else if strings.HasPrefix(contentType, context.ContentXMLHeaderValue) { + _, err = ctx.XML(v, context.XML{Indent: " "}) + } else { + // defaults to json if content type is missing or its application/json. + _, err = ctx.JSON(v, context.JSON{Indent: " "}) + } + + if err != nil { + DispatchErr(ctx, status, err) + } + + return + } + + ctx.ContentType(contentType) + // .Write even len(content) == 0 , this should be called in order to call the internal tryWriteHeader, + // it will not cost anything. + ctx.Write(content) +} + +// DispatchFuncResult is being used internally to resolve +// and send the method function's output values to the +// context's response writer using a smart way which +// respects status code, content type, content, custom struct +// and an error type. +// Supports for: +// func(c *ExampleController) Get() string | +// (string, string) | +// (string, int) | +// ... +// int | +// (int, string | +// (string, error) | +// ... +// error | +// (int, error) | +// (customStruct, error) | +// ... +// bool | +// (int, bool) | +// (string, bool) | +// (customStruct, bool) | +// ... +// customStruct | +// (customStruct, int) | +// (customStruct, string) | +// Result or (Result, error) and so on... +// +// where Get is an HTTP METHOD. +func DispatchFuncResult(ctx context.Context, values []reflect.Value) { + if len(values) == 0 { + return + } + + var ( + // if statusCode > 0 then send this status code. + // Except when err != nil then check if status code is < 400 and + // if it's set it as DefaultErrStatusCode. + // Except when found == false, then the status code is 404. + statusCode int + // if not empty then use that as content type, + // if empty and custom != nil then set it to application/json. + contentType string + // if len > 0 then write that to the response writer as raw bytes, + // except when found == false or err != nil or custom != nil. + content []byte + // if not nil then check + // for content type (or json default) and send the custom data object + // except when found == false or err != nil. + custom interface{} + // if not nil then check for its status code, + // if not status code or < 400 then set it as DefaultErrStatusCode + // and fire the error's text. + err error + // if false then skip everything and fire 404. + found = true // defaults to true of course, otherwise will break :) + ) + + for _, v := range values { + + // order of these checks matters + // for example, first we need to check for status code, + // secondly the string (for content type and content)... + // if !v.IsValid() || !v.CanInterface() { + // continue + // } + if !v.IsValid() { + continue + } + + f := v.Interface() + /* + if b, ok := f.(bool); ok { + found = b + if !found { + // skip everything, we don't care about other return values, + // this boolean is the higher in order. + break + } + continue + } + + if i, ok := f.(int); ok { + statusCode = i + continue + } + + if s, ok := f.(string); ok { + // a string is content type when it contains a slash and + // content or custom struct is being calculated already; + // (string -> content, string-> content type) + // (customStruct, string -> content type) + if (len(content) > 0 || custom != nil) && strings.IndexByte(s, slashB) > 0 { + contentType = s + } else { + // otherwise is content + content = []byte(s) + } + + continue + } + + if b, ok := f.([]byte); ok { + // it's raw content, get the latest + content = b + continue + } + + if e, ok := f.(compatibleErr); ok { + if e != nil { // it's always not nil but keep it here. + err = e + if statusCode < 400 { + statusCode = DefaultErrStatusCode + } + break // break on first error, error should be in the end but we + // need to know break the dispatcher if any error. + // at the end; we don't want to write anything to the response if error is not nil. + } + continue + } + + // else it's a custom struct or a dispatcher, we'll decide later + // because content type and status code matters + // do that check in order to be able to correctly dispatch: + // (customStruct, error) -> customStruct filled and error is nil + if custom == nil && f != nil { + custom = f + } + + } + + */ + switch value := f.(type) { + case bool: + found = value + if !found { + // skip everything, skip other values, we don't care about other return values, + // this boolean is the higher in order. + break + } + case int: + statusCode = value + case string: + // a string is content type when it contains a slash and + // content or custom struct is being calculated already; + // (string -> content, string-> content type) + // (customStruct, string -> content type) + if (len(content) > 0 || custom != nil) && strings.IndexByte(value, slashB) > 0 { + contentType = value + } else { + // otherwise is content + content = []byte(value) + } + + case []byte: + // it's raw content, get the latest + content = value + case compatibleErr: + if value != nil { // it's always not nil but keep it here. + err = value + if statusCode < 400 { + statusCode = DefaultErrStatusCode + } + break // break on first error, error should be in the end but we + // need to know break the dispatcher if any error. + // at the end; we don't want to write anything to the response if error is not nil. + } + default: + // else it's a custom struct or a dispatcher, we'll decide later + // because content type and status code matters + // do that check in order to be able to correctly dispatch: + // (customStruct, error) -> customStruct filled and error is nil + if custom == nil && f != nil { + custom = f + } + } + } + + DispatchCommon(ctx, statusCode, contentType, content, custom, err, found) +} + +// Response completes the `methodfunc.Result` interface. +// It's being used as an alternative return value which +// wraps the status code, the content type, a content as bytes or as string +// and an error, it's smart enough to complete the request and send the correct response to the client. +type Response struct { + Code int + ContentType string + Content []byte + + // if not empty then content type is the text/plain + // and content is the text as []byte. + Text string + // If not nil then it will fire that as "application/json" or the + // "ContentType" if not empty. + Object interface{} + + // If Path is not empty then it will redirect + // the client to this Path, if Code is >= 300 and < 400 + // then it will use that Code to do the redirection, otherwise + // StatusFound(302) or StatusSeeOther(303) for post methods will be used. + // Except when err != nil. + Path string + + // if not empty then fire a 400 bad request error + // unless the Status is > 200, then fire that error code + // with the Err.Error() string as its content. + // + // if Err.Error() is empty then it fires the custom error handler + // if any otherwise the framework sends the default http error text based on the status. + Err error + Try func() int + + // if true then it skips everything else and it throws a 404 not found error. + // Can be named as Failure but NotFound is more precise name in order + // to be visible that it's different than the `Err` + // because it throws a 404 not found instead of a 400 bad request. + // NotFound bool + // let's don't add this yet, it has its dangerous of missuse. +} + +var _ Result = Response{} + +// Dispatch writes the response result to the context's response writer. +func (r Response) Dispatch(ctx context.Context) { + if r.Path != "" && r.Err == nil { + // it's not a redirect valid status + if r.Code < 300 || r.Code >= 400 { + if ctx.Method() == "POST" { + r.Code = 303 // StatusSeeOther + } + r.Code = 302 // StatusFound + } + ctx.Redirect(r.Path, r.Code) + return + } + + if s := r.Text; s != "" { + r.Content = []byte(s) + } + + DispatchCommon(ctx, r.Code, r.ContentType, r.Content, r.Object, r.Err, true) +} + +// View completes the `hero.Result` interface. +// It's being used as an alternative return value which +// wraps the template file name, layout, (any) view data, status code and error. +// It's smart enough to complete the request and send the correct response to the client. +// +// Example at: https://github.com/kataras/iris/blob/master/_examples/hero/overview/web/controllers/hello_controller.go. +type View struct { + Name string + Layout string + Data interface{} // map or a custom struct. + Code int + Err error +} + +var _ Result = View{} + +const dotB = byte('.') + +// DefaultViewExt is the default extension if `view.Name `is missing, +// but note that it doesn't care about +// the app.RegisterView(iris.$VIEW_ENGINE("./$dir", "$ext"))'s $ext. +// so if you don't use the ".html" as extension for your files +// you have to append the extension manually into the `view.Name` +// or change this global variable. +var DefaultViewExt = ".html" + +func ensureExt(s string) string { + if len(s) == 0 { + return "index" + DefaultViewExt + } + + if strings.IndexByte(s, dotB) < 1 { + s += DefaultViewExt + } + + return s +} + +// Dispatch writes the template filename, template layout and (any) data to the client. +// Completes the `Result` interface. +func (r View) Dispatch(ctx context.Context) { // r as Response view. + if r.Err != nil { + if r.Code < 400 { + r.Code = DefaultErrStatusCode + } + ctx.StatusCode(r.Code) + ctx.WriteString(r.Err.Error()) + ctx.StopExecution() + return + } + + if r.Code > 0 { + ctx.StatusCode(r.Code) + } + + if r.Name != "" { + r.Name = ensureExt(r.Name) + + if r.Layout != "" { + r.Layout = ensureExt(r.Layout) + ctx.ViewLayout(r.Layout) + } + + if r.Data != nil { + // In order to respect any c.Ctx.ViewData that may called manually before; + dataKey := ctx.Application().ConfigurationReadOnly().GetViewDataContextKey() + if ctx.Values().Get(dataKey) == nil { + // if no c.Ctx.ViewData set-ed before (the most common scenario) then do a + // simple set, it's faster. + ctx.Values().Set(dataKey, r.Data) + } else { + // else check if r.Data is map or struct, if struct convert it to map, + // do a range loop and modify the data one by one. + // context.Map is actually a map[string]interface{} but we have to make that check: + if m, ok := r.Data.(map[string]interface{}); ok { + setViewData(ctx, m) + } else if m, ok := r.Data.(context.Map); ok { + setViewData(ctx, m) + } else if di.IndirectValue(reflect.ValueOf(r.Data)).Kind() == reflect.Struct { + setViewData(ctx, structs.Map(r)) + } + } + } + + ctx.View(r.Name) + } +} + +func setViewData(ctx context.Context, data map[string]interface{}) { + for k, v := range data { + ctx.ViewData(k, v) + } +} diff --git a/hero/func_result_test.go b/hero/func_result_test.go new file mode 100644 index 0000000000..54a62c6270 --- /dev/null +++ b/hero/func_result_test.go @@ -0,0 +1,152 @@ +package hero_test + +import ( + "errors" + "testing" + + "github.com/kataras/iris" + "github.com/kataras/iris/context" + "github.com/kataras/iris/httptest" + + . "github.com/kataras/iris/hero" +) + +func GetText() string { + return "text" +} + +func GetStatus() int { + return iris.StatusBadGateway +} + +func GetTextWithStatusOk() (string, int) { + return "OK", iris.StatusOK +} + +// tests should have output arguments mixed +func GetStatusWithTextNotOkBy(first string, second string) (int, string) { + return iris.StatusForbidden, "NOT_OK_" + first + second +} + +func GetTextAndContentType() (string, string) { + return "text", "text/html" +} + +type testCustomResult struct { + HTML string +} + +// The only one required function to make that a custom Response dispatcher. +func (r testCustomResult) Dispatch(ctx context.Context) { + ctx.HTML(r.HTML) +} + +func GetCustomResponse() testCustomResult { + return testCustomResult{"text"} +} + +func GetCustomResponseWithStatusOk() (testCustomResult, int) { + return testCustomResult{"OK"}, iris.StatusOK +} + +func GetCustomResponseWithStatusNotOk() (testCustomResult, int) { + return testCustomResult{"internal server error"}, iris.StatusInternalServerError +} + +type testCustomStruct struct { + Name string `json:"name" xml:"name"` + Age int `json:"age" xml:"age"` +} + +func GetCustomStruct() testCustomStruct { + return testCustomStruct{"Iris", 2} +} + +func GetCustomStructWithStatusNotOk() (testCustomStruct, int) { + return testCustomStruct{"Iris", 2}, iris.StatusInternalServerError +} + +func GetCustomStructWithContentType() (testCustomStruct, string) { + return testCustomStruct{"Iris", 2}, "text/xml" +} + +func GetCustomStructWithError(ctx iris.Context) (s testCustomStruct, err error) { + s = testCustomStruct{"Iris", 2} + if ctx.URLParamExists("err") { + err = errors.New("omit return of testCustomStruct and fire error") + } + + // it should send the testCustomStruct as JSON if error is nil + // otherwise it should fire the default error(BadRequest) with the error's text. + return +} + +func TestFuncResult(t *testing.T) { + app := iris.New() + h := New() + // for any 'By', by is not required but we use this suffix here, like controllers + // to make it easier for the future to resolve if any bug. + // add the binding for path parameters. + + app.Get("/text", h.Handler(GetText)) + app.Get("/status", h.Handler(GetStatus)) + app.Get("/text/with/status/ok", h.Handler(GetTextWithStatusOk)) + app.Get("/status/with/text/not/ok/{first}/{second}", h.Handler(GetStatusWithTextNotOkBy)) + app.Get("/text/and/content/type", h.Handler(GetTextAndContentType)) + // + app.Get("/custom/response", h.Handler(GetCustomResponse)) + app.Get("/custom/response/with/status/ok", h.Handler(GetCustomResponseWithStatusOk)) + app.Get("/custom/response/with/status/not/ok", h.Handler(GetCustomResponseWithStatusNotOk)) + // + app.Get("/custom/struct", h.Handler(GetCustomStruct)) + app.Get("/custom/struct/with/status/not/ok", h.Handler(GetCustomStructWithStatusNotOk)) + app.Get("/custom/struct/with/content/type", h.Handler(GetCustomStructWithContentType)) + app.Get("/custom/struct/with/error", h.Handler(GetCustomStructWithError)) + + e := httptest.New(t, app) + + e.GET("/text").Expect().Status(iris.StatusOK). + Body().Equal("text") + + e.GET("/status").Expect().Status(iris.StatusBadGateway) + + e.GET("/text/with/status/ok").Expect().Status(iris.StatusOK). + Body().Equal("OK") + + e.GET("/status/with/text/not/ok/first/second").Expect().Status(iris.StatusForbidden). + Body().Equal("NOT_OK_firstsecond") + // Author's note: <-- if that fails means that the last binder called for both input args, + // see path_param_binder.go + + e.GET("/text/and/content/type").Expect().Status(iris.StatusOK). + ContentType("text/html", "utf-8"). + Body().Equal("text") + + e.GET("/custom/response").Expect().Status(iris.StatusOK). + ContentType("text/html", "utf-8"). + Body().Equal("text") + e.GET("/custom/response/with/status/ok").Expect().Status(iris.StatusOK). + ContentType("text/html", "utf-8"). + Body().Equal("OK") + e.GET("/custom/response/with/status/not/ok").Expect().Status(iris.StatusInternalServerError). + ContentType("text/html", "utf-8"). + Body().Equal("internal server error") + + expectedResultFromCustomStruct := map[string]interface{}{ + "name": "Iris", + "age": 2, + } + e.GET("/custom/struct").Expect().Status(iris.StatusOK). + JSON().Equal(expectedResultFromCustomStruct) + e.GET("/custom/struct/with/status/not/ok").Expect().Status(iris.StatusInternalServerError). + JSON().Equal(expectedResultFromCustomStruct) + e.GET("/custom/struct/with/content/type").Expect().Status(iris.StatusOK). + ContentType("text/xml", "utf-8") + e.GET("/custom/struct/with/error").Expect().Status(iris.StatusOK). + JSON().Equal(expectedResultFromCustomStruct) + e.GET("/custom/struct/with/error").WithQuery("err", true).Expect(). + Status(iris.StatusBadRequest). // the default status code if error is not nil + // the content should be not JSON it should be the status code's text + // it will fire the error's text + Body().Equal("omit return of testCustomStruct and fire error") +} diff --git a/hero/handler.go b/hero/handler.go new file mode 100644 index 0000000000..bb427d772c --- /dev/null +++ b/hero/handler.go @@ -0,0 +1,97 @@ +package hero + +import ( + "fmt" + "reflect" + "runtime" + + "github.com/kataras/iris/hero/di" + + "github.com/kataras/golog" + "github.com/kataras/iris/context" +) + +var contextTyp = reflect.TypeOf((*context.Context)(nil)).Elem() + +// IsContext returns true if the "inTyp" is a type of Context. +func IsContext(inTyp reflect.Type) bool { + return inTyp.Implements(contextTyp) +} + +// checks if "handler" is context.Handler: func(context.Context). +func isContextHandler(handler interface{}) (context.Handler, bool) { + h, is := handler.(context.Handler) + if !is { + fh, is := handler.(func(context.Context)) + if is { + return fh, is + } + } + return h, is +} + +func validateHandler(handler interface{}) error { + if typ := reflect.TypeOf(handler); !di.IsFunc(typ) { + return fmt.Errorf("handler expected to be a kind of func but got typeof(%s)", typ.String()) + } + return nil +} + +// makeHandler accepts a "handler" function which can accept any input arguments that match +// with the "values" types and any output result, that matches the hero types, like string, int (string,int), +// custom structs, Result(View | Response) and anything that you can imagine, +// and returns a low-level `context/iris.Handler` which can be used anywhere in the Iris Application, +// as middleware or as simple route handler or party handler or subdomain handler-router. +func makeHandler(handler interface{}, values ...reflect.Value) (context.Handler, error) { + if err := validateHandler(handler); err != nil { + return nil, err + } + + if h, is := isContextHandler(handler); is { + golog.Warnf("the standard API to register a context handler could be used instead") + return h, nil + } + + fn := reflect.ValueOf(handler) + n := fn.Type().NumIn() + + if n == 0 { + h := func(ctx context.Context) { + DispatchFuncResult(ctx, fn.Call(di.EmptyIn)) + } + + return h, nil + } + + funcInjector := di.Func(fn, values...) + valid := funcInjector.Length == n + + if !valid { + // is invalid when input len and values are not match + // or their types are not match, we will take look at the + // second statement, here we will re-try it + // using binders for path parameters: string, int, int64, bool. + // We don't have access to the path, so neither to the macros here, + // but in mvc. So we have to do it here. + if valid = funcInjector.Retry(new(params).resolve); !valid { + pc := fn.Pointer() + fpc := runtime.FuncForPC(pc) + callerFileName, callerLineNumber := fpc.FileLine(pc) + callerName := fpc.Name() + + err := fmt.Errorf("input arguments length(%d) and valid binders length(%d) are not equal for typeof '%s' which is defined at %s:%d by %s", + n, funcInjector.Length, fn.Type().String(), callerFileName, callerLineNumber, callerName) + return nil, err + } + } + + h := func(ctx context.Context) { + // in := make([]reflect.Value, n, n) + // funcInjector.Inject(&in, reflect.ValueOf(ctx)) + // DispatchFuncResult(ctx, fn.Call(in)) + DispatchFuncResult(ctx, funcInjector.Call(reflect.ValueOf(ctx))) + } + + return h, nil + +} diff --git a/hero/handler_test.go b/hero/handler_test.go new file mode 100644 index 0000000000..9766b37783 --- /dev/null +++ b/hero/handler_test.go @@ -0,0 +1,128 @@ +package hero_test + +// black-box + +import ( + "fmt" + "testing" + + "github.com/kataras/iris" + "github.com/kataras/iris/httptest" + + . "github.com/kataras/iris/hero" +) + +// dynamic func +type testUserStruct struct { + ID int64 + Username string +} + +func testBinderFunc(ctx iris.Context) testUserStruct { + id, _ := ctx.Params().GetInt64("id") + username := ctx.Params().Get("username") + return testUserStruct{ + ID: id, + Username: username, + } +} + +// service +type ( + // these TestService and TestServiceImpl could be in lowercase, unexported + // but the `Say` method should be exported however we have those exported + // because of the controller handler test. + TestService interface { + Say(string) string + } + TestServiceImpl struct { + prefix string + } +) + +func (s *TestServiceImpl) Say(message string) string { + return s.prefix + " " + message +} + +var ( + // binders, as user-defined + testBinderFuncUserStruct = testBinderFunc + testBinderService = &TestServiceImpl{prefix: "say"} + testBinderFuncParam = func(ctx iris.Context) string { + return ctx.Params().Get("param") + } + + // consumers + // a context as first input arg, which is not needed to be binded manually, + // and a user struct which is binded to the input arg by the #1 func(ctx) any binder. + testConsumeUserHandler = func(ctx iris.Context, user testUserStruct) { + ctx.JSON(user) + } + + // just one input arg, the service which is binded by the #2 service binder. + testConsumeServiceHandler = func(service TestService) string { + return service.Say("something") + } + // just one input arg, a standar string which is binded by the #3 func(ctx) any binder. + testConsumeParamHandler = func(myParam string) string { + return "param is: " + myParam + } +) + +func TestHandler(t *testing.T) { + Register(testBinderFuncUserStruct, testBinderService, testBinderFuncParam) + var ( + h1 = Handler(testConsumeUserHandler) + h2 = Handler(testConsumeServiceHandler) + h3 = Handler(testConsumeParamHandler) + ) + + testAppWithHeroHandlers(t, h1, h2, h3) +} + +func testAppWithHeroHandlers(t *testing.T, h1, h2, h3 iris.Handler) { + app := iris.New() + app.Get("/{id:long}/{username:string}", h1) + app.Get("/service", h2) + app.Get("/param/{param:string}", h3) + + expectedUser := testUserStruct{ + ID: 42, + Username: "kataras", + } + + e := httptest.New(t, app) + // 1 + e.GET(fmt.Sprintf("/%d/%s", expectedUser.ID, expectedUser.Username)).Expect().Status(httptest.StatusOK). + JSON().Equal(expectedUser) + // 2 + e.GET("/service").Expect().Status(httptest.StatusOK). + Body().Equal("say something") + // 3 + e.GET("/param/the_param_value").Expect().Status(httptest.StatusOK). + Body().Equal("param is: the_param_value") +} + +// TestBindFunctionAsFunctionInputArgument tests to bind +// a whole dynamic function based on the current context +// as an input argument in the hero handler's function. +func TestBindFunctionAsFunctionInputArgument(t *testing.T) { + app := iris.New() + postsBinder := func(ctx iris.Context) func(string) string { + return ctx.PostValue // or FormValue, the same here. + } + + h := New().Register(postsBinder).Handler(func(get func(string) string) string { + // send the `ctx.PostValue/FormValue("username")` value + // to the client. + return get("username") + }) + + app.Post("/", h) + + e := httptest.New(t, app) + + expectedUsername := "kataras" + e.POST("/").WithFormField("username", expectedUsername). + Expect().Status(iris.StatusOK).Body().Equal(expectedUsername) +} diff --git a/hero/hero.go b/hero/hero.go new file mode 100644 index 0000000000..9862816d12 --- /dev/null +++ b/hero/hero.go @@ -0,0 +1,106 @@ +package hero + +import ( + "github.com/kataras/iris/hero/di" + + "github.com/kataras/golog" + "github.com/kataras/iris/context" +) + +// def is the default herp value which can be used for dependencies share. +var def = New() + +// Hero contains the Dependencies which will be binded +// to the controller(s) or handler(s) that can be created +// using the Hero's `Handler` and `Controller` methods. +// +// This is not exported for being used by everyone, use it only when you want +// to share heroes between multi mvc.go#Application +// or make custom hero handlers that can be used on the standard +// iris' APIBuilder. The last one reason is the most useful here, +// although end-devs can use the `MakeHandler` as well. +// +// For a more high-level structure please take a look at the "mvc.go#Application". +type Hero struct { + values di.Values +} + +// New returns a new Hero, a container for dependencies and a factory +// for handlers and controllers, this is used internally by the `mvc#Application` structure. +// Please take a look at the structure's documentation for more information. +func New() *Hero { + return &Hero{ + values: di.NewValues(), + } +} + +// Dependencies returns the dependencies collection if the default hero, +// those can be modified at any way but before the consumer `Handler`. +func Dependencies() *di.Values { + return def.Dependencies() +} + +// Dependencies returns the dependencies collection of this hero, +// those can be modified at any way but before the consumer `Handler`. +func (h *Hero) Dependencies() *di.Values { + return &h.values +} + +// Register adds one or more values as dependencies. +// The value can be a single struct value-instance or a function +// which has one input and one output, the input should be +// an `iris.Context` and the output can be any type, that output type +// will be binded to the handler's input argument, if matching. +// +// Example: `.Register(loggerService{prefix: "dev"}, func(ctx iris.Context) User {...})`. +func Register(values ...interface{}) *Hero { + return def.Register(values...) +} + +// Register adds one or more values as dependencies. +// The value can be a single struct value-instance or a function +// which has one input and one output, the input should be +// an `iris.Context` and the output can be any type, that output type +// will be binded to the handler's input argument, if matching. +// +// Example: `.Register(loggerService{prefix: "dev"}, func(ctx iris.Context) User {...})`. +func (h *Hero) Register(values ...interface{}) *Hero { + h.values.Add(values...) + return h +} + +// Clone creates and returns a new hero with the default Dependencies. +// It copies the default's dependencies and returns a new hero. +func Clone() *Hero { + return def.Clone() +} + +// Clone creates and returns a new hero with the parent's(current) Dependencies. +// It copies the current "h" dependencies and returns a new hero. +func (h *Hero) Clone() *Hero { + child := New() + child.values = h.values.Clone() + return child +} + +// Handler accepts a "handler" function which can accept any input arguments that match +// with the Hero's `Dependencies` and any output result; like string, int (string,int), +// custom structs, Result(View | Response) and anything you can imagine. +// It returns a standard `iris/context.Handler` which can be used anywhere in an Iris Application, +// as middleware or as simple route handler or subdomain's handler. +func Handler(handler interface{}) context.Handler { + return def.Handler(handler) +} + +// Handler accepts a handler "fn" function which can accept any input arguments that match +// with the Hero's `Dependencies` and any output result; like string, int (string,int), +// custom structs, Result(View | Response) and anything you can imagine. +// It returns a standard `iris/context.Handler` which can be used anywhere in an Iris Application, +// as middleware or as simple route handler or subdomain's handler. +func (h *Hero) Handler(fn interface{}) context.Handler { + handler, err := makeHandler(fn, h.values.Clone()...) + if err != nil { + golog.Errorf("hero handler: %v", err) + } + return handler +} diff --git a/hero/param.go b/hero/param.go new file mode 100644 index 0000000000..9a9f028f33 --- /dev/null +++ b/hero/param.go @@ -0,0 +1,68 @@ +package hero + +import ( + "reflect" + + "github.com/kataras/iris/context" +) + +// weak because we don't have access to the path, neither +// the macros, so this is just a guess based on the index of the path parameter, +// the function's path parameters should be like a chain, in the same order as +// the caller registers a route's path. +// A context or any value(s) can be in front or back or even between them. +type params struct { + // the next function input index of where the next path parameter + // should be inside the CONTEXT. + next int +} + +func (p *params) resolve(index int, typ reflect.Type) (reflect.Value, bool) { + currentParamIndex := p.next + v, ok := resolveParam(currentParamIndex, typ) + + p.next = p.next + 1 + return v, ok +} + +func resolveParam(currentParamIndex int, typ reflect.Type) (reflect.Value, bool) { + var fn interface{} + + switch typ.Kind() { + case reflect.Int: + fn = func(ctx context.Context) int { + // the second "ok/found" check is not necessary, + // because even if the entry didn't found on that "index" + // it will return an empty entry which will return the + // default value passed from the xDefault(def) because its `ValueRaw` is nil. + entry, _ := ctx.Params().GetEntryAt(currentParamIndex) + v, _ := entry.IntDefault(0) + return v + } + case reflect.Int64: + fn = func(ctx context.Context) int64 { + entry, _ := ctx.Params().GetEntryAt(currentParamIndex) + v, _ := entry.Int64Default(0) + + return v + } + case reflect.Bool: + fn = func(ctx context.Context) bool { + entry, _ := ctx.Params().GetEntryAt(currentParamIndex) + v, _ := entry.BoolDefault(false) + return v + } + case reflect.String: + fn = func(ctx context.Context) string { + entry, _ := ctx.Params().GetEntryAt(currentParamIndex) + // print(entry.Key + " with index of: ") + // print(currentParamIndex) + // println(" and value: " + entry.String()) + return entry.String() + } + default: + return reflect.Value{}, false + } + + return reflect.ValueOf(fn), true +} diff --git a/hero/param_test.go b/hero/param_test.go new file mode 100644 index 0000000000..112edd0f2e --- /dev/null +++ b/hero/param_test.go @@ -0,0 +1,48 @@ +package hero + +import ( + "testing" + + "github.com/kataras/iris/context" +) + +func TestPathParams(t *testing.T) { + got := "" + h := New() + handler := h.Handler(func(firstname string, lastname string) { + got = firstname + lastname + }) + + h.Register(func(ctx context.Context) func() string { return func() string { return "" } }) + handlerWithOther := h.Handler(func(f func() string, firstname string, lastname string) { + got = f() + firstname + lastname + }) + + handlerWithOtherBetweenThem := h.Handler(func(firstname string, f func() string, lastname string) { + got = f() + firstname + lastname + }) + + ctx := context.NewContext(nil) + ctx.Params().Set("firstname", "Gerasimos") + ctx.Params().Set("lastname", "Maropoulos") + handler(ctx) + expected := "GerasimosMaropoulos" + if got != expected { + t.Fatalf("expected the params 'firstname' + 'lastname' to be '%s' but got '%s'", expected, got) + } + + got = "" + handlerWithOther(ctx) + expected = "GerasimosMaropoulos" + if got != expected { + t.Fatalf("expected the params 'firstname' + 'lastname' to be '%s' but got '%s'", expected, got) + } + + got = "" + handlerWithOtherBetweenThem(ctx) + expected = "GerasimosMaropoulos" + if got != expected { + t.Fatalf("expected the params 'firstname' + 'lastname' to be '%s' but got '%s'", expected, got) + } + +} diff --git a/hero/session.go b/hero/session.go new file mode 100644 index 0000000000..8d00865367 --- /dev/null +++ b/hero/session.go @@ -0,0 +1,10 @@ +package hero + +// It's so easy, no need to be lived anywhere as built'n.. users should understand +// how easy it's by using it. + +// // Session is a binder that will fill a *sessions.Session function input argument +// // or a Controller struct's field. +// func Session(sess *sessions.Sessions) func(context.Context) *sessions.Session { +// return sess.Start +// } diff --git a/learn.jpg.REMOVED.git-id b/learn.jpg.REMOVED.git-id deleted file mode 100644 index 48fe3cda3b..0000000000 --- a/learn.jpg.REMOVED.git-id +++ /dev/null @@ -1 +0,0 @@ -efa10f1e68d7042b7340b8db37c38e18e351bc3f \ No newline at end of file diff --git a/logo_white_35_24.png b/logo_white_35_24.png deleted file mode 100644 index 4f5500107e..0000000000 Binary files a/logo_white_35_24.png and /dev/null differ diff --git a/mvc/AUTHORS b/mvc/AUTHORS new file mode 100644 index 0000000000..848245bb12 --- /dev/null +++ b/mvc/AUTHORS @@ -0,0 +1 @@ +Gerasimos Maropoulos diff --git a/mvc/LICENSE b/mvc/LICENSE new file mode 100644 index 0000000000..469fb44d08 --- /dev/null +++ b/mvc/LICENSE @@ -0,0 +1,27 @@ +Copyright (c) 2018 Gerasimos Maropoulos. All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + + * Redistributions of source code must retain the above copyright +notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above +copyright notice, this list of conditions and the following disclaimer +in the documentation and/or other materials provided with the +distribution. + * Neither the name of Iris nor the names of its +contributor, Gerasimos Maropoulos, may be used to endorse or promote products +derived from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. \ No newline at end of file diff --git a/mvc/activator/activate_listener.go b/mvc/activator/activate_listener.go deleted file mode 100644 index 708d08fcfb..0000000000 --- a/mvc/activator/activate_listener.go +++ /dev/null @@ -1,78 +0,0 @@ -package activator - -import ( - "reflect" -) - -// CallOnActivate simply calls the "controller"'s `OnActivate(*ActivatePayload)` function, -// if any. -// -// Look `activator.go#Register` and `ActivateListener` for more. -func CallOnActivate(controller interface{}, - bindValues *[]interface{}, registerFunc RegisterFunc) { - - if ac, ok := controller.(ActivateListener); ok { - p := &ActivatePayload{ - BindValues: bindValues, - Handle: registerFunc, - } - ac.OnActivate(p) - } -} - -// ActivateListener is an interface which should be declared -// on a Controller which needs to register or change the bind values -// that the caller-"user" has been passed to; via the `app.Controller`. -// If that interface is completed by a controller -// then the `OnActivate` function will be called ONCE, NOT in every request -// but ONCE at the application's lifecycle. -type ActivateListener interface { - // OnActivate accepts a pointer to the `ActivatePayload`. - // - // The `Controller` can make use of the `OnActivate` function - // to register custom routes - // or modify the provided values that will be binded to the - // controller later on. - // - // Look `ActivatePayload` for more. - OnActivate(*ActivatePayload) -} - -// ActivatePayload contains the necessary information and the ability -// to alt a controller's registration options, i.e the binder. -// -// With `ActivatePayload` the `Controller` can register custom routes -// or modify the provided values that will be binded to the -// controller later on. -type ActivatePayload struct { - BindValues *[]interface{} - Handle RegisterFunc -} - -// EnsureBindValue will make sure that this "bindValue" -// will be registered to the controller's binder -// if its type is not already passed by the caller.. -// -// For example, on `SessionController` it looks if *sessions.Sessions -// has been binded from the caller and if not then the "bindValue" -// will be binded and used as a default sessions manager instead. -// -// At general, if the caller has already provided a value with the same Type -// then the "bindValue" will be ignored and not be added to the controller's bind values. -// -// Returns true if the caller has NOT already provided a value with the same Type -// and "bindValue" is NOT ignored therefore is appended to the controller's bind values. -func (i *ActivatePayload) EnsureBindValue(bindValue interface{}) bool { - valueTyp := reflect.TypeOf(bindValue) - localBindValues := *i.BindValues - - for _, bindedValue := range localBindValues { - // type already exists, remember: binding here is per-type. - if reflect.TypeOf(bindedValue) == valueTyp { - return false - } - } - - *i.BindValues = append(localBindValues, bindValue) - return true -} diff --git a/mvc/activator/activator.go b/mvc/activator/activator.go deleted file mode 100644 index 67ac4a39b3..0000000000 --- a/mvc/activator/activator.go +++ /dev/null @@ -1,228 +0,0 @@ -package activator - -import ( - "reflect" - "strings" - - "github.com/kataras/iris/mvc/activator/methodfunc" - "github.com/kataras/iris/mvc/activator/model" - "github.com/kataras/iris/mvc/activator/persistence" - - "github.com/kataras/golog" - - "github.com/kataras/iris/context" - "github.com/kataras/iris/core/errors" -) - -type ( - // TController is the type of the controller, - // it contains all the necessary information to load - // and serve the controller to the outside world, - // think it as a "supervisor" of your Controller which - // cares about you. - TController struct { - // The name of the front controller struct. - Name string - // FullName it's the last package path segment + "." + the Name. - // i.e: if login-example/user/controller.go, the FullName is "user.Controller". - FullName string - // the type of the user/dev's "c" controller (interface{}). - Type reflect.Type - // it's the first passed value of the controller instance, - // we need this to collect and save the persistence fields' values. - Value reflect.Value - - binder *binder // executed even before the BeginRequest if not nil. - modelController *model.Controller - persistenceController *persistence.Controller - } -) - -// the parent package should complete this "interface" -// it's not exported, so their functions -// but reflect doesn't care about it, so we are ok -// to compare the type of the base controller field -// with this "ctrl", see `buildTypeInfo` and `buildMethodHandler`. - -var ( - // ErrMissingControllerInstance is a static error which fired from `Controller` when - // the passed "c" instnace is not a valid type of `Controller` or `C`. - ErrMissingControllerInstance = errors.New("controller should have a field of mvc.Controller or mvc.C type") - // ErrInvalidControllerType fired when the "Controller" field is not - // the correct type. - ErrInvalidControllerType = errors.New("controller instance is not a valid implementation") -) - -// BaseController is the controller interface, -// which the main request `Controller` will implement automatically. -// End-User doesn't need to have any knowledge of this if she/he doesn't want to implement -// a new Controller type. -// Controller looks the whole flow as one handler, so `ctx.Next` -// inside `BeginRequest` is not be respected. -// Alternative way to check if a middleware was procceed successfully -// and called its `ctx.Next` is the `ctx.Proceed(handler) bool`. -// You have to navigate to the `context/context#Proceed` function's documentation. -type BaseController interface { - SetName(name string) - BeginRequest(ctx context.Context) - EndRequest(ctx context.Context) -} - -// ActivateController returns a new controller type info description. -func ActivateController(base BaseController, bindValues []interface{}) (TController, error) { - // get and save the type. - typ := reflect.TypeOf(base) - if typ.Kind() != reflect.Ptr { - typ = reflect.PtrTo(typ) - } - - // first instance value, needed to validate - // the actual type of the controller field - // and to collect and save the instance's persistence fields' - // values later on. - val := reflect.Indirect(reflect.ValueOf(base)) - ctrlName := val.Type().Name() - pkgPath := val.Type().PkgPath() - fullName := pkgPath[strings.LastIndexByte(pkgPath, '/')+1:] + "." + ctrlName - - // set the binder, can be nil this check at made at runtime. - binder := newBinder(typ.Elem(), bindValues) - if binder != nil { - for _, bf := range binder.fields { - golog.Debugf("MVC %s: binder loaded for '%s' with value:\n%#v", - fullName, bf.GetFullName(), bf.GetValue()) - } - } - - t := TController{ - Name: ctrlName, - FullName: fullName, - Type: typ, - Value: val, - binder: binder, - modelController: model.Load(typ), - persistenceController: persistence.Load(typ, val), - } - - return t, nil -} - -// HandlerOf builds the handler for a type based on the specific method func. -func (t TController) HandlerOf(methodFunc methodfunc.MethodFunc) context.Handler { - var ( - // shared, per-controller - elem = t.Type.Elem() - ctrlName = t.Name - - hasPersistenceData = t.persistenceController != nil - hasModels = t.modelController != nil - // per-handler - handleRequest = methodFunc.MethodCall - ) - - return func(ctx context.Context) { - // create a new controller instance of that type(>ptr). - c := reflect.New(elem) - if t.binder != nil { - t.binder.handle(c) - } - - b := c.Interface().(BaseController) - b.SetName(ctrlName) - - // if has persistence data then set them - // before the end-developer's handler in order to be available there. - if hasPersistenceData { - t.persistenceController.Handle(c) - } - - // if previous (binded) handlers stopped the execution - // we should know that. - if ctx.IsStopped() { - return - } - - // init the request. - b.BeginRequest(ctx) - if ctx.IsStopped() { // if begin request stopped the execution - return - } - - // the most important, execute the specific function - // from the controller that is responsible to handle - // this request, by method and path. - handleRequest(ctx, c.Method(methodFunc.Index)) - // if had models, set them after the end-developer's handler. - if hasModels { - t.modelController.Handle(ctx, c) - } - - // end the request, don't check for stopped because this does the actual writing - // if no response written already. - b.EndRequest(ctx) - } -} - -// RegisterFunc used by the caller to register the result routes. -type RegisterFunc func(relPath string, httpMethod string, handler ...context.Handler) - -// RegisterMethodHandlers receives a `TController`, description of the -// user's controller, and calls the "registerFunc" for each of its -// method handlers. -// -// Not useful for the end-developer, but may needed for debugging -// at the future. -func RegisterMethodHandlers(t TController, registerFunc RegisterFunc) { - var middleware context.Handlers - - if t.binder != nil { - if m := t.binder.middleware; len(m) > 0 { - middleware = m - } - } - // the actual method functions - // i.e for "GET" it's the `Get()`. - methods, err := methodfunc.Resolve(t.Type) - if err != nil { - golog.Errorf("MVC %s: %s", t.FullName, err.Error()) - // don't stop here. - } - // range over the type info's method funcs, - // build a new handler for each of these - // methods and register them to their - // http methods using the registerFunc, which is - // responsible to convert these into routes - // and add them to router via the APIBuilder. - for _, m := range methods { - h := t.HandlerOf(m) - if h == nil { - golog.Warnf("MVC %s: nil method handler found for %s", t.FullName, m.Name) - continue - } - registeredHandlers := append(middleware, h) - registerFunc(m.RelPath, m.HTTPMethod, registeredHandlers...) - - golog.Debugf("MVC %s: %s %s maps to function[%d] '%s'", t.FullName, - m.HTTPMethod, - m.RelPath, - m.Index, - m.Name) - } -} - -// Register receives a "controller", -// a pointer of an instance which embeds the `Controller`, -// the value of "baseControllerFieldName" should be `Controller`. -func Register(controller BaseController, bindValues []interface{}, - registerFunc RegisterFunc) error { - - CallOnActivate(controller, &bindValues, registerFunc) - - t, err := ActivateController(controller, bindValues) - if err != nil { - return err - } - - RegisterMethodHandlers(t, registerFunc) - return nil -} diff --git a/mvc/activator/binder.go b/mvc/activator/binder.go deleted file mode 100644 index b0aee3b171..0000000000 --- a/mvc/activator/binder.go +++ /dev/null @@ -1,107 +0,0 @@ -package activator - -import ( - "reflect" - - "github.com/kataras/iris/mvc/activator/field" - - "github.com/kataras/iris/context" -) - -type binder struct { - values []interface{} - fields []field.Field - - // saves any middleware that may need to be passed to the router, - // statically, to gain performance. - middleware context.Handlers -} - -// binder accepts a value of something -// and tries to find its equalivent type -// inside the controller and sets that to it, -// after that each new instance of the controller will have -// this value on the specific field, like persistence data control does. -// -// returns a nil binder if values are not valid bindable data to the controller type. -func newBinder(elemType reflect.Type, values []interface{}) *binder { - if len(values) == 0 { - return nil - } - - b := &binder{values: values} - b.fields = b.lookup(elemType) - - // if nothing valid found return nil, so the caller - // can omit the binder. - if len(b.fields) == 0 && len(b.middleware) == 0 { - return nil - } - - return b -} - -func (b *binder) storeValueIfMiddleware(value reflect.Value) bool { - if value.CanInterface() { - if m, ok := value.Interface().(context.Handler); ok { - b.middleware = append(b.middleware, m) - return true - } - if m, ok := value.Interface().(func(context.Context)); ok { - b.middleware = append(b.middleware, m) - return true - } - } - return false -} - -func (b *binder) lookup(elem reflect.Type) (fields []field.Field) { - for _, v := range b.values { - value := reflect.ValueOf(v) - // handlers will be recognised as middleware, not struct fields. - // End-Developer has the option to call any handler inside - // the controller's `BeginRequest` and `EndRequest`, the - // state is respected from the method handler already. - if b.storeValueIfMiddleware(value) { - // stored as middleware, continue to the next field, we don't have - // to bind anything here. - continue - } - - matcher := func(elemField reflect.StructField) bool { - // If the controller's field is interface then check - // if the given binded value implements that interface. - // i.e MovieController { Service services.MovieService /* interface */ } - // app.Controller("/", new(MovieController), - // services.NewMovieMemoryService(...)) - // - // `services.NewMovieMemoryService` returns a `*MovieMemoryService` - // that implements the `MovieService` interface. - if elemField.Type.Kind() == reflect.Interface { - return value.Type().Implements(elemField.Type) - } - return elemField.Type == value.Type() - } - - handler := func(f *field.Field) { - f.Value = value - } - - fields = append(fields, field.LookupFields(elem, matcher, handler)...) - } - return -} - -func (b *binder) handle(c reflect.Value) { - // we could make check for middlewares here but - // these could easly be used outside of the controller - // so we don't have to initialize a controller to call them - // so they don't belong actually here, we will register them to the - // router itself, before the controller's handler to gain performance, - // look `activator.go#RegisterMethodHandlers` for more. - - elem := c.Elem() // controller should always be a pointer at this state - for _, f := range b.fields { - f.SendTo(elem) - } -} diff --git a/mvc/activator/field/field.go b/mvc/activator/field/field.go deleted file mode 100644 index 1a3d041333..0000000000 --- a/mvc/activator/field/field.go +++ /dev/null @@ -1,220 +0,0 @@ -package field - -import ( - "reflect" -) - -// Field is a controller's field -// contains all the necessary, internal, information -// to work with. -type Field struct { - Name string // the field's original name - // but if a tag with `name: "other"` - // exist then this fill is filled, otherwise it's the same as the Name. - TagName string - Index int - Type reflect.Type - Value reflect.Value - - embedded *Field -} - -// GetIndex returns all the "dimensions" -// of the controller struct field's position that this field is referring to, -// recursively. -// Usage: elem.FieldByIndex(field.getIndex()) -// for example the {0,1} means that the field is on the second field of the first's -// field of this struct. -func (ff Field) GetIndex() []int { - deepIndex := []int{ff.Index} - - if emb := ff.embedded; emb != nil { - deepIndex = append(deepIndex, emb.GetIndex()...) - } - - return deepIndex -} - -// GetType returns the type of the referring field, recursively. -func (ff Field) GetType() reflect.Type { - typ := ff.Type - if emb := ff.embedded; emb != nil { - return emb.GetType() - } - - return typ -} - -// GetFullName returns the full name of that field -// i.e: UserController.SessionController.Manager, -// it's useful for debugging only. -func (ff Field) GetFullName() string { - name := ff.Name - - if emb := ff.embedded; emb != nil { - return name + "." + emb.GetFullName() - } - - return name -} - -// GetTagName returns the tag name of the referring field -// recursively. -func (ff Field) GetTagName() string { - name := ff.TagName - - if emb := ff.embedded; emb != nil { - return emb.GetTagName() - } - - return name -} - -// checkVal checks if that value -// is valid to be set-ed to the new controller's instance. -// Used on binder. -func checkVal(val reflect.Value) bool { - return val.IsValid() && (val.Kind() == reflect.Ptr && !val.IsNil()) && val.CanInterface() -} - -// GetValue returns a valid value of the referring field, recursively. -func (ff Field) GetValue() interface{} { - if ff.embedded != nil { - return ff.embedded.GetValue() - } - - if checkVal(ff.Value) { - return ff.Value.Interface() - } - - return "undefinied value" -} - -// SendTo should be used when this field or its embedded -// has a Value on it. -// It sets the field's value to the "elem" instance, it's the new controller. -func (ff Field) SendTo(elem reflect.Value) { - // note: - // we don't use the getters here - // because we do recursively search by our own here - // to be easier to debug if ever needed. - if embedded := ff.embedded; embedded != nil { - if ff.Index >= 0 { - embedded.SendTo(elem.Field(ff.Index)) - } - return - } - elemField := elem.Field(ff.Index) - if elemField.Kind() == reflect.Ptr && !elemField.IsNil() { - return - } - - elemField.Set(ff.Value) -} - -// lookupTagName checks if the "elemField" struct's field -// contains a tag `name`, if it contains then it returns its value -// otherwise returns the field's original Name. -func lookupTagName(elemField reflect.StructField) string { - vname := elemField.Name - - if taggedName, ok := elemField.Tag.Lookup("name"); ok { - vname = taggedName - } - return vname -} - -// LookupFields iterates all "elem"'s fields and its fields -// if structs, recursively. -// Compares them to the "matcher", if they passed -// then it executes the "handler" if any, -// the handler can change the field as it wants to. -// -// It finally returns that collection of the valid fields, can be empty. -func LookupFields(elem reflect.Type, matcher func(reflect.StructField) bool, handler func(*Field)) (fields []Field) { - for i, n := 0, elem.NumField(); i < n; i++ { - elemField := elem.Field(i) - if matcher(elemField) { - field := Field{ - Index: i, - Name: elemField.Name, - TagName: lookupTagName(elemField), - Type: elemField.Type, - } - - if handler != nil { - handler(&field) - } - - // we area inside the correct type - fields = append(fields, field) - continue - } - - f := lookupStructField(elemField.Type, matcher, handler) - if f != nil { - fields = append(fields, Field{ - Index: i, - Name: elemField.Name, - Type: elemField.Type, - embedded: f, - }) - } - - } - return -} - -// lookupStructField is here to search for embedded field only, -// is working with the "lookupFields". -// We could just one one function -// for both structured (embedded) fields and normal fields -// but we keep that as it's, a new function like this -// is easier for debugging, if ever needed. -func lookupStructField(elem reflect.Type, matcher func(reflect.StructField) bool, handler func(*Field)) *Field { - // fmt.Printf("lookup struct for elem: %s\n", elem.Name()) - // ignore if that field is not a struct - if elem.Kind() != reflect.Struct { - return nil - } - - // search by fields. - for i, n := 0, elem.NumField(); i < n; i++ { - elemField := elem.Field(i) - - if matcher(elemField) { - // we area inside the correct type. - f := &Field{ - Index: i, - Name: elemField.Name, - TagName: lookupTagName(elemField), - Type: elemField.Type, - } - - if handler != nil { - handler(f) - } - - return f - } - - // if field is struct and the value is struct - // then try inside its fields for a compatible - // field type. - if elemField.Type.Kind() == reflect.Struct { // 3-level - elemFieldEmb := elem.Field(i) - f := lookupStructField(elemFieldEmb.Type, matcher, handler) - if f != nil { - fp := &Field{ - Index: i, - Name: elemFieldEmb.Name, - TagName: lookupTagName(elemFieldEmb), - Type: elemFieldEmb.Type, - embedded: f, - } - return fp - } - } - } - return nil -} diff --git a/mvc/activator/methodfunc/func_caller.go b/mvc/activator/methodfunc/func_caller.go deleted file mode 100644 index 108b40c0c4..0000000000 --- a/mvc/activator/methodfunc/func_caller.go +++ /dev/null @@ -1,19 +0,0 @@ -package methodfunc - -import ( - "reflect" - - "github.com/kataras/iris/context" -) - -// buildMethodCall builds the method caller. -// We have repeated code here but it's the only way -// to support more than one input arguments without performance cost compared to previous implementation. -// so it's hard-coded written to check the length of input args and their types. -func buildMethodCall(a *ast) func(ctx context.Context, f reflect.Value) { - // if func input arguments are more than one then - // use the Call method (slower). - return func(ctx context.Context, f reflect.Value) { - DispatchFuncResult(ctx, f.Call(a.paramValues(ctx))) - } -} diff --git a/mvc/activator/methodfunc/func_info.go b/mvc/activator/methodfunc/func_info.go deleted file mode 100644 index 836dfa6cd6..0000000000 --- a/mvc/activator/methodfunc/func_info.go +++ /dev/null @@ -1,91 +0,0 @@ -package methodfunc - -import ( - "reflect" - "strings" - "unicode" -) - -var availableMethods = [...]string{ - "ANY", // will be registered using the `core/router#APIBuilder#Any` - "ALL", // same as ANY - "NONE", // offline route - // valid http methods - "GET", - "POST", - "PUT", - "DELETE", - "CONNECT", - "HEAD", - "PATCH", - "OPTIONS", - "TRACE", -} - -// FuncInfo is part of the `TController`, -// it contains the index for a specific http method, -// taken from user's controller struct. -type FuncInfo struct { - // Name is the map function name. - Name string - // Trailing is not empty when the Name contains - // characters after the titled method, i.e - // if Name = Get -> empty - // if Name = GetLogin -> Login - // if Name = GetUserPost -> UserPost - Trailing string - - // The Type of the method, includes the receivers. - Type reflect.Type - - // Index is the index of this function inside the controller type. - Index int - // HTTPMethod is the original http method that this - // function should be registered to and serve. - // i.e "GET","POST","PUT"... - HTTPMethod string -} - -// or resolve methods -func fetchInfos(typ reflect.Type) (methods []FuncInfo) { - // search the entire controller - // for any compatible method function - // and add that. - for i, n := 0, typ.NumMethod(); i < n; i++ { - m := typ.Method(i) - name := m.Name - - for _, method := range availableMethods { - possibleMethodFuncName := methodTitle(method) - - if strings.Index(name, possibleMethodFuncName) == 0 { - trailing := "" - // if has chars after the method itself - if lname, lmethod := len(name), len(possibleMethodFuncName); lname > lmethod { - ch := rune(name[lmethod]) - // if the next char is upper, otherise just skip the whole func info. - if unicode.IsUpper(ch) { - trailing = name[lmethod:] - } else { - continue - } - } - - methodInfo := FuncInfo{ - Name: name, - Trailing: trailing, - Type: m.Type, - HTTPMethod: method, - Index: m.Index, - } - methods = append(methods, methodInfo) - } - } - } - return -} - -func methodTitle(httpMethod string) string { - httpMethodFuncName := strings.Title(strings.ToLower(httpMethod)) - return httpMethodFuncName -} diff --git a/mvc/activator/methodfunc/func_lexer.go b/mvc/activator/methodfunc/func_lexer.go deleted file mode 100644 index e06352a767..0000000000 --- a/mvc/activator/methodfunc/func_lexer.go +++ /dev/null @@ -1,89 +0,0 @@ -package methodfunc - -import ( - "unicode" -) - -const ( - tokenBy = "By" - tokenWildcard = "Wildcard" // should be followed by "By", -) - -// word lexer, not characters. -type lexer struct { - words []string - cur int -} - -func newLexer(s string) *lexer { - l := new(lexer) - l.reset(s) - return l -} - -func (l *lexer) reset(trailing string) { - l.cur = -1 - var words []string - if trailing != "" { - end := len(trailing) - start := -1 - - for i, n := 0, end; i < n; i++ { - c := rune(trailing[i]) - if unicode.IsUpper(c) { - // it doesn't count the last uppercase - if start != -1 { - end = i - words = append(words, trailing[start:end]) - } - start = i - continue - } - end = i + 1 - } - - if end > 0 && len(trailing) >= end { - words = append(words, trailing[start:end]) - } - } - - l.words = words -} - -func (l *lexer) next() (w string) { - cur := l.cur + 1 - - if w = l.peek(cur); w != "" { - l.cur++ - } - - return -} - -func (l *lexer) skip() { - if cur := l.cur + 1; cur < len(l.words) { - l.cur = cur - } else { - l.cur = len(l.words) - 1 - } -} - -func (l *lexer) peek(idx int) string { - if idx < len(l.words) { - return l.words[idx] - } - return "" -} - -func (l *lexer) peekNext() (w string) { - return l.peek(l.cur + 1) -} - -func (l *lexer) peekPrev() (w string) { - if l.cur > 0 { - cur := l.cur - 1 - w = l.words[cur] - } - - return w -} diff --git a/mvc/activator/methodfunc/func_parser.go b/mvc/activator/methodfunc/func_parser.go deleted file mode 100644 index 511b20909f..0000000000 --- a/mvc/activator/methodfunc/func_parser.go +++ /dev/null @@ -1,212 +0,0 @@ -package methodfunc - -import ( - "errors" - "fmt" - "reflect" - "strings" - - "github.com/kataras/iris/context" -) - -var posWords = map[int]string{ - 0: "", - 1: "first", - 2: "second", - 3: "third", - 4: "forth", - 5: "five", - 6: "sixth", - 7: "seventh", - 8: "eighth", - 9: "ninth", -} - -func genParamKey(argIdx int) string { - return "param" + posWords[argIdx] // paramfirst, paramsecond... -} - -const ( - paramTypeInt = "int" - paramTypeLong = "long" - paramTypeBoolean = "boolean" - paramTypeString = "string" - paramTypePath = "path" -) - -var macroTypes = map[string]string{ - "int": paramTypeInt, - "int64": paramTypeLong, - "bool": paramTypeBoolean, - "string": paramTypeString, - // there is "path" param type but it's being captured "on-air" - // "file" param type is not supported by the current implementation, yet - // but if someone ask for it I'll implement it, it's easy. -} - -type funcParser struct { - info FuncInfo - lexer *lexer -} - -func newFuncParser(info FuncInfo) *funcParser { - return &funcParser{ - info: info, - lexer: newLexer(info.Trailing), - } -} - -func (p *funcParser) parse() (*ast, error) { - a := new(ast) - funcArgPos := 0 - - for { - w := p.lexer.next() - if w == "" { - break - } - - if w == tokenBy { - funcArgPos++ // starting with 1 because in typ.NumIn() the first is the struct receiver. - - // No need for these: - // ByBy will act like /{param:type}/{param:type} as users expected - // if func input arguments are there, else act By like normal path /by. - // - // if p.lexer.peekPrev() == tokenBy || typ.NumIn() == 1 { // ByBy, then act this second By like a path - // a.relPath += "/" + strings.ToLower(w) - // continue - // } - - if err := p.parsePathParam(a, w, funcArgPos); err != nil { - return nil, err - } - - continue - } - - a.relPath += "/" + strings.ToLower(w) - } - - // This fixes a problem when developer misses to append the keyword `By` - // to the method function when input arguments are declared (for path parameters binding). - // We could just use it with `By` keyword but this is not a good practise - // because what happens if we will become naive and declare something like - // Get(id int) and GetBy(username string) or GetBy(id int) ? it's not working because of duplication of the path. - // Docs are clear about that but we are humans, they may do a mistake by accident but - // framework will not allow that. - // So the best thing we can do to help prevent those errors is by printing that message - // below to the developer. - // Note: it should be at the end of the words loop because a.dynamic may be true later on. - if numIn := p.info.Type.NumIn(); numIn > 1 && !a.dynamic { - return nil, fmt.Errorf("found %d input arguments but keyword 'By' is missing from '%s'", - // -1 because end-developer wants to know the actual input arguments, without the struct holder. - numIn-1, p.info.Name) - } - return a, nil -} - -func (p *funcParser) parsePathParam(a *ast, w string, funcArgPos int) error { - typ := p.info.Type - - if typ.NumIn() <= funcArgPos { - // old: - // return nil, errors.New("keyword 'By' found but length of input receivers are not match for " + - // p.info.Name) - - // By found but input arguments are not there, so act like /by path without restricts. - a.relPath += "/" + strings.ToLower(w) - return nil - } - - var ( - paramKey = genParamKey(funcArgPos) // paramfirst, paramsecond... - paramType = paramTypeString // default string - ) - - // string, int... - goType := typ.In(funcArgPos).Name() - nextWord := p.lexer.peekNext() - - if nextWord == tokenWildcard { - p.lexer.skip() // skip the Wildcard word. - paramType = paramTypePath - } else if pType, ok := macroTypes[goType]; ok { - // it's not wildcard, so check base on our available macro types. - paramType = pType - } else { - return errors.New("invalid syntax for " + p.info.Name) - } - - a.paramKeys = append(a.paramKeys, paramKey) - a.paramTypes = append(a.paramTypes, paramType) - // /{paramfirst:path}, /{paramfirst:long}... - a.relPath += fmt.Sprintf("/{%s:%s}", paramKey, paramType) - a.dynamic = true - - if nextWord == "" && typ.NumIn() > funcArgPos+1 { - // By is the latest word but func is expected - // more path parameters values, i.e: - // GetBy(name string, age int) - // The caller (parse) doesn't need to know - // about the incremental funcArgPos because - // it will not need it. - return p.parsePathParam(a, nextWord, funcArgPos+1) - } - - return nil -} - -type ast struct { - paramKeys []string // paramfirst, paramsecond... [0] - paramTypes []string // string, int, long, path... [0] - relPath string - dynamic bool // when paramKeys (and paramTypes, are equal) > 0 -} - -// moved to func_caller#buildMethodcall, it's bigger and with repeated code -// than this, below function but it's faster. -// func (a *ast) MethodCall(ctx context.Context, f reflect.Value) { -// if a.dynamic { -// f.Call(a.paramValues(ctx)) -// return -// } -// -// f.Interface().(func())() -// } - -func (a *ast) paramValues(ctx context.Context) []reflect.Value { - if !a.dynamic { - return nil - } - - l := len(a.paramKeys) - values := make([]reflect.Value, l, l) - for i := 0; i < l; i++ { - paramKey := a.paramKeys[i] - paramType := a.paramTypes[i] - values[i] = getParamValueFromType(ctx, paramType, paramKey) - } - - return values -} - -func getParamValueFromType(ctx context.Context, paramType string, paramKey string) reflect.Value { - if paramType == paramTypeInt { - v, _ := ctx.Params().GetInt(paramKey) - return reflect.ValueOf(v) - } - - if paramType == paramTypeLong { - v, _ := ctx.Params().GetInt64(paramKey) - return reflect.ValueOf(v) - } - - if paramType == paramTypeBoolean { - v, _ := ctx.Params().GetBool(paramKey) - return reflect.ValueOf(v) - } - - // string, path... - return reflect.ValueOf(ctx.Params().Get(paramKey)) -} diff --git a/mvc/activator/methodfunc/func_result_dispatcher.go b/mvc/activator/methodfunc/func_result_dispatcher.go deleted file mode 100644 index 480a9e362e..0000000000 --- a/mvc/activator/methodfunc/func_result_dispatcher.go +++ /dev/null @@ -1,230 +0,0 @@ -package methodfunc - -import ( - "reflect" - "strings" - - "github.com/kataras/iris/context" -) - -// Result is a response dispatcher. -// All types that complete this interface -// can be returned as values from the method functions. -type Result interface { - // Dispatch should sends the response to the context's response writer. - Dispatch(ctx context.Context) -} - -const slashB byte = '/' - -type compatibleErr interface { - Error() string -} - -// DefaultErrStatusCode is the default error status code (400) -// when the response contains an error which is not nil. -var DefaultErrStatusCode = 400 - -// DispatchErr writes the error to the response. -func DispatchErr(ctx context.Context, status int, err error) { - if status < 400 { - status = DefaultErrStatusCode - } - ctx.StatusCode(status) - if text := err.Error(); text != "" { - ctx.WriteString(text) - ctx.StopExecution() - } -} - -// DispatchCommon is being used internally to send -// commonly used data to the response writer with a smart way. -func DispatchCommon(ctx context.Context, - statusCode int, contentType string, content []byte, v interface{}, err error, found bool) { - - // if we have a false boolean as a return value - // then skip everything and fire a not found, - // we even don't care about the given status code or the object or the content. - if !found { - ctx.NotFound() - return - } - - status := statusCode - if status == 0 { - status = 200 - } - - if err != nil { - DispatchErr(ctx, status, err) - return - } - - // write the status code, the rest will need that before any write ofc. - ctx.StatusCode(status) - if contentType == "" { - // to respect any ctx.ContentType(...) call - // especially if v is not nil. - contentType = ctx.GetContentType() - } - - if v != nil { - if d, ok := v.(Result); ok { - // write the content type now (internal check for empty value) - ctx.ContentType(contentType) - d.Dispatch(ctx) - return - } - - if strings.HasPrefix(contentType, context.ContentJavascriptHeaderValue) { - _, err = ctx.JSONP(v) - } else if strings.HasPrefix(contentType, context.ContentXMLHeaderValue) { - _, err = ctx.XML(v, context.XML{Indent: " "}) - } else { - // defaults to json if content type is missing or its application/json. - _, err = ctx.JSON(v, context.JSON{Indent: " "}) - } - - if err != nil { - DispatchErr(ctx, status, err) - } - - return - } - - ctx.ContentType(contentType) - // .Write even len(content) == 0 , this should be called in order to call the internal tryWriteHeader, - // it will not cost anything. - ctx.Write(content) -} - -// DispatchFuncResult is being used internally to resolve -// and send the method function's output values to the -// context's response writer using a smart way which -// respects status code, content type, content, custom struct -// and an error type. -// Supports for: -// func(c *ExampleController) Get() string | -// (string, string) | -// (string, int) | -// ... -// int | -// (int, string | -// (string, error) | -// ... -// error | -// (int, error) | -// (customStruct, error) | -// ... -// bool | -// (int, bool) | -// (string, bool) | -// (customStruct, bool) | -// ... -// customStruct | -// (customStruct, int) | -// (customStruct, string) | -// Result or (Result, error) and so on... -// -// where Get is an HTTP METHOD. -func DispatchFuncResult(ctx context.Context, values []reflect.Value) { - numOut := len(values) - if numOut == 0 { - return - } - - var ( - // if statusCode > 0 then send this status code. - // Except when err != nil then check if status code is < 400 and - // if it's set it as DefaultErrStatusCode. - // Except when found == false, then the status code is 404. - statusCode int - // if not empty then use that as content type, - // if empty and custom != nil then set it to application/json. - contentType string - // if len > 0 then write that to the response writer as raw bytes, - // except when found == false or err != nil or custom != nil. - content []byte - // if not nil then check - // for content type (or json default) and send the custom data object - // except when found == false or err != nil. - custom interface{} - // if not nil then check for its status code, - // if not status code or < 400 then set it as DefaultErrStatusCode - // and fire the error's text. - err error - // if false then skip everything and fire 404. - found = true // defaults to true of course, otherwise will break :) - ) - - for _, v := range values { - // order of these checks matters - // for example, first we need to check for status code, - // secondly the string (for content type and content)... - if !v.IsValid() { - continue - } - - f := v.Interface() - - if b, ok := f.(bool); ok { - found = b - if !found { - // skip everything, we don't care about other return values, - // this boolean is the higher in order. - break - } - continue - } - - if i, ok := f.(int); ok { - statusCode = i - continue - } - - if s, ok := f.(string); ok { - // a string is content type when it contains a slash and - // content or custom struct is being calculated already; - // (string -> content, string-> content type) - // (customStruct, string -> content type) - if (len(content) > 0 || custom != nil) && strings.IndexByte(s, slashB) > 0 { - contentType = s - } else { - // otherwise is content - content = []byte(s) - } - - continue - } - - if b, ok := f.([]byte); ok { - // it's raw content, get the latest - content = b - continue - } - - if e, ok := f.(compatibleErr); ok { - if e != nil { // it's always not nil but keep it here. - err = e - if statusCode < 400 { - statusCode = DefaultErrStatusCode - } - break // break on first error, error should be in the end but we - // need to know break the dispatcher if any error. - // at the end; we don't want to write anything to the response if error is not nil. - } - continue - } - - // else it's a custom struct or a dispatcher, we'll decide later - // because content type and status code matters - // do that check in order to be able to correctly dispatch: - // (customStruct, error) -> customStruct filled and error is nil - if custom == nil && f != nil { - custom = f - } - - } - - DispatchCommon(ctx, statusCode, contentType, content, custom, err, found) -} diff --git a/mvc/activator/methodfunc/methodfunc.go b/mvc/activator/methodfunc/methodfunc.go deleted file mode 100644 index f76a9a9048..0000000000 --- a/mvc/activator/methodfunc/methodfunc.go +++ /dev/null @@ -1,50 +0,0 @@ -package methodfunc - -import ( - "reflect" - - "github.com/kataras/iris/context" - "github.com/kataras/iris/core/errors" -) - -// MethodFunc the handler function. -type MethodFunc struct { - FuncInfo - // MethodCall fires the actual handler. - // The "ctx" is the current context, helps us to get any path parameter's values. - // - // The "f" is the controller's function which is responsible - // for that request for this http method. - // That function can accept one parameter. - // - // The default callers (and the only one for now) - // are pre-calculated by the framework. - MethodCall func(ctx context.Context, f reflect.Value) - RelPath string -} - -// Resolve returns all the method funcs -// necessary information and actions to -// perform the request. -func Resolve(typ reflect.Type) ([]MethodFunc, error) { - r := errors.NewReporter() - var methodFuncs []MethodFunc - infos := fetchInfos(typ) - for _, info := range infos { - parser := newFuncParser(info) - a, err := parser.parse() - if r.AddErr(err) { - continue - } - - methodFunc := MethodFunc{ - RelPath: a.relPath, - FuncInfo: info, - MethodCall: buildMethodCall(a), - } - - methodFuncs = append(methodFuncs, methodFunc) - } - - return methodFuncs, r.Return() -} diff --git a/mvc/activator/model/model.go b/mvc/activator/model/model.go deleted file mode 100644 index 078cc6d46c..0000000000 --- a/mvc/activator/model/model.go +++ /dev/null @@ -1,73 +0,0 @@ -package model - -import ( - "reflect" - - "github.com/kataras/iris/mvc/activator/field" - - "github.com/kataras/iris/context" -) - -// Controller is responsible -// to load and handle the `Model(s)` inside a controller struct -// via the `iris:"model"` tag field. -// It stores the optional models from -// the struct's fields values that -// are being setted by the method function -// and set them as ViewData. -type Controller struct { - fields []field.Field -} - -// Load tries to lookup and set for any valid model field. -// Returns nil if no models are being used. -func Load(typ reflect.Type) *Controller { - matcher := func(f reflect.StructField) bool { - if tag, ok := f.Tag.Lookup("iris"); ok { - if tag == "model" { - return true - } - } - return false - } - - fields := field.LookupFields(typ.Elem(), matcher, nil) - - if len(fields) == 0 { - return nil - } - - mc := &Controller{ - fields: fields, - } - return mc -} - -// Handle transfer the models to the view. -func (mc *Controller) Handle(ctx context.Context, c reflect.Value) { - elem := c.Elem() // controller should always be a pointer at this state - - for _, f := range mc.fields { - - index := f.GetIndex() - typ := f.GetType() - name := f.GetTagName() - - elemField := elem.FieldByIndex(index) - // check if current controller's element field - // is valid, is not nil and it's type is the same (should be but make that check to be sure). - if !elemField.IsValid() || - (elemField.Kind() == reflect.Ptr && elemField.IsNil()) || - elemField.Type() != typ { - continue - } - - fieldValue := elemField.Interface() - ctx.ViewData(name, fieldValue) - // /*maybe some time in the future*/ if resetable { - // // clean up - // elemField.Set(reflect.Zero(typ)) - // } - - } -} diff --git a/mvc/activator/persistence/persistence.go b/mvc/activator/persistence/persistence.go deleted file mode 100644 index 8b81ccbc93..0000000000 --- a/mvc/activator/persistence/persistence.go +++ /dev/null @@ -1,60 +0,0 @@ -package persistence - -import ( - "reflect" - - "github.com/kataras/iris/mvc/activator/field" -) - -// Controller is responsible to load from the original -// end-developer's main controller's value -// and re-store the persistence data by scanning the original. -// It stores and sets to each new controller -// the optional data that should be shared among all requests. -type Controller struct { - fields []field.Field -} - -// Load scans and load for persistence data based on the `iris:"persistence"` tag. -// -// The type is the controller's Type. -// the "val" is the original end-developer's controller's Value. -// Returns nil if no persistence data to store found. -func Load(typ reflect.Type, val reflect.Value) *Controller { - matcher := func(elemField reflect.StructField) bool { - if tag, ok := elemField.Tag.Lookup("iris"); ok { - if tag == "persistence" { - return true - } - } - return false - } - - handler := func(f *field.Field) { - valF := val.Field(f.Index) - if valF.IsValid() || (valF.Kind() == reflect.Ptr && !valF.IsNil()) { - val := reflect.ValueOf(valF.Interface()) - if val.IsValid() || (val.Kind() == reflect.Ptr && !val.IsNil()) { - f.Value = val - } - } - } - - fields := field.LookupFields(typ.Elem(), matcher, handler) - - if len(fields) == 0 { - return nil - } - - return &Controller{ - fields: fields, - } -} - -// Handle re-stores the persistence data at the current controller. -func (pc *Controller) Handle(c reflect.Value) { - elem := c.Elem() // controller should always be a pointer at this state - for _, f := range pc.fields { - f.SendTo(elem) - } -} diff --git a/mvc/controller.go b/mvc/controller.go index fe50663862..41f37ac200 100644 --- a/mvc/controller.go +++ b/mvc/controller.go @@ -1,369 +1,406 @@ package mvc import ( + "fmt" "reflect" "strings" "github.com/kataras/iris/context" - "github.com/kataras/iris/core/memstore" - "github.com/kataras/iris/mvc/activator" + "github.com/kataras/iris/core/router" + "github.com/kataras/iris/core/router/macro" + "github.com/kataras/iris/hero" + "github.com/kataras/iris/hero/di" + + "github.com/kataras/golog" ) -// C is the lightweight BaseController type as an alternative of the `Controller` struct type. -// It contains only the Name of the controller and the Context, it's the best option -// to balance the performance cost reflection uses -// if your controller uses the new func output values dispatcher feature; -// func(c *ExampleController) Get() string | -// (string, string) | -// (string, int) | -// int | -// (int, string | -// (string, error) | -// bool | -// (any, bool) | -// error | -// (int, error) | -// (customStruct, error) | -// customStruct | -// (customStruct, int) | -// (customStruct, string) | -// Result or (Result, error) -// where Get is an HTTP Method func. +// BaseController is the optional controller interface, if it's +// completed by the end controller then the BeginRequest and EndRequest +// are called between the controller's method responsible for the incoming request. +type BaseController interface { + BeginRequest(context.Context) + EndRequest(context.Context) +} + +type shared interface { + Name() string + Router() router.Party + GetRoute(methodName string) *router.Route + Handle(httpMethod, path, funcName string, middleware ...context.Handler) *router.Route +} + +// BeforeActivation is being used as the onle one input argument of a +// `func(c *Controller) BeforeActivation(b mvc.BeforeActivation) {}`. // -// Look `core/router#APIBuilder#Controller` method too. +// It's being called before the controller's dependencies binding to the fields or the input arguments +// but before server ran. // -// It completes the `activator.BaseController` interface. +// It's being used to customize a controller if needed inside the controller itself, +// it's called once per application. +type BeforeActivation interface { + shared + Dependencies() *di.Values +} + +// AfterActivation is being used as the onle one input argument of a +// `func(c *Controller) AfterActivation(a mvc.AfterActivation) {}`. +// +// It's being called after the `BeforeActivation`, +// and after controller's dependencies binded to the fields or the input arguments but before server ran. // -// Example at: https://github.com/kataras/iris/tree/master/_examples/mvc/overview/web/controllers. -// Example usage at: https://github.com/kataras/iris/blob/master/mvc/method_result_test.go#L17. -type C struct { - // The Name of the `C` controller. - Name string - // The current context.Context. - // - // we have to name it for two reasons: - // 1: can't ignore these via reflection, it doesn't give an option to - // see if the functions is derived from another type. - // 2: end-developer may want to use some method functions - // or any fields that could be conflict with the context's. - Ctx context.Context +// It's being used to customize a controller if needed inside the controller itself, +// it's called once per application. +type AfterActivation interface { + shared + DependenciesReadOnly() ValuesReadOnly + Singleton() bool } -var _ activator.BaseController = &C{} +var ( + _ BeforeActivation = (*ControllerActivator)(nil) + _ AfterActivation = (*ControllerActivator)(nil) +) -// SetName sets the controller's full name. -// It's called internally. -func (c *C) SetName(name string) { c.Name = name } +// ControllerActivator returns a new controller type info description. +// Its functionality can be overridden by the end-dev. +type ControllerActivator struct { + // the router is used on the `Activate` and can be used by end-dev on the `BeforeActivation` + // to register any custom controller's methods as handlers. + router router.Party + + // initRef BaseController // the BaseController as it's passed from the end-dev. + Value reflect.Value // the BaseController's Value. + Type reflect.Type // raw type of the BaseController (initRef). + // FullName it's the last package path segment + "." + the Name. + // i.e: if login-example/user/controller.go, the FullName is "user.Controller". + fullName string + + // the already-registered routes, key = the controller's function name. + // End-devs can change some properties of the *Route on the `BeforeActivator` by using the + // `GetRoute(functionName)`. It's also protects for duplicatations. + routes map[string]*router.Route + + // the bindings that comes from the Engine and the controller's filled fields if any. + // Can be binded to the the new controller's fields and method that is fired + // on incoming requests. + dependencies di.Values + + // initialized on the first `Handle`. + injector *di.StructInjector +} -// BeginRequest starts the request by initializing the `Context` field. -func (c *C) BeginRequest(ctx context.Context) { c.Ctx = ctx } +// NameOf returns the package name + the struct type's name, +// it's used to take the full name of an Controller, the `ControllerActivator#Name`. +func NameOf(v interface{}) string { + elemTyp := di.IndirectType(di.ValueOf(v).Type()) -// EndRequest does nothing, is here to complete the `BaseController` interface. -func (c *C) EndRequest(ctx context.Context) {} + typName := elemTyp.Name() + pkgPath := elemTyp.PkgPath() + fullname := pkgPath[strings.LastIndexByte(pkgPath, '/')+1:] + "." + typName -// Controller is the base controller for the high level controllers instances. -// -// This base controller is used as an alternative way of building -// APIs, the controller can register all type of http methods. -// -// Keep note that controllers are bit slow -// because of the reflection use however it's as fast as possible because -// it does preparation before the serve-time handler but still -// remains slower than the low-level handlers -// such as `Handle, Get, Post, Put, Delete, Connect, Head, Trace, Patch`. -// -// -// All fields that are tagged with iris:"persistence"` or binded -// are being persistence and kept the same between the different requests. -// -// An Example Controller can be: -// -// type IndexController struct { -// Controller -// } -// -// func (c *IndexController) Get() { -// c.Tmpl = "index.html" -// c.Data["title"] = "Index page" -// c.Data["message"] = "Hello world!" -// } -// -// Usage: app.Controller("/", new(IndexController)) -// -// -// Another example with bind: -// -// type UserController struct { -// mvc.Controller -// -// DB *DB -// CreatedAt time.Time -// } -// -// // Get serves using the User controller when HTTP Method is "GET". -// func (c *UserController) Get() { -// c.Tmpl = "user/index.html" -// c.Data["title"] = "User Page" -// c.Data["username"] = "kataras " + c.Params.Get("userid") -// c.Data["connstring"] = c.DB.Connstring -// c.Data["uptime"] = time.Now().Sub(c.CreatedAt).Seconds() -// } -// -// Usage: app.Controller("/user/{id:int}", new(UserController), db, time.Now()) -// Note: Binded values of context.Handler type are being recognised as middlewares by the router. -// -// Look `core/router/APIBuilder#Controller` method too. -// -// It completes the `activator.BaseController` interface. -type Controller struct { - // Name contains the current controller's full name. - // - // doesn't change on different paths. - Name string - - // contains the `Name` as different words, all lowercase, - // without the "Controller" suffix if exists. - // we need this as field because the activator - // we will not try to parse these if not needed - // it's up to the end-developer to call `RelPath()` or `RelTmpl()` - // which will result to fill them. - // - // doesn't change on different paths. - nameAsWords []string - - // relPath the "as assume" relative request path. - // - // If UserController and request path is "/user/messages" then it's "/messages" - // if UserPostController and request path is "/user/post" then it's "/" - // if UserProfile and request path is "/user/profile/likes" then it's "/likes" - // - // doesn't change on different paths. - relPath string - - // request path and its parameters, read-write. - // Path is the current request path, if changed then it redirects. - Path string - // Params are the request path's parameters, i.e - // for route like "/user/{id}" and request to "/user/42" - // it contains the "id" = 42. - Params *context.RequestParams - - // some info read and write, - // can be already set-ed by previous handlers as well. - Status int - Values *memstore.Store - - // relTmpl the "as assume" relative path to the view root folder. - // - // If UserController then it's "user/" - // if UserPostController then it's "user/post/" - // if UserProfile then it's "user/profile/". - // - // doesn't change on different paths. - relTmpl string - - // view read and write, - // can be already set-ed by previous handlers as well. - Layout string - Tmpl string - Data map[string]interface{} - - ContentType string - Text string // response as string - - // give access to the request context itself. - Ctx context.Context + return fullname } -var _ activator.BaseController = &Controller{} - -var ctrlSuffix = reflect.TypeOf(Controller{}).Name() +func newControllerActivator(router router.Party, controller interface{}, dependencies []reflect.Value) *ControllerActivator { + typ := reflect.TypeOf(controller) + + c := &ControllerActivator{ + // give access to the Router to the end-devs if they need it for some reason, + // i.e register done handlers. + router: router, + Value: reflect.ValueOf(controller), + Type: typ, + // the full name of the controller: its type including the package path. + fullName: NameOf(controller), + // set some methods that end-dev cann't use accidentally + // to register a route via the `Handle`, + // all available exported and compatible methods + // are being appended to the slice at the `parseMethods`, + // if a new method is registered via `Handle` its function name + // is also appended to that slice. + routes: whatReservedMethods(typ), + // CloneWithFieldsOf: include the manual fill-ed controller struct's fields to the dependencies. + dependencies: di.Values(dependencies).CloneWithFieldsOf(controller), + } -// SetName sets the controller's full name. -// It's called internally. -func (c *Controller) SetName(name string) { - c.Name = name + return c } -func (c *Controller) getNameWords() []string { - if len(c.nameAsWords) == 0 { - c.nameAsWords = findCtrlWords(c.Name) +func whatReservedMethods(typ reflect.Type) map[string]*router.Route { + methods := []string{"BeforeActivation", "AfterActivation"} + // BeforeActivatior/AfterActivation are not routes but they are + // reserved names* + if isBaseController(typ) { + methods = append(methods, "BeginRequest", "EndRequest") + } + + routes := make(map[string]*router.Route, len(methods)) + for _, m := range methods { + routes[m] = &router.Route{} } - return c.nameAsWords + + return routes +} + +// Dependencies returns the write and read access of the dependencies that are +// came from the parent MVC Application, with this you can customize +// the dependencies per controller, used at the `BeforeActivation`. +func (c *ControllerActivator) Dependencies() *di.Values { + return &c.dependencies +} + +// ValuesReadOnly returns the read-only access type of the controller's dependencies. +// Used at `AfterActivation`. +type ValuesReadOnly interface { + // Has returns true if a binder responsible to + // bind and return a type of "typ" is already registered to this controller. + Has(value interface{}) bool + // Len returns the length of the values. + Len() int + // Clone returns a copy of the current values. + Clone() di.Values + // CloneWithFieldsOf will return a copy of the current values + // plus the "s" struct's fields that are filled(non-zero) by the caller. + CloneWithFieldsOf(s interface{}) di.Values } -// Route returns the current request controller's context read-only access route. -func (c *Controller) Route() context.RouteReadOnly { - return c.Ctx.GetCurrentRoute() +// DependenciesReadOnly returns the read-only access type of the controller's dependencies. +// Used at `AfterActivation`. +func (c *ControllerActivator) DependenciesReadOnly() ValuesReadOnly { + return c.dependencies } -const slashStr = "/" +// Name returns the full name of the controller, its package name + the type name. +// Can used at both `BeforeActivation` and `AfterActivation`. +func (c *ControllerActivator) Name() string { + return c.fullName +} -// RelPath tries to return the controller's name -// without the "Controller" prefix, all lowercase -// prefixed with slash and splited by slash appended -// with the rest of the request path. -// For example: -// If UserController and request path is "/user/messages" then it's "/messages" -// if UserPostController and request path is "/user/post" then it's "/" -// if UserProfile and request path is "/user/profile/likes" then it's "/likes" +// Router is the standard Iris router's public API. +// With this you can register middleware, view layouts, subdomains, serve static files +// and even add custom standard iris handlers as normally. // -// It's useful for things like path checking and redirect. -func (c *Controller) RelPath() string { - if c.relPath == "" { - w := c.getNameWords() - rel := strings.Join(w, slashStr) - - reqPath := c.Ctx.Path() - if len(reqPath) == 0 { - // it never come here - // but to protect ourselves just return an empty slash. - return slashStr - } - // [1:]to ellimuate the prefixes like "//" - // request path has always "/" - rel = strings.Replace(reqPath[1:], rel, "", 1) - if rel == "" { - rel = slashStr +// This Router is the router instance that came from the parent MVC Application, +// it's the `app.Party(...)` argument. +// +// Can used at both `BeforeActivation` and `AfterActivation`. +func (c *ControllerActivator) Router() router.Party { + return c.router +} + +// GetRoute returns a registered route based on the controller's method name. +// It can be used to change the route's name, which is useful for reverse routing +// inside views. Custom routes can be registered with `Handle`, which returns the *Route. +// This method exists mostly for the automatic method parsing based on the known patterns +// inside a controller. +// +// A check for `nil` is necessary for unregistered methods. +// +// See `Handle` too. +func (c *ControllerActivator) GetRoute(methodName string) *router.Route { + for name, route := range c.routes { + if name == methodName { + return route } - c.relPath = rel - // this will return any dynamic path after the static one - // or a a slash "/": - // - // reqPath := c.Ctx.Path() - // if len(reqPath) == 0 { - // // it never come here - // // but to protect ourselves just return an empty slash. - // return slashStr - // } - // var routeVParams []string - // c.Params.Visit(func(key string, value string) { - // routeVParams = append(routeVParams, value) - // }) - - // rel := c.Route().StaticPath() - // println(rel) - // // [1:]to ellimuate the prefixes like "//" - // // request path has always "/" - // rel = strings.Replace(reqPath, rel[1:], "", 1) - // println(rel) - // if rel == "" { - // rel = slashStr - // } - // c.relPath = rel } - - return c.relPath + return nil } -// RelTmpl tries to return the controller's name -// without the "Controller" prefix, all lowercase -// splited by slash and suffixed by slash. -// For example: -// If UserController then it's "user/" -// if UserPostController then it's "user/post/" -// if UserProfile then it's "user/profile/". -// -// It's useful to locate templates if the controller and views path have aligned names. -func (c *Controller) RelTmpl() string { - if c.relTmpl == "" { - c.relTmpl = strings.Join(c.getNameWords(), slashStr) + slashStr +// Singleton returns new if all incoming clients' requests +// have the same controller instance. +// This is done automatically by iris to reduce the creation +// of a new controller on each request, if the controller doesn't contain +// any unexported fields and all fields are services-like, static. +func (c *ControllerActivator) Singleton() bool { + if c.injector == nil { + panic("MVC: Singleton used on an invalid state the API gives access to it only `AfterActivation`, report this as bug") } - return c.relTmpl + return c.injector.Scope == di.Singleton } -// Write writes to the client via the context's ResponseWriter. -// Controller completes the `io.Writer` interface for the shake of ease. -func (c *Controller) Write(contents []byte) (int, error) { - c.tryWriteHeaders() - return c.Ctx.ResponseWriter().Write(contents) +// checks if a method is already registered. +func (c *ControllerActivator) isReservedMethod(name string) bool { + for methodName := range c.routes { + if methodName == name { + return true + } + } + + return false } -// Writef formats according to a format specifier and writes to the response. -func (c *Controller) Writef(format string, a ...interface{}) (int, error) { - c.tryWriteHeaders() - return c.Ctx.ResponseWriter().Writef(format, a...) +func (c *ControllerActivator) activate() { + c.parseMethods() } -// BeginRequest starts the main controller -// it initialize the Ctx and other fields. -// -// It's called internally. -// End-Developer can ovverride it but it still MUST be called. -func (c *Controller) BeginRequest(ctx context.Context) { - // path and path params - c.Path = ctx.Path() - c.Params = ctx.Params() - // response status code - c.Status = ctx.GetStatusCode() - - // share values - c.Values = ctx.Values() - // view data for templates, remember - // each controller is a new instance, so - // checking for nil and then init those type of fields - // have no meaning. - c.Data = make(map[string]interface{}, 0) - - // context itself - c.Ctx = ctx +func (c *ControllerActivator) addErr(err error) bool { + return c.router.GetReporter().AddErr(err) } -func (c *Controller) tryWriteHeaders() { - if c.Status > 0 && c.Status != c.Ctx.GetStatusCode() { - c.Ctx.StatusCode(c.Status) +// register all available, exported methods to handlers if possible. +func (c *ControllerActivator) parseMethods() { + n := c.Type.NumMethod() + for i := 0; i < n; i++ { + m := c.Type.Method(i) + c.parseMethod(m) } +} + +func (c *ControllerActivator) parseMethod(m reflect.Method) { + httpMethod, httpPath, err := parseMethod(m, c.isReservedMethod) + if err != nil { + if err != errSkip { + c.addErr(fmt.Errorf("MVC: fail to parse the route path and HTTP method for '%s.%s': %v", c.fullName, m.Name, err)) + } - if c.ContentType != "" { - c.Ctx.ContentType(c.ContentType) + return } + + c.Handle(httpMethod, httpPath, m.Name) } -// EndRequest is the final method which will be executed -// before response sent. +// Handle registers a route based on a http method, the route's path +// and a function name that belongs to the controller, it accepts +// a forth, optionally, variadic parameter which is the before handlers. // -// It checks for the fields and calls the necessary context's -// methods to modify the response to the client. -// -// It's called internally. -// End-Developer can ovveride it but still should be called at the end. -func (c *Controller) EndRequest(ctx context.Context) { - if ctx.ResponseWriter().Written() >= 0 { // status code only (0) or actual body written(>0) - return +// Just like `APIBuilder`, it returns the `*router.Route`, if failed +// then it logs the errors and it returns nil, you can check the errors +// programmatically by the `APIBuilder#GetReporter`. +func (c *ControllerActivator) Handle(method, path, funcName string, middleware ...context.Handler) *router.Route { + if method == "" || path == "" || funcName == "" || + c.isReservedMethod(funcName) { + // isReservedMethod -> if it's already registered + // by a previous Handle or analyze methods internally. + return nil } - if path := c.Path; path != "" && path != ctx.Path() { - // then redirect and exit. - ctx.Redirect(path, c.Status) - return + // get the method from the controller type. + m, ok := c.Type.MethodByName(funcName) + if !ok { + c.addErr(fmt.Errorf("MVC: function '%s' doesn't exist inside the '%s' controller", + funcName, c.fullName)) + return nil + } + + // parse a route template which contains the parameters organised. + tmpl, err := macro.Parse(path, c.router.Macros()) + if err != nil { + c.addErr(fmt.Errorf("MVC: fail to parse the path for '%s.%s': %v", c.fullName, funcName, err)) + return nil } - c.tryWriteHeaders() - if response := c.Text; response != "" { - ctx.WriteString(response) - return // exit here + // get the function's input. + funcIn := getInputArgsFromFunc(m.Type) + // get the path parameters bindings from the template, + // use the function's input except the receiver which is the + // end-dev's controller pointer. + pathParams := getPathParamsForInput(tmpl.Params, funcIn[1:]...) + // get the function's input arguments' bindings. + funcDependencies := c.dependencies.Clone() + funcDependencies.AddValues(pathParams...) + + handler := c.handlerOf(m, funcDependencies) + + // register the handler now. + route := c.router.Handle(method, path, append(middleware, handler)...) + if route == nil { + c.addErr(fmt.Errorf("MVC: unable to register a route for the path for '%s.%s'", c.fullName, funcName)) + return nil } - if view := c.Tmpl; view != "" { - if layout := c.Layout; layout != "" { - ctx.ViewLayout(layout) + // change the main handler's name in order to respect the controller's and give + // a proper debug message. + route.MainHandlerName = fmt.Sprintf("%s.%s", c.fullName, funcName) + + // add this as a reserved method name in order to + // be sure that the same func will not be registered again, + // even if a custom .Handle later on. + c.routes[funcName] = route + + return route +} + +var emptyIn = []reflect.Value{} + +func (c *ControllerActivator) handlerOf(m reflect.Method, funcDependencies []reflect.Value) context.Handler { + // Remember: + // The `Handle->handlerOf` can be called from `BeforeActivation` event + // then, the c.injector is nil because + // we may not have the dependencies binded yet. + // To solve this we're doing a check on the FIRST `Handle`, + // if c.injector is nil, then set it with the current bindings, + // these bindings can change after, so first add dependencies and after register routes. + if c.injector == nil { + c.injector = di.Struct(c.Value, c.dependencies...) + if c.injector.Has { + golog.Debugf("MVC dependencies of '%s':\n%s", c.fullName, c.injector.String()) + } + } + + // fmt.Printf("for %s | values: %s\n", funcName, funcDependencies) + funcInjector := di.Func(m.Func, funcDependencies...) + // fmt.Printf("actual injector's inputs length: %d\n", funcInjector.Length) + if funcInjector.Has { + golog.Debugf("MVC dependencies of method '%s.%s':\n%s", c.fullName, m.Name, funcInjector.String()) + } + + var ( + implementsBase = isBaseController(c.Type) + hasBindableFields = c.injector.CanInject + hasBindableFuncInputs = funcInjector.Has + + call = m.Func.Call + ) + + if !implementsBase && !hasBindableFields && !hasBindableFuncInputs { + return func(ctx context.Context) { + hero.DispatchFuncResult(ctx, call(c.injector.AcquireSlice())) } - if len(c.Data) > 0 { - dataKey := ctx.Application().ConfigurationReadOnly().GetViewDataContextKey() - // In order to respect any c.Ctx.ViewData that may called manually before; - if ctx.Values().Get(dataKey) == nil { - // if no c.Ctx.ViewData then it's empty do a - // pure set, it's faster. - ctx.Values().Set(dataKey, c.Data) - } else { - // else do a range loop and set the data one by one. - for k, v := range c.Data { - ctx.ViewData(k, v) - } + } + + n := m.Type.NumIn() + return func(ctx context.Context) { + var ( + ctrl = c.injector.Acquire() + ctxValue reflect.Value + ) + + // inject struct fields first before the BeginRequest and EndRequest, if any, + // in order to be able to have access there. + if hasBindableFields { + ctxValue = reflect.ValueOf(ctx) + c.injector.InjectElem(ctrl.Elem(), ctxValue) + } + + // check if has BeginRequest & EndRequest, before try to bind the method's inputs. + if implementsBase { + // the Interface(). is faster than MethodByName or pre-selected methods. + b := ctrl.Interface().(BaseController) + // init the request. + b.BeginRequest(ctx) + + // if begin request stopped the execution. + if ctx.IsStopped() { + return } + defer b.EndRequest(ctx) } - ctx.View(view) + if hasBindableFuncInputs { + // means that ctxValue is not initialized before by the controller's struct injector. + if !hasBindableFields { + ctxValue = reflect.ValueOf(ctx) + } + + in := make([]reflect.Value, n, n) + in[0] = ctrl + funcInjector.Inject(&in, ctxValue) + hero.DispatchFuncResult(ctx, call(in)) + return + } + + hero.DispatchFuncResult(ctx, ctrl.Method(m.Index).Call(emptyIn)) } + } diff --git a/mvc/controller_handle_test.go b/mvc/controller_handle_test.go new file mode 100644 index 0000000000..55e200d4c4 --- /dev/null +++ b/mvc/controller_handle_test.go @@ -0,0 +1,127 @@ +package mvc_test + +import ( + "testing" + + "github.com/kataras/iris" + "github.com/kataras/iris/context" + "github.com/kataras/iris/httptest" + + . "github.com/kataras/iris/mvc" +) + +// service +type ( + // these TestService and TestServiceImpl could be in lowercase, unexported + // but the `Say` method should be exported however we have those exported + // because of the controller handler test. + TestService interface { + Say(string) string + } + TestServiceImpl struct { + prefix string + } +) + +func (s *TestServiceImpl) Say(message string) string { + return s.prefix + " " + message +} + +type testControllerHandle struct { + Ctx context.Context + Service TestService + + reqField string +} + +func (c *testControllerHandle) BeforeActivation(b BeforeActivation) { + b.Handle("GET", "/histatic", "HiStatic") + b.Handle("GET", "/hiservice", "HiService") + b.Handle("GET", "/hiparam/{ps:string}", "HiParamBy") + b.Handle("GET", "/hiparamempyinput/{ps:string}", "HiParamEmptyInputBy") +} + +// test `GetRoute` for custom routes. +func (c *testControllerHandle) AfterActivation(a AfterActivation) { + // change automatic parser's route change name. + rget := a.GetRoute("Get") + if rget == nil { + panic("route from function name: 'Get' doesn't exist on `AfterActivation`") + } + rget.Name = "index_route" + + // change a custom route's name. + r := a.GetRoute("HiStatic") + if r == nil { + panic("route from function name: HiStatic doesn't exist on `AfterActivation`") + } + // change the name here, and test if name changed in the handler. + r.Name = "hi_static_route" +} + +func (c *testControllerHandle) BeginRequest(ctx iris.Context) { + c.reqField = ctx.URLParam("reqfield") +} + +func (c *testControllerHandle) EndRequest(ctx iris.Context) {} + +func (c *testControllerHandle) Get() string { + if c.Ctx.GetCurrentRoute().Name() != "index_route" { + return "Get's route's name didn't change on AfterActivation" + } + return "index" +} + +func (c *testControllerHandle) HiStatic() string { + if c.Ctx.GetCurrentRoute().Name() != "hi_static_route" { + return "HiStatic's route's name didn't change on AfterActivation" + } + + return c.reqField +} + +func (c *testControllerHandle) HiService() string { + return c.Service.Say("hi") +} + +func (c *testControllerHandle) HiParamBy(v string) string { + return v +} + +func (c *testControllerHandle) HiParamEmptyInputBy() string { + return "empty in but served with ctx.Params.Get('ps')=" + c.Ctx.Params().Get("ps") +} + +func TestControllerHandle(t *testing.T) { + app := iris.New() + + m := New(app) + m.Register(&TestServiceImpl{prefix: "service:"}) + m.Handle(new(testControllerHandle)) + + e := httptest.New(t, app) + + // test the index, is not part of the current package's implementation but do it. + e.GET("/").Expect().Status(httptest.StatusOK).Body().Equal("index") + + // the important things now. + + // this test ensures that the BeginRequest of the controller will be + // called correctly and also the controller is binded to the first input argument + // (which is the function's receiver, if any, in this case the *testController in go). + expectedReqField := "this is a request field filled by this url param" + e.GET("/histatic").WithQuery("reqfield", expectedReqField).Expect().Status(httptest.StatusOK). + Body().Equal(expectedReqField) + // this test makes sure that the binded values of the controller is handled correctly + // and can be used in a user-defined, dynamic "mvc handler". + e.GET("/hiservice").Expect().Status(httptest.StatusOK). + Body().Equal("service: hi") + + // this worked with a temporary variadic on the resolvemethodfunc which is not + // correct design, I should split the path and params with the rest of implementation + // in order a simple template.Src can be given. + e.GET("/hiparam/value").Expect().Status(httptest.StatusOK). + Body().Equal("value") + e.GET("/hiparamempyinput/value").Expect().Status(httptest.StatusOK). + Body().Equal("empty in but served with ctx.Params.Get('ps')=value") +} diff --git a/mvc/controller_method_parser.go b/mvc/controller_method_parser.go new file mode 100644 index 0000000000..97c1282edb --- /dev/null +++ b/mvc/controller_method_parser.go @@ -0,0 +1,254 @@ +package mvc + +import ( + "errors" + "fmt" + "reflect" + "strings" + "unicode" + + "github.com/kataras/iris/core/router" + "github.com/kataras/iris/core/router/macro/interpreter/ast" +) + +const ( + tokenBy = "By" + tokenWildcard = "Wildcard" // "ByWildcard". +) + +// word lexer, not characters. +type methodLexer struct { + words []string + cur int +} + +func newMethodLexer(s string) *methodLexer { + l := new(methodLexer) + l.reset(s) + return l +} + +func (l *methodLexer) reset(s string) { + l.cur = -1 + var words []string + if s != "" { + end := len(s) + start := -1 + + for i, n := 0, end; i < n; i++ { + c := rune(s[i]) + if unicode.IsUpper(c) { + // it doesn't count the last uppercase + if start != -1 { + end = i + words = append(words, s[start:end]) + } + start = i + continue + } + end = i + 1 + } + + if end > 0 && len(s) >= end { + words = append(words, s[start:end]) + } + } + + l.words = words +} + +func (l *methodLexer) next() (w string) { + cur := l.cur + 1 + + if w = l.peek(cur); w != "" { + l.cur++ + } + + return +} + +func (l *methodLexer) skip() { + if cur := l.cur + 1; cur < len(l.words) { + l.cur = cur + } else { + l.cur = len(l.words) - 1 + } +} + +func (l *methodLexer) peek(idx int) string { + if idx < len(l.words) { + return l.words[idx] + } + return "" +} + +func (l *methodLexer) peekNext() (w string) { + return l.peek(l.cur + 1) +} + +func (l *methodLexer) peekPrev() (w string) { + if l.cur > 0 { + cur := l.cur - 1 + w = l.words[cur] + } + + return w +} + +var posWords = map[int]string{ + 0: "", + 1: "first", + 2: "second", + 3: "third", + 4: "forth", + 5: "five", + 6: "sixth", + 7: "seventh", + 8: "eighth", + 9: "ninth", + 10: "tenth", + 11: "eleventh", + 12: "twelfth", + 13: "thirteenth", + 14: "fourteenth", + 15: "fifteenth", + 16: "sixteenth", + 17: "seventeenth", + 18: "eighteenth", + 19: "nineteenth", + 20: "twentieth", +} + +func genParamKey(argIdx int) string { + return "arg" + posWords[argIdx] // argfirst, argsecond... +} + +type methodParser struct { + lexer *methodLexer + fn reflect.Method +} + +func parseMethod(fn reflect.Method, skipper func(string) bool) (method, path string, err error) { + if skipper(fn.Name) { + return "", "", errSkip + } + + p := &methodParser{ + fn: fn, + lexer: newMethodLexer(fn.Name), + } + return p.parse() +} + +func methodTitle(httpMethod string) string { + httpMethodFuncName := strings.Title(strings.ToLower(httpMethod)) + return httpMethodFuncName +} + +var errSkip = errors.New("skip") + +var allMethods = append(router.AllMethods[0:], []string{"ALL", "ANY"}...) + +func (p *methodParser) parse() (method, path string, err error) { + funcArgPos := 0 + path = "/" + // take the first word and check for the method. + w := p.lexer.next() + + for _, httpMethod := range allMethods { + possibleMethodFuncName := methodTitle(httpMethod) + if strings.Index(w, possibleMethodFuncName) == 0 { + method = httpMethod + break + } + } + + if method == "" { + // this is not a valid method to parse, we just skip it, + // it may be used for end-dev's use cases. + return "", "", errSkip + } + + for { + w := p.lexer.next() + if w == "" { + break + } + + if w == tokenBy { + funcArgPos++ // starting with 1 because in typ.NumIn() the first is the struct receiver. + + // No need for these: + // ByBy will act like /{param:type}/{param:type} as users expected + // if func input arguments are there, else act By like normal path /by. + // + // if p.lexer.peekPrev() == tokenBy || typ.NumIn() == 1 { // ByBy, then act this second By like a path + // a.relPath += "/" + strings.ToLower(w) + // continue + // } + + if path, funcArgPos, err = p.parsePathParam(path, w, funcArgPos); err != nil { + return "", "", err + } + + continue + } + // static path. + path += "/" + strings.ToLower(w) + } + return +} + +func (p *methodParser) parsePathParam(path string, w string, funcArgPos int) (string, int, error) { + typ := p.fn.Type + + if typ.NumIn() <= funcArgPos { + + // By found but input arguments are not there, so act like /by path without restricts. + path += "/" + strings.ToLower(w) + return path, funcArgPos, nil + } + + var ( + paramKey = genParamKey(funcArgPos) // argfirst, argsecond... + paramType = ast.ParamTypeString // default string + ) + + // string, int... + goType := typ.In(funcArgPos).Name() + nextWord := p.lexer.peekNext() + + if nextWord == tokenWildcard { + p.lexer.skip() // skip the Wildcard word. + paramType = ast.ParamTypePath + } else if pType := ast.LookupParamTypeFromStd(goType); pType != ast.ParamTypeUnExpected { + // it's not wildcard, so check base on our available macro types. + paramType = pType + } else { + if typ.NumIn() > funcArgPos { + // has more input arguments but we are not in the correct + // index now, maybe the first argument was an `iris/context.Context` + // so retry with the "funcArgPos" incremented. + // + // the "funcArgPos" will be updated to the caller as well + // because we return it among the path and the error. + return p.parsePathParam(path, w, funcArgPos+1) + } + return "", 0, errors.New("invalid syntax for " + p.fn.Name) + } + + // /{argfirst:path}, /{argfirst:long}... + path += fmt.Sprintf("/{%s:%s}", paramKey, paramType.String()) + + if nextWord == "" && typ.NumIn() > funcArgPos+1 { + // By is the latest word but func is expected + // more path parameters values, i.e: + // GetBy(name string, age int) + // The caller (parse) doesn't need to know + // about the incremental funcArgPos because + // it will not need it. + return p.parsePathParam(path, nextWord, funcArgPos+1) + } + + return path, funcArgPos, nil +} diff --git a/mvc/method_result_test.go b/mvc/controller_method_result_test.go similarity index 85% rename from mvc/method_result_test.go rename to mvc/controller_method_result_test.go index b6e2c28eff..845c931451 100644 --- a/mvc/method_result_test.go +++ b/mvc/controller_method_result_test.go @@ -7,24 +7,22 @@ import ( "github.com/kataras/iris" "github.com/kataras/iris/context" "github.com/kataras/iris/httptest" - "github.com/kataras/iris/mvc" -) -// activator/methodfunc/func_caller.go. -// and activator/methodfunc/func_result_dispatcher.go + . "github.com/kataras/iris/mvc" +) type testControllerMethodResult struct { - mvc.C + Ctx context.Context } -func (c *testControllerMethodResult) Get() mvc.Result { - return mvc.Response{ +func (c *testControllerMethodResult) Get() Result { + return Response{ Text: "Hello World!", } } -func (c *testControllerMethodResult) GetWithStatus() mvc.Response { // or mvc.Result again, no problem. - return mvc.Response{ +func (c *testControllerMethodResult) GetWithStatus() Response { // or Result again, no problem. + return Response{ Text: "This page doesn't exist", Code: iris.StatusNotFound, } @@ -35,12 +33,12 @@ type testCustomStruct struct { Age int `json:"age" xml:"age"` } -func (c *testControllerMethodResult) GetJson() mvc.Result { +func (c *testControllerMethodResult) GetJson() Result { var err error if c.Ctx.URLParamExists("err") { err = errors.New("error here") } - return mvc.Response{ + return Response{ Err: err, // if err != nil then it will fire the error's text with a BadRequest. Object: testCustomStruct{Name: "Iris", Age: 2}, } @@ -48,30 +46,30 @@ func (c *testControllerMethodResult) GetJson() mvc.Result { var things = []string{"thing 0", "thing 1", "thing 2"} -func (c *testControllerMethodResult) GetThingWithTryBy(index int) mvc.Result { - failure := mvc.Response{ +func (c *testControllerMethodResult) GetThingWithTryBy(index int) Result { + failure := Response{ Text: "thing does not exist", Code: iris.StatusNotFound, } - return mvc.Try(func() mvc.Result { + return Try(func() Result { // if panic because of index exceed the slice // then the "failure" response will be returned instead. - return mvc.Response{Text: things[index]} + return Response{Text: things[index]} }, failure) } -func (c *testControllerMethodResult) GetThingWithTryDefaultBy(index int) mvc.Result { - return mvc.Try(func() mvc.Result { +func (c *testControllerMethodResult) GetThingWithTryDefaultBy(index int) Result { + return Try(func() Result { // if panic because of index exceed the slice // then the default failure response will be returned instead (400 bad request). - return mvc.Response{Text: things[index]} + return Response{Text: things[index]} }) } func TestControllerMethodResult(t *testing.T) { app := iris.New() - app.Controller("/", new(testControllerMethodResult)) + New(app).Handle(new(testControllerMethodResult)) e := httptest.New(t, app) @@ -105,7 +103,7 @@ func TestControllerMethodResult(t *testing.T) { } type testControllerMethodResultTypes struct { - mvc.Controller + Ctx context.Context } func (c *testControllerMethodResultTypes) GetText() string { @@ -175,7 +173,7 @@ func (c *testControllerMethodResultTypes) GetCustomStructWithError() (s testCust func TestControllerMethodResultTypes(t *testing.T) { app := iris.New() - app.Controller("/", new(testControllerMethodResultTypes)) + New(app).Handle(new(testControllerMethodResultTypes)) e := httptest.New(t, app) @@ -189,6 +187,8 @@ func TestControllerMethodResultTypes(t *testing.T) { e.GET("/status/with/text/not/ok/first/second").Expect().Status(iris.StatusForbidden). Body().Equal("NOT_OK_firstsecond") + // Author's note: <-- if that fails means that the last binder called for both input args, + // see path_param_binder.go e.GET("/text/and/content/type").Expect().Status(iris.StatusOK). ContentType("text/html", "utf-8"). @@ -225,17 +225,14 @@ func TestControllerMethodResultTypes(t *testing.T) { type testControllerViewResultRespectCtxViewData struct { T *testing.T - mvc.C } func (t *testControllerViewResultRespectCtxViewData) BeginRequest(ctx context.Context) { - t.C.BeginRequest(ctx) ctx.ViewData("name_begin", "iris_begin") } func (t *testControllerViewResultRespectCtxViewData) EndRequest(ctx context.Context) { - t.C.EndRequest(ctx) - // check if data is not overridden by return mvc.View {Data: context.Map...} + // check if data is not overridden by return View {Data: context.Map...} dataWritten := ctx.GetViewData() if dataWritten == nil { @@ -244,18 +241,18 @@ func (t *testControllerViewResultRespectCtxViewData) EndRequest(ctx context.Cont } if dataWritten["name_begin"] == nil { - t.T.Fatalf(`view data[name_begin] is nil, - BeginRequest's ctx.ViewData call have been overridden by Get's return mvc.View {Data: }. + t.T.Fatalf(`view data[name_begin] is nil, + BeginRequest's ctx.ViewData call have been overridden by Get's return View {Data: }. Total view data: %v`, dataWritten) } if dataWritten["name"] == nil { - t.T.Fatalf("view data[name] is nil, Get's return mvc.View {Data: } didn't work. Total view data: %v", dataWritten) + t.T.Fatalf("view data[name] is nil, Get's return View {Data: } didn't work. Total view data: %v", dataWritten) } } -func (t *testControllerViewResultRespectCtxViewData) Get() mvc.Result { - return mvc.View{ +func (t *testControllerViewResultRespectCtxViewData) Get() Result { + return View{ Name: "doesnt_exists.html", Data: context.Map{"name": "iris"}, // we care about this only. Code: iris.StatusInternalServerError, @@ -264,7 +261,10 @@ func (t *testControllerViewResultRespectCtxViewData) Get() mvc.Result { func TestControllerViewResultRespectCtxViewData(t *testing.T) { app := iris.New() - app.Controller("/", new(testControllerViewResultRespectCtxViewData), t) + m := New(app.Party("/")) + m.Register(t) + m.Handle(new(testControllerViewResultRespectCtxViewData)) + e := httptest.New(t, app) e.GET("/").Expect().Status(iris.StatusInternalServerError) diff --git a/mvc/controller_test.go b/mvc/controller_test.go index 67a1baa533..475b79dc5c 100644 --- a/mvc/controller_test.go +++ b/mvc/controller_test.go @@ -6,67 +6,67 @@ import ( "github.com/kataras/iris" "github.com/kataras/iris/context" - "github.com/kataras/iris/mvc" - "github.com/kataras/iris/mvc/activator" - "github.com/kataras/iris/core/router" "github.com/kataras/iris/httptest" + + . "github.com/kataras/iris/mvc" ) type testController struct { - mvc.Controller + Ctx context.Context } -var writeMethod = func(c mvc.Controller) { - c.Ctx.Writef(c.Ctx.Method()) +var writeMethod = func(ctx context.Context) { + ctx.Writef(ctx.Method()) } func (c *testController) Get() { - writeMethod(c.Controller) + writeMethod(c.Ctx) } func (c *testController) Post() { - writeMethod(c.Controller) + writeMethod(c.Ctx) } func (c *testController) Put() { - writeMethod(c.Controller) + writeMethod(c.Ctx) } func (c *testController) Delete() { - writeMethod(c.Controller) + writeMethod(c.Ctx) } func (c *testController) Connect() { - writeMethod(c.Controller) + writeMethod(c.Ctx) } func (c *testController) Head() { - writeMethod(c.Controller) + writeMethod(c.Ctx) } func (c *testController) Patch() { - writeMethod(c.Controller) + writeMethod(c.Ctx) } func (c *testController) Options() { - writeMethod(c.Controller) + writeMethod(c.Ctx) } func (c *testController) Trace() { - writeMethod(c.Controller) + writeMethod(c.Ctx) } type ( - testControllerAll struct{ mvc.Controller } - testControllerAny struct{ mvc.Controller } // exactly the same as All + testControllerAll struct{ Ctx context.Context } + testControllerAny struct{ Ctx context.Context } // exactly the same as All. ) func (c *testControllerAll) All() { - writeMethod(c.Controller) + writeMethod(c.Ctx) } func (c *testControllerAny) Any() { - writeMethod(c.Controller) + writeMethod(c.Ctx) } func TestControllerMethodFuncs(t *testing.T) { app := iris.New() - app.Controller("/", new(testController)) - app.Controller("/all", new(testControllerAll)) - app.Controller("/any", new(testControllerAny)) + + New(app).Handle(new(testController)) + New(app.Party("/all")).Handle(new(testControllerAll)) + New(app.Party("/any")).Handle(new(testControllerAny)) e := httptest.New(t, app) for _, method := range router.AllMethods { @@ -82,44 +82,8 @@ func TestControllerMethodFuncs(t *testing.T) { } } -func TestControllerMethodAndPathHandleMany(t *testing.T) { - app := iris.New() - app.Controller("/ /path1 /path2 /path3", new(testController)) - - e := httptest.New(t, app) - for _, method := range router.AllMethods { - - e.Request(method, "/").Expect().Status(iris.StatusOK). - Body().Equal(method) - - e.Request(method, "/path1").Expect().Status(iris.StatusOK). - Body().Equal(method) - - e.Request(method, "/path2").Expect().Status(iris.StatusOK). - Body().Equal(method) - } -} - -type testControllerPersistence struct { - mvc.Controller - Data string `iris:"persistence"` -} - -func (c *testControllerPersistence) Get() { - c.Ctx.WriteString(c.Data) -} - -func TestControllerPersistenceFields(t *testing.T) { - data := "this remains the same for all requests" - app := iris.New() - app.Controller("/", &testControllerPersistence{Data: data}) - e := httptest.New(t, app) - e.GET("/").Expect().Status(iris.StatusOK). - Body().Equal(data) -} - type testControllerBeginAndEndRequestFunc struct { - mvc.Controller + Ctx context.Context Username string } @@ -129,16 +93,12 @@ type testControllerBeginAndEndRequestFunc struct { // useful when more than one methods using the // same request values or context's function calls. func (c *testControllerBeginAndEndRequestFunc) BeginRequest(ctx context.Context) { - c.Controller.BeginRequest(ctx) c.Username = ctx.Params().Get("username") - // or t.Params.Get("username") because the - // t.Ctx == ctx and is being initialized at the t.Controller.BeginRequest. } // called after every method (Get() or Post()). func (c *testControllerBeginAndEndRequestFunc) EndRequest(ctx context.Context) { ctx.Writef("done") // append "done" to the response - c.Controller.EndRequest(ctx) } func (c *testControllerBeginAndEndRequestFunc) Get() { @@ -151,7 +111,8 @@ func (c *testControllerBeginAndEndRequestFunc) Post() { func TestControllerBeginAndEndRequestFunc(t *testing.T) { app := iris.New() - app.Controller("/profile/{username}", new(testControllerBeginAndEndRequestFunc)) + New(app.Party("/profile/{username}")). + Handle(new(testControllerBeginAndEndRequestFunc)) e := httptest.New(t, app) usernames := []string{ @@ -194,7 +155,10 @@ func TestControllerBeginAndEndRequestFuncBindMiddleware(t *testing.T) { ctx.Writef("forbidden") } - app.Controller("/profile/{username}", new(testControllerBeginAndEndRequestFunc), middlewareCheck) + app.PartyFunc("/profile/{username}", func(r iris.Party) { + r.Use(middlewareCheck) + New(r).Handle(new(testControllerBeginAndEndRequestFunc)) + }) e := httptest.New(t, app) @@ -223,17 +187,17 @@ type Model struct { Username string } -type testControllerModel struct { - mvc.Controller - - TestModel Model `iris:"model" name:"myModel"` - TestModel2 Model `iris:"model"` +type testControllerEndRequestAwareness struct { + Ctx context.Context } -func (c *testControllerModel) Get() { +func (c *testControllerEndRequestAwareness) Get() { username := c.Ctx.Params().Get("username") - c.TestModel = Model{Username: username} - c.TestModel2 = Model{Username: username + "2"} + c.Ctx.Values().Set(c.Ctx.Application().ConfigurationReadOnly().GetViewDataContextKey(), + map[string]interface{}{ + "TestModel": Model{Username: username}, + "myModel": Model{Username: username + "2"}, + }) } func writeModels(ctx context.Context, names ...string) { @@ -260,13 +224,14 @@ func writeModels(ctx context.Context, names ...string) { } } -func (c *testControllerModel) EndRequest(ctx context.Context) { - writeModels(ctx, "myModel", "TestModel2") - c.Controller.EndRequest(ctx) +func (c *testControllerEndRequestAwareness) BeginRequest(ctx context.Context) {} +func (c *testControllerEndRequestAwareness) EndRequest(ctx context.Context) { + writeModels(ctx, "TestModel", "myModel") } -func TestControllerModel(t *testing.T) { + +func TestControllerEndRequestAwareness(t *testing.T) { app := iris.New() - app.Controller("/model/{username}", new(testControllerModel)) + New(app.Party("/era/{username}")).Handle(new(testControllerEndRequestAwareness)) e := httptest.New(t, app) usernames := []string{ @@ -275,7 +240,7 @@ func TestControllerModel(t *testing.T) { } for _, username := range usernames { - e.GET("/model/" + username).Expect().Status(iris.StatusOK). + e.GET("/era/" + username).Expect().Status(iris.StatusOK). Body().Equal(username + username + "2") } } @@ -285,7 +250,8 @@ type testBindType struct { } type testControllerBindStruct struct { - mvc.Controller + Ctx context.Context + // should start with upper letter of course TitlePointer *testBindType // should have the value of the "myTitlePtr" on test TitleValue testBindType // should have the value of the "myTitleV" on test @@ -296,6 +262,12 @@ func (t *testControllerBindStruct) Get() { t.Ctx.Writef(t.TitlePointer.title + t.TitleValue.title + t.Other) } +// test if context can be binded to the controller's function +// without need to declare it to a struct if not needed. +func (t *testControllerBindStruct) GetCtx(ctx iris.Context) { + ctx.StatusCode(iris.StatusContinue) +} + type testControllerBindDeep struct { testControllerBindStruct } @@ -304,94 +276,40 @@ func (t *testControllerBindDeep) Get() { // t.testControllerBindStruct.Get() t.Ctx.Writef(t.TitlePointer.title + t.TitleValue.title + t.Other) } -func TestControllerBind(t *testing.T) { + +func TestControllerDependencies(t *testing.T) { app := iris.New() + // app.Logger().SetLevel("debug") t1, t2 := "my pointer title", "val title" // test bind pointer to pointer of the correct type myTitlePtr := &testBindType{title: t1} // test bind value to value of the correct type myTitleV := testBindType{title: t2} - - app.Controller("/", new(testControllerBindStruct), myTitlePtr, myTitleV) - app.Controller("/deep", new(testControllerBindDeep), myTitlePtr, myTitleV) + m := New(app) + m.Register(myTitlePtr, myTitleV) + m.Handle(new(testControllerBindStruct)) + m.Clone(app.Party("/deep")).Handle(new(testControllerBindDeep)) e := httptest.New(t, app) expected := t1 + t2 e.GET("/").Expect().Status(iris.StatusOK). Body().Equal(expected) + e.GET("/ctx").Expect().Status(iris.StatusContinue) + e.GET("/deep").Expect().Status(iris.StatusOK). Body().Equal(expected) } -type ( - UserController struct{ mvc.Controller } - Profile struct{ mvc.Controller } - UserProfilePostController struct{ mvc.Controller } -) - -func writeRelatives(c mvc.Controller) { - c.Ctx.JSON(context.Map{ - "RelPath": c.RelPath(), - "TmplPath": c.RelTmpl(), - }) -} -func (c *UserController) Get() { - writeRelatives(c.Controller) -} - -func (c *Profile) Get() { - writeRelatives(c.Controller) -} - -func (c *UserProfilePostController) Get() { - writeRelatives(c.Controller) -} - -func TestControllerRelPathAndRelTmpl(t *testing.T) { - app := iris.New() - var tests = map[string]context.Map{ - // UserController - "/user": {"RelPath": "/", "TmplPath": "user/"}, - "/user/42": {"RelPath": "/42", "TmplPath": "user/"}, - "/user/me": {"RelPath": "/me", "TmplPath": "user/"}, - // Profile (without Controller suffix, should work as expected) - "/profile": {"RelPath": "/", "TmplPath": "profile/"}, - "/profile/42": {"RelPath": "/42", "TmplPath": "profile/"}, - "/profile/me": {"RelPath": "/me", "TmplPath": "profile/"}, - // UserProfilePost - "/user/profile/post": {"RelPath": "/", "TmplPath": "user/profile/post/"}, - "/user/profile/post/42": {"RelPath": "/42", "TmplPath": "user/profile/post/"}, - "/user/profile/post/mine": {"RelPath": "/mine", "TmplPath": "user/profile/post/"}, - } - - app.Controller("/user /user/me /user/{id}", - new(UserController)) - - app.Controller("/profile /profile/me /profile/{id}", - new(Profile)) - - app.Controller("/user/profile/post /user/profile/post/mine /user/profile/post/{id}", - new(UserProfilePostController)) - - e := httptest.New(t, app) - for path, tt := range tests { - e.GET(path).Expect().Status(iris.StatusOK).JSON().Equal(tt) - } -} - type testCtrl0 struct { testCtrl00 } -func (c *testCtrl0) Get() { - username := c.Params.Get("username") - c.Model = Model{Username: username} +func (c *testCtrl0) Get() string { + return c.Ctx.Params().Get("username") } func (c *testCtrl0) EndRequest(ctx context.Context) { - writeModels(ctx, "myModel") - if c.TitlePointer == nil { ctx.Writef("\nTitlePointer is nil!\n") } else { @@ -403,9 +321,9 @@ func (c *testCtrl0) EndRequest(ctx context.Context) { } type testCtrl00 struct { - testCtrl000 + Ctx context.Context - Model Model `iris:"model" name:"myModel"` + testCtrl000 } type testCtrl000 struct { @@ -415,9 +333,9 @@ type testCtrl000 struct { } type testCtrl0000 struct { - mvc.Controller } +func (c *testCtrl0000) BeginRequest(ctx context.Context) {} func (c *testCtrl0000) EndRequest(ctx context.Context) { ctx.Writef("finish") } @@ -430,20 +348,20 @@ func TestControllerInsideControllerRecursively(t *testing.T) { ) app := iris.New() - - app.Controller("/user/{username}", new(testCtrl0), - &testBindType{title: title}) + m := New(app.Party("/user/{username}")) + m.Register(&testBindType{title: title}) + m.Handle(new(testCtrl0)) e := httptest.New(t, app) e.GET("/user/" + username).Expect(). Status(iris.StatusOK).Body().Equal(expected) } -type testControllerRelPathFromFunc struct{ mvc.Controller } +type testControllerRelPathFromFunc struct{} +func (c *testControllerRelPathFromFunc) BeginRequest(ctx context.Context) {} func (c *testControllerRelPathFromFunc) EndRequest(ctx context.Context) { ctx.Writef("%s:%s", ctx.Method(), ctx.Path()) - c.Controller.EndRequest(ctx) } func (c *testControllerRelPathFromFunc) Get() {} @@ -464,7 +382,7 @@ func (c *testControllerRelPathFromFunc) GetSomethingByElseThisBy(bool, int) {} / func TestControllerRelPathFromFunc(t *testing.T) { app := iris.New() - app.Controller("/", new(testControllerRelPathFromFunc)) + New(app).Handle(new(testControllerRelPathFromFunc)) e := httptest.New(t, app) e.GET("/").Expect().Status(iris.StatusOK). @@ -497,34 +415,81 @@ func TestControllerRelPathFromFunc(t *testing.T) { Body().Equal("GET:/42") e.GET("/anything/here").Expect().Status(iris.StatusOK). Body().Equal("GET:/anything/here") + } type testControllerActivateListener struct { - mvc.Controller - TitlePointer *testBindType } -func (c *testControllerActivateListener) OnActivate(p *activator.ActivatePayload) { - p.EnsureBindValue(&testBindType{ - title: "default title", - }) +func (c *testControllerActivateListener) BeforeActivation(b BeforeActivation) { + b.Dependencies().AddOnce(&testBindType{title: "default title"}) } -func (c *testControllerActivateListener) Get() { - c.Text = c.TitlePointer.title +func (c *testControllerActivateListener) Get() string { + return c.TitlePointer.title } func TestControllerActivateListener(t *testing.T) { app := iris.New() - app.Controller("/", new(testControllerActivateListener)) - app.Controller("/manual", new(testControllerActivateListener), &testBindType{ + New(app).Handle(new(testControllerActivateListener)) + m := New(app) + m.Register(&testBindType{ title: "my title", }) + m.Party("/manual").Handle(new(testControllerActivateListener)) + // or + m.Party("/manual2").Handle(&testControllerActivateListener{ + TitlePointer: &testBindType{ + title: "my title", + }, + }) e := httptest.New(t, app) e.GET("/").Expect().Status(iris.StatusOK). Body().Equal("default title") e.GET("/manual").Expect().Status(iris.StatusOK). Body().Equal("my title") + e.GET("/manual2").Expect().Status(iris.StatusOK). + Body().Equal("my title") +} + +type testControllerNotCreateNewDueManuallySettingAllFields struct { + T *testing.T + + TitlePointer *testBindType +} + +func (c *testControllerNotCreateNewDueManuallySettingAllFields) AfterActivation(a AfterActivation) { + if n := a.DependenciesReadOnly().Len(); n != 2 { + c.T.Fatalf(`expecting 2 dependency, the 'T' and the 'TitlePointer' that we manually insert + and the fields total length is 2 so it will not create a new controller on each request + however the dependencies are available here + although the struct injector is being ignored when + creating the controller's handlers because we set it to invalidate state at "newControllerActivator" + -- got dependencies length: %d`, n) + } + + if !a.Singleton() { + c.T.Fatalf(`this controller should be tagged as Singleton. It shouldn't be tagged used as request scoped(create new instances on each request), + it doesn't contain any dynamic value or dependencies that should be binded via the iris mvc engine`) + } +} + +func (c *testControllerNotCreateNewDueManuallySettingAllFields) Get() string { + return c.TitlePointer.title +} + +func TestControllerNotCreateNewDueManuallySettingAllFields(t *testing.T) { + app := iris.New() + New(app).Handle(&testControllerNotCreateNewDueManuallySettingAllFields{ + T: t, + TitlePointer: &testBindType{ + title: "my title", + }, + }) + + e := httptest.New(t, app) + e.GET("/").Expect().Status(iris.StatusOK). + Body().Equal("my title") } diff --git a/mvc/go19.go b/mvc/go19.go index 8473fa6931..ab83472780 100644 --- a/mvc/go19.go +++ b/mvc/go19.go @@ -2,26 +2,20 @@ package mvc -import ( - "html/template" - - "github.com/kataras/iris/mvc/activator" -) +import "github.com/kataras/iris/hero" type ( - // HTML wraps the "s" with the template.HTML - // in order to be marked as safe content, to be rendered as html and not escaped. - HTML = template.HTML - // ActivatePayload contains the necessary information and the ability - // to alt a controller's registration options, i.e the binder. - // - // With `ActivatePayload` the `Controller` can register custom routes - // or modify the provided values that will be binded to the - // controller later on. - // - // Look the `mvc/activator#ActivatePayload` for its implementation. - // - // A shortcut for the `mvc/activator#ActivatePayload`, useful when `OnActivate` is being used. - ActivatePayload = activator.ActivatePayload + // Result is a type alias for the `hero#Result`, useful for output controller's methods. + Result = hero.Result + // Response is a type alias for the `hero#Response`, useful for output controller's methods. + Response = hero.Response + // View is a type alias for the `hero#View`, useful for output controller's methods. + View = hero.View +) + +var ( + // Try is a type alias for the `hero#Try`, + // useful to return a result based on two cases: failure(including panics) and a succeess. + Try = hero.Try ) diff --git a/mvc/method_result.go b/mvc/method_result.go deleted file mode 100644 index 7512ff59be..0000000000 --- a/mvc/method_result.go +++ /dev/null @@ -1,58 +0,0 @@ -package mvc - -import ( - "github.com/kataras/iris/context" - "github.com/kataras/iris/mvc/activator/methodfunc" -) - -// build go1.9 only(go19.go)--> -// // Result is a response dispatcher. -// // All types that complete this interface -// // can be returned as values from the method functions. -// Result = methodfunc.Result -// <-- -// No, let's just copy-paste in order to go 1.8 users have this type -// easy to be used from the root mvc package, -// sometimes duplication doesn't hurt. - -// Result is a response dispatcher. -// All types that complete this interface -// can be returned as values from the method functions. -// -// Example at: https://github.com/kataras/iris/tree/master/_examples/mvc/overview. -type Result interface { // NOTE: Should be always compatible with the methodfunc.Result. - // Dispatch should sends the response to the context's response writer. - Dispatch(ctx context.Context) -} - -var defaultFailureResponse = Response{Code: methodfunc.DefaultErrStatusCode} - -// Try will check if "fn" ran without any panics, -// using recovery, -// and return its result as the final response -// otherwise it returns the "failure" response if any, -// if not then a 400 bad request is being sent. -// -// Example usage at: https://github.com/kataras/iris/blob/master/mvc/method_result_test.go. -func Try(fn func() Result, failure ...Result) Result { - var failed bool - var actionResponse Result - - func() { - defer func() { - if rec := recover(); rec != nil { - failed = true - } - }() - actionResponse = fn() - }() - - if failed { - if len(failure) > 0 { - return failure[0] - } - return defaultFailureResponse - } - - return actionResponse -} diff --git a/mvc/method_result_response.go b/mvc/method_result_response.go deleted file mode 100644 index 4c11cdaabc..0000000000 --- a/mvc/method_result_response.go +++ /dev/null @@ -1,69 +0,0 @@ -package mvc - -import ( - "github.com/kataras/iris/context" - "github.com/kataras/iris/mvc/activator/methodfunc" -) - -// Response completes the `methodfunc.Result` interface. -// It's being used as an alternative return value which -// wraps the status code, the content type, a content as bytes or as string -// and an error, it's smart enough to complete the request and send the correct response to the client. -type Response struct { - Code int - ContentType string - Content []byte - - // if not empty then content type is the text/plain - // and content is the text as []byte. - Text string - // If not nil then it will fire that as "application/json" or the - // "ContentType" if not empty. - Object interface{} - - // If Path is not empty then it will redirect - // the client to this Path, if Code is >= 300 and < 400 - // then it will use that Code to do the redirection, otherwise - // StatusFound(302) or StatusSeeOther(303) for post methods will be used. - // Except when err != nil. - Path string - - // if not empty then fire a 400 bad request error - // unless the Status is > 200, then fire that error code - // with the Err.Error() string as its content. - // - // if Err.Error() is empty then it fires the custom error handler - // if any otherwise the framework sends the default http error text based on the status. - Err error - Try func() int - - // if true then it skips everything else and it throws a 404 not found error. - // Can be named as Failure but NotFound is more precise name in order - // to be visible that it's different than the `Err` - // because it throws a 404 not found instead of a 400 bad request. - // NotFound bool - // let's don't add this yet, it has its dangerous of missuse. -} - -var _ methodfunc.Result = Response{} - -// Dispatch writes the response result to the context's response writer. -func (r Response) Dispatch(ctx context.Context) { - if r.Path != "" && r.Err == nil { - // it's not a redirect valid status - if r.Code < 300 || r.Code >= 400 { - if ctx.Method() == "POST" { - r.Code = 303 // StatusSeeOther - } - r.Code = 302 // StatusFound - } - ctx.Redirect(r.Path, r.Code) - return - } - - if s := r.Text; s != "" { - r.Content = []byte(s) - } - - methodfunc.DispatchCommon(ctx, r.Code, r.ContentType, r.Content, r.Object, r.Err, true) -} diff --git a/mvc/method_result_view.go b/mvc/method_result_view.go deleted file mode 100644 index 59742fb267..0000000000 --- a/mvc/method_result_view.go +++ /dev/null @@ -1,104 +0,0 @@ -package mvc - -import ( - "strings" - - "github.com/kataras/iris/context" - "github.com/kataras/iris/mvc/activator/methodfunc" - - "github.com/fatih/structs" -) - -// View completes the `methodfunc.Result` interface. -// It's being used as an alternative return value which -// wraps the template file name, layout, (any) view data, status code and error. -// It's smart enough to complete the request and send the correct response to the client. -// -// Example at: https://github.com/kataras/iris/blob/master/_examples/mvc/overview/web/controllers/hello_controller.go. -type View struct { - Name string - Layout string - Data interface{} // map or a custom struct. - Code int - Err error -} - -var _ methodfunc.Result = View{} - -const dotB = byte('.') - -// DefaultViewExt is the default extension if `view.Name `is missing, -// but note that it doesn't care about -// the app.RegisterView(iris.$VIEW_ENGINE("./$dir", "$ext"))'s $ext. -// so if you don't use the ".html" as extension for your files -// you have to append the extension manually into the `view.Name` -// or change this global variable. -var DefaultViewExt = ".html" - -func ensureExt(s string) string { - if len(s) == 0 { - return "index" + DefaultViewExt - } - - if strings.IndexByte(s, dotB) < 1 { - s += DefaultViewExt - } - - return s -} - -// Dispatch writes the template filename, template layout and (any) data to the client. -// Completes the `Result` interface. -func (r View) Dispatch(ctx context.Context) { // r as Response view. - if r.Err != nil { - if r.Code < 400 { - r.Code = methodfunc.DefaultErrStatusCode - } - ctx.StatusCode(r.Code) - ctx.WriteString(r.Err.Error()) - ctx.StopExecution() - return - } - - if r.Code > 0 { - ctx.StatusCode(r.Code) - } - - if r.Name != "" { - r.Name = ensureExt(r.Name) - - if r.Layout != "" { - r.Layout = ensureExt(r.Layout) - ctx.ViewLayout(r.Layout) - } - - if r.Data != nil { - // In order to respect any c.Ctx.ViewData that may called manually before; - dataKey := ctx.Application().ConfigurationReadOnly().GetViewDataContextKey() - if ctx.Values().Get(dataKey) == nil { - // if no c.Ctx.ViewData then it's empty do a - // pure set, it's faster. - ctx.Values().Set(dataKey, r.Data) - } else { - // else check if r.Data is map or struct, if struct convert it to map, - // do a range loop and set the data one by one. - // context.Map is actually a map[string]interface{} but we have to make that check; - if m, ok := r.Data.(map[string]interface{}); ok { - setViewData(ctx, m) - } else if m, ok := r.Data.(context.Map); ok { - setViewData(ctx, m) - } else if structs.IsStruct(r.Data) { - setViewData(ctx, structs.Map(r)) - } - } - } - - ctx.View(r.Name) - } -} - -func setViewData(ctx context.Context, data map[string]interface{}) { - for k, v := range data { - ctx.ViewData(k, v) - } -} diff --git a/mvc/mvc.go b/mvc/mvc.go new file mode 100644 index 0000000000..f11e02d44d --- /dev/null +++ b/mvc/mvc.go @@ -0,0 +1,194 @@ +package mvc + +import ( + "github.com/kataras/iris/context" + "github.com/kataras/iris/core/router" + "github.com/kataras/iris/hero" + "github.com/kataras/iris/hero/di" +) + +var ( + // HeroDependencies let you share bindable dependencies between + // package-level hero's registered dependencies and all MVC instances that comes later. + // + // `hero.Register(...)` + // `myMVC := mvc.New(app.Party(...))` + // the "myMVC" registers the dependencies provided by the `hero.Register` func + // automatically. + // + // Set it to false to disable that behavior, you have to use the `mvc#Register` + // even if you had register dependencies with the `hero` package. + // + // Defaults to true. + HeroDependencies = true +) + +// Application is the high-level compoment of the "mvc" package. +// It's the API that you will be using to register controllers among with their +// dependencies that your controllers may expecting. +// It contains the Router(iris.Party) in order to be able to register +// template layout, middleware, done handlers as you used with the +// standard Iris APIBuilder. +// +// The Engine is created by the `New` method and it's the dependencies holder +// and controllers factory. +// +// See `mvc#New` for more. +type Application struct { + Dependencies di.Values + Router router.Party +} + +func newApp(subRouter router.Party, values di.Values) *Application { + return &Application{ + Router: subRouter, + Dependencies: values, + } +} + +// New returns a new mvc Application based on a "party". +// Application creates a new engine which is responsible for binding the dependencies +// and creating and activating the app's controller(s). +// +// Example: `New(app.Party("/todo"))` or `New(app)` as it's the same as `New(app.Party("/"))`. +func New(party router.Party) *Application { + values := di.NewValues() + if HeroDependencies { + values = hero.Dependencies().Clone() + } + return newApp(party, values) +} + +// Configure creates a new controller and configures it, +// this function simply calls the `New(party)` and its `.Configure(configurators...)`. +// +// A call of `mvc.New(app.Party("/path").Configure(buildMyMVC)` is equal to +// `mvc.Configure(app.Party("/path"), buildMyMVC)`. +// +// Read more at `New() Application` and `Application#Configure` methods. +func Configure(party router.Party, configurators ...func(*Application)) *Application { + // Author's Notes-> + // About the Configure's comment: +5 space to be shown in equal width to the previous or after line. + // + // About the Configure's design chosen: + // Yes, we could just have a `New(party, configurators...)` + // but I think the `New()` and `Configure(configurators...)` API seems more native to programmers, + // at least to me and the people I ask for their opinion between them. + // Because the `New()` can actually return something that can be fully configured without its `Configure`, + // its `Configure` is there just to design the apps better and help end-devs to split their code wisely. + return New(party).Configure(configurators...) +} + +// Configure can be used to pass one or more functions that accept this +// Application, use this to add dependencies and controller(s). +// +// Example: `New(app.Party("/todo")).Configure(func(mvcApp *mvc.Application){...})`. +func (app *Application) Configure(configurators ...func(*Application)) *Application { + for _, c := range configurators { + c(app) + } + return app +} + +// Register appends one or more values as dependencies. +// The value can be a single struct value-instance or a function +// which has one input and one output, the input should be +// an `iris.Context` and the output can be any type, that output type +// will be binded to the controller's field, if matching or to the +// controller's methods, if matching. +// +// These dependencies "values" can be changed per-controller as well, +// via controller's `BeforeActivation` and `AfterActivation` methods, +// look the `Handle` method for more. +// +// It returns this Application. +// +// Example: `.Register(loggerService{prefix: "dev"}, func(ctx iris.Context) User {...})`. +func (app *Application) Register(values ...interface{}) *Application { + app.Dependencies.Add(values...) + return app +} + +// Handle serves a controller for the current mvc application's Router. +// It accept any custom struct which its functions will be transformed +// to routes. +// +// If "controller" has `BeforeActivation(b mvc.BeforeActivation)` +// or/and `AfterActivation(a mvc.AfterActivation)` then these will be called between the controller's `.activate`, +// use those when you want to modify the controller before or/and after +// the controller will be registered to the main Iris Application. +// +// It returns this mvc Application. +// +// Usage: `.Handle(new(TodoController))`. +// +// Controller accepts a sub router and registers any custom struct +// as controller, if struct doesn't have any compatible methods +// neither are registered via `ControllerActivator`'s `Handle` method +// then the controller is not registered at all. +// +// A Controller may have one or more methods +// that are wrapped to a handler and registered as routes before the server ran. +// The controller's method can accept any input argument that are previously binded +// via the dependencies or route's path accepts dynamic path parameters. +// The controller's fields are also bindable via the dependencies, either a +// static value (service) or a function (dynamically) which accepts a context +// and returns a single value (this type is being used to find the relative field or method's input argument). +// +// func(c *ExampleController) Get() string | +// (string, string) | +// (string, int) | +// int | +// (int, string | +// (string, error) | +// bool | +// (any, bool) | +// error | +// (int, error) | +// (customStruct, error) | +// customStruct | +// (customStruct, int) | +// (customStruct, string) | +// Result or (Result, error) +// where Get is an HTTP Method func. +// +// Examples at: https://github.com/kataras/iris/tree/master/_examples/mvc +func (app *Application) Handle(controller interface{}) *Application { + // initialize the controller's activator, nothing too magical so far. + c := newControllerActivator(app.Router, controller, app.Dependencies) + + // check the controller's "BeforeActivation" or/and "AfterActivation" method(s) between the `activate` + // call, which is simply parses the controller's methods, end-dev can register custom controller's methods + // by using the BeforeActivation's (a ControllerActivation) `.Handle` method. + if before, ok := controller.(interface { + BeforeActivation(BeforeActivation) + }); ok { + before.BeforeActivation(c) + } + + c.activate() + + if after, okAfter := controller.(interface { + AfterActivation(AfterActivation) + }); okAfter { + after.AfterActivation(c) + } + return app +} + +// Clone returns a new mvc Application which has the dependencies +// of the current mvc Mpplication's dependencies. +// +// Example: `.Clone(app.Party("/path")).Handle(new(TodoSubController))`. +func (app *Application) Clone(party router.Party) *Application { + return newApp(party, app.Dependencies.Clone()) +} + +// Party returns a new child mvc Application based on the current path + "relativePath". +// The new mvc Application has the same dependencies of the current mvc Application, +// until otherwise specified later manually. +// +// The router's root path of this child will be the current mvc Application's root path + "relativePath". +func (app *Application) Party(relativePath string, middleware ...context.Handler) *Application { + return app.Clone(app.Router.Party(relativePath, middleware...)) +} diff --git a/mvc/param.go b/mvc/param.go new file mode 100644 index 0000000000..8b680aeef6 --- /dev/null +++ b/mvc/param.go @@ -0,0 +1,63 @@ +package mvc + +import ( + "reflect" + + "github.com/kataras/iris/context" + "github.com/kataras/iris/core/router/macro" + "github.com/kataras/iris/core/router/macro/interpreter/ast" +) + +func getPathParamsForInput(params []macro.TemplateParam, funcIn ...reflect.Type) (values []reflect.Value) { + if len(funcIn) == 0 || len(params) == 0 { + return + } + + consumedParams := make(map[int]bool, 0) + for _, in := range funcIn { + for j, p := range params { + if _, consumed := consumedParams[j]; consumed { + continue + } + paramType := p.Type + paramName := p.Name + // fmt.Printf("%s input arg type vs %s param type\n", in.Kind().String(), p.Type.Kind().String()) + if paramType.Assignable(in.Kind()) { + consumedParams[j] = true + // fmt.Printf("param.go: bind path param func for paramName = '%s' and paramType = '%s'\n", paramName, paramType.String()) + values = append(values, makeFuncParamGetter(paramType, paramName)) + } + } + } + + return +} + +func makeFuncParamGetter(paramType ast.ParamType, paramName string) reflect.Value { + var fn interface{} + + switch paramType { + case ast.ParamTypeInt: + fn = func(ctx context.Context) int { + v, _ := ctx.Params().GetInt(paramName) + return v + } + case ast.ParamTypeLong: + fn = func(ctx context.Context) int64 { + v, _ := ctx.Params().GetInt64(paramName) + return v + } + case ast.ParamTypeBoolean: + fn = func(ctx context.Context) bool { + v, _ := ctx.Params().GetBool(paramName) + return v + } + default: + // string, path... + fn = func(ctx context.Context) string { + return ctx.Params().Get(paramName) + } + } + + return reflect.ValueOf(fn) +} diff --git a/mvc/reflect.go b/mvc/reflect.go new file mode 100644 index 0000000000..1b13bbe614 --- /dev/null +++ b/mvc/reflect.go @@ -0,0 +1,18 @@ +package mvc + +import "reflect" + +var baseControllerTyp = reflect.TypeOf((*BaseController)(nil)).Elem() + +func isBaseController(ctrlTyp reflect.Type) bool { + return ctrlTyp.Implements(baseControllerTyp) +} + +func getInputArgsFromFunc(funcTyp reflect.Type) []reflect.Type { + n := funcTyp.NumIn() + funcIn := make([]reflect.Type, n, n) + for i := 0; i < n; i++ { + funcIn[i] = funcTyp.In(i) + } + return funcIn +} diff --git a/mvc/session_controller.go b/mvc/session_controller.go index 06c17f6afd..a4885fa5c7 100644 --- a/mvc/session_controller.go +++ b/mvc/session_controller.go @@ -2,40 +2,39 @@ package mvc import ( "github.com/kataras/iris/context" - "github.com/kataras/iris/mvc/activator" "github.com/kataras/iris/sessions" - - "github.com/kataras/golog" ) -var defaultManager = sessions.New(sessions.Config{}) +var defaultSessionManager = sessions.New(sessions.Config{}) // SessionController is a simple `Controller` implementation // which requires a binded session manager in order to give // direct access to the current client's session via its `Session` field. +// +// SessionController is deprecated please use the new dependency injection's methods instead, +// i.e `mvcApp.Register(sessions.New(sessions.Config{}).Start)`. +// It's more controlled by you, +// also *sessions.Session type can now `Destroy` itself without the need of the manager, embrace it. type SessionController struct { - Controller - Manager *sessions.Sessions Session *sessions.Session } -// OnActivate called, once per application lifecycle NOT request, +// BeforeActivation called, once per application lifecycle NOT request, // every single time the dev registers a specific SessionController-based controller. // It makes sure that its "Manager" field is filled -// even if the caller didn't provide any sessions manager via the `app.Controller` function. -func (s *SessionController) OnActivate(p *activator.ActivatePayload) { - if p.EnsureBindValue(defaultManager) { - golog.Warnf(`MVC SessionController: couldn't find any "*sessions.Sessions" bindable value to fill the "Manager" field, -therefore this controller is using the default sessions manager instead. -Please refer to the documentation to learn how you can provide the session manager`) +// even if the caller didn't provide any sessions manager via the MVC's Application's `Handle` function. +func (s *SessionController) BeforeActivation(b BeforeActivation) { + if didntBindManually := b.Dependencies().AddOnce(defaultSessionManager); didntBindManually { + b.Router().GetReporter().Add( + `MVC SessionController: couldn't find any "*sessions.Sessions" bindable value to fill the "Manager" field, + therefore this controller is using the default sessions manager instead. + Please refer to the documentation to learn how you can provide the session manager`) } } -// BeginRequest calls the Controller's BeginRequest -// and tries to initialize the current user's Session. +// BeginRequest initializes the current user's Session. func (s *SessionController) BeginRequest(ctx context.Context) { - s.Controller.BeginRequest(ctx) if s.Manager == nil { ctx.Application().Logger().Errorf(`MVC SessionController: sessions manager is nil, report this as a bug because the SessionController should predict this on its activation state and use a default one automatically`) @@ -44,3 +43,6 @@ because the SessionController should predict this on its activation state and us s.Session = s.Manager.Start(ctx) } + +// EndRequest is here to complete the `BaseController`. +func (s *SessionController) EndRequest(ctx context.Context) {} diff --git a/mvc/strutil.go b/mvc/strutil.go deleted file mode 100644 index 686da607b0..0000000000 --- a/mvc/strutil.go +++ /dev/null @@ -1,38 +0,0 @@ -package mvc - -import ( - "strings" - "unicode" -) - -func findCtrlWords(ctrlName string) (w []string) { - end := len(ctrlName) - start := -1 - for i, n := 0, end; i < n; i++ { - c := rune(ctrlName[i]) - if unicode.IsUpper(c) { - // it doesn't count the last uppercase - if start != -1 { - end = i - w = append(w, strings.ToLower(ctrlName[start:end])) - } - start = i - continue - } - end = i + 1 - - } - - // We can't omit the last name, we have to take it. - // because of controller names like - // "UserProfile", we need to return "user", "profile" - // if "UserController", we need to return "user" - // if "User", we need to return "user". - last := ctrlName[start:end] - if last == ctrlSuffix { - return - } - - w = append(w, strings.ToLower(last)) - return -} diff --git a/mvc/strutil_test.go b/mvc/strutil_test.go deleted file mode 100644 index 34648ed4b3..0000000000 --- a/mvc/strutil_test.go +++ /dev/null @@ -1,31 +0,0 @@ -package mvc - -import ( - "testing" -) - -func TestFindCtrlWords(t *testing.T) { - var tests = map[string][]string{ - "UserController": {"user"}, - "UserPostController": {"user", "post"}, - "ProfileController": {"profile"}, - "UserProfileController": {"user", "profile"}, - "UserProfilePostController": {"user", "profile", "post"}, - "UserProfile": {"user", "profile"}, - "Profile": {"profile"}, - "User": {"user"}, - } - - for ctrlName, expected := range tests { - words := findCtrlWords(ctrlName) - if len(expected) != len(words) { - t.Fatalf("expected words and return don't have the same length: [%d] != [%d] | '%s' != '%s'", - len(expected), len(words), expected, words) - } - for i, w := range words { - if expected[i] != w { - t.Fatalf("expected word is not equal with the return one: '%s' != '%s'", expected[i], w) - } - } - } -} diff --git a/sessions/LICENSE b/sessions/LICENSE index 74d791643e..6a39906b6d 100644 --- a/sessions/LICENSE +++ b/sessions/LICENSE @@ -1,4 +1,4 @@ -Copyright (c) 2017 The Iris Sessions Authors. All rights reserved. +Copyright (c) 2017-2018 The Iris Sessions Authors. All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are diff --git a/sessions/config.go b/sessions/config.go index ecfd99a69c..29181d8f82 100644 --- a/sessions/config.go +++ b/sessions/config.go @@ -39,7 +39,7 @@ type Encoding interface { } type ( - // Config is the configuration for sessions. Please review it well before using sessions. + // Config is the configuration for sessions. Please read it before using sessions. Config struct { // Cookie string, the session's client cookie name, for example: "mysessionid" // @@ -89,6 +89,8 @@ type ( // Encoding same as Encode and Decode but receives a single instance which // completes the "CookieEncoder" interface, `Encode` and `Decode` functions. + // + // Defaults to nil. Encoding Encoding // Expires the duration of which the cookie must expires (created_time.Add(Expires)). diff --git a/sessions/session.go b/sessions/session.go index 3d9d46c87b..a78295fe55 100644 --- a/sessions/session.go +++ b/sessions/session.go @@ -31,6 +31,18 @@ type ( } ) +// Destroy destroys this session, it removes its session values and any flashes. +// This session entry will be removed from the server, +// the registered session databases will be notified for this deletion as well. +// +// Note that this method does NOT remove the client's cookie, although +// it should be reseted if new session is attached to that (client). +// +// Use the session's manager `Destroy(ctx)` in order to remove the cookie as well. +func (s *Session) Destroy() { + s.provider.deleteSession(s) +} + // ID returns the session's ID. func (s *Session) ID() string { return s.sid @@ -120,6 +132,14 @@ func (s *Session) GetStringDefault(key string, defaultValue string) string { if v, ok := value.(string); ok { return v } + + if v, ok := value.(int); ok { + return strconv.Itoa(v) + } + + if v, ok := value.(int64); ok { + return strconv.FormatInt(v, 10) + } } return defaultValue @@ -167,6 +187,26 @@ func (s *Session) GetIntDefault(key string, defaultValue int) (int, error) { return defaultValue, errFindParse.Format("int", key, v) } +// Increment increments the stored int value saved as "key" by +"n". +// If value doesn't exist on that "key" then it creates one with the "n" as its value. +// It returns the new, incremented, value. +func (s *Session) Increment(key string, n int) (newValue int) { + newValue, _ = s.GetIntDefault(key, 0) + newValue += n + s.Set(key, newValue) + return +} + +// Decrement decrements the stored int value saved as "key" by -"n". +// If value doesn't exist on that "key" then it creates one with the "n" as its value. +// It returns the new, decremented, value even if it's less than zero. +func (s *Session) Decrement(key string, n int) (newValue int) { + newValue, _ = s.GetIntDefault(key, 0) + newValue -= n + s.Set(key, newValue) + return +} + // GetInt64 same as `Get` but returns its int64 representation, // if key doesn't exist then it returns -1. func (s *Session) GetInt64(key string) (int64, error) { @@ -337,7 +377,7 @@ func (s *Session) set(key string, value interface{}, immutable bool) { syncDatabases(s.provider.databases, p) } -// Set fills the session with an entry"value", based on its "key". +// Set fills the session with an entry "value", based on its "key". func (s *Session) Set(key string, value interface{}) { s.set(key, value, false) } diff --git a/typescript/LICENSE b/typescript/LICENSE index bf0ef63712..ec4015993a 100644 --- a/typescript/LICENSE +++ b/typescript/LICENSE @@ -1,4 +1,4 @@ -Copyright (c) 2017 The Iris Typescript Authors. All rights reserved. +Copyright (c) 2017-2018 The Iris Typescript Authors. All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are diff --git a/websocket/LICENSE b/websocket/LICENSE index 08f2312966..47aea48dc5 100644 --- a/websocket/LICENSE +++ b/websocket/LICENSE @@ -1,4 +1,4 @@ -Copyright (c) 2017 The Iris Websocket Authors. All rights reserved. +Copyright (c) 2017-2018 The Iris Websocket Authors. All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are diff --git a/websocket/client.go b/websocket/client.go index 77d0696225..771e5eb8d7 100644 --- a/websocket/client.go +++ b/websocket/client.go @@ -105,8 +105,9 @@ var Ws = (function () { t = websocketJSONMessageType; m = JSON.stringify(data); } - else { - console.log("Invalid, javascript-side should contains an empty second parameter."); + else if (data !== null && typeof(data) !== "undefined" ) { + // if it has a second parameter but it's not a type we know, then fire this: + console.log("unsupported type of input argument passed, try to not include this argument to the 'Emit'"); } return this._msg(event, t, m); }; diff --git a/websocket/client.ts b/websocket/client.ts index b4b384135c..98392492d3 100644 --- a/websocket/client.ts +++ b/websocket/client.ts @@ -106,8 +106,9 @@ class Ws { //propably json-object t = websocketJSONMessageType; m = JSON.stringify(data); - } else { - console.log("Invalid, javascript-side should contains an empty second parameter."); + } else if (data !== null && typeof (data) !== "undefined") { + // if it has a second parameter but it's not a type we know, then fire this: + console.log("unsupported type of input argument passed, try to not include this argument to the 'Emit'"); } return this._msg(event, t, m); diff --git a/websocket/config.go b/websocket/config.go index a356eb509a..4280c8e9ce 100644 --- a/websocket/config.go +++ b/websocket/config.go @@ -41,8 +41,11 @@ type Config struct { // 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) + // Error is the function that will be fired if any client couldn't upgrade the HTTP connection + // to a websocket connection, a handshake error. + Error func(w http.ResponseWriter, r *http.Request, status int, reason error) + // CheckOrigin a function that is called right before the handshake, + // if returns false then that client is not allowed to connect with the websocket server. CheckOrigin func(r *http.Request) bool // HandshakeTimeout specifies the duration for the handshake to complete. HandshakeTimeout time.Duration @@ -57,7 +60,7 @@ type Config struct { // 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. + // PingPeriod send ping messages to the connection within this period. Must be less than PongTimeout. // Default value is 60 *time.Second PingPeriod time.Duration // MaxMessageSize max message size allowed from connection. @@ -65,18 +68,20 @@ type Config struct { 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 + // Default value is false BinaryMessages bool - // ReadBufferSize is the buffer size for the underline reader + // ReadBufferSize is the buffer size for the connection reader. // Default value is 4096 ReadBufferSize int - // WriteBufferSize is the buffer size for the underline writer + // WriteBufferSize is the buffer size for the connection 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. + // + // Defauls to false and it should be remain as it is, unless special requirements. EnableCompression bool // Subprotocols specifies the server's supported protocols in order of diff --git a/websocket/connection.go b/websocket/connection.go index 9f18dcbd09..ce887e95a6 100644 --- a/websocket/connection.go +++ b/websocket/connection.go @@ -67,8 +67,8 @@ func (r *ConnectionValues) Reset() { *r = (*r)[:0] } -// UnderlineConnection is used for compatible with fasthttp and net/http underline websocket libraries -// we only need ~8 funcs from websocket.Conn so: +// UnderlineConnection is the underline connection, nothing to think about, +// it's used internally mostly but can be used for extreme cases with other libraries. type UnderlineConnection interface { // SetWriteDeadline sets the write deadline on the underlying network // connection. After a write has timed out, the websocket state is corrupt and @@ -118,13 +118,13 @@ type UnderlineConnection interface { // ------------------------------------------------------------------------------------- type ( - // DisconnectFunc is the callback which fires when a client/connection closed + // DisconnectFunc is the callback which is fired when a client/connection closed DisconnectFunc func() - // LeaveRoomFunc is the callback which fires when a client/connection leaves from any room. + // LeaveRoomFunc is the callback which is fired when a client/connection leaves from any room. // This is called automatically when client/connection disconnected // (because websocket server automatically leaves from all joined rooms) LeaveRoomFunc func(roomName string) - // ErrorFunc is the callback which fires when an error happens + // ErrorFunc is the callback which fires whenever an error occurs ErrorFunc (func(string)) // NativeMessageFunc is the callback for native websocket messages, receives one []byte parameter which is the raw client's message NativeMessageFunc func([]byte) @@ -137,6 +137,9 @@ type ( Connection interface { // Emitter implements EmitMessage & Emit Emitter + // Err is not nil if the upgrader failed to upgrade http to websocket connection. + Err() error + // ID returns the connection's identifier ID() string @@ -152,26 +155,26 @@ type ( // then you use it to receive user information, for example: from headers Context() context.Context - // OnDisconnect registers a callback which fires when this connection is closed by an error or manual + // OnDisconnect registers a callback which is fired when this connection is closed by an error or manual OnDisconnect(DisconnectFunc) // OnError registers a callback which fires when this connection occurs an error OnError(ErrorFunc) // OnPing registers a callback which fires on each ping OnPing(PingFunc) - // FireStatusCode can be used to send a custom error message to the connection + // FireOnError can be used to send a custom error message to the connection // - // It does nothing more than firing the OnError listeners. It doesn't sends anything to the client. + // It does nothing more than firing the OnError listeners. It doesn't send anything to the client. FireOnError(errorMessage string) - // To defines where server should send a message - // returns an emitter to send messages + // To defines on what "room" (see Join) the server should send a message + // returns an Emmiter(`EmitMessage` & `Emit`) to send messages. To(string) Emitter // OnMessage registers a callback which fires when native websocket message received OnMessage(NativeMessageFunc) - // On registers a callback to a particular event which fires when a message to this event received + // On registers a callback to a particular event which is fired when a message to this event is received On(string, MessageFunc) - // Join join a connection to a room, it doesn't check if connection is already there, so care + // Join registers this connection to a room, if it doesn't exist then it creates a new. One room can have one or more connections. One connection can be joined to many rooms. All connections are joined to a room specified by their `ID` automatically. Join(string) - // Leave removes a connection from a room + // Leave removes this connection entry from a room // Returns true if the connection has actually left from the particular room. Leave(string) bool // OnLeave registers a callback which fires when this connection left from any joined room. @@ -181,6 +184,11 @@ type ( // Note: the callback(s) called right before the server deletes the connection from the room // so the connection theoretical can still send messages to its room right before it is being disconnected. OnLeave(roomLeaveCb LeaveRoomFunc) + // Wait starts the pinger and the messages reader, + // it's named as "Wait" because it should be called LAST, + // after the "On" events IF server's `Upgrade` is used, + // otherise you don't have to call it because the `Handler()` does it automatically. + Wait() // Disconnect disconnects the client, close the underline websocket conn and removes it from the conn list // returns the error, if any, from the underline connection Disconnect() error @@ -197,6 +205,7 @@ type ( } connection struct { + err error underline UnderlineConnection id string messageType int @@ -207,6 +216,7 @@ type ( onPingListeners []PingFunc onNativeMessageListeners []NativeMessageFunc onEventListeners map[string][]MessageFunc + started bool // these were maden for performance only self Emitter // pre-defined emitter than sends message to its self client broadcast Emitter // pre-defined emitter that sends message to all except this @@ -237,6 +247,7 @@ func newConnection(ctx context.Context, s *Server, underlineConn UnderlineConnec onErrorListeners: make([]ErrorFunc, 0), onNativeMessageListeners: make([]NativeMessageFunc, 0), onEventListeners: make(map[string][]MessageFunc, 0), + started: false, ctx: ctx, server: s, } @@ -252,6 +263,11 @@ func newConnection(ctx context.Context, s *Server, underlineConn UnderlineConnec return c } +// Err is not nil if the upgrader failed to upgrade http to websocket connection. +func (c *connection) Err() error { + return c.err +} + // write writes a raw websocket message with a specific type to the client // used by ping messages and any CloseMessage types. func (c *connection) write(websocketMessageType int, data []byte) error { @@ -322,6 +338,13 @@ func (c *connection) startPinger() { }() } +func (c *connection) fireOnPing() { + // fire the onPingListeners + for i := range c.onPingListeners { + c.onPingListeners[i]() + } +} + func (c *connection) startReader() { conn := c.underline hasReadTimeout := c.server.config.ReadTimeout > 0 @@ -503,11 +526,20 @@ func (c *connection) fireOnLeave(roomName string) { } } -func (c *connection) fireOnPing() { - // fire the onPingListeners - for i := range c.onPingListeners { - c.onPingListeners[i]() +// Wait starts the pinger and the messages reader, +// it's named as "Wait" because it should be called LAST, +// after the "On" events IF server's `Upgrade` is used, +// otherise you don't have to call it because the `Handler()` does it automatically. +func (c *connection) Wait() { + if c.started { + return } + c.started = true + // start the ping + c.startPinger() + + // start the messages reader + c.startReader() } func (c *connection) Disconnect() error { diff --git a/websocket/emitter.go b/websocket/emitter.go index 5827b24ded..859bf0ee61 100644 --- a/websocket/emitter.go +++ b/websocket/emitter.go @@ -1,10 +1,10 @@ package websocket const ( - // All is the string which the Emitter use to send a message to all + // All is the string which the Emitter use to send a message to all. All = "" - // Broadcast is the string which the Emitter use to send a message to all except this connection - Broadcast = ";ionwebsocket;to;all;except;me;" + // Broadcast is the string which the Emitter use to send a message to all except this connection. + Broadcast = ";to;all;except;me;" ) type ( diff --git a/websocket/server.go b/websocket/server.go index 3ef38450c3..cd17c3ce85 100644 --- a/websocket/server.go +++ b/websocket/server.go @@ -109,7 +109,7 @@ type ( mu sync.RWMutex // for rooms onConnectionListeners []ConnectionFunc //connectionPool sync.Pool // sadly we can't make this because the websocket connection is live until is closed. - handler context.Handler + upgrader websocket.Upgrader } ) @@ -119,10 +119,20 @@ type ( // // To serve the built'n javascript client-side library look the `websocket.ClientHandler`. func New(cfg Config) *Server { + cfg = cfg.Validate() return &Server{ - config: cfg.Validate(), + config: cfg, rooms: make(map[string][]string, 0), onConnectionListeners: make([]ConnectionFunc, 0), + upgrader: websocket.Upgrader{ + HandshakeTimeout: cfg.HandshakeTimeout, + ReadBufferSize: cfg.ReadBufferSize, + WriteBufferSize: cfg.WriteBufferSize, + Error: cfg.Error, + CheckOrigin: cfg.CheckOrigin, + Subprotocols: cfg.Subprotocols, + EnableCompression: cfg.EnableCompression, + }, } } @@ -135,40 +145,50 @@ func New(cfg Config) *Server { // // To serve the built'n javascript client-side library look the `websocket.ClientHandler`. func (s *Server) Handler() context.Handler { - // build the upgrader once - c := s.config - - upgrader := websocket.Upgrader{ - HandshakeTimeout: c.HandshakeTimeout, - ReadBufferSize: c.ReadBufferSize, - WriteBufferSize: c.WriteBufferSize, - Error: c.Error, - CheckOrigin: c.CheckOrigin, - Subprotocols: c.Subprotocols, - EnableCompression: c.EnableCompression, - } - return func(ctx context.Context) { - // Upgrade upgrades the HTTP Server connection to the WebSocket protocol. - // - // The responseHeader is included in the response to the client's upgrade - // request. Use the responseHeader to specify cookies (Set-Cookie) and the - // application negotiated subprotocol (Sec--Protocol). - // - // If the upgrade fails, then Upgrade replies to the client with an HTTP error - // response. - conn, err := upgrader.Upgrade(ctx.ResponseWriter(), ctx.Request(), ctx.ResponseWriter().Header()) - if err != nil { - ctx.Application().Logger().Warnf("websocket error: %v\n", err) - ctx.StatusCode(503) // Status Service Unavailable - return + c := s.Upgrade(ctx) + // NOTE TO ME: fire these first BEFORE startReader and startPinger + // in order to set the events and any messages to send + // the startPinger will send the OK to the client and only + // then the client is able to send and receive from Server + // when all things are ready and only then. DO NOT change this order. + + // fire the on connection event callbacks, if any + for i := range s.onConnectionListeners { + s.onConnectionListeners[i](c) } - s.handleConnection(ctx, conn) + + // start the ping and the messages reader + c.Wait() } } -// handleConnection creates & starts to listening to a new connection -func (s *Server) handleConnection(ctx context.Context, websocketConn UnderlineConnection) { +// Upgrade upgrades the HTTP Server connection to the WebSocket protocol. +// +// The responseHeader is included in the response to the client's upgrade +// request. Use the responseHeader to specify cookies (Set-Cookie) and the +// application negotiated subprotocol (Sec--Protocol). +// +// If the upgrade fails, then Upgrade replies to the client with an HTTP error +// response and the return `Connection.Err()` is filled with that error. +// +// For a more high-level function use the `Handler()` and `OnConnecton` events. +// This one does not starts the connection's writer and reader, so after your `On/OnMessage` events registration +// the caller has to call the `Connection#Wait` function, otherwise the connection will be not handled. +func (s *Server) Upgrade(ctx context.Context) Connection { + conn, err := s.upgrader.Upgrade(ctx.ResponseWriter(), ctx.Request(), ctx.ResponseWriter().Header()) + if err != nil { + ctx.Application().Logger().Warnf("websocket error: %v\n", err) + ctx.StatusCode(503) // Status Service Unavailable + return &connection{err: err} + } + + return s.handleConnection(ctx, conn) +} + +// wrapConnection wraps an underline connection to an iris websocket connection. +// It does NOT starts its writer, reader and event mux, the caller is responsible for that. +func (s *Server) handleConnection(ctx context.Context, websocketConn UnderlineConnection) *connection { // use the config's id generator (or the default) to create a websocket client/connection id cid := s.config.IDGenerator(ctx) // create the new connection @@ -179,22 +199,7 @@ func (s *Server) handleConnection(ctx context.Context, websocketConn UnderlineCo // join to itself s.Join(c.ID(), c.ID()) - // NOTE TO ME: fire these first BEFORE startReader and startPinger - // in order to set the events and any messages to send - // the startPinger will send the OK to the client and only - // then the client is able to send and receive from Server - // when all things are ready and only then. DO NOT change this order. - - // fire the on connection event callbacks, if any - for i := range s.onConnectionListeners { - s.onConnectionListeners[i](c) - } - - // start the ping - c.startPinger() - - // start the messages reader - c.startReader() + return c } /* Notes: