Update to v8.3.1 | MVC: RelPath and RelTmpl implemented. Read HISTORY.md

Read HISTORY.md

https://github.com/kataras/iris/blob/master/HISTORY.md#sa-19-august-2017--v831

Former-commit-id: 23f7c1c0dc3bc64f27db591a9b22cd5934337891
This commit is contained in:
kataras 2017-08-19 21:54:33 +03:00
parent 27f0e0b4b1
commit 8c1a4da804
11 changed files with 352 additions and 25 deletions

View File

@ -18,6 +18,84 @@ 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`.
# 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.
Let's refactor our [ProfileController](_examples/mvc/controller-with-model-and-view/main.go) enhancemed by these two new functions.
```go
func (pc *ProfileController) tmpl(relativeTmplPath string) {
// the relative template files directory of this controller.
views := pc.RelTmpl()
pc.Tmpl = views + relativeTmplPath
}
func (pc *ProfileController) match(relativeRequestPath string) bool {
// the relative request path of this controller.
path := pc.RelPath()
return path == relativeRequestPath
}
func (pc *ProfileController) Get() {
// requested: "/profile"
// so relative path is "/" because of the ProfileController.
if pc.match("/") {
// views/profile/index.html
pc.tmpl("index.html")
return
}
// requested: "/profile/browse"
// so relative path is "/browse".
if pc.match("/browse") {
pc.Path = "/profile"
return
}
// requested: "/profile/me"
// so the relative path is "/me"
if pc.match("/me") {
// views/profile/me.html
pc.tmpl("me.html")
return
}
// requested: "/profile/$ID"
// so the relative path is "/$ID"
id, _ := pc.Params.GetInt64("id")
user, found := pc.DB.GetUserByID(id)
if !found {
pc.Status = iris.StatusNotFound
// views/profile/notfound.html
pc.tmpl("notfound.html")
pc.Data["ID"] = id
return
}
// views/profile/profile.html
pc.tmpl("profile.html")
pc.User = user
}
```
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.
@ -206,6 +284,10 @@ 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.
@ -217,6 +299,7 @@ Optional `EndRequest(ctx)` function to perform any finalization after any method
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).
**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.

View File

@ -38,7 +38,7 @@ Iris may have reached version 8, but we're not stopping there. We have many feat
### 📑 Table of contents
* [Installation](#-installation)
* [Latest changes](https://github.com/kataras/iris/blob/master/HISTORY.md#fr-18-august-2017--v830)
* [Latest changes](https://github.com/kataras/iris/blob/master/HISTORY.md#sa-19-august-2017--v831)
* [Learn](#-learn)
* [HTTP Listening](_examples/#http-listening)
* [Configuration](_examples/#configuration)
@ -69,7 +69,6 @@ Iris may have reached version 8, but we're not stopping there. We have many feat
- [Iris, a modular web framework](https://medium.com/@corebreaker/iris-web-cd684b4685c7)
- [Deploying a Iris Golang app in hasura](https://docs.hasura.io/0.14/tutorials/deploying-a-go-iris-app.html)
- [How to Turn an Android Device into a Web Server](https://twitter.com/ThePracticalDev/status/892022594031017988)
- [Echo vs Iris vs .Net Core benchmark](https://medium.com/geekseat-development-blog/go-vs-iris-benchmark-f7822f84e86f)
- [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)
* [Versioning](#-version)
@ -339,11 +338,11 @@ $ go get -u github.com/iris-contrib/middleware/...
Iris exceeded all expectations, started as one-man project.
- 7288 github stars
- 766 github forks
- 7300 github stars
- 778 github forks
- 1m total views at its documentation
- ~800$ at donations, small amount for the work we put here but it's a good start
- ~554 reported bugs fixed
- ~819$ at donations, small amount for the work we put here but it's a good start
- ~557 reported bugs fixed
- ~30 community feature requests have been implemented
### 📌 Version

View File

@ -1 +1 @@
8.3.0:https://github.com/kataras/iris/blob/master/HISTORY.md#fr-18-august-2017--v830
8.3.1:https://github.com/kataras/iris/blob/master/HISTORY.md#sa-19-august-2017--v831

View File

@ -60,40 +60,53 @@ type ProfileController struct {
DB *DB
}
// These two functions are totally optional, of course, don't use them if you
// don't need such as a coupled behavior.
func (pc *ProfileController) tmpl(relativeTmplPath string) {
// the relative templates directory of this controller.
views := pc.RelTmpl()
pc.Tmpl = views + relativeTmplPath
}
func (pc *ProfileController) match(relativeRequestPath string) bool {
// the relative request path based on this controller's name.
path := pc.RelPath()
return path == relativeRequestPath
}
// 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"
// requested: "/profile"
if pc.match("/") {
pc.tmpl("index.html")
return
}
// requested: /profile/browse
// requested: "/profile/browse"
// this exists only to proof the concept of changing the path:
// it will result to a redirection.
if path == "/profile/browse" {
if pc.match("/browse") {
pc.Path = "/profile"
return
}
// requested: /profile/me path
if path == "/profile/me" {
pc.Tmpl = "profile/me.html"
// requested: "/profile/me"
if pc.match("/me") {
pc.tmpl("me.html")
return
}
// requested: /profile/$ID
// 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.tmpl("notfound.html")
pc.Data["ID"] = id
return
}
pc.Tmpl = "profile/profile.html"
pc.tmpl("profile.html")
pc.User = user
}

7
doc.go
View File

@ -35,7 +35,7 @@ Source code and other details for the project are available at GitHub:
Current Version
8.3.0
8.3.1
Installation
@ -808,6 +808,10 @@ 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.
@ -819,6 +823,7 @@ Optional `EndRequest(ctx)` function to perform any finalization after any method
Inheritance, see for example our `mvc.SessionController`, it has the `mvc.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.
Using Iris MVC for code reuse
By creating components that are independent of one another,

