Update to version 8.5.0 | NEW: MVC Output Result | Read HISTORY.md

Former-commit-id: 6a3579f2500fc715d7dc606478960946dcade61d
This commit is contained in:
Gerasimos (Makis) Maropoulos 2017-10-09 15:26:46 +03:00
parent fda35cbdb5
commit 49ee8f2d75
40 changed files with 1959 additions and 191 deletions

View File

@ -18,6 +18,157 @@ 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`.
# Su, 09 October 2017 | v8.5.0
## MVC
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).
Iris now gives you the ability to render a response based on the **output values** returned method functions!
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 `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 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}
}
```
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.
### The complete example source code can be found at [_examples/mvc/using-method-result](_examples/mvc/using-method-result) folder.
----
Upgrade with `go get -u -v github.com/kataras/iris` or let the auto-updater to do its job.
# Fr, 06 October 2017 | v8.4.5

472
README.md
View File

@ -8,34 +8,34 @@ Three days ago, _at 03 October_, we announced the first [Iris User Experience fo
At overall, the results (so far) are very promising, high number of participations and the answers to the questions are near to the green feedback we were receiving over the past months from Gophers worldwide via our [rocket chat](https://chat.iris-go.com) and [author's twitter](https://twitter.com/makismaropoulos). **If you didn't complete the form yet, [please do so](https://docs.google.com/forms/d/e/1FAIpQLSdCxZXPANg_xHWil4kVAdhmh7EBBHQZ_4_xSZVDL-oCC_z5pA/viewform?usp=sf_link) as soon as possible!**
However, as everything in life; nothing goes as expected, people are strange, we programmers even more. The last part of the form has a text area which participiations can add any "questions or comments", there we saw one comment that surprised me, in the bad sense. We respect all individual singularities the same, we do not discriminate between people. The data are anonymous, so the only place to answer to that person is, _surprisingly_, here!
However, as everything in life; nothing goes as expected, people are strange, we programmers even more. The last part of the form has a text area which participiations can add any "questions or comments", there we saw one comment that surprised me the most, in the bad sense. We respect all individual singularities the same, we do not discriminate between people. The data are anonymous, so the only place to answer to that person is, _surprisingly_, here!
<details>
<summary>"I admire your dedication to iris and I am in love with its speed..."</summary>
The comment was "I admire your dedication to iris and I am in love with its speed but.. I've read some things on that blog and blablabla..." you get the point, at the first we were happy and suddenly we saw that "but... I've" and we broke xD.
My answer to this is clear in simple words so that anyone can understand; Did you really believed those unsubstantial things even if you could take some time off to read the source code?🤔
The answer to this is clear in simple words so that anyone can understand; Did you really believed those unsubstantial things even if you could take some time off to read the source code?🤔
Iris was one of the top github trending projects written in Go Programming Language for the 2016 and the most trending web framework in the globe. We couldn't even imagine that we will be the receiver of countless "[thank you for iris, finally a web framework I can work on](https://twitter.com/_mgale/status/818591490305761280)" comments from hundreds strangers around the globe!
Iris was one of the top github trending projects written in Go Programming Language for the 2016 and the most trending web framework in the globe. We couldn't even imagine that we will be the receivers of countless "[thank you for iris, finally a web framework I can work on](https://twitter.com/_mgale/status/818591490305761280)" comments from hundreds strangers around the globe!
Please do research before reading and assimilate everything, those blog spots are not always telling the whole truth, they are not so innocent :)
Please do research before digestion, those blog posts are not always telling the whole truth, they are not so innocent :)
Especially those from that kid which do not correspond to reality;
Especially those from that kid that even don't correspond to reality;
```go
/* start */
```
First of all, that article **is reffering 1.5 years ago**, to pretend that this article speaks for the present is hilariously ridiculous! Iris is on version 8 now and it's not a router any more, it's a fully featured web framework with its own ecosystem.
First of all, that article **is referring 1.5 years ago**, to pretend that this article speaks for the present is hilariously ridiculous! Iris is on version 8 now and it's not a router any more, it's a fully featured web framework with its own ecosystem.
1. Iris does NOT use any third-party code inside it, like "httprouter" or "fasthttp". Just navigate to the source code. If you care about historical things you can search the project but it doesn't matter because the internal implementation of Iris changed a lot of times, a lot more than its public API changes:P.
2. Iris makes use of its own routing mechanisms with a unique **language interpreter** in order to serve even the most demanding of us `/user/{id:int min(2)}`, `/alphabetical/{param:string regexp(^[a-zA-Z ]+$)}` et cetera.
3. Iris has its own unique MVC architectural parser with heart-breaking performance.
4. Was it possible to do all those things and [much more](_examples) before Iris? Exaclty. Iris offers you all these for free, plus the unmatched performance.
4. Was it possible to do all those things and [much more](_examples) before Iris? Exactly. Iris offers you all these for free, plus the unmatched performance.
5. Iris is the result of hundreds(or thousands(?)) of hours of **FREE and UNPAID** work. There are people who actually found a decent job because of Iris. Thousands of Gophers are watching or/and helping to make Iris even better, the silent majority loves Iris even more.
That 23 years old, inhibited boy, who published that post had played you with the most immoral way! Reading the Iris' source code doesn't cost you a thing! Iris is free to use for everyone, Iris is an open-source software, no hidden spots. **Don't stuck on the past, get over that, Iris has succeed, move on now.**
That 23 years old, inhibited boy, who published that post had played you with the most immoral way! Reading the Iris' source code doesn't cost you a thing! Iris is free to use for everyone, Iris is an open-source software, no hidden spots. **Don't stuck in the past, get over that, Iris has succeed, move on now.**
```go
/* end */
@ -43,7 +43,6 @@ That 23 years old, inhibited boy, who published that post had played you with th
</details>
_Psst_, we've produced a small video about your feelings regrating to Iris! You can watch the whole video at https://www.youtube.com/watch?v=jGx0LkuUs4A.
[![build status](https://img.shields.io/travis/kataras/iris/master.svg?style=flat-square)](https://travis-ci.org/kataras/iris)
@ -60,15 +59,15 @@ _Psst_, we've produced a small video about your feelings regrating to Iris! You
## Installation
The only requirement is the [Go Programming Language](https://golang.org/dl/), at least version 1.9.
The only requirement is the [Go Programming Language](https://golang.org/dl/), at least version 1.9
```sh
$ go get -u github.com/kataras/iris
```
* 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.
- 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.
* [Latest changes](https://github.com/kataras/iris/blob/master/HISTORY.md#fr-06-october-2017--v845)
- [Latest changes | v8.5.0](https://github.com/kataras/iris/blob/master/HISTORY.md#su-09-october-2017--v850)
## Getting Started
@ -132,6 +131,8 @@ $ go run main.go
> Application started. Press CTRL+C to shut down.
```
Guidelines for bootstrapping handler-based applications can be found at the [_examples/structuring/handler-based](_examples/structuring/handler-based) folder.
### Quick MVC Tutorial
```go
@ -153,7 +154,7 @@ func main() {
type HelloWorldController struct {
mvc.Controller
// [ your fields here ]
// [ Your fields here ]
// Request lifecycle data
// Models
// Database
@ -163,21 +164,30 @@ type HelloWorldController struct {
//
// GET: /helloworld
func (c *HelloWorldController) Get() {
c.Ctx.Text("This is my default action...")
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() {
c.Ctx.HTML("This is the <b>GetWelcome</b> action func...")
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)
}
```
@ -188,9 +198,431 @@ Every `exported` func prefixed with an HTTP Method(`Get`, `Post`, `Put`, `Delete
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 second comment specifies an [HTTP GET](https://www.w3schools.com/tags/ref_httpmethods.asp) method that is invoked by appending "/helloworld/welcome/" to the URL.
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" 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.
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 `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 is just a lightweight alternative
// to the "mvc.Controller" controller type.
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/using-method-result/folder_structure.png)](_examples/mvc/using-method-result)
Shhh, let's spread the code itself.
```go
// file: 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,
}
}
```
```html
<!-- file: views/hello/index.html -->
<html>
<head>
<title>{{.Title}} - My App</title>
</head>
<body>
<p>{{.MyMessage}}</p>
</body>
</html>
```
```html
<!-- file: views/hello/name.html -->
<html>
<head>
<title>{{.}}' Portfolio - My App</title>
</head>
<body>
<h1>Hello {{.}}</h1>
</body>
</html>
```
> Navigate to the [_examples/view](_examples/#view) for more examples
like shared layouts, tmpl funcs, reverse routing and more!
```go
// file: models/movie.go
package models
// 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"`
}
```
```go
// file: controllers/movies_controller.go
package controllers
import (
"github.com/kataras/iris/_examples/mvc/using-method-result/datasource"
"github.com/kataras/iris/_examples/mvc/using-method-result/models"
"github.com/kataras/iris"
"github.com/kataras/iris/mvc"
)
// 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() []models.Movie {
return datasource.Movies
}
// GetBy returns a movie.
// Demo:
// curl -i http://localhost:8080/movies/1
func (c *MoviesController) GetBy(id int) models.Movie {
return datasource.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) (models.Movie, int) {
// get the movie
m := datasource.Movies[id]
// get the request data for poster and genre
file, info, err := c.Ctx.FormFile("poster")
if err != nil {
return models.Movie{}, iris.StatusInternalServerError
}
// 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")
// update the poster
m.Poster = poster
m.Genre = genre
datasource.Movies[id] = m
return m, iris.StatusOK
}
// 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 := datasource.Movies[id].Name
datasource.Movies = append(datasource.Movies[:id], datasource.Movies[id+1:]...)
// and return the deleted movie's name
return iris.Map{"deleted": deleted}
}
```
```go
// file: datasource/movies.go
package datasource
import "github.com/kataras/iris/_examples/mvc/using-method-result/models"
// Movies is our imaginary data source.
var Movies = []models.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",
},
{
Name: "North by Northwest",
Year: 1959,
Genre: "Thriller",
Poster: "https://iris-go.com/images/examples/mvc-movies/5.jpg",
},
}
```
```go
// file: 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",
},
})
```
```go
// file: main.go
package main
import (
"github.com/kataras/iris/_examples/mvc/using-method-result/controllers"
"github.com/kataras/iris/_examples/mvc/using-method-result/middleware"
"github.com/kataras/iris"
)
func main() {
app := iris.New()
// Load the template files.
app.RegisterView(iris.HTML("./views", ".html"))
// Register our controllers.
app.Controller("/hello", new(controllers.HelloController))
// Add the basic authentication(admin:password) middleware
// for the /movies based requests.
app.Controller("/movies", new(controllers.MoviesController), middleware.BasicAuth)
// Start the web server at localhost:8080
// http://localhost:8080/hello
// http://localhost:8080/hello/iris
// http://localhost:8080/movies/1
app.Run(
iris.Addr("localhost:8080"),
iris.WithoutVersionChecker,
iris.WithoutServerError(iris.ErrServerClosed),
iris.WithOptimizations, // enables faster json serialization and more
)
}
```
More folder structure guidelines can be found at the [_examples/#structuring](_examples/#structuring) section.
## 😃 Do you like what you see so far?
@ -206,7 +638,7 @@ Controller knows how to handle the "name" and "numTimes" at `GetWelcomeBy`, beca
- [A URL Shortener Service using Go, Iris and Bolt](https://medium.com/@kataras/a-url-shortener-service-using-go-iris-and-bolt-4182f0b00ae7)
- [Why I preferred Go over Node.js for simple Web Application](https://medium.com/@tigranbs/why-i-preferred-go-over-node-js-for-simple-web-application-d4a549e979b9)
Take some time, `don't say we didn't warn you`, and continue your journey by [navigating to the bigger README page](README_BIG.md).
Take some time, `don't say we didn't warn you`, and continue your journey by [navigating to the next README page](README_NEXT.md).
## License

View File

@ -86,7 +86,7 @@ _Psst_, we've produced a small video about your feelings regrating to Iris! You
### 📑 Table Of Content
* [Installation](#-installation)
* [Latest changes](https://github.com/kataras/iris/blob/master/HISTORY.md#fr-06-october-2017--v845)
* [Latest changes](https://github.com/kataras/iris/blob/master/HISTORY.md#su-09-october-2017--v850)
* [Learn](#-learn)
* [Structuring](_examples/#structuring)
* [HTTP Listening](_examples/#http-listening)

View File

@ -1 +1 @@
8.4.5:https://github.com/kataras/iris/blob/master/HISTORY.md#fr-06-october-2017--v845
8.5.0:https://github.com/kataras/iris/blob/master/HISTORY.md#su-09-october-2017--v850

View File

@ -0,0 +1,14 @@
package controllers
import "github.com/kataras/iris/mvc"
type IndexControllerHeavy struct{ mvc.C }
func (c *IndexControllerHeavy) Get() mvc.View {
return mvc.View{
Name: "index.html",
Data: map[string]interface{}{
"Title": "Home Page",
},
}
}

View File

@ -0,0 +1,16 @@
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
}

View File

@ -19,13 +19,14 @@ It doesn't always contain the "best ways" but it does cover each important featu
### Structuring
Nothing stops you from using your favorite folder structure. Iris is a low level web framework, it has got MVC support but it doesn't limit your folder structure, this is your choice.
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 is always depends on your needs. We can't tell you how to design your own application for sure but you're free to take a closer look to the examples below; you may find something useful that you can borrow for your app
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 the examples below; you may find something useful that you can borrow for your app
- [Example 1](mvc/login)
- [Example 2](structuring/mvc)
- [Example 3](structuring/handler-based)
- [Example 4](mvc/using-method-result)
### HTTP Listening
@ -200,7 +201,7 @@ Follow the examples below,
- [From func(w http.ResponseWriter, r *http.Request, next http.HandlerFunc)](convert-handlers/negroni-like/main.go)
- [From http.Handler or http.HandlerFunc](convert-handlers/nethttp/main.go)
### View
### View
| Engine | Declaration |
| -----------|-------------|

View File

@ -0,0 +1,60 @@
// file: 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,
}
}

View File

@ -0,0 +1,75 @@
// file: controllers/movies_controller.go
//
// This is just an example of usage, don't use it for production, it even doesn't check for
// index exceed!
package controllers
import (
"github.com/kataras/iris/_examples/mvc/using-method-result/datasource"
"github.com/kataras/iris/_examples/mvc/using-method-result/models"
"github.com/kataras/iris"
"github.com/kataras/iris/mvc"
)
// 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() []models.Movie {
return datasource.Movies
}
// GetBy returns a movie.
// Demo:
// curl -i http://localhost:8080/movies/1
func (c *MoviesController) GetBy(id int) models.Movie {
return datasource.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) (models.Movie, int) {
// get the movie
m := datasource.Movies[id]
// get the request data for poster and genre
file, info, err := c.Ctx.FormFile("poster")
if err != nil {
return models.Movie{}, iris.StatusInternalServerError
}
// 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")
// update the poster
m.Poster = poster
m.Genre = genre
datasource.Movies[id] = m
return m, iris.StatusOK
}
// 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 := datasource.Movies[id].Name
datasource.Movies = append(datasource.Movies[:id], datasource.Movies[id+1:]...)
// and return the deleted movie's name
return iris.Map{"deleted": deleted}
}

View File

@ -0,0 +1,39 @@
// file: datasource/movies.go
package datasource
import "github.com/kataras/iris/_examples/mvc/using-method-result/models"
// Movies is our imaginary data source.
var Movies = []models.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",
},
{
Name: "North by Northwest",
Year: 1959,
Genre: "Thriller",
Poster: "https://iris-go.com/images/examples/mvc-movies/5.jpg",
},
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

View File

@ -0,0 +1,33 @@
// file: main.go
package main
import (
"github.com/kataras/iris/_examples/mvc/using-method-result/controllers"
"github.com/kataras/iris/_examples/mvc/using-method-result/middleware"
"github.com/kataras/iris"
)
func main() {
app := iris.New()
// Load the template files.
app.RegisterView(iris.HTML("./views", ".html"))
// Register our controllers.
app.Controller("/hello", new(controllers.HelloController))
// Add the basic authentication(admin:password) middleware
// for the /movies based requests.
app.Controller("/movies", new(controllers.MoviesController), middleware.BasicAuth)
// Start the web server at localhost:8080
// http://localhost:8080/hello
// http://localhost:8080/hello/iris
// 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
)
}

View File

@ -0,0 +1,12 @@
// file: 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",
},
})

View File

@ -0,0 +1,11 @@
// file: models/movie.go
package models
// 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"`
}

View File

@ -0,0 +1,12 @@
<!-- file: views/hello/index.html -->
<html>
<head>
<title>{{.Title}} - My App</title>
</head>
<body>
<p>{{.MyMessage}}</p>
</body>
</html>

View File

@ -0,0 +1,12 @@
<!-- file: views/hello/name.html -->
<html>
<head>
<title>{{.}}' Portfolio - My App</title>
</head>
<body>
<h1>Hello {{.}}</h1>
</body>
</html>

View File

@ -6,6 +6,8 @@ import (
"github.com/gorilla/securecookie"
"github.com/kataras/iris"
"github.com/kataras/iris/middleware/logger"
"github.com/kataras/iris/middleware/recover"
"github.com/kataras/iris/sessions"
"github.com/kataras/iris/websocket"
)
@ -104,6 +106,10 @@ func (b *Bootstrapper) Bootstrap() *Bootstrapper {
b.Favicon(StaticAssets + Favicon)
b.StaticWeb(StaticAssets[1:len(StaticAssets)-1], StaticAssets)
// middleware, after static files
b.Use(recover.New())
b.Use(logger.New())
return b
}

View File

@ -858,11 +858,22 @@ func (ctx *context) EndRequest() {
// author's note:
// if recording, the error handler can handle
// the rollback and remove any response written before,
// we don't have to do anything here, written is -1 when Recording
// we don't have to do anything here, written is <=0 (-1 for default empty, even no status code)
// when Recording
// because we didn't flush the response yet
// if !recording then check if the previous handler didn't send something
// to the client
if ctx.writer.Written() == -1 {
// to the client.
if ctx.writer.Written() <= 0 {
// Author's notes:
// previously: == -1,
// <=0 means even if empty write called which has meaning;
// rel: core/router/status.go#Fire-else
// mvc/activator/funcmethod/func_result_dispatcher.go#DispatchCommon-write
// mvc/response.go#defaultFailureResponse - no text given but
// status code should be fired, but it couldn't because of the .Write
// action, the .Written() was 0 even on empty response, this 0 means that
// a status code given, the previous check of the "== -1" didn't make check for that,
// we do now.
ctx.Application().FireErrorCode(ctx)
}
}
@ -1233,7 +1244,7 @@ func (ctx *context) ContentType(cType string) {
}
// if doesn't contain a charset already then append it
if !strings.Contains(cType, "charset") {
if cType != contentBinaryHeaderValue {
if cType != ContentBinaryHeaderValue {
charset := ctx.Application().ConfigurationReadOnly().GetCharset()
cType += "; charset=" + charset
}
@ -1941,7 +1952,7 @@ func (ctx *context) GetViewData() map[string]interface{} {
//
// Examples: https://github.com/kataras/iris/tree/master/_examples/view/
func (ctx *context) View(filename string) error {
ctx.ContentType(contentHTMLHeaderValue)
ctx.ContentType(ContentHTMLHeaderValue)
cfg := ctx.Application().ConfigurationReadOnly()
layout := ctx.values.GetString(cfg.GetViewLayoutContextKey())
@ -1957,38 +1968,38 @@ func (ctx *context) View(filename string) error {
}
const (
// contentBinaryHeaderValue header value for binary data.
contentBinaryHeaderValue = "application/octet-stream"
// contentHTMLHeaderValue is the string of text/html response header's content type value.
contentHTMLHeaderValue = "text/html"
// ContentJSON header value for JSON data.
contentJSONHeaderValue = "application/json"
// ContentJSONP header value for JSONP & Javascript data.
contentJavascriptHeaderValue = "application/javascript"
// contentTextHeaderValue header value for Text data.
contentTextHeaderValue = "text/plain"
// contentXMLHeaderValue header value for XML data.
contentXMLHeaderValue = "text/xml"
// ContentBinaryHeaderValue header value for binary data.
ContentBinaryHeaderValue = "application/octet-stream"
// ContentHTMLHeaderValue is the string of text/html response header's content type value.
ContentHTMLHeaderValue = "text/html"
// ContentJSONHeaderValue header value for JSON data.
ContentJSONHeaderValue = "application/json"
// ContentJavascriptHeaderValue header value for JSONP & Javascript data.
ContentJavascriptHeaderValue = "application/javascript"
// ContentTextHeaderValue header value for Text data.
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"
// ContentMarkdownHeaderValue custom key/content type, the real is the text/html.
ContentMarkdownHeaderValue = "text/markdown"
)
// Binary writes out the raw bytes as binary data.
func (ctx *context) Binary(data []byte) (int, error) {
ctx.ContentType(contentBinaryHeaderValue)
ctx.ContentType(ContentBinaryHeaderValue)
return ctx.Write(data)
}
// Text writes out a string as plain text.
func (ctx *context) Text(text string) (int, error) {
ctx.ContentType(contentTextHeaderValue)
ctx.ContentType(ContentTextHeaderValue)
return ctx.writer.WriteString(text)
}
// HTML writes out a string as text/html.
func (ctx *context) HTML(htmlContents string) (int, error) {
ctx.ContentType(contentHTMLHeaderValue)
ctx.ContentType(ContentHTMLHeaderValue)
return ctx.writer.WriteString(htmlContents)
}
@ -2078,11 +2089,13 @@ func WriteJSON(writer io.Writer, v interface{}, options JSON, enableOptimization
return writer.Write(result)
}
var defaultJSONOptions = JSON{}
// DefaultJSONOptions is the optional settings that are being used
// inside `ctx.JSON`.
var DefaultJSONOptions = JSON{}
// JSON marshals the given interface object and writes the JSON response to the client.
func (ctx *context) JSON(v interface{}, opts ...JSON) (n int, err error) {
options := defaultJSONOptions
options := DefaultJSONOptions
if len(opts) > 0 {
options = opts[0]
@ -2090,7 +2103,7 @@ func (ctx *context) JSON(v interface{}, opts ...JSON) (n int, err error) {
optimize := ctx.shouldOptimize()
ctx.ContentType(contentJSONHeaderValue)
ctx.ContentType(ContentJSONHeaderValue)
if options.StreamingJSON {
if optimize {
@ -2162,17 +2175,19 @@ func WriteJSONP(writer io.Writer, v interface{}, options JSONP, enableOptimizati
return writer.Write(result)
}
var defaultJSONPOptions = JSONP{}
// DefaultJSONPOptions is the optional settings that are being used
// inside `ctx.JSONP`.
var DefaultJSONPOptions = JSONP{}
// JSONP marshals the given interface object and writes the JSON response to the client.
func (ctx *context) JSONP(v interface{}, opts ...JSONP) (int, error) {
options := defaultJSONPOptions
options := DefaultJSONPOptions
if len(opts) > 0 {
options = opts[0]
}
ctx.ContentType(contentJavascriptHeaderValue)
ctx.ContentType(ContentJavascriptHeaderValue)
n, err := WriteJSONP(ctx.writer, v, options, ctx.shouldOptimize())
if err != nil {
@ -2205,17 +2220,19 @@ func WriteXML(writer io.Writer, v interface{}, options XML) (int, error) {
return writer.Write(result)
}
var defaultXMLOptions = XML{}
// DefaultXMLOptions is the optional settings that are being used
// from `ctx.XML`.
var DefaultXMLOptions = XML{}
// XML marshals the given interface object and writes the XML response to the client.
func (ctx *context) XML(v interface{}, opts ...XML) (int, error) {
options := defaultXMLOptions
options := DefaultXMLOptions
if len(opts) > 0 {
options = opts[0]
}
ctx.ContentType(contentXMLHeaderValue)
ctx.ContentType(ContentXMLHeaderValue)
n, err := WriteXML(ctx.writer, v, options)
if err != nil {
@ -2235,17 +2252,19 @@ func WriteMarkdown(writer io.Writer, markdownB []byte, options Markdown) (int, e
return writer.Write(buf)
}
var defaultMarkdownOptions = Markdown{}
// DefaultMarkdownOptions is the optional settings that are being used
// from `WriteMarkdown` and `ctx.Markdown`.
var DefaultMarkdownOptions = Markdown{}
// Markdown parses the markdown to html and renders to the client.
func (ctx *context) Markdown(markdownB []byte, opts ...Markdown) (int, error) {
options := defaultMarkdownOptions
options := DefaultMarkdownOptions
if len(opts) > 0 {
options = opts[0]
}
ctx.ContentType(contentHTMLHeaderValue)
ctx.ContentType(ContentHTMLHeaderValue)
n, err := WriteMarkdown(ctx.writer, markdownB, options)
if err != nil {

View File

@ -117,7 +117,7 @@ func (w *GzipResponseWriter) Write(contents []byte) (int, error) {
func (w *GzipResponseWriter) Writef(format string, a ...interface{}) (n int, err error) {
n, err = fmt.Fprintf(w, format, a...)
if err == nil {
w.ResponseWriter.Header().Set(contentTypeHeaderKey, contentTextHeaderValue)
w.ResponseWriter.Header().Set(contentTypeHeaderKey, ContentTextHeaderValue)
}
return
@ -128,7 +128,7 @@ func (w *GzipResponseWriter) Writef(format string, a ...interface{}) (n int, err
func (w *GzipResponseWriter) WriteString(s string) (n int, err error) {
n, err = w.Write([]byte(s))
if err == nil {
w.ResponseWriter.Header().Set(contentTypeHeaderKey, contentTextHeaderValue)
w.ResponseWriter.Header().Set(contentTypeHeaderKey, ContentTextHeaderValue)
}
return
}

View File

@ -61,6 +61,10 @@ type ResponseWriter interface {
// > 0 means that the reply was written and it's the total number of bytes were written.
Written() int
// SetWritten sets manually a value for written, it can be
// NoWritten(-1) or StatusCodeWritten(0), > 0 means body length which is useless here.
SetWritten(int)
// SetBeforeFlush registers the unique callback which called exactly before the response is flushed to the client.
SetBeforeFlush(cb func())
// GetBeforeFlush returns (not execute) the before flush callback, or nil if not setted by SetBeforeFlush.
@ -143,6 +147,14 @@ func (w *responseWriter) EndResponse() {
releaseResponseWriter(w)
}
// SetWritten sets manually a value for written, it can be
// NoWritten(-1) or StatusCodeWritten(0), > 0 means body length which is useless here.
func (w *responseWriter) SetWritten(n int) {
if n >= NoWritten && n <= StatusCodeWritten {
w.written = n
}
}
// Written should returns the total length of bytes that were being written to the client.
// In addition iris provides some variables to help low-level actions:
// NoWritten, means that nothing were written yet and the response writer is still live.
@ -152,11 +164,6 @@ func (w *responseWriter) Written() int {
return w.written
}
// prin to write na benei to write header
// meta to write den ginete edw
// prepei omws kai mono me WriteHeader kai xwris Write na pigenei to status code
// ara...wtf prepei na exw function flushStatusCode kai na elenxei an exei dw9ei status code na to kanei write aliws 200
// WriteHeader sends an HTTP response header with status code.
// If WriteHeader is not called explicitly, the first call to Write
// will trigger an implicit WriteHeader(http.StatusOK).

View File

@ -101,7 +101,7 @@ func (t *Transaction) Complete(err error) {
if err != nil {
t.hasError = true
statusCode := 504 // bad request
statusCode := 400 // bad request
reason := err.Error()
cType := "text/plain; charset=" + t.context.Application().ConfigurationReadOnly().GetCharset()

View File

@ -35,7 +35,7 @@ func (ch *ErrorCodeHandler) Fire(ctx context.Context) {
// if we can't reset the body and the body has been filled
// which means that the status code already sent,
// then do not fire this custom error code.
if ctx.ResponseWriter().Written() != -1 {
if ctx.ResponseWriter().Written() > 0 { // != -1, rel: context/context.go#EndRequest
return
}
}

163
doc.go
View File

@ -35,7 +35,7 @@ Source code and other details for the project are available at GitHub:
Current Version
8.4.5
8.5.0
Installation
@ -855,6 +855,167 @@ Register one or more relative paths and able to get path parameters, i.e
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) |
int |
(int, string |
(string, error) |
error |
(int, error) |
(customStruct, error) |
customStruct |
(customStruct, int) |
(customStruct, string) |
Result or (Result, error)
where 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 `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 {
// 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}
}
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/using-method-result.
Using Iris MVC for code reuse

59
go19.go
View File

@ -109,7 +109,7 @@ type (
// Look `core/router#APIBuilder#Controller` method too.
//
// A shortcut for the `mvc#Controller`,
// useful when `app.Controller` is being used.
// useful when `app.Controller` method is being used.
//
// A Controller can be declared by importing
// the "github.com/kataras/iris/mvc"
@ -119,4 +119,61 @@ type (
// 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

View File

@ -32,12 +32,18 @@ type Configuration struct {
// Debug if true then debug messages from the httpexpect will be shown when a test runs
// Defaults to false.
Debug bool
// LogLevel sets the application's log level.
// Defaults to "disable" when testing.
LogLevel string
}
// Set implements the OptionSetter for the Configuration itself
func (c Configuration) Set(main *Configuration) {
main.URL = c.URL
main.Debug = c.Debug
if c.LogLevel != "" {
main.LogLevel = c.LogLevel
}
}
var (
@ -55,11 +61,19 @@ var (
c.Debug = val
}
}
// LogLevel sets the application's log level "val".
// Defaults to disabled when testing.
LogLevel = func(val string) OptionSet {
return func(c *Configuration) {
c.LogLevel = val
}
}
)
// DefaultConfiguration returns the default configuration for the httptest.
func DefaultConfiguration() *Configuration {
return &Configuration{URL: "", Debug: false}
return &Configuration{URL: "", Debug: false, LogLevel: "disable"}
}
// New Prepares and returns a new test framework based on the "app".
@ -71,7 +85,7 @@ func New(t *testing.T, app *iris.Application, setters ...OptionSetter) *httpexpe
}
// disable the logger
app.Logger().SetLevel("disable")
app.Logger().SetLevel(conf.LogLevel)
app.Build()
testConfiguration := httpexpect.Config{

View File

@ -32,7 +32,7 @@ import (
const (
// Version is the current version number of the Iris Web Framework.
Version = "8.4.5"
Version = "8.5.0"
)
// HTTP status codes as registered with IANA.

View File

@ -131,9 +131,15 @@ func (t TController) HandlerOf(methodFunc methodfunc.MethodFunc) context.Handler
t.persistenceController.Handle(c)
}
// if previous (binded) handlers stoped the execution
// we should know that.
if ctx.IsStopped() {
return
}
// init the request.
b.BeginRequest(ctx)
if ctx.IsStopped() {
if ctx.IsStopped() { // if begin request stopped the execution
return
}
@ -146,7 +152,8 @@ func (t TController) HandlerOf(methodFunc methodfunc.MethodFunc) context.Handler
t.modelController.Handle(ctx, c)
}
// finally, execute the controller, don't check for IsStopped.
// end the request, don't check for stopped because this does the actual writing
// if no response written already.
b.EndRequest(ctx)
}
}
@ -170,8 +177,11 @@ func RegisterMethodHandlers(t TController, registerFunc RegisterFunc) {
}
// the actual method functions
// i.e for "GET" it's the `Get()`.
methods := methodfunc.Resolve(t.Type)
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
@ -181,7 +191,7 @@ func RegisterMethodHandlers(t TController, registerFunc RegisterFunc) {
for _, m := range methods {
h := t.HandlerOf(m)
if h == nil {
golog.Debugf("MVC %s: nil method handler found for %s", t.FullName, m.Name)
golog.Warnf("MVC %s: nil method handler found for %s", t.FullName, m.Name)
continue
}
registeredHandlers := append(middleware, h)

View File

@ -173,7 +173,6 @@ func LookupFields(elem reflect.Type, matcher func(reflect.StructField) bool, han
// 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
@ -182,6 +181,7 @@ func lookupStructField(elem reflect.Type, matcher func(reflect.StructField) bool
// 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{

View File

@ -11,52 +11,9 @@ import (
// 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 accepts one or more parameters.
if a.dynamic {
// if one function input argument then call the function
// by "casting" (faster).
if l := len(a.paramKeys); l == 1 {
paramType := a.paramTypes[0]
paramKey := a.paramKeys[0]
if paramType == paramTypeInt {
return func(ctx context.Context, f reflect.Value) {
v, _ := ctx.Params().GetInt(paramKey)
f.Interface().(func(int))(v)
}
}
if paramType == paramTypeLong {
return func(ctx context.Context, f reflect.Value) {
v, _ := ctx.Params().GetInt64(paramKey)
f.Interface().(func(int64))(v)
}
}
if paramType == paramTypeBoolean {
return func(ctx context.Context, f reflect.Value) {
v, _ := ctx.Params().GetBool(paramKey)
f.Interface().(func(bool))(v)
}
}
// string, path...
return func(ctx context.Context, f reflect.Value) {
f.Interface().(func(string))(ctx.Params().Get(paramKey))
}
}
// if func input arguments are more than one then
// use the Call method (slower).
return func(ctx context.Context, f reflect.Value) {
f.Call(a.paramValues(ctx))
}
}
// if it's static without any receivers then just call it.
// if func input arguments are more than one then
// use the Call method (slower).
return func(ctx context.Context, f reflect.Value) {
f.Interface().(func())()
DispatchFuncResult(ctx, f.Call(a.paramValues(ctx)))
}
}

View File

@ -51,7 +51,6 @@ 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

View File

@ -87,6 +87,22 @@ func (p *funcParser) parse() (*ast, error) {
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
}
@ -160,6 +176,10 @@ type ast struct {
// }
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++ {

View File

@ -0,0 +1,187 @@
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) {
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) |
// customStruct |
// (customStruct, int) |
// (customStruct, string) |
// Result or (Result, error)
// where Get is an HTTP METHOD.
func DispatchFuncResult(ctx context.Context, values []reflect.Value) {
numOut := len(values)
if numOut == 0 {
return
}
var (
statusCode int
contentType string
content []byte
custom interface{}
err error
)
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 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)
}

View File

@ -3,8 +3,8 @@ package methodfunc
import (
"reflect"
"github.com/kataras/golog"
"github.com/kataras/iris/context"
"github.com/kataras/iris/core/errors"
)
// MethodFunc the handler function.
@ -26,15 +26,17 @@ type MethodFunc struct {
// Resolve returns all the method funcs
// necessary information and actions to
// perform the request.
func Resolve(typ reflect.Type) (methodFuncs []MethodFunc) {
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 err != nil {
golog.Errorf("MVC: %s\n", err)
if r.AddErr(err) {
continue
}
methodFunc := MethodFunc{
RelPath: a.relPath,
FuncInfo: info,
@ -44,5 +46,5 @@ func Resolve(typ reflect.Type) (methodFuncs []MethodFunc) {
methodFuncs = append(methodFuncs, methodFunc)
}
return
return methodFuncs, r.Return()
}

View File

@ -9,6 +9,56 @@ import (
"github.com/kataras/iris/mvc/activator"
)
// 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.
//
// It completes the `activator.BaseController` interface.
//
// Example at: https://github.com/kataras/iris/tree/master/_examples/mvc/using-method-result/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
}
var _ activator.BaseController = &C{}
// SetName sets the controller's full name.
// It's called internally.
func (c *C) SetName(name string) { c.Name = name }
// BeginRequest starts the request by initializing the `Context` field.
func (c *C) BeginRequest(ctx context.Context) { c.Ctx = ctx }
// EndRequest does nothing, is here to complete the `BaseController` interface.
func (c *C) EndRequest(ctx context.Context) {}
// Controller is the base controller for the high level controllers instances.
//
// This base controller is used as an alternative way of building
@ -61,6 +111,8 @@ import (
// 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.
//
@ -121,6 +173,10 @@ type Controller struct {
Ctx context.Context
}
var _ activator.BaseController = &Controller{}
var ctrlSuffix = reflect.TypeOf(Controller{}).Name()
// SetName sets the controller's full name.
// It's called internally.
func (c *Controller) SetName(name string) {
@ -238,6 +294,7 @@ func (c *Controller) BeginRequest(ctx context.Context) {
c.Params = ctx.Params()
// response status code
c.Status = ctx.GetStatusCode()
// share values
c.Values = ctx.Values()
// view data for templates, remember
@ -251,12 +308,12 @@ func (c *Controller) BeginRequest(ctx context.Context) {
}
func (c *Controller) tryWriteHeaders() {
if status := c.Status; status > 0 && status != c.Ctx.GetStatusCode() {
c.Ctx.StatusCode(status)
if c.Status > 0 && c.Status != c.Ctx.GetStatusCode() {
c.Ctx.StatusCode(c.Status)
}
if contentType := c.ContentType; contentType != "" {
c.Ctx.ContentType(contentType)
if c.ContentType != "" {
c.Ctx.ContentType(c.ContentType)
}
}
@ -269,7 +326,7 @@ func (c *Controller) tryWriteHeaders() {
// 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 {
if ctx.ResponseWriter().Written() >= 0 { // status code only (0) or actual body written(>0)
return
}
@ -289,16 +346,10 @@ func (c *Controller) EndRequest(ctx context.Context) {
if layout := c.Layout; layout != "" {
ctx.ViewLayout(layout)
}
if data := c.Data; len(data) > 0 {
for k, v := range data {
ctx.ViewData(k, v)
}
if len(c.Data) > 0 {
ctx.Values().Set(ctx.Application().ConfigurationReadOnly().GetViewDataContextKey(), c.Data)
}
ctx.View(view)
}
}
var ctrlSuffix = reflect.TypeOf(Controller{}).Name()
var _ activator.BaseController = &Controller{}

View File

@ -71,13 +71,13 @@ func TestControllerMethodFuncs(t *testing.T) {
e := httptest.New(t, app)
for _, method := range router.AllMethods {
e.Request(method, "/").Expect().Status(httptest.StatusOK).
e.Request(method, "/").Expect().Status(iris.StatusOK).
Body().Equal(method)
e.Request(method, "/all").Expect().Status(httptest.StatusOK).
e.Request(method, "/all").Expect().Status(iris.StatusOK).
Body().Equal(method)
e.Request(method, "/any").Expect().Status(httptest.StatusOK).
e.Request(method, "/any").Expect().Status(iris.StatusOK).
Body().Equal(method)
}
}
@ -89,13 +89,13 @@ func TestControllerMethodAndPathHandleMany(t *testing.T) {
e := httptest.New(t, app)
for _, method := range router.AllMethods {
e.Request(method, "/").Expect().Status(httptest.StatusOK).
e.Request(method, "/").Expect().Status(iris.StatusOK).
Body().Equal(method)
e.Request(method, "/path1").Expect().Status(httptest.StatusOK).
e.Request(method, "/path1").Expect().Status(iris.StatusOK).
Body().Equal(method)
e.Request(method, "/path2").Expect().Status(httptest.StatusOK).
e.Request(method, "/path2").Expect().Status(iris.StatusOK).
Body().Equal(method)
}
}
@ -114,7 +114,7 @@ func TestControllerPersistenceFields(t *testing.T) {
app := iris.New()
app.Controller("/", &testControllerPersistence{Data: data})
e := httptest.New(t, app)
e.GET("/").Expect().Status(httptest.StatusOK).
e.GET("/").Expect().Status(iris.StatusOK).
Body().Equal(data)
}
@ -165,9 +165,9 @@ func TestControllerBeginAndEndRequestFunc(t *testing.T) {
doneResponse := "done"
for _, username := range usernames {
e.GET("/profile/" + username).Expect().Status(httptest.StatusOK).
e.GET("/profile/" + username).Expect().Status(iris.StatusOK).
Body().Equal(username + doneResponse)
e.POST("/profile/" + username).Expect().Status(httptest.StatusOK).
e.POST("/profile/" + username).Expect().Status(iris.StatusOK).
Body().Equal(username + doneResponse)
}
}
@ -190,7 +190,7 @@ func TestControllerBeginAndEndRequestFuncBindMiddleware(t *testing.T) {
}
}
ctx.StatusCode(httptest.StatusForbidden)
ctx.StatusCode(iris.StatusForbidden)
ctx.Writef("forbidden")
}
@ -203,18 +203,18 @@ func TestControllerBeginAndEndRequestFuncBindMiddleware(t *testing.T) {
for username, allow := range usernames {
getEx := e.GET("/profile/" + username).Expect()
if allow {
getEx.Status(httptest.StatusOK).
getEx.Status(iris.StatusOK).
Body().Equal(username + doneResponse)
} else {
getEx.Status(httptest.StatusForbidden).Body().Equal("forbidden")
getEx.Status(iris.StatusForbidden).Body().Equal("forbidden")
}
postEx := e.POST("/profile/" + username).Expect()
if allow {
postEx.Status(httptest.StatusOK).
postEx.Status(iris.StatusOK).
Body().Equal(username + doneResponse)
} else {
postEx.Status(httptest.StatusForbidden).Body().Equal("forbidden")
postEx.Status(iris.StatusForbidden).Body().Equal("forbidden")
}
}
}
@ -275,7 +275,7 @@ func TestControllerModel(t *testing.T) {
}
for _, username := range usernames {
e.GET("/model/" + username).Expect().Status(httptest.StatusOK).
e.GET("/model/" + username).Expect().Status(iris.StatusOK).
Body().Equal(username + username + "2")
}
}
@ -318,9 +318,9 @@ func TestControllerBind(t *testing.T) {
e := httptest.New(t, app)
expected := t1 + t2
e.GET("/").Expect().Status(httptest.StatusOK).
e.GET("/").Expect().Status(iris.StatusOK).
Body().Equal(expected)
e.GET("/deep").Expect().Status(httptest.StatusOK).
e.GET("/deep").Expect().Status(iris.StatusOK).
Body().Equal(expected)
}
@ -376,7 +376,7 @@ func TestControllerRelPathAndRelTmpl(t *testing.T) {
e := httptest.New(t, app)
for path, tt := range tests {
e.GET(path).Expect().Status(httptest.StatusOK).JSON().Equal(tt)
e.GET(path).Expect().Status(iris.StatusOK).JSON().Equal(tt)
}
}
@ -436,7 +436,7 @@ func TestControllerInsideControllerRecursively(t *testing.T) {
e := httptest.New(t, app)
e.GET("/user/" + username).Expect().
Status(httptest.StatusOK).Body().Equal(expected)
Status(iris.StatusOK).Body().Equal(expected)
}
type testControllerRelPathFromFunc struct{ mvc.Controller }
@ -467,35 +467,35 @@ func TestControllerRelPathFromFunc(t *testing.T) {
app.Controller("/", new(testControllerRelPathFromFunc))
e := httptest.New(t, app)
e.GET("/").Expect().Status(httptest.StatusOK).
e.GET("/").Expect().Status(iris.StatusOK).
Body().Equal("GET:/")
e.GET("/42").Expect().Status(httptest.StatusOK).
e.GET("/42").Expect().Status(iris.StatusOK).
Body().Equal("GET:/42")
e.GET("/something/true").Expect().Status(httptest.StatusOK).
e.GET("/something/true").Expect().Status(iris.StatusOK).
Body().Equal("GET:/something/true")
e.GET("/something/false").Expect().Status(httptest.StatusOK).
e.GET("/something/false").Expect().Status(iris.StatusOK).
Body().Equal("GET:/something/false")
e.GET("/something/truee").Expect().Status(httptest.StatusNotFound)
e.GET("/something/falsee").Expect().Status(httptest.StatusNotFound)
e.GET("/something/kataras/42").Expect().Status(httptest.StatusOK).
e.GET("/something/truee").Expect().Status(iris.StatusNotFound)
e.GET("/something/falsee").Expect().Status(iris.StatusNotFound)
e.GET("/something/kataras/42").Expect().Status(iris.StatusOK).
Body().Equal("GET:/something/kataras/42")
e.GET("/something/new/kataras/42").Expect().Status(httptest.StatusOK).
e.GET("/something/new/kataras/42").Expect().Status(iris.StatusOK).
Body().Equal("GET:/something/new/kataras/42")
e.GET("/something/true/else/this/42").Expect().Status(httptest.StatusOK).
e.GET("/something/true/else/this/42").Expect().Status(iris.StatusOK).
Body().Equal("GET:/something/true/else/this/42")
e.GET("/login").Expect().Status(httptest.StatusOK).
e.GET("/login").Expect().Status(iris.StatusOK).
Body().Equal("GET:/login")
e.POST("/login").Expect().Status(httptest.StatusOK).
e.POST("/login").Expect().Status(iris.StatusOK).
Body().Equal("POST:/login")
e.GET("/admin/login").Expect().Status(httptest.StatusOK).
e.GET("/admin/login").Expect().Status(iris.StatusOK).
Body().Equal("GET:/admin/login")
e.PUT("/something/into/this").Expect().Status(httptest.StatusOK).
e.PUT("/something/into/this").Expect().Status(iris.StatusOK).
Body().Equal("PUT:/something/into/this")
e.GET("/42").Expect().Status(httptest.StatusOK).
e.GET("/42").Expect().Status(iris.StatusOK).
Body().Equal("GET:/42")
e.GET("/anything/here").Expect().Status(httptest.StatusOK).
e.GET("/anything/here").Expect().Status(iris.StatusOK).
Body().Equal("GET:/anything/here")
}
@ -523,8 +523,8 @@ func TestControllerActivateListener(t *testing.T) {
})
e := httptest.New(t, app)
e.GET("/").Expect().Status(httptest.StatusOK).
e.GET("/").Expect().Status(iris.StatusOK).
Body().Equal("default title")
e.GET("/manual").Expect().Status(httptest.StatusOK).
e.GET("/manual").Expect().Status(iris.StatusOK).
Body().Equal("my title")
}

View File

@ -3,17 +3,25 @@
package mvc
import (
"html/template"
"github.com/kataras/iris/mvc/activator"
)
// 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.
type ActivatePayload = activator.ActivatePayload
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
)

58
mvc/method_result.go Normal file
View File

@ -0,0 +1,58 @@
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/using-method-result.
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
}

View File

@ -0,0 +1,43 @@
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 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
}
var _ methodfunc.Result = Response{}
// Dispatch writes the response result to the context's response writer.
func (r Response) Dispatch(ctx context.Context) {
if s := r.Text; s != "" {
r.Content = []byte(s)
}
methodfunc.DispatchCommon(ctx, r.Code, r.ContentType, r.Content, r.Object, r.Err)
}

224
mvc/method_result_test.go Normal file
View File

@ -0,0 +1,224 @@
package mvc_test
import (
"errors"
"testing"
"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
type testControllerMethodResult struct {
mvc.C
}
func (c *testControllerMethodResult) Get() mvc.Result {
return mvc.Response{
Text: "Hello World!",
}
}
func (c *testControllerMethodResult) GetWithStatus() mvc.Response { // or mvc.Result again, no problem.
return mvc.Response{
Text: "This page doesn't exist",
Code: iris.StatusNotFound,
}
}
type testCustomStruct struct {
Name string `json:"name" xml:"name"`
Age int `json:"age" xml:"age"`
}
func (c *testControllerMethodResult) GetJson() mvc.Result {
var err error
if c.Ctx.URLParamExists("err") {
err = errors.New("error here")
}
return mvc.Response{
Err: err, // if err != nil then it will fire the error's text with a BadRequest.
Object: testCustomStruct{Name: "Iris", Age: 2},
}
}
var things = []string{"thing 0", "thing 1", "thing 2"}
func (c *testControllerMethodResult) GetThingWithTryBy(index int) mvc.Result {
failure := mvc.Response{
Text: "thing does not exist",
Code: iris.StatusNotFound,
}
return mvc.Try(func() mvc.Result {
// if panic because of index exceed the slice
// then the "failure" response will be returned instead.
return mvc.Response{Text: things[index]}
}, failure)
}
func (c *testControllerMethodResult) GetThingWithTryDefaultBy(index int) mvc.Result {
return mvc.Try(func() mvc.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]}
})
}
func TestControllerMethodResult(t *testing.T) {
app := iris.New()
app.Controller("/", new(testControllerMethodResult))
e := httptest.New(t, app)
e.GET("/").Expect().Status(iris.StatusOK).
Body().Equal("Hello World!")
e.GET("/with/status").Expect().Status(iris.StatusNotFound).
Body().Equal("This page doesn't exist")
e.GET("/json").Expect().Status(iris.StatusOK).
JSON().Equal(iris.Map{
"name": "Iris",
"age": 2,
})
e.GET("/json").WithQuery("err", true).Expect().
Status(iris.StatusBadRequest).
Body().Equal("error here")
e.GET("/thing/with/try/1").Expect().
Status(iris.StatusOK).
Body().Equal("thing 1")
// failure because of index exceed the slice
e.GET("/thing/with/try/3").Expect().
Status(iris.StatusNotFound).
Body().Equal("thing does not exist")
e.GET("/thing/with/try/default/3").Expect().
Status(iris.StatusBadRequest).
Body().Equal("Bad Request")
}
type testControllerMethodResultTypes struct {
mvc.Controller
}
func (c *testControllerMethodResultTypes) GetText() string {
return "text"
}
func (c *testControllerMethodResultTypes) GetStatus() int {
return iris.StatusBadGateway
}
func (c *testControllerMethodResultTypes) GetTextWithStatusOk() (string, int) {
return "OK", iris.StatusOK
}
// tests should have output arguments mixed
func (c *testControllerMethodResultTypes) GetStatusWithTextNotOkBy(first string, second string) (int, string) {
return iris.StatusForbidden, "NOT_OK_" + first + second
}
func (c *testControllerMethodResultTypes) GetTextAndContentType() (string, string) {
return "<b>text</b>", "text/html"
}
type testControllerMethodCustomResult struct {
HTML string
}
// The only one required function to make that a custom Response dispatcher.
func (r testControllerMethodCustomResult) Dispatch(ctx context.Context) {
ctx.HTML(r.HTML)
}
func (c *testControllerMethodResultTypes) GetCustomResponse() testControllerMethodCustomResult {
return testControllerMethodCustomResult{"<b>text</b>"}
}
func (c *testControllerMethodResultTypes) GetCustomResponseWithStatusOk() (testControllerMethodCustomResult, int) {
return testControllerMethodCustomResult{"<b>OK</b>"}, iris.StatusOK
}
func (c *testControllerMethodResultTypes) GetCustomResponseWithStatusNotOk() (testControllerMethodCustomResult, int) {
return testControllerMethodCustomResult{"<b>internal server error</b>"}, iris.StatusInternalServerError
}
func (c *testControllerMethodResultTypes) GetCustomStruct() testCustomStruct {
return testCustomStruct{"Iris", 2}
}
func (c *testControllerMethodResultTypes) GetCustomStructWithStatusNotOk() (testCustomStruct, int) {
return testCustomStruct{"Iris", 2}, iris.StatusInternalServerError
}
func (c *testControllerMethodResultTypes) GetCustomStructWithContentType() (testCustomStruct, string) {
return testCustomStruct{"Iris", 2}, "text/xml"
}
func (c *testControllerMethodResultTypes) GetCustomStructWithError() (s testCustomStruct, err error) {
s = testCustomStruct{"Iris", 2}
if c.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 TestControllerMethodResultTypes(t *testing.T) {
app := iris.New()
app.Controller("/", new(testControllerMethodResultTypes))
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")
e.GET("/text/and/content/type").Expect().Status(iris.StatusOK).
ContentType("text/html", "utf-8").
Body().Equal("<b>text</b>")
e.GET("/custom/response").Expect().Status(iris.StatusOK).
ContentType("text/html", "utf-8").
Body().Equal("<b>text</b>")
e.GET("/custom/response/with/status/ok").Expect().Status(iris.StatusOK).
ContentType("text/html", "utf-8").
Body().Equal("<b>OK</b>")
e.GET("/custom/response/with/status/not/ok").Expect().Status(iris.StatusInternalServerError).
ContentType("text/html", "utf-8").
Body().Equal("<b>internal server error</b>")
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")
}

77
mvc/method_result_view.go Normal file
View File

@ -0,0 +1,77 @@
package mvc
import (
"strings"
"github.com/kataras/iris/context"
"github.com/kataras/iris/mvc/activator/methodfunc"
)
// 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/using-method-result/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 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 {
ctx.Values().Set(
ctx.Application().ConfigurationReadOnly().GetViewDataContextKey(),
r.Data,
)
}
ctx.View(r.Name)
}
}