View File

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

View File

@ -16,7 +16,7 @@ type (
// think it as a "supervisor" of your Controller which
// cares about you.
TController struct {
// the type of the user/dev's "c" controller (interface{})
// 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.
@ -123,6 +123,7 @@ var (
// End-User doesn't need to have any knowledge of this if she/he doesn't want to implement
// a new Controller type.
type BaseController interface {
SetName(name string)
BeginRequest(ctx context.Context)
EndRequest(ctx context.Context)
}
@ -201,6 +202,7 @@ func ActivateController(base BaseController, bindValues []interface{},
// builds the handler for a type based on the method index (i.e Get() -> [0], Post() -> [1]).
func buildMethodHandler(t TController, methodFuncIndex int) context.Handler {
elem := t.Type.Elem()
ctrlName := t.Value.Type().Name()
/*
// good idea, it speeds up the whole thing by ~1MB per 20MB at my personal
// laptop but this way the Model for example which is not a persistence
@ -255,6 +257,7 @@ func buildMethodHandler(t TController, methodFuncIndex int) context.Handler {
// but if somone tries to "crack" that, then just stop the world in order to be notified,
// we don't want to go away from that type of mistake.
b := c.Interface().(BaseController)
b.SetName(ctrlName)
// init the request.
b.BeginRequest(ctx)

View File

@ -1,6 +1,9 @@
package mvc
import (
"reflect"
"strings"
"github.com/kataras/iris/context"
"github.com/kataras/iris/core/memstore"
"github.com/kataras/iris/mvc/activator"
@ -58,8 +61,29 @@ import (
//
// Look `core/router/APIBuilder#Controller` method too.
type Controller struct {
// path and path params.
Path string
// Name contains the current controller's full name.
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.
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"
relPath string
// request path and its parameters, read-write.
// Path is the current request path.
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,
@ -67,6 +91,12 @@ type Controller struct {
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/".
relTmpl string
// view read and write,
// can be already set-ed by previous handlers as well.
Layout string
@ -77,9 +107,74 @@ type Controller struct {
Ctx context.Context
}
// SetName sets the controller's full name.
// It's called internally.
func (c *Controller) SetName(name string) {
c.Name = name
}
func (c *Controller) getNameWords() []string {
if len(c.nameAsWords) == 0 {
c.nameAsWords = findCtrlWords(c.Name)
}
return c.nameAsWords
}
const slashStr = "/"
// 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"
//
// 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 jsut return an empty slash.
return slashStr
}
// [1:]to ellimuate the prefixes like "//"
// request path has always "/"
rel = strings.Replace(c.Ctx.Path()[1:], rel, "", 1)
if rel == "" {
rel = slashStr
}
c.relPath = rel
}
return c.relPath
}
// 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
}
return c.relTmpl
}
// 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
@ -101,6 +196,7 @@ func (c *Controller) BeginRequest(ctx context.Context) {
// 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 path := c.Path; path != "" && path != ctx.Path() {
@ -126,4 +222,6 @@ func (c *Controller) EndRequest(ctx context.Context) {
}
}
var ctrlSuffix = reflect.TypeOf(Controller{}).Name()
var _ activator.BaseController = &Controller{}

View File

@ -282,3 +282,60 @@ func TestControllerBind(t *testing.T) {
e.GET("/deep").Expect().Status(httptest.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(iris.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]iris.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(httptest.StatusOK).JSON().Equal(tt)
}
}

38
mvc/strutil.go Normal file
View File

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

31
mvc/strutil_test.go Normal file
View File

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