From 244a59e05520df89f22593bbdbd5d11bc1283370 Mon Sep 17 00:00:00 2001 From: "Gerasimos (Makis) Maropoulos" Date: Tue, 14 Feb 2017 05:54:11 +0200 Subject: [PATCH] 20 days of unstoppable work. Waiting fo go 1.8, I didn't finish yet, some touches remains. Former-commit-id: ed84f99c89f43fe5e980a8e6d0ee22c186f0e1b9 --- .github/ISSUE_TEMPLATE.md | 2 +- .github/PULL_REQUEST_TEMPLATE.md | 61 + .gitignore | 2 + DONATIONS.md | 4 +- HISTORY.md | 1002 ++++++- README.md | 64 +- adaptors/cors/LICENSE | 19 + adaptors/cors/cors.go | 33 + adaptors/gorillamux/LICENSE | 28 + adaptors/gorillamux/README.md | 103 + adaptors/gorillamux/gorillamux.go | 150 + adaptors/httprouter/LICENSE | 47 + adaptors/httprouter/_example/main.go | 39 + adaptors/httprouter/httprouter.go | 725 +++++ adaptors/httprouter/urlpath.go | 23 + adaptors/typescript/LICENSE | 21 + adaptors/typescript/README.md | 89 + adaptors/typescript/_example/main.go | 31 + adaptors/typescript/_example/www/index.html | 8 + .../typescript/_example/www/scripts/app.ts | 16 + adaptors/typescript/config.go | 192 ++ adaptors/typescript/editor/LICENSE | 21 + adaptors/typescript/editor/_example/main.go | 35 + .../typescript/editor/_example/www/index.html | 8 + .../editor/_example/www/scripts/app.ts | 16 + .../editor/_example/www/scripts/tsconfig.json | 22 + adaptors/typescript/editor/config.go | 78 + adaptors/typescript/editor/editor.go | 216 ++ {utils => adaptors/typescript/npm}/exec.go | 2 +- adaptors/typescript/npm/npm.go | 124 + adaptors/typescript/typescript.go | 236 ++ adaptors/view/_examples/overview/main.go | 93 + .../view/_examples/overview/templates/hi.html | 8 + .../view/_examples/template_binary/bindata.go | 237 ++ .../view/_examples/template_binary/main.go | 24 + .../template_binary/templates/hi.html | 8 + .../view/_examples/template_html_0/main.go | 23 + .../template_html_0/templates/hi.html | 8 + .../view/_examples/template_html_1/main.go | 32 + .../template_html_1/templates/layout.html | 11 + .../template_html_1/templates/mypage.html | 4 + .../view/_examples/template_html_2/README.md | 3 + .../view/_examples/template_html_2/main.go | 49 + .../templates/layouts/layout.html | 12 + .../templates/layouts/mylayout.html | 12 + .../template_html_2/templates/page1.html | 7 + .../templates/partials/page1_partial1.html | 3 + .../view/_examples/template_html_3/main.go | 53 + .../template_html_3/templates/page.html | 27 + adaptors/view/_examples/template_html_4/hosts | 32 + .../view/_examples/template_html_4/main.go | 53 + .../template_html_4/templates/page.html | 16 + adaptors/view/adaptor.go | 136 + adaptors/view/amber.go | 48 + adaptors/view/django.go | 93 + adaptors/view/handlebars.go | 62 + adaptors/view/html.go | 92 + adaptors/view/pug.go | 21 + addr.go | 299 ++ configuration.go | 86 +- configuration_test.go | 88 - context.go | 349 ++- context_test.go | 827 ------ doc.go | 521 ++++ docs/QUICK_START.md | 865 ------ docs/README.md | 12 - examples/README.md | 16 - webfs.go => fs.go | 24 +- handler.go | 159 ++ http.go | 1544 ----------- http_test.go | 792 ------ httptest/httptest.go | 15 +- iris.go | 2412 ++++------------- iris/doc.go | 2 +- iris/main.go | 2 +- middleware/README.md | 84 - middleware/basicauth/_example/main.go | 56 + middleware/basicauth/basicauth.go | 131 + middleware/basicauth/config.go | 48 + middleware/i18n/LICENSE | 167 ++ middleware/i18n/README.md | 69 + .../i18n/_example/locales/locale_el-GR.ini | 1 + .../i18n/_example/locales/locale_en-US.ini | 1 + .../i18n/_example/locales/locale_zh-CN.ini | 1 + middleware/i18n/_example/main.go | 50 + middleware/i18n/config.go | 18 + middleware/i18n/i18n.go | 101 + middleware/logger/_example/main.go | 53 + middleware/logger/config.go | 21 + middleware/logger/logger.go | 62 + middleware/recover/_example/main.go | 31 + middleware/recover/recover.go | 58 + plugin.go | 615 ----- plugin_test.go | 205 -- plugins/README.md | 19 - policy.go | 432 +++ response_recorder.go | 4 +- response_writer.go | 2 +- response_writer_test.go | 158 -- route.go | 332 +++ router.go | 676 +++++ router_policy_test.go | 189 ++ status.go | 215 ++ template.go | 137 - transactions.go => transaction.go | 0 utils/bytes.go | 40 - utils/installer.go | 29 - websocket.go | 10 +- 108 files changed, 9016 insertions(+), 7596 deletions(-) create mode 100644 adaptors/cors/LICENSE create mode 100644 adaptors/cors/cors.go create mode 100644 adaptors/gorillamux/LICENSE create mode 100644 adaptors/gorillamux/README.md create mode 100644 adaptors/gorillamux/gorillamux.go create mode 100644 adaptors/httprouter/LICENSE create mode 100644 adaptors/httprouter/_example/main.go create mode 100644 adaptors/httprouter/httprouter.go create mode 100644 adaptors/httprouter/urlpath.go create mode 100644 adaptors/typescript/LICENSE create mode 100644 adaptors/typescript/README.md create mode 100644 adaptors/typescript/_example/main.go create mode 100644 adaptors/typescript/_example/www/index.html create mode 100644 adaptors/typescript/_example/www/scripts/app.ts create mode 100644 adaptors/typescript/config.go create mode 100644 adaptors/typescript/editor/LICENSE create mode 100644 adaptors/typescript/editor/_example/main.go create mode 100644 adaptors/typescript/editor/_example/www/index.html create mode 100644 adaptors/typescript/editor/_example/www/scripts/app.ts create mode 100644 adaptors/typescript/editor/_example/www/scripts/tsconfig.json create mode 100644 adaptors/typescript/editor/config.go create mode 100644 adaptors/typescript/editor/editor.go rename {utils => adaptors/typescript/npm}/exec.go (99%) create mode 100644 adaptors/typescript/npm/npm.go create mode 100644 adaptors/typescript/typescript.go create mode 100644 adaptors/view/_examples/overview/main.go create mode 100644 adaptors/view/_examples/overview/templates/hi.html create mode 100644 adaptors/view/_examples/template_binary/bindata.go create mode 100644 adaptors/view/_examples/template_binary/main.go create mode 100644 adaptors/view/_examples/template_binary/templates/hi.html create mode 100644 adaptors/view/_examples/template_html_0/main.go create mode 100644 adaptors/view/_examples/template_html_0/templates/hi.html create mode 100644 adaptors/view/_examples/template_html_1/main.go create mode 100644 adaptors/view/_examples/template_html_1/templates/layout.html create mode 100644 adaptors/view/_examples/template_html_1/templates/mypage.html create mode 100644 adaptors/view/_examples/template_html_2/README.md create mode 100644 adaptors/view/_examples/template_html_2/main.go create mode 100644 adaptors/view/_examples/template_html_2/templates/layouts/layout.html create mode 100644 adaptors/view/_examples/template_html_2/templates/layouts/mylayout.html create mode 100644 adaptors/view/_examples/template_html_2/templates/page1.html create mode 100644 adaptors/view/_examples/template_html_2/templates/partials/page1_partial1.html create mode 100644 adaptors/view/_examples/template_html_3/main.go create mode 100644 adaptors/view/_examples/template_html_3/templates/page.html create mode 100644 adaptors/view/_examples/template_html_4/hosts create mode 100644 adaptors/view/_examples/template_html_4/main.go create mode 100644 adaptors/view/_examples/template_html_4/templates/page.html create mode 100644 adaptors/view/adaptor.go create mode 100644 adaptors/view/amber.go create mode 100644 adaptors/view/django.go create mode 100644 adaptors/view/handlebars.go create mode 100644 adaptors/view/html.go create mode 100644 adaptors/view/pug.go create mode 100644 addr.go delete mode 100644 configuration_test.go delete mode 100644 context_test.go create mode 100644 doc.go delete mode 100644 docs/QUICK_START.md delete mode 100644 docs/README.md delete mode 100644 examples/README.md rename webfs.go => fs.go (88%) create mode 100644 handler.go delete mode 100644 http.go delete mode 100644 http_test.go delete mode 100644 middleware/README.md create mode 100644 middleware/basicauth/_example/main.go create mode 100644 middleware/basicauth/basicauth.go create mode 100644 middleware/basicauth/config.go create mode 100644 middleware/i18n/LICENSE create mode 100644 middleware/i18n/README.md create mode 100644 middleware/i18n/_example/locales/locale_el-GR.ini create mode 100644 middleware/i18n/_example/locales/locale_en-US.ini create mode 100644 middleware/i18n/_example/locales/locale_zh-CN.ini create mode 100644 middleware/i18n/_example/main.go create mode 100644 middleware/i18n/config.go create mode 100644 middleware/i18n/i18n.go create mode 100644 middleware/logger/_example/main.go create mode 100644 middleware/logger/config.go create mode 100644 middleware/logger/logger.go create mode 100644 middleware/recover/_example/main.go create mode 100644 middleware/recover/recover.go delete mode 100644 plugin.go delete mode 100644 plugin_test.go delete mode 100644 plugins/README.md create mode 100644 policy.go delete mode 100644 response_writer_test.go create mode 100644 route.go create mode 100644 router.go create mode 100644 router_policy_test.go create mode 100644 status.go delete mode 100644 template.go rename transactions.go => transaction.go (100%) delete mode 100644 utils/bytes.go delete mode 100644 utils/installer.go diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md index a63ed0e5..1f822f11 100644 --- a/.github/ISSUE_TEMPLATE.md +++ b/.github/ISSUE_TEMPLATE.md @@ -1,4 +1,4 @@ -- Version : **6.1.2** +- Version : **6.2.0** - Operating System: diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index 12daacd0..9843d477 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -1 +1,62 @@ If you are interested in contributing to the Iris project, please see the document [CONTRIBUTING](https://github.com/kataras/iris/blob/master/.github/CONTRIBUTING.md). + +##### Note that I do not accept pull requests here and that I use the issue tracker for bug reports and proposals only. Please ask questions on the [https://kataras.rocket.chat/channel/iris][Chat] or [http://stackoverflow.com/](http://stackoverflow.com). + +> **it's not personal to somebody specific**, I don't want to attack to anyone, I am polite but I had to write these complaints, eventually. + +I wanted to answer on some users who complaining that I'm not accepting PRs to the main code base. + +I am accepting PRs to the [iris-contrib](https://github.com/iris-contrib) and I see how things were gone. +As you already know I'm not a native english speaker, the gitbook PRs were **all excellent and I thanks you from my deep of my heart!**. My complain and my personal experience with PRs +you will read below is a fact for all repositories **except the gitbook's PRs and zaplogger middleware**. + + +There weren't many PRs but anyway...some people 'attacking' me because I code all alone, this is what it is. + +Let's take for example the `cors` middleware. +Yes that middleware which didn't worked for you and the 80% of the overral middleware's issues were for this and only this middleware. + + +The guy who did the first PR to the cors middleware didn't answer to your issues, I had to do it that, +I had to answer for another's code. + +It's true that I don't like these situations and this is the reason why I'm always avoid of accepting PRs to the main iris codebase. +Do you imagine what would happen if I was accepting PRs here, to the main iris repository?... I don't. +Not because the community sucks and they are not good as me, maybe they are better but they are not **responsible** for their code +and I'm sorry for telling you that but this is what I received as an experience of letting people to 'help' at the beggining: +better safe and 'alone' rather than a transformation of the framework to a collection of big 'anarchy' and unnecessary features. + +**Let's explain** what I mean by 'anarchy': +Some github users made PRs without even posting a new github issue to describe the idea or the bug before push their code, +surprisingly 100% of the changes to these PRs were for things that already implemented by +the framework but they had no clue because they didn't care or didn't have time to view the code closer, they just wanted to make an one-line PR and disappear. + +We don't have any guarantee that an author of any PR will be here to answer to users or to fix a bug or +to make changes on future iris changes(and as ultimately this proved most of them they didn't care about your questions, your bug reports and you). **I'm responsible to individual developers and companies who using Iris to their projects.** +As you already noticed I'm online almost all the day to answer to your questions (almost instantly), fixing bugs and upgrading the code. +**This is why this Framework is not like others, it has the abiliy to be always synced with the latest trends and features happens around the world.** + + +Yes, I did disappointed by a small group of the community, I was waiting for more activity, I was imaging big work all together and the open-source idea. +After the results I came up, I had to pause this 'idea' on my mind for a very long time. +This is why I never ask you for a PR here, in the main repository, your contribution can be 'only' by +reporting bugs and feature requests, **these are not a small part, things like these made this framework so ideal.** + +**The main repository's flow is: community recommends -> I code. I am not special, just a regular guy who really loves to code and happens to have all the world's time to code for Iris. No, I'm not a rich guy, I had some money from my previous job but over time as you can imagine, they are spending on my body's requirements, hopefuly we have nice guys and women who cares about these things and making donations that helping me to survive, as I left from my job months ago in order to start coding for Iris explicitly.**. + + + +From the beggining, the project, gained incredible popularity in short-time, +the first month we broke all the rules, we were first on global-github-languages trending list. +**I always say 'we' because you're helping a lot, more than you can even imagine, with your bug reports and feature requests.** +So don't try to fool our selves, Iris has unique features and it's really one of the fastest web frameworks in globe, but these alone are nothing. + + +So why people loves and trust this framework for a long time of period? Because + **they know where to find the guy who coded this and can ask anything, they get an answer instantly, they know where to stand if something goes bad to their app.** + + +BUT I'm choosing to give a second chance to people who want to contribution by code. If this time, the people who want to contribute by code do their jobs and mantain their responsibility to iris' users, there are good chances on the future that I will choose some of them, based on the code quality that will be pushed and their activity, in order to ask them for collaboration on the main iris repo too. + +So the **[iris-contrib](https://github.com/iris-contrib) organisation and all its repositories will stay open for PRs.** Hopefully future-authors will be more responsible after this long text I wrote here, let's see how things will go on. **Only one term to future authors**: from now and on I want you to be responsible and answer to the future users' issues that are relative to your code. PR's authors must provide adequate support. +**Users is the most important part, we, as software authors, have to respect them. I don't accept any form of contempt to them(users) by anyone.** diff --git a/.gitignore b/.gitignore index 61d9bd43..06c382d0 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,4 @@ .settings .project +2M_donation_timeline.cpd +performance_tips.txt diff --git a/DONATIONS.md b/DONATIONS.md index dac6cccc..051f6cfe 100644 --- a/DONATIONS.md +++ b/DONATIONS.md @@ -54,6 +54,8 @@ I'm grateful for all the generous donations. Iris is fully funded by these dona - [Thanos V.](http://mykonosbiennale.com/) donated 20 EUR at Jenuary 16 of 2017 +- [George Opritescu](https://github.com/International) donated 20 EUR at February 7 of 2017 + > * The name or/and github username link added after donator's approvement via e-mail. #### Report, so far @@ -62,4 +64,4 @@ I'm grateful for all the generous donations. Iris is fully funded by these dona - **Thanks** to all of **You**, 424 EUR donated to [Holy Metropolis of Ioannina](http://www.imioanninon.gr/main/) for clothes, foods and medications for our fellow human beings. -**Available**: VAT(50) + VAT(20) + VAT(20) + VAT(50) + VAT(6) + VAT(20) + VAT(100) + VAT(20) + VAT(20) + VAT(50) + VAT(30) + VAT(50) + VAT(20) + VAT(5) + VAT(25) + VAT(20) - 13 - 424 = 47,45 + 18,97 + 18,61 + 47,05 + 5,34 + 18,97 + 98,04 + 18,97 + 18,61 + 47,95 + 28,24 + 47,05 + 18,97 + 4,59 + 23,80 + 18,77 - 13 - 424 = **43,49 EUR** +**Available**: VAT(50) + VAT(20) + VAT(20) + VAT(50) + VAT(6) + VAT(20) + VAT(100) + VAT(20) + VAT(20) + VAT(50) + VAT(30) + VAT(50) + VAT(20) + VAT(5) + VAT(25) + VAT(20) + VAT(20) - 13 - 424 = 47,45 + 18,97 + 18,61 + 47,05 + 5,34 + 18,97 + 98,04 + 18,97 + 18,61 + 47,95 + 28,24 + 47,05 + 18,97 + 4,59 + 23,80 + 18,77 + 18,97 - 13 - 424 = **62,46 EUR** diff --git a/HISTORY.md b/HISTORY.md index ec5a9bf0..88d5db59 100644 --- a/HISTORY.md +++ b/HISTORY.md @@ -2,52 +2,1004 @@ **How to upgrade**: remove your `$GOPATH/src/github.com/kataras` folder, open your command-line and execute this command: `go get -u github.com/kataras/iris/iris`. -**NOTICE**: -The next Iris version will be released **when go v1.8 stable** will be ready (it's a matter of days). +## 6.1.4 -> 6.2.0 + +> Note: I want you to know that I spent more than 200 hours (16 days of ~10-15 hours per-day, do the math) for this release, two days to write these changes, please read the sections before think that you have an issue and post a new question, thanks! -The future version will include a ton of new features and fixes. +Users already notified for some breaking-changes, this section will help you +to adapt the new changes to your application, it contains an overview of the new features too. -Users should prepare their apps for: +- Router (two lines to add, new features) +- Template engines (two lines to add, same features as before, except their easier configuration) +- Basic middleware, that have been written by me, are transfared to the main repository[/middleware](https://github.com/kataras/iris/tree/master/middleware) with a lot of improvements to the `recover middleware` (see the next) +- `func(http.ResponseWriter, r *http.Request, next http.HandlerFunc)` signature is fully compatible using `iris.ToHandler` helper wrapper func, without any need of custom boilerplate code. So all net/http middleware out there are supported, no need to re-invert the world here, search to the internet and you'll find a suitable to your case. -- Remove: the deprecated `.API`, it should be removed after v5(look on the v5 history tag), it's already removed from book after v5. -- **MOST IMPORTANT: REMOVE** Package-level exported `iris.Default's methods and variables`, `iris.Default` will exists but methods like `iris.Handle` should be replace with `iris.Default.Handle` or `app := iris.New(); app.Handle(...); // like before, these will never change`. - - Why? - Iris is bigger than it was, we need to keep a limit on the exported functions to help users understand the flow much easier. - Users often asked questions about built'n features and functions usage because they are being confused of the big public API (`iris.` vs `app := iris.New(); app.`). - This removal will also let me to describe the types with more sense. +Fixes: -- NEW feature: `app.Adapt(iris.Policy)` which you will be able to adapt a **custom http router**, **custom template functions**, **custom http router wrappers**, **flow events** and much more. I'll cover these on book and examples when it will be released. +- Websocket improvements and fix errors when using custom golang client +- Sessions performance improvements +- Fix cors by using `rs/cors` and add a new adaptor to be able to wrap the entire router +- Fix and improve oauth/oauth2 plugin(now adaptor) +- Improve and fix recover middleware +- Fix typescript compiler and hot-reloader plugin(now adaptor) +- Fix and improve the cloud-editor `alm/alm-tools` plugin(now adaptor) +- Fix gorillamux serve static files (custom routers are supported with a workaround, not a complete solution as they are now) +- Fix `iris run main.go` app reload while user saved the file from gogland -- Replace: `.Plugins.` with `EventPolicy`: `app.Adapt(iris.EventPolicy{ Boot: func(*iris.Framework){}} )`. - - Why? +Changes: - To be ready and don't confuse future users with the go's plugin mechanism. - A 'real plugin system' is coming with go 1.8 BUT we will not use that ready because - they are not ready for all operating systems. +- Remove all the package-level functions and variables for a default `*iris.Framework, iris.Default` +- Remove `.API`, use `iris.Handle/.HandleFunc/.Get/.Post/.Put/.Delete/.Trace/.Options/.Use/.UseFunc/.UseGlobal/.Party/` instead +- Remove `.Logger`, `.Config.IsDevelopment`, `.Config.LoggerOut`, `.Config.LoggerPrefix` you can adapt a logger which will log to each log message mode by `app.Adapt(iris.DevLogger())` or adapt a new one, it's just a `func(mode iris.LogMode, message string)`. +- Remove `.Config.DisableTemplateEngines`, are disabled by-default, you have to `.Adapt` a view engine by yourself +- Remove `context.RenderTemplateSource` you should make a new template file and use the `iris.Render` to specify an `io.Writer` like `bytes.Buffer` +- Remove `plugins`, replaced with more pluggable echosystem that I designed from zero on this release, named `Policy` [Adaptors](https://github.com/kataras/iris/tree/master/adaptors) (all plugins have been converted, fixed and improvement, except the iriscontrol). +- `context.Log(string,...interface{})` -> `context.Log(iris.LogMode, string)` -- Replace: `.AcquireCtx/.ReleaseCtx` with `app.Context.Acquire/Release/Run`. +- https://github.com/iris-contrib/plugin -> https://github.com/iris-contrib/adaptors -- IMPROVEMENT: Now you're able to pass an `func(http.ResponseWriter, *http.Request, http.HandlerFunc)` third-party net/http middleware(Chain-of-responsibility pattern) using the `iris.ToHandler` wrapper func without any other custom boilerplate. - * already pushed to the current version +- `import "github.com/iris-contrib/middleware/basicauth"` -> `import "github.com/kataras/iris/middleware/basicauth"` +- `import "github.com/iris-contrib/middleware/i18n"` -> `import "github.com/kataras/iris/middleware/i18n"` +- `import "github.com/iris-contrib/middleware/logger"` -> `import "github.com/kataras/iris/middleware/logger"` +- `import "github.com/iris-contrib/middleware/recovery"` -> `import "github.com/kataras/iris/middleware/recover"` +- `import "github.com/iris-contrib/plugin/typescript"` -> `import "github.com/kataras/iris/adaptors/typescript"` +- `import "github.com/iris-contrib/plugin/editor"` -> `import "github.com/kataras/iris/adaptors/typescript/editor"` +- `import "github.com/iris-contrib/plugin/cors"` -> `import "github.com/kataras/iris/adaptors/cors"` +- `import "github.com/iris-contrib/plugin/gorillamux"` -> `import "github.com/kataras/iris/adaptors/gorillamux"` +- `import github.com/iris-contrib/plugin/oauth"` -> `import "github.com/iris-contrib/adaptors/oauth"` + + +- `import "github.com/kataras/go-template/html"` -> `import "github.com/kataras/iris/adaptors/view"` +- `import "github.com/kataras/go-template/django"` -> `import "github.com/kataras/iris/adaptors/view"` +- `import "github.com/kataras/go-template/pug"` -> `import "github.com/kataras/iris/adaptors/view"` +- `import "github.com/kataras/go-template/handlebars"` -> `import "github.com/kataras/iris/adaptors/view"` +- `import "github.com/kataras/go-template/amber"` -> `import "github.com/kataras/iris/adaptors/view"` + +**Read more below** for the lines you have to change. Package-level removal is critical, you will have build-time errors. Router(less) is MUST, otherwise your app will fatal with a detailed error message. + +> If I missed something please [chat](https://kataras.rocket.chat/channel/iris). + + +### Router(less) + +**Iris server does not contain a default router anymore**, yes your eyes are ok. + +This decision came up because of your requests of using other routers than the iris' defaulted. +At the past I gave you many workarounds, but they are just workarounds, not a complete solution. + +**Don't worry:** + +- you have to add only two lines, one is the `import path` and another is the `.Adapt`, after the `iris.New()`, so it can be tolerated. +- you are able to use all iris' features as you used before, **the API for routing has not been changed**. + +Two routers available to use, today: + + +- [httprouter](https://github.com/kataras/iris/tree/master/adaptors/httprouter), the old defaulted. A router that can be adapted, it's a custom version of https://github.comjulienschmidt/httprouter which is edited to support iris' subdomains, reverse routing, custom http errors and a lot features, it should be a bit faster than the original too because of iris' Context. It uses `/mypath/:firstParameter/path/:secondParameter` and `/mypath/*wildcardParamName` . + + +Example: + +```go +package main + +import ( + "github.com/kataras/iris" + "github.com/kataras/iris/adaptors/httprouter" // <---- NEW +) + +func main() { + app := iris.New() + app.Adapt(iris.DevLogger()) + app.Adapt(httprouter.New()) // <---- NEW + + + app.OnError(iris.StatusNotFound, func(ctx *iris.Context){ + ctx.HTML(iris.StatusNotFound, "

custom http error page

") + }) + + + app.Get("/healthcheck", h) + + gamesMiddleware := func(ctx *iris.Context) { + println(ctx.Method() + ": " + ctx.Path()) + ctx.Next() + } + + games:= app.Party("/games", gamesMiddleware) + { // braces are optional of course, it's just a style of code + games.Get("/:gameID/clans", h) + games.Get("/:gameID/clans/clan/:publicID", h) + games.Get("/:gameID/clans/search", h) + + games.Put("/:gameID/players/:publicID", h) + games.Put("/:gameID/clans/clan/:publicID", h) + + games.Post("/:gameID/clans", h) + games.Post("/:gameID/players", h) + games.Post("/:gameID/clans/:publicID/leave", h) + games.Post("/:gameID/clans/:clanPublicID/memberships/application", h) + games.Post("/:gameID/clans/:clanPublicID/memberships/application/:action", h) + games.Post("/:gameID/clans/:clanPublicID/memberships/invitation", h) + games.Post("/:gameID/clans/:clanPublicID/memberships/invitation/:action", h) + games.Post("/:gameID/clans/:clanPublicID/memberships/delete", h) + games.Post("/:gameID/clans/:clanPublicID/memberships/promote", h) + games.Post("/:gameID/clans/:clanPublicID/memberships/demote", h) + } + + app.Get("/anything/*anythingparameter", func(ctx *iris.Context){ + s := ctx.Param("anythingparameter") + ctx.Writef("The path after /anything is: %s",s) + }) + + app.Listen(":80") + + /* + gameID = 1 + publicID = 2 + clanPublicID = 22 + action = 3 + + GET + http://localhost/healthcheck + http://localhost/games/1/clans + http://localhost/games/1/clans/clan/2 + http://localhost/games/1/clans/search + + PUT + http://localhost/games/1/players/2 + http://localhost/games/1/clans/clan/2 + + POST + http://localhost/games/1/clans + http://localhost/games/1/players + http://localhost/games/1/clans/2/leave + http://localhost/games/1/clans/22/memberships/application -> 494 + http://localhost/games/1/clans/22/memberships/application/3- > 404 + http://localhost/games/1/clans/22/memberships/invitation + http://localhost/games/1/clans/22/memberships/invitation/3 + http://localhost/games/1/clans/2/memberships/delete + http://localhost/games/1/clans/22/memberships/promote + http://localhost/games/1/clans/22/memberships/demote + + */ +} + +func h(ctx *iris.Context) { + ctx.HTML(iris.StatusOK, "

Path

"+ctx.Path()) +} + +``` + +- [gorillamux](https://github.com/kataras/iris/tree/master/adaptors/gorillamux), a router that can be adapted, it's the https://github.com/gorilla/mux which supports subdomains, custom http errors, reverse routing, pattern matching via regex and the rest of the iris' features. + + +Example: + +```go +package main + +import ( + "github.com/kataras/iris" + "github.com/kataras/iris/adaptors/gorillamux" // <---- NEW +) + +func main() { + app := iris.New() + app.Adapt(iris.DevLogger()) + app.Adapt(gorillamux.New()) // <---- NEW + + + app.OnError(iris.StatusNotFound, func(ctx *iris.Context){ + ctx.HTML(iris.StatusNotFound, "

custom http error page

") + }) + + + app.Get("/healthcheck", h) + + gamesMiddleware := func(ctx *iris.Context) { + println(ctx.Method() + ": " + ctx.Path()) + ctx.Next() + } + + games:= app.Party("/games", gamesMiddleware) + { // braces are optional of course, it's just a style of code + games.Get("/{gameID:[0-9]+}/clans", h) + games.Get("/{gameID:[0-9]+}/clans/clan/{publicID:[0-9]+}", h) + games.Get("/{gameID:[0-9]+}/clans/search", h) + + games.Put("/{gameID:[0-9]+}/players/{publicID:[0-9]+}", h) + games.Put("/{gameID:[0-9]+}/clans/clan/{publicID:[0-9]+}", h) + + games.Post("/{gameID:[0-9]+}/clans", h) + games.Post("/{gameID:[0-9]+}/players", h) + games.Post("/{gameID:[0-9]+}/clans/{publicID:[0-9]+}/leave", h) + games.Post("/{gameID:[0-9]+}/clans/{clanPublicID:[0-9]+}/memberships/application", h) + games.Post("/{gameID:[0-9]+}/clans/{clanPublicID:[0-9]+}/memberships/application/:action", h) + games.Post("/{gameID:[0-9]+}/clans/{clanPublicID:[0-9]+}/memberships/invitation", h) + games.Post("/{gameID:[0-9]+}/clans/{clanPublicID:[0-9]+}/memberships/invitation/:action", h) + games.Post("/{gameID:[0-9]+}/clans/{clanPublicID:[0-9]+}/memberships/delete", h) + games.Post("/{gameID:[0-9]+}/clans/{clanPublicID:[0-9]+}/memberships/promote", h) + games.Post("/{gameID:[0-9]+}/clans/{clanPublicID:[0-9]+}/memberships/demote", h) + } + + app.Get("/anything/{anythingparameter:.*}", func(ctx *iris.Context){ + s := ctx.Param("anythingparameter") + ctx.Writef("The path after /anything is: %s",s) + }) + + app.Listen(":80") + + /* + gameID = 1 + publicID = 2 + clanPublicID = 22 + action = 3 + + GET + http://localhost/healthcheck + http://localhost/games/1/clans + http://localhost/games/1/clans/clan/2 + http://localhost/games/1/clans/search + + PUT + http://localhost/games/1/players/2 + http://localhost/games/1/clans/clan/2 + + POST + http://localhost/games/1/clans + http://localhost/games/1/players + http://localhost/games/1/clans/2/leave + http://localhost/games/1/clans/22/memberships/application -> 494 + http://localhost/games/1/clans/22/memberships/application/3- > 404 + http://localhost/games/1/clans/22/memberships/invitation + http://localhost/games/1/clans/22/memberships/invitation/3 + http://localhost/games/1/clans/2/memberships/delete + http://localhost/games/1/clans/22/memberships/promote + http://localhost/games/1/clans/22/memberships/demote + + */ +} + +func h(ctx *iris.Context) { + ctx.HTML(iris.StatusOK, "

Path

"+ctx.Path()) +} + +``` + +**No changes whatever router you use**, only the `path` is changed(otherwise it doesn't make sense to support more than one router). +At the `gorillamux`'s path example we get pattern matching using regexp, at the other hand `httprouter` doesn't provides path validations +but it provides parameter and wildcard parameters too, it's also a lot faster than gorillamux. + +Original Gorilla Mux made my life easier when I had to adapt the reverse routing and subdomains features, it has got these features by its own too, so it was easy. + +Original Httprouter doesn't supports subdomains, multiple paths on different methods, reverse routing, custom http errors, I had to implement +all of them by myself and after adapt them using the policies, it was a bit painful but this is my job. Result: It runs blazy-fast! + + +As we said, all iris' features works as before even if you are able to adapt any custom router. Template funcs that were relative-closed to reverse router, like `{{ url }} and {{ urlpath }}`, works as before too, no change for your app's side need. + + +> I would love to see more routers (as more as they can provide different `path declaration` features) from the community, create an adaptor for an iris' router and I will share your repository to the rest of the users! + + +Adaptors are located [there](https://github.com/kataras/iris/tree/master/adaptors). + +### View engine (5 template engine adaptors) + +At the past, If no template engine was used then iris selected the [html standard](https://github.com/kataras/go-template/tree/master/html). + +**Now, iris doesn't defaults any template engine** (also the `.Config.DisableTemplateEngines` has been removed, it has no use anymore). + +So, again you have to do two changes, the `import path` and the `.Adapt`. + +**Template files are no need to change, the template engines does the same exactly things as before** + + +All of these **five template engines** have common features with common API, like Layout, Template Funcs, Party-specific layout, partial rendering and more. + - **the standard html**, based on [go-template/html](https://github.com/kataras/go-template/tree/master/html), its template parser is the [html/template](https://golang.org/pkg/html/template/). + + - **django**, based on [go-template/django](https://github.com/kataras/go-template/tree/master/django), its template parser is the [pongo2](https://github.com/flosch/pongo2) + + - **pug**, based on [go-template/pug](https://github.com/kataras/go-template/tree/master/pug), its template parser is the [jade](https://github.com/Joker/jade) + + - **handlebars**, based on [go-template/handlebars](https://github.com/kataras/go-template/tree/master/handlebars), its template parser is the [raymond](https://github.com/aymerick/raymond) + + - **amber**, based on [go-template/amber](https://github.com/kataras/go-template/tree/master/amber), its template parser is the [amber](https://github.com/eknkc/amber). + +Each of the template engines has different options, view adaptors are located [here](https://github.com/kataras/iris/tree/master/adaptors/view). + + +Example: + + +```go +package main + +import ( + "github.com/kataras/iris" + "github.com/kataras/iris/adaptors/gorillamux" // <--- NEW (previous section) + "github.com/kataras/iris/adaptors/view" // <--- NEW it contains all the template engines +) + +func main() { + app := iris.New() + app.Adapt(iris.DevLogger()) + app.Adapt(gorillamux.New()) // <--- NEW (previous section) + + // - standard html | view.HTML(...) + // - django | view.Django(...) + // - pug(jade) | view.Pug(...) + // - handlebars | view.Handlebars(...) + // - amber | view.Amber(...) + app.Adapt(view.HTML("./templates", ".html").Reload(true)) // <---- NEW (set .Reload to true when you're in dev mode.) + + // default template funcs: + // + // - {{ url "mynamedroute" "pathParameter_ifneeded"} } + // - {{ urlpath "mynamedroute" "pathParameter_ifneeded" }} + // - {{ render "header.html" }} + // - {{ render_r "header.html" }} // partial relative path to current page + // - {{ yield }} + // - {{ current }} + // + // to adapt custom funcs, use: + app.Adapt(iris.TemplateFuncsPolicy{"myfunc": func(s string) string { + return "hi "+s + }}) // usage inside template: {{ hi "kataras"}} + + app.Get("/hi", func(ctx *iris.Context) { + ctx.MustRender( + "hi.html", // the file name of the template relative to the './templates' + iris.Map{"Name": "Iris"}, // the .Name inside the ./templates/hi.html + iris.Map{"gzip": false}, // enable gzip for big files + ) + + }) + + // http://127.0.0.1:8080/hi + app.Listen(":8080") +} + + +``` + +`.UseTemplate` have been removed and replaced with the `.Adapt` which is using `iris.RenderPolicy` and `iris.TemplateFuncsPolicy` +to adapt the behavior of the custom template engines. + +**BEFORE** +```go +import "github.com/kataras/go-template/django" +// ... +app := iris.New() +app.UseTemplate(django.New()).Directory("./templates", ".html")/*.Binary(...)*/) +``` + +**AFTER** +```go +import "github.com/kataras/iris/adaptors/view" +// ... +app := iris.New() +app.Adapt(view.Django("./templates",".htmll")/*.Binary(...)*/) +``` + +The rest remains the same. Don't forget the real changes were `only import path and .Adapt(imported)`, at general when you see an 'adaptor' these two declarations should happen to your code. + + + +### Package-level functions and variables for `iris.Default` have been removed. + +The form of variable use for an Iris *Framework remains as it was: +```go +app := iris.New() +app.$FUNCTION/$VARIABLE +``` + + +> When I refer to `iris.$FUNCTION/$VARIABLE` it means `iris.Handle/.HandleFunc/.Get/.Post/.Put/.Delete/.Trace/.Options/.Use/.UseFunc/.UseGlobal/.Party/.Set/.Config` +and the rest of the package-level functions referred to the `iris.Default` variable. + +**BEFORE** + +```go +iris.Config.FireMethodNotAllowed = true +iris.Set(OptionDisableBanner(true)) +``` + +```go +iris.Get("/", func(ctx *iris.Context){ + +}) + +iris.ListenLETSENCRYPT(":8080") +``` + +**AFTER** + + +```go +app := iris.New() +app.Config.FireMethodNotAllowed = true +// or iris.Default.Config.FireMethodNotAllowed = true and so on +app.Set(OptionDisableBanner(true)) +// same as +// app := iris.New(iris.Configuration{FireMethodNotAllowed:true, DisableBanner:true}) +``` + +```go +app := iris.New() +app.Get("/", func(ctx *iris.Context){ + +}) + +app.ListenLETSENCRYPT(":8080") +``` + +For those who had splitted the application in different packages they could do just that `iris.$FUNCTION/$VARIABLE` without the need +of import a singleton package which would initialize a new `App := iris.New()`. + +`Iris.Default` remains, so you can refer to that if you don't want to initialize a new `App := iris.New()` by your own. + +**BEFORE** + +```go +package controllers +import "github.com/kataras/iris" +func init(){ + iris.Get("/", func(ctx *iris.Context){ + + }) +} +``` + +```go +package main + +import ( + "github.com/kataras/iris" + _ "github.com/mypackage/controllers" +) + +func main(){ + iris.Listen(":8080") +} +``` + + +**AFTER** + +```go +package controllers + +import ( + "github.com/kataras/iris" + "github.com/kataras/iris/adaptors/httprouter" +) + +func init(){ + iris.Default.Adapt(httprouter.New()) + iris.Default.Get("/", func(ctx *iris.Context){ + + }) +} +``` + +```go +package main + +import ( + "github.com/kataras/iris" + _ "github.com/mypackage/controllers" +) + +func main(){ + iris.Default.Listen(":8080") +} +``` + +You got the point, let's continue to the next conversion. + +### Remove the slow .API | iris.API(...) / app := iris.New(); app.API(...) + + +The deprecated `.API` has been removed entirely, it should be removed after v5(look on the v5 history tag). + +At first I created that func in order to give newcovers a chance to be able to quick start a new `controller-like` +with one function, but that function was using generics at runtime and it was very slow compared to the +`iris.Handle/.HandleFunc/.Get/.Post/.Put/.Delete/.Trace/.Options/.Use/.UseFunc/.UseGlobal/.Party`. + +Also some users they used only `.API`, they didn't bother to 'learn' about the standard rest api functions +and their power(including per-route middleware, cors, recover and so on). So we had many unrelational questions about the `.API` func. + + +**BEFORE** + +```go +package main + +import ( + "github.com/kataras/iris" +) + +type UserAPI struct { + *iris.Context +} + +// GET /users +func (u UserAPI) Get() { + u.Writef("Get from /users") + // u.JSON(iris.StatusOK,myDb.AllUsers()) +} + +// GET /users/:param1 which its value passed to the id argument +func (u UserAPI) GetBy(id string) { // id equals to u.Param("param1") + u.Writef("Get from /users/%s", id) + // u.JSON(iris.StatusOK, myDb.GetUserById(id)) + +} + +// POST /users +func (u UserAPI) Post() { + name := u.FormValue("name") + // myDb.InsertUser(...) + println(string(name)) + println("Post from /users") +} + +// PUT /users/:param1 +func (u UserAPI) PutBy(id string) { + name := u.FormValue("name") // you can still use the whole Context's features! + // myDb.UpdateUser(...) + println(string(name)) + println("Put from /users/" + id) +} + +// DELETE /users/:param1 +func (u UserAPI) DeleteBy(id string) { + // myDb.DeleteUser(id) + println("Delete from /" + id) +} + +func main() { + + iris.API("/users", UserAPI{}) + iris.Listen(":8080") +} + +``` + + +**AFTER** + +```go +package main + +import ( + "github.com/kataras/iris" + "github.com/kataras/iris/adaptors/gorillamux" +) + +func GetAllUsersHandler(ctx *iris.Context) { + ctx.Writef("Get from /users") + // ctx.JSON(iris.StatusOK,myDb.AllUsers()) +} + +func GetUserByIdHandler(ctx *iris.Context) { + ctx.Writef("Get from /users/%s", + ctx.Param("id")) // or id, err := ctx.ParamInt("id") + // ctx.JSON(iris.StatusOK, myDb.GetUserById(id)) +} + +func InsertUserHandler(ctx *iris.Context){ + name := ctx.FormValue("name") + // myDb.InsertUser(...) + println(string(name)) + println("Post from /users") +} + +func UpdateUserHandler(ctx *iris.Context) { + name := ctx.FormValue("name") + // myDb.UpdateUser(...) + println(string(name)) + println("Put from /users/" + ctx.Param("id")) +} + +func DeleteUserById(id string) { + // myDb.DeleteUser(id) + println("Delete from /" + ctx.param("id")) +} + +func main() { + app := iris.New() + app.Adapt(gorillamux.New()) + + // create a new router targeted for "/users" path prefix + // you can learn more about Parties on the examples and book too + // they can share middleware, template layout and more. + userRoutes := app.Party("users") + + // GET http://localhost:8080/users/ and /users + userRoutes.Get("/", GetAllUsersHandler) + + // GET http://localhost:8080/users/:id + userRoutes.Get("/:id", GetUserByIdHandler) + // POST http://localhost:8080/users + userRoutes.Post("/", InsertUserHandler) + + // PUT http://localhost:8080/users/:id + userRoutes.Put("/:id", UpdateUserHandler) + + // DELETE http://localhost:8080/users/:id + userRoutes.Delete("/:id", DeleteUserById) + + app.Listen(":8080") +} + +``` + +### Old Plugins and the new `.Adapt` Policies + +A lot of changes to old -so-called Plugins and many features have been adopted to this new ecosystem. + + +First of all plugins renamed to `policies with adaptors which, adaptors, adapts the policies to the framework` +(it is not just a simple rename of the word, it's a new concept). + + +Policies are declared inside Framework, they are implemented outside of the Framework and they are adapted to Framework by a user call. + +Policy adaptors are just like a plugins but they have to implement a specific action/behavior to a specific policy type(or more than one at the time). + +The old plugins are fired 'when something happens do that' (ex: PreBuild,PostBuild,PreListen and so on) this behavior is the new `EventPolicy` +which has **4 main flow events** with their callbacks been wrapped, so you can use more than EventPolicy (most of the policies works this way). + +```go +type ( + // EventListener is the signature for type of func(*Framework), + // which is used to register events inside an EventPolicy. + // + // Keep note that, inside the policy this is a wrapper + // in order to register more than one listener without the need of slice. + EventListener func(*Framework) + + // EventPolicy contains the available Framework's flow event callbacks. + // Available events: + // - Boot + // - Build + // - Interrupted + // - Recovery + EventPolicy struct { + // Boot with a listener type of EventListener. + // Fires when '.Boot' is called (by .Serve functions or manually), + // before the Build of the components and the Listen, + // after VHost and VSCheme configuration has been setted. + Boot EventListener + // Before Listen, after Boot + Build EventListener + // Interrupted with a listener type of EventListener. + // Fires after the terminal is interrupted manually by Ctrl/Cmd + C + // which should be used to release external resources. + // Iris will close and os.Exit at the end of custom interrupted events. + // If you want to prevent the default behavior just block on the custom Interrupted event. + Interrupted EventListener + // Recovery with a listener type of func(*Framework,error). + // Fires when an unexpected error(panic) is happening at runtime, + // while the server's net.Listener accepting requests + // or when a '.Must' call contains a filled error. + // Used to release external resources and '.Close' the server. + // Only one type of this callback is allowed. + // + // If not empty then the Framework will skip its internal + // server's '.Close' and panic to its '.Logger' and execute that callback instaed. + // Differences from Interrupted: + // 1. Fires on unexpected errors + // 2. Only one listener is allowed. + Recovery func(*Framework, error) + } +) +``` + +**A quick overview on how they can be adapted** to an iris *Framework (iris.New()'s result). +Let's adapt `EventPolicy`: + +```go +app := iris.New() + +evts := iris.EventPolicy{ + // we ommit the *Framework's variable name because we have already the 'app' + // if we were on different file with no access to the 'app' then the varialbe name will be useful. + Boot: func(*Framework){ + app.Log("Here you can change any field and configuration for iris before being used + also you can adapt more policies that should be used to the next step which is the Build and Listen, + only the app.Config.VHost and app.Config.VScheme have been setted here, but you can change them too\n") + }, + Build: func(*Framework){ + app.Log("Here all configuration and all app' fields and features have been builded, here you are ready to call + anything (you shouldn't change fields and configuration here)\n") + }, +} +// Adapt the EventPolicy 'evts' to the Framework +app.Adapt(evts) + +// let's register one more +app.Adapt(iris.EventPolicy{ + Boot: func(*Framework){ + app.Log("the second log message from .Boot!\n") +}}) + +// you can also adapt multiple and different(or same) types of policies in the same call +// using: app.Adapt(iris.EventPolicy{...}, iris.LoggerPolicy(...), iris.RouterWrapperPolicy(...)) + +// starts the server, executes the Boot -> Build... +app.Adapt(httprouter.New()) // read below for this line +app.Listen(":8080") +``` + + + +This pattern allows us to be very pluggable and add features that the *Framework itself doesn't knows, +it knows only the main policies which implement but their features are our(as users) business. + + +We have 7 policies,so far, and some of them have 'subpolicies' (the RouterReversionPolicy for example). + +- LoggerPolicy +- EventPolicy + - Boot + - Build + - Interrupted + - Recover +- RouterReversionPolicy + - StaticPath + - WildcardPath + - URLPath + - RouteContextLinker +- RouterBuilderPolicy +- RouterWrapperPolicy +- RenderPolicy +- TemplateFuncsPolicy + + +**Details** of these can be found at [policy.go](https://github.com/kataras/iris/blob/master/policy.go). + +The **Community**'s adaptors are [here](https://github.com/iris-contrib/adaptors). + +**Iris' Built'n Adaptors** for these policies can be found at [/adaptors folder](https://github.com/kataras/iris/tree/master/adaptors). + +The folder contains: + +- cors, a cors (router) wrapper based on `rs/cors`. +It's a `RouterWrapperPolicy` + +- gorillamux, a router that can be adapted, it's the `gorilla/mux` which supports subdomains, custom http errors, reverse routing, pattern matching. +It's a compination of`EventPolicy`, `RouterReversionPolicy with StaticPath, WildcardPath, URLPath, RouteContextLinker` and the `RouterBuilderPolicy`. + +- httprouter, a router that can be adapted, it's a custom version of `julienschmidt/httprouter` which is edited to support iris' subdomains, reverse routing, custom http errors and a lot features, it should be a bit faster than the original too. +It's a compination of`EventPolicy`, `RouterReversionPolicy with StaticPath, WildcardPath, URLPath, RouteContextLinker` and the `RouterBuilderPolicy`. + + +- typescript and cloud editor, contains the typescript compiler with hot reload feature and a typescript cloud editor ([alm-tools](https://github.com/alm-tools/alm)), it's an `EventPolicy` + +- view, contains 5 template engines based on the `kataras/go-template`. +All of these have common features with common API, like Layout, Template Funcs, Party-specific layout, partial rendering and more. +It's a `RenderPolicy` with a compinaton of `EventPolicy` and use of `TemplateFuncsPolicy`. + - the standard html + - pug(jade) + - django(pongo2) + - handlebars + - amber. + + + + + +#### Note +Go v1.8 introduced a new plugin system with `.so` files, users should not be confused with old iris' plugins and new adaptors. +It is not ready for all operating systems(yet) when it will be ready, Iris will take leverage of this Golang's feature. + + +### http.Handler and third-party middleware + +We were compatible before this version but if a third-party middleware had the form of: +`func(http.ResponseWriter, *http.Request, http.HandlerFunc)`you were responsible of make a wrapper +which would return an `iris.Handler/HandlerFunc`. + +Now you're able to pass an `func(http.ResponseWriter, *http.Request, http.HandlerFunc)` third-party net/http middleware(Chain-of-responsibility pattern) using the `iris.ToHandler` wrapper func without any other custom boilerplate. + +Example: + +```go +package main + +import ( + "github.com/kataras/iris" + "github.com/kataras/adaptors/gorillamux" + "github.com/rs/cors" +) + +// myCors returns a new cors middleware +// with the provided options. +myCors := func(opts cors.Options) iris.HandlerFunc { + handlerWithNext := cors.New(opts).ServeHTTP + return iris.ToHandler(handlerWithNext) +} + +func main(){ + app := iris.New() + app.Adapt(httprouter.New()) + + app.Post("/user", myCors(cors.Options{}), func(ctx *iris.Context){ + // .... + }) + + app.Listen(":8080") +} + +``` + +- Irrelative info but this is the best place to put it: `iris/app.AcquireCtx/.ReleaseCtx` replaced to: `app.Context.Acquire/.Release/.Run`. + + + +### iris cmd + - FIX: [iris run main.go](https://github.com/kataras/iris/tree/master/iris#run) not reloading when file changes maden by some of the IDEs, because they do override the operating system's fs signals. The majority of editors worked before but I couldn't let some developers without support. - * already pushed to the current version -- FIX: custom routers not working well with static file serving, reverse routing and per-party http custom errors. +### Sessions - IMPROVEMENT: [Sessions manager](https://github.com/kataras/go-sessions) works even faster now. - * already pushed to the current version -- NEW: HTTP/2 Push `context.Push` +### Websockets + +There are many internal improvements to the [websocket server](https://github.com/kataras/go-websocket), and it's +operating slighty faster. + +The kataras/go-websocket library, which `app.OnConnection` is refering to, will not be changed, its API will still remain. +I am not putting anything new there (I doubt if any bug is available to fix, it's very simple and it just works). + +I started the kataras/go-websocket back then because I wanted a simple and fast websocket server for +the fasthttp iris' version and back then no one did that before. +Now(after v6) iris is compatible with any net/http websocket library that already created by third-parties. + +If the iris' websocket feature does not cover your app's needs, you can simple use any other +library for websockets, like the Golang's compatible to `socket.io`, example: + +```go +package main + +import ( + "log" + + "github.com/kataras/iris" + "github.com/kataras/iris/adaptors/httprouter" + "github.com/googollee/go-socket.io" +) + +func main() { + app := iris.New() + app.Adapt(httprouter.New()) + server, err := socketio.NewServer(nil) + if err != nil { + log.Fatal(err) + } + server.On("connection", func(so socketio.Socket) { + log.Println("on connection") + so.Join("chat") + so.On("chat message", func(msg string) { + log.Println("emit:", so.Emit("chat message", msg)) + so.BroadcastTo("chat", "chat message", msg) + }) + so.On("disconnection", func() { + log.Println("on disconnect") + }) + }) + server.On("error", func(so socketio.Socket, err error) { + log.Println("error:", err) + }) + + app.Any("/socket.io", iris.ToHandler(server)) + + app.Listen(":5000") +} +``` + +### Typescript compiler and cloud-based editor + +The Typescript compiler adaptor(old 'plugin') has been fixed (it had an issue on new typescript versions). +Example can be bound [here](https://github.com/kataras/iris/tree/master/adaptors/typescript/_example). + +The Cloud-based editor adaptor(old 'plugin') also fixed and improved to show debug messages to your iris' LoggerPolicy. +Example can be bound [here](https://github.com/kataras/iris/tree/master/adaptors/typescript/editor/_example). + +Their import paths also changed as the rest of the old plugins from: https://github.com/iris-contrib/plugin to https://github.com/kataras/adaptors. +I had them on iris-contrib because I thought that community would help but it didn't, no problem, they are at the same codebase now +which making things easier to debug for me. + + +### Oauth/OAuth2 +Fix the oauth/oauth2 adaptor (old 'plugin') . +Example can be found [here](https://github.com/iris-contrib/adaptors/tree/master/oauth/_example). + + +### CORS Middleware and the new Wrapper + +Lets speak about history of cors middleware, almost all the issues users reported to the iris-contrib/middleware repository +were relative to the CORS middleware, some users done it work some others don't... it was strange. Keep note that this was one of the two middleware that I didn't +wrote by myself, it was a PR by a member who wrote that middleware and after didn't answer on users' issues. + +Forget about it I removed it entirely and replaced with the `rs/cors`: we now use the https://github.com/rs/cors in two forms: + +First, you can use the original middlare that you can install by `go get -u github.com/rs/cors` +(You had already see its example on the net/http handlers and iris.ToHandler section) + +Can be registered globally or per-route but the `MethodsAllowed option doesn't works`. + +Example: + +```go +package main + +import ( + "github.com/kataras/iris" + "github.com/kataras/adaptors/gorillamux" + "github.com/rs/cors" +) + +func main(){ + app := iris.New() + app.Adapt(httprouter.New()) // see below for that + corsMiddleware := iris.ToHandler(cors.Default().ServeHTTP) + app.Post("/user", corsMiddleware, func(ctx *iris.Context){ + // .... + }) + + app.Listen(":8080") +} +``` + +Secondly, probably the one which you will choose to use, is the `cors` Router Wrapper Adaptor. +It's already installed when you install iris because it's located at `kataras/iris/adaptors/cors`. + +This will wrap the entirely router so the whole of your app will be passing by the rules you setted up on its `cors.Options`. + +Again, it's functionality comes from the well-tested `rs/cors`, all known Options are working as expected. + +Example: + +```go +package main + +import ( + "github.com/kataras/iris" + "github.com/kataras/adaptors/httprouter" + "github.com/kataras/adaptors/cors" +) + +func main(){ + app := iris.New() + app.Adapt(httprouter.New()) // see below for that + app.Adapt(cors.New(cors.Options{})) // or cors.Default() + + app.Post("/user", func(ctx *iris.Context){ + // .... + }) + + app.Listen(":8080") +} + +``` + +### FAQ +You know that you can always share your opinion and ask anything iris-relative with the rest of us, [here](https://kataras.rocket.chat/channel/iris). -The full list of next version's features among with one by one steps to refactor your code in breaking-cases will be noticed here when it will be released. ## 6.1.3 -> 6.1.4 @@ -58,7 +1010,7 @@ editors worked before but I couldn't let some developers without support. - IMPROVEMENT: Now you're able to pass an `func(http.ResponseWriter, *http.Request, http.HandlerFunc)` third-party net/http middleware(Chain-of-responsibility pattern) using the `iris.ToHandler` wrapper func without any other custom boilerplate. - IMPROVEMENT: [Sessions manager](https://github.com/kataras/go-sessions) works even faster now. - * Change: `context.Session().GetAll()` returns an empty map instead of nil when session has no values. + * Change: `context.Session().GetAll()` returns an empty map instead of nil when session has no values. ## 6.1.2 -> 6.1.3 diff --git a/README.md b/README.md index d8425568..5512781e 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,7 @@
-Build Status +Build Status http://goreportcard.com/report/kataras/iris @@ -21,7 +21,7 @@
-CHANGELOG/HISTORY +CHANGELOG/HISTORY Examples @@ -30,9 +30,11 @@ Chat

-Iris is the fastest HTTP/2 web framework written in Go. -
-Easy to learn while it's highly customizable, +Iris provides efficient and well-designed toolbox with robust set of features to
create your own +perfect high-performance web application
with unlimited portability using the Go Programming Language. + +

+Easy to learn while it's highly customizable, ideally suited for
both experienced and novice developers.

If you're coming from Node.js world, this is the expressjs for the Go Programming Language. @@ -58,11 +60,9 @@ Installation The only requirement is the [Go Programming Language](https://golang.org/dl/), at least v1.7. ```bash -$ go get -u github.com/kataras/iris/iris +$ go get gopkg.in/kataras/iris.v6 ``` -![Benchmark Wizzard July 21, 2016- Processing Time Horizontal Graph](https://raw.githubusercontent.com/smallnest/go-web-framework-benchmark/4db507a22c964c9bc9774c5b31afdc199a0fe8b7/benchmark.png) - Overview ----------- @@ -71,26 +71,29 @@ Overview package main import ( - "github.com/kataras/go-template/html" - "github.com/kataras/iris" + "gopkg.in/kataras/iris.v6" + "gopkg.in/kataras/iris.v6/adaptors/httprouter" + "gopkg.in/kataras/iris.v6/adaptors/view" ) func main() { app := iris.New() - // 6 template engines are supported out-of-the-box: + app.Adapt(iris.Devlogger()) // adapt a logger which prints all errors to the os.Stdout + app.Adapt(httprouter.New()) // adapt the adaptors/httprouter or adaptors/gorillamux + + // 5 template engines are supported out-of-the-box: // // - standard html/template // - amber // - django // - handlebars // - pug(jade) - // - markdown // // Use the html standard engine for all files inside "./views" folder with extension ".html" - // Defaults to: - app.UseTemplate(html.New()).Directory("./views", ".html") + templates := view.HTML("./views", ".html") + app.Adapt(templates) - // http://localhost:6111 + // http://localhost:6200 // Method: "GET" // Render ./views/index.html app.Get("/", func(ctx *iris.Context) { @@ -102,24 +105,24 @@ func main() { Layout("layouts/userLayout.html") { // Fire userNotFoundHandler when Not Found - // inside http://localhost:6111/users/*anything + // inside http://localhost:6200/users/*anything userAPI.OnError(404, userNotFoundHandler) - // http://localhost:6111/users + // http://localhost:6200/users // Method: "GET" userAPI.Get("/", getAllHandler) - // http://localhost:6111/users/42 + // http://localhost:6200/users/42 // Method: "GET" userAPI.Get("/:id", getByIDHandler) - // http://localhost:6111/users + // http://localhost:6200/users // Method: "POST" userAPI.Post("/", saveUserHandler) } - // Start the server at 0.0.0.0:6111 - app.Listen(":6111") + // Start the server at 127.0.0.1:6200 + app.Listen(":6200") } func getByIDHandler(ctx *iris.Context) { @@ -141,7 +144,7 @@ func getByIDHandler(ctx *iris.Context) { ``` > TIP: Execute `iris run main.go` to enable hot-reload on .go source code changes. -> TIP: Set `app.Config.IsDevelopment = true` to monitor the template changes. +> TIP: Add `templates.Reload(true)` to monitor the template changes. Documentation ----------- @@ -149,11 +152,13 @@ Documentation - - The most important is to read [the practical guide](https://docs.iris-go.com/). + - The most important is to read [the practical guide](https://docs.iris-go.com/) - - Navigate through [examples](https://github.com/iris-contrib/examples). + - Read [godocs](https://godoc.org/github.com/kataras/iris) for the details - - [HISTORY.md](https://github.com//kataras/iris/tree/master/HISTORY.md) file is your best friend. + - Navigate through [examples](https://github.com/iris-contrib/examples) + + - [HISTORY.md](https://github.com//kataras/iris/tree/v6/HISTORY.md) file is your best friend. Testing @@ -162,8 +167,8 @@ Testing You can find RESTFUL test examples by navigating to the following links: - [gavv/_examples/iris_test.go](https://github.com/gavv/httpexpect/blob/master/_examples/iris_test.go). -- [./http_test.go](https://github.com/kataras/iris/blob/master/http_test.go). -- [./context_test.go](https://github.com/kataras/iris/blob/master/context_test.go). +- [./http_test.go](https://github.com/kataras/iris/blob/v6/http_test.go). +- [./context_test.go](https://github.com/kataras/iris/blob/v6/context_test.go). FAQ @@ -218,7 +223,7 @@ Besides the fact that we have a [community chat][Chat] for questions or reports Versioning ------------ -Current: **v6.1.4** +Current: **v6.2.0** v5: https://github.com/kataras/iris/tree/5.0.0 @@ -229,4 +234,7 @@ License Unless otherwise noted, the source files are distributed under the MIT License found in the [LICENSE file](LICENSE). +Note that some optional components that you may use with Iris requires +different license agreements. + [Chat]: https://kataras.rocket.chat/channel/iris diff --git a/adaptors/cors/LICENSE b/adaptors/cors/LICENSE new file mode 100644 index 00000000..d8e2df5a --- /dev/null +++ b/adaptors/cors/LICENSE @@ -0,0 +1,19 @@ +Copyright (c) 2014 Olivier Poitrey + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is furnished +to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/adaptors/cors/cors.go b/adaptors/cors/cors.go new file mode 100644 index 00000000..21e712e4 --- /dev/null +++ b/adaptors/cors/cors.go @@ -0,0 +1,33 @@ +package cors + +// +------------------------------------------------------------+ +// | Cors wrapper usage | +// +------------------------------------------------------------+ +// +// import "github.com/kataras/iris/adaptors/cors" +// +// app := iris.New() +// app.Adapt(cors.New(cors.Options{}))) + +import ( + "github.com/rs/cors" + "gopkg.in/kataras/iris.v6" +) + +// Options is a configuration container to setup the CORS. +type Options cors.Options + +// New returns a new cors router wrapper policy +// with the provided options. +// +// create a new cors middleware, pass its options (casted) +// and pass the .ServeHTTP(w,r,next(http.HandlerFunc)) as the wrapper +// for the whole iris' Router +// +// Unlike the cors middleware, this wrapper wraps the entirely router, +// it cannot be registered to specific routes. It works better and all Options are working. +func New(opts Options) iris.RouterWrapperPolicy { return cors.New(cors.Options(opts)).ServeHTTP } + +// Default returns a New cors wrapper with the default options +// accepting GET and POST requests and allowing all origins. +func Default() iris.RouterWrapperPolicy { return New(Options{}) } diff --git a/adaptors/gorillamux/LICENSE b/adaptors/gorillamux/LICENSE new file mode 100644 index 00000000..5ad848c4 --- /dev/null +++ b/adaptors/gorillamux/LICENSE @@ -0,0 +1,28 @@ +Copyright (c) 2012 Rodrigo Moraes author of gorillamux. +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + + * Redistributions of source code must retain the above copyright +notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above +copyright notice, this list of conditions and the following disclaimer +in the documentation and/or other materials provided with the +distribution. + * Neither the name of Google Inc. nor the names of its +contributors may be used to endorse or promote products derived from +this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/adaptors/gorillamux/README.md b/adaptors/gorillamux/README.md new file mode 100644 index 00000000..51d36935 --- /dev/null +++ b/adaptors/gorillamux/README.md @@ -0,0 +1,103 @@ +## Package information + +Gorillamux is a plugin for Iris which overrides the Iris' default router with the [Gorilla Mux](https://github.com/gorilla/mux) +which enables path matching using custom `regexp` ( thing that the Iris' default router doesn't supports for performance reasons). + +All these without need to change any of your existing Iris code. All features are supported. + +## Install + +```sh +$ go get -u github.com/iris-contrib/plugin/gorillamux +``` + + +```go +iris.Plugins.Add(gorillamux.New()) +``` + +## [Example](https://github.com/iris-contrib/examples/tree/master/plugin_gorillamux) + + +```go +package main + +import ( + "github.com/iris-contrib/plugin/gorillamux" + "github.com/kataras/iris" +) + +func main() { + iris.Plugins.Add(gorillamux.New()) + + // CUSTOM HTTP ERRORS ARE SUPPORTED + // NOTE: Gorilla mux allows customization only on StatusNotFound(404) + // Iris allows for everything, so you can register any other custom http error + // but you have to call it manually from ctx.EmitError(status_code) // 500 for example + // this will work because it's StatusNotFound: + iris.Default.OnError(iris.StatusNotFound, func(ctx *iris.Context) { + ctx.HTML(iris.StatusNotFound, "

CUSTOM NOT FOUND ERROR PAGE

") + }) + + // GLOBAL/PARTY MIDDLEWARE ARE SUPPORTED + iris.Default.UseFunc(func(ctx *iris.Context) { + println("Request: " + ctx.Path()) + ctx.Next() + }) + + // http://mydomain.com + iris.Default.Get("/", func(ctx *iris.Context) { + ctx.Writef("Hello from index") + }) + + /// -------------------------------------- IMPORTANT -------------------------------------- + /// GORILLA MUX PARAMETERS(regexp) ARE SUPPORTED + /// http://mydomain.com/api/users/42 + /// --------------------------------------------------------------------------------------- + iris.Default.Get("/api/users/{userid:[0-9]+}", func(ctx *iris.Context) { + ctx.Writef("User with id: %s", ctx.Param("userid")) + }) + + // PER-ROUTE MIDDLEWARE ARE SUPPORTED + // http://mydomain.com/other + iris.Default.Get("/other", func(ctx *iris.Context) { + ctx.Writef("/other 1 middleware \n") + ctx.Next() + }, func(ctx *iris.Context) { + ctx.HTML(iris.StatusOK, "Hello from /other") + }) + + // SUBDOMAINS ARE SUPPORTED + // http://admin.mydomain.com + iris.Default.Party("admin.").Get("/", func(ctx *iris.Context) { + ctx.Writef("Hello from admin. subdomain!") + }) + + // WILDCARD SUBDOMAINS ARE SUPPORTED + // http://api.mydomain.com/hi + // http://admin.mydomain.com/hi + // http://x.mydomain.com/hi + // [depends on your host configuration, + // you will see an example(win) outside of this folder]. + iris.Default.Party("*.").Get("/hi", func(ctx *iris.Context) { + ctx.Writef("Hello from wildcard subdomain: %s", ctx.Subdomain()) + }) + + // DOMAIN NAMING IS SUPPORTED + iris.Default.Listen("mydomain.com") + // iris.Default.Listen(":80") +} + +/* HOSTS FILE LINES TO RUN THIS EXAMPLE: + +127.0.0.1 mydomain.com +127.0.0.1 admin.mydomain.com +127.0.0.1 api.mydomain.com +127.0.0.1 x.mydomain.com + +*/ + + +``` + +> Custom domain is totally optionally, you can still use `iris.Default.Listen(":8080")` of course. diff --git a/adaptors/gorillamux/gorillamux.go b/adaptors/gorillamux/gorillamux.go new file mode 100644 index 00000000..207fa6d0 --- /dev/null +++ b/adaptors/gorillamux/gorillamux.go @@ -0,0 +1,150 @@ +package gorillamux + +// +------------------------------------------------------------+ +// | Usage | +// +------------------------------------------------------------+ +// +// +// package main +// +// import ( +// "gopkg.in/kataras/iris.v6/adaptors/gorillamux" +// "gopkg.in/kataras/iris.v6" +// ) +// +// func main() { +// app := iris.New() +// +// app.Adapt(gorillamux.New()) // Add this line and you're ready. +// +// app.Get("/api/users/{userid:[0-9]+}", func(ctx *iris.Context) { +// ctx.Writef("User with id: %s", ctx.Param("userid")) +// }) +// +// app.Listen(":8080") +// } + +import ( + "net/http" + "strings" + + "github.com/gorilla/mux" + "gopkg.in/kataras/iris.v6" +) + +const dynamicSymbol = '{' + +// New returns a new gorilla mux router which can be plugged inside iris. +// This is magic. +func New() iris.Policies { + router := mux.NewRouter() + var logger func(iris.LogMode, string) + return iris.Policies{ + EventPolicy: iris.EventPolicy{Boot: func(s *iris.Framework) { + logger = s.Log + }}, + RouterReversionPolicy: iris.RouterReversionPolicy{ + // path normalization done on iris' side + StaticPath: func(path string) string { + i := strings.IndexByte(path, dynamicSymbol) + if i > -1 { + return path[0:i] + } + + return path + }, + WildcardPath: func(requestPath string, paramName string) string { + return requestPath + "/{" + paramName + ":.*}" + }, + // Note: on gorilla mux the {{ url }} and {{ path}} should give the key and the value, not only the values by order. + // {{ url "nameOfTheRoute" "parameterName" "parameterValue"}}. + // + // so: {{ url "providerLink" "facebook"}} should become + // {{ url "providerLink" "provider" "facebook"}} + // for a path: "/auth/{provider}" with name 'providerLink' + URLPath: func(r iris.RouteInfo, args ...string) string { + if r == nil { + return "" + } + if gr := router.Get(r.Name()); gr != nil { + u, err := gr.URLPath(args...) + if err != nil { + logger(iris.DevMode, "error on gorilla mux adaptor's URLPath(reverse routing): "+err.Error()) + return "" + } + return u.Path + } + return "" + }, + RouteContextLinker: func(r iris.RouteInfo, ctx *iris.Context) { + if r == nil { + return + } + route := router.Get(r.Name()) + if route != nil { + mapToContext(ctx.Request, r.Middleware(), ctx) + } + }, + }, + RouterBuilderPolicy: func(repo iris.RouteRepository, context iris.ContextPool) http.Handler { + repo.Visit(func(route iris.RouteInfo) { + registerRoute(route, router, context) + }) + + router.NotFoundHandler = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + ctx := context.Acquire(w, r) + // to catch custom 404 not found http errors may registered by user + ctx.EmitError(iris.StatusNotFound) + context.Release(ctx) + }) + return router + }, + } +} + +func mapToContext(r *http.Request, middleware iris.Middleware, ctx *iris.Context) { + if params := mux.Vars(r); len(params) > 0 { + // set them with ctx.Set in order to be accesible by ctx.Param in the user's handler + for k, v := range params { + ctx.Set(k, v) + } + } + // including the iris.Default.Use/UseFunc and the route's middleware, + // main handler and any done handlers. + ctx.Middleware = middleware +} + +// so easy: +func registerRoute(route iris.RouteInfo, gorillaRouter *mux.Router, context iris.ContextPool) { + if route.IsOnline() { + handler := func(w http.ResponseWriter, r *http.Request) { + ctx := context.Acquire(w, r) + + mapToContext(r, route.Middleware(), ctx) + ctx.Do() + + context.Release(ctx) + } + + // remember, we get a new iris.Route foreach of the HTTP Methods, so this should be work + methods := []string{route.Method()} + // if route has cors then we register the route with the "OPTIONS" method too + if route.HasCors() { + methods = append(methods, http.MethodOptions) + } + gorillaRoute := gorillaRouter.HandleFunc(route.Path(), handler).Methods(methods...).Name(route.Name()) + + subdomain := route.Subdomain() + if subdomain != "" { + if subdomain == "*." { + // it's an iris wildcard subdomain + // so register it as wildcard on gorilla mux too (hopefuly, it supports these things) + subdomain = "{subdomain}." + } else { + // it's a static subdomain (which contains the dot) + } + // host = subdomain + listening host + gorillaRoute.Host(subdomain + context.Framework().Config.VHost) + } + } +} diff --git a/adaptors/httprouter/LICENSE b/adaptors/httprouter/LICENSE new file mode 100644 index 00000000..fd7e2609 --- /dev/null +++ b/adaptors/httprouter/LICENSE @@ -0,0 +1,47 @@ +The MIT License (MIT) + +Copyright (c) 2016-2017 Gerasimos Maropoulos + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + + +Copyright (c) 2013 Julien Schmidt. All rights reserved. + + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + * The names of the contributors may not be used to endorse or promote + products derived from this software without specific prior written + permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL JULIEN SCHMIDT BE LIABLE FOR ANY +DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND +ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/adaptors/httprouter/_example/main.go b/adaptors/httprouter/_example/main.go new file mode 100644 index 00000000..4161b0b2 --- /dev/null +++ b/adaptors/httprouter/_example/main.go @@ -0,0 +1,39 @@ +package main + +import ( + "gopkg.in/kataras/iris.v6" + "gopkg.in/kataras/iris.v6/adaptors/httprouter" +) + +func hello(ctx *iris.Context) { + ctx.Writef("Hello from %s", ctx.Path()) +} + +func main() { + app := iris.New() + app.Adapt(iris.DevLogger()) + app.Adapt(httprouter.New()) + + app.OnError(iris.StatusNotFound, func(ctx *iris.Context) { + ctx.HTML(iris.StatusNotFound, "

Custom not found handler

") + }) + + app.Get("/", hello) + app.Get("/users/:userid", func(ctx *iris.Context) { + ctx.Writef("Hello user with id: %s", ctx.Param("userid")) + }) + + app.Get("/myfiles/*file", func(ctx *iris.Context) { + ctx.HTML(iris.StatusOK, "Hello, the dynamic path after /myfiles is:
"+ctx.Param("file")+"") + }) + + app.Get("/users/:userid/messages/:messageid", func(ctx *iris.Context) { + ctx.HTML(iris.StatusOK, `Message from user with id:
`+ctx.Param("userid")+`, + message id: `+ctx.Param("messageid")+``) + }) + + // http://127.0.0.1:8080/users/42 + // http://127.0.0.1:8080/myfiles/mydirectory/myfile.zip + // http://127.0.0.1:8080/users/42/messages/1 + app.Listen(":8080") +} diff --git a/adaptors/httprouter/httprouter.go b/adaptors/httprouter/httprouter.go new file mode 100644 index 00000000..11802ee3 --- /dev/null +++ b/adaptors/httprouter/httprouter.go @@ -0,0 +1,725 @@ +package httprouter + +// +------------------------------------------------------------+ +// | Usage | +// +------------------------------------------------------------+ +// +// +// package main +// +// import ( +// "gopkg.in/kataras/iris.v6/adaptors/httprouter" +// "gopkg.in/kataras/iris.v6" +// ) +// +// func main() { +// app := iris.New() +// +// app.Adapt(httprouter.New()) // Add this line and you're ready. +// +// app.Get("/api/users/:userid", func(ctx *iris.Context) { +// ctx.Writef("User with id: %s", ctx.Param("userid")) +// }) +// +// app.Listen(":8080") +// } + +import ( + "net/http" + "strings" + + "github.com/kataras/go-errors" + "gopkg.in/kataras/iris.v6" +) + +const ( + // parameterStartByte is very used on the node, it's just contains the byte for the ':' rune/char + parameterStartByte = byte(':') + // slashByte is just a byte of '/' rune/char + slashByte = byte('/') + // slash is just a string of "/" + slash = "/" + // matchEverythingByte is just a byte of '*" rune/char + matchEverythingByte = byte('*') + + isRoot entryCase = iota + hasParams + matchEverything +) + +type ( + // entryCase is the type which the type of muxEntryusing in order to determinate what type (parameterized, anything, static...) is the perticular node + entryCase uint8 + + // muxEntry is the node of a tree of the routes, + // in order to learn how this is working, google 'trie' or watch this lecture: https://www.youtube.com/watch?v=uhAUk63tLRM + // this method is used by the BSD's kernel also + muxEntry struct { + part string + entryCase entryCase + hasWildNode bool + tokens string + nodes []*muxEntry + middleware iris.Middleware + precedence uint64 + paramsLen uint8 + } +) + +var ( + errMuxEntryConflictsWildcard = errors.New(` + Router: '%s' in new path '%s' + conflicts with existing wildcarded route with path: '%s' + in existing prefix of'%s' `) + + errMuxEntryMiddlewareAlreadyExists = errors.New(` + Router: Middleware were already registered for the path: '%s'`) + + errMuxEntryInvalidWildcard = errors.New(` + Router: More than one wildcard found in the path part: '%s' in route's path: '%s'`) + + errMuxEntryConflictsExistingWildcard = errors.New(` + Router: Wildcard for route path: '%s' conflicts with existing children in route path: '%s'`) + + errMuxEntryWildcardUnnamed = errors.New(` + Router: Unnamed wildcard found in path: '%s'`) + + errMuxEntryWildcardInvalidPlace = errors.New(` + Router: Wildcard is only allowed at the end of the path, in the route path: '%s'`) + + errMuxEntryWildcardConflictsMiddleware = errors.New(` + Router: Wildcard conflicts with existing middleware for the route path: '%s'`) + + errMuxEntryWildcardMissingSlash = errors.New(` + Router: No slash(/) were found before wildcard in the route path: '%s'`) +) + +// getParamsLen returns the parameters length from a given path +func getParamsLen(path string) uint8 { + var n uint + for i := 0; i < len(path); i++ { + if path[i] != ':' && path[i] != '*' { // ParameterStartByte & MatchEverythingByte + continue + } + n++ + } + if n >= 255 { + return 255 + } + return uint8(n) +} + +// findLower returns the smaller number between a and b +func findLower(a, b int) int { + if a <= b { + return a + } + return b +} + +// add adds a muxEntry to the existing muxEntry or to the tree if no muxEntry has the prefix of +func (e *muxEntry) add(path string, middleware iris.Middleware) error { + fullPath := path + e.precedence++ + numParams := getParamsLen(path) + + if len(e.part) > 0 || len(e.nodes) > 0 { + loop: + for { + if numParams > e.paramsLen { + e.paramsLen = numParams + } + + i := 0 + max := findLower(len(path), len(e.part)) + for i < max && path[i] == e.part[i] { + i++ + } + + if i < len(e.part) { + node := muxEntry{ + part: e.part[i:], + hasWildNode: e.hasWildNode, + tokens: e.tokens, + nodes: e.nodes, + middleware: e.middleware, + precedence: e.precedence - 1, + } + + for i := range node.nodes { + if node.nodes[i].paramsLen > node.paramsLen { + node.paramsLen = node.nodes[i].paramsLen + } + } + + e.nodes = []*muxEntry{&node} + e.tokens = string([]byte{e.part[i]}) + e.part = path[:i] + e.middleware = nil + e.hasWildNode = false + } + + if i < len(path) { + path = path[i:] + + if e.hasWildNode { + e = e.nodes[0] + e.precedence++ + + if numParams > e.paramsLen { + e.paramsLen = numParams + } + numParams-- + + if len(path) >= len(e.part) && e.part == path[:len(e.part)] && + // Check for longer wildcard, e.g. :name and :names + (len(e.part) >= len(path) || path[len(e.part)] == '/') { + continue loop + } else { + // Wildcard conflict + part := strings.SplitN(path, "/", 2)[0] + prefix := fullPath[:strings.Index(fullPath, part)] + e.part + return errMuxEntryConflictsWildcard.Format(fullPath, e.part, prefix) + + } + + } + + c := path[0] + + if e.entryCase == hasParams && c == slashByte && len(e.nodes) == 1 { + e = e.nodes[0] + e.precedence++ + continue loop + } + for i := range e.tokens { + if c == e.tokens[i] { + i = e.precedenceTo(i) + e = e.nodes[i] + continue loop + } + } + + if c != parameterStartByte && c != matchEverythingByte { + + e.tokens += string([]byte{c}) + node := &muxEntry{ + paramsLen: numParams, + } + e.nodes = append(e.nodes, node) + e.precedenceTo(len(e.tokens) - 1) + e = node + } + return e.addNode(numParams, path, fullPath, middleware) + + } else if i == len(path) { + if e.middleware != nil { + return errMuxEntryMiddlewareAlreadyExists.Format(fullPath) + } + e.middleware = middleware + } + return nil + } + } else { + if err := e.addNode(numParams, path, fullPath, middleware); err != nil { + return err + } + e.entryCase = isRoot + } + return nil +} + +// addNode adds a muxEntry as children to other muxEntry +func (e *muxEntry) addNode(numParams uint8, path string, fullPath string, middleware iris.Middleware) error { + var offset int + + for i, max := 0, len(path); numParams > 0; i++ { + c := path[i] + if c != parameterStartByte && c != matchEverythingByte { + continue + } + + end := i + 1 + for end < max && path[end] != slashByte { + switch path[end] { + case parameterStartByte, matchEverythingByte: + return errMuxEntryInvalidWildcard.Format(path[i:], fullPath) + default: + end++ + } + } + + if len(e.nodes) > 0 { + return errMuxEntryConflictsExistingWildcard.Format(path[i:end], fullPath) + } + + if end-i < 2 { + return errMuxEntryWildcardUnnamed.Format(fullPath) + } + + if c == parameterStartByte { + + if i > 0 { + e.part = path[offset:i] + offset = i + } + + child := &muxEntry{ + entryCase: hasParams, + paramsLen: numParams, + } + e.nodes = []*muxEntry{child} + e.hasWildNode = true + e = child + e.precedence++ + numParams-- + + if end < max { + e.part = path[offset:end] + offset = end + + child := &muxEntry{ + paramsLen: numParams, + precedence: 1, + } + e.nodes = []*muxEntry{child} + e = child + } + + } else { + if end != max || numParams > 1 { + return errMuxEntryWildcardInvalidPlace.Format(fullPath) + } + + if len(e.part) > 0 && e.part[len(e.part)-1] == '/' { + return errMuxEntryWildcardConflictsMiddleware.Format(fullPath) + } + + i-- + if path[i] != slashByte { + return errMuxEntryWildcardMissingSlash.Format(fullPath) + } + + e.part = path[offset:i] + + child := &muxEntry{ + hasWildNode: true, + entryCase: matchEverything, + paramsLen: 1, + } + e.nodes = []*muxEntry{child} + e.tokens = string(path[i]) + e = child + e.precedence++ + + child = &muxEntry{ + part: path[i:], + entryCase: matchEverything, + paramsLen: 1, + middleware: middleware, + precedence: 1, + } + e.nodes = []*muxEntry{child} + + return nil + } + } + + e.part = path[offset:] + e.middleware = middleware + + return nil +} + +// get is used by the Router, it finds and returns the correct muxEntry for a path +func (e *muxEntry) get(path string, ctx *iris.Context) (mustRedirect bool) { +loop: + for { + if len(path) > len(e.part) { + if path[:len(e.part)] == e.part { + path = path[len(e.part):] + + if !e.hasWildNode { + c := path[0] + for i := range e.tokens { + if c == e.tokens[i] { + e = e.nodes[i] + continue loop + } + } + + mustRedirect = (path == slash && e.middleware != nil) + return + } + + e = e.nodes[0] + switch e.entryCase { + case hasParams: + + end := 0 + for end < len(path) && path[end] != '/' { + end++ + } + + ctx.Set(e.part[1:], path[:end]) + + if end < len(path) { + if len(e.nodes) > 0 { + path = path[end:] + e = e.nodes[0] + continue loop + } + + mustRedirect = (len(path) == end+1) + return + } + if ctx.Middleware = e.middleware; ctx.Middleware != nil { + return + } else if len(e.nodes) == 1 { + e = e.nodes[0] + mustRedirect = (e.part == slash && e.middleware != nil) + } + + return + + case matchEverything: + + ctx.Set(e.part[2:], path) + ctx.Middleware = e.middleware + return + + default: + return + } + } + } else if path == e.part { + if ctx.Middleware = e.middleware; ctx.Middleware != nil { + return + } + + if path == slash && e.hasWildNode && e.entryCase != isRoot { + mustRedirect = true + return + } + + for i := range e.tokens { + if e.tokens[i] == slashByte { + e = e.nodes[i] + mustRedirect = (len(e.part) == 1 && e.middleware != nil) || + (e.entryCase == matchEverything && e.nodes[0].middleware != nil) + return + } + } + + return + } + + mustRedirect = (path == slash) || + (len(e.part) == len(path)+1 && e.part[len(path)] == slashByte && + path == e.part[:len(e.part)-1] && e.middleware != nil) + return + } +} + +// precedenceTo just adds the priority of this muxEntry by an index +func (e *muxEntry) precedenceTo(index int) int { + e.nodes[index].precedence++ + _precedence := e.nodes[index].precedence + + newindex := index + for newindex > 0 && e.nodes[newindex-1].precedence < _precedence { + tmpN := e.nodes[newindex-1] + e.nodes[newindex-1] = e.nodes[newindex] + e.nodes[newindex] = tmpN + + newindex-- + } + + if newindex != index { + e.tokens = e.tokens[:newindex] + + e.tokens[index:index+1] + + e.tokens[newindex:index] + e.tokens[index+1:] + } + + return newindex +} + +type ( + muxTree struct { + method string + // subdomain is empty for default-hostname routes, + // ex: mysubdomain. + subdomain string + entry *muxEntry + } + + serveMux struct { + garden []*muxTree + maxParameters uint8 + methodEqual func(string, string) bool + hosts bool + } +) + +// New returns a new iris' policy to create and attach the router. +// It's based on the julienschmidt/httprouter with more features and some iris-relative performance tips: +// subdomains(wildcard/dynamic and static) and faster parameters set (use of the already-created context's values) +// and support for reverse routing. +func New() iris.Policies { + var logger func(iris.LogMode, string) + mux := &serveMux{ + methodEqual: func(reqMethod string, treeMethod string) bool { + return reqMethod == treeMethod + }, + } + matchEverythingString := string(matchEverythingByte) + return iris.Policies{ + EventPolicy: iris.EventPolicy{ + Boot: func(s *iris.Framework) { + logger = s.Log + }, + }, + RouterReversionPolicy: iris.RouterReversionPolicy{ + // path normalization done on iris' side + StaticPath: func(path string) string { + + i := strings.IndexByte(path, parameterStartByte) + x := strings.IndexByte(path, matchEverythingByte) + if i > -1 { + return path[0:i] + } + if x > -1 { + return path[0:x] + } + + return path + }, + WildcardPath: func(path string, paramName string) string { + return path + slash + matchEverythingString + paramName + }, + + // URLPath: func(r iris.RouteInfo, args ...string) string { + // argsLen := len(args) + // + // // we have named parameters but arguments not given + // if argsLen == 0 && r.formattedParts > 0 { + // return "" + // } else if argsLen == 0 && r.formattedParts == 0 { + // // it's static then just return the path + // return r.path + // } + // + // // we have arguments but they are much more than the named parameters + // + // // 1 check if we have /*, if yes then join all arguments to one as path and pass that as parameter + // if argsLen > r.formattedParts { + // if r.path[len(r.path)-1] == matchEverythingByte { + // // we have to convert each argument to a string in this case + // + // argsString := make([]string, argsLen, argsLen) + // + // for i, v := range args { + // if s, ok := v.(string); ok { + // argsString[i] = s + // } else if num, ok := v.(int); ok { + // argsString[i] = strconv.Itoa(num) + // } else if b, ok := v.(bool); ok { + // argsString[i] = strconv.FormatBool(b) + // } else if arr, ok := v.([]string); ok { + // if len(arr) > 0 { + // argsString[i] = arr[0] + // argsString = append(argsString, arr[1:]...) + // } + // } + // } + // + // parameter := strings.Join(argsString, slash) + // result := fmt.Sprintf(r.formattedPath, parameter) + // return result + // } + // // 2 if !1 return false + // return "" + // } + // + // arguments := joinPathArguments(args...) + // + // return fmt.Sprintf(r.formattedPath, arguments...) + // }, + RouteContextLinker: func(r iris.RouteInfo, ctx *iris.Context) { + tree := mux.getTree(r.Method(), r.Subdomain()) + if tree != nil { + tree.entry.get(ctx.Request.URL.Path, ctx) + } + }, + }, + RouterBuilderPolicy: func(repo iris.RouteRepository, context iris.ContextPool) http.Handler { + fatalErr := false + repo.Visit(func(r iris.RouteInfo) { + if fatalErr { + return + } + // add to the registry tree + method := r.Method() + subdomain := r.Subdomain() + path := r.Path() + middleware := r.Middleware() + tree := mux.getTree(method, subdomain) + if tree == nil { + //first time we register a route to this method with this domain + tree = &muxTree{method: method, subdomain: subdomain, entry: &muxEntry{}} + mux.garden = append(mux.garden, tree) + } + // I decide that it's better to explicit give subdomain and a path to it than registeredPath(mysubdomain./something) now its: subdomain: mysubdomain., path: /something + // we have different tree for each of subdomains, now you can use everything you can use with the normal paths ( before you couldn't set /any/*path) + if err := tree.entry.add(path, middleware); err != nil { + // while ProdMode means that the iris should not continue running + // by-default it panics on these errors, but to make sure let's introduce the fatalErr to stop visiting + fatalErr = true + logger(iris.ProdMode, "fatal error on httprouter build adaptor: "+err.Error()) + return + } + + if mp := tree.entry.paramsLen; mp > mux.maxParameters { + mux.maxParameters = mp + } + + // check for method equality if at least one route has cors + if r.HasCors() { + mux.methodEqual = func(reqMethod string, treeMethod string) bool { + // preflights + return reqMethod == iris.MethodOptions || reqMethod == treeMethod + } + } + + if subdomain != "" { + mux.hosts = true + } + }) + if !fatalErr { + return mux.buildHandler(context) + } + return nil + + }, + } +} + +func (mux *serveMux) getTree(method string, subdomain string) *muxTree { + for i := range mux.garden { + t := mux.garden[i] + if t.method == method && t.subdomain == subdomain { + return t + } + } + return nil +} + +func (mux *serveMux) buildHandler(pool iris.ContextPool) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + pool.Run(w, r, func(context *iris.Context) { + routePath := context.Path() + for i := range mux.garden { + tree := mux.garden[i] + if !mux.methodEqual(context.Request.Method, tree.method) { + continue + } + + if mux.hosts && tree.subdomain != "" { + // context.VirtualHost() is a slow method because it makes + // string.Replaces but user can understand that if subdomain then server will have some nano/or/milleseconds performance cost + requestHost := context.VirtualHostname() + hostname := context.Framework().Config.VHost + if requestHost != hostname { + //println(requestHost + " != " + mux.hostname) + // we have a subdomain + if strings.Contains(tree.subdomain, iris.DynamicSubdomainIndicator) { + } else { + //println(requestHost + " = " + mux.hostname) + // mux.host = iris-go.com:8080, the subdomain for example is api., + // so the host must be api.iris-go.com:8080 + if tree.subdomain+hostname != requestHost { + // go to the next tree, we have a subdomain but it is not the correct + continue + } + + } + } else { + //("it's subdomain but the request is the same as the listening addr mux.host == requestHost =>" + mux.host + "=" + requestHost + " ____ and tree's subdomain was: " + tree.subdomain) + continue + } + } + + mustRedirect := tree.entry.get(routePath, context) // pass the parameters here for 0 allocation + if context.Middleware != nil { + // ok we found the correct route, serve it and exit entirely from here + //ctx.Request.Header.SetUserAgentBytes(DefaultUserAgent) + context.Do() + return + } else if mustRedirect && !context.Framework().Config.DisablePathCorrection { // && context.Method() == MethodConnect { + reqPath := routePath + pathLen := len(reqPath) + + if pathLen > 1 { + if reqPath[pathLen-1] == '/' { + reqPath = reqPath[:pathLen-1] //remove the last / + } else { + //it has path prefix, it doesn't ends with / and it hasn't be found, then just add the slash + reqPath = reqPath + "/" + } + + urlToRedirect := reqPath + + statusForRedirect := iris.StatusMovedPermanently // StatusMovedPermanently, this document is obselte, clients caches this. + if tree.method == iris.MethodPost || + tree.method == iris.MethodPut || + tree.method == iris.MethodDelete { + statusForRedirect = iris.StatusTemporaryRedirect // To maintain POST data + } + + context.Redirect(urlToRedirect, statusForRedirect) + // RFC2616 recommends that a short note "SHOULD" be included in the + // response because older user agents may not understand 301/307. + // Shouldn't send the response for POST or HEAD; that leaves GET. + if tree.method == iris.MethodGet { + note := "Moved Permanently.\n" + // ignore error + context.WriteString(note) + } + return + } + } + // not found + break + } + // https://github.com/kataras/iris/issues/469 + if context.Framework().Config.FireMethodNotAllowed { + for i := range mux.garden { + tree := mux.garden[i] + if !mux.methodEqual(context.Method(), tree.method) { + continue + } + } + context.EmitError(iris.StatusMethodNotAllowed) + return + } + context.EmitError(iris.StatusNotFound) + }) + }) + +} + +//THESE ARE FROM Go Authors "html" package +var htmlReplacer = strings.NewReplacer( + "&", "&", + "<", "<", + ">", ">", + // """ is shorter than """. + `"`, """, + // "'" is shorter than "'" and apos was not in HTML until HTML5. + "'", "'", +) + +// HTMLEscape returns a string which has no valid html code +func HTMLEscape(s string) string { + return htmlReplacer.Replace(s) +} diff --git a/adaptors/httprouter/urlpath.go b/adaptors/httprouter/urlpath.go new file mode 100644 index 00000000..294a6dc7 --- /dev/null +++ b/adaptors/httprouter/urlpath.go @@ -0,0 +1,23 @@ +package httprouter + +func joinPathArguments(args ...interface{}) []interface{} { + arguments := args[0:] + for i, v := range arguments { + if arr, ok := v.([]string); ok { + if len(arr) > 0 { + interfaceArr := make([]interface{}, len(arr)) + for j, sv := range arr { + interfaceArr[j] = sv + } + // replace the current slice + // with the first string element (always as interface{}) + arguments[i] = interfaceArr[0] + // append the rest of them to the slice itself + // the range is not affected by these things in go, + // so we are safe to do it. + arguments = append(args, interfaceArr[1:]...) + } + } + } + return arguments +} diff --git a/adaptors/typescript/LICENSE b/adaptors/typescript/LICENSE new file mode 100644 index 00000000..2935ad5d --- /dev/null +++ b/adaptors/typescript/LICENSE @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2016-2017 Gerasimos Maropoulos + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/adaptors/typescript/README.md b/adaptors/typescript/README.md new file mode 100644 index 00000000..d15a30f4 --- /dev/null +++ b/adaptors/typescript/README.md @@ -0,0 +1,89 @@ +## Package information + +This is an Iris and typescript bridge plugin. + +1. Search for typescript files (.ts) +2. Search for typescript projects (.tsconfig) +3. If 1 || 2 continue else stop +4. Check if typescript is installed, if not then auto-install it (always inside npm global modules, -g) +5. If typescript project then build the project using tsc -p $dir +6. If typescript files and no project then build each typescript using tsc $filename +7. Watch typescript files if any changes happens, then re-build (5|6) + +> Note: Ignore all typescript files & projects whose path has '/node_modules/' + +## Install + +```sh +$ go get -u github.com/iris-contrib/plugin/typescript +``` + + +## Options + +This plugin has **optionally** options +1. Bin: string, the typescript installation path/bin/tsc or tsc.cmd, if empty then it will search to the global npm modules +2. Dir: string, Dir set the root, where to search for typescript files/project. Default "./" +3. Ignore: string, comma separated ignore typescript files/project from these directories. Default "" (node_modules are always ignored) +4. Tsconfig: &typescript.Tsconfig{}, here you can set all compilerOptions if no tsconfig.json exists inside the 'Dir' +5. Editor: typescript.Editor(), if setted then alm-tools browser-based typescript IDE will be available. Defailt is nil + +> Note: if any string in Ignore doesn't start with './' then it will ignore all files which contains this path string. +For example /node_modules/ will ignore all typescript files that are inside at ANY '/node_modules/', that means and the submodules. + + +## How to use + +```go + +package main + +import ( + "github.com/kataras/iris" + "github.com/iris-contrib/plugin/typescript" +) + +func main(){ + /* Options + Bin -> the typescript installation path/bin/tsc or tsc.cmd, if empty then it will search to the global npm modules + Dir -> where to search for typescript files/project. Default "./" + Ignore -> comma separated ignore typescript files/project from these directories (/node_modules/ are always ignored). Default "" + Tsconfig -> &typescript.Tsconfig{}, here you can set all compilerOptions if no tsconfig.json exists inside the 'Dir' + Editor -> typescript.Editor(), if setted then alm-tools browser-based typescript IDE will be available. Default is nil. + */ + + config := typescript.Config { + Dir: "./scripts/src", + Tsconfig: &typescript.Tsconfig{Module: "commonjs", Target: "es5"}, // or typescript.DefaultTsconfig() + } + + //if you want to change only certain option(s) but you want default to all others then you have to do this: + config = typescript.DefaultConfig() + // + + iris.Plugins.Add(typescript.New(config)) //or with the default options just: typescript.New() + + iris.Default.Get("/", func (ctx *iris.Context){}) + + iris.Default.Listen(":8080") +} + + +``` + +## Editor + +[alm-tools](http://alm.tools) is a typescript online IDE/Editor, made by [@basarat](https://twitter.com/basarat) one of the top contributors of the [Typescript](http://www.typescriptlang.org). + +Iris gives you the opportunity to edit your client-side using the alm-tools editor, via the editor plugin. +With typescript plugin you have to set the Editor option and you're ready: + +```go +typescript.Config { + //... + Editor: typescript.Editor("username","passowrd") + //... +} +``` + +> [Read more](https://github.com/kataras/iris/tree/development/plugin/editor) for Editor diff --git a/adaptors/typescript/_example/main.go b/adaptors/typescript/_example/main.go new file mode 100644 index 00000000..07733d55 --- /dev/null +++ b/adaptors/typescript/_example/main.go @@ -0,0 +1,31 @@ +package main + +import ( + "gopkg.in/kataras/iris.v6" + "gopkg.in/kataras/iris.v6/adaptors/httprouter" + "gopkg.in/kataras/iris.v6/adaptors/typescript" +) + +func main() { + app := iris.New() + app.Adapt(iris.DevLogger()) + app.Adapt(httprouter.New()) // adapt a router, order doesn't matters but before Listen. + + ts := typescript.New() + ts.Config.Dir = "./www/scripts" + app.Adapt(ts) // adapt the typescript compiler adaptor + + app.StaticWeb("/", "./www") // serve the index.html + app.Listen(":8080") +} + +// open http://localhost:8080 +// go to ./www/scripts/app.ts +// make a change +// reload the http://localhost:8080 and you should see the changes +// +// what it does? +// - compiles the typescript files using default compiler options if not tsconfig found +// - watches for changes on typescript files, if a change then it recompiles the .ts to .js +// +// same as you used to do with gulp-like tools, but here at Iris I do my bests to help GO developers. diff --git a/adaptors/typescript/_example/www/index.html b/adaptors/typescript/_example/www/index.html new file mode 100644 index 00000000..92bb58ae --- /dev/null +++ b/adaptors/typescript/_example/www/index.html @@ -0,0 +1,8 @@ + + +Load my script (lawl) + + + + + diff --git a/adaptors/typescript/_example/www/scripts/app.ts b/adaptors/typescript/_example/www/scripts/app.ts new file mode 100644 index 00000000..66af02a5 --- /dev/null +++ b/adaptors/typescript/_example/www/scripts/app.ts @@ -0,0 +1,16 @@ +class User{ + private name: string; + + constructor(fullname:string) { + this.name = fullname; + } + + Hi(msg: string): string { + return msg + " "+ this.name; + } + +} + +var user = new User("kataras"); +var hi = user.Hi("Hello"); +window.alert(hi); diff --git a/adaptors/typescript/config.go b/adaptors/typescript/config.go new file mode 100644 index 00000000..912fbfc8 --- /dev/null +++ b/adaptors/typescript/config.go @@ -0,0 +1,192 @@ +package typescript + +import ( + "encoding/json" + "io/ioutil" + "os" + "reflect" + "strconv" + + "gopkg.in/kataras/iris.v6/adaptors/typescript/npm" +) + +var ( + pathSeparator = string(os.PathSeparator) + nodeModules = pathSeparator + "node_modules" + pathSeparator +) + +type ( + // Tsconfig the struct for tsconfig.json + Tsconfig struct { + CompilerOptions CompilerOptions `json:"compilerOptions"` + Exclude []string `json:"exclude"` + } + + // CompilerOptions contains all the compiler options used by the tsc (typescript compiler) + CompilerOptions struct { + Declaration bool `json:"declaration"` + Module string `json:"module"` + Target string `json:"target"` + Watch bool `json:"watch"` + Charset string `json:"charset"` + Diagnostics bool `json:"diagnostics"` + EmitBOM bool `json:"emitBOM"` + EmitDecoratorMetadata bool `json:"emitDecoratorMetadata"` + ExperimentalDecorators bool `json:"experimentalDecorators"` + InlineSourceMap bool `json:"inlineSourceMap"` + InlineSources bool `json:"inlineSources"` + IsolatedModules bool `json:"isolatedModules"` + Jsx string `json:"jsx"` + ReactNamespace string `json:"reactNamespace"` + ListFiles bool `json:"listFiles"` + Locale string `json:"locale"` + MapRoot string `json:"mapRoot"` + ModuleResolution string `json:"moduleResolution"` + NewLine string `json:"newLine"` + NoEmit bool `json:"noEmit"` + NoEmitOnError bool `json:"noEmitOnError"` + NoEmitHelpers bool `json:"noEmitHelpers"` + NoImplicitAny bool `json:"noImplicitAny"` + NoLib bool `json:"noLib"` + NoResolve bool `json:"noResolve"` + SkipDefaultLibCheck bool `json:"skipDefaultLibCheck"` + OutDir string `json:"outDir"` + OutFile string `json:"outFile"` + PreserveConstEnums bool `json:"preserveConstEnums"` + Pretty bool `json:"pretty"` + RemoveComments bool `json:"removeComments"` + RootDir string `json:"rootDir"` + SourceMap bool `json:"sourceMap"` + SourceRoot string `json:"sourceRoot"` + StripInternal bool `json:"stripInternal"` + SuppressExcessPropertyErrors bool `json:"suppressExcessPropertyErrors"` + SuppressImplicitAnyIndexErrors bool `json:"suppressImplicitAnyIndexErrors"` + AllowUnusedLabels bool `json:"allowUnusedLabels"` + NoImplicitReturns bool `json:"noImplicitReturns"` + NoFallthroughCasesInSwitch bool `json:"noFallthroughCasesInSwitch"` + AllowUnreachableCode bool `json:"allowUnreachableCode"` + ForceConsistentCasingInFileNames bool `json:"forceConsistentCasingInFileNames"` + AllowSyntheticDefaultImports bool `json:"allowSyntheticDefaultImports"` + AllowJs bool `json:"allowJs"` + NoImplicitUseStrict bool `json:"noImplicitUseStrict"` + } + + // Config the configs for the Typescript plugin + // Has five (5) fields + // + // 1. Bin: string, the typescript installation directory/typescript/lib/tsc.js, if empty it will search inside global npm modules + // 2. Dir: string, Dir set the root, where to search for typescript files/project. Default "./" + // 3. Ignore: string, comma separated ignore typescript files/project from these directories. Default "" (node_modules are always ignored) + // 4. Tsconfig: &typescript.Tsconfig{}, here you can set all compilerOptions if no tsconfig.json exists inside the 'Dir' + // 5. Editor: typescript.Editor("username","password"), if setted then alm-tools browser-based typescript IDE will be available. Defailt is nil + Config struct { + // Bin the path of the tsc binary file + // if empty then the plugin tries to find it + Bin string + // Dir the client side directory, which typescript (.ts) files are live + Dir string + // Ignore ignore folders, default is /node_modules/ + Ignore string + // Tsconfig the typescript build configs, including the compiler's options + Tsconfig *Tsconfig + } +) + +// CompilerArgs returns the CompilerOptions' contents of the Tsconfig +// it reads the json tags, add '--' at the start of each one and returns an array of strings +// this is from file +func (tsconfig *Tsconfig) CompilerArgs() []string { + val := reflect.ValueOf(tsconfig).Elem().FieldByName("CompilerOptions") // -> for tsconfig *Tsconfig + // val := reflect.ValueOf(tsconfig.CompilerOptions) + compilerOpts := make([]string, 0) // 0 because we don't know the real valid options yet. + for i := 0; i < val.NumField(); i++ { + typeField := val.Type().Field(i) + valueFieldG := val.Field(i) + var valueField string + // only if it's string or int we need to put that + if valueFieldG.Kind() == reflect.String { + //if valueFieldG.String() != "" { + //valueField = strconv.QuoteToASCII(valueFieldG.String()) + // } + valueField = valueFieldG.String() + } else if valueFieldG.Kind() == reflect.Int { + if valueFieldG.Int() > 0 { + valueField = strconv.Itoa(int(valueFieldG.Int())) + } + } else if valueFieldG.Kind() == reflect.Bool { + valueField = strconv.FormatBool(valueFieldG.Bool()) + } + + if valueField != "" && valueField != "false" { + // var opt string + + // key := typeField.Tag.Get("json") + // // it's bool value of true then just --key, for example --watch + // if valueField == "true" { + // opt = "--" + key + // } else { + // // it's a string now, for example -m commonjs + // opt = "-" + string(key[0]) + " " + valueField + // } + key := "--" + typeField.Tag.Get("json") + compilerOpts = append(compilerOpts, key) + // the form is not '--module ES6' but os.Exec should recognise them as arguments + // so we need to put the values on the next index + if valueField != "true" { + // it's a string now, for example -m commonjs + compilerOpts = append(compilerOpts, valueField) + } + + } + + } + + return compilerOpts +} + +// FromFile reads a file & returns the Tsconfig by its contents +func FromFile(tsConfigAbsPath string) (config Tsconfig, err error) { + file, err := ioutil.ReadFile(tsConfigAbsPath) + if err != nil { + return + } + config = Tsconfig{} + err = json.Unmarshal(file, &config) + + return +} + +// DefaultTsconfig returns the default Tsconfig, with CompilerOptions module: commonjs, target: es5 and ignore the node_modules +func DefaultTsconfig() Tsconfig { + return Tsconfig{ + CompilerOptions: CompilerOptions{ + Module: "commonjs", + Target: "ES6", + Jsx: "react", + ModuleResolution: "classic", + Locale: "en", + Watch: true, + NoImplicitAny: false, + SourceMap: false, + }, + Exclude: []string{"node_modules"}, + } + +} + +// DefaultConfig returns the default Options of the Typescript adaptor +// Bin and Editor are setting in runtime via the adaptor +func DefaultConfig() Config { + root, err := os.Getwd() + if err != nil { + panic("Typescript Adaptor: Cannot get the Current Working Directory !!! [os.getwd()]") + } + compilerTsConfig := DefaultTsconfig() + c := Config{ + Dir: root + pathSeparator, + Ignore: nodeModules, + Tsconfig: &compilerTsConfig, + } + c.Bin = npm.NodeModuleAbs("typescript/lib/tsc.js") + return c +} diff --git a/adaptors/typescript/editor/LICENSE b/adaptors/typescript/editor/LICENSE new file mode 100644 index 00000000..902fed9a --- /dev/null +++ b/adaptors/typescript/editor/LICENSE @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2016 Basarat Ali Syed author of alm-tools + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. \ No newline at end of file diff --git a/adaptors/typescript/editor/_example/main.go b/adaptors/typescript/editor/_example/main.go new file mode 100644 index 00000000..6d39db6c --- /dev/null +++ b/adaptors/typescript/editor/_example/main.go @@ -0,0 +1,35 @@ +package main + +import ( + "gopkg.in/kataras/iris.v6" + "gopkg.in/kataras/iris.v6/adaptors/httprouter" + "gopkg.in/kataras/iris.v6/adaptors/typescript" // optinally + "gopkg.in/kataras/iris.v6/adaptors/typescript/editor" +) + +func main() { + app := iris.New() + app.Adapt(iris.DevLogger()) + app.Adapt(httprouter.New()) // adapt a router, order doesn't matters + + // optionally but good to have, I didn't put inside editor or the editor in the typescript compiler adaptors + // because you may use tools like gulp and you may use the editor without the typescript compiler adaptor. + // but if you need auto-compilation on .ts, we have a solution: + ts := typescript.New() + ts.Config.Dir = "./www/scripts/" + app.Adapt(ts) // adapt the typescript compiler adaptor + + editorConfig := editor.Config{ + Hostname: "127.0.0.1", + Port: 4444, + WorkingDir: "./www/scripts/", // "/path/to/the/client/side/directory/", + Username: "myusername", + Password: "mypassword", + } + e := editor.New(editorConfig) + app.Adapt(e) // adapt the editor + + app.StaticWeb("/", "./www") // serve the index.html + + app.Listen(":8080") +} diff --git a/adaptors/typescript/editor/_example/www/index.html b/adaptors/typescript/editor/_example/www/index.html new file mode 100644 index 00000000..92bb58ae --- /dev/null +++ b/adaptors/typescript/editor/_example/www/index.html @@ -0,0 +1,8 @@ + + +Load my script (lawl) + + + + + diff --git a/adaptors/typescript/editor/_example/www/scripts/app.ts b/adaptors/typescript/editor/_example/www/scripts/app.ts new file mode 100644 index 00000000..cc750583 --- /dev/null +++ b/adaptors/typescript/editor/_example/www/scripts/app.ts @@ -0,0 +1,16 @@ +class User{ + private name: string; + + constructor(fullname:string) { + this.name = fullname; + } + + Hi(msg: string): string { + return msg + " " + this.name; + } + +} + +var user = new User("kataras"); +var hi = user.Hi("Hello"); +window.alert(hi); diff --git a/adaptors/typescript/editor/_example/www/scripts/tsconfig.json b/adaptors/typescript/editor/_example/www/scripts/tsconfig.json new file mode 100644 index 00000000..39188141 --- /dev/null +++ b/adaptors/typescript/editor/_example/www/scripts/tsconfig.json @@ -0,0 +1,22 @@ +{ + "compilerOptions": { + "module": "commonjs", + "noImplicitAny": false, + "removeComments": true, + "preserveConstEnums": true, + "sourceMap": false, + "target": "ES5", + "noEmit": false, + "watch":true, + "noEmitOnError": true, + "experimentalDecorators": false, + "outDir": "./", + "charset": "UTF-8", + "noLib": false, + "diagnostics": true, + "declaration": false + }, + "files": [ + "./app.ts" + ] +} diff --git a/adaptors/typescript/editor/config.go b/adaptors/typescript/editor/config.go new file mode 100644 index 00000000..928fc410 --- /dev/null +++ b/adaptors/typescript/editor/config.go @@ -0,0 +1,78 @@ +package editor + +import ( + "github.com/imdario/mergo" + "os" +) + +// Default values for the configuration +const ( + DefaultPort = 4444 +) + +// Config the configs for the Editor plugin +type Config struct { + // Hostname if empty used the iris server's hostname + Hostname string + // Port if 0 4444 + Port int + // KeyFile the key file(ssl optional) + KeyFile string + // CertFile the cert file (ssl optional) + CertFile string + // WorkingDir if empty "./" + WorkingDir string + // Username defaults to empty, you should set this + Username string + // Password defaults to empty, you should set this + Password string + // DisableOutput set that to true if you don't care about alm-tools' messages + // they are useful because that the default value is "false" + DisableOutput bool +} + +// DefaultConfig returns the default configs for the Editor plugin +func DefaultConfig() Config { + // explicit + return Config{ + Hostname: "", + Port: 4444, + KeyFile: "", + CertFile: "", + WorkingDir: "." + string(os.PathSeparator), // alm-tools should end with path separator. + Username: "", + Password: "", + DisableOutput: false, + } +} + +// Merge merges the default with the given config and returns the result +func (c Config) Merge(cfg []Config) (config Config) { + + if len(cfg) > 0 { + config = cfg[0] + if err := mergo.Merge(&config, c); err != nil { + if !c.DisableOutput { + panic(err) + } + } + } else { + _default := c + config = _default + } + + return +} + +// MergeSingle merges the default with the given config and returns the result +func (c Config) MergeSingle(cfg Config) (config Config) { + + config = cfg + if err := mergo.Merge(&config, c); err != nil { + if !c.DisableOutput { + panic(err) + } + } + + return +} diff --git a/adaptors/typescript/editor/editor.go b/adaptors/typescript/editor/editor.go new file mode 100644 index 00000000..2613f35f --- /dev/null +++ b/adaptors/typescript/editor/editor.go @@ -0,0 +1,216 @@ +package editor + +// +------------------------------------------------------------+ +// | Editor usage | +// +------------------------------------------------------------+ +// +// import "gopkg.in/kataras/iris.v6/adaptors/editor" +// +// e := editor.New(editor.Config{}) +// app.Adapt(e) +// +// app.Listen(":8080") + +// +// +------------------------------------------------------------+ +// | General notes for authentication | +// +------------------------------------------------------------+ +// +// The Authorization specifies the authentication mechanism (in this case Basic) followed by the username and password. +// Although, the string aHR0cHdhdGNoOmY= may look encrypted it is simply a base64 encoded version of :. +// Would be readily available to anyone who could intercept the HTTP request. + +import ( + "bufio" + "fmt" + "io" + "os" + "path/filepath" + "strconv" + + "gopkg.in/kataras/iris.v6" + "gopkg.in/kataras/iris.v6/adaptors/typescript/npm" +) + +type ( + // Editor is the alm-tools adaptor. + // + // It holds a logger from the iris' station + // username,password for basic auth + // directory which the client side code is + // keyfile,certfile for TLS listening + // and a host which is listening for + Editor struct { + config *Config + logger func(iris.LogMode, string) + enabled bool // default true + // after alm started + process *os.Process + debugOutput io.Writer + } +) + +// New creates and returns an Editor Plugin instance +func New(cfg ...Config) *Editor { + c := DefaultConfig().Merge(cfg) + c.WorkingDir = validateWorkingDir(c.WorkingDir) // add "/" if not exists + + return &Editor{ + enabled: true, + config: &c, + } +} + +// Adapt adapts the editor with Iris. +// Note: +// We use that method and not the return on New because we +// want to export the Editor's functionality to the user. +func (e *Editor) Adapt(frame *iris.Policies) { + policy := iris.EventPolicy{ + Build: e.build, + Interrupted: e.close, + } + + policy.Adapt(frame) +} + +// User set a user, accepts two parameters: username (string), string (string) +func (e *Editor) User(username string, password string) *Editor { + e.config.Username = username + e.config.Password = password + return e +} + +func validateWorkingDir(workingDir string) string { + l := workingDir[len(workingDir)-1] + + if l != '/' && l != os.PathSeparator { + workingDir += "/" + } + return workingDir +} + +// Dir sets the directory which the client side source code alive +func (e *Editor) Dir(workingDir string) *Editor { + e.config.WorkingDir = validateWorkingDir(workingDir) + return e +} + +// Port sets the port (int) for the editor adaptor's standalone server +func (e *Editor) Port(port int) *Editor { + e.config.Port = port + return e +} + +// SetEnable if true enables the editor adaptor, otherwise disables it +func (e *Editor) SetEnable(enable bool) { + e.enabled = enable +} + +// DisableOutput call that if you don't care about alm-tools' messages +// they are useful because that the default configuration shows them +func (e *Editor) DisableOutput() { + e.config.DisableOutput = true +} + +// GetDescription EditorPlugin is a bridge between Iris and the alm-tools, the browser-based IDE for client-side sources. +func (e *Editor) GetDescription() string { + return "A bridge between Iris and the alm-tools, the browser-based IDE." +} + +// we use that editorWriter to prefix the editor's output with "Editor Adaptor: " +type editorWriter struct { + underline io.Writer +} + +// build runs before the server's listens, creates the listener ( use of port parent hostname:DefaultPort if not exist) +func (e *Editor) build(s *iris.Framework) { + e.logger = s.Log + if e.config.Hostname == "" { + e.config.Hostname = iris.ParseHostname(s.Config.VHost) + } + + if e.config.Port <= 0 { + e.config.Port = DefaultPort + } + + if s, err := filepath.Abs(e.config.WorkingDir); err == nil { + e.config.WorkingDir = s + } + + e.start() +} + +// close kills the editor's server when Iris is closed +func (e *Editor) close(s *iris.Framework) { + if e.process != nil { + err := e.process.Kill() + if err != nil { + e.logger(iris.DevMode, fmt.Sprintf(`Error while trying to terminate the Editor, + please kill this process by yourself, process id: %d`, e.process.Pid)) + } + } +} + +// start starts the job +func (e *Editor) start() { + if e.config.Username == "" || e.config.Password == "" { + e.logger(iris.ProdMode, `Error before running alm-tools. + You have to set username & password for security reasons, otherwise this adaptor won't run.`) + return + } + + if !npm.NodeModuleExists("alm/bin/alm") { + e.logger(iris.DevMode, "Installing alm-tools, please wait...") + res := npm.NodeModuleInstall("alm") + if res.Error != nil { + e.logger(iris.ProdMode, res.Error.Error()) + return + } + e.logger(iris.DevMode, res.Message) + } + + cmd := npm.CommandBuilder("node", npm.NodeModuleAbs("alm/src/server.js")) + cmd.AppendArguments("-a", e.config.Username+":"+e.config.Password, + "-h", e.config.Hostname, "-t", strconv.Itoa(e.config.Port), "-d", e.config.WorkingDir[0:len(e.config.WorkingDir)-1]) + // for auto-start in the browser: cmd.AppendArguments("-o") + if e.config.KeyFile != "" && e.config.CertFile != "" { + cmd.AppendArguments("--httpskey", e.config.KeyFile, "--httpscert", e.config.CertFile) + } + + // when debug is not disabled + // show any messages to the user( they are useful here) + // to the io.Writer that iris' user is defined from configuration + if !e.config.DisableOutput { + + outputReader, err := cmd.StdoutPipe() + if err == nil { + outputScanner := bufio.NewScanner(outputReader) + + go func() { + for outputScanner.Scan() { + e.logger(iris.DevMode, "Editor: "+outputScanner.Text()) + } + }() + + errReader, err := cmd.StderrPipe() + if err == nil { + errScanner := bufio.NewScanner(errReader) + go func() { + for errScanner.Scan() { + e.logger(iris.DevMode, "Editor: "+errScanner.Text()) + } + }() + } + } + } + + err := cmd.Start() + if err != nil { + e.logger(iris.ProdMode, "Error while running alm-tools. Trace: "+err.Error()) + return + } + + // no need, alm-tools post these + // e.logger.Printf("Editor is running at %s:%d | %s", e.config.Hostname, e.config.Port, e.config.WorkingDir) +} diff --git a/utils/exec.go b/adaptors/typescript/npm/exec.go similarity index 99% rename from utils/exec.go rename to adaptors/typescript/npm/exec.go index d990100a..d26bf889 100644 --- a/utils/exec.go +++ b/adaptors/typescript/npm/exec.go @@ -1,4 +1,4 @@ -package utils // #nosec +package npm // #nosec import ( "fmt" diff --git a/adaptors/typescript/npm/npm.go b/adaptors/typescript/npm/npm.go new file mode 100644 index 00000000..bbc8a056 --- /dev/null +++ b/adaptors/typescript/npm/npm.go @@ -0,0 +1,124 @@ +package npm + +import ( + "fmt" + "strings" + "time" +) + +var ( + // nodeModulesPath is the path of the root npm modules + // Ex: C:\\Users\\kataras\\AppData\\Roaming\\npm\\node_modules + nodeModulesPath string +) + +type ( + // NodeModuleResult holds Message and Error, if error != nil then the npm command has failed + NodeModuleResult struct { + // Message the message (string) + Message string + // Error the error (if any) + Error error + } +) + +// NodeModulesPath sets the root directory for the node_modules and returns that +func NodeModulesPath() string { + if nodeModulesPath == "" { + nodeModulesPath = MustCommand("npm", "root", "-g") //here it ends with \n we have to remove it + nodeModulesPath = nodeModulesPath[0 : len(nodeModulesPath)-1] + } + return nodeModulesPath +} + +func success(output string, a ...interface{}) NodeModuleResult { + return NodeModuleResult{fmt.Sprintf(output, a...), nil} +} + +func fail(errMsg string, a ...interface{}) NodeModuleResult { + return NodeModuleResult{"", fmt.Errorf("\n"+errMsg, a...)} +} + +// Output returns the error message if result.Error exists, otherwise returns the result.Message +func (res NodeModuleResult) Output() (out string) { + if res.Error != nil { + out = res.Error.Error() + } else { + out = res.Message + } + return +} + +// NodeModuleInstall installs a module +func NodeModuleInstall(moduleName string) NodeModuleResult { + finish := make(chan bool) + + go func() { + print("\n|") + print("_") + print("|") + + for { + select { + case v := <-finish: + { + if v { + print("\010\010\010") //remove the loading chars + close(finish) + return + } + + } + default: + print("\010\010-") + time.Sleep(time.Second / 2) + print("\010\\") + time.Sleep(time.Second / 2) + print("\010|") + time.Sleep(time.Second / 2) + print("\010/") + time.Sleep(time.Second / 2) + print("\010-") + time.Sleep(time.Second / 2) + print("|") + } + } + + }() + out, err := Command("npm", "install", moduleName, "-g") + finish <- true + if err != nil { + return fail("Error installing module %s. Trace: %s", moduleName, err.Error()) + } + + return success("\n%s installed %s", moduleName, out) + +} + +// NodeModuleUnistall removes a module +func NodeModuleUnistall(moduleName string) NodeModuleResult { + out, err := Command("npm", "unistall", "-g", moduleName) + if err != nil { + return fail("Error unstalling module %s. Trace: %s", moduleName, err.Error()) + } + return success("\n %s unistalled %s", moduleName, out) + +} + +// NodeModuleAbs returns the absolute path of the global node_modules directory + relative +func NodeModuleAbs(relativePath string) string { + return NodeModulesPath() + PathSeparator + strings.Replace(relativePath, "/", PathSeparator, -1) +} + +// NodeModuleExists returns true if a module exists +// here we have two options +//1 . search by command something like npm -ls -g --depth=x +//2. search on files, we choose the second +func NodeModuleExists(executableRelativePath string) bool { + execAbsPath := NodeModuleAbs(executableRelativePath) + if execAbsPath == "" { + return false + } + + return Exists(execAbsPath) +} diff --git a/adaptors/typescript/typescript.go b/adaptors/typescript/typescript.go new file mode 100644 index 00000000..035e36c2 --- /dev/null +++ b/adaptors/typescript/typescript.go @@ -0,0 +1,236 @@ +package typescript + +import ( + "errors" + "fmt" + "os" + "path/filepath" + "strings" + + "gopkg.in/kataras/iris.v6" + "gopkg.in/kataras/iris.v6/adaptors/typescript/npm" +) + +type ( + // TsAdaptor the struct of the Typescript TsAdaptor, holds all necessary fields & methods + TsAdaptor struct { + Config *Config + // taken from framework + logger func(iris.LogMode, string) + } +) + +// New creates & returns a new instnace typescript plugin +func New() *TsAdaptor { + c := DefaultConfig() + + if !strings.Contains(c.Ignore, nodeModules) { + c.Ignore += "," + nodeModules + } + + return &TsAdaptor{Config: &c} +} + +// Adapt addapts a TsAdaptor to the Policies via EventPolicy. +// We use that method instead of direct return EventPolicy from new because +// the user should be able to change its configuration from that public API +func (t *TsAdaptor) Adapt(frame *iris.Policies) { + policy := iris.EventPolicy{ + Build: t.build, + } + + policy.Adapt(frame) +} + +func (t *TsAdaptor) build(s *iris.Framework) { + t.logger = s.Log + t.start() +} + +// + +// implementation + +func (t *TsAdaptor) start() { + + if t.hasTypescriptFiles() { + //Can't check if permission denied returns always exists = true.... + + if !npm.NodeModuleExists(t.Config.Bin) { + t.logger(iris.DevMode, "Installing typescript, please wait...") + res := npm.NodeModuleInstall("typescript") + if res.Error != nil { + t.logger(iris.ProdMode, res.Error.Error()) + return + } + t.logger(iris.DevMode, res.Message) + } + + projects := t.getTypescriptProjects() + if len(projects) > 0 { + watchedProjects := 0 + //typescript project (.tsconfig) found + for _, project := range projects { + cmd := npm.CommandBuilder("node", t.Config.Bin, "-p", project[0:strings.LastIndex(project, npm.PathSeparator)]) //remove the /tsconfig.json) + projectConfig, perr := FromFile(project) + if perr != nil { + t.logger(iris.ProdMode, "error while trying to read tsconfig: "+perr.Error()) + continue + } + + if projectConfig.CompilerOptions.Watch { + watchedProjects++ + // if has watch : true then we have to wrap the command to a goroutine (I don't want to use the .Start here) + go func() { + _, err := cmd.Output() + if err != nil { + t.logger(iris.DevMode, err.Error()) + return + } + }() + } else { + + _, err := cmd.Output() + if err != nil { + t.logger(iris.DevMode, err.Error()) + return + } + + } + + } + t.logger(iris.DevMode, fmt.Sprintf("%d Typescript project(s) compiled ( %d monitored by a background file watcher ) ", len(projects), watchedProjects)) + } else { + //search for standalone typescript (.ts) files and compile them + files := t.getTypescriptFiles() + if len(files) > 0 { + watchedFiles := 0 + if t.Config.Tsconfig.CompilerOptions.Watch { + watchedFiles = len(files) + } + //it must be always > 0 if we came here, because of if hasTypescriptFiles == true. + for _, file := range files { + absPath, err := filepath.Abs(file) + if err != nil { + continue + } + + //these will be used if no .tsconfig found. + // cmd := npm.CommandBuilder("node", t.Config.Bin) + // cmd.Arguments(t.Config.Bin, t.Config.Tsconfig.CompilerArgs()...) + // cmd.AppendArguments(absPath) + compilerArgs := t.Config.Tsconfig.CompilerArgs() + cmd := npm.CommandBuilder("node", t.Config.Bin) + for _, s := range compilerArgs { + cmd.AppendArguments(s) + } + cmd.AppendArguments(absPath) + go func() { + compilerMsgB, _ := cmd.Output() + compilerMsg := string(compilerMsgB) + cmd.Args = cmd.Args[0 : len(cmd.Args)-1] //remove the last, which is the file + + if strings.Contains(compilerMsg, "error") { + t.logger(iris.DevMode, compilerMsg) + } + + }() + + } + t.logger(iris.DevMode, fmt.Sprintf("%d Typescript file(s) compiled ( %d monitored by a background file watcher )", len(files), watchedFiles)) + } + + } + + } +} + +func (t *TsAdaptor) hasTypescriptFiles() bool { + root := t.Config.Dir + ignoreFolders := strings.Split(t.Config.Ignore, ",") + hasTs := false + if !npm.Exists(root) { + t.logger(iris.ProdMode, fmt.Sprintf("Typescript Adaptor Error: Directory '%s' couldn't be found,\nplease specify a valid path for your *.ts files", root)) + return false + } + // ignore error + filepath.Walk(root, func(path string, fi os.FileInfo, err error) error { + + if fi.IsDir() { + return nil + } + for i := range ignoreFolders { + if strings.Contains(path, ignoreFolders[i]) { + return nil + } + } + if strings.HasSuffix(path, ".ts") { + hasTs = true + return errors.New("Typescript found, hope that will stop here") + } + + return nil + }) + return hasTs +} + +func (t *TsAdaptor) getTypescriptProjects() []string { + var projects []string + ignoreFolders := strings.Split(t.Config.Ignore, ",") + + root := t.Config.Dir + //t.logger.Printf("\nSearching for typescript projects in %s", root) + + // ignore error + filepath.Walk(root, func(path string, fi os.FileInfo, err error) error { + if fi.IsDir() { + return nil + } + for i := range ignoreFolders { + if strings.Contains(path, ignoreFolders[i]) { + //t.logger.Println(path + " ignored") + return filepath.SkipDir + } + } + + if strings.HasSuffix(path, npm.PathSeparator+"tsconfig.json") { + //t.logger.Printf("\nTypescript project found in %s", path) + projects = append(projects, path) + } + + return nil + }) + return projects +} + +// this is being called if getTypescriptProjects return 0 len, then we are searching for files using that: +func (t *TsAdaptor) getTypescriptFiles() []string { + var files []string + ignoreFolders := strings.Split(t.Config.Ignore, ",") + + root := t.Config.Dir + + // ignore error + filepath.Walk(root, func(path string, fi os.FileInfo, err error) error { + if fi.IsDir() { + return nil + } + for i := range ignoreFolders { + if strings.Contains(path, ignoreFolders[i]) { + //t.logger.Println(path + " ignored") + return nil + } + } + + if strings.HasSuffix(path, ".ts") { + //t.logger.Printf("\nTypescript file found in %s", path) + files = append(files, path) + } + + return nil + }) + return files +} + +// +// diff --git a/adaptors/view/_examples/overview/main.go b/adaptors/view/_examples/overview/main.go new file mode 100644 index 00000000..c78e39e4 --- /dev/null +++ b/adaptors/view/_examples/overview/main.go @@ -0,0 +1,93 @@ +package main + +import ( + "encoding/xml" + + "gopkg.in/kataras/iris.v6" + "gopkg.in/kataras/iris.v6/adaptors/gorillamux" + "gopkg.in/kataras/iris.v6/adaptors/view" +) + +// ExampleXML just a test struct to view represents xml content-type +type ExampleXML struct { + XMLName xml.Name `xml:"example"` + One string `xml:"one,attr"` + Two string `xml:"two,attr"` +} + +func main() { + app := iris.New() + app.Adapt(iris.DevLogger()) + app.Adapt(gorillamux.New()) + + app.Get("/data", func(ctx *iris.Context) { + ctx.Data(iris.StatusOK, []byte("Some binary data here.")) + }) + + app.Get("/text", func(ctx *iris.Context) { + ctx.Text(iris.StatusOK, "Plain text here") + }) + + app.Get("/json", func(ctx *iris.Context) { + ctx.JSON(iris.StatusOK, map[string]string{"hello": "json"}) // or myjsonStruct{hello:"json} + }) + + app.Get("/jsonp", func(ctx *iris.Context) { + ctx.JSONP(iris.StatusOK, "callbackName", map[string]string{"hello": "jsonp"}) + }) + + app.Get("/xml", func(ctx *iris.Context) { + ctx.XML(iris.StatusOK, ExampleXML{One: "hello", Two: "xml"}) // or iris.Map{"One":"hello"...} + }) + + app.Get("/markdown", func(ctx *iris.Context) { + ctx.Markdown(iris.StatusOK, "# Hello Dynamic Markdown Iris") + }) + + app.Adapt(view.HTML("./templates", ".html")) + app.Get("/template", func(ctx *iris.Context) { + + ctx.MustRender( + "hi.html", // the file name of the template relative to the './templates' + iris.Map{"Name": "Iris"}, // the .Name inside the ./templates/hi.html + iris.Map{"gzip": false}, // enable gzip for big files + ) + + }) + + // ------ first customization without even the need of *Context or a Handler-------- + // + // Custom new content-/type: + // app.Adapt(iris.RenderPolicy(func(out io.Writer, name string, binding interface{}, options ...map[string]interface{}) (error, bool) { + // if name == "customcontent-type" { + // + // // some very advanced things here: + // out.Write([]byte(binding.(string))) + // return nil, true + // } + // return nil, false + // })) + // + // app.Get("/custom", func(ctx *iris.Context) { + // ctx.RenderWithStatus(iris.StatusOK, // or MustRender + // "customcontent-type", + // "my custom content here!", + // ) + // }) + // + // ---- second ----------------------------------------------------------------------- + // + // Override the defaults (the json,xml,jsonp,text,data and so on), an existing content-type: + // app.Adapt(iris.RenderPolicy(func(out io.Writer, name string, binding interface{}, options ...map[string]interface{}) (error, bool) { + // if name == "text/plain" { + // out.Write([]byte("From the custom text/plain renderer: " + binding.(string))) + // return nil, true + // } + // + // return nil, false + // })) + // // the context.Text's behaviors was changed now by your custom renderer. + // + + app.Listen(":8080") +} diff --git a/adaptors/view/_examples/overview/templates/hi.html b/adaptors/view/_examples/overview/templates/hi.html new file mode 100644 index 00000000..bb7e3e71 --- /dev/null +++ b/adaptors/view/_examples/overview/templates/hi.html @@ -0,0 +1,8 @@ + + +Hi Iris + + +

Hi {{.Name}}

+ + diff --git a/adaptors/view/_examples/template_binary/bindata.go b/adaptors/view/_examples/template_binary/bindata.go new file mode 100644 index 00000000..2d995fb7 --- /dev/null +++ b/adaptors/view/_examples/template_binary/bindata.go @@ -0,0 +1,237 @@ +// Code generated by go-bindata. +// sources: +// templates/hi.html +// DO NOT EDIT! + +package main + +import ( + "bytes" + "compress/gzip" + "fmt" + "io" + "io/ioutil" + "os" + "path/filepath" + "strings" + "time" +) + +func bindataRead(data []byte, name string) ([]byte, error) { + gz, err := gzip.NewReader(bytes.NewBuffer(data)) + if err != nil { + return nil, fmt.Errorf("Read %q: %v", name, err) + } + + var buf bytes.Buffer + _, err = io.Copy(&buf, gz) + clErr := gz.Close() + + if err != nil { + return nil, fmt.Errorf("Read %q: %v", name, err) + } + if clErr != nil { + return nil, err + } + + return buf.Bytes(), nil +} + +type asset struct { + bytes []byte + info os.FileInfo +} + +type bindataFileInfo struct { + name string + size int64 + mode os.FileMode + modTime time.Time +} + +func (fi bindataFileInfo) Name() string { + return fi.name +} +func (fi bindataFileInfo) Size() int64 { + return fi.size +} +func (fi bindataFileInfo) Mode() os.FileMode { + return fi.mode +} +func (fi bindataFileInfo) ModTime() time.Time { + return fi.modTime +} +func (fi bindataFileInfo) IsDir() bool { + return false +} +func (fi bindataFileInfo) Sys() interface{} { + return nil +} + +var _templatesHiHtml = []byte("\x1f\x8b\x08\x00\x00\x09\x6e\x88\x00\xff\xb2\xc9\x28\xc9\xcd\xb1\xe3\xe5\xb2\xc9\x48\x4d\x4c\x01\xd1\x25\x99\x25\x39\xa9\x76\x1e\x99\x0a\x9e\x45\x99\xc5\x0a\xd1\x21\x1e\xae\x0a\x21\x9e\x21\x3e\xae\xb1\x36\xfa\x10\x29\xa0\x1a\x7d\x98\xe2\xa4\xfc\x94\x4a\x20\xcd\x69\x93\x61\x08\xd2\x52\x5d\xad\xe7\x97\x98\x9b\x5a\x5b\x0b\x52\x03\x95\x03\x2a\x86\xd8\x00\x08\x00\x00\xff\xff\xed\x0e\xad\x42\x6a\x00\x00\x00") + +func templatesHiHtmlBytes() ([]byte, error) { + return bindataRead( + _templatesHiHtml, + "templates/hi.html", + ) +} + +func templatesHiHtml() (*asset, error) { + bytes, err := templatesHiHtmlBytes() + if err != nil { + return nil, err + } + + info := bindataFileInfo{name: "templates/hi.html", size: 106, mode: os.FileMode(438), modTime: time.Unix(1468907204, 0)} + a := &asset{bytes: bytes, info: info} + return a, nil +} + +// Asset loads and returns the asset for the given name. +// It returns an error if the asset could not be found or +// could not be loaded. +func Asset(name string) ([]byte, error) { + cannonicalName := strings.Replace(name, "\\", "/", -1) + if f, ok := _bindata[cannonicalName]; ok { + a, err := f() + if err != nil { + return nil, fmt.Errorf("Asset %s can't read by error: %v", name, err) + } + return a.bytes, nil + } + return nil, fmt.Errorf("Asset %s not found", name) +} + +// MustAsset is like Asset but panics when Asset would return an error. +// It simplifies safe initialization of global variables. +func MustAsset(name string) []byte { + a, err := Asset(name) + if err != nil { + panic("asset: Asset(" + name + "): " + err.Error()) + } + + return a +} + +// AssetInfo loads and returns the asset info for the given name. +// It returns an error if the asset could not be found or +// could not be loaded. +func AssetInfo(name string) (os.FileInfo, error) { + cannonicalName := strings.Replace(name, "\\", "/", -1) + if f, ok := _bindata[cannonicalName]; ok { + a, err := f() + if err != nil { + return nil, fmt.Errorf("AssetInfo %s can't read by error: %v", name, err) + } + return a.info, nil + } + return nil, fmt.Errorf("AssetInfo %s not found", name) +} + +// AssetNames returns the names of the assets. +func AssetNames() []string { + names := make([]string, 0, len(_bindata)) + for name := range _bindata { + names = append(names, name) + } + return names +} + +// _bindata is a table, holding each asset generator, mapped to its name. +var _bindata = map[string]func() (*asset, error){ + "templates/hi.html": templatesHiHtml, +} + +// AssetDir returns the file names below a certain +// directory embedded in the file by go-bindata. +// For example if you run go-bindata on data/... and data contains the +// following hierarchy: +// data/ +// foo.txt +// img/ +// a.png +// b.png +// then AssetDir("data") would return []string{"foo.txt", "img"} +// AssetDir("data/img") would return []string{"a.png", "b.png"} +// AssetDir("foo.txt") and AssetDir("notexist") would return an error +// AssetDir("") will return []string{"data"}. +func AssetDir(name string) ([]string, error) { + node := _bintree + if len(name) != 0 { + cannonicalName := strings.Replace(name, "\\", "/", -1) + pathList := strings.Split(cannonicalName, "/") + for _, p := range pathList { + node = node.Children[p] + if node == nil { + return nil, fmt.Errorf("Asset %s not found", name) + } + } + } + if node.Func != nil { + return nil, fmt.Errorf("Asset %s not found", name) + } + rv := make([]string, 0, len(node.Children)) + for childName := range node.Children { + rv = append(rv, childName) + } + return rv, nil +} + +type bintree struct { + Func func() (*asset, error) + Children map[string]*bintree +} + +var _bintree = &bintree{nil, map[string]*bintree{ + "templates": {nil, map[string]*bintree{ + "hi.html": {templatesHiHtml, map[string]*bintree{}}, + }}, +}} + +// RestoreAsset restores an asset under the given directory +func RestoreAsset(dir, name string) error { + data, err := Asset(name) + if err != nil { + return err + } + info, err := AssetInfo(name) + if err != nil { + return err + } + err = os.MkdirAll(_filePath(dir, filepath.Dir(name)), os.FileMode(0755)) + if err != nil { + return err + } + err = ioutil.WriteFile(_filePath(dir, name), data, info.Mode()) + if err != nil { + return err + } + err = os.Chtimes(_filePath(dir, name), info.ModTime(), info.ModTime()) + if err != nil { + return err + } + return nil +} + +// RestoreAssets restores an asset under the given directory recursively +func RestoreAssets(dir, name string) error { + children, err := AssetDir(name) + // File + if err != nil { + return RestoreAsset(dir, name) + } + // Dir + for _, child := range children { + err = RestoreAssets(dir, filepath.Join(name, child)) + if err != nil { + return err + } + } + return nil +} + +func _filePath(dir, name string) string { + cannonicalName := strings.Replace(name, "\\", "/", -1) + return filepath.Join(append([]string{dir}, strings.Split(cannonicalName, "/")...)...) +} diff --git a/adaptors/view/_examples/template_binary/main.go b/adaptors/view/_examples/template_binary/main.go new file mode 100644 index 00000000..05882ac4 --- /dev/null +++ b/adaptors/view/_examples/template_binary/main.go @@ -0,0 +1,24 @@ +package main + +import ( + "gopkg.in/kataras/iris.v6" + "gopkg.in/kataras/iris.v6/adaptors/httprouter" + "gopkg.in/kataras/iris.v6/adaptors/view" +) + +func main() { + app := iris.New() + app.Adapt(iris.DevLogger()) + app.Adapt(httprouter.New()) + + //$ go-bindata ./templates/... + // templates are not used, you can delete the folder and run the example + app.Adapt(view.HTML("./templates", ".html").Binary(Asset, AssetNames)) + + app.Get("/hi", hi) + app.Listen(":8080") +} + +func hi(ctx *iris.Context) { + ctx.MustRender("hi.html", struct{ Name string }{Name: "iris"}) +} diff --git a/adaptors/view/_examples/template_binary/templates/hi.html b/adaptors/view/_examples/template_binary/templates/hi.html new file mode 100644 index 00000000..66ddf25d --- /dev/null +++ b/adaptors/view/_examples/template_binary/templates/hi.html @@ -0,0 +1,8 @@ + + +Hi Iris [THE TITLE] + + +

Hi {{.Name}} + + diff --git a/adaptors/view/_examples/template_html_0/main.go b/adaptors/view/_examples/template_html_0/main.go new file mode 100644 index 00000000..49998e8d --- /dev/null +++ b/adaptors/view/_examples/template_html_0/main.go @@ -0,0 +1,23 @@ +package main + +import ( + "gopkg.in/kataras/iris.v6" + "gopkg.in/kataras/iris.v6/adaptors/httprouter" + "gopkg.in/kataras/iris.v6/adaptors/view" +) + +func main() { + app := iris.New(iris.Configuration{Gzip: false, Charset: "UTF-8"}) // defaults to these + + app.Adapt(iris.DevLogger()) + app.Adapt(httprouter.New()) + + app.Adapt(view.HTML("./templates", ".html")) + + app.Get("/hi", hi) + app.Listen(":8080") +} + +func hi(ctx *iris.Context) { + ctx.MustRender("hi.html", struct{ Name string }{Name: "iris"}) +} diff --git a/adaptors/view/_examples/template_html_0/templates/hi.html b/adaptors/view/_examples/template_html_0/templates/hi.html new file mode 100644 index 00000000..bb7e3e71 --- /dev/null +++ b/adaptors/view/_examples/template_html_0/templates/hi.html @@ -0,0 +1,8 @@ + + +Hi Iris + + +

Hi {{.Name}}

+ + diff --git a/adaptors/view/_examples/template_html_1/main.go b/adaptors/view/_examples/template_html_1/main.go new file mode 100644 index 00000000..f795f3a6 --- /dev/null +++ b/adaptors/view/_examples/template_html_1/main.go @@ -0,0 +1,32 @@ +package main + +import ( + "gopkg.in/kataras/iris.v6" + "gopkg.in/kataras/iris.v6/adaptors/httprouter" + "gopkg.in/kataras/iris.v6/adaptors/view" +) + +type mypage struct { + Title string + Message string +} + +func main() { + app := iris.New() + app.Adapt(iris.DevLogger()) + app.Adapt(httprouter.New()) + + tmpl := view.HTML("./templates", ".html") + tmpl.Layout("layout.html") + + app.Adapt(tmpl) + + app.Get("/", func(ctx *iris.Context) { + ctx.Render("mypage.html", mypage{"My Page title", "Hello world!"}, iris.Map{"gzip": true}) + // Note that: you can pass "layout" : "otherLayout.html" to bypass the config's Layout property + // or iris.NoLayout to disable layout on this render action. + // third is an optional parameter + }) + + app.Listen(":8080") +} diff --git a/adaptors/view/_examples/template_html_1/templates/layout.html b/adaptors/view/_examples/template_html_1/templates/layout.html new file mode 100644 index 00000000..41fa2330 --- /dev/null +++ b/adaptors/view/_examples/template_html_1/templates/layout.html @@ -0,0 +1,11 @@ + + +My Layout + + + +

Body is:

+ + {{ yield }} + + diff --git a/adaptors/view/_examples/template_html_1/templates/mypage.html b/adaptors/view/_examples/template_html_1/templates/mypage.html new file mode 100644 index 00000000..9bb0af63 --- /dev/null +++ b/adaptors/view/_examples/template_html_1/templates/mypage.html @@ -0,0 +1,4 @@ +

+ Title: {{.Title}} +

+

Message: {{.Message}}

\ No newline at end of file diff --git a/adaptors/view/_examples/template_html_2/README.md b/adaptors/view/_examples/template_html_2/README.md new file mode 100644 index 00000000..da4c76a1 --- /dev/null +++ b/adaptors/view/_examples/template_html_2/README.md @@ -0,0 +1,3 @@ +## Info + +This folder examines the {{render "dir/templatefilename"}} functionality to manually render any template inside any template diff --git a/adaptors/view/_examples/template_html_2/main.go b/adaptors/view/_examples/template_html_2/main.go new file mode 100644 index 00000000..07bb99e1 --- /dev/null +++ b/adaptors/view/_examples/template_html_2/main.go @@ -0,0 +1,49 @@ +package main + +import ( + "gopkg.in/kataras/iris.v6" + "gopkg.in/kataras/iris.v6/adaptors/httprouter" + "gopkg.in/kataras/iris.v6/adaptors/view" +) + +func main() { + app := iris.New() + app.Adapt(iris.DevLogger()) + app.Adapt(httprouter.New()) + + tmpl := view.HTML("./templates", ".html") + tmpl.Layout("layouts/layout.html") + tmpl.Funcs(map[string]interface{}{ + "greet": func(s string) string { + return "Greetings " + s + "!" + }, + }) + + app.Adapt(tmpl) + + app.Get("/", func(ctx *iris.Context) { + if err := ctx.Render("page1.html", nil); err != nil { + println(err.Error()) + } + }) + + // remove the layout for a specific route + app.Get("/nolayout", func(ctx *iris.Context) { + if err := ctx.Render("page1.html", nil, iris.RenderOptions{"layout": iris.NoLayout}); err != nil { + println(err.Error()) + } + }) + + // set a layout for a party, .Layout should be BEFORE any Get or other Handle party's method + my := app.Party("/my").Layout("layouts/mylayout.html") + { + my.Get("/", func(ctx *iris.Context) { + ctx.MustRender("page1.html", nil) + }) + my.Get("/other", func(ctx *iris.Context) { + ctx.MustRender("page1.html", nil) + }) + } + + app.Listen(":8080") +} diff --git a/adaptors/view/_examples/template_html_2/templates/layouts/layout.html b/adaptors/view/_examples/template_html_2/templates/layouts/layout.html new file mode 100644 index 00000000..69b545ec --- /dev/null +++ b/adaptors/view/_examples/template_html_2/templates/layouts/layout.html @@ -0,0 +1,12 @@ + + +Layout + + + +

This is the global layout

+
+ + {{ yield }} + + diff --git a/adaptors/view/_examples/template_html_2/templates/layouts/mylayout.html b/adaptors/view/_examples/template_html_2/templates/layouts/mylayout.html new file mode 100644 index 00000000..d22426fe --- /dev/null +++ b/adaptors/view/_examples/template_html_2/templates/layouts/mylayout.html @@ -0,0 +1,12 @@ + + +my Layout + + + +

This is the layout for the /my/ and /my/other routes only

+
+ + {{ yield }} + + diff --git a/adaptors/view/_examples/template_html_2/templates/page1.html b/adaptors/view/_examples/template_html_2/templates/page1.html new file mode 100644 index 00000000..6f63f7b3 --- /dev/null +++ b/adaptors/view/_examples/template_html_2/templates/page1.html @@ -0,0 +1,7 @@ +
+ +

Page 1 {{ greet "iris developer"}}

+ + {{ render "partials/page1_partial1.html"}} + +
diff --git a/adaptors/view/_examples/template_html_2/templates/partials/page1_partial1.html b/adaptors/view/_examples/template_html_2/templates/partials/page1_partial1.html new file mode 100644 index 00000000..66ba9266 --- /dev/null +++ b/adaptors/view/_examples/template_html_2/templates/partials/page1_partial1.html @@ -0,0 +1,3 @@ +
+

Page 1's Partial 1

+
diff --git a/adaptors/view/_examples/template_html_3/main.go b/adaptors/view/_examples/template_html_3/main.go new file mode 100644 index 00000000..b498e32c --- /dev/null +++ b/adaptors/view/_examples/template_html_3/main.go @@ -0,0 +1,53 @@ +// Package main an example on how to naming your routes & use the custom 'url' HTML Template Engine, same for other template engines. +package main + +import ( + "gopkg.in/kataras/iris.v6" + "gopkg.in/kataras/iris.v6/adaptors/gorillamux" + "gopkg.in/kataras/iris.v6/adaptors/view" +) + +func main() { + app := iris.New() + app.Adapt(iris.DevLogger()) + app.Adapt(gorillamux.New()) + + app.Adapt(view.HTML("./templates", ".html")) + + app.Get("/mypath", emptyHandler).ChangeName("my-page1") + app.Get("/mypath2/{param1}/{param2}", emptyHandler).ChangeName("my-page2") + app.Get("/mypath3/{param1}/statichere/{param2}", emptyHandler).ChangeName("my-page3") + app.Get("/mypath4/{param1}/statichere/{param2}/{otherparam}/{something:.*}", emptyHandler).ChangeName("my-page4") + + // same with Handle/Func + app.HandleFunc("GET", "/mypath5/{param1}/statichere/{param2}/{otherparam}/anything/{something:.*}", emptyHandler).ChangeName("my-page5") + + app.Get("/mypath6/{param1}/{param2}/staticParam/{param3AfterStatic}", emptyHandler).ChangeName("my-page6") + + app.Get("/", func(ctx *iris.Context) { + // for /mypath6... + paramsAsArray := []string{"param1", "theParam1", + "param2", "theParam2", + "param3AfterStatic", "theParam3"} + + if err := ctx.Render("page.html", iris.Map{"ParamsAsArray": paramsAsArray}); err != nil { + panic(err) + } + }) + + app.Get("/redirect/{namedRoute}", func(ctx *iris.Context) { + routeName := ctx.Param("namedRoute") + + println("The full uri of " + routeName + "is: " + app.URL(routeName)) + // if routeName == "my-page1" + // prints: The full uri of my-page1 is: http://127.0.0.1:8080/mypath + ctx.RedirectTo(routeName) + // http://127.0.0.1:8080/redirect/my-page1 will redirect to -> http://127.0.0.1:8080/mypath + }) + + app.Listen("localhost:8080") +} + +func emptyHandler(ctx *iris.Context) { + ctx.Writef("Hello from %s.", ctx.Path()) +} diff --git a/adaptors/view/_examples/template_html_3/templates/page.html b/adaptors/view/_examples/template_html_3/templates/page.html new file mode 100644 index 00000000..798f34c8 --- /dev/null +++ b/adaptors/view/_examples/template_html_3/templates/page.html @@ -0,0 +1,27 @@ +http://127.0.0.1:8080/mypath +
+
+ +http://localhost:8080/mypath2/:param1/:param2 + +
+
+ + + http://localhost:8080/mypath3/:param1/statichere/:param2 + +
+
+ +http://localhost/mypath4/:param1/statichere/:param2/:otherparam/*something + +
+
+ + + http://localhost:8080/mypath5/:param1/statichere/:param2/:otherparam/anything/*anything + +
+
+ +http://localhost:8080/mypath6/{param1}/{param2}/staticParam/{param3AfterStatic} diff --git a/adaptors/view/_examples/template_html_4/hosts b/adaptors/view/_examples/template_html_4/hosts new file mode 100644 index 00000000..ecc92ad3 --- /dev/null +++ b/adaptors/view/_examples/template_html_4/hosts @@ -0,0 +1,32 @@ +# Copyright (c) 1993-2009 Microsoft Corp. +# +# This is a sample HOSTS file used by Microsoft TCP/IP for Windows. +# +# This file contains the mappings of IP addresses to host names. Each +# entry should be kept on an individual line. The IP address should +# be placed in the first column followed by the corresponding host name. +# The IP address and the host name should be separated by at least one +# space. +# +# Additionally, comments (such as these) may be inserted on individual +# lines or following the machine name denoted by a '#' symbol. +# +# For example: +# +# 102.54.94.97 rhino.acme.com # source server +# 38.25.63.10 x.acme.com # x client host + +# localhost name resolution is handled within DNS itself. +127.0.0.1 localhost +::1 localhost +#-IRIS-For development machine, you have to configure your dns also for online, search google how to do it if you don't know + +127.0.0.1 username1.127.0.0.1 +127.0.0.1 username2.127.0.0.1 +127.0.0.1 username3.127.0.0.1 +127.0.0.1 username4.127.0.0.1 +127.0.0.1 username5.127.0.0.1 +# note that you can always use custom subdomains +#-END IRIS- + +# Windows: Drive:/Windows/system32/drivers/etc/hosts, on Linux: /etc/hosts \ No newline at end of file diff --git a/adaptors/view/_examples/template_html_4/main.go b/adaptors/view/_examples/template_html_4/main.go new file mode 100644 index 00000000..b498e32c --- /dev/null +++ b/adaptors/view/_examples/template_html_4/main.go @@ -0,0 +1,53 @@ +// Package main an example on how to naming your routes & use the custom 'url' HTML Template Engine, same for other template engines. +package main + +import ( + "gopkg.in/kataras/iris.v6" + "gopkg.in/kataras/iris.v6/adaptors/gorillamux" + "gopkg.in/kataras/iris.v6/adaptors/view" +) + +func main() { + app := iris.New() + app.Adapt(iris.DevLogger()) + app.Adapt(gorillamux.New()) + + app.Adapt(view.HTML("./templates", ".html")) + + app.Get("/mypath", emptyHandler).ChangeName("my-page1") + app.Get("/mypath2/{param1}/{param2}", emptyHandler).ChangeName("my-page2") + app.Get("/mypath3/{param1}/statichere/{param2}", emptyHandler).ChangeName("my-page3") + app.Get("/mypath4/{param1}/statichere/{param2}/{otherparam}/{something:.*}", emptyHandler).ChangeName("my-page4") + + // same with Handle/Func + app.HandleFunc("GET", "/mypath5/{param1}/statichere/{param2}/{otherparam}/anything/{something:.*}", emptyHandler).ChangeName("my-page5") + + app.Get("/mypath6/{param1}/{param2}/staticParam/{param3AfterStatic}", emptyHandler).ChangeName("my-page6") + + app.Get("/", func(ctx *iris.Context) { + // for /mypath6... + paramsAsArray := []string{"param1", "theParam1", + "param2", "theParam2", + "param3AfterStatic", "theParam3"} + + if err := ctx.Render("page.html", iris.Map{"ParamsAsArray": paramsAsArray}); err != nil { + panic(err) + } + }) + + app.Get("/redirect/{namedRoute}", func(ctx *iris.Context) { + routeName := ctx.Param("namedRoute") + + println("The full uri of " + routeName + "is: " + app.URL(routeName)) + // if routeName == "my-page1" + // prints: The full uri of my-page1 is: http://127.0.0.1:8080/mypath + ctx.RedirectTo(routeName) + // http://127.0.0.1:8080/redirect/my-page1 will redirect to -> http://127.0.0.1:8080/mypath + }) + + app.Listen("localhost:8080") +} + +func emptyHandler(ctx *iris.Context) { + ctx.Writef("Hello from %s.", ctx.Path()) +} diff --git a/adaptors/view/_examples/template_html_4/templates/page.html b/adaptors/view/_examples/template_html_4/templates/page.html new file mode 100644 index 00000000..fab4d355 --- /dev/null +++ b/adaptors/view/_examples/template_html_4/templates/page.html @@ -0,0 +1,16 @@ + + +username1.127.0.0.1:8080/mypath +
+
+username2.127.0.0.1:8080/mypath2/:param1/:param2 +
+
+username3.127.0.0.1:8080/mypath3/:param1/statichere/:param2 +
+
+username4.127.0.0.1:8080/mypath4/:param1/statichere/:param2/:otherparam/*something +
+
+username5.127.0.0.1:8080/mypath6/:param1/:param2/staticParam/:param3AfterStatic diff --git a/adaptors/view/adaptor.go b/adaptors/view/adaptor.go new file mode 100644 index 00000000..6c11a0e4 --- /dev/null +++ b/adaptors/view/adaptor.go @@ -0,0 +1,136 @@ +package view + +import ( + "io" + "strings" + + "github.com/kataras/go-template" + "gopkg.in/kataras/iris.v6" +) + +// Adaptor contains the common actions +// that all template engines share. +// +// We need to export that as it is without an interface +// because it may be used as a wrapper for a template engine +// that is not exists yet but community can create. +type Adaptor struct { + dir string + extension string + // for a .go template file lives inside the executable + assetFn func(name string) ([]byte, error) + namesFn func() []string + + reload bool + + engine template.Engine // used only on Adapt, we could make + //it as adaptEngine and pass a second parameter there but this would break the pattern. +} + +// NewAdaptor returns a new general template engine policy adaptor. +func NewAdaptor(directory string, extension string, e template.Engine) *Adaptor { + return &Adaptor{ + dir: directory, + extension: extension, + engine: e, + } +} + +// Binary optionally, use it when template files are distributed +// inside the app executable (.go generated files). +func (h *Adaptor) Binary(assetFn func(name string) ([]byte, error), namesFn func() []string) *Adaptor { + h.assetFn = assetFn + h.namesFn = namesFn + return h +} + +// Reload if setted to true the templates are reloading on each call, +// use it when you're in development and you're boring of restarting +// the whole app when you edit a template file +func (h *Adaptor) Reload(developmentMode bool) *Adaptor { + h.reload = developmentMode + return h +} + +// Adapt adapts a template engine to the main Iris' policies. +// this specific Adapt is a multi-policies adaptors +// we use that instead of just return New() iris.RenderPolicy +// for two reasons: +// - the user may need to edit the adaptor's fields +// like Directory, Binary +// - we need to adapt an event policy to add the engine to the external mux +// and load it. +func (h *Adaptor) Adapt(frame *iris.Policies) { + mux := template.DefaultMux + // on the build state in order to have the shared funcs also + evt := iris.EventPolicy{ + Build: func(s *iris.Framework) { + // mux has default to ./templates and .html ext + // no need for any checks here. + // the RenderPolicy will give a "no templates found on 'directory'" + // if user tries to use the context.Render without having the template files + // no need to panic here because we will use the html as the default template engine + // even if the user doesn't asks for + // or no? we had the defaults before... maybe better to give the user + // the oportunity to learn about the template's configuration + // (now 6.1.4 ) they are defaulted and users may don't know how and if they can change the configuration + // even if the book and examples covers these things, many times they're asking me on chat............. + // so no defaults? ok no defaults. This file then will be saved to /adaptors as with other template engines. + // simple. + mux.AddEngine(h.engine). + Directory(h.dir, h.extension). + Binary(h.assetFn, h.namesFn) + + mux.Reload = h.reload + + // notes for me: per-template engine funcs are setted by each template engine adaptor itself, + // here we will set the template funcs policy'. + // as I explain on the TemplateFuncsPolicy it exists in order to allow community to create plugins + // even by adding custom template funcs to their behaviors. + + // We know that iris.TemplateFuncsPolicy is useless without this specific + // adaptor. We also know that it is not a good idea to have two + // policies with the same function or we can? wait. No we can't. + // We can't because: + // - the RenderPolicy should accept any type of render process, not only tempaltes. + // - I didn't design iris/policy.go to keep logic about implementation, this would make that very limited + // and I don't want to break that just for the templates. + // - We want community to be able to create packages which can adapt template functions but + // not to worry about the rest of the template engine adaptor policy. + // And even don't worry if the user has registered or use a template engine at all. + // So we should keep separate the TemplateFuncsPolicy(just map[string]interface{}) + // from the rest of the implementation. + // + // So when the community wants to create a template adaptor has two options: + // - Use the RenderPolicy which is just a func + // - Use the kataras/iris/adaptors/view.Adaptor adaptor wrapper for RenderPolicy with a compination of kataras/go-template/.Engine + // + // + // So here is the only place we adapt the iris.TemplateFuncsPolicy to the tempaltes, if and only if templates are used, + // otherwise they are just ignored without any creepy things. + // + // The TemplateFuncsPolicy will work in compination with the specific template adaptor's functions(see html.go and the rest) + + if len(frame.TemplateFuncsPolicy) > 0 { + mux.SetFuncMapToEngine(frame.TemplateFuncsPolicy, h.engine) + } + + if err := mux.Load(); err != nil { + s.Log(iris.ProdMode, err.Error()) + } + }, + } + // adapt the build event to the main policies + evt.Adapt(frame) + + r := iris.RenderPolicy(func(out io.Writer, file string, tmplContext interface{}, options ...map[string]interface{}) (error, bool) { + // template mux covers that but maybe we have more than one RenderPolicy + // and each of them carries a different mux on the new design. + if strings.Contains(file, h.extension) { + return mux.ExecuteWriter(out, file, tmplContext, options...), true + } + return nil, false + }) + + r.Adapt(frame) +} diff --git a/adaptors/view/amber.go b/adaptors/view/amber.go new file mode 100644 index 00000000..f7d03010 --- /dev/null +++ b/adaptors/view/amber.go @@ -0,0 +1,48 @@ +package view + +import ( + "github.com/kataras/go-template/amber" +) + +// AmberAdaptor is the adaptor for the Amber, simple, engine. +// Read more about the Amber Go Template at: +// https://github.com/eknkc/amber +// and https://github.com/kataras/go-template/tree/master/amber +type AmberAdaptor struct { + *Adaptor + engine *amber.Engine +} + +// Amber returns a new kataras/go-template/amber template engine +// with the same features as all iris' view engines have: +// Binary assets load (templates inside your executable with .go extension) +// Layout, Funcs, {{ url }} {{ urlpath}} for reverse routing and much more. +// +// Read more: https://github.com/eknkc/amber +func Amber(directory string, extension string) *AmberAdaptor { + e := amber.New() + return &AmberAdaptor{ + Adaptor: NewAdaptor(directory, extension, e), + engine: e, + } +} + +// Funcs adds the elements of the argument map to the template's function map. +// It is legal to overwrite elements of the default actions: +// - url func(routeName string, args ...string) string +// - urlpath func(routeName string, args ...string) string +// - render func(fullPartialName string) (template.HTML, error). +func (a *AmberAdaptor) Funcs(funcMap map[string]interface{}) *AmberAdaptor { + if len(funcMap) == 0 { + return a + } + + // configuration maps are never nil, because + // they are initialized at each of the engine's New func + // so we're just passing them inside it. + for k, v := range funcMap { + a.engine.Config.Funcs[k] = v + } + + return a +} diff --git a/adaptors/view/django.go b/adaptors/view/django.go new file mode 100644 index 00000000..fffe82cf --- /dev/null +++ b/adaptors/view/django.go @@ -0,0 +1,93 @@ +package view + +import ( + "github.com/kataras/go-template/django" +) + +type ( + // Value conversion for django.Value + Value django.Value + // Error conversion for django.Error + Error django.Error + // FilterFunction conversion for django.FilterFunction + FilterFunction func(in *Value, param *Value) (out *Value, err *Error) +) + +// this exists because of moving the pongo2 to the vendors without conflictitions if users +// wants to register pongo2 filters they can use this django.FilterFunc to do so. +func convertFilters(djangoFilters map[string]FilterFunction) map[string]django.FilterFunction { + filters := make(map[string]django.FilterFunction, len(djangoFilters)) + for k, v := range djangoFilters { + func(filterName string, filterFunc FilterFunction) { + fn := django.FilterFunction(func(in *django.Value, param *django.Value) (*django.Value, *django.Error) { + theOut, theErr := filterFunc((*Value)(in), (*Value)(param)) + return (*django.Value)(theOut), (*django.Error)(theErr) + }) + filters[filterName] = fn + }(k, v) + } + return filters +} + +// DjangoAdaptor is the adaptor for the Django engine. +// Read more about the Django Go Template at: +// https://github.com/flosch/pongo2 +// and https://github.com/kataras/go-template/tree/master/django +type DjangoAdaptor struct { + *Adaptor + engine *django.Engine + filters map[string]FilterFunction +} + +// Django returns a new kataras/go-template/django template engine +// with the same features as all iris' view engines have: +// Binary assets load (templates inside your executable with .go extension) +// Layout, Funcs, {{ url }} {{ urlpath}} for reverse routing and much more. +// +// Read more: https://github.com/flosch/pongo2 +func Django(directory string, extension string) *DjangoAdaptor { + e := django.New() + return &DjangoAdaptor{ + Adaptor: NewAdaptor(directory, extension, e), + engine: e, + filters: make(map[string]FilterFunction, 0), + } +} + +// Filters for pongo2, map[name of the filter] the filter function . +// +// Note, these Filters function overrides ALL the previous filters +// It SETS a new filter map based on the given 'filtersMap' parameter. +func (d *DjangoAdaptor) Filters(filtersMap map[string]FilterFunction) *DjangoAdaptor { + + if len(filtersMap) == 0 { + return d + } + // configuration maps are never nil, because + // they are initialized at each of the engine's New func + // so we're just passing them inside it. + + filters := convertFilters(filtersMap) + d.engine.Config.Filters = filters + return d +} + +// Globals share context fields between templates. https://github.com/flosch/pongo2/issues/35 +func (d *DjangoAdaptor) Globals(globalsMap map[string]interface{}) *DjangoAdaptor { + if len(globalsMap) == 0 { + return d + } + + for k, v := range globalsMap { + d.engine.Config.Globals[k] = v + } + + return d +} + +// DebugTemplates enables template debugging. +// The verbose error messages will appear in browser instead of quiet passes with error code. +func (d *DjangoAdaptor) DebugTemplates(debug bool) *DjangoAdaptor { + d.engine.Config.DebugTemplates = debug + return d +} diff --git a/adaptors/view/handlebars.go b/adaptors/view/handlebars.go new file mode 100644 index 00000000..e24f9d49 --- /dev/null +++ b/adaptors/view/handlebars.go @@ -0,0 +1,62 @@ +package view + +import ( + "github.com/kataras/go-template/handlebars" +) + +// HandlebarsAdaptor is the adaptor for the Handlebars engine. +// Read more about the Handlebars Go Template at: +// https://github.com/aymerick/raymond +// and https://github.com/kataras/go-template/tree/master/handlebars +type HandlebarsAdaptor struct { + *Adaptor + engine *handlebars.Engine +} + +// Handlebars returns a new kataras/go-template/handlebars template engine +// with the same features as all iris' view engines have: +// Binary assets load (templates inside your executable with .go extension) +// Layout, Funcs, {{ url }} {{ urlpath}} for reverse routing and much more. +// +// Read more: https://github.com/aymerick/raymond +func Handlebars(directory string, extension string) *HandlebarsAdaptor { + e := handlebars.New() + return &HandlebarsAdaptor{ + Adaptor: NewAdaptor(directory, extension, e), + engine: e, + } +} + +// Layout sets the layout template file which inside should use +// the {{ yield }} func to yield the main template file +// and optionally {{partial/partial_r/render}} to render other template files like headers and footers +// +// The 'tmplLayoutFile' is a relative path of the templates base directory, +// for the template file whith its extension. +// +// Example: Handlebars("./templates", ".html").Layout("layouts/mainLayout.html") +// // mainLayout.html is inside: "./templates/layouts/". +// +// Note: Layout can be changed for a specific call +// action with the option: "layout" on the Iris' context.Render function. +func (h *HandlebarsAdaptor) Layout(tmplLayoutFile string) *HandlebarsAdaptor { + h.engine.Config.Layout = tmplLayoutFile + return h +} + +// Funcs adds the elements of the argument map to the template's function map. +// It is legal to overwrite elements of the default actions: +// - url func(routeName string, args ...string) string +// - urlpath func(routeName string, args ...string) string +// - and handlebars specific, read more: https://github.com/aymerick/raymond. +func (h *HandlebarsAdaptor) Funcs(funcMap map[string]interface{}) *HandlebarsAdaptor { + if funcMap == nil { + return h + } + + for k, v := range funcMap { + h.engine.Config.Helpers[k] = v + } + + return h +} diff --git a/adaptors/view/html.go b/adaptors/view/html.go new file mode 100644 index 00000000..b0a34baf --- /dev/null +++ b/adaptors/view/html.go @@ -0,0 +1,92 @@ +package view + +import ( + "github.com/kataras/go-template/html" +) + +// HTMLAdaptor is the html engine policy adaptor +// used like the "html/template" standard go package +// but with a lot of extra features by. +// +// This is just a wrapper of kataras/go-template/html. +type HTMLAdaptor struct { + *Adaptor + engine *html.Engine +} + +// HTML creates and returns a new kataras/go-template/html engine. +// The html engine used like the "html/template" standard go package +// but with a lot of extra features. +func HTML(directory string, extension string) *HTMLAdaptor { + engine := html.New() + return &HTMLAdaptor{ + Adaptor: NewAdaptor(directory, extension, engine), + // create the underline engine with the default configuration, + // which can be changed by this type. + //The whole funcs should called only before Iris' build & listen state. + engine: engine, // we need that for the configuration only. + } + +} + +// Delims sets the action delimiters to the specified strings, to be used in +// subsequent calls to Parse, ParseFiles, or ParseGlob. Nested template +// definitions will inherit the settings. An empty delimiter stands for the +// corresponding default: {{ or }}. +func (h *HTMLAdaptor) Delims(left string, right string) *HTMLAdaptor { + if left != "" && right != "" { + h.engine.Config.Left = left + h.engine.Config.Right = right + } + return h +} + +// Layout sets the layout template file which inside should use +// the {{ yield }} func to yield the main template file +// and optionally {{partial/partial_r/render}} to render other template files like headers and footers +// +// The 'tmplLayoutFile' is a relative path of the templates base directory, +// for the template file whith its extension. +// +// Example: HTML("./templates", ".html").Layout("layouts/mainLayout.html") +// // mainLayout.html is inside: "./templates/layouts/". +// +// Note: Layout can be changed for a specific call +// action with the option: "layout" on the Iris' context.Render function. +func (h *HTMLAdaptor) Layout(tmplLayoutFile string) *HTMLAdaptor { + h.engine.Config.Layout = tmplLayoutFile + return h +} + +// LayoutFuncs adds the elements of the argument map to the template's layout-only function map. +// It is legal to overwrite elements of the default layout actions: +// - yield func() (template.HTML, error) +// - current func() (string, error) +// - partial func(partialName string) (template.HTML, error) +// - partial_r func(partialName string) (template.HTML, error) +// - render func(fullPartialName string) (template.HTML, error). +func (h *HTMLAdaptor) LayoutFuncs(funcMap map[string]interface{}) *HTMLAdaptor { + if funcMap != nil { + h.engine.Config.LayoutFuncs = funcMap + } + return h +} + +// Funcs adds the elements of the argument map to the template's function map. +// It is legal to overwrite elements of the default actions: +// - url func(routeName string, args ...string) string +// - urlpath func(routeName string, args ...string) string +// - render func(fullPartialName string) (template.HTML, error). +func (h *HTMLAdaptor) Funcs(funcMap map[string]interface{}) *HTMLAdaptor { + if len(funcMap) == 0 { + return h + } + // configuration maps are never nil, because + // they are initialized at each of the engine's New func + // so we're just passing them inside it. + for k, v := range funcMap { + h.engine.Config.Funcs[k] = v + } + + return h +} diff --git a/adaptors/view/pug.go b/adaptors/view/pug.go new file mode 100644 index 00000000..3667c499 --- /dev/null +++ b/adaptors/view/pug.go @@ -0,0 +1,21 @@ +package view + +import ( + "github.com/Joker/jade" +) + +// Pug (or Jade) returns a new kataras/go-template/pug engine. +// It shares the same exactly logic with the +// HTMLAdaptor, it uses the same exactly configuration. +// It has got some features and a lot of functions +// which will make your life easier. +// Read more about the Jade Go Template: https://github.com/Joker/jade +func Pug(directory string, extension string) *HTMLAdaptor { + h := HTML(directory, extension) + // because I has designed the kataras/go-template + // so powerful, we can just pass a parser middleware + // into the standard html template engine + // and we're ready to go. + h.engine.Middleware = jade.Parse + return h +} diff --git a/addr.go b/addr.go new file mode 100644 index 00000000..3b0bde7a --- /dev/null +++ b/addr.go @@ -0,0 +1,299 @@ +package iris + +import ( + "crypto/tls" + "net" + "net/http" + "os" + "strconv" + "strings" + "time" + + "github.com/kataras/go-errors" + "golang.org/x/crypto/acme/autocert" +) + +var ( + errPortAlreadyUsed = errors.New("Port is already used") + errRemoveUnix = errors.New("Unexpected error when trying to remove unix socket file. Addr: %s | Trace: %s") + errChmod = errors.New("Cannot chmod %#o for %q: %s") + errCertKeyMissing = errors.New("You should provide certFile and keyFile for TLS/SSL") + errParseTLS = errors.New("Couldn't load TLS, certFile=%q, keyFile=%q. Trace: %s") +) + +// TCP4 returns a new tcp4 Listener +func TCP4(addr string) (net.Listener, error) { + return net.Listen("tcp4", ParseHost(addr)) +} + +// TCPKeepAlive returns a new tcp4 keep alive Listener +func TCPKeepAlive(addr string) (net.Listener, error) { + ln, err := TCP4(addr) + if err != nil { + return nil, err + } + return TCPKeepAliveListener{ln.(*net.TCPListener)}, err +} + +// UNIX returns a new unix(file) Listener +func UNIX(addr string, mode os.FileMode) (net.Listener, error) { + if errOs := os.Remove(addr); errOs != nil && !os.IsNotExist(errOs) { + return nil, errRemoveUnix.Format(addr, errOs.Error()) + } + + listener, err := net.Listen("unix", addr) + if err != nil { + return nil, errPortAlreadyUsed.AppendErr(err) + } + + if err = os.Chmod(addr, mode); err != nil { + return nil, errChmod.Format(mode, addr, err.Error()) + } + + return listener, nil +} + +// TLS returns a new TLS Listener +func TLS(addr, certFile, keyFile string) (net.Listener, error) { + + if certFile == "" || keyFile == "" { + return nil, errCertKeyMissing + } + + cert, err := tls.LoadX509KeyPair(certFile, keyFile) + if err != nil { + return nil, errParseTLS.Format(certFile, keyFile, err) + } + + return CERT(addr, cert) +} + +// CERT returns a listener which contans tls.Config with the provided certificate, use for ssl +func CERT(addr string, cert tls.Certificate) (net.Listener, error) { + ln, err := TCP4(addr) + if err != nil { + return nil, err + } + + tlsConfig := &tls.Config{ + Certificates: []tls.Certificate{cert}, + PreferServerCipherSuites: true, + } + return tls.NewListener(ln, tlsConfig), nil +} + +// LETSENCRYPT returns a new Automatic TLS Listener using letsencrypt.org service +// receives two parameters, the first is the domain of the server +// and the second is optionally, the cache directory, if you skip it then the cache directory is "./certcache" +// if you want to disable cache directory then simple give it a value of empty string "" +// +// does NOT supports localhost domains for testing. +// +// this is the recommended function to use when you're ready for production state +func LETSENCRYPT(addr string, cacheDirOptional ...string) (net.Listener, error) { + if portIdx := strings.IndexByte(addr, ':'); portIdx == -1 { + addr += ":443" + } + + ln, err := TCP4(addr) + if err != nil { + return nil, err + } + + cacheDir := "./certcache" + if len(cacheDirOptional) > 0 { + cacheDir = cacheDirOptional[0] + } + + m := autocert.Manager{ + Prompt: autocert.AcceptTOS, + } // HostPolicy is missing, if user wants it, then she/he should manually + // configure the autocertmanager and use the `iris.Default.Serve` to pass that listener + + if cacheDir == "" { + // then the user passed empty by own will, then I guess she/he doesnt' want any cache directory + } else { + m.Cache = autocert.DirCache(cacheDir) + } + + tlsConfig := &tls.Config{GetCertificate: m.GetCertificate} + tlsLn := tls.NewListener(ln, tlsConfig) + + return tlsLn, nil +} + +// TCPKeepAliveListener sets TCP keep-alive timeouts on accepted +// connections. +// Dead TCP connections (e.g. closing laptop mid-download) eventually +// go away +// It is not used by default if you want to pass a keep alive listener +// then just pass the child listener, example: +// listener := iris.TCPKeepAliveListener{iris.TCP4(":8080").(*net.TCPListener)} +type TCPKeepAliveListener struct { + *net.TCPListener +} + +// Accept implements the listener and sets the keep alive period which is 3minutes +func (ln TCPKeepAliveListener) Accept() (c net.Conn, err error) { + tc, err := ln.AcceptTCP() + if err != nil { + return + } + err = tc.SetKeepAlive(true) + if err != nil { + return + } + err = tc.SetKeepAlivePeriod(3 * time.Minute) + if err != nil { + return + } + return tc, nil +} + +///TODO: +// func (ln TCPKeepAliveListener) Close() error { +// return nil +// } + +// ParseHost tries to convert a given string to an address which is compatible with net.Listener and server +func ParseHost(addr string) string { + // check if addr has :port, if not do it +:80 ,we need the hostname for many cases + a := addr + if a == "" { + // check for os environments + if oshost := os.Getenv("ADDR"); oshost != "" { + a = oshost + } else if oshost := os.Getenv("HOST"); oshost != "" { + a = oshost + } else if oshost := os.Getenv("HOSTNAME"); oshost != "" { + a = oshost + // check for port also here + if osport := os.Getenv("PORT"); osport != "" { + a += ":" + osport + } + } else if osport := os.Getenv("PORT"); osport != "" { + a = ":" + osport + } else { + a = ":http" + } + } + if portIdx := strings.IndexByte(a, ':'); portIdx == 0 { + if a[portIdx:] == ":https" { + a = DefaultServerHostname + ":443" + } else { + // if contains only :port ,then the : is the first letter, so we dont have setted a hostname, lets set it + a = DefaultServerHostname + a + } + } + + /* changed my mind, don't add 80, this will cause problems on unix listeners, and it's not really necessary because we take the port using parsePort + if portIdx := strings.IndexByte(a, ':'); portIdx < 0 { + // missing port part, add it + a = a + ":80" + }*/ + + return a +} + +// ParseHostname receives an addr of form host[:port] and returns the hostname part of it +// ex: localhost:8080 will return the `localhost`, mydomain.com:8080 will return the 'mydomain' +func ParseHostname(addr string) string { + idx := strings.IndexByte(addr, ':') + if idx == 0 { + // only port, then return 0.0.0.0 + return "0.0.0.0" + } else if idx > 0 { + return addr[0:idx] + } + // it's already hostname + return addr +} + +// ParsePort receives an addr of form host[:port] and returns the port part of it +// ex: localhost:8080 will return the `8080`, mydomain.com will return the '80' +func ParsePort(addr string) int { + if portIdx := strings.IndexByte(addr, ':'); portIdx != -1 { + afP := addr[portIdx+1:] + p, err := strconv.Atoi(afP) + if err == nil { + return p + } else if afP == "https" { // it's not number, check if it's :https + return 443 + } + } + return 80 +} + +const ( + // SchemeHTTPS returns "https://" (full) + SchemeHTTPS = "https://" + // SchemeHTTP returns "http://" (full) + SchemeHTTP = "http://" +) + +// ParseScheme returns the scheme based on the host,addr,domain +// Note: the full scheme not just http*,https* *http:// *https:// +func ParseScheme(domain string) string { + // pure check + if strings.HasPrefix(domain, SchemeHTTPS) || ParsePort(domain) == 443 { + return SchemeHTTPS + } + return SchemeHTTP +} + +// ProxyHandler returns a new net/http.Handler which works as 'proxy', maybe doesn't suits you look its code before using that in production +var ProxyHandler = func(redirectSchemeAndHost string) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + + // override the handler and redirect all requests to this addr + redirectTo := redirectSchemeAndHost + fakehost := r.URL.Host + path := r.URL.EscapedPath() + if strings.Count(fakehost, ".") >= 3 { // propably a subdomain, pure check but doesn't matters don't worry + if sufIdx := strings.LastIndexByte(fakehost, '.'); sufIdx > 0 { + // check if the last part is a number instead of .com/.gr... + // if it's number then it's propably is 0.0.0.0 or 127.0.0.1... so it shouldn' use subdomain + if _, err := strconv.Atoi(fakehost[sufIdx+1:]); err != nil { + // it's not number then process the try to parse the subdomain + redirectScheme := ParseScheme(redirectSchemeAndHost) + realHost := strings.Replace(redirectSchemeAndHost, redirectScheme, "", 1) + redirectHost := strings.Replace(fakehost, fakehost, realHost, 1) + redirectTo = redirectScheme + redirectHost + path + http.Redirect(w, r, redirectTo, StatusMovedPermanently) + return + } + } + } + if path != "/" { + redirectTo += path + } + if redirectTo == r.URL.String() { + return + } + + // redirectTo := redirectSchemeAndHost + r.RequestURI + + http.Redirect(w, r, redirectTo, StatusMovedPermanently) + } +} + +// Proxy not really a proxy, it's just +// starts a server listening on proxyAddr but redirects all requests to the redirectToSchemeAndHost+$path +// nothing special, use it only when you want to start a secondary server which its only work is to redirect from one requested path to another +// +// returns a close function +func Proxy(proxyAddr string, redirectSchemeAndHost string) func() error { + proxyAddr = ParseHost(proxyAddr) + + // override the handler and redirect all requests to this addr + h := ProxyHandler(redirectSchemeAndHost) + prx := New(OptionDisableBanner(true)) + prx.Adapt(RouterBuilderPolicy(func(RouteRepository, ContextPool) http.Handler { + return h + })) + + go prx.Listen(proxyAddr) + time.Sleep(300 * time.Millisecond) + + return func() error { return prx.Close() } +} diff --git a/configuration.go b/configuration.go index d2cc030f..1858104d 100644 --- a/configuration.go +++ b/configuration.go @@ -2,11 +2,9 @@ package iris import ( "crypto/tls" - "io" "net" "net/http" "net/url" - "os" "strconv" "time" @@ -39,7 +37,7 @@ func (o OptionSet) Set(c *Configuration) { o(c) } -// Configuration the whole configuration for an iris instance ($instance.Config) or global iris instance (iris.Config) +// Configuration the whole configuration for an iris instance ($instance.Config) or global iris instance (iris.Default.Config) // these can be passed via options also, look at the top of this file(configuration.go) // // Configuration is also implements the OptionSet so it's a valid option itself, this is brilliant enough @@ -57,7 +55,7 @@ type Configuration struct { // Note: this is the main's server Host, you can setup unlimited number of net/http servers // listening to the $instance.Handler after the manually-called $instance.Build // - // Default comes from iris.Listen/.Serve with iris' listeners (iris.TCP4/UNIX/TLS/LETSENCRYPT) + // Default comes from iris.Default.Listen/.Serve with iris' listeners (iris.TCP4/UNIX/TLS/LETSENCRYPT) VHost string // VScheme is the scheme (http:// or https://) putted at the template function '{{url }}' @@ -66,7 +64,7 @@ type Configuration struct { // 1. You didn't start the main server using $instance.Listen/ListenTLS/ListenLETSENCRYPT or $instance.Serve($instance.TCP4()/.TLS...) // 2. if you're using something like nginx and have iris listening with addr only(http://) but the nginx mapper is listening to https:// // - // Default comes from iris.Listen/.Serve with iris' listeners (TCP4,UNIX,TLS,LETSENCRYPT) + // Default comes from iris.Default.Listen/.Serve with iris' listeners (TCP4,UNIX,TLS,LETSENCRYPT) VScheme string ReadTimeout time.Duration // maximum duration before timing out read of the request @@ -105,8 +103,8 @@ type Configuration struct { // 3. If you as developer edited the $GOPATH/src/github/kataras or any other Iris' Go dependencies at the past // then the update process will fail. // - // Usage: iris.Set(iris.OptionCheckForUpdates(true)) or - // iris.Config.CheckForUpdates = true or + // Usage: iris.Default.Set(iris.OptionCheckForUpdates(true)) or + // iris.Default.Config.CheckForUpdates = true or // app := iris.New(iris.OptionCheckForUpdates(true)) // Default is false CheckForUpdates bool @@ -159,24 +157,10 @@ type Configuration struct { // The body will not be changed and existing data before the context.UnmarshalBody/ReadJSON/ReadXML will be not consumed. DisableBodyConsumptionOnUnmarshal bool - // LoggerOut is the destination for output - // - // Default is os.Stdout - LoggerOut io.Writer - // LoggerPreffix is the logger's prefix to write at beginning of each line - // - // Default is [IRIS] - LoggerPreffix string - - // DisableTemplateEngines set to true to disable loading the default template engine (html/template) and disallow the use of iris.UseEngine + // DisableTemplateEngines set to true to disable loading the default template engine (html/template) and disallow the use of iris.Default.UseEngine // Defaults to false DisableTemplateEngines bool - // IsDevelopment iris will act like a developer, for example - // If true then re-builds the templates on each request - // Defaults to false - IsDevelopment bool - // TimeFormat time format for any kind of datetime parsing TimeFormat string @@ -204,6 +188,7 @@ type Configuration struct { // Set implements the OptionSetter func (c Configuration) Set(main *Configuration) { + // ignore error mergo.MergeWithOverwrite(main, c) } @@ -223,7 +208,7 @@ var ( // Note: this is the main's server Host, you can setup unlimited number of net/http servers // listening to the $instance.Handler after the manually-called $instance.Build // - // Default comes from iris.Listen/.Serve with iris' listeners (iris.TCP4/UNIX/TLS/LETSENCRYPT) + // Default comes from iris.Default.Listen/.Serve with iris' listeners (iris.TCP4/UNIX/TLS/LETSENCRYPT) OptionVHost = func(val string) OptionSet { return func(c *Configuration) { c.VHost = val @@ -236,7 +221,7 @@ var ( // 1. You didn't start the main server using $instance.Listen/ListenTLS/ListenLETSENCRYPT or $instance.Serve($instance.TCP4()/.TLS...) // 2. if you're using something like nginx and have iris listening with addr only(http://) but the nginx mapper is listening to https:// // - // Default comes from iris.Listen/.Serve with iris' listeners (TCP4,UNIX,TLS,LETSENCRYPT) + // Default comes from iris.Default.Listen/.Serve with iris' listeners (TCP4,UNIX,TLS,LETSENCRYPT) OptionVScheme = func(val string) OptionSet { return func(c *Configuration) { c.VScheme = val @@ -299,8 +284,8 @@ var ( // 3. If you as developer edited the $GOPATH/src/github/kataras or any other Iris' Go dependencies at the past // then the update process will fail. // - // Usage: iris.Set(iris.OptionCheckForUpdates(true)) or - // iris.Config.CheckForUpdates = true or + // Usage: iris.Default.Set(iris.OptionCheckForUpdates(true)) or + // iris.Default.Config.CheckForUpdates = true or // app := iris.New(iris.OptionCheckForUpdates(true)) // Default is false OptionCheckForUpdates = func(val bool) OptionSet { @@ -371,41 +356,6 @@ var ( } } - // OptionLoggerOut is the destination for output - // - // Default is os.Stdout - OptionLoggerOut = func(val io.Writer) OptionSet { - return func(c *Configuration) { - c.LoggerOut = val - } - } - - // OptionLoggerPreffix is the logger's prefix to write at beginning of each line - // - // Default is [IRIS] - OptionLoggerPreffix = func(val string) OptionSet { - return func(c *Configuration) { - c.LoggerPreffix = val - } - } - - // OptionDisableTemplateEngines set to true to disable loading the default template engine (html/template) and disallow the use of iris.UseEngine - // Default is false - OptionDisableTemplateEngines = func(val bool) OptionSet { - return func(c *Configuration) { - c.DisableTemplateEngines = val - } - } - - // OptionIsDevelopment iris will act like a developer, for example - // If true then re-builds the templates on each request - // Default is false - OptionIsDevelopment = func(val bool) OptionSet { - return func(c *Configuration) { - c.IsDevelopment = val - } - } - // OptionTimeFormat time format for any kind of datetime parsing OptionTimeFormat = func(val string) OptionSet { return func(c *Configuration) { @@ -459,7 +409,6 @@ const ( DefaultDisablePathCorrection = false DefaultEnablePathEscape = false DefaultCharset = "UTF-8" - DefaultLoggerPreffix = "[IRIS] " // Per-connection buffer size for requests' reading. // This also limits the maximum header size. // @@ -475,11 +424,6 @@ const ( DefaultWriteTimeout = 0 ) -var ( - // DefaultLoggerOut is the default logger's output - DefaultLoggerOut = os.Stdout -) - // DefaultConfiguration returns the default configuration for an Iris station, fills the main Configuration func DefaultConfiguration() Configuration { return Configuration{ @@ -495,10 +439,6 @@ func DefaultConfiguration() Configuration { FireMethodNotAllowed: false, DisableBanner: false, DisableBodyConsumptionOnUnmarshal: false, - LoggerOut: DefaultLoggerOut, - LoggerPreffix: DefaultLoggerPreffix, - DisableTemplateEngines: false, - IsDevelopment: false, TimeFormat: DefaultTimeFormat, Charset: DefaultCharset, Gzip: false, @@ -623,7 +563,7 @@ type WebsocketConfiguration struct { // must match the host of the request. // // The default behavior is to allow all origins - // you can change this behavior by setting the iris.Config.Websocket.CheckOrigin = iris.WebsocketCheckSameOrigin + // you can change this behavior by setting the iris.Default.Config.Websocket.CheckOrigin = iris.WebsocketCheckSameOrigin CheckOrigin func(r *http.Request) bool // IDGenerator used to create (and later on, set) // an ID for each incoming websocket connections (clients). @@ -733,7 +673,7 @@ var ( ctx.EmitError(status) } // DefaultWebsocketCheckOrigin is the default method to allow websocket clients to connect to this server - // you can change this behavior by setting the iris.Config.Websocket.CheckOrigin = iris.WebsocketCheckSameOrigin + // you can change this behavior by setting the iris.Default.Config.Websocket.CheckOrigin = iris.WebsocketCheckSameOrigin DefaultWebsocketCheckOrigin = func(r *http.Request) bool { return true } diff --git a/configuration_test.go b/configuration_test.go deleted file mode 100644 index 9cb3e82f..00000000 --- a/configuration_test.go +++ /dev/null @@ -1,88 +0,0 @@ -// Black-box Testing -package iris_test - -import ( - "reflect" - "testing" - - "github.com/kataras/iris" -) - -// go test -v -run TestConfig* - -func TestConfigStatic(t *testing.T) { - def := iris.DefaultConfiguration() - - api := iris.New(def) - afterNew := *api.Config - - if !reflect.DeepEqual(def, afterNew) { - t.Fatalf("Default configuration is not the same after NewFromConfig expected:\n %#v \ngot:\n %#v", def, afterNew) - } - - afterNew.Charset = "changed" - - if reflect.DeepEqual(def, afterNew) { - t.Fatalf("Configuration should be not equal, got: %#v", afterNew) - } - - api = iris.New(iris.Configuration{IsDevelopment: true}) - - afterNew = *api.Config - - if api.Config.IsDevelopment == false { - t.Fatalf("Passing a Configuration field as Option fails, expected IsDevelopment to be true but was false") - } - - api = iris.New() // empty , means defaults so - if !reflect.DeepEqual(def, *api.Config) { - t.Fatalf("Default configuration is not the same after NewFromConfig expected:\n %#v \ngot:\n %#v", def, *api.Config) - } -} - -func TestConfigOptions(t *testing.T) { - charset := "MYCHARSET" - dev := true - - api := iris.New(iris.OptionCharset(charset), iris.OptionIsDevelopment(dev)) - - if got := api.Config.Charset; got != charset { - t.Fatalf("Expected configuration Charset to be: %s but got: %s", charset, got) - } - - if got := api.Config.IsDevelopment; got != dev { - t.Fatalf("Expected configuration IsDevelopment to be: %#v but got: %#v", dev, got) - } - - // now check if other default values are setted (should be setted automatically) - - expected := iris.DefaultConfiguration() - expected.Charset = charset - expected.IsDevelopment = dev - - has := *api.Config - if !reflect.DeepEqual(has, expected) { - t.Fatalf("Default configuration is not the same after New expected:\n %#v \ngot:\n %#v", expected, has) - } -} - -func TestConfigOptionsDeep(t *testing.T) { - cookiename := "MYSESSIONID" - charset := "MYCHARSET" - dev := true - vhost := "mydomain.com" - // first session, after charset,dev and profilepath, no canonical order. - api := iris.New(iris.OptionSessionsCookie(cookiename), iris.OptionCharset(charset), iris.OptionIsDevelopment(dev), iris.OptionVHost(vhost)) - - expected := iris.DefaultConfiguration() - expected.Sessions.Cookie = cookiename - expected.Charset = charset - expected.IsDevelopment = dev - expected.VHost = vhost - - has := *api.Config - - if !reflect.DeepEqual(has, expected) { - t.Fatalf("DEEP configuration is not the same after New expected:\n %#v \ngot:\n %#v", expected, has) - } -} diff --git a/context.go b/context.go index 3972d20a..37e45dcf 100644 --- a/context.go +++ b/context.go @@ -17,12 +17,14 @@ import ( "runtime" "strconv" "strings" + "sync" "time" "github.com/iris-contrib/formBinder" "github.com/kataras/go-errors" "github.com/kataras/go-fs" "github.com/kataras/go-sessions" + "github.com/kataras/go-template" ) const ( @@ -119,6 +121,85 @@ func (r *requestValues) Reset() { *r = (*r)[:0] } +type ( + // ContextPool is a set of temporary *Context that may be individually saved and + // retrieved. + // + // Any item stored in the Pool may be removed automatically at any time without + // notification. If the Pool holds the only reference when this happens, the + // item might be deallocated. + // + // The ContextPool is safe for use by multiple goroutines simultaneously. + // + // ContextPool's purpose is to cache allocated but unused Contexts for later reuse, + // relieving pressure on the garbage collector. + ContextPool interface { + // Acquire returns a Context from pool. + // See Release. + Acquire(w http.ResponseWriter, r *http.Request) *Context + + // Release puts a Context back to its pull, this function releases its resources. + // See Acquire. + Release(ctx *Context) + + // Framework is never used, except when you're in a place where you don't have access to the *iris.Framework station + // but you need to fire a func or check its Config. + // + // Used mostly inside external routers to take the .Config.VHost + // without the need of other param receivers and refactors when changes + // + // note: we could make a variable inside contextPool which would be received by newContextPool + // but really doesn't need, we just need to borrow a context: we are in pre-build state + // so the server is not actually running yet, no runtime performance cost. + Framework() *Framework + + // Run is a combination of Acquire and Release , between these two the `runner` runs, + // when `runner` finishes its job then the Context is being released. + Run(w http.ResponseWriter, r *http.Request, runner func(ctx *Context)) + } + + contextPool struct { + pool sync.Pool + } +) + +var _ ContextPool = &contextPool{} + +func (c *contextPool) Acquire(w http.ResponseWriter, r *http.Request) *Context { + ctx := c.pool.Get().(*Context) + ctx.ResponseWriter = acquireResponseWriter(w) + ctx.Request = r + return ctx +} + +func (c *contextPool) Release(ctx *Context) { + // flush the body (on recorder) or just the status code (on basic response writer) + // when all finished + ctx.ResponseWriter.flushResponse() + + ctx.Middleware = nil + ctx.session = nil + ctx.Request = nil + ///TODO: + ctx.ResponseWriter.releaseMe() + ctx.values.Reset() + + c.pool.Put(ctx) +} + +func (c *contextPool) Framework() *Framework { + ctx := c.pool.Get().(*Context) + s := ctx.framework + c.pool.Put(ctx) + return s +} + +func (c *contextPool) Run(w http.ResponseWriter, r *http.Request, runner func(*Context)) { + ctx := c.Acquire(w, r) + runner(ctx) + c.Release(ctx) +} + type ( // Map is just a conversion for a map[string]interface{} @@ -198,17 +279,17 @@ func (ctx *Context) GetHandlerName() string { // it can validate paths, has sessions, path parameters and all. // // You can find the Route by iris.Lookup("theRouteName") -// you can set a route name as: myRoute := iris.Get("/mypath", handler)("theRouteName") +// you can set a route name as: myRoute := iris.Default.Get("/mypath", handler)("theRouteName") // that will set a name to the route and returns its iris.Route instance for further usage. // // It doesn't changes the global state, if a route was "offline" it remains offline. // // see ExecRouteAgainst(routeName, againstRequestPath string), -// iris.None(...) and iris.SetRouteOnline/SetRouteOffline +// iris.Default.None(...) and iris.Default.SetRouteOnline/SetRouteOffline // For more details look: https://github.com/kataras/iris/issues/585 // // Example: https://github.com/iris-contrib/examples/tree/master/route_state -func (ctx *Context) ExecRoute(r Route) *Context { +func (ctx *Context) ExecRoute(r RouteInfo) *Context { return ctx.ExecRouteAgainst(r, ctx.Path()) } @@ -219,23 +300,27 @@ func (ctx *Context) ExecRoute(r Route) *Context { // it can validate paths, has sessions, path parameters and all. // // You can find the Route by iris.Lookup("theRouteName") -// you can set a route name as: myRoute := iris.Get("/mypath", handler)("theRouteName") +// you can set a route name as: myRoute := iris.Default.Get("/mypath", handler)("theRouteName") // that will set a name to the route and returns its iris.Route instance for further usage. // // It doesn't changes the global state, if a route was "offline" it remains offline. // // see ExecRoute(routeName), -// iris.None(...) and iris.SetRouteOnline/SetRouteOffline +// iris.Default.None(...) and iris.Default.SetRouteOnline/SetRouteOffline // For more details look: https://github.com/kataras/iris/issues/585 // // Example: https://github.com/iris-contrib/examples/tree/master/route_state -func (ctx *Context) ExecRouteAgainst(r Route, againstRequestPath string) *Context { +func (ctx *Context) ExecRouteAgainst(r RouteInfo, againstRequestPath string) *Context { if r != nil { context := &(*ctx) context.Middleware = context.Middleware[0:0] context.values.Reset() - tree := ctx.framework.muxAPI.mux.getTree(r.Method(), r.Subdomain()) - tree.entry.get(againstRequestPath, context) + context.Request.RequestURI = againstRequestPath + context.Request.URL.Path = againstRequestPath + context.Request.URL.RawPath = againstRequestPath + ctx.framework.policies.RouterReversionPolicy.RouteContextLinker(r, context) + // tree := ctx.framework.muxAPI.mux.getTree(r.Method(), r.Subdomain()) + // tree.entry.get(againstRequestPath, context) if len(context.Middleware) > 0 { context.Do() return context @@ -251,16 +336,17 @@ func (ctx *Context) ExecRouteAgainst(r Route, againstRequestPath string) *Contex // then use the: if c := ExecRoute(r); c == nil { /* move to the next, the route is not valid */ } // // You can find the Route by iris.Lookup("theRouteName") -// you can set a route name as: myRoute := iris.Get("/mypath", handler)("theRouteName") +// you can set a route name as: myRoute := iris.Default.Get("/mypath", handler)("theRouteName") // that will set a name to the route and returns its iris.Route instance for further usage. // // if the route found then it executes that and don't continue to the next handler // if not found then continue to the next handler -func Prioritize(r Route) HandlerFunc { +func Prioritize(r RouteInfo) HandlerFunc { if r != nil { return func(ctx *Context) { reqPath := ctx.Path() - if strings.HasPrefix(reqPath, r.StaticPath()) { + staticPath := ctx.framework.policies.RouterReversionPolicy.StaticPath(r.Path()) + if strings.HasPrefix(reqPath, staticPath) { newctx := ctx.ExecRouteAgainst(r, reqPath) if newctx == nil { // route not found. ctx.EmitError(StatusNotFound) @@ -317,7 +403,7 @@ func (ctx *Context) Subdomain() (subdomain string) { func (ctx *Context) VirtualHostname() string { realhost := ctx.Host() hostname := realhost - virtualhost := ctx.framework.mux.hostname + virtualhost := ctx.framework.Config.VHost if portIdx := strings.IndexByte(hostname, ':'); portIdx > 0 { hostname = hostname[0:portIdx] @@ -496,7 +582,7 @@ var ( // ------------------------------------------------------------------------------------- // NOTE: No default max body size http package has some built'n protection for DoS attacks -// See iris.Config.MaxBytesReader, https://github.com/golang/go/issues/2093#issuecomment-66057813 +// See iris.Default.Config.MaxBytesReader, https://github.com/golang/go/issues/2093#issuecomment-66057813 // and https://github.com/golang/go/issues/2093#issuecomment-66057824 // LimitRequestBodySize is a middleware which sets a request body size limit for all next handlers @@ -555,7 +641,7 @@ func (u UnmarshalerFunc) Unmarshal(data []byte, v interface{}) error { // Examples of usage: context.ReadJSON, context.ReadXML func (ctx *Context) UnmarshalBody(v interface{}, unmarshaler Unmarshaler) error { if ctx.Request.Body == nil { - return errors.New("Empty body, please send request body!") + return errors.New("unmarshal: empty body") } rawData, err := ioutil.ReadAll(ctx.Request.Body) @@ -635,14 +721,12 @@ func (ctx *Context) Redirect(urlToRedirect string, statusHeader ...int) { ctx.StopExecution() httpStatus := StatusFound // a 'temporary-redirect-like' which works better than for our purpose - if statusHeader != nil && len(statusHeader) > 0 && statusHeader[0] > 0 { + if len(statusHeader) > 0 && statusHeader[0] > 0 { httpStatus = statusHeader[0] } if urlToRedirect == ctx.Path() { - if ctx.framework.Config.IsDevelopment { - ctx.Log("Trying to redirect to itself. FROM: %s TO: %s", ctx.Path(), urlToRedirect) - } + ctx.Log(DevMode, "trying to redirect to itself. FROM: %s TO: %s", ctx.Path(), urlToRedirect) } http.Redirect(ctx.ResponseWriter, ctx.Request, urlToRedirect, httpStatus) } @@ -739,46 +823,68 @@ func (ctx *Context) TryWriteGzip(b []byte) (int, error) { // ------------------------------------------------------------------------------------- // ------------------------------------------------------------------------------------- -// renderSerialized renders contents with a serializer with status OK which you can change using RenderWithStatus or ctx.SetStatusCode(iris.StatusCode) -func (ctx *Context) renderSerialized(contentType string, obj interface{}, options ...map[string]interface{}) error { - s := ctx.framework.serializers - finalResult, err := s.Serialize(contentType, obj, options...) - if err != nil { - return err +const ( + // NoLayout to disable layout for a particular template file + NoLayout = template.NoLayout + // TemplateLayoutContextKey is the name of the user values which can be used to set a template layout from a middleware and override the parent's + TemplateLayoutContextKey = "templateLayout" +) + +// getGzipOption receives a default value and the render options map and returns if gzip is enabled for this render action +func getGzipOption(defaultValue bool, options map[string]interface{}) bool { + gzipOpt := options["gzip"] // we only need that, so don't create new map to keep the options. + if b, isBool := gzipOpt.(bool); isBool { + return b + } + return defaultValue +} + +// gtCharsetOption receives a default value and the render options map and returns the correct charset for this render action +func getCharsetOption(defaultValue string, options map[string]interface{}) string { + charsetOpt := options["charset"] + if s, isString := charsetOpt.(string); isString { + return s + } + return defaultValue +} + +func (ctx *Context) fastRenderWithStatus(status int, cType string, data []byte) (err error) { + if _, shouldFirstStatusCode := ctx.ResponseWriter.(*responseWriter); shouldFirstStatusCode { + ctx.SetStatusCode(status) } gzipEnabled := ctx.framework.Config.Gzip charset := ctx.framework.Config.Charset - if len(options) > 0 { - gzipEnabled = getGzipOption(gzipEnabled, options[0]) // located to the template.go below the RenderOptions - charset = getCharsetOption(charset, options[0]) - } - ctype := contentType - if ctype == contentMarkdown { // remember the text/markdown is just a custom internal iris content type, which in reallity renders html - ctype = contentHTML + if cType != contentBinary { + cType += "; charset=" + charset } - if ctype != contentBinary { // set the charset only on non-binary data - ctype += "; charset=" + charset - } - ctx.SetContentType(ctype) - if gzipEnabled { - ctx.TryWriteGzip(finalResult) + // add the content type to the response + ctx.SetContentType(cType) + + var out io.Writer + if gzipEnabled && ctx.clientAllowsGzip() { + ctx.ResponseWriter.Header().Add(varyHeader, acceptEncodingHeader) + ctx.SetHeader(contentEncodingHeader, "gzip") + + gzipWriter := fs.AcquireGzipWriter(ctx.ResponseWriter) + defer fs.ReleaseGzipWriter(gzipWriter) + out = gzipWriter } else { - ctx.ResponseWriter.Write(finalResult) - } - ctx.SetStatusCode(StatusOK) - return nil -} - -// RenderTemplateSource serves a template source(raw string contents) from the first template engines which supports raw parsing returns its result as string -func (ctx *Context) RenderTemplateSource(status int, src string, binding interface{}, options ...map[string]interface{}) error { - err := ctx.framework.templates.renderSource(ctx, src, binding, options...) - if err == nil { - ctx.SetStatusCode(status) + out = ctx.ResponseWriter } - return err + // no need to loop through the RenderPolicy, these types must be fast as possible + // with the features like gzip and custom charset too. + _, err = out.Write(data) + + // we don't care for the last one it will not be written more than one if we have the *responseWriter + ///TODO: + // if we have ResponseRecorder order doesn't matters but I think the transactions have bugs, + // temporary let's keep it here because it 'fixes' one of them... + ctx.SetStatusCode(status) + + return } // RenderWithStatus builds up the response from the specified template or a serialize engine. @@ -789,11 +895,55 @@ func (ctx *Context) RenderWithStatus(status int, name string, binding interface{ ctx.SetStatusCode(status) } - if strings.IndexByte(name, '.') > -1 { //we have template - err = ctx.framework.templates.renderFile(ctx, name, binding, options...) - } else { - err = ctx.renderSerialized(name, binding, options...) + // we do all these because we don't want to initialize a new map for each execution... + gzipEnabled := ctx.framework.Config.Gzip + charset := ctx.framework.Config.Charset + if len(options) > 0 { + gzipEnabled = getGzipOption(gzipEnabled, options[0]) + charset = getCharsetOption(charset, options[0]) } + + ctxLayout := ctx.GetString(TemplateLayoutContextKey) + if ctxLayout != "" { + if len(options) > 0 { + options[0]["layout"] = ctxLayout + } else { + options = []map[string]interface{}{{"layout": ctxLayout}} + } + } + + // Find Content type + // if it the name is not a template file, then take that as the content type. + cType := contentHTML + if !strings.Contains(name, ".") { + // remember the text/markdown is just a custom internal + // iris content type, which in reallity renders html + if name != contentMarkdown { + cType = name + } + + } + if cType != contentBinary { + cType += "; charset=" + charset + } + + // add the content type to the response + ctx.SetContentType(cType) + + var out io.Writer + if gzipEnabled && ctx.clientAllowsGzip() { + ctx.ResponseWriter.Header().Add(varyHeader, acceptEncodingHeader) + ctx.SetHeader(contentEncodingHeader, "gzip") + + gzipWriter := fs.AcquireGzipWriter(ctx.ResponseWriter) + defer fs.ReleaseGzipWriter(gzipWriter) + out = gzipWriter + } else { + out = ctx.ResponseWriter + } + + err = ctx.framework.Render(out, name, binding, options...) + // we don't care for the last one it will not be written more than one if we have the *responseWriter ///TODO: // if we have ResponseRecorder order doesn't matters but I think the transactions have bugs , for now let's keep it here because it 'fixes' one of them... @@ -817,32 +967,45 @@ func (ctx *Context) Render(name string, binding interface{}, options ...map[stri // Note: the options: "gzip" and "charset" are built'n support by Iris, so you can pass these on any template engine or serialize engine func (ctx *Context) MustRender(name string, binding interface{}, options ...map[string]interface{}) { if err := ctx.Render(name, binding, options...); err != nil { - ctx.HTML(StatusServiceUnavailable, fmt.Sprintf("

Template: %s

%s", name, err.Error())) - if ctx.framework.Config.IsDevelopment { - ctx.framework.Logger.Printf("MustRender panics on template: %s.Trace: %s\n", name, err) + htmlErr := ctx.HTML(StatusServiceUnavailable, + fmt.Sprintf("

Template: %s

%s", name, err.Error())) + + ctx.Log(DevMode, "MustRender failed to render '%s', trace: %s\n", + name, err) + + if htmlErr != nil { + ctx.Log(DevMode, "MustRender also failed to render the html fallback: %s", + htmlErr.Error()) } - } -} -// TemplateString accepts a template filename, its context data and returns the result of the parsed template (string) -// if any error returns empty string -func (ctx *Context) TemplateString(name string, binding interface{}, options ...map[string]interface{}) string { - return ctx.framework.TemplateString(name, binding, options...) -} - -// HTML writes html string with a http status -func (ctx *Context) HTML(status int, htmlContents string) { - if err := ctx.RenderWithStatus(status, contentHTML, htmlContents); err != nil { - // if no serialize engine found for text/html - ctx.SetContentType(contentHTML + "; charset=" + ctx.framework.Config.Charset) - ctx.SetStatusCode(status) - ctx.WriteString(htmlContents) } } // Data writes out the raw bytes as binary data. -func (ctx *Context) Data(status int, v []byte) error { - return ctx.RenderWithStatus(status, contentBinary, v) +// +// RenderPolicy does NOT apply to context.HTML, context.Text and context.Data +// To change their default behavior users should use +// the context.RenderWithStatus(statusCode, contentType, content, options...) instead. +func (ctx *Context) Data(status int, data []byte) error { + return ctx.fastRenderWithStatus(status, contentBinary, data) +} + +// Text writes out a string as plain text. +// +// RenderPolicy does NOT apply to context.HTML, context.Text and context.Data +// To change their default behavior users should use +// the context.RenderWithStatus(statusCode, contentType, content, options...) instead. +func (ctx *Context) Text(status int, text string) error { + return ctx.fastRenderWithStatus(status, contentBinary, []byte(text)) +} + +// HTML writes html string with a http status +// +// RenderPolicy does NOT apply to context.HTML, context.Text and context.Data +// To change their default behavior users should use +// the context.RenderWithStatus(statusCode, contentType, content, options...) instead. +func (ctx *Context) HTML(status int, htmlContents string) error { + return ctx.fastRenderWithStatus(status, contentHTML, []byte(htmlContents)) } // JSON marshals the given interface object and writes the JSON response. @@ -855,11 +1018,6 @@ func (ctx *Context) JSONP(status int, callback string, v interface{}) error { return ctx.RenderWithStatus(status, contentJSONP, v, map[string]interface{}{"callback": callback}) } -// Text writes out a string as plain text. -func (ctx *Context) Text(status int, v string) error { - return ctx.RenderWithStatus(status, contentText, v) -} - // XML marshals the given interface object and writes the XML response. func (ctx *Context) XML(status int, v interface{}) error { return ctx.RenderWithStatus(status, contentXML, v) @@ -867,15 +1025,20 @@ func (ctx *Context) XML(status int, v interface{}) error { // MarkdownString parses the (dynamic) markdown string and returns the converted html string func (ctx *Context) MarkdownString(markdownText string) string { - return ctx.framework.SerializeToString(contentMarkdown, markdownText) + out := &bytes.Buffer{} + _, ok := ctx.framework.policies.RenderPolicy(out, contentMarkdown, markdownText) + if ok { + return out.String() + } + return "" } // Markdown parses and renders to the client a particular (dynamic) markdown string // accepts two parameters // first is the http status code // second is the markdown string -func (ctx *Context) Markdown(status int, markdown string) { - ctx.HTML(status, ctx.MarkdownString(markdown)) +func (ctx *Context) Markdown(status int, markdown string) error { + return ctx.HTML(status, ctx.MarkdownString(markdown)) } // ------------------------------------------------------------------------------------- @@ -898,9 +1061,9 @@ func (ctx *Context) staticCachePassed(modtime time.Time) bool { // SetClientCachedBody like SetBody but it sends with an expiration datetime // which is managed by the client-side (all major browsers supports this feature) -func (ctx *Context) SetClientCachedBody(status int, bodyContent []byte, cType string, modtime time.Time) { +func (ctx *Context) SetClientCachedBody(status int, bodyContent []byte, cType string, modtime time.Time) error { if ctx.staticCachePassed(modtime) { - return + return nil } modtimeFormatted := modtime.UTC().Format(ctx.framework.Config.TimeFormat) @@ -909,7 +1072,8 @@ func (ctx *Context) SetClientCachedBody(status int, bodyContent []byte, cType st ctx.ResponseWriter.Header().Set(lastModified, modtimeFormatted) ctx.SetStatusCode(status) - ctx.ResponseWriter.Write(bodyContent) + _, err := ctx.ResponseWriter.Write(bodyContent) + return err } // ServeContent serves content, headers are autoset @@ -973,9 +1137,9 @@ func (ctx *Context) ServeFile(filename string, gzipCompression bool) error { // SendFile sends file for force-download to the client // // Use this instead of ServeFile to 'force-download' bigger files to the client -func (ctx *Context) SendFile(filename string, destinationName string) { +func (ctx *Context) SendFile(filename string, destinationName string) error { ctx.ResponseWriter.Header().Set(contentDisposition, "attachment;filename="+destinationName) - ctx.ServeFile(filename, false) + return ctx.ServeFile(filename, false) } // StreamWriter registers the given stream writer for populating @@ -1134,12 +1298,11 @@ func (ctx *Context) ParamInt64(key string) (int64, error) { // hasthe form of key1=value1,key2=value2... func (ctx *Context) ParamsSentence() string { var buff bytes.Buffer - ctx.VisitValues(func(kb string, vg interface{}) { - v, ok := vg.(string) + ctx.VisitValues(func(k string, vi interface{}) { + v, ok := vi.(string) if !ok { return } - k := string(kb) buff.WriteString(k) buff.WriteString("=") buff.WriteString(v) @@ -1444,9 +1607,7 @@ func (ctx *Context) BeginTransaction(pipe func(transaction *Transaction)) { t := newTransaction(ctx) defer func() { if err := recover(); err != nil { - if ctx.framework.Config.IsDevelopment { - ctx.Log(errTransactionInterrupted.Format(err).Error()) - } + ctx.Log(DevMode, errTransactionInterrupted.Format(err).Error()) // complete (again or not , doesn't matters) the scope without loud t.Complete(nil) // we continue as normal, no need to return here* @@ -1465,8 +1626,8 @@ func (ctx *Context) BeginTransaction(pipe func(transaction *Transaction)) { } // Log logs to the iris defined logger -func (ctx *Context) Log(format string, a ...interface{}) { - ctx.framework.Logger.Printf(format, a...) +func (ctx *Context) Log(mode LogMode, format string, a ...interface{}) { + ctx.framework.Log(mode, fmt.Sprintf(format, a...)) } // Framework returns the Iris instance, containing the configuration and all other fields diff --git a/context_test.go b/context_test.go deleted file mode 100644 index 5efcee3e..00000000 --- a/context_test.go +++ /dev/null @@ -1,827 +0,0 @@ -// Black-box Testing -package iris_test - -import ( - "encoding/json" - "encoding/xml" - "fmt" - "io/ioutil" - "net/http" - "net/url" - "strconv" - "strings" - "testing" - "time" - - "github.com/gavv/httpexpect" - "github.com/kataras/iris" - "github.com/kataras/iris/httptest" -) - -// White-box testing * -func TestContextDoNextStop(t *testing.T) { - var context iris.Context - ok := false - afterStop := false - context.Middleware = iris.Middleware{iris.HandlerFunc(func(*iris.Context) { - ok = true - }), iris.HandlerFunc(func(*iris.Context) { - ok = true - }), iris.HandlerFunc(func(*iris.Context) { - // this will never execute - afterStop = true - })} - context.Do() - if context.Pos != 0 { - t.Fatalf("Expecting position 0 for context's middleware but we got: %d", context.Pos) - } - if !ok { - t.Fatalf("Unexpected behavior, first context's middleware didn't executed") - } - ok = false - - context.Next() - - if int(context.Pos) != 1 { - t.Fatalf("Expecting to have position %d but we got: %d", 1, context.Pos) - } - if !ok { - t.Fatalf("Next context's middleware didn't executed") - } - - context.StopExecution() - if context.Pos != 255 { - t.Fatalf("Context's StopExecution didn't worked, we expected to have position %d but we got %d", 255, context.Pos) - } - - if !context.IsStopped() { - t.Fatalf("Should be stopped") - } - - context.Next() - - if afterStop { - t.Fatalf("We stopped the execution but the next handler was executed") - } -} - -type pathParameter struct { - Key string - Value string -} -type pathParameters []pathParameter - -// White-box testing * -func TestContextParams(t *testing.T) { - context := &iris.Context{} - params := pathParameters{ - pathParameter{Key: "testkey", Value: "testvalue"}, - pathParameter{Key: "testkey2", Value: "testvalue2"}, - pathParameter{Key: "id", Value: "3"}, - pathParameter{Key: "bigint", Value: "548921854390354"}, - } - for _, p := range params { - context.Set(p.Key, p.Value) - } - - if v := context.Param(params[0].Key); v != params[0].Value { - t.Fatalf("Expecting parameter value to be %s but we got %s", params[0].Value, context.Param("testkey")) - } - if v := context.Param(params[1].Key); v != params[1].Value { - t.Fatalf("Expecting parameter value to be %s but we got %s", params[1].Value, context.Param("testkey2")) - } - - if context.ParamsLen() != len(params) { - t.Fatalf("Expecting to have %d parameters but we got %d", len(params), context.ParamsLen()) - } - - if vi, err := context.ParamInt(params[2].Key); err != nil { - t.Fatalf("Unexpecting error on context's ParamInt while trying to get the integer of the %s", params[2].Value) - } else if vi != 3 { - t.Fatalf("Expecting to receive %d but we got %d", 3, vi) - } - - if vi, err := context.ParamInt64(params[3].Key); err != nil { - t.Fatalf("Unexpecting error on context's ParamInt while trying to get the integer of the %s", params[2].Value) - } else if vi != 548921854390354 { - t.Fatalf("Expecting to receive %d but we got %d", 548921854390354, vi) - } - - // end-to-end test now, note that we will not test the whole mux here, this happens on http_test.go - - iris.ResetDefault() - expectedParamsStr := "param1=myparam1,param2=myparam2,param3=myparam3afterstatic,anything=/andhere/anything/you/like" - iris.Get("/path/:param1/:param2/staticpath/:param3/*anything", func(ctx *iris.Context) { - paramsStr := ctx.ParamsSentence() - ctx.WriteString(paramsStr) - }) - - httptest.New(iris.Default, t).GET("/path/myparam1/myparam2/staticpath/myparam3afterstatic/andhere/anything/you/like").Expect().Status(iris.StatusOK).Body().Equal(expectedParamsStr) - -} - -func TestContextURLParams(t *testing.T) { - iris.ResetDefault() - passedParams := map[string]string{"param1": "value1", "param2": "value2"} - iris.Get("/", func(ctx *iris.Context) { - params := ctx.URLParams() - ctx.JSON(iris.StatusOK, params) - }) - e := httptest.New(iris.Default, t, httptest.Debug(true)) - - e.GET("/").WithQueryObject(passedParams).Expect().Status(iris.StatusOK).JSON().Equal(passedParams) -} - -// hoststring returns the full host, will return the HOST:IP -func TestContextHostString(t *testing.T) { - iris.ResetDefault() - iris.Default.Config.VHost = "0.0.0.0:8080" - iris.Get("/", func(ctx *iris.Context) { - ctx.WriteString(ctx.Host()) - }) - - iris.Get("/wrong", func(ctx *iris.Context) { - ctx.WriteString(ctx.Host() + "w") - }) - - e := httptest.New(iris.Default, t) - e.GET("/").Expect().Status(iris.StatusOK).Body().Equal(iris.Default.Config.VHost) - e.GET("/wrong").Expect().Body().NotEqual(iris.Default.Config.VHost) -} - -// VirtualHostname returns the hostname only, -// if the host starts with 127.0.0.1 or localhost it gives the registered hostname part of the listening addr -func TestContextVirtualHostName(t *testing.T) { - iris.ResetDefault() - vhost := "mycustomvirtualname.com" - iris.Default.Config.VHost = vhost + ":8080" - iris.Get("/", func(ctx *iris.Context) { - ctx.WriteString(ctx.VirtualHostname()) - }) - - iris.Get("/wrong", func(ctx *iris.Context) { - ctx.WriteString(ctx.VirtualHostname() + "w") - }) - - e := httptest.New(iris.Default, t) - e.GET("/").Expect().Status(iris.StatusOK).Body().Equal(vhost) - e.GET("/wrong").Expect().Body().NotEqual(vhost) -} - -func TestContextFormValueString(t *testing.T) { - iris.ResetDefault() - var k, v string - k = "postkey" - v = "postvalue" - iris.Post("/", func(ctx *iris.Context) { - ctx.WriteString(k + "=" + ctx.FormValue(k)) - }) - e := httptest.New(iris.Default, t) - - e.POST("/").WithFormField(k, v).Expect().Status(iris.StatusOK).Body().Equal(k + "=" + v) -} - -func TestContextSubdomain(t *testing.T) { - iris.ResetDefault() - iris.Default.Config.VHost = "mydomain.com:9999" - //Default.Config.Tester.ListeningAddr = "mydomain.com:9999" - // Default.Config.Tester.ExplicitURL = true - iris.Party("mysubdomain.").Get("/mypath", func(ctx *iris.Context) { - ctx.WriteString(ctx.Subdomain()) - }) - - e := httptest.New(iris.Default, t) - - e.GET("/").WithURL("http://mysubdomain.mydomain.com:9999").Expect().Status(iris.StatusNotFound) - e.GET("/mypath").WithURL("http://mysubdomain.mydomain.com:9999").Expect().Status(iris.StatusOK).Body().Equal("mysubdomain") - - //e.GET("http://mysubdomain.mydomain.com:9999").Expect().Status(StatusNotFound) - //e.GET("http://mysubdomain.mydomain.com:9999/mypath").Expect().Status(iris.StatusOK).Body().Equal("mysubdomain") -} - -type testBinderData struct { - Username string - Mail string - Data []string `form:"mydata" json:"mydata"` -} - -type testBinderXMLData struct { - XMLName xml.Name `xml:"info"` - FirstAttr string `xml:"first,attr"` - SecondAttr string `xml:"second,attr"` - Name string `xml:"name",json:"name"` - Birth string `xml:"birth",json:"birth"` - Stars int `xml:"stars",json:"stars"` -} - -type testBinder struct { - //pointer of testBinderDataJSON or testBinderXMLData - vp interface{} - m iris.Unmarshaler - shouldError bool -} - -func (tj *testBinder) Decode(data []byte) error { - if tj.shouldError { - return fmt.Errorf("Should error") - } - return tj.m.Unmarshal(data, tj.vp) -} - -func testUnmarshaler(t *testing.T, tb *testBinder, - write func(ctx *iris.Context)) *httpexpect.Request { - - // a very dirty and awful way but here we must test in deep - // the custom object's decoder error with the custom - // unmarshaler result whenever the testUnmarshaler called. - if tb.shouldError == false { - tb.shouldError = true - testUnmarshaler(t, tb, nil) - tb.shouldError = false - } - - iris.ResetDefault() - h := func(ctx *iris.Context) { - err := ctx.UnmarshalBody(tb.vp, tb.m) - if tb.shouldError && err == nil { - t.Fatalf("Should prompted for error 'Should error' but not error returned from the custom decoder!") - } else if err != nil { - t.Fatalf("Error when parsing the body: %s", err.Error()) - } - if write != nil { - write(ctx) - } - - if iris.Config.DisableBodyConsumptionOnUnmarshal { - rawData, _ := ioutil.ReadAll(ctx.Request.Body) - if len(rawData) == 0 { - t.Fatalf("Expected data to NOT BE consumed by the previous UnmarshalBody call but we got empty body.") - } - } - } - - iris.Post("/bind_req_body", h) - - e := httptest.New(iris.Default, t) - return e.POST("/bind_req_body") -} - -// same as DecodeBody -// JSON, XML by DecodeBody passing the default unmarshalers -func TestContextBinders(t *testing.T) { - - passed := map[string]interface{}{"Username": "myusername", - "Mail": "mymail@iris-go.com", - "mydata": []string{"mydata1", "mydata2"}} - expectedObject := testBinderData{Username: "myusername", - Mail: "mymail@iris-go.com", - Data: []string{"mydata1", "mydata2"}} - - // JSON - vJSON := &testBinder{&testBinderData{}, - iris.UnmarshalerFunc(json.Unmarshal), false} - - testUnmarshaler( - t, - vJSON, - func(ctx *iris.Context) { - ctx.JSON(iris.StatusOK, vJSON.vp) - }). - WithJSON(passed). - Expect(). - Status(iris.StatusOK). - JSON().Object().Equal(expectedObject) - - // XML - expectedObj := testBinderXMLData{ - XMLName: xml.Name{Local: "info", Space: "info"}, - FirstAttr: "this is the first attr", - SecondAttr: "this is the second attr", - Name: "Iris web framework", - Birth: "13 March 2016", - Stars: 5758, - } - expectedAndPassedObjText := `<` + expectedObj.XMLName.Local + ` first="` + - expectedObj.FirstAttr + `" second="` + - expectedObj.SecondAttr + `">` + - expectedObj.Name + `` + - expectedObj.Birth + `` + - strconv.Itoa(expectedObj.Stars) + `` - - vXML := &testBinder{&testBinderXMLData{}, - iris.UnmarshalerFunc(xml.Unmarshal), false} - testUnmarshaler( - t, - vXML, - func(ctx *iris.Context) { - ctx.XML(iris.StatusOK, vXML.vp) - }). - WithText(expectedAndPassedObjText). - Expect(). - Status(iris.StatusOK). - Body().Equal(expectedAndPassedObjText) - - // JSON with DisableBodyConsumptionOnUnmarshal - iris.Config.DisableBodyConsumptionOnUnmarshal = true - testUnmarshaler( - t, - vJSON, - func(ctx *iris.Context) { - ctx.JSON(iris.StatusOK, vJSON.vp) - }). - WithJSON(passed). - Expect(). - Status(iris.StatusOK). - JSON().Object().Equal(expectedObject) -} - -func TestContextReadForm(t *testing.T) { - iris.ResetDefault() - - iris.Post("/form", func(ctx *iris.Context) { - obj := testBinderData{} - err := ctx.ReadForm(&obj) - if err != nil { - t.Fatalf("Error when parsing the FORM: %s", err.Error()) - } - ctx.JSON(iris.StatusOK, obj) - }) - - e := httptest.New(iris.Default, t) - - passed := map[string]interface{}{"Username": "myusername", "Mail": "mymail@iris-go.com", "mydata": url.Values{"[0]": []string{"mydata1"}, - "[1]": []string{"mydata2"}}} - - expectedObject := testBinderData{Username: "myusername", Mail: "mymail@iris-go.com", Data: []string{"mydata1", "mydata2"}} - - e.POST("/form").WithForm(passed).Expect().Status(iris.StatusOK).JSON().Object().Equal(expectedObject) -} - -// TestContextRedirectTo tests the named route redirect action -func TestContextRedirectTo(t *testing.T) { - iris.ResetDefault() - h := func(ctx *iris.Context) { ctx.WriteString(ctx.Path()) } - iris.Get("/mypath", h)("my-path") - iris.Get("/mypostpath", h)("my-post-path") - iris.Get("mypath/with/params/:param1/:param2", func(ctx *iris.Context) { - if l := ctx.ParamsLen(); l != 2 { - t.Fatalf("Strange error, expecting parameters to be two but we got: %d", l) - } - ctx.WriteString(ctx.Path()) - })("my-path-with-params") - - iris.Get("/redirect/to/:routeName/*anyparams", func(ctx *iris.Context) { - routeName := ctx.Param("routeName") - var args []interface{} - anyparams := ctx.Param("anyparams") - if anyparams != "" && anyparams != "/" { - params := strings.Split(anyparams[1:], "/") // firstparam/secondparam - for _, s := range params { - args = append(args, s) - } - } - ctx.RedirectTo(routeName, args...) - }) - - e := httptest.New(iris.Default, t) - - e.GET("/redirect/to/my-path/").Expect().Status(iris.StatusOK).Body().Equal("/mypath") - e.GET("/redirect/to/my-post-path/").Expect().Status(iris.StatusOK).Body().Equal("/mypostpath") - e.GET("/redirect/to/my-path-with-params/firstparam/secondparam").Expect().Status(iris.StatusOK).Body().Equal("/mypath/with/params/firstparam/secondparam") -} - -func TestContextUserValues(t *testing.T) { - iris.ResetDefault() - testCustomObjUserValue := struct{ Name string }{Name: "a name"} - values := map[string]interface{}{"key1": "value1", "key2": "value2", "key3": 3, "key4": testCustomObjUserValue, "key5": map[string]string{"key": "value"}} - - iris.Get("/test", func(ctx *iris.Context) { - - for k, v := range values { - ctx.Set(k, v) - } - - }, func(ctx *iris.Context) { - for k, v := range values { - userValue := ctx.Get(k) - if userValue != v { - t.Fatalf("Expecting user value: %s to be equal with: %#v but got: %#v", k, v, userValue) - } - - if m, isMap := userValue.(map[string]string); isMap { - if m["key"] != v.(map[string]string)["key"] { - t.Fatalf("Expecting user value: %s to be equal with: %#v but got: %#v", k, v.(map[string]string)["key"], m["key"]) - } - } else { - if userValue != v { - t.Fatalf("Expecting user value: %s to be equal with: %#v but got: %#v", k, v, userValue) - } - } - - } - }) - - e := httptest.New(iris.Default, t) - - e.GET("/test").Expect().Status(iris.StatusOK) - -} - -func TestContextCookieSetGetRemove(t *testing.T) { - iris.ResetDefault() - key := "mykey" - value := "myvalue" - iris.Get("/set", func(ctx *iris.Context) { - ctx.SetCookieKV(key, value) // should return non empty cookies - }) - - iris.Get("/set_advanced", func(ctx *iris.Context) { - c := &http.Cookie{} - c.Name = key - c.Value = value - c.HttpOnly = true - c.Expires = time.Now().Add(time.Duration((60 * 60 * 24 * 7 * 4)) * time.Second) - ctx.SetCookie(c) - }) - - iris.Get("/get", func(ctx *iris.Context) { - ctx.WriteString(ctx.GetCookie(key)) // should return my value - }) - - iris.Get("/remove", func(ctx *iris.Context) { - ctx.RemoveCookie(key) - cookieFound := false - ctx.VisitAllCookies(func(k, v string) { - cookieFound = true - }) - if cookieFound { - t.Fatalf("Cookie has been found, when it shouldn't!") - } - ctx.WriteString(ctx.GetCookie(key)) // should return "" - }) - - e := httptest.New(iris.Default, t) - e.GET("/set").Expect().Status(iris.StatusOK).Cookies().NotEmpty() - e.GET("/get").Expect().Status(iris.StatusOK).Body().Equal(value) - e.GET("/remove").Expect().Status(iris.StatusOK).Body().Equal("") - // test again with advanced set - e.GET("/set_advanced").Expect().Status(iris.StatusOK).Cookies().NotEmpty() - e.GET("/get").Expect().Status(iris.StatusOK).Body().Equal(value) - e.GET("/remove").Expect().Status(iris.StatusOK).Body().Equal("") -} - -func TestContextSessions(t *testing.T) { - t.Parallel() - values := map[string]interface{}{ - "Name": "iris", - "Months": "4", - "Secret": "dsads£2132215£%%Ssdsa", - } - - iris.ResetDefault() - iris.Default.Config.Sessions.Cookie = "mycustomsessionid" - - writeValues := func(ctx *iris.Context) { - sessValues := ctx.Session().GetAll() - ctx.JSON(iris.StatusOK, sessValues) - } - - if testEnableSubdomain { - iris.Party(testSubdomain+".").Get("/get", func(ctx *iris.Context) { - writeValues(ctx) - }) - } - - iris.Post("set", func(ctx *iris.Context) { - vals := make(map[string]interface{}, 0) - if err := ctx.ReadJSON(&vals); err != nil { - t.Fatalf("Cannot readjson. Trace %s", err.Error()) - } - for k, v := range vals { - ctx.Session().Set(k, v) - } - }) - - iris.Get("/get", func(ctx *iris.Context) { - writeValues(ctx) - }) - - iris.Get("/clear", func(ctx *iris.Context) { - ctx.Session().Clear() - writeValues(ctx) - }) - - iris.Get("/destroy", func(ctx *iris.Context) { - ctx.SessionDestroy() - writeValues(ctx) - // the cookie and all values should be empty - }) - - // request cookie should be empty - iris.Get("/after_destroy", func(ctx *iris.Context) { - }) - iris.Default.Config.VHost = "mydomain.com" - e := httptest.New(iris.Default, t) - - e.POST("/set").WithJSON(values).Expect().Status(iris.StatusOK).Cookies().NotEmpty() - e.GET("/get").Expect().Status(iris.StatusOK).JSON().Object().Equal(values) - if testEnableSubdomain { - es := subdomainTester(e) - es.Request("GET", "/get").Expect().Status(iris.StatusOK).JSON().Object().Equal(values) - } - - // test destroy which also clears first - d := e.GET("/destroy").Expect().Status(iris.StatusOK) - d.JSON().Object().Empty() - // This removed: d.Cookies().Empty(). Reason: - // httpexpect counts the cookies setted or deleted at the response time, but cookie is not removed, to be really removed needs to SetExpire(now-1second) so, - // test if the cookies removed on the next request, like the browser's behavior. - e.GET("/after_destroy").Expect().Status(iris.StatusOK).Cookies().Empty() - // set and clear again - e.POST("/set").WithJSON(values).Expect().Status(iris.StatusOK).Cookies().NotEmpty() - e.GET("/clear").Expect().Status(iris.StatusOK).JSON().Object().Empty() -} - -type renderTestInformationType struct { - XMLName xml.Name `xml:"info"` - FirstAttr string `xml:"first,attr"` - SecondAttr string `xml:"second,attr"` - Name string `xml:"name",json:"name"` - Birth string `xml:"birth",json:"birth"` - Stars int `xml:"stars",json:"stars"` -} - -func TestContextRenderRest(t *testing.T) { - iris.ResetDefault() - - dataContents := []byte("Some binary data here.") - textContents := "Plain text here" - JSONPContents := map[string]string{"hello": "jsonp"} - JSONPCallback := "callbackName" - JSONXMLContents := renderTestInformationType{ - XMLName: xml.Name{Local: "info", Space: "info"}, // only need to verify that later - FirstAttr: "this is the first attr", - SecondAttr: "this is the second attr", - Name: "Iris web framework", - Birth: "13 March 2016", - Stars: 4064, - } - markdownContents := "# Hello dynamic markdown from Iris" - - iris.Get("/data", func(ctx *iris.Context) { - ctx.Data(iris.StatusOK, dataContents) - }) - - iris.Get("/text", func(ctx *iris.Context) { - ctx.Text(iris.StatusOK, textContents) - }) - - iris.Get("/jsonp", func(ctx *iris.Context) { - ctx.JSONP(iris.StatusOK, JSONPCallback, JSONPContents) - }) - - iris.Get("/json", func(ctx *iris.Context) { - ctx.JSON(iris.StatusOK, JSONXMLContents) - }) - iris.Get("/xml", func(ctx *iris.Context) { - ctx.XML(iris.StatusOK, JSONXMLContents) - }) - - iris.Get("/markdown", func(ctx *iris.Context) { - ctx.Markdown(iris.StatusOK, markdownContents) - }) - - e := httptest.New(iris.Default, t) - dataT := e.GET("/data").Expect().Status(iris.StatusOK) - dataT.Header("Content-Type").Equal("application/octet-stream") - dataT.Body().Equal(string(dataContents)) - - textT := e.GET("/text").Expect().Status(iris.StatusOK) - textT.Header("Content-Type").Equal("text/plain; charset=UTF-8") - textT.Body().Equal(textContents) - - JSONPT := e.GET("/jsonp").Expect().Status(iris.StatusOK) - JSONPT.Header("Content-Type").Equal("application/javascript; charset=UTF-8") - JSONPT.Body().Equal(JSONPCallback + `({"hello":"jsonp"});`) - - JSONT := e.GET("/json").Expect().Status(iris.StatusOK) - JSONT.Header("Content-Type").Equal("application/json; charset=UTF-8") - JSONT.JSON().Object().Equal(JSONXMLContents) - - XMLT := e.GET("/xml").Expect().Status(iris.StatusOK) - XMLT.Header("Content-Type").Equal("text/xml; charset=UTF-8") - XMLT.Body().Equal(`<` + JSONXMLContents.XMLName.Local + ` first="` + JSONXMLContents.FirstAttr + `" second="` + JSONXMLContents.SecondAttr + `">` + JSONXMLContents.Name + `` + JSONXMLContents.Birth + `` + strconv.Itoa(JSONXMLContents.Stars) + ``) - - markdownT := e.GET("/markdown").Expect().Status(iris.StatusOK) - markdownT.Header("Content-Type").Equal("text/html; charset=UTF-8") - markdownT.Body().Equal("

" + markdownContents[2:] + "

\n") -} - -func TestContextPreRender(t *testing.T) { - iris.ResetDefault() - - preRender := func(errMsg string, shouldContinue bool) iris.PreRender { - return func(ctx *iris.Context, - src string, - binding interface{}, - options ...map[string]interface{}) bool { - // put the 'Error' binding here, for the shake of the test - if b, isMap := binding.(map[string]interface{}); isMap { - msg := "" - if prevMsg := b["Error"]; prevMsg != nil { - // we have a previous message - msg += prevMsg.(string) - } - msg += errMsg - b["Error"] = msg - } - return shouldContinue - } - } - errMsg1 := "thereIsAnError" - errMsg2 := "thereIsASecondError" - errMsg3 := "thereisAThirdError" - // only errMsg1 and errMsg2 should be rendered because - // on errMsg2 we stop the execution - iris.UsePreRender(preRender(errMsg1, true)) - iris.UsePreRender(preRender(errMsg2, false)) - iris.UsePreRender(preRender(errMsg3, false)) // false doesn't matters here - - iris.Get("/", func(ctx *iris.Context) { - ctx.RenderTemplateSource(iris.StatusOK, "

HI {{.Username}}. Error: {{.Error}}

", map[string]interface{}{"Username": "kataras"}) - }) - - e := httptest.New(iris.Default, t) - expected := "

HI kataras. Error: " + errMsg1 + errMsg2 + "

" - e.GET("/").Expect().Status(iris.StatusOK).Body().Contains(expected) -} - -func TestTemplatesDisabled(t *testing.T) { - iris.ResetDefault() - defer iris.Close() - - iris.Default.Config.DisableTemplateEngines = true - - file := "index.html" - errTmpl := "

Template: %s

%s" - expctedErrMsg := fmt.Sprintf(errTmpl, file, "Error: Unable to execute a template. Trace: Templates are disabled '.Config.DisableTemplatesEngines = true' please turn that to false, as defaulted.\n") - - iris.Get("/renderErr", func(ctx *iris.Context) { - ctx.MustRender(file, nil) - }) - - e := httptest.New(iris.Default, t) - e.GET("/renderErr").Expect().Status(iris.StatusServiceUnavailable).Body().Equal(expctedErrMsg) -} - -func TestTransactions(t *testing.T) { - iris.ResetDefault() - firstTransactionFailureMessage := "Error: Virtual failure!!!" - secondTransactionSuccessHTMLMessage := "

This will sent at all cases because it lives on different transaction and it doesn't fails

" - persistMessage := "

I persist show this message to the client!

" - - maybeFailureTransaction := func(shouldFail bool, isRequestScoped bool) func(t *iris.Transaction) { - return func(t *iris.Transaction) { - // OPTIONAl, the next transactions and the flow will not be skipped if this transaction fails - if isRequestScoped { - t.SetScope(iris.RequestTransactionScope) - } - - // OPTIONAL STEP: - // create a new custom type of error here to keep track of the status code and reason message - err := iris.NewTransactionErrResult() - - t.Context.Text(iris.StatusOK, "Blablabla this should not be sent to the client because we will fill the err with a message and status") - - fail := shouldFail - - if fail { - err.StatusCode = iris.StatusInternalServerError - err.Reason = firstTransactionFailureMessage - } - - // OPTIONAl STEP: - // but useful if we want to post back an error message to the client if the transaction failed. - // if the reason is empty then the transaction completed successfully, - // otherwise we rollback the whole response body and cookies and everything lives inside the transaction.Request. - t.Complete(err) - } - } - - successTransaction := func(scope *iris.Transaction) { - if scope.Context.Request.RequestURI == "/failAllBecauseOfRequestScopeAndFailure" { - t.Fatalf("We are inside successTransaction but the previous REQUEST SCOPED TRANSACTION HAS FAILED SO THiS SHOULD NOT BE RAN AT ALL") - } - scope.Context.HTML(iris.StatusOK, - secondTransactionSuccessHTMLMessage) - // * if we don't have any 'throw error' logic then no need of scope.Complete() - } - - persistMessageHandler := func(ctx *iris.Context) { - // OPTIONAL, depends on the usage: - // at any case, what ever happens inside the context's transactions send this to the client - ctx.HTML(iris.StatusOK, persistMessage) - } - - iris.Get("/failFirsTransactionButSuccessSecondWithPersistMessage", func(ctx *iris.Context) { - ctx.BeginTransaction(maybeFailureTransaction(true, false)) - ctx.BeginTransaction(successTransaction) - persistMessageHandler(ctx) - }) - - iris.Get("/failFirsTransactionButSuccessSecond", func(ctx *iris.Context) { - ctx.BeginTransaction(maybeFailureTransaction(true, false)) - ctx.BeginTransaction(successTransaction) - }) - - iris.Get("/failAllBecauseOfRequestScopeAndFailure", func(ctx *iris.Context) { - ctx.BeginTransaction(maybeFailureTransaction(true, true)) - ctx.BeginTransaction(successTransaction) - }) - - customErrorTemplateText := "

custom error

" - iris.OnError(iris.StatusInternalServerError, func(ctx *iris.Context) { - ctx.Text(iris.StatusInternalServerError, customErrorTemplateText) - }) - - failureWithRegisteredErrorHandler := func(ctx *iris.Context) { - ctx.BeginTransaction(func(transaction *iris.Transaction) { - transaction.SetScope(iris.RequestTransactionScope) - err := iris.NewTransactionErrResult() - err.StatusCode = iris.StatusInternalServerError // set only the status code in order to execute the registered template - transaction.Complete(err) - }) - - ctx.Text(iris.StatusOK, "this will not be sent to the client because first is requested scope and it's failed") - } - - iris.Get("/failAllBecauseFirstTransactionFailedWithRegisteredErrorTemplate", failureWithRegisteredErrorHandler) - - e := httptest.New(iris.Default, t) - - e.GET("/failFirsTransactionButSuccessSecondWithPersistMessage"). - Expect(). - Status(iris.StatusOK). - ContentType("text/html", iris.Config.Charset). - Body(). - Equal(secondTransactionSuccessHTMLMessage + persistMessage) - - e.GET("/failFirsTransactionButSuccessSecond"). - Expect(). - Status(iris.StatusOK). - ContentType("text/html", iris.Config.Charset). - Body(). - Equal(secondTransactionSuccessHTMLMessage) - - e.GET("/failAllBecauseOfRequestScopeAndFailure"). - Expect(). - Status(iris.StatusInternalServerError). - Body(). - Equal(firstTransactionFailureMessage) - - e.GET("/failAllBecauseFirstTransactionFailedWithRegisteredErrorTemplate"). - Expect(). - Status(iris.StatusInternalServerError). - Body(). - Equal(customErrorTemplateText) -} - -func TestLimitRequestBodySize(t *testing.T) { - const maxBodySize = 1 << 20 - - api := iris.New() - - // or inside handler: ctx.SetMaxRequestBodySize(int64(maxBodySize)) - api.Use(iris.LimitRequestBodySize(maxBodySize)) - - api.Post("/", func(ctx *iris.Context) { - b, err := ioutil.ReadAll(ctx.Request.Body) - if len(b) > maxBodySize { - // this is a fatal error it should never happened. - t.Fatalf("body is larger (%d) than maxBodySize (%d) even if we add the LimitRequestBodySize middleware", len(b), maxBodySize) - } - // if is larger then send a bad request status - if err != nil { - ctx.WriteHeader(iris.StatusBadRequest) - ctx.Writef(err.Error()) - return - } - - ctx.Write(b) - }) - - // UseGlobal should be called at the end used to prepend handlers - // api.UseGlobal(iris.LimitRequestBodySize(int64(maxBodySize))) - - e := httptest.New(api, t) - - // test with small body - e.POST("/").WithBytes([]byte("ok")).Expect().Status(iris.StatusOK).Body().Equal("ok") - // test with equal to max body size limit - bsent := make([]byte, maxBodySize, maxBodySize) - e.POST("/").WithBytes(bsent).Expect().Status(iris.StatusOK).Body().Length().Equal(len(bsent)) - // test with larger body sent and wait for the custom response - largerBSent := make([]byte, maxBodySize+1, maxBodySize+1) - e.POST("/").WithBytes(largerBSent).Expect().Status(iris.StatusBadRequest).Body().Equal("http: request body too large") - -} diff --git a/doc.go b/doc.go new file mode 100644 index 00000000..35bf832d --- /dev/null +++ b/doc.go @@ -0,0 +1,521 @@ +// Copyright (c) 2016-2017 Gerasimos Maropoulos +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +/* +Iris back-end web framework provides efficient and well-designed toolbox with robust set of features to +create your own perfect high performance web application +with unlimited portability using the Go Programming Language. + +Note: This package is under active development status. +Each month a new version is releasing +to adapt the latest web trends and technologies. + +Basic HTTP API. + +Iris is a very pluggable ecosystem, +router can be customized by adapting a 'RouterBuilderPolicy && RouterReversionPolicy'. + +By adapted a router users are able to use router's features on the route's Path, +the rest of the HTTP API and Context's calls remains the same for all routers, as expected. + +- httprouter, it's a custom version of https://github.comjulienschmidt/httprouter, + which is edited to support iris' subdomains, reverse routing, custom http errors and a lot features, + it should be a bit faster than the original too because of iris' Context. + It uses `/mypath/:firstParameter/path/:secondParameter` and `/mypath/*wildcardParamName` . + +- gorillamuxa, it's the https://github.com/gorilla/mux which supports subdomains, + custom http errors, reverse routing, pattern matching via regex and the rest of the iris' features. + It uses `/mypath/{firstParameter:any-regex-valid-here}/path/{secondParameter}` and `/mypath/{wildcardParamName:.*}` + +Example code: + + + package main + + import ( + "gopkg.in/kataras/iris.v6" + "gopkg.in/kataras/iris.v6/adaptors/httprouter" // <--- or adaptors/gorillamux + ) + + func main(){ + app := iris.New() + app.Adapt(httprouter.New()) // <--- or gorillamux.New() + + // HTTP Method: GET + // PATH: http://127.0.0.1/ + // Handler(s): index + app.Get("/", index) + + + app.Listen(":80") + } + + func index(ctx *iris.Context){ + ctx.HTML(iris.StatusOK, "

Welcome to my page!

") + } + + +All HTTP methods are supported, users can register handlers for same paths on different methods. +The first parameter is the HTTP Method, +second parameter is the request path of the route, +third variadic parameter should contains one or more iris.Handler/HandlerFunc executed +by the registered order when a user requests for that specific resouce path from the server. + +Example code: + + + app := iris.New() + + app.Handle("GET", "/about", aboutHandler) + + type aboutHandler struct {} + func (a aboutHandler) Serve(ctx *iris.Context){ + ctx.HTML("Hello from /about, executed from an iris.Handler") + } + + app.HandleFunc("GET", "/contact", func(ctx *iris.Context){ + ctx.HTML(iris.StatusOK, "Hello from /contact, executed from an iris.HandlerFunc") + }) + + +In order to make things easier for the user, Iris provides functions for all HTTP Methods. +The first parameter is the request path of the route, +second variadic parameter should contains one or more iris.HandlerFunc executed +by the registered order when a user requests for that specific resouce path from the server. + +Example code: + + + app := iris.New() + + // Method: "GET" + app.Get("/", handler) + + // Method: "POST" + app.Post("/", handler) + + // Method: "PUT" + app.Put("/", handler) + + // Method: "DELETE" + app.Delete("/", handler) + + // Method: "OPTIONS" + app.Options("/", handler) + + // Method: "TRACE" + app.Trace("/", handler) + + // Method: "CONNECT" + app.Connect("/", handler) + + // Method: "HEAD" + app.Head("/", handler) + + // Method: "PATCH" + app.Patch("/", handler) + + // register the route for all HTTP Methods + app.Any("/", handler) + + func handler(ctx *iris.Context){ + ctx.Writef("Hello from method: %s and path: %s", ctx.Method(), ctx.Path()) + } + + +Parameterized route's Path, depends on the selected router. + +Note: This is the only difference between the routers, the registered path form, the API remains the same for both. + +Example `gorillamux` code: + + + package main + + import ( + "gopkg.in/kataras/iris.v6" + "gopkg.in/kataras/iris.v6/adaptors/gorillamux" + ) + + func main() { + app := iris.New() + app.Adapt(iris.DevLogger()) + app.Adapt(gorillamux.New()) + + + app.OnError(iris.StatusNotFound, func(ctx *iris.Context){ + ctx.HTML(iris.StatusNotFound, "

custom http error page

") + }) + + + app.Get("/healthcheck", h) + + gamesMiddleware := func(ctx *iris.Context) { + println(ctx.Method() + ": " + ctx.Path()) + ctx.Next() + } + + games:= app.Party("/games", gamesMiddleware) + { // braces are optional of course, it's just a style of code + games.Get("/{gameID:[0-9]+}/clans", h) + games.Get("/{gameID:[0-9]+}/clans/clan/{publicID:[0-9]+}", h) + games.Get("/{gameID:[0-9]+}/clans/search", h) + + games.Put("/{gameID:[0-9]+}/players/{publicID:[0-9]+}", h) + games.Put("/{gameID:[0-9]+}/clans/clan/{publicID:[0-9]+}", h) + + games.Post("/{gameID:[0-9]+}/clans", h) + games.Post("/{gameID:[0-9]+}/players", h) + games.Post("/{gameID:[0-9]+}/clans/{publicID:[0-9]+}/leave", h) + games.Post("/{gameID:[0-9]+}/clans/{clanPublicID:[0-9]+}/memberships/application", h) + games.Post("/{gameID:[0-9]+}/clans/{clanPublicID:[0-9]+}/memberships/application/:action", h) + games.Post("/{gameID:[0-9]+}/clans/{clanPublicID:[0-9]+}/memberships/invitation", h) + games.Post("/{gameID:[0-9]+}/clans/{clanPublicID:[0-9]+}/memberships/invitation/:action", h) + games.Post("/{gameID:[0-9]+}/clans/{clanPublicID:[0-9]+}/memberships/delete", h) + games.Post("/{gameID:[0-9]+}/clans/{clanPublicID:[0-9]+}/memberships/promote", h) + games.Post("/{gameID:[0-9]+}/clans/{clanPublicID:[0-9]+}/memberships/demote", h) + } + + app.Get("/anything/{anythingparameter:.*}", func(ctx *iris.Context){ + s := ctx.Param("anythingparameter") + ctx.Writef("The path after /anything is: %s",s) + }) + + mysubdomain:= app.Party("mysubdomain.") + // http://mysubdomain.myhost.com/ + mysudomain.Get("/", h) + + app.Listen("myhost.com:80") + } + + func h(ctx *iris.Context) { + ctx.HTML(iris.StatusOK, "

Path

"+ctx.Path()) + } + + +Example `httprouter` code: + + + package main + + import ( + "gopkg.in/kataras/iris.v6" + "gopkg.in/kataras/iris.v6/adaptors/httprouter" // <---- NEW + ) + + func main() { + app := iris.New() + app.Adapt(iris.DevLogger()) + app.Adapt(httprouter.New()) // <---- NEW + + + app.OnError(iris.StatusNotFound, func(ctx *iris.Context){ + ctx.HTML(iris.StatusNotFound, "

custom http error page

") + }) + + + app.Get("/healthcheck", h) + + gamesMiddleware := func(ctx *iris.Context) { + println(ctx.Method() + ": " + ctx.Path()) + ctx.Next() + } + + games:= app.Party("/games", gamesMiddleware) + { // braces are optional of course, it's just a style of code + games.Get("/:gameID/clans", h) + games.Get("/:gameID/clans/clan/:publicID", h) + games.Get("/:gameID/clans/search", h) + + games.Put("/:gameID/players/:publicID", h) + games.Put("/:gameID/clans/clan/:publicID", h) + + games.Post("/:gameID/clans", h) + games.Post("/:gameID/players", h) + games.Post("/:gameID/clans/:publicID/leave", h) + games.Post("/:gameID/clans/:clanPublicID/memberships/application", h) + games.Post("/:gameID/clans/:clanPublicID/memberships/application/:action", h) + games.Post("/:gameID/clans/:clanPublicID/memberships/invitation", h) + games.Post("/:gameID/clans/:clanPublicID/memberships/invitation/:action", h) + games.Post("/:gameID/clans/:clanPublicID/memberships/delete", h) + games.Post("/:gameID/clans/:clanPublicID/memberships/promote", h) + games.Post("/:gameID/clans/:clanPublicID/memberships/demote", h) + } + + app.Get("/anything/*anythingparameter", func(ctx *iris.Context){ + s := ctx.Param("anythingparameter") + ctx.Writef("The path after /anything is: %s",s) + }) + + mysubdomain:= app.Party("mysubdomain.") + // http://mysubdomain.myhost.com/ + mysudomain.Get("/", h) + + app.Listen("myhost.com:80") + } + + func h(ctx *iris.Context) { + ctx.HTML(iris.StatusOK, "

Path

"+ctx.Path()) + } + +Grouping routes that can (optionally) share the same middleware handlers, template layout and path prefix. + +Example code: + + + users:= app.Party("/users", myAuthHandler) + + // http://myhost.com/users/42/profile + users.Get("/:userid/profile", userProfileHandler) // httprouter path parameters + // http://myhost.com/users/messages/1 + users.Get("/inbox/:messageid", userMessageHandler) + + app.Listen("myhost.com:80") + + +Custom HTTP Errors page + +With iris users are able to register their own handlers for http statuses like 404 not found, 500 internal server error and so on. + +Example code: + + // when 404 then render the template $templatedir/errors/404.html + // *read below for information about the view engine.* + app.OnError(iris.StatusNotFound, func(ctx *iris.Context){ + ctx.RenderWithstatus(iris.StatusNotFound, "errors/404.html", nil) + }) + + app.OnError(500, func(ctx *iris.Context){ + // ... + }) + + +Custom http errors can be also be registered to a specific group of routes. + +Example code: + + + games:= app.Party("/games", gamesMiddleware) + { + games.Get("/{gameID:[0-9]+}/clans", h) // gorillamux path parameters + games.Get("/{gameID:[0-9]+}/clans/clan/{publicID:[0-9]+}", h) + games.Get("/{gameID:[0-9]+}/clans/search", h) + } + + games.OnError(iris.StatusNotFound, gamesNotFoundHandler) + + +Middleware ecosystem. + +Middleware is just a concept of ordered chain of handlers. +Middleware can be registered globally, per-party, per-subdomain and per-route. + + +Example code: + + // globally + // before any routes, appends the middleware to all routes + app.UseFunc(func(ctx *iris.Context){ + // ... any code here + + ctx.Next() // in order to continue to the next handler, + // if that is missing then the next in chain handlers will be not executed, + // useful for authentication middleware + }) + + // globally + // after or before any routes, prepends the middleware to all routes + app.UseGlobalFunc(handlerFunc1, handlerFunc2, handlerFunc3) + + // per-route + app.Post("/login", authenticationHandler, loginPageHandler) + + // per-party(group of routes) + users := app.Party("/users", usersMiddleware) + users.Get("/", usersIndex) + + // per-subdomain + mysubdomain := app.Party("mysubdomain.", firstMiddleware) + mysubdomain.UseFunc(secondMiddleware) + mysubdomain.Get("/", mysubdomainIndex) + + // per wildcard, dynamic subdomain + dynamicSub := app.Party(".*", firstMiddleware, secondMiddleware) + dynamicSub.Get("/", func(ctx *iris.Context){ + ctx.Writef("Hello from subdomain: "+ ctx.Subdomain()) + }) + + +`iris.ToHandler` converts(by wrapping) any `http.Handler/HandlerFunc` or +`func(w http.ResponseWriter,r *http.Request, next http.HandlerFunc)` to an `iris.HandlerFunc`. + +iris.ToHandler(nativeNethttpHandler) + +Let's convert the https://github.com/rs/cors net/http external middleware which returns a `next form` handler. + + +Example code: + + package main + + import ( + "gopkg.in/kataras/iris.v6" + "github.com/kataras/adaptors/gorillamux" + "github.com/rs/cors" + ) + + // myCors returns a new cors middleware + // with the provided options. + myCors := func(opts cors.Options) iris.HandlerFunc { + handlerWithNext := cors.New(opts).ServeHTTP + + // this is the only func you will have to use if you're going to make use of any external net/http middleware. + // iris.ToHandler converts the net/http middleware to an iris-compatible. + return iris.ToHandler(handlerWithNext) + } + + func main(){ + app := iris.New() + app.Adapt(httprouter.New()) + + // Any registers a route to all http methods. + app.Any("/user", myCors(cors.Options{AllowOrigins: "*"}), func(ctx *iris.Context){ + // .... + }) + + app.Listen(":8080") + } + + +Visit https://godoc.org/github.com/kataras/iris#Router for more. + + +View engine, supports 5 template engines, developers can still use any external golang template engine, +as `context.ResponseWriter` is an `io.Writer`. + +All of these five template engines have common features with common API, +like Layout, Template Funcs, Party-specific layout, partial rendering and more. + + - the standard html, based on https://github.com/kataras/go-template/tree/master/html + its template parser is the https://golang.org/pkg/html/template/. + + - django, based on https://github.com/kataras/go-template/tree/master/django + its template parser is the https://github.com/flosch/pongo2 + + - pug, based on https://github.com/kataras/go-template/tree/master/pug + its template parser is the https://github.com/Joker/jade + + - handlebars, based on https://github.com/kataras/go-template/tree/master/handlebars + its template parser is the https://github.com/aymerick/raymond + + - amber, based on https://github.com/kataras/go-template/tree/master/amber + its template parser is the https://github.com/eknkc/amber + +Each one of these template engines has different options, +view adaptors are located here: https://github.com/kataras/iris/tree/master/adaptors/view . + +Example code: + + package main + + import ( + "gopkg.in/kataras/iris.v6" + "gopkg.in/kataras/iris.v6/adaptors/gorillamux" + "gopkg.in/kataras/iris.v6/adaptors/view" // <--- it contains all the template engines + ) + + func main() { + app := iris.New(iris.Configuration{Gzip: false, Charset: "UTF-8"}) // defaults to these + + app.Adapt(iris.DevLogger()) + app.Adapt(gorillamux.New()) + + // - standard html | view.HTML(...) + // - django | view.Django(...) + // - pug(jade) | view.Pug(...) + // - handlebars | view.Handlebars(...) + // - amber | view.Amber(...) + app.Adapt(view.HTML("./templates", ".html")) // <---- use the standard html + + // default template funcs: + // + // - {{ url "mynamedroute" "pathParameter_ifneeded"} } + // - {{ urlpath "mynamedroute" "pathParameter_ifneeded" }} + // - {{ render "header.html" }} + // - {{ render_r "header.html" }} // partial relative path to current page + // - {{ yield }} + // - {{ current }} + // + // to adapt custom funcs, use: + app.Adapt(iris.TemplateFuncsPolicy{"myfunc": func(s string) string { + return "hi "+s + }}) // usage inside template: {{ hi "kataras"}} + + app.Get("/hi", func(ctx *iris.Context) { + ctx.Render( + // the file name of the template relative to the './templates'. + "hi.html", + iris.Map{"Name": "Iris"}, + // the .Name inside the ./templates/hi.html, + // you can use any custom struct that you want to bind to the requested template. + iris.Map{"gzip": false}, // set to true to enable gzip compression. + ) + + }) + + // http://127.0.0.1:8080/hi + app.Listen(":8080") + } + +View engine supports bundled(https://github.com/jteeuwen/go-bindata) template files too. +go-bindata gives you two functions, asset and assetNames, +these can be setted to each of the template engines using the `.Binary` func. + +Example code: + + djangoEngine := view.Django("./templates", ".html") + djangoEngine.Binary(asset, assetNames) + app.Adapt(djangoEngine) + +A real example can be found here: https://github.com/kataras/iris/tree/v6/adaptors/view/_examples/template_binary . + +Enable auto-reloading of templates on each request. Useful while users are in dev mode +because they don't have to restart their app on every edit you make on the template files. + +Example code: + + + pugEngine := view.Pug("./templates", ".jade") + pugEngine.Reload(true) // <--- set to true to re-build the templates on each request. + app.Adapt(pugEngine) + + +You should have a basic idea of the framework by now, we just scratched the surface. +If you enjoy what you just saw and want to learn more, please follow the below links: + + - examples: https://github.com/iris-contrib/examples + - book: https://docs.iris-go.com + - adaptors: https://github.com/kataras/iris/tree/v6/adaptors + - middleware: https://github.com/kataras/iris/tree/v6/middleware & https://github.com/iris-contrib/middleware + - godocs: https://godoc.org/github.com/kataras/iris + + +*/ +package iris // import "gopkg.in/kataras/iris.v6" diff --git a/docs/QUICK_START.md b/docs/QUICK_START.md deleted file mode 100644 index 0bd867fd..00000000 --- a/docs/QUICK_START.md +++ /dev/null @@ -1,865 +0,0 @@ -Quick Start ------------ - -```bash -go get -u github.com/kataras/iris/iris -``` - -```sh -cat app.go -``` - -```go -package iris_test - -import ( - "github.com/kataras/go-template/html" - "github.com/kataras/iris" -) - -func main() { - app := iris.New() - // 6 template engines are supported out-of-the-box: - // - // - standard html/template - // - amber - // - django - // - handlebars - // - pug(jade) - // - markdown - // - // Use the html standard engine for all files inside "./views" folder with extension ".html" - // Defaults to: - app.UseTemplate(html.New()).Directory("./views", ".html") - - // http://localhost:6111 - // Method: "GET" - // Render ./views/index.html - app.Get("/", func(ctx *iris.Context) { - ctx.Render("index.html", nil) - }) - - // Group routes, optionally: share middleware, template layout and custom http errors. - userAPI := app.Party("/users", userAPIMiddleware). - Layout("layouts/userLayout.html") - { - // Fire userNotFoundHandler when Not Found - // inside http://localhost:6111/users/*anything - userAPI.OnError(404, userNotFoundHandler) - - // http://localhost:6111/users - // Method: "GET" - userAPI.Get("/", getAllHandler) - - // http://localhost:6111/users/42 - // Method: "GET" - userAPI.Get("/:id", getByIDHandler) - - // http://localhost:6111/users - // Method: "POST" - userAPI.Post("/", saveUserHandler) - } - - // Start the server at 0.0.0.0:6111 - app.Listen(":6111") -} - -func getByIDHandler(ctx *iris.Context) { - // take the :id from the path, parse to integer - // and set it to the new userID local variable. - userID, _ := ctx.ParamInt("id") - - // userRepo, imaginary database service <- your only job. - user := userRepo.GetByID(userID) - - // send back a response to the client, - // .JSON: content type as application/json; charset="utf-8" - // iris.StatusOK: with 200 http status code. - // - // send user as it is or make use of any json valid golang type, - // like the iris.Map{"username" : user.Username}. - ctx.JSON(iris.StatusOK, user) -} - -``` - -> TIP: $ iris run main.go to enable hot-reload on .go source code changes. - -> TIP: iris.Config.IsDevelopment = true to monitor the changes you make in the templates. - -> TIP: Want to change the default Router's behavior to something else like Gorilla's Mux? -Go [there](https://github.com/iris-contrib/examples/tree/master/plugin_gorillamux) to learn how. - - -### New - -```go -// New with default configuration -app := iris.New() - -app.Listen(....) - -// New with configuration struct -app := iris.New(iris.Configuration{ IsDevelopment: true}) - -app.Listen(...) - -// Default station -iris.Listen(...) - -// Default station with custom configuration -// view the whole configuration at: ./configuration.go -iris.Config.IsDevelopment = true -iris.Config.Charset = "UTF-8" - -iris.Listen(...) -``` - -### Listening -`Serve(ln net.Listener) error` -```go -ln, err := net.Listen("tcp4", ":8080") -if err := iris.Serve(ln); err != nil { - panic(err) -} -``` -`Listen(addr string)` -```go -iris.Listen(":8080") -``` -`ListenTLS(addr string, certFile, keyFile string)` -```go -iris.ListenTLS(":8080", "./ssl/mycert.cert", "./ssl/mykey.key") -``` -`ListenLETSENCRYPT(addr string, cacheFileOptional ...string)` -```go -iris.ListenLETSENCRYPT("mydomain.com") -``` -```go -iris.Serve(iris.LETSENCRYPTPROD("myproductionwebsite.com")) -``` - -And - -```go -ListenUNIX(addr string, mode os.FileMode) -Close() error -Reserve() error -IsRunning() bool -``` - -### Routing - -```go -iris.Get("/products/:id", getProduct) -iris.Post("/products", saveProduct) -iris.Put("products/:id", editProduct) -iris.Delete("/products/:id", deleteProduct) -``` - -And - -```go -iris.Patch("", ...) -iris.Connect("", ...) -iris.Options("", ...) -iris.Trace("", ...) -``` - -### Path Parameters - -```go -func getProduct(ctx *iris.Context){ - // Get id from path '/products/:id' - id := ctx.Param("id") -} - -``` - -### Query Parameters - -`/details?color=blue&weight=20` - -```go -func details(ctx *iris.Context){ - color := ctx.URLParam("color") - weight,_ := ctx.URLParamInt("weight") -} - -``` - -### Form `application/x-www-form-urlencoded` - -`METHOD: POST | PATH: /save` - -name | value -:--- | :--- -name | Gerasimos Maropoulos -email | kataras2006@homail.com - - -```go -func save(ctx *iris.Context) { - // Get name and email - name := ctx.FormValue("name") - email := ctx.FormValue("email") -} -``` - -### Form `multipart/form-data` - -`POST` `/save` - -name | value -:--- | :--- -name | Gerasimos Maropoulos -email | kataras2006@hotmail.com -avatar | avatar - -```go -func save(ctx *iris.Context) { - // Get name and email - name := ctx.FormValue("name") - email := ctx.FormValue("email") - // Get avatar - avatar, info, err := ctx.FormFile("avatar") - if err != nil { - ctx.EmitError(iris.StatusInternalServerError) - return - } - - defer avatar.Close() - - // Destination - dst, err := os.Create(avatar.Filename) - if err != nil { - ctx.EmitError(iris.StatusInternalServerError) - return - } - defer dst.Close() - - // Copy - if _, err = io.Copy(dst, avatar); err != nil { - ctx.EmitError(iris.StatusInternalServerError) - return - } - - ctx.HTML(iris.StatusOK, "Thanks!") -} -``` - -### Handling Request - - -- Bind `JSON` or `XML` or `form` payload into Go struct based on `Content-Type` request header. -- Render response as `JSON` or `XML` with status code. - -```go -type User struct { - Name string `json:"name" xml:"name" form:"name"` - Email string `json:"email" xml:"email" form:"email"` -} - -iris.Post("/users", func(ctx *iris.Context) { - u := new(User) - if err := ctx.ReadJSON(u); err != nil { - ctx.EmitError(iris.StatusInternalServerError) - return - } - ctx.JSON(iris.StatusCreated, u) - // or - // ctx.XML(iris.StatusCreated, u) - // ctx.JSONP(...) - // ctx.HTML(iris.StatusCreated, "Hi "+u.Name+"") - // ctx.Markdown(iris.StatusCreated, "## Name: "+u.Name) -}) -``` - - -| Name | Description | Usage | -| ------------------|:---------------------:|-------:| -| [JSON ](https://github.com/kataras/go-serializer/tree/master/json) | JSON Serializer (Default) |[example 1](https://github.com/iris-contrib/examples/blob/master/serialize_engines/json_1/main.go),[example 2](https://github.com/iris-contrib/examples/blob/master/serialize_engines/json_2/main.go), [book section](https://docs.iris-go.com/serialize-engines.html) -| [JSONP ](https://github.com/kataras/go-serializer/tree/master/jsonp) | JSONP Serializer (Default) |[example 1](https://github.com/iris-contrib/examples/blob/master/serialize_engines/jsonp_1/main.go),[example 2](https://github.com/iris-contrib/examples/blob/master/serialize_engines/jsonp_2/main.go), [book section](https://docs.iris-go.com/serialize-engines.html) -| [XML ](https://github.com/kataras/go-serializer/tree/master/xml) | XML Serializer (Default) |[example 1](https://github.com/iris-contrib/examples/blob/master/serialize_engines/xml_1/main.go),[example 2](https://github.com/iris-contrib/examples/blob/master/serialize_engines/xml_2/main.go), [book section](https://docs.iris-go.com/serialize-engines.html) -| [Markdown ](https://github.com/kataras/go-serializer/tree/master/markdown) | Markdown Serializer (Default) |[example 1](https://github.com/iris-contrib/examples/blob/master/serialize_engines/markdown_1/main.go),[example 2](https://github.com/iris-contrib/examples/blob/master/serialize_engines/markdown_2/main.go), [book section](https://docs.iris-go.com/serialize-engines.html) -| [Text](https://github.com/kataras/go-serializer/tree/master/text) | Text Serializer (Default) |[example 1](https://github.com/iris-contrib/examples/blob/master/serialize_engines/text_1/main.go), [book section](https://docs.iris-go.com/serialize-engines.html) -| [Binary Data ](https://github.com/kataras/go-serializer/tree/master/data) | Binary Data Serializer (Default) |[example 1](https://github.com/iris-contrib/examples/blob/master/serialize_engines/data_1/main.go), [book section](https://docs.iris-go.com/serialize-engines.html) - - -### HTTP Errors - -You can define your own handlers when http error occurs. - -```go -package main - -import ( - "github.com/kataras/iris" -) - -func main() { - - iris.OnError(iris.StatusInternalServerError, func(ctx *iris.Context) { - ctx.Writef("CUSTOM 500 INTERNAL SERVER ERROR PAGE") - // or ctx.Render, ctx.HTML any render method you want - ctx.Log("http status: 500 happened!") - }) - - iris.OnError(iris.StatusNotFound, func(ctx *iris.Context) { - ctx.Writef("CUSTOM 404 NOT FOUND ERROR PAGE") - ctx.Log("http status: 404 happened!") - }) - - // emit the errors to test them - iris.Get("/500", func(ctx *iris.Context) { - ctx.EmitError(iris.StatusInternalServerError) // ctx.Panic() - }) - - iris.Get("/404", func(ctx *iris.Context) { - ctx.EmitError(iris.StatusNotFound) // ctx.NotFound() - }) - - iris.Listen(":80") - -} - - -``` - -### Static Content - -Serve files or directories, use the correct for your case, if you don't know which one, just use the `StaticWeb(reqPath string, systemPath string)`. - -```go -// Favicon serves static favicon -// accepts 2 parameters, second is optional -// favPath (string), declare the system directory path of the __.ico -// requestPath (string), it's the route's path, by default this is the "/favicon.ico" because some browsers tries to get this by default first, -// you can declare your own path if you have more than one favicon (desktop, mobile and so on) -// -// this func will add a route for you which will static serve the /yuorpath/yourfile.ico to the /yourfile.ico (nothing special that you can't handle by yourself) -// Note that you have to call it on every favicon you have to serve automatically (desktop, mobile and so on) -// -// panics on error -Favicon(favPath string, requestPath ...string) RouteNameFunc - -// StaticHandler returns a new Handler which serves static files -StaticHandler(reqPath string, systemPath string, showList bool, enableGzip bool) HandlerFunc - -// StaticWeb same as Static but if index.html e -// xists and request uri is '/' then display the index.html's contents -// accepts three parameters -// first parameter is the request url path (string) -// second parameter is the system directory (string) -StaticWeb(reqPath string, systemPath string) RouteNameFunc - -// StaticEmbedded used when files are distributed inside the app executable, using go-bindata mostly -// First parameter is the request path, the path which the files in the vdir will be served to, for example "/static" -// Second parameter is the (virtual) directory path, for example "./assets" -// Third parameter is the Asset function -// Forth parameter is the AssetNames function -// -// For more take a look at the -// example: https://github.com/iris-contrib/examples/tree/master/static_files_embedded -StaticEmbedded(requestPath string, vdir string, assetFn func(name string) ([]byte, error), namesFn func() []string) RouteNameFunc - -// StaticContent serves bytes, memory cached, on the reqPath -// a good example of this is how the websocket server uses that to auto-register the /iris-ws.js -StaticContent(reqPath string, cType string, content []byte) RouteNameFunc - -// StaticServe serves a directory as web resource -// it's the simpliest form of the Static* functions -// Almost same usage as StaticWeb -// accepts only one required parameter which is the systemPath -// (the same path will be used to register the GET&HEAD routes) -// if the second parameter is empty, otherwise the requestPath is the second parameter -// it uses gzip compression (compression on each request, no file cache) -StaticServe(systemPath string, requestPath ...string) - -``` - -```go -iris.StaticWeb("/public", "./static/assets/") -//-> /public/assets/favicon.ico -``` - -```go -iris.StaticWeb("/","./my_static_html_website") -``` - -```go -context.StaticServe(systemPath string, requestPath ...string) -``` - -#### Manual static file serving - -```go -// ServeFile serves a view file, to send a file -// to the client you should use the SendFile(serverfilename,clientfilename) -// receives two parameters -// filename/path (string) -// gzipCompression (bool) -// -// You can define your own "Content-Type" header also, after this function call -context.ServeFile(filename string, gzipCompression bool) error -``` - -Serve static individual file - -```go - -iris.Get("/txt", func(ctx *iris.Context) { - ctx.ServeFile("./myfolder/staticfile.txt", false) -} -``` - -### Templates - -**HTML Template Engine, defaulted** - - -```html - - - - -Hi Iris - - -

Hi {{.Name}} - - -``` - -```go -// file ./main.go -package main - -import "github.com/kataras/iris" - -func main() { - iris.Config.IsDevelopment = true // this will reload the templates on each request - iris.Get("/hi", hi) - iris.Listen(":8080") -} - -func hi(ctx *iris.Context) { - ctx.MustRender("hi.html", struct{ Name string }{Name: "iris"}) -} - -``` - -| Name | Description | Usage | -| ------------------|:---------------------:|-------:| -| [HTML/Default Engine ](https://github.com/kataras/go-template/tree/master/html) | HTML Template Engine (Default) |[example ](https://github.com/iris-contrib/examples/blob/master/template_engines/template_html_0/main.go), [book section](https://docs.iris-go.com/template-engines.html) -| [Django Engine ](https://github.com/kataras/go-template/tree/master/django) | Django Template Engine |[example ](https://github.com/iris-contrib/examples/blob/master/template_engines/template_django_1/main.go), [book section](https://docs.iris-go.com/template-engines.html) -| [Pug/Jade Engine ](https://github.com/kataras/go-template/tree/master/pug) | Pug Template Engine |[example ](https://github.com/iris-contrib/examples/blob/master/template_engines/template_pug_1/main.go), [book section](https://docs.iris-go.com/template-engines.html) -| [Handlebars Engine ](https://github.com/kataras/go-template/tree/master/handlebars) | Handlebars Template Engine |[example ](https://github.com/iris-contrib/examples/blob/master/template_engines/template_handlebars_1/main.go), [book section](https://docs.iris-go.com/template-engines.html) -| [Amber Engine ](https://github.com/kataras/go-template/tree/master/amber) | Amber Template Engine |[example ](https://github.com/iris-contrib/examples/blob/master/template_engines/template_amber_1/main.go), [book section](https://docs.iris-go.com/template-engines.html) -| [Markdown Engine ](https://github.com/kataras/go-template/tree/master/markdown) | Markdown Template Engine |[example ](https://github.com/iris-contrib/examples/blob/master/template_engines/template_markdown_1/main.go), [book section](https://docs.iris-go.com/template-engines.html) - -> Each section of the README has its own - more advanced - subject on the book, so be sure to check book for any further research - -[Read more](https://docs.iris-go.com/template-engines.html) - -### Middleware ecosystem - - -```go - -import ( - "github.com/iris-contrib/middleware/logger" - "github.com/iris-contrib/middleware/cors" - "github.com/iris-contrib/middleware/basicauth" -) -// Root level middleware -iris.Use(logger.New()) -iris.Use(cors.Default()) - -// Group level middleware -authConfig := basicauth.Config{ - Users: map[string]string{"myusername": "mypassword", "mySecondusername": "mySecondpassword"}, - Realm: "Authorization Required", // if you don't set it it's "Authorization Required" - ContextKey: "mycustomkey", // if you don't set it it's "user" - Expires: time.Duration(30) * time.Minute, -} - -authentication := basicauth.New(authConfig) - -g := iris.Party("/admin") -g.Use(authentication) - -// Route level middleware -logme := func(ctx *iris.Context) { - println("request to /products") - ctx.Next() -} -iris.Get("/products", logme, func(ctx *iris.Context) { - ctx.Text(iris.StatusOK, "/products") -}) -``` - - -| Name | Description | Usage | -| ------------------|:---------------------:|-------:| -| [Basicauth Middleware ](https://github.com/iris-contrib/middleware/tree/master/basicauth) | HTTP Basic authentication |[example 1](https://github.com/iris-contrib/examples/blob/master/middleware_basicauth_1/main.go), [example 2](https://github.com/iris-contrib/examples/blob/master/middleware_basicauth_2/main.go), [book section](https://docs.iris-go.com/basic-authentication.html) | -| [JWT Middleware ](https://github.com/iris-contrib/middleware/tree/master/jwt) | JSON Web Tokens |[example ](https://github.com/iris-contrib/examples/blob/master/middleware_jwt/main.go), [book section](https://docs.iris-go.com/jwt.html) | -| [Cors Middleware ](https://github.com/iris-contrib/middleware/tree/master/cors) | Cross Origin Resource Sharing W3 specification | [how to use ](https://github.com/iris-contrib/middleware/tree/master/cors#how-to-use) | -| [Secure Middleware ](https://github.com/iris-contrib/middleware/tree/master/secure) | Facilitates some quick security wins | [example](https://github.com/iris-contrib/examples/blob/master/middleware_secure/main.go) | -| [I18n Middleware ](https://github.com/iris-contrib/middleware/tree/master/i18n) | Simple internationalization | [example](https://github.com/iris-contrib/examples/tree/master/middleware_internationalization_i18n), [book section](https://docs.iris-go.com/middleware-internationalization-and-localization.html) | -| [Recovery Middleware ](https://github.com/iris-contrib/middleware/tree/master/recovery) | Safety recover the station from panic | [example](https://github.com/iris-contrib/examples/blob/master/middleware_recovery/main.go) | -| [Logger Middleware ](https://github.com/iris-contrib/middleware/tree/master/logger) | Logs every request | [example](https://github.com/iris-contrib/examples/blob/master/middleware_logger/main.go), [book section](https://docs.iris-go.com/logger.html) | -| [LoggerZap Middleware ](https://github.com/iris-contrib/middleware/tree/master/loggerzap) | Logs every request using zap | [example](https://github.com/iris-contrib/examples/blob/master/middleware_logger/main.go), [book section](https://docs.iris-go.com/logger.html) | -| [Profile Middleware ](https://github.com/iris-contrib/middleware/tree/master/pprof) | Http profiling for debugging | [example](https://github.com/iris-contrib/examples/blob/master/middleware_pprof/main.go) | -| [Editor Plugin](https://github.com/iris-contrib/plugin/tree/master/editor) | Alm-tools, a typescript online IDE/Editor | [book section](https://docs.iris-go.com/plugin-editor.html) | -| [Typescript Plugin](https://github.com/iris-contrib/plugin/tree/master/typescript) | Auto-compile client-side typescript files | [book section](https://docs.iris-go.com/plugin-typescript.html) | -| [OAuth,OAuth2 Plugin](https://github.com/iris-contrib/plugin/tree/master/oauth) | User Authentication was never be easier, supports >27 providers | [example](https://github.com/iris-contrib/examples/tree/master/plugin_oauth_oauth2), [book section](https://docs.iris-go.com/plugin-oauth.html) | -| [Iris control Plugin](https://github.com/iris-contrib/plugin/tree/master/iriscontrol) | Basic (browser-based) control over your Iris station | [example](https://github.com/iris-contrib/examples/blob/master/plugin_iriscontrol/main.go), [book section](https://docs.iris-go.com/plugin-iriscontrol.html) | - -> NOTE: All net/http handlers and middleware that already created by other go developers are also compatible with Iris, even if they are not be documented here, read more [here](https://github.com/iris-contrib/middleware#can-i-use-standard-nethttp-handler-with-iris). - - -### Sessions -If you notice a bug or issue [post it here](https://github.com/kataras/go-sessions). - - -- Cleans the temp memory when a session is idle, and re-allocates it to the temp memory when it's necessary. -The most used sessions are optimized to be in the front of the memory's list. - -- Supports any type of database, currently only [Redis](https://github.com/kataras/go-sessions/tree/master/sessiondb/redis) and [LevelDB](https://github.com/kataras/go-sessions/tree/master/sessiondb/leveldb). - - -**A session can be defined as a server-side storage of information that is desired to persist throughout the user's interaction with the web application**. - -Instead of storing large and constantly changing data via cookies in the user's browser (i.e. CookieStore), -**only a unique identifier is stored on the client side** called a "session id". -This session id is passed to the web server on every request. -The web application uses the session id as the key for retrieving the stored data from the database/memory. The session data is then available inside the iris.Context. - -```go -iris.Get("/", func(ctx *iris.Context) { - ctx.Writef("You should navigate to the /set, /get, /delete, /clear,/destroy instead") - }) - - iris.Get("/set", func(ctx *iris.Context) { - - //set session values - ctx.Session().Set("name", "iris") - - //test if setted here - ctx.Writef("All ok session setted to: %s", ctx.Session().GetString("name")) - }) - - iris.Get("/get", func(ctx *iris.Context) { - // get a specific key as a string. - // returns an empty string if the key was not found. - name := ctx.Session().GetString("name") - - ctx.Writef("The name on the /set was: %s", name) - }) - - iris.Get("/delete", func(ctx *iris.Context) { - // delete a specific key - ctx.Session().Delete("name") - }) - - iris.Get("/clear", func(ctx *iris.Context) { - // removes all entries - ctx.Session().Clear() - }) - - iris.Get("/destroy", func(ctx *iris.Context) { - // destroy/removes the entire session and cookie - ctx.SessionDestroy() - ctx.Log("You have to refresh the page to completely remove the session (on browsers), so the name should NOT be empty NOW, is it?\n ame: %s\n\nAlso check your cookies in your browser's cookies, should be no field for localhost/127.0.0.1 (or whatever you use)", ctx.Session().GetString("name")) - ctx.Writef("You have to refresh the page to completely remove the session (on browsers), so the name should NOT be empty NOW, is it?\nName: %s\n\nAlso check your cookies in your browser's cookies, should be no field for localhost/127.0.0.1 (or whatever you use)", ctx.Session().GetString("name")) - }) - - iris.Listen(":8080") - -``` - -- `iris.DestroySessionByID(string)` - -```go -// DestroySessionByID removes the session entry -// from the server-side memory (and database if registered). -// Client's session cookie will still exist but it will be reseted on the next request. -// -// It's safe to use it even if you are not sure if a session with that id exists. -DestroySessionByID(string) -``` - -- `iris.DestroyAllSessions()` - -```go -// DestroyAllSessions removes all sessions -// from the server-side memory (and database if registered). -// Client's session cookie will still exist but it will be reseted on the next request. -DestroyAllSessions() -``` - -> Each section of the README has its own - more advanced - subject on the book, so be sure to check book for any further research - -[Read more](https://docs.iris-go.com/package-sessions.html) - -### Websockets - -Server configuration - -```go -iris.Config.Websocket{ - // WriteTimeout time allowed to write a message to the connection. - // Default value is 15 * time.Second - WriteTimeout time.Duration - // PongTimeout allowed to read the next pong message from the connection - // Default value is 60 * time.Second - PongTimeout time.Duration - // PingPeriod send ping messages to the connection with this period. Must be less than PongTimeout - // Default value is (PongTimeout * 9) / 10 - PingPeriod time.Duration - // MaxMessageSize max message size allowed from connection - // Default value is 1024 - MaxMessageSize int64 - // BinaryMessages set it to true in order to denotes binary data messages instead of utf-8 text - // see https://github.com/kataras/iris/issues/387#issuecomment-243006022 for more - // Defaults to false - BinaryMessages bool - // Endpoint is the path which the websocket server will listen for clients/connections - // Default value is empty string, if you don't set it the Websocket server is disabled. - Endpoint string - // ReadBufferSize is the buffer size for the underline reader - ReadBufferSize int - // WriteBufferSize is the buffer size for the underline writer - WriteBufferSize int - // Error specifies the function for generating HTTP error responses. - // - // The default behavior is to store the reason in the context (ctx.Set(reason)) and fire any custom error (ctx.EmitError(status)) - Error func(ctx *Context, status int, reason error) - // CheckOrigin returns true if the request Origin header is acceptable. If - // CheckOrigin is nil, the host in the Origin header must not be set or - // must match the host of the request. - // - // The default behavior is to allow all origins - // you can change this behavior by setting the iris.Config.Websocket.CheckOrigin = iris.WebsocketCheckSameOrigin - CheckOrigin func(r *http.Request) bool - // IDGenerator used to create (and later on, set) - // an ID for each incoming websocket connections (clients). - // If empty then the ID is generated by the result of 64 - // random combined characters - IDGenerator func(r *http.Request) string -} - -``` - -Connection's methods - -```go -ID() string - -Request() *http.Request - -// Receive from the client -On("anyCustomEvent", func(message string) {}) -On("anyCustomEvent", func(message int){}) -On("anyCustomEvent", func(message bool){}) -On("anyCustomEvent", func(message anyCustomType){}) -On("anyCustomEvent", func(){}) - -// Receive a native websocket message from the client -// compatible without need of import the iris-ws.js to the .html -OnMessage(func(message []byte){}) - -// Send to the client -Emit("anyCustomEvent", string) -Emit("anyCustomEvent", int) -Emit("anyCustomEvent", bool) -Emit("anyCustomEvent", anyCustomType) - -// Send native websocket messages -// with config.BinaryMessages = true -// useful when you use proto or something like this. -EmitMessage([]byte("anyMessage")) - -// Send to specific client(s) -To("otherConnectionId").Emit/EmitMessage... -To("anyCustomRoom").Emit/EmitMessage... - -// Send to all opened connections/clients -To(websocket.All).Emit/EmitMessage... - -// Send to all opened connections/clients EXCEPT this client -To(websocket.Broadcast).Emit/EmitMessage... - -// Rooms, group of connections/clients -Join("anyCustomRoom") -Leave("anyCustomRoom") - - -// Fired when the connection is closed -OnDisconnect(func(){}) - -// Force-disconnect the client from the server-side -Disconnect() error -``` - -```go -// file ./main.go -package main - -import ( - "fmt" - "github.com/kataras/iris" -) - -type clientPage struct { - Title string - Host string -} - -func main() { - iris.Static("/js", "./static/js", 1) - - iris.Get("/", func(ctx *iris.Context) { - ctx.Render("client.html", clientPage{"Client Page", ctx.Host()}) - }) - - // the path at which the websocket client should register itself to - iris.Config.Websocket.Endpoint = "/my_endpoint" - - var myChatRoom = "room1" - iris.Websocket.OnConnection(func(c iris.WebsocketConnection) { - - c.Join(myChatRoom) - - c.On("chat", func(message string) { - // to all except this connection -> - //c.To(iris.Broadcast).Emit("chat", "Message from: "+c.ID()+"-> "+message) - - // to the client -> - //c.Emit("chat", "Message from myself: "+message) - - // send the message to the whole room, - // all connections which are inside this room will receive this message - c.To(myChatRoom).Emit("chat", "From: "+c.ID()+": "+message) - }) - - c.OnDisconnect(func() { - fmt.Printf("\nConnection with ID: %s has been disconnected!", c.ID()) - }) - }) - - iris.Listen(":8080") -} - -``` - -```js -// file js/chat.js -var messageTxt; -var messages; - -$(function () { - - messageTxt = $("#messageTxt"); - messages = $("#messages"); - - - ws = new Ws("ws://" + HOST + "/my_endpoint"); - ws.OnConnect(function () { - console.log("Websocket connection enstablished"); - }); - - ws.OnDisconnect(function () { - appendMessage($("

Disconnected

")); - }); - - ws.On("chat", function (message) { - appendMessage($("
" + message + "
")); - }) - - $("#sendBtn").click(function () { - //ws.EmitMessage(messageTxt.val()); - ws.Emit("chat", messageTxt.val().toString()); - messageTxt.val(""); - }) - -}) - - -function appendMessage(messageDiv) { - var theDiv = messages[0] - var doScroll = theDiv.scrollTop == theDiv.scrollHeight - theDiv.clientHeight; - messageDiv.appendTo(messages) - if (doScroll) { - theDiv.scrollTop = theDiv.scrollHeight - theDiv.clientHeight; - } -} -``` - -```html - - - - - My iris-ws - - - -
- -
- - - - - - - - - - - - -``` - -View a working example by navigating [here](https://github.com/iris-contrib/examples/tree/master/websocket) and if you need more than one websocket server [click here](https://github.com/iris-contrib/examples/tree/master/websocket_unlimited_servers). - -> Each section of the README has its own - more advanced - subject on the book, so be sure to check book for any further research - -[Read more](https://docs.iris-go.com/package-websocket.html) - - - -Benchmarks ------------- - -These benchmarks are for the previous Iris version(1month ago), new benchmarks are coming after the release of the Go version 1.8 in order to include the `Push` feature inside the tests. - - -This Benchmark test aims to compare the whole HTTP request processing between Go web frameworks. - - -![Benchmark Wizzard July 21, 2016- Processing Time Horizontal Graph](https://raw.githubusercontent.com/smallnest/go-web-framework-benchmark/4db507a22c964c9bc9774c5b31afdc199a0fe8b7/benchmark.png) - -**The results have been updated on July 21, 2016** - - - -Depends on: - -- http protocol layer comes from [net/http](https://github.com/golang/go/tree/master/src/net/http), by Go Authors. -- rich and encoded responses support comes from [kataras/go-serializer](https://github.com/kataras/go-serializer/tree/0.0.4), by me. -- template support comes from [kataras/go-template](https://github.com/kataras/go-template/tree/0.0.3), by me. -- gzip support comes from [kataras/go-fs](https://github.com/kataras/go-fs/tree/0.0.5) and the super-fast compression library [klauspost/compress/gzip](https://github.com/klauspost/compress/tree/master/gzip), by me & Klaus Post. -- websockets support comes from [kataras/go-websocket](https://github.com/kataras/go-websocket/tree/0.0.2), by me. -- Base of the parameterized routing algorithm comes from [julienschmidt/httprouter](https://github.com/julienschmidt/httprouter), by Julien Schmidt, with some relative to performance edits by me. -- sessions support comes from [kataras/go-sessions](https://github.com/kataras/go-sessions/tree/0.0.6), by me. -- caching support comes from [geekypanda/httpcache](https://github.com/geekypanda/httpcache/tree/0.0.1), by me and GeekyPanda. -- end-to-end http test APIs comes from [gavv/httpexpect](https://github.com/gavv/httpexpect), by Victor Gaydov. -- hot-reload on source code changes comes from [kataras/rizla](https://github.com/kataras/rizla), by me. -- auto-updater (via github) comes from [kataras/go-fs](https://github.com/kataras/go-fs), by me. -- request body form binder is an [edited version](https://github.com/iris-contrib/formBinder) of the [monoculum/formam](https://github.com/monoculum/formam) library, by Monoculum Organisation. -- all other packages comes from the [Iris Contrib Organisation](https://github.com/iris-contrib) and the [Go standard library](https://github.com/golang/go), by me & The Go Authors. diff --git a/docs/README.md b/docs/README.md deleted file mode 100644 index 29c4c60c..00000000 --- a/docs/README.md +++ /dev/null @@ -1,12 +0,0 @@ -# Documentation - -Navigate through [https://docs.iris-go.com/](https://docs.iris-go.com/) website to read the docs. - - -## Contributing - -You can contribute to the documentation via PR to its public repository, [iris-contrib/gitbook](https://github.com/iris-contrib/gitbook). Any code-fix or even a grammar-fix is acceptable and welcomed! - -## Examples? - -Navigate through examples by clicking [here](https://github.com/iris-contrib/examples) & [run](https://github.com/kataras/iris/blob/master/examples/README.md) them. diff --git a/examples/README.md b/examples/README.md deleted file mode 100644 index 97553180..00000000 --- a/examples/README.md +++ /dev/null @@ -1,16 +0,0 @@ -# Examples - -Navigate through [iris-contrib/examples](https://github.com/iris-contrib/examples) repository to view all available examples. - -> These examples are small but practical, they do NOT cover all Iris' and Go's stdlib features. - - -## Run - -1. Download the [Go Programming Language](https://golang.org/dl/). -2. Download the [LiteIDE](https://sourceforge.net/projects/liteide/files/X30.3/), a cross-platform Go IDE. -3. Click [here](https://github.com/iris-contrib/examples/archive/master.zip) to download all examples. -4. **Unzip** the contents of your `examples-master.zip` you just downloaded. -5. Open the LiteIDE, click on the `File -> Open Folder...` menu item (top-left corner) -and select the folder which contains the contents you just unzipped. -6. Open an example folder, select its `main.go` and `run` it by pressing `Ctrl/CMD +R`. diff --git a/webfs.go b/fs.go similarity index 88% rename from webfs.go rename to fs.go index a9f9b54f..20bd9292 100644 --- a/webfs.go +++ b/fs.go @@ -14,11 +14,11 @@ type StaticHandlerBuilder interface { Gzip(enable bool) StaticHandlerBuilder Listing(listDirectoriesOnOff bool) StaticHandlerBuilder StripPath(yesNo bool) StaticHandlerBuilder - Except(r ...Route) StaticHandlerBuilder + Except(r ...RouteInfo) StaticHandlerBuilder Build() HandlerFunc } -type webfs struct { +type fsHandler struct { // user options, only directory is required. directory http.Dir requestPath string @@ -28,7 +28,7 @@ type webfs struct { // these are init on the Build() call filesystem http.FileSystem once sync.Once - exceptions []Route + exceptions []RouteInfo handler HandlerFunc } @@ -51,7 +51,7 @@ func toWebPath(systemPath string) string { // this builder is used by people who have more complicated application // structure and want a fluent api to work on. func NewStaticHandlerBuilder(dir string) StaticHandlerBuilder { - return &webfs{ + return &fsHandler{ directory: http.Dir(dir), // default route path is the same as the directory requestPath: toWebPath(dir), @@ -66,33 +66,33 @@ func NewStaticHandlerBuilder(dir string) StaticHandlerBuilder { // Path sets the request path. // Defaults to same as system path -func (w *webfs) Path(requestRoutePath string) StaticHandlerBuilder { +func (w *fsHandler) Path(requestRoutePath string) StaticHandlerBuilder { w.requestPath = toWebPath(requestRoutePath) return w } // Gzip if enable is true then gzip compression is enabled for this static directory // Defaults to false -func (w *webfs) Gzip(enable bool) StaticHandlerBuilder { +func (w *fsHandler) Gzip(enable bool) StaticHandlerBuilder { w.gzip = enable return w } // Listing turn on/off the 'show files and directories'. // Defaults to false -func (w *webfs) Listing(listDirectoriesOnOff bool) StaticHandlerBuilder { +func (w *fsHandler) Listing(listDirectoriesOnOff bool) StaticHandlerBuilder { w.listDirectories = listDirectoriesOnOff return w } -func (w *webfs) StripPath(yesNo bool) StaticHandlerBuilder { +func (w *fsHandler) StripPath(yesNo bool) StaticHandlerBuilder { w.stripPath = yesNo return w } // Except add a route exception, // gives priority to that Route over the static handler. -func (w *webfs) Except(r ...Route) StaticHandlerBuilder { +func (w *fsHandler) Except(r ...RouteInfo) StaticHandlerBuilder { w.exceptions = append(w.exceptions, r...) return w } @@ -110,7 +110,7 @@ func (n noListFile) Readdir(count int) ([]os.FileInfo, error) { // Implements the http.Filesystem // Do not call it. -func (w *webfs) Open(name string) (http.File, error) { +func (w *fsHandler) Open(name string) (http.File, error) { info, err := w.filesystem.Open(name) if err != nil { @@ -125,11 +125,11 @@ func (w *webfs) Open(name string) (http.File, error) { } // Build the handler (once) and returns it -func (w *webfs) Build() HandlerFunc { +func (w *fsHandler) Build() HandlerFunc { // we have to ensure that Build is called ONLY one time, // one instance per one static directory. w.once.Do(func() { - w.filesystem = http.Dir(w.directory) + w.filesystem = w.directory // set the filesystem to itself in order to be recognised of listing property (can be change at runtime too) fileserver := http.FileServer(w) diff --git a/handler.go b/handler.go new file mode 100644 index 00000000..dc5d2d21 --- /dev/null +++ b/handler.go @@ -0,0 +1,159 @@ +package iris + +import ( + "net/http" + + "github.com/kataras/go-errors" +) + +// errHandler returns na error with message: 'Passed argument is not func(*Context) neither an object which implements the iris.Default.Handler with Serve(ctx *Context) +// It seems to be a +type Points to: +pointer.' +var errHandler = errors.New(` +Passed argument is not an iris.Handler (or func(*iris.Context)) neither one of these types: + - http.Handler + - func(w http.ResponseWriter, r *http.Request) + - func(w http.ResponseWriter, r *http.Request, next http.HandlerFunc) + --------------------------------------------------------------------- +It seems to be a %T points to: %v`) + +type ( + // Handler the main Iris Handler interface. + Handler interface { + Serve(ctx *Context) // iris-specific + } + + // HandlerFunc type is an adapter to allow the use of + // ordinary functions as HTTP handlers. If f is a function + // with the appropriate signature, HandlerFunc(f) is a + // Handler that calls f. + HandlerFunc func(*Context) + // Middleware is just a slice of Handler []func(c *Context) + Middleware []Handler +) + +// Serve implements the Handler, is like ServeHTTP but for Iris +func (h HandlerFunc) Serve(ctx *Context) { + h(ctx) +} + +// ToNativeHandler converts an iris handler to http.Handler +func ToNativeHandler(s *Framework, h Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + s.Context.Run(w, r, func(ctx *Context) { + h.Serve(ctx) + }) + }) +} + +// ToHandler converts different style of handlers that you +// used to use (usually with third-party net/http middleware) to an iris.HandlerFunc. +// +// Supported types: +// - .ToHandler(h http.Handler) +// - .ToHandler(func(w http.ResponseWriter, r *http.Request)) +// - .ToHandler(func(w http.ResponseWriter, r *http.Request, next http.HandlerFunc)) +func ToHandler(handler interface{}) HandlerFunc { + switch handler.(type) { + case HandlerFunc: + { + // + //it's already an iris handler + // + return handler.(HandlerFunc) + } + + case http.Handler: + // + // handlerFunc.ServeHTTP(w,r) + // + { + h := handler.(http.Handler) + return func(ctx *Context) { + h.ServeHTTP(ctx.ResponseWriter, ctx.Request) + } + } + + case func(http.ResponseWriter, *http.Request): + { + // + // handlerFunc(w,r) + // + return ToHandler(http.HandlerFunc(handler.(func(http.ResponseWriter, *http.Request)))) + } + + case func(http.ResponseWriter, *http.Request, http.HandlerFunc): + { + // + // handlerFunc(w,r, http.HandlerFunc) + // + return toHandlerNextHTTPHandlerFunc(handler.(func(http.ResponseWriter, *http.Request, http.HandlerFunc))) + } + + default: + { + // + // No valid handler passed + // + panic(errHandler.Format(handler, handler)) + } + + } + +} + +func toHandlerNextHTTPHandlerFunc(h func(w http.ResponseWriter, r *http.Request, next http.HandlerFunc)) HandlerFunc { + return HandlerFunc(func(ctx *Context) { + // take the next handler in route's chain + nextIrisHandler := ctx.NextHandler() + if nextIrisHandler != nil { + executed := false // we need to watch this in order to StopExecution from all next handlers + // if this next handler is not executed by the third-party net/http next-style middleware. + nextHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + nextIrisHandler.Serve(ctx) + executed = true + }) + + h(ctx.ResponseWriter, ctx.Request, nextHandler) + + // after third-party middleware's job: + if executed { + // if next is executed then increment the ctx.Pos manually + // in order to the next handler not to be executed twice. + ctx.Pos++ + } else { + // otherwise StopExecution from all next handlers. + ctx.StopExecution() + } + return + } + + // if not next handler found then this is not a 'valid' middleware but + // some middleware may don't care about next, + // so we just execute the handler with an empty net. + h(ctx.ResponseWriter, ctx.Request, http.HandlerFunc(func(http.ResponseWriter, *http.Request) {})) + }) +} + +// convertToHandlers just make []HandlerFunc to []Handler, although HandlerFunc and Handler are the same +// we need this on some cases we explicit want a interface Handler, it is useless for users. +func convertToHandlers(handlersFn []HandlerFunc) []Handler { + hlen := len(handlersFn) + mlist := make([]Handler, hlen) + for i := 0; i < hlen; i++ { + mlist[i] = Handler(handlersFn[i]) + } + return mlist +} + +// joinMiddleware uses to create a copy of all middleware and return them in order to use inside the node +func joinMiddleware(middleware1 Middleware, middleware2 Middleware) Middleware { + nowLen := len(middleware1) + totalLen := nowLen + len(middleware2) + // create a new slice of middleware in order to store all handlers, the already handlers(middleware) and the new + newMiddleware := make(Middleware, totalLen) + //copy the already middleware to the just created + copy(newMiddleware, middleware1) + //start from there we finish, and store the new middleware too + copy(newMiddleware[nowLen:], middleware2) + return newMiddleware +} diff --git a/http.go b/http.go deleted file mode 100644 index 3d85f33a..00000000 --- a/http.go +++ /dev/null @@ -1,1544 +0,0 @@ -package iris - -import ( - "crypto/tls" - "log" - "net" - "net/http" - "os" - "sort" - "strconv" - "strings" - "sync" - "time" - - "github.com/geekypanda/httpcache" - "github.com/iris-contrib/letsencrypt" - "github.com/kataras/go-errors" - "golang.org/x/crypto/acme/autocert" -) - -const ( - // MethodGet "GET" - MethodGet = "GET" - // MethodPost "POST" - MethodPost = "POST" - // MethodPut "PUT" - MethodPut = "PUT" - // MethodDelete "DELETE" - MethodDelete = "DELETE" - // MethodConnect "CONNECT" - MethodConnect = "CONNECT" - // MethodHead "HEAD" - MethodHead = "HEAD" - // MethodPatch "PATCH" - MethodPatch = "PATCH" - // MethodOptions "OPTIONS" - MethodOptions = "OPTIONS" - // MethodTrace "TRACE" - MethodTrace = "TRACE" - // MethodNone is a Virtual method - // to store the "offline" routes - // in the mux's tree - MethodNone = "NONE" -) - -var ( - // AllMethods contains all the http valid methods: - // "GET", "POST", "PUT", "DELETE", "CONNECT", "HEAD", "PATCH", "OPTIONS", "TRACE" - AllMethods = [...]string{MethodGet, MethodPost, MethodPut, MethodDelete, MethodConnect, MethodHead, MethodPatch, MethodOptions, MethodTrace} -) - -// HTTP status codes. -const ( - StatusContinue = 100 // RFC 7231, 6.2.1 - StatusSwitchingProtocols = 101 // RFC 7231, 6.2.2 - StatusProcessing = 102 // RFC 2518, 10.1 - - StatusOK = 200 // RFC 7231, 6.3.1 - StatusCreated = 201 // RFC 7231, 6.3.2 - StatusAccepted = 202 // RFC 7231, 6.3.3 - StatusNonAuthoritativeInfo = 203 // RFC 7231, 6.3.4 - StatusNoContent = 204 // RFC 7231, 6.3.5 - StatusResetContent = 205 // RFC 7231, 6.3.6 - StatusPartialContent = 206 // RFC 7233, 4.1 - StatusMultiStatus = 207 // RFC 4918, 11.1 - StatusAlreadyReported = 208 // RFC 5842, 7.1 - StatusIMUsed = 226 // RFC 3229, 10.4.1 - - StatusMultipleChoices = 300 // RFC 7231, 6.4.1 - StatusMovedPermanently = 301 // RFC 7231, 6.4.2 - StatusFound = 302 // RFC 7231, 6.4.3 - StatusSeeOther = 303 // RFC 7231, 6.4.4 - StatusNotModified = 304 // RFC 7232, 4.1 - StatusUseProxy = 305 // RFC 7231, 6.4.5 - _ = 306 // RFC 7231, 6.4.6 (Unused) - StatusTemporaryRedirect = 307 // RFC 7231, 6.4.7 - StatusPermanentRedirect = 308 // RFC 7538, 3 - - StatusBadRequest = 400 // RFC 7231, 6.5.1 - StatusUnauthorized = 401 // RFC 7235, 3.1 - StatusPaymentRequired = 402 // RFC 7231, 6.5.2 - StatusForbidden = 403 // RFC 7231, 6.5.3 - StatusNotFound = 404 // RFC 7231, 6.5.4 - StatusMethodNotAllowed = 405 // RFC 7231, 6.5.5 - StatusNotAcceptable = 406 // RFC 7231, 6.5.6 - StatusProxyAuthRequired = 407 // RFC 7235, 3.2 - StatusRequestTimeout = 408 // RFC 7231, 6.5.7 - StatusConflict = 409 // RFC 7231, 6.5.8 - StatusGone = 410 // RFC 7231, 6.5.9 - StatusLengthRequired = 411 // RFC 7231, 6.5.10 - StatusPreconditionFailed = 412 // RFC 7232, 4.2 - StatusRequestEntityTooLarge = 413 // RFC 7231, 6.5.11 - StatusRequestURITooLong = 414 // RFC 7231, 6.5.12 - StatusUnsupportedMediaType = 415 // RFC 7231, 6.5.13 - StatusRequestedRangeNotSatisfiable = 416 // RFC 7233, 4.4 - StatusExpectationFailed = 417 // RFC 7231, 6.5.14 - StatusTeapot = 418 // RFC 7168, 2.3.3 - StatusUnprocessableEntity = 422 // RFC 4918, 11.2 - StatusLocked = 423 // RFC 4918, 11.3 - StatusFailedDependency = 424 // RFC 4918, 11.4 - StatusUpgradeRequired = 426 // RFC 7231, 6.5.15 - StatusPreconditionRequired = 428 // RFC 6585, 3 - StatusTooManyRequests = 429 // RFC 6585, 4 - StatusRequestHeaderFieldsTooLarge = 431 // RFC 6585, 5 - StatusUnavailableForLegalReasons = 451 // RFC 7725, 3 - - StatusInternalServerError = 500 // RFC 7231, 6.6.1 - StatusNotImplemented = 501 // RFC 7231, 6.6.2 - StatusBadGateway = 502 // RFC 7231, 6.6.3 - StatusServiceUnavailable = 503 // RFC 7231, 6.6.4 - StatusGatewayTimeout = 504 // RFC 7231, 6.6.5 - StatusHTTPVersionNotSupported = 505 // RFC 7231, 6.6.6 - StatusVariantAlsoNegotiates = 506 // RFC 2295, 8.1 - StatusInsufficientStorage = 507 // RFC 4918, 11.5 - StatusLoopDetected = 508 // RFC 5842, 7.2 - StatusNotExtended = 510 // RFC 2774, 7 - StatusNetworkAuthenticationRequired = 511 // RFC 6585, 6 -) - -var statusText = map[int]string{ - StatusContinue: "Continue", - StatusSwitchingProtocols: "Switching Protocols", - StatusProcessing: "Processing", - - StatusOK: "OK", - StatusCreated: "Created", - StatusAccepted: "Accepted", - StatusNonAuthoritativeInfo: "Non-Authoritative Information", - StatusNoContent: "No Content", - StatusResetContent: "Reset Content", - StatusPartialContent: "Partial Content", - StatusMultiStatus: "Multi-Status", - StatusAlreadyReported: "Already Reported", - StatusIMUsed: "IM Used", - - StatusMultipleChoices: "Multiple Choices", - StatusMovedPermanently: "Moved Permanently", - StatusFound: "Found", - StatusSeeOther: "See Other", - StatusNotModified: "Not Modified", - StatusUseProxy: "Use Proxy", - StatusTemporaryRedirect: "Temporary Redirect", - StatusPermanentRedirect: "Permanent Redirect", - - StatusBadRequest: "Bad Request", - StatusUnauthorized: "Unauthorized", - StatusPaymentRequired: "Payment Required", - StatusForbidden: "Forbidden", - StatusNotFound: "Not Found", - StatusMethodNotAllowed: "Method Not Allowed", - StatusNotAcceptable: "Not Acceptable", - StatusProxyAuthRequired: "Proxy Authentication Required", - StatusRequestTimeout: "Request Timeout", - StatusConflict: "Conflict", - StatusGone: "Gone", - StatusLengthRequired: "Length Required", - StatusPreconditionFailed: "Precondition Failed", - StatusRequestEntityTooLarge: "Request Entity Too Large", - StatusRequestURITooLong: "Request URI Too Long", - StatusUnsupportedMediaType: "Unsupported Media Type", - StatusRequestedRangeNotSatisfiable: "Requested Range Not Satisfiable", - StatusExpectationFailed: "Expectation Failed", - StatusTeapot: "I'm a teapot", - StatusUnprocessableEntity: "Unprocessable Entity", - StatusLocked: "Locked", - StatusFailedDependency: "Failed Dependency", - StatusUpgradeRequired: "Upgrade Required", - StatusPreconditionRequired: "Precondition Required", - StatusTooManyRequests: "Too Many Requests", - StatusRequestHeaderFieldsTooLarge: "Request Header Fields Too Large", - StatusUnavailableForLegalReasons: "Unavailable For Legal Reasons", - - StatusInternalServerError: "Internal Server Error", - StatusNotImplemented: "Not Implemented", - StatusBadGateway: "Bad Gateway", - StatusServiceUnavailable: "Service Unavailable", - StatusGatewayTimeout: "Gateway Timeout", - StatusHTTPVersionNotSupported: "HTTP Version Not Supported", - StatusVariantAlsoNegotiates: "Variant Also Negotiates", - StatusInsufficientStorage: "Insufficient Storage", - StatusLoopDetected: "Loop Detected", - StatusNotExtended: "Not Extended", - StatusNetworkAuthenticationRequired: "Network Authentication Required", -} - -// StatusText returns a text for the HTTP status code. It returns the empty -// string if the code is unknown. -func StatusText(code int) string { - return statusText[code] -} - -// errHandler returns na error with message: 'Passed argument is not func(*Context) neither an object which implements the iris.Default.Handler with Serve(ctx *Context) -// It seems to be a +type Points to: +pointer.' -var errHandler = errors.New(` -Passed argument is not an iris.Handler (or func(*iris.Context)) neither one of these types: - - http.Handler - - func(w http.ResponseWriter, r *http.Request) - - func(w http.ResponseWriter, r *http.Request, next http.HandlerFunc) - --------------------------------------------------------------------- -It seems to be a %T points to: %v`) - -type ( - // Handler the main Iris Handler interface. - Handler interface { - Serve(ctx *Context) // iris-specific - } - - // HandlerFunc type is an adapter to allow the use of - // ordinary functions as HTTP handlers. If f is a function - // with the appropriate signature, HandlerFunc(f) is a - // Handler that calls f. - HandlerFunc func(*Context) - // Middleware is just a slice of Handler []func(c *Context) - Middleware []Handler - - // HandlerAPI empty interface used for .API - HandlerAPI interface{} -) - -// Serve implements the Handler, is like ServeHTTP but for Iris -func (h HandlerFunc) Serve(ctx *Context) { - h(ctx) -} - -// ToNativeHandler converts an iris handler to http.Handler -func ToNativeHandler(s *Framework, h Handler) http.Handler { - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - ctx := s.AcquireCtx(w, r) - h.Serve(ctx) - s.ReleaseCtx(ctx) - }) -} - -// ToHandler converts different type styles of handlers that you -// used to use (usually with third-party net/http middleware) to an iris.HandlerFunc. -// -// Supported types: -// - .ToHandler(h http.Handler) -// - .ToHandler(func(w http.ResponseWriter, r *http.Request)) -// - .ToHandler(func(w http.ResponseWriter, r *http.Request, next http.HandlerFunc)) -func ToHandler(handler interface{}) HandlerFunc { - switch handler.(type) { - case HandlerFunc: - { - // - //it's already an iris handler - // - return handler.(HandlerFunc) - } - - case http.Handler: - // - // handlerFunc.ServeHTTP(w,r) - // - { - h := handler.(http.Handler) - return func(ctx *Context) { - h.ServeHTTP(ctx.ResponseWriter, ctx.Request) - } - } - - case func(http.ResponseWriter, *http.Request): - { - // - // handlerFunc(w,r) - // - return ToHandler(http.HandlerFunc(handler.(func(http.ResponseWriter, *http.Request)))) - } - - case func(http.ResponseWriter, *http.Request, http.HandlerFunc): - { - // - // handlerFunc(w,r, http.HandlerFunc) - // - return toHandlerNextHTTPHandlerFunc(handler.(func(http.ResponseWriter, *http.Request, http.HandlerFunc))) - } - - default: - { - // - // No valid handler passed - // - panic(errHandler.Format(handler, handler)) - } - - } - -} - -func toHandlerNextHTTPHandlerFunc(h func(w http.ResponseWriter, r *http.Request, next http.HandlerFunc)) HandlerFunc { - return HandlerFunc(func(ctx *Context) { - // take the next handler in route's chain - nextIrisHandler := ctx.NextHandler() - if nextIrisHandler != nil { - executed := false // we need to watch this in order to StopExecution from all next handlers - // if this next handler is not executed by the third-party net/http next-style middleware. - nextHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - nextIrisHandler.Serve(ctx) - executed = true - }) - - h(ctx.ResponseWriter, ctx.Request, nextHandler) - - // after third-party middleware's job: - if executed { - // if next is executed then increment the ctx.Pos manually - // in order to the next handler not to be executed twice. - ctx.Pos++ - } else { - // otherwise StopExecution from all next handlers. - ctx.StopExecution() - } - return - } - - // if not next handler found then this is not a 'valid' middleware but - // some middleware may don't care about next, - // so we just execute the handler with an empty net. - h(ctx.ResponseWriter, ctx.Request, http.HandlerFunc(func(http.ResponseWriter, *http.Request) {})) - }) -} - -// convertToHandlers just make []HandlerFunc to []Handler, although HandlerFunc and Handler are the same -// we need this on some cases we explicit want a interface Handler, it is useless for users. -func convertToHandlers(handlersFn []HandlerFunc) []Handler { - hlen := len(handlersFn) - mlist := make([]Handler, hlen) - for i := 0; i < hlen; i++ { - mlist[i] = Handler(handlersFn[i]) - } - return mlist -} - -// joinMiddleware uses to create a copy of all middleware and return them in order to use inside the node -func joinMiddleware(middleware1 Middleware, middleware2 Middleware) Middleware { - nowLen := len(middleware1) - totalLen := nowLen + len(middleware2) - // create a new slice of middleware in order to store all handlers, the already handlers(middleware) and the new - newMiddleware := make(Middleware, totalLen) - //copy the already middleware to the just created - copy(newMiddleware, middleware1) - //start from there we finish, and store the new middleware too - copy(newMiddleware[nowLen:], middleware2) - return newMiddleware -} - -const ( - // parameterStartByte is very used on the node, it's just contains the byte for the ':' rune/char - parameterStartByte = byte(':') - // slashByte is just a byte of '/' rune/char - slashByte = byte('/') - // slash is just a string of "/" - slash = "/" - // matchEverythingByte is just a byte of '*" rune/char - matchEverythingByte = byte('*') - - isRoot entryCase = iota - hasParams - matchEverything -) - -type ( - // entryCase is the type which the type of muxEntryusing in order to determinate what type (parameterized, anything, static...) is the perticular node - entryCase uint8 - - // muxEntry is the node of a tree of the routes, - // in order to learn how this is working, google 'trie' or watch this lecture: https://www.youtube.com/watch?v=uhAUk63tLRM - // this method is used by the BSD's kernel also - muxEntry struct { - part string - entryCase entryCase - hasWildNode bool - tokens string - nodes []*muxEntry - middleware Middleware - precedence uint64 - paramsLen uint8 - } -) - -var ( - errMuxEntryConflictsWildcard = errors.New("Router: Path's part: '%s' conflicts with wildcard '%s' in the route path: '%s' !") - errMuxEntryMiddlewareAlreadyExists = errors.New("Router: Middleware were already registered for the path: '%s' !") - errMuxEntryInvalidWildcard = errors.New("Router: More than one wildcard found in the path part: '%s' in route's path: '%s' !") - errMuxEntryConflictsExistingWildcard = errors.New("Router: Wildcard for route path: '%s' conflicts with existing children in route path: '%s' !") - errMuxEntryWildcardUnnamed = errors.New("Router: Unnamed wildcard found in path: '%s' !") - errMuxEntryWildcardInvalidPlace = errors.New("Router: Wildcard is only allowed at the end of the path, in the route path: '%s' !") - errMuxEntryWildcardConflictsMiddleware = errors.New("Router: Wildcard conflicts with existing middleware for the route path: '%s' !") - errMuxEntryWildcardMissingSlash = errors.New("Router: No slash(/) were found before wildcard in the route path: '%s' !") -) - -// getParamsLen returns the parameters length from a given path -func getParamsLen(path string) uint8 { - var n uint - for i := 0; i < len(path); i++ { - if path[i] != ':' && path[i] != '*' { // ParameterStartByte & MatchEverythingByte - continue - } - n++ - } - if n >= 255 { - return 255 - } - return uint8(n) -} - -// findLower returns the smaller number between a and b -func findLower(a, b int) int { - if a <= b { - return a - } - return b -} - -// add adds a muxEntry to the existing muxEntry or to the tree if no muxEntry has the prefix of -func (e *muxEntry) add(path string, middleware Middleware) error { - fullPath := path - e.precedence++ - numParams := getParamsLen(path) - - if len(e.part) > 0 || len(e.nodes) > 0 { - loop: - for { - if numParams > e.paramsLen { - e.paramsLen = numParams - } - - i := 0 - max := findLower(len(path), len(e.part)) - for i < max && path[i] == e.part[i] { - i++ - } - - if i < len(e.part) { - node := muxEntry{ - part: e.part[i:], - hasWildNode: e.hasWildNode, - tokens: e.tokens, - nodes: e.nodes, - middleware: e.middleware, - precedence: e.precedence - 1, - } - - for i := range node.nodes { - if node.nodes[i].paramsLen > node.paramsLen { - node.paramsLen = node.nodes[i].paramsLen - } - } - - e.nodes = []*muxEntry{&node} - e.tokens = string([]byte{e.part[i]}) - e.part = path[:i] - e.middleware = nil - e.hasWildNode = false - } - - if i < len(path) { - path = path[i:] - - if e.hasWildNode { - e = e.nodes[0] - e.precedence++ - - if numParams > e.paramsLen { - e.paramsLen = numParams - } - numParams-- - - if len(path) >= len(e.part) && e.part == path[:len(e.part)] { - - if len(e.part) >= len(path) || path[len(e.part)] == slashByte { - continue loop - } - } - return errMuxEntryConflictsWildcard.Format(path, e.part, fullPath) - } - - c := path[0] - - if e.entryCase == hasParams && c == slashByte && len(e.nodes) == 1 { - e = e.nodes[0] - e.precedence++ - continue loop - } - for i := range e.tokens { - if c == e.tokens[i] { - i = e.precedenceTo(i) - e = e.nodes[i] - continue loop - } - } - - if c != parameterStartByte && c != matchEverythingByte { - - e.tokens += string([]byte{c}) - node := &muxEntry{ - paramsLen: numParams, - } - e.nodes = append(e.nodes, node) - e.precedenceTo(len(e.tokens) - 1) - e = node - } - e.addNode(numParams, path, fullPath, middleware) - return nil - - } else if i == len(path) { - if e.middleware != nil { - return errMuxEntryMiddlewareAlreadyExists.Format(fullPath) - } - e.middleware = middleware - } - return nil - } - } else { - e.addNode(numParams, path, fullPath, middleware) - e.entryCase = isRoot - } - return nil -} - -// addNode adds a muxEntry as children to other muxEntry -func (e *muxEntry) addNode(numParams uint8, path string, fullPath string, middleware Middleware) error { - var offset int - - for i, max := 0, len(path); numParams > 0; i++ { - c := path[i] - if c != parameterStartByte && c != matchEverythingByte { - continue - } - - end := i + 1 - for end < max && path[end] != slashByte { - switch path[end] { - case parameterStartByte, matchEverythingByte: - /* - panic("only one wildcard per path segment is allowed, has: '" + - path[i:] + "' in path '" + fullPath + "'") - */ - return errMuxEntryInvalidWildcard.Format(path[i:], fullPath) - default: - end++ - } - } - - if len(e.nodes) > 0 { - return errMuxEntryConflictsExistingWildcard.Format(path[i:end], fullPath) - } - - if end-i < 2 { - return errMuxEntryWildcardUnnamed.Format(fullPath) - } - - if c == parameterStartByte { - - if i > 0 { - e.part = path[offset:i] - offset = i - } - - child := &muxEntry{ - entryCase: hasParams, - paramsLen: numParams, - } - e.nodes = []*muxEntry{child} - e.hasWildNode = true - e = child - e.precedence++ - numParams-- - - if end < max { - e.part = path[offset:end] - offset = end - - child := &muxEntry{ - paramsLen: numParams, - precedence: 1, - } - e.nodes = []*muxEntry{child} - e = child - } - - } else { - if end != max || numParams > 1 { - return errMuxEntryWildcardInvalidPlace.Format(fullPath) - } - - if len(e.part) > 0 && e.part[len(e.part)-1] == '/' { - return errMuxEntryWildcardConflictsMiddleware.Format(fullPath) - } - - i-- - if path[i] != slashByte { - return errMuxEntryWildcardMissingSlash.Format(fullPath) - } - - e.part = path[offset:i] - - child := &muxEntry{ - hasWildNode: true, - entryCase: matchEverything, - paramsLen: 1, - } - e.nodes = []*muxEntry{child} - e.tokens = string(path[i]) - e = child - e.precedence++ - - child = &muxEntry{ - part: path[i:], - entryCase: matchEverything, - paramsLen: 1, - middleware: middleware, - precedence: 1, - } - e.nodes = []*muxEntry{child} - - return nil - } - } - - e.part = path[offset:] - e.middleware = middleware - - return nil -} - -// get is used by the Router, it finds and returns the correct muxEntry for a path -func (e *muxEntry) get(path string, ctx *Context) (mustRedirect bool) { -loop: - for { - if len(path) > len(e.part) { - if path[:len(e.part)] == e.part { - path = path[len(e.part):] - - if !e.hasWildNode { - c := path[0] - for i := range e.tokens { - if c == e.tokens[i] { - e = e.nodes[i] - continue loop - } - } - - mustRedirect = (path == slash && e.middleware != nil) - return - } - - e = e.nodes[0] - switch e.entryCase { - case hasParams: - - end := 0 - for end < len(path) && path[end] != '/' { - end++ - } - - ctx.Set(e.part[1:], path[:end]) - - if end < len(path) { - if len(e.nodes) > 0 { - path = path[end:] - e = e.nodes[0] - continue loop - } - - mustRedirect = (len(path) == end+1) - return - } - if ctx.Middleware = e.middleware; ctx.Middleware != nil { - return - } else if len(e.nodes) == 1 { - e = e.nodes[0] - mustRedirect = (e.part == slash && e.middleware != nil) - } - - return - - case matchEverything: - - ctx.Set(e.part[2:], path) - ctx.Middleware = e.middleware - return - - default: - return - } - } - } else if path == e.part { - if ctx.Middleware = e.middleware; ctx.Middleware != nil { - return - } - - if path == slash && e.hasWildNode && e.entryCase != isRoot { - mustRedirect = true - return - } - - for i := range e.tokens { - if e.tokens[i] == slashByte { - e = e.nodes[i] - mustRedirect = (len(e.part) == 1 && e.middleware != nil) || - (e.entryCase == matchEverything && e.nodes[0].middleware != nil) - return - } - } - - return - } - - mustRedirect = (path == slash) || - (len(e.part) == len(path)+1 && e.part[len(path)] == slashByte && - path == e.part[:len(e.part)-1] && e.middleware != nil) - return - } -} - -// precedenceTo just adds the priority of this muxEntry by an index -func (e *muxEntry) precedenceTo(index int) int { - e.nodes[index].precedence++ - _precedence := e.nodes[index].precedence - - newindex := index - for newindex > 0 && e.nodes[newindex-1].precedence < _precedence { - tmpN := e.nodes[newindex-1] - e.nodes[newindex-1] = e.nodes[newindex] - e.nodes[newindex] = tmpN - - newindex-- - } - - if newindex != index { - e.tokens = e.tokens[:newindex] + - e.tokens[index:index+1] + - e.tokens[newindex:index] + e.tokens[index+1:] - } - - return newindex -} - -// cachedMuxEntry is just a wrapper for the Cache functionality -// it seems useless but I prefer to keep the cached handler on its own memory stack, -// reason: no clojures hell in the Cache function -type cachedMuxEntry struct { - cachedHandler http.Handler -} - -func newCachedMuxEntry(s *Framework, bodyHandler HandlerFunc, expiration time.Duration) *cachedMuxEntry { - httphandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - ctx := s.AcquireCtx(w, r) - bodyHandler.Serve(ctx) - s.ReleaseCtx(ctx) - }) - - cachedHandler := httpcache.Cache(httphandler, expiration) - return &cachedMuxEntry{ - cachedHandler: cachedHandler, - } -} - -func (c *cachedMuxEntry) Serve(ctx *Context) { - c.cachedHandler.ServeHTTP(ctx.ResponseWriter, ctx.Request) -} - -type ( - // Route contains some useful information about a route - Route interface { - // Name returns the name of the route - Name() string - // Subdomain returns the subdomain,if any - Subdomain() string - // Method returns the http method - Method() string - // SetMethod sets the route's method - // requires re-build of the iris.Router - SetMethod(string) - - // Path returns the path - Path() string - - // staticPath returns the static part of the path - StaticPath() string - - // SetPath changes/sets the path for this route - SetPath(string) - // Middleware returns the slice of Handler([]Handler) registered to this route - Middleware() Middleware - // SetMiddleware changes/sets the middleware(handler(s)) for this route - SetMiddleware(Middleware) - // IsOnline returns true if the route is marked as "online" (state) - IsOnline() bool - } - - route struct { - // if no name given then it's the subdomain+path - name string - subdomain string - method string - path string - staticPath string - middleware Middleware - formattedPath string - formattedParts int - } - - bySubdomain []*route -) - -// Sorting happens when the mux's request handler initialized -func (s bySubdomain) Len() int { - return len(s) -} -func (s bySubdomain) Swap(i, j int) { - s[i], s[j] = s[j], s[i] -} -func (s bySubdomain) Less(i, j int) bool { - return len(s[i].Subdomain()) > len(s[j].Subdomain()) -} - -var _ Route = &route{} - -func newRoute(method string, subdomain string, path string, middleware Middleware) *route { - r := &route{name: path + subdomain, method: method, subdomain: subdomain, path: path, middleware: middleware} - r.formatPath() - r.calculateStaticPath() - return r -} - -func (r *route) formatPath() { - // we don't care about performance here. - n1Len := strings.Count(r.path, ":") - isMatchEverything := len(r.path) > 0 && r.path[len(r.path)-1] == matchEverythingByte - if n1Len == 0 && !isMatchEverything { - // its a static - return - } - if n1Len == 0 && isMatchEverything { - //if we have something like: /mypath/anything/* -> /mypatch/anything/%v - r.formattedPath = r.path[0:len(r.path)-2] + "%v" - r.formattedParts++ - return - } - - tempPath := r.path - splittedN1 := strings.Split(r.path, "/") - - for _, v := range splittedN1 { - if len(v) > 0 { - if v[0] == ':' || v[0] == matchEverythingByte { - r.formattedParts++ - tempPath = strings.Replace(tempPath, v, "%v", -1) // n1Len, but let it we don't care about performance here. - } - } - - } - r.formattedPath = tempPath -} - -func (r *route) calculateStaticPath() { - for i := 0; i < len(r.path); i++ { - if r.path[i] == matchEverythingByte || r.path[i] == parameterStartByte { - r.staticPath = r.path[0 : i-1] // stop at the first dynamic path symbol and set the static path to its [0:previous] - return - } - } - // not a dynamic symbol found, set its static path to its path. - r.staticPath = r.path -} - -func (r *route) setName(newName string) Route { - r.name = newName - return r -} - -func (r route) Name() string { - return r.name -} - -func (r route) Subdomain() string { - return r.subdomain -} - -func (r route) Method() string { - return r.method -} - -func (r *route) SetMethod(method string) { - r.method = method -} - -func (r route) Path() string { - return r.path -} - -func (r route) StaticPath() string { - return r.staticPath -} - -func (r *route) SetPath(s string) { - r.path = s -} - -func (r route) Middleware() Middleware { - return r.middleware -} - -func (r *route) SetMiddleware(m Middleware) { - r.middleware = m -} - -func (r route) IsOnline() bool { - return r.method != MethodNone -} - -// RouteConflicts checks for route's middleware conflicts -func RouteConflicts(r *route, with string) bool { - for _, h := range r.middleware { - if m, ok := h.(interface { - Conflicts() string - }); ok { - if c := m.Conflicts(); c == with { - return true - } - } - } - return false -} - -func (r *route) hasCors() bool { - return RouteConflicts(r, "httpmethod") -} - -const ( - // subdomainIndicator where './' exists in a registered path then it contains subdomain - subdomainIndicator = "./" - // dynamicSubdomainIndicator where a registered path starts with '*.' then it contains a dynamic subdomain, if subdomain == "*." then its dynamic - dynamicSubdomainIndicator = "*." -) - -type ( - muxTree struct { - method string - // subdomain is empty for default-hostname routes, - // ex: mysubdomain. - subdomain string - entry *muxEntry - } - - serveMux struct { - garden []*muxTree - lookups []*route - maxParameters uint8 - - onLookup func(Route) - - api *muxAPI - errorHandlers map[int]Handler - logger *log.Logger - // the main server host's name, ex: localhost, 127.0.0.1, 0.0.0.0, iris-go.com - hostname string - // if any of the trees contains not empty subdomain - hosts bool - // if false then the /something it's not the same as /something/ - // defaults to true - correctPath bool - // if enabled then the router checks and fires an error for 405 http status method not allowed too if no method compatible method was found - // by default is false - fireMethodNotAllowed bool - mu sync.Mutex - } -) - -func newServeMux(logger *log.Logger) *serveMux { - mux := &serveMux{ - lookups: make([]*route, 0), - errorHandlers: make(map[int]Handler, 0), - hostname: DefaultServerHostname, // these are changing when the server is up - correctPath: !DefaultDisablePathCorrection, - fireMethodNotAllowed: false, - logger: logger, - } - - return mux -} - -func (mux *serveMux) setHostname(h string) { - mux.hostname = h -} - -func (mux *serveMux) setCorrectPath(b bool) { - mux.correctPath = b -} - -func (mux *serveMux) setFireMethodNotAllowed(b bool) { - mux.fireMethodNotAllowed = b -} - -// registerError registers a handler to a http status -func (mux *serveMux) registerError(statusCode int, handler Handler) { - mux.mu.Lock() - func(statusCode int, handler Handler) { - mux.errorHandlers[statusCode] = HandlerFunc(func(ctx *Context) { - if w, ok := ctx.IsRecording(); ok { - w.Reset() - } - ctx.SetStatusCode(statusCode) - handler.Serve(ctx) - }) - }(statusCode, handler) - mux.mu.Unlock() -} - -// fireError fires an error -func (mux *serveMux) fireError(statusCode int, ctx *Context) { - mux.mu.Lock() - errHandler := mux.errorHandlers[statusCode] - if errHandler == nil { - errHandler = HandlerFunc(func(ctx *Context) { - if w, ok := ctx.IsRecording(); ok { - w.Reset() - } - ctx.SetStatusCode(statusCode) - ctx.WriteString(statusText[statusCode]) - }) - mux.errorHandlers[statusCode] = errHandler - } - mux.mu.Unlock() - errHandler.Serve(ctx) -} - -func (mux *serveMux) getTree(method string, subdomain string) *muxTree { - for i := range mux.garden { - t := mux.garden[i] - if t.method == method && t.subdomain == subdomain { - return t - } - } - return nil -} - -func (mux *serveMux) register(method string, subdomain string, path string, middleware Middleware) *route { - mux.mu.Lock() - defer mux.mu.Unlock() - - if subdomain != "" { - mux.hosts = true - } - - // add to the lookups, it's just a collection of routes information - lookup := newRoute(method, subdomain, path, middleware) - if mux.onLookup != nil { - mux.onLookup(lookup) - } - mux.lookups = append(mux.lookups, lookup) - - return lookup - -} - -// build collects all routes info and adds them to the registry in order to be served from the request handler -// this happens once(except when a route changes its state) when server is setting the mux's handler. -func (mux *serveMux) build() (methodEqual func(string, string) bool) { - - sort.Sort(bySubdomain(mux.lookups)) - // clear them for any case - // build may called internally to re-build the routes. - // re-build happens from BuildHandler() when a route changes its state - // from offline to online or from online to offline - mux.garden = mux.garden[0:0] - // this is not used anywhere for now, but keep it here. - mux.maxParameters = 0 - - for i := range mux.lookups { - r := mux.lookups[i] - // add to the registry tree - tree := mux.getTree(r.method, r.subdomain) - if tree == nil { - //first time we register a route to this method with this domain - tree = &muxTree{method: r.method, subdomain: r.subdomain, entry: &muxEntry{}} - mux.garden = append(mux.garden, tree) - } - // I decide that it's better to explicit give subdomain and a path to it than registeredPath(mysubdomain./something) now its: subdomain: mysubdomain., path: /something - // we have different tree for each of subdomains, now you can use everything you can use with the normal paths ( before you couldn't set /any/*path) - if err := tree.entry.add(r.path, r.middleware); err != nil { - mux.logger.Panic(err) - } - - if mp := tree.entry.paramsLen; mp > mux.maxParameters { - mux.maxParameters = mp - } - } - - methodEqual = func(reqMethod string, treeMethod string) bool { - return reqMethod == treeMethod - } - // check for cors conflicts FIRST in order to put them in OPTIONS tree also - for i := range mux.lookups { - r := mux.lookups[i] - if r.hasCors() { - // cors middleware is updated also, ref: https://github.com/kataras/iris/issues/461 - methodEqual = func(reqMethod string, treeMethod string) bool { - // preflights - return reqMethod == MethodOptions || reqMethod == treeMethod - } - break - } - } - - return - -} - -func (mux *serveMux) lookup(routeName string) *route { - for i := range mux.lookups { - if r := mux.lookups[i]; r.name == routeName { - return r - } - } - return nil -} - -//THESE ARE FROM Go Authors -var htmlReplacer = strings.NewReplacer( - "&", "&", - "<", "<", - ">", ">", - // """ is shorter than """. - `"`, """, - // "'" is shorter than "'" and apos was not in HTML until HTML5. - "'", "'", -) - -// HTMLEscape returns a string which has no valid html code -func HTMLEscape(s string) string { - return htmlReplacer.Replace(s) -} - -// BuildHandler the default Iris router when iris.Router is nil -// -// NOTE: Is called and re-set to the iris.Router when -// a route changes its state from "online" to "offline" or "offline" to "online" -// look iris.None(...) for more -// and: https://github.com/kataras/iris/issues/585 -func (mux *serveMux) BuildHandler() HandlerFunc { - - // initialize the router once - methodEqual := mux.build() - - return func(context *Context) { - routePath := context.Path() - for i := range mux.garden { - tree := mux.garden[i] - if !methodEqual(context.Request.Method, tree.method) { - continue - } - - if mux.hosts && tree.subdomain != "" { - // context.VirtualHost() is a slow method because it makes - // string.Replaces but user can understand that if subdomain then server will have some nano/or/milleseconds performance cost - requestHost := context.VirtualHostname() - if requestHost != mux.hostname { - //println(requestHost + " != " + mux.hostname) - // we have a subdomain - if strings.Index(tree.subdomain, dynamicSubdomainIndicator) != -1 { - } else { - //println(requestHost + " = " + mux.hostname) - // mux.host = iris-go.com:8080, the subdomain for example is api., - // so the host must be api.iris-go.com:8080 - if tree.subdomain+mux.hostname != requestHost { - // go to the next tree, we have a subdomain but it is not the correct - continue - } - - } - } else { - //("it's subdomain but the request is the same as the listening addr mux.host == requestHost =>" + mux.host + "=" + requestHost + " ____ and tree's subdomain was: " + tree.subdomain) - continue - } - } - - mustRedirect := tree.entry.get(routePath, context) // pass the parameters here for 0 allocation - if context.Middleware != nil { - // ok we found the correct route, serve it and exit entirely from here - //ctx.Request.Header.SetUserAgentBytes(DefaultUserAgent) - context.Do() - return - } else if mustRedirect && mux.correctPath { // && context.Method() == MethodConnect { - reqPath := routePath - pathLen := len(reqPath) - - if pathLen > 1 { - if reqPath[pathLen-1] == '/' { - reqPath = reqPath[:pathLen-1] //remove the last / - } else { - //it has path prefix, it doesn't ends with / and it hasn't be found, then just add the slash - reqPath = reqPath + "/" - } - - urlToRedirect := reqPath - - statusForRedirect := StatusMovedPermanently // StatusMovedPermanently, this document is obselte, clients caches this. - if tree.method == MethodPost || - tree.method == MethodPut || - tree.method == MethodDelete { - statusForRedirect = StatusTemporaryRedirect // To maintain POST data - } - - context.Redirect(urlToRedirect, statusForRedirect) - // RFC2616 recommends that a short note "SHOULD" be included in the - // response because older user agents may not understand 301/307. - // Shouldn't send the response for POST or HEAD; that leaves GET. - if tree.method == MethodGet { - note := "Moved Permanently.\n" - context.WriteString(note) - } - return - } - } - // not found - break - } - // https://github.com/kataras/iris/issues/469 - if mux.fireMethodNotAllowed { - for i := range mux.garden { - tree := mux.garden[i] - if !methodEqual(context.Method(), tree.method) { - continue - } - } - mux.fireError(StatusMethodNotAllowed, context) - return - } - mux.fireError(StatusNotFound, context) - } -} - -var ( - errPortAlreadyUsed = errors.New("Port is already used") - errRemoveUnix = errors.New("Unexpected error when trying to remove unix socket file. Addr: %s | Trace: %s") - errChmod = errors.New("Cannot chmod %#o for %q: %s") - errCertKeyMissing = errors.New("You should provide certFile and keyFile for TLS/SSL") - errParseTLS = errors.New("Couldn't load TLS, certFile=%q, keyFile=%q. Trace: %s") -) - -// TCPKeepAlive returns a new tcp keep alive Listener -func TCPKeepAlive(addr string) (net.Listener, error) { - ln, err := net.Listen("tcp", ParseHost(addr)) - if err != nil { - return nil, err - } - return TCPKeepAliveListener{ln.(*net.TCPListener)}, err -} - -// TCP4 returns a new tcp4 Listener -func TCP4(addr string) (net.Listener, error) { - return net.Listen("tcp4", ParseHost(addr)) -} - -// UNIX returns a new unix(file) Listener -func UNIX(addr string, mode os.FileMode) (net.Listener, error) { - if errOs := os.Remove(addr); errOs != nil && !os.IsNotExist(errOs) { - return nil, errRemoveUnix.Format(addr, errOs.Error()) - } - - listener, err := net.Listen("unix", addr) - if err != nil { - return nil, errPortAlreadyUsed.AppendErr(err) - } - - if err = os.Chmod(addr, mode); err != nil { - return nil, errChmod.Format(mode, addr, err.Error()) - } - - return listener, nil -} - -// TLS returns a new TLS Listener -func TLS(addr, certFile, keyFile string) (net.Listener, error) { - - if certFile == "" || keyFile == "" { - return nil, errCertKeyMissing - } - - cert, err := tls.LoadX509KeyPair(certFile, keyFile) - if err != nil { - return nil, errParseTLS.Format(certFile, keyFile, err) - } - - return CERT(addr, cert) -} - -// CERT returns a listener which contans tls.Config with the provided certificate, use for ssl -func CERT(addr string, cert tls.Certificate) (net.Listener, error) { - ln, err := TCP4(addr) - if err != nil { - return nil, err - } - - tlsConfig := &tls.Config{ - Certificates: []tls.Certificate{cert}, - PreferServerCipherSuites: true, - } - return tls.NewListener(ln, tlsConfig), nil -} - -// LETSENCRYPT returns a new Automatic TLS Listener using letsencrypt.org service -// receives two parameters, the first is the domain of the server -// and the second is optionally, the cache file, if you skip it then the cache directory is "./letsencrypt.cache" -// if you want to disable cache file then simple give it a value of empty string "" -// -// supports localhost domains for testing, -// but I recommend you to use the LETSENCRYPTPROD if you gonna to use it on production -func LETSENCRYPT(addr string, cacheFileOptional ...string) (net.Listener, error) { - if portIdx := strings.IndexByte(addr, ':'); portIdx == -1 { - addr += ":443" - } - - ln, err := TCP4(addr) - if err != nil { - return nil, err - } - - cacheFile := "./letsencrypt.cache" - if len(cacheFileOptional) > 0 { - cacheFile = cacheFileOptional[0] - } - - var m letsencrypt.Manager - - if cacheFile != "" { - if err = m.CacheFile(cacheFile); err != nil { - return nil, err - } - } - - tlsConfig := &tls.Config{GetCertificate: m.GetCertificate} - tlsLn := tls.NewListener(ln, tlsConfig) - - return tlsLn, nil -} - -// LETSENCRYPTPROD returns a new Automatic TLS Listener using letsencrypt.org service -// receives two parameters, the first is the domain of the server -// and the second is optionally, the cache directory, if you skip it then the cache directory is "./certcache" -// if you want to disable cache directory then simple give it a value of empty string "" -// -// does NOT supports localhost domains for testing, use LETSENCRYPT instead. -// -// this is the recommended function to use when you're ready for production state -func LETSENCRYPTPROD(addr string, cacheDirOptional ...string) (net.Listener, error) { - if portIdx := strings.IndexByte(addr, ':'); portIdx == -1 { - addr += ":443" - } - - ln, err := TCP4(addr) - if err != nil { - return nil, err - } - - cacheDir := "./certcache" - if len(cacheDirOptional) > 0 { - cacheDir = cacheDirOptional[0] - } - - m := autocert.Manager{ - Prompt: autocert.AcceptTOS, - } // HostPolicy is missing, if user wants it, then she/he should manually - // configure the autocertmanager and use the `iris.Serve` to pass that listener - - if cacheDir == "" { - // then the user passed empty by own will, then I guess she/he doesnt' want any cache directory - } else { - m.Cache = autocert.DirCache(cacheDir) - } - - tlsConfig := &tls.Config{GetCertificate: m.GetCertificate} - tlsLn := tls.NewListener(ln, tlsConfig) - - return tlsLn, nil -} - -// TCPKeepAliveListener sets TCP keep-alive timeouts on accepted -// connections. -// Dead TCP connections (e.g. closing laptop mid-download) eventually -// go away -// It is not used by default if you want to pass a keep alive listener -// then just pass the child listener, example: -// listener := iris.TCPKeepAliveListener{iris.TCP4(":8080").(*net.TCPListener)} -type TCPKeepAliveListener struct { - *net.TCPListener -} - -// Accept implements the listener and sets the keep alive period which is 3minutes -func (ln TCPKeepAliveListener) Accept() (c net.Conn, err error) { - tc, err := ln.AcceptTCP() - if err != nil { - return - } - tc.SetKeepAlive(true) - tc.SetKeepAlivePeriod(3 * time.Minute) - return tc, nil -} - -// ParseHost tries to convert a given string to an address which is compatible with net.Listener and server -func ParseHost(addr string) string { - // check if addr has :port, if not do it +:80 ,we need the hostname for many cases - a := addr - if a == "" { - // check for os environments - if oshost := os.Getenv("ADDR"); oshost != "" { - a = oshost - } else if oshost := os.Getenv("HOST"); oshost != "" { - a = oshost - } else if oshost := os.Getenv("HOSTNAME"); oshost != "" { - a = oshost - // check for port also here - if osport := os.Getenv("PORT"); osport != "" { - a += ":" + osport - } - } else if osport := os.Getenv("PORT"); osport != "" { - a = ":" + osport - } else { - a = ":http" - } - } - if portIdx := strings.IndexByte(a, ':'); portIdx == 0 { - if a[portIdx:] == ":https" { - a = DefaultServerHostname + ":443" - } else { - // if contains only :port ,then the : is the first letter, so we dont have setted a hostname, lets set it - a = DefaultServerHostname + a - } - } - - /* changed my mind, don't add 80, this will cause problems on unix listeners, and it's not really necessary because we take the port using parsePort - if portIdx := strings.IndexByte(a, ':'); portIdx < 0 { - // missing port part, add it - a = a + ":80" - }*/ - - return a -} - -// ParseHostname receives an addr of form host[:port] and returns the hostname part of it -// ex: localhost:8080 will return the `localhost`, mydomain.com:8080 will return the 'mydomain' -func ParseHostname(addr string) string { - idx := strings.IndexByte(addr, ':') - if idx == 0 { - // only port, then return 0.0.0.0 - return "0.0.0.0" - } else if idx > 0 { - return addr[0:idx] - } - // it's already hostname - return addr -} - -// ParsePort receives an addr of form host[:port] and returns the port part of it -// ex: localhost:8080 will return the `8080`, mydomain.com will return the '80' -func ParsePort(addr string) int { - if portIdx := strings.IndexByte(addr, ':'); portIdx != -1 { - afP := addr[portIdx+1:] - p, err := strconv.Atoi(afP) - if err == nil { - return p - } else if afP == "https" { // it's not number, check if it's :https - return 443 - } - } - return 80 -} - -const ( - // SchemeHTTPS returns "https://" (full) - SchemeHTTPS = "https://" - // SchemeHTTP returns "http://" (full) - SchemeHTTP = "http://" -) - -// ParseScheme returns the scheme based on the host,addr,domain -// Note: the full scheme not just http*,https* *http:// *https:// -func ParseScheme(domain string) string { - // pure check - if strings.HasPrefix(domain, SchemeHTTPS) || ParsePort(domain) == 443 { - return SchemeHTTPS - } - return SchemeHTTP -} - -// ProxyHandler returns a new net/http.Handler which works as 'proxy', maybe doesn't suits you look its code before using that in production -var ProxyHandler = func(redirectSchemeAndHost string) http.HandlerFunc { - return func(w http.ResponseWriter, r *http.Request) { - - // override the handler and redirect all requests to this addr - redirectTo := redirectSchemeAndHost - fakehost := r.URL.Host - path := r.URL.EscapedPath() - if strings.Count(fakehost, ".") >= 3 { // propably a subdomain, pure check but doesn't matters don't worry - if sufIdx := strings.LastIndexByte(fakehost, '.'); sufIdx > 0 { - // check if the last part is a number instead of .com/.gr... - // if it's number then it's propably is 0.0.0.0 or 127.0.0.1... so it shouldn' use subdomain - if _, err := strconv.Atoi(fakehost[sufIdx+1:]); err != nil { - // it's not number then process the try to parse the subdomain - redirectScheme := ParseScheme(redirectSchemeAndHost) - realHost := strings.Replace(redirectSchemeAndHost, redirectScheme, "", 1) - redirectHost := strings.Replace(fakehost, fakehost, realHost, 1) - redirectTo = redirectScheme + redirectHost + path - http.Redirect(w, r, redirectTo, StatusMovedPermanently) - return - } - } - } - if path != "/" { - redirectTo += path - } - if redirectTo == r.URL.String() { - return - } - - // redirectTo := redirectSchemeAndHost + r.RequestURI - - http.Redirect(w, r, redirectTo, StatusMovedPermanently) - } -} - -// Proxy not really a proxy, it's just -// starts a server listening on proxyAddr but redirects all requests to the redirectToSchemeAndHost+$path -// nothing special, use it only when you want to start a secondary server which its only work is to redirect from one requested path to another -// -// returns a close function -func Proxy(proxyAddr string, redirectSchemeAndHost string) func() error { - proxyAddr = ParseHost(proxyAddr) - - // override the handler and redirect all requests to this addr - h := ProxyHandler(redirectSchemeAndHost) - prx := New(OptionDisableBanner(true)) - prx.Router = h - - go prx.Listen(proxyAddr) - if ok := <-prx.Available; !ok { - prx.Logger.Panic("Unexpected error: proxy server cannot start, please report this as bug!!") - } - - return func() error { return prx.Close() } -} diff --git a/http_test.go b/http_test.go deleted file mode 100644 index eca75b1e..00000000 --- a/http_test.go +++ /dev/null @@ -1,792 +0,0 @@ -// Black-box Testing -package iris_test - -import ( - "fmt" - "io/ioutil" - "math/rand" - "net/http" - "os" - "strconv" - "testing" - "time" - - "github.com/gavv/httpexpect" - "github.com/kataras/iris" - "github.com/kataras/iris/httptest" -) - -const ( - testTLSCert = `-----BEGIN CERTIFICATE----- -MIIDAzCCAeugAwIBAgIJAP0pWSuIYyQCMA0GCSqGSIb3DQEBBQUAMBgxFjAUBgNV -BAMMDWxvY2FsaG9zdDozMzEwHhcNMTYxMjI1MDk1OTI3WhcNMjYxMjIzMDk1OTI3 -WjAYMRYwFAYDVQQDDA1sb2NhbGhvc3Q6MzMxMIIBIjANBgkqhkiG9w0BAQEFAAOC -AQ8AMIIBCgKCAQEA5vETjLa+8W856rWXO1xMF/CLss9vn5xZhPXKhgz+D7ogSAXm -mWP53eeBUGC2r26J++CYfVqwOmfJEu9kkGUVi8cGMY9dHeIFPfxD31MYX175jJQe -tu0WeUII7ciNsSUDyBMqsl7yi1IgN7iLONM++1+QfbbmNiEbghRV6icEH6M+bWlz -3YSAMEdpK3mg2gsugfLKMwJkaBKEehUNMySRlIhyLITqt1exYGaggRd1zjqUpqpD -sL2sRVHJ3qHGkSh8nVy8MvG8BXiFdYQJP3mCQDZzruCyMWj5/19KAyu7Cto3Bcvu -PgujnwRtU+itt8WhZUVtU1n7Ivf6lMJTBcc4OQIDAQABo1AwTjAdBgNVHQ4EFgQU -MXrBvbILQmiwjUj19aecF2N+6IkwHwYDVR0jBBgwFoAUMXrBvbILQmiwjUj19aec -F2N+6IkwDAYDVR0TBAUwAwEB/zANBgkqhkiG9w0BAQUFAAOCAQEA4zbFml1t9KXJ -OijAV8gALePR8v04DQwJP+jsRxXw5zzhc8Wqzdd2hjUd07mfRWAvmyywrmhCV6zq -OHznR+aqIqHtm0vV8OpKxLoIQXavfBd6axEXt3859RDM4xJNwIlxs3+LWGPgINud -wjJqjyzSlhJpQpx4YZ5Da+VMiqAp8N1UeaZ5lBvmSDvoGh6HLODSqtPlWMrldRW9 -AfsXVxenq81MIMeKW2fSOoPnWZ4Vjf1+dSlbJE/DD4zzcfbyfgY6Ep/RrUltJ3ag -FQbuNTQlgKabe21dSL9zJ2PengVKXl4Trl+4t/Kina9N9Jw535IRCSwinD6a/2Ca -m7DnVXFiVA== ------END CERTIFICATE----- -` - - testTLSKey = `-----BEGIN RSA PRIVATE KEY----- -MIIEpAIBAAKCAQEA5vETjLa+8W856rWXO1xMF/CLss9vn5xZhPXKhgz+D7ogSAXm -mWP53eeBUGC2r26J++CYfVqwOmfJEu9kkGUVi8cGMY9dHeIFPfxD31MYX175jJQe -tu0WeUII7ciNsSUDyBMqsl7yi1IgN7iLONM++1+QfbbmNiEbghRV6icEH6M+bWlz -3YSAMEdpK3mg2gsugfLKMwJkaBKEehUNMySRlIhyLITqt1exYGaggRd1zjqUpqpD -sL2sRVHJ3qHGkSh8nVy8MvG8BXiFdYQJP3mCQDZzruCyMWj5/19KAyu7Cto3Bcvu -PgujnwRtU+itt8WhZUVtU1n7Ivf6lMJTBcc4OQIDAQABAoIBAQCTLE0eHpPevtg0 -+FaRUMd5diVA5asoF3aBIjZXaU47bY0G+SO02x6wSMmDFK83a4Vpy/7B3Bp0jhF5 -DLCUyKaLdmE/EjLwSUq37ty+JHFizd7QtNBCGSN6URfpmSabHpCjX3uVQqblHIhF -mki3BQCdJ5CoXPemxUCHjDgYSZb6JVNIPJExjekc0+4A2MYWMXV6Wr86C7AY3659 -KmveZpC3gOkLA/g/IqDQL/QgTq7/3eloHaO+uPBihdF56do4eaOO0jgFYpl8V7ek -PZhHfhuPZV3oq15+8Vt77ngtjUWVI6qX0E3ilh+V5cof+03q0FzHPVe3zBUNXcm0 -OGz19u/FAoGBAPSm4Aa4xs/ybyjQakMNix9rak66ehzGkmlfeK5yuQ/fHmTg8Ac+ -ahGs6A3lFWQiyU6hqm6Qp0iKuxuDh35DJGCWAw5OUS/7WLJtu8fNFch6iIG29rFs -s+Uz2YLxJPebpBsKymZUp7NyDRgEElkiqsREmbYjLrc8uNKkDy+k14YnAoGBAPGn -ZlN0Mo5iNgQStulYEP5pI7WOOax9KOYVnBNguqgY9c7fXVXBxChoxt5ebQJWG45y -KPG0hB0bkA4YPu4bTRf5acIMpjFwcxNlmwdc4oCkT4xqAFs9B/AKYZgkf4IfKHqW -P9PD7TbUpkaxv25bPYwUSEB7lPa+hBtRyN9Wo6qfAoGAPBkeISiU1hJE0i7YW55h -FZfKZoqSYq043B+ywo+1/Dsf+UH0VKM1ZSAnZPpoVc/hyaoW9tAb98r0iZ620wJl -VkCjgYklknbY5APmw/8SIcxP6iVq1kzQqDYjcXIRVa3rEyWEcLzM8VzL8KFXbIQC -lPIRHFfqKuMEt+HLRTXmJ7MCgYAHGvv4QjdmVl7uObqlG9DMGj1RjlAF0VxNf58q -NrLmVG2N2qV86wigg4wtZ6te4TdINfUcPkmQLYpLz8yx5Z2bsdq5OPP+CidoD5nC -WqnSTIKGR2uhQycjmLqL5a7WHaJsEFTqHh2wego1k+5kCUzC/KmvM7MKmkl6ICp+ -3qZLUwKBgQCDOhKDwYo1hdiXoOOQqg/LZmpWOqjO3b4p99B9iJqhmXN0GKXIPSBh -5nqqmGsG8asSQhchs7EPMh8B80KbrDTeidWskZuUoQV27Al1UEmL6Zcl83qXD6sf -k9X9TwWyZtp5IL1CAEd/Il9ZTXFzr3lNaN8LCFnU+EIsz1YgUW8LTg== ------END RSA PRIVATE KEY----- -` -) - -func TestParseAddr(t *testing.T) { - - // test hosts - expectedHost1 := "mydomain.com:1993" - expectedHost2 := "mydomain.com" - expectedHost3 := iris.DefaultServerHostname + ":9090" - expectedHost4 := "mydomain.com:443" - - host1 := iris.ParseHost(expectedHost1) - host2 := iris.ParseHost(expectedHost2) - host3 := iris.ParseHost(":9090") - host4 := iris.ParseHost(expectedHost4) - - if host1 != expectedHost1 { - t.Fatalf("Expecting server 1's host to be %s but we got %s", expectedHost1, host1) - } - if host2 != expectedHost2 { - t.Fatalf("Expecting server 2's host to be %s but we got %s", expectedHost2, host2) - } - if host3 != expectedHost3 { - t.Fatalf("Expecting server 3's host to be %s but we got %s", expectedHost3, host3) - } - if host4 != expectedHost4 { - t.Fatalf("Expecting server 4's host to be %s but we got %s", expectedHost4, host4) - } - - // test hostname - expectedHostname1 := "mydomain.com" - expectedHostname2 := "mydomain.com" - expectedHostname3 := iris.DefaultServerHostname - expectedHostname4 := "mydomain.com" - - hostname1 := iris.ParseHostname(host1) - hostname2 := iris.ParseHostname(host2) - hostname3 := iris.ParseHostname(host3) - hostname4 := iris.ParseHostname(host4) - if hostname1 != expectedHostname1 { - t.Fatalf("Expecting server 1's hostname to be %s but we got %s", expectedHostname1, hostname1) - } - - if hostname2 != expectedHostname2 { - t.Fatalf("Expecting server 2's hostname to be %s but we got %s", expectedHostname2, hostname2) - } - - if hostname3 != expectedHostname3 { - t.Fatalf("Expecting server 3's hostname to be %s but we got %s", expectedHostname3, hostname3) - } - - if hostname4 != expectedHostname4 { - t.Fatalf("Expecting server 4's hostname to be %s but we got %s", expectedHostname4, hostname4) - } - - // test scheme, no need to test fullhost(scheme+host) - expectedScheme1 := iris.SchemeHTTP - expectedScheme2 := iris.SchemeHTTP - expectedScheme3 := iris.SchemeHTTP - expectedScheme4 := iris.SchemeHTTPS - scheme1 := iris.ParseScheme(host1) - scheme2 := iris.ParseScheme(host2) - scheme3 := iris.ParseScheme(host3) - scheme4 := iris.ParseScheme(host4) - if scheme1 != expectedScheme1 { - t.Fatalf("Expecting server 1's hostname to be %s but we got %s", expectedScheme1, scheme1) - } - - if scheme2 != expectedScheme2 { - t.Fatalf("Expecting server 2's hostname to be %s but we got %s", expectedScheme2, scheme2) - } - - if scheme3 != expectedScheme3 { - t.Fatalf("Expecting server 3's hostname to be %s but we got %s", expectedScheme3, scheme3) - } - - if scheme4 != expectedScheme4 { - t.Fatalf("Expecting server 4's hostname to be %s but we got %s", expectedScheme4, scheme4) - } -} - -func getRandomNumber(min int, max int) int { - rand.Seed(time.Now().Unix()) - return rand.Intn(max-min) + min -} - -// works as -// defer listenTLS(iris.Default, hostTLS)() -func listenTLS(api *iris.Framework, hostTLS string) func() { - api.Close() // close any prev listener - api.Config.DisableBanner = true - // create the key and cert files on the fly, and delete them when this test finished - certFile, ferr := ioutil.TempFile("", "cert") - - if ferr != nil { - api.Logger.Panic(ferr.Error()) - } - - keyFile, ferr := ioutil.TempFile("", "key") - if ferr != nil { - api.Logger.Panic(ferr.Error()) - } - - certFile.WriteString(testTLSCert) - keyFile.WriteString(testTLSKey) - - go api.ListenTLS(hostTLS, certFile.Name(), keyFile.Name()) - if ok := <-api.Available; !ok { - api.Logger.Panic("Unexpected error: server cannot start, please report this as bug!!") - } - - return func() { - certFile.Close() - time.Sleep(50 * time.Millisecond) - os.Remove(certFile.Name()) - - keyFile.Close() - time.Sleep(50 * time.Millisecond) - os.Remove(keyFile.Name()) - } -} - -// Contains the server test for multi running servers -func TestMultiRunningServers_v1_PROXY(t *testing.T) { - api := iris.New() - - host := "localhost" - hostTLS := host + ":" + strconv.Itoa(getRandomNumber(1919, 2221)) - api.Get("/", func(ctx *iris.Context) { - ctx.Writef("Hello from %s", hostTLS) - }) - // println("running main on: " + hostTLS) - - defer listenTLS(api, hostTLS)() - - e := httptest.New(api, t, httptest.ExplicitURL(true)) - e.Request("GET", "/").Expect().Status(iris.StatusOK).Body().Equal("Hello from " + hostTLS) - - // proxy http to https - proxyHost := host + ":" + strconv.Itoa(getRandomNumber(3300, 3340)) - // println("running proxy on: " + proxyHost) - - iris.Proxy(proxyHost, "https://"+hostTLS) - - // proxySrv := &http.Server{Addr: proxyHost, Handler: iris.ProxyHandler("https://" + hostTLS)} - // go proxySrv.ListenAndServe() - // time.Sleep(3 * time.Second) - - eproxy := httptest.NewInsecure("http://"+proxyHost, t, httptest.ExplicitURL(true)) - eproxy.Request("GET", "/").Expect().Status(iris.StatusOK).Body().Equal("Hello from " + hostTLS) -} - -// Contains the server test for multi running servers -func TestMultiRunningServers_v2(t *testing.T) { - api := iris.New() - - domain := "localhost" - hostTLS := domain + ":" + strconv.Itoa(getRandomNumber(2222, 2229)) - srv1Host := domain + ":" + strconv.Itoa(getRandomNumber(4446, 5444)) - srv2Host := domain + ":" + strconv.Itoa(getRandomNumber(7778, 8887)) - - api.Get("/", func(ctx *iris.Context) { - ctx.Writef("Hello from %s", hostTLS) - }) - - defer listenTLS(api, hostTLS)() - - // using the same iris' handler but not as proxy, just the same handler - srv2 := &http.Server{Handler: api.Router, Addr: srv2Host} - go srv2.ListenAndServe() - - // using the proxy handler - srv1 := &http.Server{Handler: iris.ProxyHandler("https://" + hostTLS), Addr: srv1Host} - go srv1.ListenAndServe() - time.Sleep(500 * time.Millisecond) // wait a little for the http servers - - e := httptest.New(api, t, httptest.ExplicitURL(true)) - e.Request("GET", "/").Expect().Status(iris.StatusOK).Body().Equal("Hello from " + hostTLS) - - eproxy1 := httptest.NewInsecure("http://"+srv1Host, t, httptest.ExplicitURL(true)) - eproxy1.Request("GET", "/").Expect().Status(iris.StatusOK).Body().Equal("Hello from " + hostTLS) - - eproxy2 := httptest.NewInsecure("http://"+srv2Host, t) - eproxy2.Request("GET", "/").Expect().Status(iris.StatusOK).Body().Equal("Hello from " + hostTLS) - -} - -const ( - testEnableSubdomain = true - testSubdomain = "mysubdomain" -) - -func testSubdomainHost() string { - s := testSubdomain + "." + iris.Default.Config.VHost - return s -} - -func testSubdomainURL() string { - subdomainHost := testSubdomainHost() - return iris.Default.Config.VScheme + subdomainHost -} - -func subdomainTester(e *httpexpect.Expect) *httpexpect.Expect { - es := e.Builder(func(req *httpexpect.Request) { - req.WithURL(testSubdomainURL()) - }) - return es -} - -type param struct { - Key string - Value string -} - -type testRoute struct { - Method string - Path string - RequestPath string - RequestQuery string - Body string - Status int - Register bool - Params []param - URLParams []param -} - -func TestMuxSimple(t *testing.T) { - testRoutes := []testRoute{ - // FOUND - registered - {"GET", "/test_get", "/test_get", "", "hello, get!", 200, true, nil, nil}, - {"POST", "/test_post", "/test_post", "", "hello, post!", 200, true, nil, nil}, - {"PUT", "/test_put", "/test_put", "", "hello, put!", 200, true, nil, nil}, - {"DELETE", "/test_delete", "/test_delete", "", "hello, delete!", 200, true, nil, nil}, - {"HEAD", "/test_head", "/test_head", "", "hello, head!", 200, true, nil, nil}, - {"OPTIONS", "/test_options", "/test_options", "", "hello, options!", 200, true, nil, nil}, - {"CONNECT", "/test_connect", "/test_connect", "", "hello, connect!", 200, true, nil, nil}, - {"PATCH", "/test_patch", "/test_patch", "", "hello, patch!", 200, true, nil, nil}, - {"TRACE", "/test_trace", "/test_trace", "", "hello, trace!", 200, true, nil, nil}, - // NOT FOUND - not registered - {"GET", "/test_get_nofound", "/test_get_nofound", "", "Not Found", 404, false, nil, nil}, - {"POST", "/test_post_nofound", "/test_post_nofound", "", "Not Found", 404, false, nil, nil}, - {"PUT", "/test_put_nofound", "/test_put_nofound", "", "Not Found", 404, false, nil, nil}, - {"DELETE", "/test_delete_nofound", "/test_delete_nofound", "", "Not Found", 404, false, nil, nil}, - {"HEAD", "/test_head_nofound", "/test_head_nofound", "", "Not Found", 404, false, nil, nil}, - {"OPTIONS", "/test_options_nofound", "/test_options_nofound", "", "Not Found", 404, false, nil, nil}, - {"CONNECT", "/test_connect_nofound", "/test_connect_nofound", "", "Not Found", 404, false, nil, nil}, - {"PATCH", "/test_patch_nofound", "/test_patch_nofound", "", "Not Found", 404, false, nil, nil}, - {"TRACE", "/test_trace_nofound", "/test_trace_nofound", "", "Not Found", 404, false, nil, nil}, - // Parameters - {"GET", "/test_get_parameter1/:name", "/test_get_parameter1/iris", "", "name=iris", 200, true, []param{{"name", "iris"}}, nil}, - {"GET", "/test_get_parameter2/:name/details/:something", "/test_get_parameter2/iris/details/anything", "", "name=iris,something=anything", 200, true, []param{{"name", "iris"}, {"something", "anything"}}, nil}, - {"GET", "/test_get_parameter2/:name/details/:something/*else", "/test_get_parameter2/iris/details/anything/elsehere", "", "name=iris,something=anything,else=/elsehere", 200, true, []param{{"name", "iris"}, {"something", "anything"}, {"else", "elsehere"}}, nil}, - // URL Parameters - {"GET", "/test_get_urlparameter1/first", "/test_get_urlparameter1/first", "name=irisurl", "name=irisurl", 200, true, nil, []param{{"name", "irisurl"}}}, - {"GET", "/test_get_urlparameter2/second", "/test_get_urlparameter2/second", "name=irisurl&something=anything", "name=irisurl,something=anything", 200, true, nil, []param{{"name", "irisurl"}, {"something", "anything"}}}, - {"GET", "/test_get_urlparameter2/first/second/third", "/test_get_urlparameter2/first/second/third", "name=irisurl&something=anything&else=elsehere", "name=irisurl,something=anything,else=elsehere", 200, true, nil, []param{{"name", "irisurl"}, {"something", "anything"}, {"else", "elsehere"}}}, - } - - iris.ResetDefault() - - for idx := range testRoutes { - r := testRoutes[idx] - if r.Register { - iris.HandleFunc(r.Method, r.Path, func(ctx *iris.Context) { - ctx.SetStatusCode(r.Status) - if r.Params != nil && len(r.Params) > 0 { - ctx.WriteString(ctx.ParamsSentence()) - } else if r.URLParams != nil && len(r.URLParams) > 0 { - if len(r.URLParams) != len(ctx.URLParams()) { - t.Fatalf("Error when comparing length of url parameters %d != %d", len(r.URLParams), len(ctx.URLParams())) - } - paramsKeyVal := "" - for idxp, p := range r.URLParams { - val := ctx.URLParam(p.Key) - paramsKeyVal += p.Key + "=" + val + "," - if idxp == len(r.URLParams)-1 { - paramsKeyVal = paramsKeyVal[0 : len(paramsKeyVal)-1] - } - } - ctx.WriteString(paramsKeyVal) - } else { - ctx.WriteString(r.Body) - } - - }) - } - } - - e := httptest.New(iris.Default, t, httptest.Debug(true)) - - // run the tests (1) - for idx := range testRoutes { - r := testRoutes[idx] - e.Request(r.Method, r.RequestPath).WithQueryString(r.RequestQuery). - Expect(). - Status(r.Status).Body().Equal(r.Body) - } - -} - -func TestMuxSimpleParty(t *testing.T) { - iris.ResetDefault() - - h := func(c *iris.Context) { c.WriteString(c.Request.URL.Host + c.Request.RequestURI) } - - if testEnableSubdomain { - subdomainParty := iris.Party(testSubdomain + ".") - { - subdomainParty.Get("/", h) - subdomainParty.Get("/path1", h) - subdomainParty.Get("/path2", h) - subdomainParty.Get("/namedpath/:param1/something/:param2", h) - subdomainParty.Get("/namedpath/:param1/something/:param2/else", h) - } - } - - // simple - p := iris.Party("/party1") - { - p.Get("/", h) - p.Get("/path1", h) - p.Get("/path2", h) - p.Get("/namedpath/:param1/something/:param2", h) - p.Get("/namedpath/:param1/something/:param2/else", h) - } - - iris.Default.Config.VHost = "0.0.0.0:" + strconv.Itoa(getRandomNumber(2222, 2399)) - // iris.Default.Config.Tester.Debug = true - // iris.Default.Config.Tester.ExplicitURL = true - e := httptest.New(iris.Default, t) - - request := func(reqPath string) { - - e.Request("GET", reqPath). - Expect(). - Status(iris.StatusOK).Body().Equal(iris.Default.Config.VHost + reqPath) - } - - // run the tests - request("/party1/") - request("/party1/path1") - request("/party1/path2") - request("/party1/namedpath/theparam1/something/theparam2") - request("/party1/namedpath/theparam1/something/theparam2/else") - - if testEnableSubdomain { - es := subdomainTester(e) - subdomainRequest := func(reqPath string) { - es.Request("GET", reqPath). - Expect(). - Status(iris.StatusOK).Body().Equal(testSubdomainHost() + reqPath) - } - - subdomainRequest("/") - subdomainRequest("/path1") - subdomainRequest("/path2") - subdomainRequest("/namedpath/theparam1/something/theparam2") - subdomainRequest("/namedpath/theparam1/something/theparam2/else") - } -} - -// TestRealSubdomainSimple exists because the local examples some times passed but... -// hope that travis will not has problem with this -func TestRealSubdomainSimple(t *testing.T) { - - api := iris.New() - host := "localhost:" + strconv.Itoa(getRandomNumber(4732, 4958)) - subdomain := "admin" - subdomainHost := subdomain + "." + host - - // no order, you can register subdomains at the end also. - admin := api.Party(subdomain + ".") - { - // admin.mydomain.com - admin.Get("/", func(c *iris.Context) { - c.Writef("INDEX FROM %s", subdomainHost) - }) - // admin.mydomain.com/hey - admin.Get("/hey", func(c *iris.Context) { - c.Writef(subdomainHost + c.Request.RequestURI) - }) - // admin.mydomain.com/hey2 - admin.Get("/hey2", func(c *iris.Context) { - c.Writef(subdomainHost + c.Request.RequestURI) - }) - } - - // mydomain.com/ - api.Get("/", func(c *iris.Context) { - c.Writef("INDEX FROM no-subdomain hey") - }) - - // mydomain.com/hey - api.Get("/hey", func(c *iris.Context) { - c.Writef("HEY FROM no-subdomain hey") - }) - - api.Config.DisableBanner = true - go api.Listen(host) - - <-api.Available - - e := httptest.New(api, t, httptest.ExplicitURL(true)) - - e.GET("/").Expect().Status(iris.StatusOK).Body().Equal("INDEX FROM no-subdomain hey") - e.GET("/hey").Expect().Status(iris.StatusOK).Body().Equal("HEY FROM no-subdomain hey") - - sub := e.Builder(func(req *httpexpect.Request) { - req.WithURL("http://admin." + host) - }) - - sub.GET("/").Expect().Status(iris.StatusOK).Body().Equal("INDEX FROM " + subdomainHost) - sub.GET("/hey").Expect().Status(iris.StatusOK).Body().Equal(subdomainHost + "/hey") - sub.GET("/hey2").Expect().Status(iris.StatusOK).Body().Equal(subdomainHost + "/hey2") -} - -func TestMuxPathEscape(t *testing.T) { - iris.ResetDefault() - iris.Config.EnablePathEscape = true - - iris.Get("/details/:name", func(ctx *iris.Context) { - name := ctx.ParamDecoded("name") - highlight := ctx.URLParam("highlight") - - ctx.Text(iris.StatusOK, fmt.Sprintf("name=%s,highlight=%s", name, highlight)) - }) - - e := httptest.New(iris.Default, t) - - e.GET("/details/Sakamoto desu ga"). - WithQuery("highlight", "text"). - Expect().Status(iris.StatusOK).Body().Equal("name=Sakamoto desu ga,highlight=text") -} - -func TestMuxDecodeURL(t *testing.T) { - iris.ResetDefault() - - iris.Get("/encoding/:url", func(ctx *iris.Context) { - url := ctx.ParamDecoded("url") - - ctx.SetStatusCode(iris.StatusOK) - ctx.WriteString(url) - }) - - e := httptest.New(iris.Default, t) - - e.GET("/encoding/http%3A%2F%2Fsome-url.com").Expect().Status(iris.StatusOK).Body().Equal("http://some-url.com") -} - -func TestMuxCustomErrors(t *testing.T) { - var ( - notFoundMessage = "Iris custom message for 404 not found" - internalServerMessage = "Iris custom message for 500 internal server error" - testRoutesCustomErrors = []testRoute{ - // NOT FOUND CUSTOM ERRORS - not registered - {"GET", "/test_get_nofound_custom", "/test_get_nofound_custom", "", notFoundMessage, 404, false, nil, nil}, - {"POST", "/test_post_nofound_custom", "/test_post_nofound_custom", "", notFoundMessage, 404, false, nil, nil}, - {"PUT", "/test_put_nofound_custom", "/test_put_nofound_custom", "", notFoundMessage, 404, false, nil, nil}, - {"DELETE", "/test_delete_nofound_custom", "/test_delete_nofound_custom", "", notFoundMessage, 404, false, nil, nil}, - {"HEAD", "/test_head_nofound_custom", "/test_head_nofound_custom", "", notFoundMessage, 404, false, nil, nil}, - {"OPTIONS", "/test_options_nofound_custom", "/test_options_nofound_custom", "", notFoundMessage, 404, false, nil, nil}, - {"CONNECT", "/test_connect_nofound_custom", "/test_connect_nofound_custom", "", notFoundMessage, 404, false, nil, nil}, - {"PATCH", "/test_patch_nofound_custom", "/test_patch_nofound_custom", "", notFoundMessage, 404, false, nil, nil}, - {"TRACE", "/test_trace_nofound_custom", "/test_trace_nofound_custom", "", notFoundMessage, 404, false, nil, nil}, - // SERVER INTERNAL ERROR 500 PANIC CUSTOM ERRORS - registered - {"GET", "/test_get_panic_custom", "/test_get_panic_custom", "", internalServerMessage, 500, true, nil, nil}, - {"POST", "/test_post_panic_custom", "/test_post_panic_custom", "", internalServerMessage, 500, true, nil, nil}, - {"PUT", "/test_put_panic_custom", "/test_put_panic_custom", "", internalServerMessage, 500, true, nil, nil}, - {"DELETE", "/test_delete_panic_custom", "/test_delete_panic_custom", "", internalServerMessage, 500, true, nil, nil}, - {"HEAD", "/test_head_panic_custom", "/test_head_panic_custom", "", internalServerMessage, 500, true, nil, nil}, - {"OPTIONS", "/test_options_panic_custom", "/test_options_panic_custom", "", internalServerMessage, 500, true, nil, nil}, - {"CONNECT", "/test_connect_panic_custom", "/test_connect_panic_custom", "", internalServerMessage, 500, true, nil, nil}, - {"PATCH", "/test_patch_panic_custom", "/test_patch_panic_custom", "", internalServerMessage, 500, true, nil, nil}, - {"TRACE", "/test_trace_panic_custom", "/test_trace_panic_custom", "", internalServerMessage, 500, true, nil, nil}, - } - ) - iris.ResetDefault() - // first register the testRoutes needed - for _, r := range testRoutesCustomErrors { - if r.Register { - iris.HandleFunc(r.Method, r.Path, func(ctx *iris.Context) { - ctx.EmitError(r.Status) - }) - } - } - - // register the custom errors - iris.OnError(iris.StatusNotFound, func(ctx *iris.Context) { - ctx.Writef("%s", notFoundMessage) - }) - - iris.OnError(iris.StatusInternalServerError, func(ctx *iris.Context) { - ctx.Writef("%s", internalServerMessage) - }) - - // create httpexpect instance that will call fasthtpp.RequestHandler directly - e := httptest.New(iris.Default, t) - - // run the tests - for _, r := range testRoutesCustomErrors { - e.Request(r.Method, r.RequestPath). - Expect(). - Status(r.Status).Body().Equal(r.Body) - } -} - -type testUserAPI struct { - *iris.Context -} - -// GET /users -func (u testUserAPI) Get() { - u.WriteString("Get Users\n") -} - -// GET /users/:param1 which its value passed to the id argument -func (u testUserAPI) GetBy(id string) { // id equals to u.Param("param1") - u.Writef("Get By %s\n", id) -} - -// PUT /users -func (u testUserAPI) Put() { - u.Writef("Put, name: %s\n", u.FormValue("name")) -} - -// POST /users/:param1 -func (u testUserAPI) PostBy(id string) { - u.Writef("Post By %s, name: %s\n", id, u.FormValue("name")) -} - -// DELETE /users/:param1 -func (u testUserAPI) DeleteBy(id string) { - u.Writef("Delete By %s\n", id) -} - -func TestMuxAPI(t *testing.T) { - iris.ResetDefault() - - middlewareResponseText := "I assume that you are authenticated\n" - h := []iris.HandlerFunc{func(ctx *iris.Context) { // optional middleware for .API - // do your work here, or render a login window if not logged in, get the user and send it to the next middleware, or do all here - ctx.Set("user", "username") - ctx.Next() - }, func(ctx *iris.Context) { - if ctx.Get("user") == "username" { - ctx.WriteString(middlewareResponseText) - ctx.Next() - } else { - ctx.SetStatusCode(iris.StatusUnauthorized) - } - }} - - iris.API("/users", testUserAPI{}, h...) - // test a simple .Party with combination of .API - iris.Party("sites/:site").API("/users", testUserAPI{}, h...) - - e := httptest.New(iris.Default, t) - - siteID := "1" - apiPath := "/sites/" + siteID + "/users" - userID := "4077" - formname := "kataras" - - // .API - e.GET("/users").Expect().Status(iris.StatusOK).Body().Equal(middlewareResponseText + "Get Users\n") - e.GET("/users/" + userID).Expect().Status(iris.StatusOK).Body().Equal(middlewareResponseText + "Get By " + userID + "\n") - e.PUT("/users").WithFormField("name", formname).Expect().Status(iris.StatusOK).Body().Equal(middlewareResponseText + "Put, name: " + formname + "\n") - e.POST("/users/"+userID).WithFormField("name", formname).Expect().Status(iris.StatusOK).Body().Equal(middlewareResponseText + "Post By " + userID + ", name: " + formname + "\n") - e.DELETE("/users/" + userID).Expect().Status(iris.StatusOK).Body().Equal(middlewareResponseText + "Delete By " + userID + "\n") - - // .Party - e.GET(apiPath).Expect().Status(iris.StatusOK).Body().Equal(middlewareResponseText + "Get Users\n") - e.GET(apiPath + "/" + userID).Expect().Status(iris.StatusOK).Body().Equal(middlewareResponseText + "Get By " + userID + "\n") - e.PUT(apiPath).WithFormField("name", formname).Expect().Status(iris.StatusOK).Body().Equal(middlewareResponseText + "Put, name: " + formname + "\n") - e.POST(apiPath+"/"+userID).WithFormField("name", formname).Expect().Status(iris.StatusOK).Body().Equal(middlewareResponseText + "Post By " + userID + ", name: " + formname + "\n") - e.DELETE(apiPath + "/" + userID).Expect().Status(iris.StatusOK).Body().Equal(middlewareResponseText + "Delete By " + userID + "\n") - -} - -type myTestHandlerData struct { - Sysname string // this will be the same for all requests - Version int // this will be the same for all requests - DynamicPathParameter string // this will be different for each request -} - -type myTestCustomHandler struct { - data myTestHandlerData -} - -func (m *myTestCustomHandler) Serve(ctx *iris.Context) { - data := &m.data - data.DynamicPathParameter = ctx.Param("myparam") - ctx.JSON(iris.StatusOK, data) -} - -func TestMuxCustomHandler(t *testing.T) { - iris.ResetDefault() - myData := myTestHandlerData{ - Sysname: "Redhat", - Version: 1, - } - iris.Handle("GET", "/custom_handler_1/:myparam", &myTestCustomHandler{myData}) - iris.Handle("GET", "/custom_handler_2/:myparam", &myTestCustomHandler{myData}) - - e := httptest.New(iris.Default, t) - // two times per testRoute - param1 := "thisimyparam1" - expectedData1 := myData - expectedData1.DynamicPathParameter = param1 - e.GET("/custom_handler_1/" + param1).Expect().Status(iris.StatusOK).JSON().Equal(expectedData1) - - param2 := "thisimyparam2" - expectedData2 := myData - expectedData2.DynamicPathParameter = param2 - e.GET("/custom_handler_1/" + param2).Expect().Status(iris.StatusOK).JSON().Equal(expectedData2) - - param3 := "thisimyparam3" - expectedData3 := myData - expectedData3.DynamicPathParameter = param3 - e.GET("/custom_handler_2/" + param3).Expect().Status(iris.StatusOK).JSON().Equal(expectedData3) - - param4 := "thisimyparam4" - expectedData4 := myData - expectedData4.DynamicPathParameter = param4 - e.GET("/custom_handler_2/" + param4).Expect().Status(iris.StatusOK).JSON().Equal(expectedData4) -} - -func TestMuxFireMethodNotAllowed(t *testing.T) { - iris.ResetDefault() - iris.Default.Config.FireMethodNotAllowed = true - h := func(ctx *iris.Context) { - ctx.WriteString(ctx.Method()) - } - - iris.Default.OnError(iris.StatusMethodNotAllowed, func(ctx *iris.Context) { - ctx.WriteString("Hello from my custom 405 page") - }) - - iris.Get("/mypath", h) - iris.Put("/mypath", h) - - e := httptest.New(iris.Default, t) - - e.GET("/mypath").Expect().Status(iris.StatusOK).Body().Equal("GET") - e.PUT("/mypath").Expect().Status(iris.StatusOK).Body().Equal("PUT") - // this should fail with 405 and catch by the custom http error - - e.POST("/mypath").Expect().Status(iris.StatusMethodNotAllowed).Body().Equal("Hello from my custom 405 page") - iris.Close() -} - -func TestRedirectHTTPS(t *testing.T) { - - api := iris.New(iris.OptionIsDevelopment(true)) - - host := "localhost:" + strconv.Itoa(getRandomNumber(1717, 9281)) - - expectedBody := "Redirected to /redirected" - - api.Get("/redirect", func(ctx *iris.Context) { ctx.Redirect("/redirected") }) - api.Get("/redirected", func(ctx *iris.Context) { ctx.Text(iris.StatusOK, "Redirected to "+ctx.Path()) }) - defer listenTLS(api, host)() - - e := httptest.New(api, t) - e.GET("/redirect").Expect().Status(iris.StatusOK).Body().Equal(expectedBody) -} - -func TestRouteStateSimple(t *testing.T) { - iris.ResetDefault() - offlineRoutePath := "/api/user/:userid" - offlineRouteRequestedTestPath := "/api/user/42" - offlineBody := "user with id: 42" - - offlineRoute := iris.None(offlineRoutePath, func(ctx *iris.Context) { - userid := ctx.Param("userid") - if userid != "42" { - // we are expecting userid 42 always in this test so - t.Fatalf("what happened? expected userid to be 42 but got %s", userid) - } - ctx.Writef(offlineBody) - })("api.users") // or an empty (), required, in order to get the Route instance. - - // change the "user.api" state from offline to online and online to offline - iris.Get("/change", func(ctx *iris.Context) { - // here - if offlineRoute.IsOnline() { - // set to offline - iris.SetRouteOffline(offlineRoute) - } else { - // set to online if it was not online(so it was offline) - iris.SetRouteOnline(offlineRoute, iris.MethodGet) - } - }) - - iris.Get("/execute", func(ctx *iris.Context) { - // here - ctx.ExecRouteAgainst(offlineRoute, "/api/user/42") - }) - - hello := "Hello from index" - iris.Get("/", func(ctx *iris.Context) { - ctx.Writef(hello) - }) - - e := httptest.New(iris.Default, t) - - e.GET("/").Expect().Status(iris.StatusOK).Body().Equal(hello) - // here - // the status should be not found, the route is invisible from outside world - e.GET(offlineRouteRequestedTestPath).Expect().Status(iris.StatusNotFound) - - // set the route online with the /change - e.GET("/change").Expect().Status(iris.StatusOK) - // try again, it should be online now - e.GET(offlineRouteRequestedTestPath).Expect().Status(iris.StatusOK).Body().Equal(offlineBody) - // change to offline again - e.GET("/change").Expect().Status(iris.StatusOK) - // and test again, it should be offline now - e.GET(offlineRouteRequestedTestPath).Expect().Status(iris.StatusNotFound) - - // finally test the execute on the offline route - // it should be remains offline but execute the route like it is from client request. - e.GET("/execute").Expect().Status(iris.StatusOK).Body().Equal(offlineBody) - e.GET(offlineRouteRequestedTestPath).Expect().Status(iris.StatusNotFound) -} diff --git a/httptest/httptest.go b/httptest/httptest.go index c272bd8a..f20fbf27 100644 --- a/httptest/httptest.go +++ b/httptest/httptest.go @@ -5,8 +5,8 @@ import ( "net/http" "testing" - "github.com/gavv/httpexpect" - "github.com/kataras/iris" + "github.com/iris-contrib/httpexpect" + "gopkg.in/kataras/iris.v6" ) type ( @@ -66,7 +66,7 @@ func DefaultConfiguration() *Configuration { // New Prepares and returns a new test framework based on the api // is useful when you need to have more than one test framework for the same iris instance // usage: -// iris.Get("/mypath", func(ctx *iris.Context){ctx.Write("my body")}) +// iris.Default.Get("/mypath", func(ctx *iris.Context){ctx.Write("my body")}) // ... // e := httptest.New(iris.Default, t) // e.GET("/mypath").Expect().Status(iris.StatusOK).Body().Equal("my body") @@ -79,11 +79,10 @@ func New(api *iris.Framework, t *testing.T, setters ...OptionSetter) *httpexpect } api.Set(iris.OptionDisableBanner(true)) - + api.Adapt(iris.DevLogger()) baseURL := "" - if !api.Plugins.PreBuildFired() { - api.Build() - } + api.Boot() + if !conf.ExplicitURL { baseURL = api.Config.VScheme + api.Config.VHost // if it's still empty then set it to the default server addr @@ -96,7 +95,7 @@ func New(api *iris.Framework, t *testing.T, setters ...OptionSetter) *httpexpect testConfiguration := httpexpect.Config{ BaseURL: baseURL, Client: &http.Client{ - Transport: httpexpect.NewBinder(api.Router), + Transport: httpexpect.NewBinder(api), Jar: httpexpect.NewJar(), }, Reporter: httpexpect.NewAssertReporter(t), diff --git a/iris.go b/iris.go index fa5b6e6d..cabd08a1 100644 --- a/iris.go +++ b/iris.go @@ -1,87 +1,37 @@ -/* -Package iris the fastest go web framework in (this) Earth. - -Basic usage ----------------------------------------------------------------------- - -package main - -import "github.com/kataras/iris" - -func main() { - iris.Get("/hi_json", func(ctx *iris.Context) { - ctx.JSON(iris.StatusOK, iris.Map{ - "Name": "Iris", - "Released": "13 March 2016", - "Stars": "5883", - }) - }) - iris.ListenLETSENCRYPT("mydomain.com") -} - ----------------------------------------------------------------------- - -package main - -import "github.com/kataras/iris" - -func main() { - s1 := iris.New() - s1.Get("/hi_json", func(ctx *iris.Context) { - ctx.JSON(iris.StatusOK, iris.Map{ - "Name": "Iris", - "Released": "13 March 2016", - "Stars": "5883", - }) - }) - - s2 := iris.New() - s2.Get("/hi_raw_html", func(ctx *iris.Context) { - ctx.HTML(iris.StatusOK, " Iris welcomes

you!

") - }) - - go s1.Listen(":8080") - s2.Listen(":1993") -} - ----------------------------------------------------------------------- - -For middleware, template engines, response engines, sessions, websockets, mails, subdomains, -dynamic subdomains, routes, party of subdomains & routes and more - -visit https://docs.iris-go.com -*/ -package iris // import "github.com/kataras/iris" +// Package iris provides efficient and well-designed toolbox with robust set of features to +// create your own perfect high performance web application +// with unlimited portability using the Go Programming Language. +// +// For middleware, template engines, response engines, sessions, websockets, mails, subdomains, +// dynamic subdomains, routes, party of subdomains & routes and more +// +// visit https://docs.iris-go.com +package iris import ( "fmt" + "io" "log" "net" "net/http" "net/url" "os" "os/signal" - "path" - "reflect" "strconv" "strings" "sync" - "sync/atomic" "time" + "github.com/geekypanda/httpcache" "github.com/kataras/go-errors" "github.com/kataras/go-fs" "github.com/kataras/go-serializer" "github.com/kataras/go-sessions" - "github.com/kataras/go-template" - "github.com/kataras/go-template/html" ) const ( - // IsLongTermSupport flag is true when the below version number is a long-term-support version - IsLongTermSupport = false // Version is the current version number of the Iris web framework - Version = "6.1.4" + Version = "6.2.0" banner = ` _____ _ |_ _| (_) @@ -91,302 +41,115 @@ const ( |_____|_| |_||___/ ` + Version + ` ` ) -// Default iris instance entry and its public fields, use it with iris.$anyPublicFuncOrField +// Default is the field which keeps an empty `Framework` +// instance with its default configuration (config can change at runtime). +// +// Use that as `iris.Default.Handle(...)` +// or create a new, ex: `app := iris.New(); app.Handle(...)` var ( Default *Framework - Config *Configuration - Logger *log.Logger // if you want colors in your console then you should use this https://github.com/iris-contrib/logger instead. - Plugins PluginContainer - // Router field holds the main http.Handler which can be changed. - // if you want to get benefit with iris' context make use of: - // ctx:= iris.AcquireCtx(http.ResponseWriter, *http.Request) to get the context at the beginning of your handler - // iris.ReleaseCtx(ctx) to release/put the context to the pool, at the very end of your custom handler. + // ResetDefault resets the `.Default` + // to an empty *Framework with the default configuration. // - // Want to change the default Router's behavior to something else like Gorilla's Mux? - // See more: https://github.com/iris-contrib/plugin/tree/master/gorillamux - Router http.Handler - Websocket *WebsocketServer - // Available is a channel type of bool, fired to true when the server is opened and all plugins ran - // never fires false, if the .Close called then the channel is re-allocating. - // the channel remains open until you close it. - // - // look at the http_test.go file for a usage example - Available chan bool + // Note: ResetDefault field itself can be setted + // to custom function too. + ResetDefault = func() { Default = New() } ) -// ResetDefault resets the iris.Default which is the instance which is used on the default iris station for -// iris.Get(all api functions) -// iris.Config -// iris.Logger -// iris.Plugins -// iris.Router -// iris.Websocket -// iris.Available channel -// useful mostly when you are not using the form of app := iris.New() inside your tests, to make sure that you're using a new iris instance -func ResetDefault() { - Default = New() - Config = Default.Config - Logger = Default.Logger - Plugins = Default.Plugins - Router = Default.Router - Websocket = Default.Websocket - Available = Default.Available -} - func init() { ResetDefault() } -// ------------------------------------------------------------------------------------- -// ------------------------------------------------------------------------------------- -// --------------------------------Framework implementation----------------------------- -// ------------------------------------------------------------------------------------- -// ------------------------------------------------------------------------------------- - -type ( - // FrameworkAPI contains the main Iris Public API - FrameworkAPI interface { - MuxAPI - Set(options ...OptionSetter) - Must(err error) - - Build() - Serve(ln net.Listener) error - Listen(addr string) - ListenTLS(addr string, certFilePath string, keyFilePath string) - ListenLETSENCRYPT(addr string, cacheOptionalStoreFilePath ...string) - ListenUNIX(fileOrAddr string, fileMode os.FileMode) - Close() error - Reserve() error - - AcquireCtx(w http.ResponseWriter, r *http.Request) *Context - ReleaseCtx(ctx *Context) - - CheckForUpdates(check bool) - - UseSessionDB(sessDB sessions.Database) - DestroySessionByID(sid string) - DestroyAllSessions() - - UseSerializer(contentType string, serializerEngine serializer.Serializer) - UsePreRender(prerenderFunc PreRender) - UseTemplateFunc(functionName string, function interface{}) - UseTemplate(tmplEngine template.Engine) *template.Loader - - UseGlobal(middleware ...Handler) - UseGlobalFunc(middleware ...HandlerFunc) - - ChangeRouter(http.Handler) - Lookup(routeName string) Route - Lookups() []Route - SetRouteOnline(r Route, HTTPMethod string) bool - SetRouteOffline(r Route) bool - ChangeRouteState(r Route, HTTPMethod string) bool - - Path(routeName string, optionalPathParameters ...interface{}) (routePath string) - URL(routeName string, optionalPathParameters ...interface{}) (routeURL string) - - TemplateString(file string, binding interface{}, options ...map[string]interface{}) (parsedTemplate string) - TemplateSourceString(src string, binding interface{}) (parsedTemplate string) - SerializeToString(string, interface{}, ...map[string]interface{}) (serializedContent string) - - Cache(handlerToCache HandlerFunc, expiration time.Duration) (cachedHandler HandlerFunc) - } - - // MuxAPI the visible api for the serveMux - MuxAPI interface { - Party(reqRelativeRootPath string, middleware ...HandlerFunc) MuxAPI - // middleware serial, appending - Use(middleware ...Handler) MuxAPI - UseFunc(middleware ...HandlerFunc) MuxAPI - Done(middleware ...Handler) MuxAPI - DoneFunc(middleware ...HandlerFunc) MuxAPI - - // main handlers - Handle(method string, reqPath string, middleware ...Handler) RouteNameFunc - HandleFunc(method string, reqPath string, middleware ...HandlerFunc) RouteNameFunc - API(reqRelativeRootPath string, api HandlerAPI, middleware ...HandlerFunc) - - // virtual method for "offline" routes new feature - None(reqRelativePath string, Middleware ...HandlerFunc) RouteNameFunc - // http methods - Get(reqRelativePath string, middleware ...HandlerFunc) RouteNameFunc - Post(reqRelativePath string, middleware ...HandlerFunc) RouteNameFunc - Put(reqRelativePath string, middleware ...HandlerFunc) RouteNameFunc - Delete(reqRelativePath string, middleware ...HandlerFunc) RouteNameFunc - Connect(reqRelativePath string, middleware ...HandlerFunc) RouteNameFunc - Head(reqRelativePath string, middleware ...HandlerFunc) RouteNameFunc - Options(reqRelativePath string, middleware ...HandlerFunc) RouteNameFunc - Patch(reqRelativePath string, middleware ...HandlerFunc) RouteNameFunc - Trace(reqRelativePath string, middleware ...HandlerFunc) RouteNameFunc - Any(reqRelativePath string, middleware ...HandlerFunc) - - // static content - StaticServe(systemFilePath string, optionalReqRelativePath ...string) RouteNameFunc - StaticContent(reqRelativePath string, contentType string, contents []byte) RouteNameFunc - StaticEmbedded(reqRelativePath string, contentType string, assets func(string) ([]byte, error), assetsNames func() []string) RouteNameFunc - Favicon(systemFilePath string, optionalReqRelativePath ...string) RouteNameFunc - // static file system - StaticHandler(reqRelativePath string, systemPath string, showList bool, enableGzip bool, exceptRoutes ...Route) HandlerFunc - StaticWeb(reqRelativePath string, systemPath string, exceptRoutes ...Route) RouteNameFunc - - // party layout for template engines - Layout(layoutTemplateFileName string) MuxAPI - - // errors - OnError(statusCode int, handler HandlerFunc) - EmitError(statusCode int, ctx *Context) - } - - // RouteNameFunc the func returns from the MuxAPi's methods, optionally sets the name of the Route (*route) - // - // You can find the Route by iris.Lookup("theRouteName") - // you can set a route name as: myRoute := iris.Get("/mypath", handler)("theRouteName") - // that will set a name to the route and returns its iris.Route instance for further usage. - // - RouteNameFunc func(customRouteName string) Route -) - -// Framework is our God |\| Google.Search('Greek mythology Iris') -// -// Implements the FrameworkAPI +// Framework is our God |\| Google.Search('Greek mythology Iris'). type Framework struct { - *muxAPI + *Router + policies Policies + // HTTP Server runtime fields is the iris' defined main server, developer can use unlimited number of servers // note: they're available after .Build, and .Serve/Listen/ListenTLS/ListenLETSENCRYPT/ListenUNIX - ln net.Listener - srv *http.Server - Available chan bool - // - // Router field holds the main http.Handler which can be changed. - // if you want to get benefit with iris' context make use of: - // ctx:= iris.AcquireCtx(http.ResponseWriter, *http.Request) to get the context at the beginning of your handler - // iris.ReleaseCtx(ctx) to release/put the context to the pool, at the very end of your custom handler. - // - // Want to change the default Router's behavior to something else like Gorilla's Mux? - // See more: https://github.com/iris-contrib/plugin/tree/master/gorillamux - Router http.Handler + ln net.Listener + closedManually bool - contextPool sync.Pool - once sync.Once - Config *Configuration - sessions sessions.Sessions - serializers serializer.Serializers - templates *templateEngines - Logger *log.Logger - Plugins PluginContainer - Websocket *WebsocketServer + once sync.Once + Config *Configuration + sessions sessions.Sessions + Websocket *WebsocketServer } -var _ FrameworkAPI = &Framework{} +var defaultGlobalLoggerOuput = log.New(os.Stdout, "[iris] ", log.LstdFlags) -// New creates and returns a new Iris instance. +// DevLogger returns a new Logger which prints both ProdMode and DevMode messages +// to the default global logger printer. // -// Receives (optional) multi options, use iris.Option and your editor should show you the available options to set -// all options are inside ./configuration.go -// example 1: iris.New(iris.OptionIsDevelopment(true), iris.OptionCharset("UTF-8"), irisOptionSessionsCookie("mycookieid"),iris.OptionWebsocketEndpoint("my_endpoint")) -// example 2: iris.New(iris.Configuration{IsDevelopment:true, Charset: "UTF-8", Sessions: iris.SessionsConfiguration{Cookie:"mycookieid"}, Websocket: iris.WebsocketConfiguration{Endpoint:"/my_endpoint"}}) -// both ways are totally valid and equal -func New(setters ...OptionSetter) *Framework { +// Usage: app := iris.New() +// app.Adapt(iris.DevLogger()) +// +// Users can always ignore that and adapt a custom LoggerPolicy, +// which will use your custom printer instead. +func DevLogger() LoggerPolicy { + return func(mode LogMode, logMessage string) { + defaultGlobalLoggerOuput.Println(logMessage) + } +} +// New creates and returns a fresh Iris *Framework instance +// with the default configuration if no 'setters' parameters passed. +func New(setters ...OptionSetter) *Framework { s := &Framework{} + + // +------------------------------------------------------------+ + // | Set the config passed from setters | + // | or use the default one | + // +------------------------------------------------------------+ s.Set(setters...) - // logger & plugins { - // set the Logger, which it's configuration should be declared before .Listen because the servemux and plugins needs that - s.Logger = log.New(s.Config.LoggerOut, s.Config.LoggerPreffix, log.LstdFlags) - s.Plugins = newPluginContainer(s.Logger) + // +------------------------------------------------------------+ + // | Module Name: Logger | + // | On Init: If user didn't adapt a custom loggger then attach | + // | a new logger using log.Logger as printer with | + // | some default options | + // +------------------------------------------------------------+ + + // The logger policy is never nil and it doesn't defaults to an empty func, + // instead it defaults to a logger with os.Stdout as the print target which prints + // ONLY prodction level messages. + // While in ProdMode Iris logs only panics and fatal errors. + // You can override the default log policy with app.Adapt(iris.DevLogger()) + // or app.Adapt(iris.LoggerPolicy(customLogger)) + // to log both ProdMode and DevMode messages. + // + // Note: + // The decision to not log everything and use middleware for http requests instead of built'n + // is because I'm using Iris on production so I don't want many logs to my screens + // while server is running. + s.Adapt(LoggerPolicy(func(mode LogMode, logMessage string) { + if mode == ProdMode { + defaultGlobalLoggerOuput.Println(logMessage) + } + })) + } - // rendering - { - s.serializers = serializer.Serializers{} - // set the templates - s.templates = newTemplateEngines(map[string]interface{}{ - "url": s.URL, - "urlpath": s.Path, - }) - } - - // websocket & sessions - { - // in order to be able to call $instance.Websocket.OnConnection - // the whole ws configuration and websocket server is really initialized only on first OnConnection - s.Websocket = NewWebsocketServer(s) - - // set the sessions in order to UseSessionDB to work - // see Build state - s.sessions = sessions.New() - } - - // routing - { - // set the servemux, which will provide us the public API also, with its context pool - mux := newServeMux(s.Logger) - mux.setCorrectPath(!s.Config.DisablePathCorrection) // correctPath is re-setted on .Set and after build* - - mux.onLookup = s.Plugins.DoPreLookup - s.contextPool.New = func() interface{} { - return &Context{framework: s} - } - // set the public router API (and party) - s.muxAPI = &muxAPI{mux: mux, relativePath: "/"} - s.Available = make(chan bool) - } - - return s -} - -// Set sets an option aka configuration field to the default iris instance -func Set(setters ...OptionSetter) { - Default.Set(setters...) -} - -// Set sets an option aka configuration field to this iris instance -func (s *Framework) Set(setters ...OptionSetter) { - if s.Config == nil { - defaultConfiguration := DefaultConfiguration() - s.Config = &defaultConfiguration - } - - for _, setter := range setters { - setter.Set(s.Config) - } - - if s.muxAPI != nil && s.mux != nil { // if called after .New, which it does, correctPath is the only field we need to be updated before .Listen, so: - s.mux.setCorrectPath(!s.Config.DisablePathCorrection) - } -} - -// Must panics on error, it panics on registered iris' logger -func Must(err error) { - Default.Must(err) -} - -// Must panics on error, it panics on registered iris' logger -func (s *Framework) Must(err error) { - if err != nil { - // s.Logger.Panicf("%s. Trace:\n%s", err, debug.Stack()) - s.Logger.Panic(err) - } -} - -// Build builds the whole framework's parts together -// DO NOT CALL IT MANUALLY IF YOU ARE NOT: -// SERVE IRIS BEHIND AN EXTERNAL CUSTOM net/http.Server, CAN BE CALLED ONCE PER IRIS INSTANCE FOR YOUR SAFETY -func Build() { - Default.Build() -} - -// Build builds the whole framework's parts together -// DO NOT CALL IT MANUALLY IF YOU ARE NOT: -// SERVE IRIS BEHIND AN EXTERNAL CUSTOM nethttp.Server, CAN BE CALLED ONCE PER IRIS INSTANCE FOR YOUR SAFETY -func (s *Framework) Build() { - s.once.Do(func() { - // .Build, normally*, auto-called after station's listener setted but before the real Serve, so here set the host, scheme - // and the mux hostname(*this is here because user may not call .Serve/.Listen functions if listen by a custom server) + // +------------------------------------------------------------+ + // | | + // | Please take a look at the policy.go file first. | + // | The EventPolicy contains all the necessary information | + // | user should know about the framework's flow. | + // | | + // +------------------------------------------------------------+ + // +------------------------------------------------------------+ + // | On Boot: Set the VHost and VScheme config fields | + // | based on the net.Listener which (or not) | + // | setted on Serve and Listen funcs. | + // | | + // | It's the only pre-defined Boot event because of | + // | any user's custom 'Build' events should know | + // | the Host of the server. | + // +------------------------------------------------------------+ + s.Adapt(EventPolicy{Boot: func(s *Framework) { + // set the host and scheme if s.Config.VHost == "" { // if not setted by Listen functions if s.ln != nil { // but user called .Serve // then take the listener's addr @@ -401,146 +164,291 @@ func (s *Framework) Build() { if s.Config.VScheme == "" { s.Config.VScheme = ParseScheme(s.Config.VHost) } + }}) - s.Plugins.DoPreBuild(s) // once after configuration has been setted. *nothing stops you to change the VHost and VScheme at this point* - // re-nwe logger's attrs - s.Logger.SetPrefix(s.Config.LoggerPreffix) - s.Logger.SetOutput(s.Config.LoggerOut) + { + // +------------------------------------------------------------+ + // | Module Name: Renderer | + // | On Init: set templates and serializers | + // | and adapt the RenderPolicy for both | + // | templates and content-type specific renderer (serializer) | + // | On Build: build the serializers and templates | + // | based on the user's calls | + // +------------------------------------------------------------+ - // prepare the serializers, if not any other serializers setted for the default serializer types(json,jsonp,xml,markdown,text,data) then the defaults are setted: - serializer.RegisterDefaults(s.serializers) + { + // +------------------------------------------------------------+ + // | Module Name: Rich Content-Type Renderer | + // | On Init: Attach a new empty content-type serializers | + // | On Build: register the default serializers + the user's | + // +------------------------------------------------------------+ - // prepare the templates if enabled - if !s.Config.DisableTemplateEngines { + // prepare the serializers, + // serializer content-types(json,jsonp,xml,markdown) the defaults are setted: + serializers := serializer.Serializers{} + serializer.RegisterDefaults(serializers) - s.templates.Reload = s.Config.IsDevelopment - // check and prepare the templates - if len(s.templates.Entries) == 0 { // no template engines were registered, let's use the default - s.UseTemplate(html.New()) - } + // + // notes for me: Why not at the build state? in order to be overridable and not only them, + // these are easy to be overriden by external adaptors too, no matter the order, + // this is why the RenderPolicy last registration executing first and the first last. + // + + // Adapt the RenderPolicy on the Build in order to be the last + // render policy, so the users can adapt their own before the default(= to override json,xml,jsonp renderer). + // + // Notes: the Renderer of the view system is managed by the + // adaptors because they are optional. + // If templates are binded to the RenderPolicy then + // If a key contains a dot('.') then is a template file + // otherwise try to find a serializer, if contains error then we return false and the error + // in order the renderer to continue to search for any other custom registerer RenderPolicy + // if no error then check if it has written anything, if yes write the content + // to the writer(which is the context.ResponseWriter or the gzip version of it) + // if no error but nothing written then we return false and the error + s.Adapt(RenderPolicy(func(out io.Writer, name string, bind interface{}, options ...map[string]interface{}) (error, bool) { + b, err := serializers.Serialize(name, bind, options...) + if err != nil { + return err, false // errors should be wrapped + } + if len(b) > 0 { + _, err = out.Write(b) + return err, true + } + // continue to the next if any or notice there is no available renderer for that name + return nil, false + })) + } + { + // +------------------------------------------------------------+ + // | Module Name: Template engine's funcs | + // | On Init: Use the template mux builder to | + // | adapt the reverse routing tmpl funcs | + // | for any template engine that will be registered | + // +------------------------------------------------------------+ + s.Adapt(TemplateFuncsPolicy{ + "url": s.URL, + "urlpath": s.policies.RouterReversionPolicy.URLPath, + }) + + // the entire template registration logic lives inside the ./adaptors/template now. - if err := s.templates.Load(); err != nil { - s.Logger.Panic(err) // panic on templates loading before listening if we have an error. - } } - // set the user's configuration (may changed after .New()) - s.sessions.Set(s.Config.Sessions) + } - // prepare the mux runtime fields again, for any case - s.mux.setCorrectPath(!s.Config.DisablePathCorrection) - s.mux.setFireMethodNotAllowed(s.Config.FireMethodNotAllowed) + { + // +------------------------------------------------------------+ + // | Module Name: Sessions | + // | On Init: Attach a session manager with empty config | + // | On Build: Set the configuration if allowed | + // +------------------------------------------------------------+ - // prepare the server's handler, we do that check because iris supports - // custom routers (you can take the routes registered by iris using iris.Lookups function) - if s.Router == nil { - // build and get the default mux' handler(*Context) - serve := s.mux.BuildHandler() - // build the net/http.Handler to bind it to the servers - defaultHandler := ToNativeHandler(s, serve) + // set the sessions in order to UseSessionDB to work + s.sessions = sessions.New() + // On Build: + s.Adapt(EventPolicy{Build: func(*Framework) { + // re-set the configuration field to update users configuration + s.sessions.Set(s.Config.Sessions) + }}) + } - s.Router = defaultHandler + { + // +------------------------------------------------------------+ + // | Module Name: Websocket | + // | On Init: Attach a new websocket server. | + // | It starts on first callback registration | + // +------------------------------------------------------------+ + + // in order to be able to call $instance.Websocket.OnConnection. + // The whole server's configuration will be + // initialized on the first OnConnection registration (no runtime) + s.Websocket = NewWebsocketServer(s) + } + + { + // +------------------------------------------------------------+ + // | Module Name: Router | + // | On Init: Attach a new router, pass a new repository, | + // | an empty error handlers list, the context pool binded | + // | to the Framework and the root path "/" | + // | On Build: Use the policies to build the router's handler | + // | based on its route repository | + // +------------------------------------------------------------+ + + s.Router = &Router{ + repository: new(routeRepository), + Errors: &ErrorHandlers{ + handlers: make(map[int]Handler, 0), + }, + Context: &contextPool{ + sync.Pool{New: func() interface{} { return &Context{framework: s} }}, + }, + relativePath: "/", } - // set the mux' hostname (for multi subdomain routing) - s.mux.hostname = ParseHostname(s.Config.VHost) - if s.ln != nil { // user called Listen functions or Serve, - // create the main server - s.srv = &http.Server{ - ReadTimeout: s.Config.ReadTimeout, - WriteTimeout: s.Config.WriteTimeout, - MaxHeaderBytes: s.Config.MaxHeaderBytes, - TLSNextProto: s.Config.TLSNextProto, - ConnState: s.Config.ConnState, - Handler: s.Router, - Addr: s.Config.VHost, - ErrorLog: s.Logger, - } - if s.Config.TLSNextProto != nil { - s.srv.TLSNextProto = s.Config.TLSNextProto - } - if s.Config.ConnState != nil { - s.srv.ConnState = s.Config.ConnState - } - } + s.Adapt(EventPolicy{Build: func(*Framework) { + // first check if it's not setted already by any Boot event. + if s.Router.handler == nil { + // and most importantly, check if the user has provided a router + // adaptor, if not then it should panic here, iris can't run without a router attached to it + // and default router not any more, user should select one from ./adaptors or + // any other third-party adaptor may done by community. + // I was coding the new iris version for more than 20 days(~200+ hours of code) + // and I hope that once per application the addition of +1 line users have to put, + // is not a big deal. + if s.policies.RouterBuilderPolicy == nil { + // this is important panic and app can't continue as we said. + s.handlePanic(errRouterIsMissing.Format(s.Config.VHost)) + // don't trace anything else, + // the detailed errRouterIsMissing message will tell the user what to do to fix that. + os.Exit(0) - // updates, to cover the default station's irs.Config.checkForUpdates - // note: we could use the IsDevelopment configuration field to do that BUT - // the developer may want to check for updates without, for example, re-build template files (comes from IsDevelopment) on each request - if s.Config.CheckForUpdatesSync { - s.CheckForUpdates(false) - } else if s.Config.CheckForUpdates { - go s.CheckForUpdates(false) - } - }) + } + // buid the router using user's selection build policy + s.Router.build(s.policies.RouterBuilderPolicy) + } + }}) + + } + + { + // +------------------------------------------------------------+ + // | Module Name: System | + // | On Build: Check for updates on Build | + // +------------------------------------------------------------+ + + // On Build: local repository updates + s.Adapt(EventPolicy{Build: func(*Framework) { + if s.Config.CheckForUpdatesSync { + s.CheckForUpdates(false) + } else if s.Config.CheckForUpdates { + go s.CheckForUpdates(false) + } + }}) + } + + return s } -var ( - errServerAlreadyStarted = errors.New("Server is already started and listening") -) +// Set sets an option, configuration field to its Config +func (s *Framework) Set(setters ...OptionSetter) { + if s.Config == nil { + defaultConfiguration := DefaultConfiguration() + s.Config = &defaultConfiguration + } -// Serve serves incoming connections from the given listener. + for _, setter := range setters { + setter.Set(s.Config) + } +} + +// Log logs to the defined logger policy. // -// Serve blocks until the given listener returns permanent error. -func Serve(ln net.Listener) error { - return Default.Serve(ln) +// The default outputs to the os.Stdout when EnvMode is 'ProductionEnv' +func (s *Framework) Log(mode LogMode, log string) { + s.policies.LoggerPolicy(mode, log) +} + +// Must checks if the error is not nil, if it isn't +// panics on registered iris' logger or +// to a recovery event handler, otherwise does nothing. +func (s *Framework) Must(err error) { + if err != nil { + s.handlePanic(err) + } +} + +func (s *Framework) handlePanic(err error) { + if recoveryHandler := s.policies.EventPolicy.Recover; recoveryHandler != nil { + recoveryHandler(s, err) + return + } + // if not a registered recovery event handler found + // then call the logger's Panic. + s.Log(ProdMode, err.Error()) +} + +// Boot runs only once, automatically +// when 'Serve/Listen/ListenTLS/ListenUNIX/ListenLETSENCRYPT' called. +// It's exported because you may want to build the router +// and its components but not run the server. +// +// See ./httptest/httptest.go to understand its usage. +func (s *Framework) Boot() (firstTime bool) { + s.once.Do(func() { + // here execute the boot events, before build events, if exists, here is + // where the user can make an event module to adapt custom routers and other things + // fire the before build event + s.policies.EventPolicy.Fire(s.policies.EventPolicy.Boot, s) + + // here execute the build events if exists + // right before the Listen, all methods have been setted + // usually is used to adapt third-party servers or proxies or load balancer(s) + s.policies.EventPolicy.Fire(s.policies.EventPolicy.Build, s) + + firstTime = true + }) + return } // Serve serves incoming connections from the given listener. // // Serve blocks until the given listener returns permanent error. func (s *Framework) Serve(ln net.Listener) error { - if s.IsRunning() { - return errServerAlreadyStarted + if s.isRunning() { + return errors.New("Server is already started and listening") } // maybe a 'race' here but user should not call .Serve more than one time especially in more than one go routines... s.ln = ln - // build the handler and all other components - s.Build() - // fire all PreListen plugins - s.Plugins.DoPreListen(s) + s.Boot() - // catch any panics to the user defined logger. + // post any panics to the user defined logger. defer func() { - if err := recover(); err != nil { - s.Logger.Panic(err) + if rerr := recover(); rerr != nil { + if x, ok := rerr.(*net.OpError); ok && x.Op == "accept" && s.closedManually { + ///TODO: + // here we don't report it back because the user called .Close manually. + // NOTES: + // + // I know that the best option to actual Close a server is + // by using a custom net.Listener and do it via channels on its Accept. + // BUT I am not doing this right now because as I'm learning the new go v1.8 will have a shutdown + // options by-default and we will use that instead. + // println("iris.go:355:recover but closed manually so we don't run the handler") + return + } + if err, ok := rerr.(error); ok { + s.handlePanic(err) + } } }() - // prepare for 'after serve' actions - var stop uint32 - go func() { - // wait for the server's Serve func (309 mill is a lot or not, I found that this number is the perfect for most of the environments.) - time.Sleep(309 * time.Millisecond) - if atomic.LoadUint32(&stop) > 0 { - return - } - // print the banner - // fire the PostListen plugins - // wait for system channel interrupt - // fire the PostInterrupt plugins or close and exit. - s.postServe() - }() - - serverStartUpErr := s.srv.Serve(ln) - if serverStartUpErr != nil { - // if an error then it would be nice to stop the banner and all next plugin events. - atomic.AddUint32(&stop, 1) + srv := &http.Server{ + ReadTimeout: s.Config.ReadTimeout, + WriteTimeout: s.Config.WriteTimeout, + MaxHeaderBytes: s.Config.MaxHeaderBytes, + TLSNextProto: s.Config.TLSNextProto, + ConnState: s.Config.ConnState, + Addr: s.Config.VHost, + ErrorLog: s.policies.LoggerPolicy.ToLogger(log.LstdFlags), + Handler: s.Router, } + + if s.Config.TLSNextProto != nil { + srv.TLSNextProto = s.Config.TLSNextProto + } + if s.Config.ConnState != nil { + srv.ConnState = s.Config.ConnState + } + // print the banner and wait for system channel interrupt + go s.postServe() // finally return the error or block here, remember, - // you can always use the iris.Available to 'see' if and when the server is up (virtually), // until go1.8 these are our best options. - return serverStartUpErr + return srv.Serve(s.ln) } -// runs only when server starts without errors -// what it does? -// 0. print the banner -// 1. fire the PostListen plugins -// 2. wait for system channel interrupt -// 3. fire the PostInterrupt plugins or close and exit. func (s *Framework) postServe() { - if !s.Config.DisableBanner { bannerMessage := fmt.Sprintf("%s: Running at %s", time.Now().Format(s.Config.TimeFormat), s.Config.VHost) // we don't print it via Logger because: @@ -550,43 +458,23 @@ func (s *Framework) postServe() { fmt.Printf("%s\n\n%s\n", banner, bannerMessage) } - s.Plugins.DoPostListen(s) - - go func() { s.Available <- true }() - ch := make(chan os.Signal, 1) signal.Notify(ch, os.Interrupt) <-ch - // catch custom plugin event for interrupt - // Example: https://github.com/iris-contrib/examples/tree/master/os_interrupt - s.Plugins.DoPostInterrupt(s) - if !s.Plugins.PostInterruptFired() { - // if no PostInterrupt events fired, then I assume that the user doesn't cares - // so close the server automatically. - if err := s.Close(); err != nil { - if s.Config.IsDevelopment { - s.Logger.Printf("Error while closing the server: %s\n", err) - } - } - os.Exit(1) - } + // fire any custom interrupted events and at the end close and exit + // if the custom event blocks then it decides what to do next. + s.policies.Fire(s.policies.Interrupted, s) + // .Close doesn't really closes but it releases the ip:port, wait for go1.8 and see comments on IsRunning + s.Close() + os.Exit(1) } // Listen starts the standalone http server // which listens to the addr parameter which as the form of // host:port // -// It panics on error if you need a func to return an error, use the Serve -func Listen(addr string) { - Default.Listen(addr) -} - -// Listen starts the standalone http server -// which listens to the addr parameter which as the form of -// host:port -// -// It panics on error if you need a func to return an error, use the Serve +// If you need to manually monitor any error please use `.Serve` instead. func (s *Framework) Listen(addr string) { addr = ParseHost(addr) if s.Config.VHost == "" { @@ -604,7 +492,7 @@ func (s *Framework) Listen(addr string) { ln, err := TCPKeepAlive(addr) if err != nil { - s.Logger.Panic(err) + s.handlePanic(err) } s.Must(s.Serve(ln)) @@ -616,20 +504,8 @@ func (s *Framework) Listen(addr string) { // which listens to the addr parameter which as the form of // host:port // -// It panics on error if you need a func to return an error, use the Serve -// ex: iris.ListenTLS(":8080","yourfile.cert","yourfile.key") -func ListenTLS(addr string, certFile string, keyFile string) { - Default.ListenTLS(addr, certFile, keyFile) -} - -// ListenTLS Starts a https server with certificates, -// if you use this method the requests of the form of 'http://' will fail -// only https:// connections are allowed -// which listens to the addr parameter which as the form of -// host:port // -// It panics on error if you need a func to return an error, use the Serve -// ex: iris.ListenTLS(":8080","yourfile.cert","yourfile.key") +// If you need to manually monitor any error please use `.Serve` instead. func (s *Framework) ListenTLS(addr string, certFile, keyFile string) { addr = ParseHost(addr) if s.Config.VHost == "" { @@ -639,26 +515,11 @@ func (s *Framework) ListenTLS(addr string, certFile, keyFile string) { ln, err := TLS(addr, certFile, keyFile) if err != nil { - s.Logger.Panic(err) + s.handlePanic(err) } s.Must(s.Serve(ln)) } -// ListenLETSENCRYPT starts a server listening at the specific nat address -// using key & certification taken from the letsencrypt.org 's servers -// it's also starts a second 'http' server to redirect all 'http://$ADDR_HOSTNAME:80' to the' https://$ADDR' -// it creates a cache file to store the certifications, for performance reasons, this file by-default is "./letsencrypt.cache" -// if you skip the second parameter then the cache file is "./letsencrypt.cache" -// if you want to disable cache then simple pass as second argument an empty empty string "" -// -// example: https://github.com/iris-contrib/examples/blob/master/letsencrypt/main.go -// -// supports localhost domains for testing, -// NOTE: if you are ready for production then use `$app.Serve(iris.LETSENCRYPTPROD("mydomain.com"))` instead -func ListenLETSENCRYPT(addr string, cacheFileOptional ...string) { - Default.ListenLETSENCRYPT(addr, cacheFileOptional...) -} - // ListenLETSENCRYPT starts a server listening at the specific nat address // using key & certification taken from the letsencrypt.org 's servers // it's also starts a second 'http' server to redirect all 'http://$ADDR_HOSTNAME:80' to the' https://$ADDR' @@ -678,7 +539,7 @@ func (s *Framework) ListenLETSENCRYPT(addr string, cacheFileOptional ...string) } ln, err := LETSENCRYPT(addr, cacheFileOptional...) if err != nil { - s.Logger.Panic(err) + s.handlePanic(err) } // starts a second server which listening on HOST:80 to redirect all requests to the HTTPS://HOST:PORT @@ -688,16 +549,8 @@ func (s *Framework) ListenLETSENCRYPT(addr string, cacheFileOptional ...string) // ListenUNIX starts the process of listening to the new requests using a 'socket file', this works only on unix // -// It panics on error if you need a func to return an error, use the Serve -// ex: iris.ListenUNIX(":8080", Mode: os.FileMode) -func ListenUNIX(addr string, mode os.FileMode) { - Default.ListenUNIX(addr, mode) -} - -// ListenUNIX starts the process of listening to the new requests using a 'socket file', this works only on unix // -// It panics on error if you need a func to return an error, use the Serve -// ex: iris.ListenUNIX(":8080", Mode: os.FileMode) +// If you need to manually monitor any error please use `.Serve` instead. func (s *Framework) ListenUNIX(addr string, mode os.FileMode) { // *on unix listen we don't parse the host, because sometimes it causes problems to the user if s.Config.VHost == "" { @@ -706,87 +559,40 @@ func (s *Framework) ListenUNIX(addr string, mode os.FileMode) { } ln, err := UNIX(addr, mode) if err != nil { - s.Logger.Panic(err) + s.handlePanic(err) } s.Must(s.Serve(ln)) } // IsRunning returns true if server is running -func IsRunning() bool { - return Default.IsRunning() -} - -// IsRunning returns true if server is running -func (s *Framework) IsRunning() bool { +func (s *Framework) isRunning() bool { + ///TODO: this will change on gov1.8, + // Reseve or Restart and Close will be re-added again when 1.8 final release. return s != nil && s.ln != nil && s.ln.Addr() != nil && s.ln.Addr().String() != "" } -// Close terminates all the registered servers and returns an error if any -// if you want to panic on this error use the iris.Must(iris.Close()) -func Close() error { - return Default.Close() -} - -// Close terminates all the registered servers and returns an error if any -// if you want to panic on this error use the iris.Must(iris.Close()) +// Close is not working propetly but it releases the host:port. func (s *Framework) Close() error { - if s.IsRunning() { - s.Plugins.DoPreClose(s) - s.Available = make(chan bool) - + if s.isRunning() { + ///TODO: + // This code below doesn't works without custom net listener which will work in a stop channel which will cost us performance. + // This will work on go v1.8 BUT FOR NOW make unexported reserve/reboot/restart in order to be non confusual for the user. + // Close need to be exported because whitebox tests are using this method to release the port. return s.ln.Close() } return nil } -// Reserve re-starts the server using the last .Serve's listener -func Reserve() error { - return Default.Reserve() -} +// restart re-starts the server using the last .Serve's listener +// func (s *Framework) restart() error { +// ///TODO: See .close() notes +// return s.Serve(s.ln) +// } -// Reserve re-starts the server using the last .Serve's listener -func (s *Framework) Reserve() error { - return s.Serve(s.ln) -} - -// AcquireCtx gets an Iris' Context from pool -// see .ReleaseCtx & .Serve -func AcquireCtx(w http.ResponseWriter, r *http.Request) *Context { - return Default.AcquireCtx(w, r) -} - -// AcquireCtx gets an Iris' Context from pool -// see .ReleaseCtx & .Serve -func (s *Framework) AcquireCtx(w http.ResponseWriter, r *http.Request) *Context { - ctx := s.contextPool.Get().(*Context) // Changed to use the pool's New 09/07/2016, ~ -4k nanoseconds(9 bench tests) per requests (better performance) - ctx.ResponseWriter = acquireResponseWriter(w) - ctx.Request = r - return ctx -} - -// ReleaseCtx puts the Iris' Context back to the pool in order to be re-used -// see .AcquireCtx & .Serve -func ReleaseCtx(ctx *Context) { - Default.ReleaseCtx(ctx) -} - -// ReleaseCtx puts the Iris' Context back to the pool in order to be re-used -// see .AcquireCtx & .Serve -func (s *Framework) ReleaseCtx(ctx *Context) { - // flush the body (on recorder) or just the status code (on basic response writer) - // when all finished - ctx.ResponseWriter.flushResponse() - - ctx.Middleware = nil - ctx.session = nil - ctx.Request = nil - ///TODO: - ctx.ResponseWriter.releaseMe() - ctx.values.Reset() - - s.contextPool.Put(ctx) +func (s *Framework) ServeHTTP(w http.ResponseWriter, r *http.Request) { + s.Router.ServeHTTP(w, r) } // global once because is not necessary to check for updates on more than one iris station* @@ -797,14 +603,6 @@ const ( githubRepo = "iris" ) -// CheckForUpdates will try to search for newer version of Iris based on the https://github.com/kataras/iris/releases -// If a newer version found then the app will ask the he dev/user if want to update the 'x' version -// if 'y' is pressed then the updater will try to install the latest version -// the updater, will notify the dev/user that the update is finished and should restart the App manually. -func CheckForUpdates(force bool) { - Default.CheckForUpdates(force) -} - // CheckForUpdates will try to search for newer version of Iris based on the https://github.com/kataras/iris/releases // If a newer version found then the app will ask the he dev/user if want to update the 'x' version // if 'y' is pressed then the updater will try to install the latest version @@ -813,21 +611,18 @@ func CheckForUpdates(force bool) { func (s *Framework) CheckForUpdates(force bool) { updated := false checker := func() { - writer := s.Config.LoggerOut - if writer == nil { - writer = os.Stdout // we need a writer because the update process will not be silent. - } - - fs.DefaultUpdaterAlreadyInstalledMessage = "INFO: Running with the latest version(%s)\n" + fs.DefaultUpdaterAlreadyInstalledMessage = "Updater: Running with the latest version(%s)\n" updater, err := fs.GetUpdater(githubOwner, githubRepo, Version) if err != nil { - writer.Write([]byte("Update failed: " + err.Error())) + // ignore writer's error + s.Log(DevMode, "update failed: "+err.Error()) return } - updated = updater.Run(fs.Stdout(writer), fs.Stderr(writer), fs.Silent(false)) + updated = updater.Run(fs.Stdout(s.policies.LoggerPolicy), fs.Stderr(s.policies.LoggerPolicy), fs.Silent(false)) + } if force { @@ -837,22 +632,28 @@ func (s *Framework) CheckForUpdates(force bool) { } if updated { // if updated, then do not run the web server - if s.Logger != nil { - s.Logger.Println("exiting now...") - } + s.Log(DevMode, "exiting now...") os.Exit(1) } } -// UseSessionDB registers a session database, you can register more than one -// accepts a session database which implements a Load(sid string) map[string]interface{} and an Update(sid string, newValues map[string]interface{}) -// the only reason that a session database will be useful for you is when you want to keep the session's values/data after the app restart -// a session database doesn't have write access to the session, it doesn't accept the context, so forget 'cookie database' for sessions, I will never allow that, for your protection. +// Adapt adapds a policy to the Framework. +// It accepts single or more objects that implements the iris.Policy. +// Iris provides some of them but you can build your own based on one or more of these: +// - iris.EventPolicy +// - iris.RouterReversionPolicy +// - iris.RouterBuilderPolicy +// - iris.RouterWrapperPolicy +// - iris.TemplateRenderPolicy +// - iris.TemplateFuncsPolicy // -// Note: Don't worry if no session database is registered, your context.Session will continue to work. -func UseSessionDB(db sessions.Database) { - Default.UseSessionDB(db) +// With a Policy you can change the behavior of almost each of the existing Iris' features. +// See policy.go for more. +func (s *Framework) Adapt(policies ...Policy) { + for i := range policies { + policies[i].Adapt(&s.policies) + } } // UseSessionDB registers a session database, you can register more than one @@ -865,15 +666,6 @@ func (s *Framework) UseSessionDB(db sessions.Database) { s.sessions.UseDatabase(db) } -// DestroySessionByID removes the session entry -// from the server-side memory (and database if registered). -// Client's session cookie will still exist but it will be reseted on the next request. -// -// It's safe to use it even if you are not sure if a session with that id exists. -func DestroySessionByID(sid string) { - Default.DestroySessionByID(sid) -} - // DestroySessionByID removes the session entry // from the server-side memory (and database if registered). // Client's session cookie will still exist but it will be reseted on the next request. @@ -883,13 +675,6 @@ func (s *Framework) DestroySessionByID(sid string) { s.sessions.DestroyByID(sid) } -// DestroyAllSessions removes all sessions -// from the server-side memory (and database if registered). -// Client's session cookie will still exist but it will be reseted on the next request. -func DestroyAllSessions() { - Default.DestroyAllSessions() -} - // DestroyAllSessions removes all sessions // from the server-side memory (and database if registered). // Client's session cookie will still exist but it will be reseted on the next request. @@ -897,367 +682,77 @@ func (s *Framework) DestroyAllSessions() { s.sessions.DestroyAll() } -// UseSerializer accepts a Serializer and the key or content type on which the developer wants to register this serializer -// the gzip and charset are automatically supported by Iris, by passing the iris.RenderOptions{} map on the context.Render -// context.Render renders this response or a template engine if no response engine with the 'key' found -// with these engines you can inject the context.JSON,Text,Data,JSONP,XML also -// to do that just register with UseSerializer(mySerializer,"application/json") and so on -// look at the https://github.com/kataras/go-serializer for examples -// -// if more than one serializer with the same key/content type exists then the results will be appended to the final request's body -// this allows the developer to be able to create 'middleware' responses engines -// -// Note: if you pass an engine which contains a dot('.') as key, then the engine will not be registered. -// you don't have to import and use github.com/iris-contrib/json, jsonp, xml, data, text, markdown -// because iris uses these by default if no other response engine is registered for these content types -func UseSerializer(forContentType string, e serializer.Serializer) { - Default.UseSerializer(forContentType, e) +// cachedMuxEntry is just a wrapper for the Cache functionality +// it seems useless but I prefer to keep the cached handler on its own memory stack, +// reason: no clojures hell in the Cache function +type cachedMuxEntry struct { + cachedHandler http.Handler } -// UseSerializer accepts a Serializer and the key or content type on which the developer wants to register this serializer -// the gzip and charset are automatically supported by Iris, by passing the iris.RenderOptions{} map on the context.Render -// context.Render renders this response or a template engine if no response engine with the 'key' found -// with these engines you can inject the context.JSON,Text,Data,JSONP,XML also -// to do that just register with UseSerializer(mySerializer,"application/json") and so on -// look at the https://github.com/kataras/go-serializer for examples -// -// if more than one serializer with the same key/content type exists then the results will be appended to the final request's body -// this allows the developer to be able to create 'middleware' responses engines -// -// Note: if you pass an engine which contains a dot('.') as key, then the engine will not be registered. -// you don't have to import and use github.com/iris-contrib/json, jsonp, xml, data, text, markdown -// because iris uses these by default if no other response engine is registered for these content types -func (s *Framework) UseSerializer(forContentType string, e serializer.Serializer) { - s.serializers.For(forContentType, e) -} +func newCachedMuxEntry(s *Framework, bodyHandler HandlerFunc, expiration time.Duration) *cachedMuxEntry { + httpHandler := ToNativeHandler(s, bodyHandler) -// UsePreRender adds a Template's PreRender -// PreRender is typeof func(*iris.Context, filenameOrSource string, binding interface{}, options ...map[string]interface{}) bool -// PreRenders helps developers to pass middleware between the route Handler and a context.Render call -// all parameter receivers can be changed before passing it to the actual context's Render -// so, you can change the filenameOrSource, the page binding, the options, and even add cookies, session value or a flash message through ctx -// the return value of a PreRender is a boolean, if returns false then the next PreRender will not be executed, keep note -// that the actual context's Render will be called at any case. -// -// Example: https://github.com/iris-contrib/examples/tree/master/template_engines/template_prerender -func UsePreRender(pre PreRender) { - Default.UsePreRender(pre) -} - -// UsePreRender adds a Template's PreRender -// PreRender is typeof func(*iris.Context, filenameOrSource string, binding interface{}, options ...map[string]interface{}) bool -// PreRenders helps developers to pass middleware between the route Handler and a context.Render call -// all parameter receivers can be changed before passing it to the actual context's Render -// so, you can change the filenameOrSource, the page binding, the options, and even add cookies, session value or a flash message through ctx -// the return value of a PreRender is a boolean, if returns false then the next PreRender will not be executed, keep note -// that the actual context's Render will be called at any case. -// -// Example: https://github.com/iris-contrib/examples/tree/master/template_engines/template_prerender -func (s *Framework) UsePreRender(pre PreRender) { - s.templates.usePreRender(pre) -} - -// UseTemplateFunc sets or replaces a TemplateFunc from the shared available TemplateFuncMap -// defaults are the iris.URL and iris.Path, all the template engines supports the following: -// {{ url "mynamedroute" "pathParameter_ifneeded"} } -// {{ urlpath "mynamedroute" "pathParameter_ifneeded" }} -// {{ render "header.html" }} -// {{ render_r "header.html" }} // partial relative path to current page -// {{ yield }} -// {{ current }} -// -// See more https:/github.com/iris-contrib/examples/tree/master/template_engines/template_funcmap -func UseTemplateFunc(functionName string, function interface{}) { - Default.UseTemplateFunc(functionName, function) -} - -// UseTemplateFunc sets or replaces a TemplateFunc from the shared available TemplateFuncMap -// defaults are the iris.URL and iris.Path, all the template engines supports the following: -// {{ url "mynamedroute" "pathParameter_ifneeded"} } -// {{ urlpath "mynamedroute" "pathParameter_ifneeded" }} -// {{ render "header.html" }} -// {{ render_r "header.html" }} // partial relative path to current page -// {{ yield }} -// {{ current }} -// -// See more https:/github.com/iris-contrib/examples/tree/master/template_engines/template_funcmap -func (s *Framework) UseTemplateFunc(functionName string, function interface{}) { - s.templates.SharedFuncs[functionName] = function -} - -// UseTemplate adds a template engine to the iris view system -// it does not build/load them yet -func UseTemplate(e template.Engine) *template.Loader { - return Default.UseTemplate(e) -} - -// UseTemplate adds a template engine to the iris view system -// it does not build/load them yet -func (s *Framework) UseTemplate(e template.Engine) *template.Loader { - return s.templates.AddEngine(e) -} - -// UseGlobal registers Handler middleware to the beginning, prepends them instead of append -// -// Use it when you want to add a global middleware to all parties, to all routes in all subdomains -// It should be called right before Listen functions -func UseGlobal(handlers ...Handler) { - Default.UseGlobal(handlers...) -} - -// UseGlobalFunc registers HandlerFunc middleware to the beginning, prepends them instead of append -// -// Use it when you want to add a global middleware to all parties, to all routes in all subdomains -// It should be called right before Listen functions -func UseGlobalFunc(handlersFn ...HandlerFunc) { - Default.UseGlobalFunc(handlersFn...) -} - -// UseGlobal registers Handler middleware to the beginning, prepends them instead of append -// -// Use it when you want to add a global middleware to all parties, to all routes in all subdomains -// It should be called right before Listen functions -func (s *Framework) UseGlobal(handlers ...Handler) { - if len(s.mux.lookups) > 0 { - for _, r := range s.mux.lookups { - r.middleware = append(handlers, r.middleware...) - } - return + cachedHandler := httpcache.Cache(httpHandler, expiration) + return &cachedMuxEntry{ + cachedHandler: cachedHandler, } - - s.Use(handlers...) } -// UseGlobalFunc registers HandlerFunc middleware to the beginning, prepends them instead of append -// -// Use it when you want to add a global middleware to all parties, to all routes in all subdomains -// It should be called right before Listen functions -func (s *Framework) UseGlobalFunc(handlersFn ...HandlerFunc) { - s.UseGlobal(convertToHandlers(handlersFn)...) +func (c *cachedMuxEntry) Serve(ctx *Context) { + c.cachedHandler.ServeHTTP(ctx.ResponseWriter, ctx.Request) } -// ChangeRouter force-changes the pre-defined iris' router while RUNTIME -// this function can be used to wrap the existing router with other. -// You can already do all these things with plugins, this function is a sugar for the craziest among us. +// Cache is just a wrapper for a route's handler which you want to enable body caching +// Usage: iris.Default.Get("/", iris.Cache(func(ctx *iris.Context){ +// ctx.WriteString("Hello, world!") // or a template or anything else +// }, time.Duration(10*time.Second))) // duration of expiration +// if <=time.Second then it tries to find it though request header's "cache-control" maxage value // -// Example of its only usage: -// https://github.com/iris-contrib/plugin/blob/master/cors/plugin.go#L22 -// https://github.com/iris-contrib/plugin/blob/master/cors/plugin.go#L25 -// https://github.com/iris-contrib/plugin/blob/master/cors/plugin.go#L28 -// -// It's recommended that you use Plugin.PreBuild to change the router BEFORE the BUILD state. -func ChangeRouter(h http.Handler) { - Default.ChangeRouter(h) -} - -// ChangeRouter force-changes the pre-defined iris' router while RUNTIME -// this function can be used to wrap the existing router with other. -// You can already do all these things with plugins, this function is a sugar for the craziest among us. -// -// Example of its only usage: -// https://github.com/iris-contrib/plugin/blob/master/cors/plugin.go#L22 -// https://github.com/iris-contrib/plugin/blob/master/cors/plugin.go#L25 -// https://github.com/iris-contrib/plugin/blob/master/cors/plugin.go#L28 -// -// It's recommended that you use Plugin.PreBuild to change the router BEFORE the BUILD state. -func (s *Framework) ChangeRouter(h http.Handler) { - s.Router = h - s.srv.Handler = h -} - -///TODO: Inside note for author: -// make one and only one common API interface for all iris' supported Routers(gorillamux,httprouter,corsrouter) - -// Lookup returns a registered route by its name -func Lookup(routeName string) Route { - return Default.Lookup(routeName) -} - -// Lookups returns all registered routes -func Lookups() []Route { - return Default.Lookups() -} - -// Lookup returns a registered route by its name -func (s *Framework) Lookup(routeName string) Route { - r := s.mux.lookup(routeName) - if nil == r { - return nil - } - return r -} - -// Lookups returns all registered routes -func (s *Framework) Lookups() (routes []Route) { - // silly but... - for i := range s.mux.lookups { - routes = append(routes, s.mux.lookups[i]) - } - return -} - -// SetRouteOnline sets the state of the route to "online" with a specific http method -// it re-builds the router -// -// returns true if state was actually changed -// -// see context.ExecRoute(routeName), -// iris.None(...) and iris.SetRouteOnline/SetRouteOffline -// For more details look: https://github.com/kataras/iris/issues/585 -// -// Example: https://github.com/iris-contrib/examples/tree/master/route_state -func SetRouteOnline(r Route, HTTPMethod string) bool { - return Default.SetRouteOnline(r, HTTPMethod) -} - -// SetRouteOffline sets the state of the route to "offline" and re-builds the router -// -// returns true if state was actually changed -// -// see context.ExecRoute(routeName), -// iris.None(...) and iris.SetRouteOnline/SetRouteOffline -// For more details look: https://github.com/kataras/iris/issues/585 -// -// Example: https://github.com/iris-contrib/examples/tree/master/route_state -func SetRouteOffline(r Route) bool { - return Default.SetRouteOffline(r) -} - -// ChangeRouteState changes the state of the route. -// iris.MethodNone for offline -// and iris.MethodGet/MethodPost/MethodPut/MethodDelete /MethodConnect/MethodOptions/MethodHead/MethodTrace/MethodPatch for online -// it re-builds the router -// -// returns true if state was actually changed -// -// see context.ExecRoute(routeName), -// iris.None(...) and iris.SetRouteOnline/SetRouteOffline -// For more details look: https://github.com/kataras/iris/issues/585 -// -// Example: https://github.com/iris-contrib/examples/tree/master/route_state -func ChangeRouteState(r Route, HTTPMethod string) bool { - return Default.ChangeRouteState(r, HTTPMethod) -} - -// SetRouteOnline sets the state of the route to "online" with a specific http method -// it re-builds the router -// -// returns true if state was actually changed -func (s *Framework) SetRouteOnline(r Route, HTTPMethod string) bool { - return s.ChangeRouteState(r, HTTPMethod) -} - -// SetRouteOffline sets the state of the route to "offline" and re-builds the router -// -// returns true if state was actually changed -func (s *Framework) SetRouteOffline(r Route) bool { - return s.ChangeRouteState(r, MethodNone) -} - -// ChangeRouteState changes the state of the route. -// iris.MethodNone for offline -// and iris.MethodGet/MethodPost/MethodPut/MethodDelete /MethodConnect/MethodOptions/MethodHead/MethodTrace/MethodPatch for online -// it re-builds the router -// -// returns true if state was actually changed -func (s *Framework) ChangeRouteState(r Route, HTTPMethod string) bool { - if r != nil { - nonSpecificMethod := len(HTTPMethod) == 0 - if r.Method() != HTTPMethod { - if nonSpecificMethod { - r.SetMethod(MethodGet) // if no method given, then do it for "GET" only - } else { - r.SetMethod(HTTPMethod) - } - // re-build the router/main handler - s.Router = ToNativeHandler(s, s.mux.BuildHandler()) - return true - } - } - return false -} - -// Path used to check arguments with the route's named parameters and return the correct url -// if parse failed returns empty string -func Path(routeName string, args ...interface{}) string { - return Default.Path(routeName, args...) -} - -func joinPathArguments(args ...interface{}) []interface{} { - arguments := args[0:] - for i, v := range arguments { - if arr, ok := v.([]string); ok { - if len(arr) > 0 { - interfaceArr := make([]interface{}, len(arr)) - for j, sv := range arr { - interfaceArr[j] = sv - } - // replace the current slice - // with the first string element (always as interface{}) - arguments[i] = interfaceArr[0] - // append the rest of them to the slice itself - // the range is not affected by these things in go, - // so we are safe to do it. - arguments = append(args, interfaceArr[1:]...) - } - } - } - return arguments +// Note that it depends on a station instance's cache service. +// Do not try to call it from default' station if you use the form of app := iris.New(), +// use the app.Cache instead of iris.Cache +func (s *Framework) Cache(bodyHandler HandlerFunc, expiration time.Duration) HandlerFunc { + ce := newCachedMuxEntry(s, bodyHandler, expiration) + return ce.Serve } // Path used to check arguments with the route's named parameters and return the correct url // if parse failed returns empty string func (s *Framework) Path(routeName string, args ...interface{}) string { - r := s.mux.lookup(routeName) + r := s.Router.Routes().Lookup(routeName) if r == nil { return "" } - argsLen := len(args) + // why receive interface{} + // but need string? + // because the key:value are string for a route path + // but in the template functions all works fine with ...string + // except when the developer wants to pass that string from a binding + // via Render, then the template will fail to render + // because of expecting string; but got []string - // we have named parameters but arguments not given - if argsLen == 0 && r.formattedParts > 0 { - return "" - } else if argsLen == 0 && r.formattedParts == 0 { - // it's static then just return the path - return r.path + var argsString []string + if len(args) > 0 { + argsString = make([]string, len(args)) } - // we have arguments but they are much more than the named parameters - - // 1 check if we have /*, if yes then join all arguments to one as path and pass that as parameter - if argsLen > r.formattedParts { - if r.path[len(r.path)-1] == matchEverythingByte { - // we have to convert each argument to a string in this case - - argsString := make([]string, argsLen, argsLen) - - for i, v := range args { - if s, ok := v.(string); ok { - argsString[i] = s - } else if num, ok := v.(int); ok { - argsString[i] = strconv.Itoa(num) - } else if b, ok := v.(bool); ok { - argsString[i] = strconv.FormatBool(b) - } else if arr, ok := v.([]string); ok { - if len(arr) > 0 { - argsString[i] = arr[0] - argsString = append(argsString, arr[1:]...) - } - } + for i, v := range args { + if s, ok := v.(string); ok { + argsString[i] = s + } else if num, ok := v.(int); ok { + argsString[i] = strconv.Itoa(num) + } else if b, ok := v.(bool); ok { + argsString[i] = strconv.FormatBool(b) + } else if arr, ok := v.([]string); ok { + if len(arr) > 0 { + argsString[i] = arr[0] + argsString = append(argsString, arr[1:]...) } - - parameter := strings.Join(argsString, slash) - result := fmt.Sprintf(r.formattedPath, parameter) - return result } - // 2 if !1 return false - return "" } - arguments := joinPathArguments(args...) - - return fmt.Sprintf(r.formattedPath, arguments...) + return s.policies.RouterReversionPolicy.URLPath(r, argsString...) } // DecodeQuery returns the uri parameter as url (string) @@ -1291,1008 +786,101 @@ func DecodeURL(uri string) string { return u.String() } -// URL returns the subdomain+ host + Path(...optional named parameters if route is dynamic) -// returns an empty string if parse is failed -func URL(routeName string, args ...interface{}) (url string) { - return Default.URL(routeName, args...) -} - // URL returns the subdomain+ host + Path(...optional named parameters if route is dynamic) // returns an empty string if parse is failed func (s *Framework) URL(routeName string, args ...interface{}) (url string) { - r := s.mux.lookup(routeName) + r := s.Router.Routes().Lookup(routeName) if r == nil { return } scheme := s.Config.VScheme // if s.Config.VScheme was setted, that will be used instead of the real, in order to make easy to run behind nginx host := s.Config.VHost // if s.Config.VHost was setted, that will be used instead of the real, in order to make easy to run behind nginx - arguments := joinPathArguments(args...) // if it's dynamic subdomain then the first argument is the subdomain part - if r.subdomain == dynamicSubdomainIndicator { - if len(arguments) == 0 { // it's a wildcard subdomain but not arguments + // for this part we are responsible not the custom routers + if r.Subdomain() == DynamicSubdomainIndicator { + if len(args) == 0 { // it's a wildcard subdomain but not arguments return } - if subdomain, ok := arguments[0].(string); ok { + if subdomain, ok := args[0].(string); ok { host = subdomain + "." + host } else { // it is not array because we join them before. if not pass a string then this is not a subdomain part, return empty uri return } - - arguments = arguments[1:] + args = args[1:] // remove the subdomain part for the arguments, } - if parsedPath := s.Path(routeName, arguments...); parsedPath != "" { + if parsedPath := s.Path(routeName, args...); parsedPath != "" { url = scheme + host + parsedPath } return } -// TemplateString executes a template from the default template engine and returns its result as string, useful when you want it for sending rich e-mails -// returns empty string on error -func TemplateString(templateFile string, pageContext interface{}, options ...map[string]interface{}) string { - return Default.TemplateString(templateFile, pageContext, options...) -} +var errTemplateRendererIsMissing = errors.New( + ` +manually call of Render for a template: '%s' without specified RenderPolicy! +Please .Adapt one of the available view engines inside 'kataras/iris/adaptors/view'. +By-default Iris supports five template engines: + - standard html | view.HTML(...) + - django | view.Django(...) + - handlebars | view.Handlebars(...) + - pug(jade) | view.Pug(...) + - amber | view.Amber(...) -// TemplateString executes a template from the default template engine and returns its result as string, useful when you want it for sending rich e-mails -// returns empty string on error -func (s *Framework) TemplateString(templateFile string, pageContext interface{}, options ...map[string]interface{}) string { - if s.Config.DisableTemplateEngines { - return "" +Edit your main .go source file to adapt one of these and restart your app. + i.e: lines (<---) were missing. + ------------------------------------------------------------------- + import ( + "github.com/kataras/iris" + "github.com/kataras/iris/adaptors/httprouter" // or gorillamux + "github.com/kataras/iris/adaptors/view" // <--- this line + ) + + func main(){ + app := iris.New() + app.Adapt(httprouter.New()) // or gorillamux.New() + // right below the iris.New() + app.Adapt(view.HTML("./templates", ".html")) // <--- and this line were missing. + + app.Listen("%s") } + ------------------------------------------------------------------- + `) - res, err := s.templates.ExecuteString(templateFile, pageContext, options...) - if err != nil { - return "" - } - return res -} +// RenderOptions is a helper type for the optional runtime options can be passed by user when Render called. +// I.e the "layout" or "gzip" option +// same as iris.Map but more specific name +type RenderOptions map[string]interface{} -// TemplateSourceString executes a template source(raw string contents) from the first template engines which supports raw parsing returns its result as string, -// useful when you want it for sending rich e-mails -// returns empty string on error -func TemplateSourceString(src string, pageContext interface{}) string { - return Default.TemplateSourceString(src, pageContext) -} - -// TemplateSourceString executes a template source(raw string contents) from the first template engines which supports raw parsing returns its result as string, -// useful when you want it for sending rich e-mails -// returns empty string on error -func (s *Framework) TemplateSourceString(src string, pageContext interface{}) string { - if s.Config.DisableTemplateEngines { - return "" - } - res, err := s.templates.ExecuteRawString(src, pageContext) - if err != nil { - res = "" - } - return res -} - -// SerializeToString returns the string of a serializer, -// does not render it to the client -// returns empty string on error -func SerializeToString(keyOrContentType string, obj interface{}, options ...map[string]interface{}) string { - return Default.SerializeToString(keyOrContentType, obj, options...) -} - -// SerializeToString returns the string of a serializer, -// does not render it to the client -// returns empty string on error -func (s *Framework) SerializeToString(keyOrContentType string, obj interface{}, options ...map[string]interface{}) string { - res, err := s.serializers.SerializeToString(keyOrContentType, obj, options...) - if err != nil { - if s.Config.IsDevelopment { - s.Logger.Printf("Error on SerializeToString, Key(content-type): %s. Trace: %s\n", keyOrContentType, err) - } - return "" - } - return res -} - -// Cache is just a wrapper for a route's handler which you want to enable body caching -// Usage: iris.Get("/", iris.Cache(func(ctx *iris.Context){ -// ctx.WriteString("Hello, world!") // or a template or anything else -// }, time.Duration(10*time.Second))) // duration of expiration -// if <=2 seconds then it tries to find it though request header's "cache-control" maxage value +// Render renders using the specific template or any other rich content renderer to the 'w'. // -// Note that it depends on a station instance's cache service. -// Do not try to call it from default' station if you use the form of app := iris.New(), -// use the app.Cache instead of iris.Cache -func Cache(bodyHandler HandlerFunc, expiration time.Duration) HandlerFunc { - return Default.Cache(bodyHandler, expiration) -} - -// Cache is just a wrapper for a route's handler which you want to enable body caching -// Usage: iris.Get("/", iris.Cache(func(ctx *iris.Context){ -// ctx.WriteString("Hello, world!") // or a template or anything else -// }, time.Duration(10*time.Second))) // duration of expiration -// if <=time.Second then it tries to find it though request header's "cache-control" maxage value +// Example of usage: +// - send an e-mail using a template engine that you already +// adapted via: app.Adapt(view.HTML("./templates", ".html")) or app.Adapt(iris.RenderPolicy(mycustomRenderer)). // -// Note that it depends on a station instance's cache service. -// Do not try to call it from default' station if you use the form of app := iris.New(), -// use the app.Cache instead of iris.Cache -func (s *Framework) Cache(bodyHandler HandlerFunc, expiration time.Duration) HandlerFunc { - ce := newCachedMuxEntry(s, bodyHandler, expiration) - return ce.Serve -} - -// ------------------------------------------------------------------------------------- -// ------------------------------------------------------------------------------------- -// ----------------------------------MuxAPI implementation------------------------------ -// ------------------------------------------------------------------------------------- -// ------------------------------------------------------------------------------------- - -type muxAPI struct { - mux *serveMux - doneMiddleware Middleware - apiRoutes []*route // used to register the .Done middleware - relativePath string - middleware Middleware -} - -var _ MuxAPI = &muxAPI{} - -var ( - // errAPIContextNotFound returns an error with message: 'From .API: "Context *iris.Context could not be found..' - errAPIContextNotFound = errors.New("From .API: Context *iris.Context could not be found.") - // errDirectoryFileNotFound returns an error with message: 'Directory or file %s couldn't found. Trace: +error trace' - errDirectoryFileNotFound = errors.New("Directory or file %s couldn't found. Trace: %s") -) - -// Party is just a group joiner of routes which have the same prefix and share same middleware(s) also. -// Party can also be named as 'Join' or 'Node' or 'Group' , Party chosen because it has more fun -func Party(relativePath string, handlersFn ...HandlerFunc) MuxAPI { - return Default.Party(relativePath, handlersFn...) -} - -// Party is just a group joiner of routes which have the same prefix and share same middleware(s) also. -// Party can also be named as 'Join' or 'Node' or 'Group' , Party chosen because it has more fun -func (api *muxAPI) Party(relativePath string, handlersFn ...HandlerFunc) MuxAPI { - parentPath := api.relativePath - dot := string(subdomainIndicator[0]) - if len(parentPath) > 0 && parentPath[0] == slashByte && strings.HasSuffix(relativePath, dot) { // if ends with . , example: admin., it's subdomain-> - parentPath = parentPath[1:] // remove first slash - } - - fullpath := parentPath + relativePath - middleware := convertToHandlers(handlersFn) - // append the parent's +child's handlers - middleware = joinMiddleware(api.middleware, middleware) - - return &muxAPI{relativePath: fullpath, mux: api.mux, apiRoutes: make([]*route, 0), middleware: middleware, doneMiddleware: api.doneMiddleware} -} - -// Use registers Handler middleware -func Use(handlers ...Handler) MuxAPI { - return Default.Use(handlers...) -} - -// UseFunc registers HandlerFunc middleware -func UseFunc(handlersFn ...HandlerFunc) MuxAPI { - return Default.UseFunc(handlersFn...) -} - -// Done registers Handler 'middleware' the only difference from .Use is that it -// should be used BEFORE any party route registered or AFTER ALL party's routes have been registered. -// -// returns itself -func Done(handlers ...Handler) MuxAPI { - return Default.Done(handlers...) -} - -// DoneFunc registers HandlerFunc 'middleware' the only difference from .Use is that it -// should be used BEFORE any party route registered or AFTER ALL party's routes have been registered. -// -// returns itself -func DoneFunc(handlersFn ...HandlerFunc) MuxAPI { - return Default.DoneFunc(handlersFn...) -} - -// Use registers Handler middleware -// returns itself -func (api *muxAPI) Use(handlers ...Handler) MuxAPI { - api.middleware = append(api.middleware, handlers...) - return api -} - -// UseFunc registers HandlerFunc middleware -// returns itself -func (api *muxAPI) UseFunc(handlersFn ...HandlerFunc) MuxAPI { - return api.Use(convertToHandlers(handlersFn)...) -} - -// Done registers Handler 'middleware' the only difference from .Use is that it -// should be used BEFORE any party route registered or AFTER ALL party's routes have been registered. -// -// returns itself -func (api *muxAPI) Done(handlers ...Handler) MuxAPI { - if len(api.apiRoutes) > 0 { // register these middleware on previous-party-defined routes, it called after the party's route methods (Handle/HandleFunc/Get/Post/Put/Delete/...) - for i, n := 0, len(api.apiRoutes); i < n; i++ { - api.apiRoutes[i].middleware = append(api.apiRoutes[i].middleware, handlers...) - } - } else { - // register them on the doneMiddleware, which will be used on Handle to append these middlweare as the last handler(s) - api.doneMiddleware = append(api.doneMiddleware, handlers...) - } - - return api -} - -// Done registers HandlerFunc 'middleware' the only difference from .Use is that it -// should be used BEFORE any party route registered or AFTER ALL party's routes have been registered. -// -// returns itself -func (api *muxAPI) DoneFunc(handlersFn ...HandlerFunc) MuxAPI { - return api.Done(convertToHandlers(handlersFn)...) -} - -// Handle registers a route to the server's router -// if empty method is passed then registers handler(s) for all methods, same as .Any, but returns nil as result -func Handle(method string, registeredPath string, handlers ...Handler) RouteNameFunc { - return Default.Handle(method, registeredPath, handlers...) -} - -// HandleFunc registers and returns a route with a method string, path string and a handler -// registeredPath is the relative url path -func HandleFunc(method string, registeredPath string, handlersFn ...HandlerFunc) RouteNameFunc { - return Default.HandleFunc(method, registeredPath, handlersFn...) -} - -// Handle registers a route to the server's router -// if empty method is passed then registers handler(s) for all methods, same as .Any, but returns nil as result -func (api *muxAPI) Handle(method string, registeredPath string, handlers ...Handler) RouteNameFunc { - if method == "" { // then use like it was .Any - for _, k := range AllMethods { - api.Handle(k, registeredPath, handlers...) - } - return nil - } - - fullpath := api.relativePath + registeredPath // for now, keep the last "/" if any, "/xyz/" - - middleware := joinMiddleware(api.middleware, handlers) - - // here we separate the subdomain and relative path - subdomain := "" - path := fullpath - - if dotWSlashIdx := strings.Index(path, subdomainIndicator); dotWSlashIdx > 0 { - subdomain = fullpath[0 : dotWSlashIdx+1] // admin. - path = fullpath[dotWSlashIdx+1:] // / - } - - // we splitted the path and subdomain parts so we're ready to check only the path, - // otherwise we will had problems with subdomains - // if the user wants beta:= iris.Party("/beta"); beta.Get("/") to be registered as - //: /beta/ then should disable the path correction OR register it like: beta.Get("//") - // this is only for the party's roots in order to have expected paths, - // as we do with iris.Get("/") which is localhost:8080 as RFC points, not localhost:8080/ - if api.mux.correctPath && registeredPath == slash { // check the given relative path - // remove last "/" if any, "/xyz/" - if len(path) > 1 { // if it's the root, then keep it* - if path[len(path)-1] == slashByte { - // ok we are inside /xyz/ - } - } - } - - path = strings.Replace(path, "//", "/", -1) // fix the path if double // - - if len(api.doneMiddleware) > 0 { - middleware = append(middleware, api.doneMiddleware...) // register the done middleware, if any - } - r := api.mux.register(method, subdomain, path, middleware) - - api.apiRoutes = append(api.apiRoutes, r) - // should we remove the api.apiRoutes on the .Party (new children party) ?, No, because the user maybe use this party later - // should we add to the 'inheritance tree' the api.apiRoutes, No, these are for this specific party only, because the user propably, will have unexpected behavior when using Use/UseFunc, Done/DoneFunc - return r.setName -} - -// HandleFunc registers and returns a route with a method string, path string and a handler -// registeredPath is the relative url path -func (api *muxAPI) HandleFunc(method string, registeredPath string, handlersFn ...HandlerFunc) RouteNameFunc { - return api.Handle(method, registeredPath, convertToHandlers(handlersFn)...) -} - -// API converts & registers a custom struct to the router -// receives two parameters -// first is the request path (string) -// second is the custom struct (interface{}) which can be anything that has a *iris.Context as field. -// third is the common middlewares, it's optional -// -// Note that API's routes have their default-name to the full registered path, -// no need to give a special name for it, because it's not supposed to be used inside your templates. -// -// Recommend to use when you retrieve data from an external database, -// and the router-performance is not the (only) thing which slows the server's overall performance. -// -// This is a slow method, if you care about router-performance use the Handle/HandleFunc/Get/Post/Put/Delete/Trace/Options... instead -func API(path string, restAPI HandlerAPI, middleware ...HandlerFunc) { - Default.API(path, restAPI, middleware...) -} - -// API converts & registers a custom struct to the router -// receives two parameters -// first is the request path (string) -// second is the custom struct (interface{}) which can be anything that has a *iris.Context as field. -// third is the common middleware, it's optional -// -// Note that API's routes have their default-name to the full registered path, -// no need to give a special name for it, because it's not supposed to be used inside your templates. -// -// Recommend to use when you retrieve data from an external database, -// and the router-performance is not the (only) thing which slows the server's overall performance. -// -// This is a slow method, if you care about router-performance use the Handle/HandleFunc/Get/Post/Put/Delete/Trace/Options... instead -func (api *muxAPI) API(path string, restAPI HandlerAPI, middleware ...HandlerFunc) { - // here we need to find the registered methods and convert them to handler funcs - // methods are collected by method naming: Get(),GetBy(...), Post(),PostBy(...), Put() and so on - if len(path) == 0 { - path = "/" - } - if path[0] != slashByte { - // the route's paths always starts with "/", when the client navigates, the router works without "/" also , - // but the developer should always prepend the slash ("/") to register the routes - path = "/" + path - } - typ := reflect.ValueOf(restAPI).Type() - contextField, found := typ.FieldByName("Context") - if !found { - panic(errAPIContextNotFound) - } - - // check & register the Get(),Post(),Put(),Delete() and so on - for _, methodName := range AllMethods { - - methodCapitalName := strings.Title(strings.ToLower(methodName)) - - if method, found := typ.MethodByName(methodCapitalName); found { - methodFunc := method.Func - if !methodFunc.IsValid() || methodFunc.Type().NumIn() > 1 { // for any case - continue - } - - func(path string, typ reflect.Type, contextField reflect.StructField, methodFunc reflect.Value, method string) { - var handlersFn []HandlerFunc - - handlersFn = append(handlersFn, middleware...) - handlersFn = append(handlersFn, func(ctx *Context) { - newController := reflect.New(typ).Elem() - newController.FieldByName("Context").Set(reflect.ValueOf(ctx)) - methodFunc.Call([]reflect.Value{newController}) - }) - // register route - api.HandleFunc(method, path, handlersFn...) - }(path, typ, contextField, methodFunc, methodName) +// It can also render json,xml,jsonp and markdown by-default before or after .Build too. +func (s *Framework) Render(w io.Writer, name string, bind interface{}, options ...map[string]interface{}) error { + err, ok := s.policies.RenderPolicy(w, name, bind, options...) + if !ok { + // ok is false ONLY WHEN there is no registered render policy + // that is responsible for that 'name` (if contains dot '.' it's for templates). + // We don't use default template engines on the new version, + // so we should notice the user here, we could make it to panic but because that is on runtime + // we don't want to panic for that, let's give a message if the user adapted a logger for dev. + // And return that error in the case the user wasn't in dev mode, she/he can catch this error. + // Also on the README we will add the .Adapt(iris.DevLogger()) to mention that + // logging for any runtime info(except http server's panics and unexpected serious errors) is not enabled by-default. + if strings.Contains(name, ".") { + err = errTemplateRendererIsMissing.Format(name, s.Config.VHost) + s.Log(DevMode, err.Error()) + return err } } - - // check for GetBy/PostBy(id string, something_else string) , these must be requested by the same order. - // (we could do this in the same top loop but I don't want) - // GET, DELETE -> with url named parameters (/users/:id/:secondArgumentIfExists) - // POST, PUT -> with post values (form) - // all other with URL Parameters (?something=this&else=other - // - // or no, I changed my mind, let all be named parameters and let users to decide what info they need, - // using the Context to take more values (post form,url params and so on).- - - paramPrefix := "param" - for _, methodName := range AllMethods { - methodWithBy := strings.Title(strings.ToLower(methodName)) + "By" - if method, found := typ.MethodByName(methodWithBy); found { - methodFunc := method.Func - if !methodFunc.IsValid() || methodFunc.Type().NumIn() < 2 { //it's By but it has not receive any arguments so its not api's - continue - } - methodFuncType := methodFunc.Type() - numInLen := methodFuncType.NumIn() // how much data we should receive from the request - registeredPath := path - - for i := 1; i < numInLen; i++ { // from 1 because the first is the 'object' - if registeredPath[len(registeredPath)-1] == slashByte { - registeredPath += ":" + string(paramPrefix) + strconv.Itoa(i) - } else { - registeredPath += "/:" + string(paramPrefix) + strconv.Itoa(i) - } - } - - func(registeredPath string, typ reflect.Type, contextField reflect.StructField, methodFunc reflect.Value, paramsLen int, method string) { - var handlersFn []HandlerFunc - - handlersFn = append(handlersFn, middleware...) - handlersFn = append(handlersFn, func(ctx *Context) { - newController := reflect.New(typ).Elem() - newController.FieldByName("Context").Set(reflect.ValueOf(ctx)) - args := make([]reflect.Value, paramsLen+1, paramsLen+1) - args[0] = newController - j := 1 - - ctx.VisitValues(func(k string, v interface{}) { - if strings.HasPrefix(k, paramPrefix) { - args[j] = reflect.ValueOf(v.(string)) - - j++ // the first parameter is the context, other are the path parameters, j++ to be align with (API's registered)paramsLen - } - }) - - methodFunc.Call(args) - }) - // register route - api.HandleFunc(method, registeredPath, handlersFn...) - }(registeredPath, typ, contextField, methodFunc, numInLen-1, methodName) - - } - - } - -} - -// None registers an "offline" route -// see context.ExecRoute(routeName), -// iris.None(...) and iris.SetRouteOnline/SetRouteOffline -// For more details look: https://github.com/kataras/iris/issues/585 -// -// Example: https://github.com/iris-contrib/examples/tree/master/route_state -func None(path string, handlersFn ...HandlerFunc) RouteNameFunc { - return Default.None(path, handlersFn...) -} - -// Get registers a route for the Get http method -func Get(path string, handlersFn ...HandlerFunc) RouteNameFunc { - return Default.Get(path, handlersFn...) -} - -// Post registers a route for the Post http method -func Post(path string, handlersFn ...HandlerFunc) RouteNameFunc { - return Default.Post(path, handlersFn...) -} - -// Put registers a route for the Put http method -func Put(path string, handlersFn ...HandlerFunc) RouteNameFunc { - return Default.Put(path, handlersFn...) -} - -// Delete registers a route for the Delete http method -func Delete(path string, handlersFn ...HandlerFunc) RouteNameFunc { - return Default.Delete(path, handlersFn...) -} - -// Connect registers a route for the Connect http method -func Connect(path string, handlersFn ...HandlerFunc) RouteNameFunc { - return Default.Connect(path, handlersFn...) -} - -// Head registers a route for the Head http method -func Head(path string, handlersFn ...HandlerFunc) RouteNameFunc { - return Default.Head(path, handlersFn...) -} - -// Options registers a route for the Options http method -func Options(path string, handlersFn ...HandlerFunc) RouteNameFunc { - return Default.Options(path, handlersFn...) -} - -// Patch registers a route for the Patch http method -func Patch(path string, handlersFn ...HandlerFunc) RouteNameFunc { - return Default.Patch(path, handlersFn...) -} - -// Trace registers a route for the Trace http method -func Trace(path string, handlersFn ...HandlerFunc) RouteNameFunc { - return Default.Trace(path, handlersFn...) -} - -// Any registers a route for ALL of the http methods (Get,Post,Put,Head,Patch,Options,Connect,Delete) -func Any(registeredPath string, handlersFn ...HandlerFunc) { - Default.Any(registeredPath, handlersFn...) -} - -// None registers an "offline" route -// see context.ExecRoute(routeName), -// iris.None(...) and iris.SetRouteOnline/SetRouteOffline -// For more details look: https://github.com/kataras/iris/issues/585 -// -// Example: https://github.com/iris-contrib/examples/tree/master/route_state -func (api *muxAPI) None(path string, handlersFn ...HandlerFunc) RouteNameFunc { - return api.HandleFunc(MethodNone, path, handlersFn...) -} - -// Get registers a route for the Get http method -func (api *muxAPI) Get(path string, handlersFn ...HandlerFunc) RouteNameFunc { - return api.HandleFunc(MethodGet, path, handlersFn...) -} - -// Post registers a route for the Post http method -func (api *muxAPI) Post(path string, handlersFn ...HandlerFunc) RouteNameFunc { - return api.HandleFunc(MethodPost, path, handlersFn...) -} - -// Put registers a route for the Put http method -func (api *muxAPI) Put(path string, handlersFn ...HandlerFunc) RouteNameFunc { - return api.HandleFunc(MethodPut, path, handlersFn...) -} - -// Delete registers a route for the Delete http method -func (api *muxAPI) Delete(path string, handlersFn ...HandlerFunc) RouteNameFunc { - return api.HandleFunc(MethodDelete, path, handlersFn...) -} - -// Connect registers a route for the Connect http method -func (api *muxAPI) Connect(path string, handlersFn ...HandlerFunc) RouteNameFunc { - return api.HandleFunc(MethodConnect, path, handlersFn...) -} - -// Head registers a route for the Head http method -func (api *muxAPI) Head(path string, handlersFn ...HandlerFunc) RouteNameFunc { - return api.HandleFunc(MethodHead, path, handlersFn...) -} - -// Options registers a route for the Options http method -func (api *muxAPI) Options(path string, handlersFn ...HandlerFunc) RouteNameFunc { - return api.HandleFunc(MethodOptions, path, handlersFn...) -} - -// Patch registers a route for the Patch http method -func (api *muxAPI) Patch(path string, handlersFn ...HandlerFunc) RouteNameFunc { - return api.HandleFunc(MethodPatch, path, handlersFn...) -} - -// Trace registers a route for the Trace http method -func (api *muxAPI) Trace(path string, handlersFn ...HandlerFunc) RouteNameFunc { - return api.HandleFunc(MethodTrace, path, handlersFn...) -} - -// Any registers a route for ALL of the http methods (Get,Post,Put,Head,Patch,Options,Connect,Delete) -func (api *muxAPI) Any(registeredPath string, handlersFn ...HandlerFunc) { - for _, k := range AllMethods { - api.HandleFunc(k, registeredPath, handlersFn...) - } -} - -// if / then returns /*wildcard or /something then /something/*wildcard -// if empty then returns /*wildcard too -func validateWildcard(reqPath string, paramName string) string { - if reqPath[len(reqPath)-1] != slashByte { - reqPath += slash - } - reqPath += "*" + paramName - return reqPath -} - -func (api *muxAPI) registerResourceRoute(reqPath string, h HandlerFunc) RouteNameFunc { - api.Head(reqPath, h) - return api.Get(reqPath, h) -} - -// StaticServe serves a directory as web resource -// it's the simpliest form of the Static* functions -// Almost same usage as StaticWeb -// accepts only one required parameter which is the systemPath ( the same path will be used to register the GET&HEAD routes) -// if second parameter is empty, otherwise the requestPath is the second parameter -// it uses gzip compression (compression on each request, no file cache) -func StaticServe(systemPath string, requestPath ...string) RouteNameFunc { - return Default.StaticServe(systemPath, requestPath...) -} - -// StaticServe serves a directory as web resource -// it's the simpliest form of the Static* functions -// Almost same usage as StaticWeb -// accepts only one required parameter which is the systemPath ( the same path will be used to register the GET&HEAD routes) -// if second parameter is empty, otherwise the requestPath is the second parameter -// it uses gzip compression (compression on each request, no file cache) -func (api *muxAPI) StaticServe(systemPath string, requestPath ...string) RouteNameFunc { - var reqPath string - - if len(requestPath) == 0 { - reqPath = strings.Replace(systemPath, fs.PathSeparator, slash, -1) // replaces any \ to / - reqPath = strings.Replace(reqPath, "//", slash, -1) // for any case, replaces // to / - reqPath = strings.Replace(reqPath, ".", "", -1) // replace any dots (./mypath -> /mypath) - } else { - reqPath = requestPath[0] - } - - return api.Get(reqPath+"/*file", func(ctx *Context) { - filepath := ctx.Param("file") - - spath := strings.Replace(filepath, "/", fs.PathSeparator, -1) - spath = path.Join(systemPath, spath) - - if !fs.DirectoryExists(spath) { - ctx.NotFound() - return - } - - ctx.ServeFile(spath, true) - }) -} - -// StaticContent serves bytes, memory cached, on the reqPath -// a good example of this is how the websocket server uses that to auto-register the /iris-ws.js -func StaticContent(reqPath string, contentType string, content []byte) RouteNameFunc { - return Default.StaticContent(reqPath, contentType, content) -} - -// StaticContent serves bytes, memory cached, on the reqPath -// a good example of this is how the websocket server uses that to auto-register the /iris-ws.js -func (api *muxAPI) StaticContent(reqPath string, cType string, content []byte) RouteNameFunc { // func(string) because we use that on websockets - modtime := time.Now() - h := func(ctx *Context) { - ctx.SetClientCachedBody(StatusOK, content, cType, modtime) - } - - return api.registerResourceRoute(reqPath, h) -} - -// StaticEmbedded used when files are distributed inside the app executable, using go-bindata mostly -// First parameter is the request path, the path which the files in the vdir(second parameter) will be served to, for example "/static" -// Second parameter is the (virtual) directory path, for example "./assets" -// Third parameter is the Asset function -// Forth parameter is the AssetNames function -// -// For more take a look at the -// example: https://github.com/iris-contrib/examples/tree/master/static_files_embedded -func StaticEmbedded(requestPath string, vdir string, assetFn func(name string) ([]byte, error), namesFn func() []string) RouteNameFunc { - return Default.StaticEmbedded(requestPath, vdir, assetFn, namesFn) -} - -// StaticEmbedded used when files are distributed inside the app executable, using go-bindata mostly -// First parameter is the request path, the path which the files in the vdir will be served to, for example "/static" -// Second parameter is the (virtual) directory path, for example "./assets" -// Third parameter is the Asset function -// Forth parameter is the AssetNames function -// -// For more take a look at the -// example: https://github.com/iris-contrib/examples/tree/master/static_files_embedded -func (api *muxAPI) StaticEmbedded(requestPath string, vdir string, assetFn func(name string) ([]byte, error), namesFn func() []string) RouteNameFunc { - - // check if requestPath already contains an asterix-match to anything symbol: /path/* - requestPath = strings.Replace(requestPath, "//", "/", -1) - matchEverythingIdx := strings.IndexByte(requestPath, matchEverythingByte) - paramName := "path" - - if matchEverythingIdx != -1 { - // found so it should has a param name, take it - paramName = requestPath[matchEverythingIdx+1:] - } else { - // make the requestPath - if requestPath[len(requestPath)-1] == slashByte { - // ends with / remove it - requestPath = requestPath[0 : len(requestPath)-2] - } - - requestPath += slash + "*" + paramName // $requestPath/*path - } - - if len(vdir) > 0 { - if vdir[0] == '.' { // first check for .wrong - vdir = vdir[1:] - } - if vdir[0] == '/' || vdir[0] == os.PathSeparator { // second check for /something, (or ./something if we had dot on 0 it will be removed - vdir = vdir[1:] - } - } - - // collect the names we are care for, because not all Asset used here, we need the vdir's assets. - allNames := namesFn() - - var names []string - for _, path := range allNames { - // check if path is the path name we care for - if !strings.HasPrefix(path, vdir) { - continue - } - - path = strings.Replace(path, "\\", "/", -1) // replace system paths with double slashes - path = strings.Replace(path, "./", "/", -1) // replace ./assets/favicon.ico to /assets/favicon.ico in order to be ready for compare with the reqPath later - path = path[len(vdir):] // set it as the its 'relative' ( we should re-setted it when assetFn will be used) - names = append(names, path) - - } - if len(names) == 0 { - // we don't start the server yet, so: - panic("iris.StaticEmbedded: Unable to locate any embedded files located to the (virtual) directory: " + vdir) - } - - modtime := time.Now() - h := func(ctx *Context) { - - reqPath := ctx.Param(paramName) - - for _, path := range names { - - if path != reqPath { - continue - } - - cType := fs.TypeByExtension(path) - fullpath := vdir + path - - buf, err := assetFn(fullpath) - - if err != nil { - continue - } - - ctx.SetClientCachedBody(StatusOK, buf, cType, modtime) - return - } - - // not found or error - ctx.EmitError(StatusNotFound) - - } - - return api.registerResourceRoute(requestPath, h) -} - -// Favicon serves static favicon -// accepts 2 parameters, second is optional -// favPath (string), declare the system directory path of the __.ico -// requestPath (string), it's the route's path, by default this is the "/favicon.ico" because some browsers tries to get this by default first, -// you can declare your own path if you have more than one favicon (desktop, mobile and so on) -// -// this func will add a route for you which will static serve the /yuorpath/yourfile.ico to the /yourfile.ico (nothing special that you can't handle by yourself) -// Note that you have to call it on every favicon you have to serve automatically (desktop, mobile and so on) -// -// panics on error -func Favicon(favPath string, requestPath ...string) RouteNameFunc { - return Default.Favicon(favPath, requestPath...) -} - -// Favicon serves static favicon -// accepts 2 parameters, second is optional -// favPath (string), declare the system directory path of the __.ico -// requestPath (string), it's the route's path, by default this is the "/favicon.ico" because some browsers tries to get this by default first, -// you can declare your own path if you have more than one favicon (desktop, mobile and so on) -// -// this func will add a route for you which will static serve the /yuorpath/yourfile.ico to the /yourfile.ico (nothing special that you can't handle by yourself) -// Note that you have to call it on every favicon you have to serve automatically (desktop, mobile and so on) -// -// panics on error -func (api *muxAPI) Favicon(favPath string, requestPath ...string) RouteNameFunc { - f, err := os.Open(favPath) - if err != nil { - panic(errDirectoryFileNotFound.Format(favPath, err.Error())) - } - defer f.Close() - fi, _ := f.Stat() - if fi.IsDir() { // if it's dir the try to get the favicon.ico - fav := path.Join(favPath, "favicon.ico") - f, err = os.Open(fav) - if err != nil { - //we try again with .png - return api.Favicon(path.Join(favPath, "favicon.png")) - } - favPath = fav - fi, _ = f.Stat() - } - - cType := fs.TypeByExtension(favPath) - // copy the bytes here in order to cache and not read the ico on each request. - cacheFav := make([]byte, fi.Size()) - if _, err = f.Read(cacheFav); err != nil { - panic(errDirectoryFileNotFound.Format(favPath, "Couldn't read the data bytes for Favicon: "+err.Error())) - } - modtime := "" - h := func(ctx *Context) { - if modtime == "" { - modtime = fi.ModTime().UTC().Format(ctx.framework.Config.TimeFormat) - } - if t, err := time.Parse(ctx.framework.Config.TimeFormat, ctx.RequestHeader(ifModifiedSince)); err == nil && fi.ModTime().Before(t.Add(StaticCacheDuration)) { - - ctx.ResponseWriter.Header().Del(contentType) - ctx.ResponseWriter.Header().Del(contentLength) - ctx.SetStatusCode(StatusNotModified) - return - } - - ctx.ResponseWriter.Header().Set(contentType, cType) - ctx.ResponseWriter.Header().Set(lastModified, modtime) - ctx.SetStatusCode(StatusOK) - ctx.Write(cacheFav) - } - - reqPath := "/favicon" + path.Ext(fi.Name()) //we could use the filename, but because standards is /favicon.ico/.png. - if len(requestPath) > 0 { - reqPath = requestPath[0] - } - - return api.registerResourceRoute(reqPath, h) -} - -// StaticHandler returns a new Handler which serves static files -func StaticHandler(reqPath string, systemPath string, showList bool, enableGzip bool) HandlerFunc { - return Default.StaticHandler(reqPath, systemPath, showList, enableGzip) -} - -// StaticHandler returns a new Handler which serves static files -func (api *muxAPI) StaticHandler(reqPath string, systemPath string, showList bool, enableGzip bool, exceptRoutes ...Route) HandlerFunc { - // here we separate the path from the subdomain (if any), we care only for the path - // fixes a bug when serving static files via a subdomain - fullpath := api.relativePath + reqPath - path := fullpath - if dotWSlashIdx := strings.Index(path, subdomainIndicator); dotWSlashIdx > 0 { - path = fullpath[dotWSlashIdx+1:] - } - - h := NewStaticHandlerBuilder(systemPath). - Path(path). - Listing(showList). - Gzip(enableGzip). - Except(exceptRoutes...). - Build() - - managedStaticHandler := func(ctx *Context) { - h(ctx) - prevStatusCode := ctx.ResponseWriter.StatusCode() - if prevStatusCode >= 400 { // we have an error - // fire the custom error handler - api.mux.fireError(prevStatusCode, ctx) - } - // go to the next middleware - if ctx.Pos < len(ctx.Middleware)-1 { - ctx.Next() - } - } - return managedStaticHandler -} - -// StaticWeb returns a handler that serves HTTP requests -// with the contents of the file system rooted at directory. -// -// first parameter: the route path -// second parameter: the system directory -// third OPTIONAL parameter: the exception routes -// (= give priority to these routes instead of the static handler) -// for more options look iris.StaticHandler. -// -// iris.StaticWeb("/static", "./static") -// -// As a special case, the returned file server redirects any request -// ending in "/index.html" to the same path, without the final -// "index.html". -// -// StaticWeb calls the StaticHandler(reqPath, systemPath, listingDirectories: false, gzip: false ). -func StaticWeb(reqPath string, systemPath string, exceptRoutes ...Route) RouteNameFunc { - return Default.StaticWeb(reqPath, systemPath, exceptRoutes...) -} - -// StaticWeb returns a handler that serves HTTP requests -// with the contents of the file system rooted at directory. -// -// first parameter: the route path -// second parameter: the system directory -// third OPTIONAL parameter: the exception routes -// (= give priority to these routes instead of the static handler) -// for more options look iris.StaticHandler. -// -// iris.StaticWeb("/static", "./static") -// -// As a special case, the returned file server redirects any request -// ending in "/index.html" to the same path, without the final -// "index.html". -// -// StaticWeb calls the StaticHandler(reqPath, systemPath, listingDirectories: false, gzip: false ). -func (api *muxAPI) StaticWeb(reqPath string, systemPath string, exceptRoutes ...Route) RouteNameFunc { - h := api.StaticHandler(reqPath, systemPath, false, false, exceptRoutes...) - routePath := validateWildcard(reqPath, "file") - return api.registerResourceRoute(routePath, h) -} - -// Layout oerrides the parent template layout with a more specific layout for this Party -// returns this Party, to continue as normal -// example: -// my := iris.Party("/my").Layout("layouts/mylayout.html") -// { -// my.Get("/", func(ctx *iris.Context) { -// ctx.MustRender("page1.html", nil) -// }) -// } -// -func Layout(tmplLayoutFile string) MuxAPI { - return Default.Layout(tmplLayoutFile) -} - -// Layout oerrides the parent template layout with a more specific layout for this Party -// returns this Party, to continue as normal -// example: -// my := iris.Party("/my").Layout("layouts/mylayout.html") -// { -// my.Get("/", func(ctx *iris.Context) { -// ctx.MustRender("page1.html", nil) -// }) -// } -// -func (api *muxAPI) Layout(tmplLayoutFile string) MuxAPI { - api.UseFunc(func(ctx *Context) { - ctx.Set(TemplateLayoutContextKey, tmplLayoutFile) - ctx.Next() - }) - - return api -} - -// OnError registers a custom http error handler -func OnError(statusCode int, handlerFn HandlerFunc) { - Default.OnError(statusCode, handlerFn) -} - -// EmitError fires a custom http error handler to the client -// -// if no custom error defined with this statuscode, then iris creates one, and once at runtime -func EmitError(statusCode int, ctx *Context) { - Default.EmitError(statusCode, ctx) -} - -// OnError registers a custom http error handler -func (api *muxAPI) OnError(statusCode int, handlerFn HandlerFunc) { - - path := strings.Replace(api.relativePath, "//", "/", -1) // fix the path if double // - staticPath := path - // find the static path (on Party the path should be ALWAYS a static path, as we all know, - // but do this check for any case) - dynamicPathIdx := strings.IndexByte(path, parameterStartByte) // check for /mypath/:param - - if dynamicPathIdx == -1 { - dynamicPathIdx = strings.IndexByte(path, matchEverythingByte) // check for /mypath/*param - } - - if dynamicPathIdx > 1 { //yes after / and one character more ( /*param or /:param will break the root path, and this is not allowed even on error handlers). - staticPath = api.relativePath[0:dynamicPathIdx] - } - - if staticPath == "/" { - api.mux.registerError(statusCode, handlerFn) // register the user-specific error message, as the global error handler, for now. - return - } - - //after this, we have more than one error handler for one status code, and that's dangerous some times, but use it for non-globals error catching by your own risk - // NOTES: - // subdomains error will not work if same path of a non-subdomain (maybe a TODO for later) - // errors for parties should be registered from the biggest path length to the smaller. - - // get the previous - prevErrHandler := api.mux.errorHandlers[statusCode] - if prevErrHandler == nil { - /* - make a new one with the standard error message, - this will be used as the last handler if no other error handler catches the error (by prefix(?)) - */ - prevErrHandler = HandlerFunc(func(ctx *Context) { - if w, ok := ctx.IsRecording(); ok { - w.Reset() - } - ctx.SetStatusCode(statusCode) - ctx.WriteString(statusText[statusCode]) - }) - } - - func(statusCode int, staticPath string, prevErrHandler Handler, newHandler Handler) { // to separate the logic - errHandler := HandlerFunc(func(ctx *Context) { - if strings.HasPrefix(ctx.Path(), staticPath) { // yes the user should use OnError from longest to lower static path's length in order this to work, so we can find another way, like a builder on the end. - newHandler.Serve(ctx) - return - } - // serve with the user-specific global ("/") pure iris.OnError receiver Handler or the standar handler if OnError called only from inside a no-relative Party. - prevErrHandler.Serve(ctx) - }) - - api.mux.registerError(statusCode, errHandler) - }(statusCode, staticPath, prevErrHandler, handlerFn) - -} - -// EmitError fires a custom http error handler to the client -// -// if no custom error defined with this statuscode, then iris creates one, and once at runtime -func (api *muxAPI) EmitError(statusCode int, ctx *Context) { - api.mux.fireError(statusCode, ctx) + return err } diff --git a/iris/doc.go b/iris/doc.go index 743ef584..aabd078f 100644 --- a/iris/doc.go +++ b/iris/doc.go @@ -1,4 +1,4 @@ -package main // import "github.com/kataras/iris/iris" +package main /* diff --git a/iris/main.go b/iris/main.go index 3f3c74a6..fedccea8 100644 --- a/iris/main.go +++ b/iris/main.go @@ -2,7 +2,7 @@ package main import ( "github.com/kataras/cli" - "github.com/kataras/iris" + "gopkg.in/kataras/iris.v6" ) var ( diff --git a/middleware/README.md b/middleware/README.md deleted file mode 100644 index c5ececdb..00000000 --- a/middleware/README.md +++ /dev/null @@ -1,84 +0,0 @@ -# Middleware - -We should mention that Iris is compatible with **ALL** net/http middleware out there, -You are not restricted to so-called 'iris-made' middleware. They do exists, mostly, for your learning curve. - -Navigate through [iris-contrib/middleware](https://github.com/iris-contrib/through) repository to view iris-made 'middleware'. - -> By the word 'middleware', we mean a single or a collection of route handlers which may execute before/or after the main route handler. - - -## Installation - -```sh -$ go get github.com/iris-contrib/middleware/... -``` - -## How can I register a middleware? - -```go -app := iris.New() -/* per root path and all its children */ -app.Use(logger) - -/* execute always last */ -// app.Done(logger) - -/* per-route, order matters. */ -// app.Get("/", logger, indexHandler) - -/* per party (group of routes) */ -// userRoutes := app.Party("/user", logger) -// userRoutes.Post("/login", loginAuthHandler) -``` - -## How 'hard' is to create an Iris middleware? - -```go -myMiddleware := func(ctx *iris.Context){ - /* using ctx.Set you can transfer ANY data between handlers, - use ctx.Get("welcomed") to get its value on the next handler(s). - */ - ctx.Set("welcomed", true) - - println("My middleware!") -} -``` -> func(ctx *iris.Context) is just the `iris.HandlerFunc` signature which implements the `iris.Handler`/ `Serve(Context)` method. - -```go -app := iris.New() -/* root path and all its children */ -app.UseFunc(myMiddleware) - -app.Get("/", indexHandler) -``` - -## Convert `http.Handler` to `iris.Handler` using the `iris.ToHandler` - -```go -// ToHandler converts different type styles of handlers that you -// used to use (usually with third-party net/http middleware) to an iris.HandlerFunc. -// -// Supported types: -// - .ToHandler(h http.Handler) -// - .ToHandler(func(w http.ResponseWriter, r *http.Request)) -// - .ToHandler(func(w http.ResponseWriter, r *http.Request, next http.HandlerFunc)) -func ToHandler(handler interface{}) HandlerFunc -``` - - -```go -app := iris.New() - -sillyHTTPHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request){ - println(r.RequestURI) -}) - -app.Use(iris.ToHandler(sillyHTTPHandler)) -``` - - -## What next? - -Read more about [iris.Handler](https://docs.iris-go.com/using-handlers.html), [iris.HandlerFunc](https://docs.iris-go.com/using-handlerfuncs.html) and [Middleware](https://docs.iris-go.com/middleware.html). diff --git a/middleware/basicauth/_example/main.go b/middleware/basicauth/_example/main.go new file mode 100644 index 00000000..c72134ff --- /dev/null +++ b/middleware/basicauth/_example/main.go @@ -0,0 +1,56 @@ +package main + +import ( + "time" + + "gopkg.in/kataras/iris.v6" + "gopkg.in/kataras/iris.v6/adaptors/httprouter" + "gopkg.in/kataras/iris.v6/middleware/basicauth" +) + +func main() { + app := iris.New() + app.Adapt(httprouter.New()) // adapt a router first of all + + authConfig := basicauth.Config{ + Users: map[string]string{"myusername": "mypassword", "mySecondusername": "mySecondpassword"}, + Realm: "Authorization Required", // defaults to "Authorization Required" + ContextKey: "mycustomkey", // defaults to "user" + Expires: time.Duration(30) * time.Minute, + } + + authentication := basicauth.New(authConfig) + app.Get("/", func(ctx *iris.Context) { ctx.Redirect("/admin") }) + // to global app.Use(authentication) (or app.UseGlobal before the .Listen) + // to routes + /* + app.Get("/mysecret", authentication, func(ctx *iris.Context) { + username := ctx.GetString("mycustomkey") // the Contextkey from the authConfig + ctx.Writef("Hello authenticated user: %s ", username) + }) + */ + + // to party + + needAuth := app.Party("/admin", authentication) + { + //http://localhost:8080/admin + needAuth.Get("/", func(ctx *iris.Context) { + username := ctx.GetString("mycustomkey") // the Contextkey from the authConfig + ctx.Writef("Hello authenticated user: %s from: %s ", username, ctx.Path()) + }) + // http://localhost:8080/admin/profile + needAuth.Get("/profile", func(ctx *iris.Context) { + username := ctx.GetString("mycustomkey") // the Contextkey from the authConfig + ctx.Writef("Hello authenticated user: %s from: %s ", username, ctx.Path()) + }) + // http://localhost:8080/admin/settings + needAuth.Get("/settings", func(ctx *iris.Context) { + username := authConfig.User(ctx) // shortcut for ctx.GetString("mycustomkey") + ctx.Writef("Hello authenticated user: %s from: %s ", username, ctx.Path()) + }) + } + + // open http://localhost:8080/admin + app.Listen(":8080") +} diff --git a/middleware/basicauth/basicauth.go b/middleware/basicauth/basicauth.go new file mode 100644 index 00000000..8290d23b --- /dev/null +++ b/middleware/basicauth/basicauth.go @@ -0,0 +1,131 @@ +package basicauth + +import ( + "encoding/base64" + "strconv" + "time" + + "gopkg.in/kataras/iris.v6" +) + +// +------------------------------------------------------------+ +// | Middleware usage | +// +------------------------------------------------------------+ +// +// import "gopkg.in/kataras/iris.v6/middleware/basicauth" +// +// app := iris.New() +// authentication := basicauth.Default(map[string]string{"myusername": "mypassword", "mySecondusername": "mySecondpassword"}) +// app.Get("/dashboard", authentication, func(ctx *iris.Context){}) +// +// for more configuration basicauth.New(basicauth.Config{...}) +// see _example + +type ( + encodedUser struct { + HeaderValue string + Username string + logged bool + expires time.Time + } + encodedUsers []encodedUser + + basicAuthMiddleware struct { + config Config + // these are filled from the config.Users map at the startup + auth encodedUsers + realmHeaderValue string + expireEnabled bool // if the config.Expires is a valid date, default disabled + } +) + +// + +// New takes one parameter, the Config returns a HandlerFunc +// use: iris.UseFunc(New(...)), iris.Get(...,New(...),...) +func New(c Config) iris.HandlerFunc { + b := &basicAuthMiddleware{config: DefaultConfig().MergeSingle(c)} + b.init() + return b.Serve +} + +// Default takes one parameter, the users returns a HandlerFunc +// use: iris.UseFunc(Default(...)), iris.Get(...,Default(...),...) +func Default(users map[string]string) iris.HandlerFunc { + c := DefaultConfig() + c.Users = users + return New(c) +} + +// + +// User returns the user from context key same as 'ctx.GetString("user")' but cannot be used by the developer, use the basicauth.Config.User func instead. +func (b *basicAuthMiddleware) User(ctx *iris.Context) string { + return b.config.User(ctx) +} + +func (b *basicAuthMiddleware) init() { + // pass the encoded users from the user's config's Users value + b.auth = make(encodedUsers, 0, len(b.config.Users)) + + for k, v := range b.config.Users { + fullUser := k + ":" + v + header := "Basic " + base64.StdEncoding.EncodeToString([]byte(fullUser)) + b.auth = append(b.auth, encodedUser{HeaderValue: header, Username: k, logged: false, expires: DefaultExpireTime}) + } + + // set the auth realm header's value + b.realmHeaderValue = "Basic realm=" + strconv.Quote(b.config.Realm) + + if b.config.Expires > 0 { + b.expireEnabled = true + } +} + +func (b *basicAuthMiddleware) findAuth(headerValue string) (auth *encodedUser, found bool) { + if len(headerValue) == 0 { + return + } + + for _, user := range b.auth { + if user.HeaderValue == headerValue { + auth = &user + found = true + break + } + } + + return +} + +func (b *basicAuthMiddleware) askForCredentials(ctx *iris.Context) { + ctx.SetHeader("WWW-Authenticate", b.realmHeaderValue) + ctx.SetStatusCode(iris.StatusUnauthorized) +} + +// Serve the actual middleware +func (b *basicAuthMiddleware) Serve(ctx *iris.Context) { + + if auth, found := b.findAuth(ctx.RequestHeader("Authorization")); !found { + b.askForCredentials(ctx) + // don't continue to the next handler + } else { + // all ok set the context's value in order to be getable from the next handler + ctx.Set(b.config.ContextKey, auth.Username) + if b.expireEnabled { + + if auth.logged == false { + auth.expires = time.Now().Add(b.config.Expires) + auth.logged = true + } + + if time.Now().After(auth.expires) { + b.askForCredentials(ctx) // ask for authentication again + return + } + + } + ctx.Next() // continue + } + +} diff --git a/middleware/basicauth/config.go b/middleware/basicauth/config.go new file mode 100644 index 00000000..5de97d02 --- /dev/null +++ b/middleware/basicauth/config.go @@ -0,0 +1,48 @@ +package basicauth + +import ( + "time" + + "github.com/imdario/mergo" + "gopkg.in/kataras/iris.v6" +) + +const ( + // DefaultBasicAuthRealm is "Authorization Required" + DefaultBasicAuthRealm = "Authorization Required" + // DefaultBasicAuthContextKey is the "auth" + // this key is used to do context.Set("user", theUsernameFromBasicAuth) + DefaultBasicAuthContextKey = "user" +) + +// DefaultExpireTime zero time +var DefaultExpireTime time.Time // 0001-01-01 00:00:00 +0000 UTC + +// Config the configs for the basicauth middleware +type Config struct { + // Users a map of login and the value (username/password) + Users map[string]string + // Realm http://tools.ietf.org/html/rfc2617#section-1.2. Default is "Authorization Required" + Realm string + // ContextKey the key for ctx.GetString(...). Default is 'user' + ContextKey string + // Expires expiration duration, default is 0 never expires + Expires time.Duration +} + +// DefaultConfig returns the default configs for the BasicAuth middleware +func DefaultConfig() Config { + return Config{make(map[string]string), DefaultBasicAuthRealm, DefaultBasicAuthContextKey, 0} +} + +// MergeSingle merges the default with the given config and returns the result +func (c Config) MergeSingle(cfg Config) (config Config) { + config = cfg + mergo.Merge(&config, c) + return +} + +// User returns the user from context key same as 'ctx.GetString("user")' but cannot be used by the developer, this is only here in order to understand how you can get the authenticated username +func (c Config) User(ctx *iris.Context) string { + return ctx.GetString(c.ContextKey) +} diff --git a/middleware/i18n/LICENSE b/middleware/i18n/LICENSE new file mode 100644 index 00000000..d0978209 --- /dev/null +++ b/middleware/i18n/LICENSE @@ -0,0 +1,167 @@ +Apache License +Version 2.0, January 2004 +http://www.apache.org/licenses/ + +TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + +1. Definitions. + +"License" shall mean the terms and conditions for use, reproduction, and +distribution as defined by Sections 1 through 9 of this document. + +"Licensor" shall mean the copyright owner or entity authorized by the copyright +owner that is granting the License. + +"Legal Entity" shall mean the union of the acting entity and all other entities +that control, are controlled by, or are under common control with that entity. +For the purposes of this definition, "control" means (i) the power, direct or +indirect, to cause the direction or management of such entity, whether by +contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the +outstanding shares, or (iii) beneficial ownership of such entity. + +"You" (or "Your") shall mean an individual or Legal Entity exercising +permissions granted by this License. + +"Source" form shall mean the preferred form for making modifications, including +but not limited to software source code, documentation source, and configuration +files. + +"Object" form shall mean any form resulting from mechanical transformation or +translation of a Source form, including but not limited to compiled object code, +generated documentation, and conversions to other media types. + +"Work" shall mean the work of authorship, whether in Source or Object form, made +available under the License, as indicated by a copyright notice that is included +in or attached to the work (an example is provided in the Appendix below). + +"Derivative Works" shall mean any work, whether in Source or Object form, that +is based on (or derived from) the Work and for which the editorial revisions, +annotations, elaborations, or other modifications represent, as a whole, an +original work of authorship. For the purposes of this License, Derivative Works +shall not include works that remain separable from, or merely link (or bind by +name) to the interfaces of, the Work and Derivative Works thereof. + +"Contribution" shall mean any work of authorship, including the original version +of the Work and any modifications or additions to that Work or Derivative Works +thereof, that is intentionally submitted to Licensor for inclusion in the Work +by the copyright owner or by an individual or Legal Entity authorized to submit +on behalf of the copyright owner. For the purposes of this definition, +"submitted" means any form of electronic, verbal, or written communication sent +to the Licensor or its representatives, including but not limited to +communication on electronic mailing lists, source code control systems, and +issue tracking systems that are managed by, or on behalf of, the Licensor for +the purpose of discussing and improving the Work, but excluding communication +that is conspicuously marked or otherwise designated in writing by the copyright +owner as "Not a Contribution." + +"Contributor" shall mean Licensor and any individual or Legal Entity on behalf +of whom a Contribution has been received by Licensor and subsequently +incorporated within the Work. + +2. Grant of Copyright License. + +Subject to the terms and conditions of this License, each Contributor hereby +grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, +irrevocable copyright license to reproduce, prepare Derivative Works of, +publicly display, publicly perform, sublicense, and distribute the Work and such +Derivative Works in Source or Object form. + +3. Grant of Patent License. + +Subject to the terms and conditions of this License, each Contributor hereby +grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, +irrevocable (except as stated in this section) patent license to make, have +made, use, offer to sell, sell, import, and otherwise transfer the Work, where +such license applies only to those patent claims licensable by such Contributor +that are necessarily infringed by their Contribution(s) alone or by combination +of their Contribution(s) with the Work to which such Contribution(s) was +submitted. If You institute patent litigation against any entity (including a +cross-claim or counterclaim in a lawsuit) alleging that the Work or a +Contribution incorporated within the Work constitutes direct or contributory +patent infringement, then any patent licenses granted to You under this License +for that Work shall terminate as of the date such litigation is filed. + +4. Redistribution. + +You may reproduce and distribute copies of the Work or Derivative Works thereof +in any medium, with or without modifications, and in Source or Object form, +provided that You meet the following conditions: + +You must give any other recipients of the Work or Derivative Works a copy of +this License; and +You must cause any modified files to carry prominent notices stating that You +changed the files; and +You must retain, in the Source form of any Derivative Works that You distribute, +all copyright, patent, trademark, and attribution notices from the Source form +of the Work, excluding those notices that do not pertain to any part of the +Derivative Works; and +If the Work includes a "NOTICE" text file as part of its distribution, then any +Derivative Works that You distribute must include a readable copy of the +attribution notices contained within such NOTICE file, excluding those notices +that do not pertain to any part of the Derivative Works, in at least one of the +following places: within a NOTICE text file distributed as part of the +Derivative Works; within the Source form or documentation, if provided along +with the Derivative Works; or, within a display generated by the Derivative +Works, if and wherever such third-party notices normally appear. The contents of +the NOTICE file are for informational purposes only and do not modify the +License. You may add Your own attribution notices within Derivative Works that +You distribute, alongside or as an addendum to the NOTICE text from the Work, +provided that such additional attribution notices cannot be construed as +modifying the License. +You may add Your own copyright statement to Your modifications and may provide +additional or different license terms and conditions for use, reproduction, or +distribution of Your modifications, or for any such Derivative Works as a whole, +provided Your use, reproduction, and distribution of the Work otherwise complies +with the conditions stated in this License. + +5. Submission of Contributions. + +Unless You explicitly state otherwise, any Contribution intentionally submitted +for inclusion in the Work by You to the Licensor shall be under the terms and +conditions of this License, without any additional terms or conditions. +Notwithstanding the above, nothing herein shall supersede or modify the terms of +any separate license agreement you may have executed with Licensor regarding +such Contributions. + +6. Trademarks. + +This License does not grant permission to use the trade names, trademarks, +service marks, or product names of the Licensor, except as required for +reasonable and customary use in describing the origin of the Work and +reproducing the content of the NOTICE file. + +7. Disclaimer of Warranty. + +Unless required by applicable law or agreed to in writing, Licensor provides the +Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, +including, without limitation, any warranties or conditions of TITLE, +NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are +solely responsible for determining the appropriateness of using or +redistributing the Work and assume any risks associated with Your exercise of +permissions under this License. + +8. Limitation of Liability. + +In no event and under no legal theory, whether in tort (including negligence), +contract, or otherwise, unless required by applicable law (such as deliberate +and grossly negligent acts) or agreed to in writing, shall any Contributor be +liable to You for damages, including any direct, indirect, special, incidental, +or consequential damages of any character arising as a result of this License or +out of the use or inability to use the Work (including but not limited to +damages for loss of goodwill, work stoppage, computer failure or malfunction, or +any and all other commercial damages or losses), even if such Contributor has +been advised of the possibility of such damages. + +9. Accepting Warranty or Additional Liability. + +While redistributing the Work or Derivative Works thereof, You may choose to +offer, and charge a fee for, acceptance of support, warranty, indemnity, or +other liability obligations and/or rights consistent with this License. However, +in accepting such obligations, You may act only on Your own behalf and on Your +sole responsibility, not on behalf of any other Contributor, and only if You +agree to indemnify, defend, and hold each Contributor harmless for any liability +incurred by, or claims asserted against, such Contributor by reason of your +accepting any such warranty or additional liability. + +END OF TERMS AND CONDITIONS diff --git a/middleware/i18n/README.md b/middleware/i18n/README.md new file mode 100644 index 00000000..457c9995 --- /dev/null +++ b/middleware/i18n/README.md @@ -0,0 +1,69 @@ +## Middleware information + +This folder contains a middleware for internationalization uses a third-party package named i81n. + +More can be found here: +[https://github.com/Unknwon/i18n](https://github.com/Unknwon/i18n) + +## Install + +```sh +$ go get -u github.com/iris-contrib/middleware/i18n +``` + +## Description + +Package i18n is for app Internationalization and Localization. + + +## How to use + +Create folder named 'locales' +``` +///Files: + +./locales/locale_en-US.ini +./locales/locale_el-US.ini +``` +Contents on locale_en-US: +``` +hi = hello, %s +``` +Contents on locale_el-GR: +``` +hi = ����, %s +``` + +```go + +package main + +import ( + "github.com/kataras/iris" + "github.com/iris-contrib/middleware/i18n" +) + +func main() { + + iris.UseFunc(i18n.New(i18n.Config{Default: "en-US", + Languages: map[string]string{ + "en-US": "./locales/locale_en-US.ini", + "el-GR": "./locales/locale_el-GR.ini", + "zh-CN": "./locales/locale_zh-CN.ini"}})) + // or iris.Use(i18n.I18nHandler(....)) + // or iris.Get("/",i18n.I18n(....), func (ctx *iris.Context){}) + + iris.Get("/", func(ctx *iris.Context) { + hi := ctx.GetFmt("translate")("hi", "maki") // hi is the key, 'maki' is the %s, the second parameter is optional + language := ctx.Get("language") // language is the language key, example 'en-US' + + ctx.Write("From the language %s translated output: %s", language, hi) + }) + + iris.Listen(":8080") + +} + +``` + +### [For a working example, click here](https://github.com/kataras/iris/tree/examples/middleware_internationalization_i18n) diff --git a/middleware/i18n/_example/locales/locale_el-GR.ini b/middleware/i18n/_example/locales/locale_el-GR.ini new file mode 100644 index 00000000..53258cff --- /dev/null +++ b/middleware/i18n/_example/locales/locale_el-GR.ini @@ -0,0 +1 @@ +hi = Γεια, %s \ No newline at end of file diff --git a/middleware/i18n/_example/locales/locale_en-US.ini b/middleware/i18n/_example/locales/locale_en-US.ini new file mode 100644 index 00000000..b2a39433 --- /dev/null +++ b/middleware/i18n/_example/locales/locale_en-US.ini @@ -0,0 +1 @@ +hi = hello, %s \ No newline at end of file diff --git a/middleware/i18n/_example/locales/locale_zh-CN.ini b/middleware/i18n/_example/locales/locale_zh-CN.ini new file mode 100644 index 00000000..0a7c91b0 --- /dev/null +++ b/middleware/i18n/_example/locales/locale_zh-CN.ini @@ -0,0 +1 @@ +hi = 您好,%s \ No newline at end of file diff --git a/middleware/i18n/_example/main.go b/middleware/i18n/_example/main.go new file mode 100644 index 00000000..425fc55b --- /dev/null +++ b/middleware/i18n/_example/main.go @@ -0,0 +1,50 @@ +package main + +import ( + "gopkg.in/kataras/iris.v6" + "gopkg.in/kataras/iris.v6/adaptors/httprouter" + "gopkg.in/kataras/iris.v6/middleware/i18n" +) + +func main() { + app := iris.New() + app.Adapt(httprouter.New()) // adapt a router first of all + + app.Use(i18n.New(i18n.Config{ + Default: "en-US", + URLParameter: "lang", + Languages: map[string]string{ + "en-US": "./locales/locale_en-US.ini", + "el-GR": "./locales/locale_el-GR.ini", + "zh-CN": "./locales/locale_zh-CN.ini"}})) + + app.Get("/", func(ctx *iris.Context) { + + // it tries to find the language by: + // ctx.Get("language") , that should be setted on other middleware before the i18n middleware* + // if that was empty then + // it tries to find from the URLParameter setted on the configuration + // if not found then + // it tries to find the language by the "lang" cookie + // if didn't found then it it set to the Default setted on the configuration + + // hi is the key, 'kataras' is the %s on the .ini file + // the second parameter is optional + + // hi := ctx.Translate("hi", "kataras") + // or: + hi := i18n.Translate(ctx, "hi", "kataras") + + language := ctx.Get(iris.TranslateLanguageContextKey) // language is the language key, example 'en-US' + + // The first succeed language found saved at the cookie with name ("language"), + // you can change that by changing the value of the: iris.TranslateLanguageContextKey + ctx.Writef("From the language %s translated output: %s", language, hi) + }) + + // go to http://localhost:8080/?lang=el-GR + // or http://localhost:8080 + // or http://localhost:8080/?lang=zh-CN + app.Listen(":8080") + +} diff --git a/middleware/i18n/config.go b/middleware/i18n/config.go new file mode 100644 index 00000000..becc89fc --- /dev/null +++ b/middleware/i18n/config.go @@ -0,0 +1,18 @@ +package i18n + +// Config the i18n options +type Config struct { + // Default set it if you want a default language + // + // Checked: Configuration state, not at runtime + Default string + // URLParameter is the name of the url parameter which the language can be indentified + // + // Checked: Serving state, runtime + URLParameter string + // Languages is a map[string]string which the key is the language i81n and the value is the file location + // + // Example of key is: 'en-US' + // Example of value is: './locales/en-US.ini' + Languages map[string]string +} diff --git a/middleware/i18n/i18n.go b/middleware/i18n/i18n.go new file mode 100644 index 00000000..26ea7223 --- /dev/null +++ b/middleware/i18n/i18n.go @@ -0,0 +1,101 @@ +package i18n + +import ( + "reflect" + "strings" + + "github.com/Unknwon/i18n" + "gopkg.in/kataras/iris.v6" +) + +type i18nMiddleware struct { + config Config +} + +// Serve serves the request, the actual middleware's job is here +func (i *i18nMiddleware) Serve(ctx *iris.Context) { + wasByCookie := false + + language := i.config.Default + if ctx.GetString(iris.TranslateLanguageContextKey) == "" { + // try to get by url parameter + language = ctx.URLParam(i.config.URLParameter) + + if language == "" { + // then try to take the lang field from the cookie + language = ctx.GetCookie(iris.TranslateLanguageContextKey) + + if len(language) > 0 { + wasByCookie = true + } else { + // try to get by the request headers(?) + if langHeader := ctx.RequestHeader("Accept-Language"); i18n.IsExist(langHeader) { + language = langHeader + } + } + } + // if it was not taken by the cookie, then set the cookie in order to have it + if !wasByCookie { + ctx.SetCookieKV(iris.TranslateLanguageContextKey, language) + } + if language == "" { + language = i.config.Default + } + ctx.Set(iris.TranslateLanguageContextKey, language) + } + locale := i18n.Locale{Lang: language} + + ctx.Set(iris.TranslateFunctionContextKey, locale.Tr) + ctx.Next() +} + +// Translate returns the translated word from a context +// the second parameter is the key of the world or line inside the .ini file +// the third parameter is the '%s' of the world or line inside the .ini file +func Translate(ctx *iris.Context, format string, args ...interface{}) string { + return ctx.Translate(format, args...) +} + +// New returns a new i18n middleware +func New(c Config) iris.HandlerFunc { + if len(c.Languages) == 0 { + panic("You cannot use this middleware without set the Languages option, please try again and read the _example.") + } + i := &i18nMiddleware{config: c} + firstlanguage := "" + //load the files + for k, v := range c.Languages { + if !strings.HasSuffix(v, ".ini") { + v += ".ini" + } + err := i18n.SetMessage(k, v) + if err != nil && err != i18n.ErrLangAlreadyExist { + panic("Iris i18n Middleware: Failed to set locale file" + k + " Error:" + err.Error()) + } + if firstlanguage == "" { + firstlanguage = k + } + } + // if not default language setted then set to the first of the i.options.Languages + if c.Default == "" { + c.Default = firstlanguage + } + + i18n.SetDefaultLang(i.config.Default) + return i.Serve +} + +// TranslatedMap returns translated map[string]interface{} from i18n structure +func TranslatedMap(sourceInterface interface{}, ctx *iris.Context) map[string]interface{} { + iType := reflect.TypeOf(sourceInterface).Elem() + result := make(map[string]interface{}) + + for i := 0; i < iType.NumField(); i++ { + fieldName := reflect.TypeOf(sourceInterface).Elem().Field(i).Name + fieldValue := reflect.ValueOf(sourceInterface).Elem().Field(i).String() + + result[fieldName] = Translate(ctx, fieldValue) + } + + return result +} diff --git a/middleware/logger/_example/main.go b/middleware/logger/_example/main.go new file mode 100644 index 00000000..0554be19 --- /dev/null +++ b/middleware/logger/_example/main.go @@ -0,0 +1,53 @@ +package main + +import ( + "gopkg.in/kataras/iris.v6" + "gopkg.in/kataras/iris.v6/adaptors/httprouter" + "gopkg.in/kataras/iris.v6/middleware/logger" +) + +func main() { + app := iris.New() + + app.Adapt(iris.DevLogger()) // it just enables the print of the iris.DevMode logs. Enable it to view the middleware's messages. + app.Adapt(httprouter.New()) + + customLogger := logger.New(logger.Config{ + // Status displays status code + Status: true, + // IP displays request's remote address + IP: true, + // Method displays the http method + Method: true, + // Path displays the request path + Path: true, + }) + + app.Use(customLogger) + + app.Get("/", func(ctx *iris.Context) { + ctx.Writef("hello") + }) + + app.Get("/1", func(ctx *iris.Context) { + ctx.Writef("hello") + }) + + app.Get("/2", func(ctx *iris.Context) { + ctx.Writef("hello") + }) + + // log http errors + errorLogger := logger.New() + + app.OnError(iris.StatusNotFound, func(ctx *iris.Context) { + errorLogger.Serve(ctx) + ctx.Writef("My Custom 404 error page ") + }) + + // http://localhost:8080 + // http://localhost:8080/1 + // http://localhost:8080/2 + app.Listen(":8080") + +} diff --git a/middleware/logger/config.go b/middleware/logger/config.go new file mode 100644 index 00000000..ba2d56d1 --- /dev/null +++ b/middleware/logger/config.go @@ -0,0 +1,21 @@ +package logger + +// Config are the options of the logger middlweare +// contains 4 bools +// Status, IP, Method, Path +// if set to true then these will print +type Config struct { + // Status displays status code (bool) + Status bool + // IP displays request's remote address (bool) + IP bool + // Method displays the http method (bool) + Method bool + // Path displays the request path (bool) + Path bool +} + +// DefaultConfig returns an options which all properties are true except EnableColors +func DefaultConfig() Config { + return Config{true, true, true, true} +} diff --git a/middleware/logger/logger.go b/middleware/logger/logger.go new file mode 100644 index 00000000..0d9946b1 --- /dev/null +++ b/middleware/logger/logger.go @@ -0,0 +1,62 @@ +package logger + +import ( + "fmt" + "strconv" + "time" + + "gopkg.in/kataras/iris.v6" +) + +type loggerMiddleware struct { + config Config +} + +// Serve serves the middleware +func (l *loggerMiddleware) Serve(ctx *iris.Context) { + //all except latency to string + var date, status, ip, method, path string + var latency time.Duration + var startTime, endTime time.Time + path = ctx.Path() + method = ctx.Method() + + startTime = time.Now() + + ctx.Next() + //no time.Since in order to format it well after + endTime = time.Now() + date = endTime.Format("01/02 - 15:04:05") + latency = endTime.Sub(startTime) + + if l.config.Status { + status = strconv.Itoa(ctx.ResponseWriter.StatusCode()) + } + + if l.config.IP { + ip = ctx.RemoteAddr() + } + + if !l.config.Method { + method = "" + } + + if !l.config.Path { + path = "" + } + + //finally print the logs + ctx.Log(iris.DevMode, fmt.Sprintf("%s %v %4v %s %s %s \n", date, status, latency, ip, method, path)) +} + +// New returns the logger middleware +// receives optional configs(logger.Config) +func New(cfg ...Config) iris.HandlerFunc { + c := DefaultConfig() + if len(cfg) > 0 { + c = cfg[0] + } + l := &loggerMiddleware{config: c} + + return l.Serve +} diff --git a/middleware/recover/_example/main.go b/middleware/recover/_example/main.go new file mode 100644 index 00000000..91703f6a --- /dev/null +++ b/middleware/recover/_example/main.go @@ -0,0 +1,31 @@ +package main + +import ( + "gopkg.in/kataras/iris.v6" + "gopkg.in/kataras/iris.v6/adaptors/httprouter" + "gopkg.in/kataras/iris.v6/middleware/recover" +) + +func main() { + app := iris.New() + app.Adapt(httprouter.New()) + + app.Adapt(iris.DevLogger()) // fast way to enable non-fatal messages to be printed to the user + // (yes in iris even recover's errors are not fatal because it's restarting, + // ProdMode messages are only for things that Iris cannot continue at all, + // these are logged by-default but you can change that behavior too by passing a different LoggerPolicy to the .Adapt) + app.Use(recover.New()) // it's io.Writer is the same as app.Config.LoggerOut + + i := 0 + // let's simmilate a panic every next request + app.Get("/", func(ctx *iris.Context) { + i++ + if i%2 == 0 { + panic("a panic here") + } + ctx.Writef("Hello, refresh one time more to get panic!") + }) + + // http://localhost:8080 + app.Listen(":8080") +} diff --git a/middleware/recover/recover.go b/middleware/recover/recover.go new file mode 100644 index 00000000..ccde3b51 --- /dev/null +++ b/middleware/recover/recover.go @@ -0,0 +1,58 @@ +package recover + +import ( + "fmt" + "runtime" + "strconv" + + "gopkg.in/kataras/iris.v6" +) + +func getRequestLogs(ctx *iris.Context) string { + var status, ip, method, path string + status = strconv.Itoa(ctx.ResponseWriter.StatusCode()) + path = ctx.Path() + method = ctx.Method() + ip = ctx.RemoteAddr() + // the date should be logged by iris' Logger, so we skip them + return fmt.Sprintf("%v %s %s %s", status, path, method, ip) +} + +// New returns a new recover middleware +// it logs to the LoggerOut iris' configuration field if its IsDeveloper configuration field is enabled. +// otherwise it just continues to serve +func New() iris.HandlerFunc { + return func(ctx *iris.Context) { + defer func() { + if err := recover(); err != nil { + if ctx.IsStopped() { + return + } + + var stacktrace string + for i := 1; ; i++ { + _, f, l, got := runtime.Caller(i) + if !got { + break + + } + + stacktrace += fmt.Sprintf("%s:%d\n", f, l) + } + + // when stack finishes + logMessage := fmt.Sprintf("Recovered from a route's Handler('%s')\n", ctx.GetHandlerName()) + logMessage += fmt.Sprintf("At Request: %s\n", getRequestLogs(ctx)) + logMessage += fmt.Sprintf("Trace: %s\n", err) + logMessage += fmt.Sprintf("\n%s\n", stacktrace) + ctx.Log(iris.DevMode, logMessage) + + ctx.StopExecution() + ctx.EmitError(iris.StatusInternalServerError) + + } + }() + + ctx.Next() + } +} diff --git a/plugin.go b/plugin.go deleted file mode 100644 index 767227ce..00000000 --- a/plugin.go +++ /dev/null @@ -1,615 +0,0 @@ -package iris - -///TODO: Be ready for go v1.8 in order to accomplish my first idea. - -import ( - "log" - "sync" - - "github.com/kataras/go-errors" - "github.com/kataras/go-fs" -) - -var ( - // errPluginAlreadyExists returns an error with message: 'Cannot activate the same plugin again, plugin '+plugin name[+plugin description]' is already exists' - errPluginAlreadyExists = errors.New("Cannot use the same plugin again, '%s[%s]' is already exists") - // errPluginActivate returns an error with message: 'While trying to activate plugin '+plugin name'. Trace: +specific error' - errPluginActivate = errors.New("While trying to activate plugin '%s'. Trace: %s") - // errPluginRemoveNoPlugins returns an error with message: 'No plugins are registered yet, you cannot remove a plugin from an empty list!' - errPluginRemoveNoPlugins = errors.New("No plugins are registered yet, you cannot remove a plugin from an empty list!") - // errPluginRemoveEmptyName returns an error with message: 'Plugin with an empty name cannot be removed' - errPluginRemoveEmptyName = errors.New("Plugin with an empty name cannot be removed") - // errPluginRemoveNotFound returns an error with message: 'Cannot remove a plugin which doesn't exists' - errPluginRemoveNotFound = errors.New("Cannot remove a plugin which doesn't exists") -) - -type ( - // Plugin just an empty base for plugins - // A Plugin can be added with: .Add(PreListenFunc(func(*Framework))) and so on... or - // .Add(myPlugin{},myPlugin2{}) which myPlugin is a struct with any of the methods below or - // .PostListen(func(*Framework)) and so on... - Plugin interface { - } - - // pluginGetName implements the GetName() string method - pluginGetName interface { - // GetName has to returns the name of the plugin, a name is unique - // name has to be not dependent from other methods of the plugin, - // because it is being called even before the Activate - GetName() string - } - - // pluginGetDescription implements the GetDescription() string method - pluginGetDescription interface { - // GetDescription has to returns the description of what the plugins is used for - GetDescription() string - } - - // pluginActivate implements the Activate(pluginContainer) error method - pluginActivate interface { - // Activate called BEFORE the plugin being added to the plugins list, - // if Activate returns none nil error then the plugin is not being added to the list - // it is being called only one time - // - // PluginContainer parameter used to add other plugins if that's necessary by the plugin - Activate(PluginContainer) error - } - // pluginPreLookup implements the PreRoute(Route) method - pluginPreLookup interface { - // PreLookup called before register a route - PreLookup(Route) - } - // PreLookupFunc implements the simple function listener for the PreLookup(Route) - PreLookupFunc func(Route) - // pluginPreBuild implements the PreBuild(*Framework) method - pluginPreBuild interface { - // PreBuild it's being called once time, BEFORE the Server is started and before PreListen - // is used to do work before all other things are ready - // use this event if you want to add routes to your iris station - // or make any changes to the iris main configuration - // receiver is the station - PreBuild(*Framework) - } - // PreBuildFunc implements the simple function listener for the PreBuild(*Framework) - PreBuildFunc func(*Framework) - // pluginPreListen implements the PreListen(*Framework) method - pluginPreListen interface { - // PreListen it's being called only one time, BEFORE the Server is started (if .Listen called) - // is used to do work at the time all other things are ready to go - // receiver is the station - PreListen(*Framework) - } - // PreListenFunc implements the simple function listener for the PreListen(*Framework) - PreListenFunc func(*Framework) - // pluginPostListen implements the PostListen(*Framework) method - pluginPostListen interface { - // PostListen it's being called only one time, AFTER the Server is started (if .Listen called) - // parameter is the station - PostListen(*Framework) - } - // PostListenFunc implements the simple function listener for the PostListen(*Framework) - PostListenFunc func(*Framework) - - // pluginPostInterrupt implements the PostInterrupt(*Framework) method - pluginPostInterrupt interface { - // PostInterrupt it's being called only one time, when os.Interrupt system event catched - // graceful shutdown can be done here - // - // Read more here: https://github.com/kataras/iris/blob/master/HISTORY.md#608---609 - // Example: https://github.com/iris-contrib/examples/tree/master/os_interrupt - PostInterrupt(*Framework) - } - // PostInterruptFunc implements the simple function listener for the PostInterrupt(*Framework) - // - // Read more here: https://github.com/kataras/iris/blob/master/HISTORY.md#608---609 - // Example: https://github.com/iris-contrib/examples/tree/master/os_interrupt - PostInterruptFunc func(*Framework) - - // pluginPreClose implements the PreClose(*Framework) method - pluginPreClose interface { - // PreClose it's being called only one time, BEFORE the Iris .Close method - // any plugin cleanup/clear memory happens here - // - // The plugin is deactivated after this state - PreClose(*Framework) - } - // PreCloseFunc implements the simple function listener for the PreClose(*Framework) - PreCloseFunc func(*Framework) - - // pluginPreDownload It's for the future, not being used, I need to create - // and return an ActivatedPlugin type which will have it's methods, and pass it on .Activate - // but now we return the whole pluginContainer, which I can't determinate which plugin tries to - // download something, so we will leave it here for the future. - pluginPreDownload interface { - // PreDownload it's being called every time a plugin tries to download something - // - // first parameter is the plugin - // second parameter is the download url - // must return a boolean, if false then the plugin is not permmited to download this file - PreDownload(plugin Plugin, downloadURL string) // bool - } - - // PreDownloadFunc implements the simple function listener for the PreDownload(plugin,string) - PreDownloadFunc func(Plugin, string) - - // PluginContainer is the interface which the pluginContainer should implements - PluginContainer interface { - Add(...Plugin) error - Remove(string) error - Len() int - GetName(Plugin) string - GetDescription(Plugin) string - GetByName(string) Plugin - Printf(string, ...interface{}) - Fired(string) int - PreLookup(PreLookupFunc) - DoPreLookup(Route) - PreLookupFired() bool - PreBuild(PreBuildFunc) - DoPreBuild(*Framework) - PreBuildFired() bool - PreListen(PreListenFunc) - DoPreListen(*Framework) - DoPreListenParallel(*Framework) - PreListenFired() bool - PostListen(PostListenFunc) - DoPostListen(*Framework) - PostListenFired() bool - PostInterrupt(PostInterruptFunc) - DoPostInterrupt(*Framework) - PostInterruptFired() bool - PreClose(PreCloseFunc) - DoPreClose(*Framework) - PreCloseFired() bool - PreDownload(PreDownloadFunc) - DoPreDownload(Plugin, string) - PreDownloadFired() bool - // - GetAll() []Plugin - // GetDownloader is the only one module that is used and fire listeners at the same time in this file - GetDownloader() PluginDownloadManager - } //Note: custom event callbacks, never used internaly by Iris, but if you need them use this: github.com/kataras/go-events - // PluginDownloadManager is the interface which the DownloadManager should implements - PluginDownloadManager interface { - DirectoryExists(string) bool - DownloadZip(string, string) (string, error) - Unzip(string, string) (string, error) - Remove(string) error - // install is just the flow of: downloadZip -> unzip -> removeFile(zippedFile) - // accepts 2 parameters - // - // first parameter is the remote url file zip - // second parameter is the target directory - // returns a string(installedDirectory) and an error - // - // (string) installedDirectory is the directory which the zip file had, this is the real installation path, you don't need to know what it's because these things maybe change to the future let's keep it to return the correct path. - // the installedDirectory is not empty when the installation is succed, the targetDirectory is not already exists and no error happens - // the installedDirectory is empty when the installation is already done by previous time or an error happens - Install(remoteFileZip string, targetDirectory string) (string, error) - } - - // pluginDownloadManager is just a struch which exports the util's downloadZip, directoryExists, unzip methods, used by the plugins via the pluginContainer - pluginDownloadManager struct { - } -) - -// convert the functions to plugin - -// PreLookup called before register a route -func (fn PreLookupFunc) PreLookup(r Route) { - fn(r) -} - -// PreBuild it's being called once time, BEFORE the Server is started and before PreListen -// is used to do work before all other things are ready -// use this event if you want to add routes to your iris station -// or make any changes to the iris main configuration -// receiver is the station -func (fn PreBuildFunc) PreBuild(station *Framework) { - fn(station) -} - -// PreListen it's being called only one time, BEFORE the Server is started (if .Listen called) -// is used to do work at the time all other things are ready to go -// parameter is the station -func (fn PreListenFunc) PreListen(station *Framework) { - fn(station) -} - -// PostListen it's being called only one time, AFTER the Server is started (if .Listen called) -// parameter is the station -func (fn PostListenFunc) PostListen(station *Framework) { - fn(station) -} - -// PostInterrupt it's being called only one time, when os.Interrupt system event catched -// graceful shutdown can be done here -// -// Read more here: https://github.com/kataras/iris/blob/master/HISTORY.md#608---609 -// Example: https://github.com/iris-contrib/examples/tree/master/os_interrupt -func (fn PostInterruptFunc) PostInterrupt(station *Framework) { - fn(station) -} - -// PreClose it's being called only one time, BEFORE the Iris .Close method -// any plugin cleanup/clear memory happens here -// -// The plugin is deactivated after this state -func (fn PreCloseFunc) PreClose(station *Framework) { - fn(station) -} - -// PreDownload it's being called every time a plugin tries to download something -// -// first parameter is the plugin -// second parameter is the download url -// must return a boolean, if false then the plugin is not permmited to download this file -func (fn PreDownloadFunc) PreDownload(pl Plugin, downloadURL string) { - fn(pl, downloadURL) -} - -// - -var _ PluginDownloadManager = &pluginDownloadManager{} -var _ PluginContainer = &pluginContainer{} - -// DirectoryExists returns true if a given local directory exists -func (d *pluginDownloadManager) DirectoryExists(dir string) bool { - return fs.DirectoryExists(dir) -} - -// DownloadZip downlodas a zip to the given local path location -func (d *pluginDownloadManager) DownloadZip(zipURL string, targetDir string) (string, error) { - return fs.DownloadZip(zipURL, targetDir, true) -} - -// Unzip unzips a zip to the given local path location -func (d *pluginDownloadManager) Unzip(archive string, target string) (string, error) { - return fs.DownloadZip(archive, target, true) -} - -// Remove deletes/removes/rm a file -func (d *pluginDownloadManager) Remove(filePath string) error { - return fs.RemoveFile(filePath) -} - -// Install is just the flow of the: DownloadZip->Unzip->Remove the zip -func (d *pluginDownloadManager) Install(remoteFileZip string, targetDirectory string) (string, error) { - return fs.Install(remoteFileZip, targetDirectory, true) -} - -// pluginContainer is the base container of all Iris, registered plugins -type pluginContainer struct { - activatedPlugins []Plugin - customEvents map[string][]func() - downloader *pluginDownloadManager - logger *log.Logger - mu *sync.Mutex - fired map[string]int // event/plugin type name and the times fired -} - -// newPluginContainer receives a logger and returns a new PluginContainer -func newPluginContainer(l *log.Logger) PluginContainer { - return &pluginContainer{logger: l, fired: make(map[string]int, 0), mu: &sync.Mutex{}} -} - -// Add activates the plugins and if succeed then adds it to the activated plugins list -func (p *pluginContainer) Add(plugins ...Plugin) error { - for _, plugin := range plugins { - - if p.activatedPlugins == nil { - p.activatedPlugins = make([]Plugin, 0) - } - - // Check if it's a plugin first, has Activate GetName - - // Check if the plugin already exists - pName := p.GetName(plugin) - if pName != "" && p.GetByName(pName) != nil { - return errPluginAlreadyExists.Format(pName, p.GetDescription(plugin)) - } - // Activate the plugin, if no error then add it to the plugins - if pluginObj, ok := plugin.(pluginActivate); ok { - - tempPluginContainer := *p - err := pluginObj.Activate(&tempPluginContainer) - if err != nil { - return errPluginActivate.Format(pName, err.Error()) - } - - tempActivatedPluginsLen := len(tempPluginContainer.activatedPlugins) - if tempActivatedPluginsLen != len(p.activatedPlugins)+tempActivatedPluginsLen+1 { // see test: plugin_test.go TestPluginActivate && TestPluginActivationError - p.activatedPlugins = tempPluginContainer.activatedPlugins - } - - } - - // All ok, add it to the plugins list - p.activatedPlugins = append(p.activatedPlugins, plugin) - } - return nil -} - -// Remove removes a plugin by it's name, if pluginName is empty "" or no plugin found with this name, then nothing is removed and a specific error is returned. -// This doesn't calls the PreClose method -func (p *pluginContainer) Remove(pluginName string) error { - if p.activatedPlugins == nil { - return errPluginRemoveNoPlugins - } - - if pluginName == "" { - //return error: cannot delete an unamed plugin - return errPluginRemoveEmptyName - } - - indexToRemove := -1 - for i := range p.activatedPlugins { - if p.GetName(p.activatedPlugins[i]) == pluginName { // Note: if GetName is not implemented then the name is "" which is != with the plugiName, we checked this before. - indexToRemove = i - } - } - if indexToRemove == -1 { //if index stills -1 then no plugin was found with this name, just return an error. it is not a critical error. - return errPluginRemoveNotFound - } - - p.activatedPlugins = append(p.activatedPlugins[:indexToRemove], p.activatedPlugins[indexToRemove+1:]...) - - return nil -} - -// Len returns the number of activate plugins -func (p *pluginContainer) Len() int { - return len(p.activatedPlugins) -} - -// GetName returns the name of a plugin, if no GetName() implemented it returns an empty string "" -func (p *pluginContainer) GetName(plugin Plugin) string { - if pluginObj, ok := plugin.(pluginGetName); ok { - return pluginObj.GetName() - } - return "" -} - -// GetDescription returns the name of a plugin, if no GetDescription() implemented it returns an empty string "" -func (p *pluginContainer) GetDescription(plugin Plugin) string { - if pluginObj, ok := plugin.(pluginGetDescription); ok { - return pluginObj.GetDescription() - } - return "" -} - -// GetByName returns a plugin instance by it's name -func (p *pluginContainer) GetByName(pluginName string) Plugin { - if p.activatedPlugins == nil { - return nil - } - - for i := range p.activatedPlugins { - if pluginObj, ok := p.activatedPlugins[i].(pluginGetName); ok { - if pluginObj.GetName() == pluginName { - return pluginObj - } - } - } - - return nil -} - -// GetAll returns all activated plugins -func (p *pluginContainer) GetAll() []Plugin { - return p.activatedPlugins -} - -// GetDownloader returns the download manager -func (p *pluginContainer) GetDownloader() PluginDownloadManager { - // create it if and only if it used somewhere - if p.downloader == nil { - p.downloader = &pluginDownloadManager{} - } - return p.downloader -} - -// Printf sends plain text to any registered logger (future), some plugins maybe want use this method -// maybe at the future I change it, instead of sync even-driven to async channels... -func (p *pluginContainer) Printf(format string, a ...interface{}) { - if p.logger != nil { - p.logger.Printf(format, a...) //for now just this. - } - -} - -// fire adds a fired event on the (statically type named) map and returns the new times -func (p *pluginContainer) fire(name string) int { - p.mu.Lock() - var times int - // maybe unnecessary but for clarity reasons - if t, found := p.fired[name]; found { - times = t - } - times++ - p.fired[name] = times - p.mu.Unlock() - return times -} - -// Fired receives an event name/plugin type and returns the times which this event is fired and how many plugins are fired this event, -// if zero then it's not fired at all -func (p *pluginContainer) Fired(name string) (times int) { - if t, found := p.fired[name]; found { - times = t - } - return -} - -// PreLookup adds a PreLookup plugin-function to the plugin flow container -func (p *pluginContainer) PreLookup(fn PreLookupFunc) { - p.Add(fn) -} - -// DoPreLookup raise all plugins which has the PreLookup method -func (p *pluginContainer) DoPreLookup(r Route) { - for i := range p.activatedPlugins { - // check if this method exists on our plugin obj, these are optionaly and call it - if pluginObj, ok := p.activatedPlugins[i].(pluginPreLookup); ok { - // fire will add times to the number of events fired this event - p.fire("prelookup") - pluginObj.PreLookup(r) - } - } -} - -// PreLookupFired returns true if PreLookup event/ plugin type is fired at least one time -func (p *pluginContainer) PreLookupFired() bool { - return p.Fired("prelookup") > 0 -} - -// PreBuild adds a PreBuild plugin-function to the plugin flow container -func (p *pluginContainer) PreBuild(fn PreBuildFunc) { - p.Add(fn) -} - -// DoPreBuild raise all plugins that have the PreBuild method -func (p *pluginContainer) DoPreBuild(station *Framework) { - for i := range p.activatedPlugins { - // check if this method exists on our plugin obj, these are optionaly and call it - if pluginObj, ok := p.activatedPlugins[i].(pluginPreBuild); ok { - pluginObj.PreBuild(station) - p.fire("prebuild") - } - } -} - -// PreBuildFired returns true if PreBuild event/ plugin type is fired at least one time -func (p *pluginContainer) PreBuildFired() bool { - return p.Fired("prebuild") > 0 -} - -// PreListen adds a PreListen plugin-function to the plugin flow container -func (p *pluginContainer) PreListen(fn PreListenFunc) { - p.Add(fn) -} - -// DoPreListen raise all plugins which has the PreListen method -func (p *pluginContainer) DoPreListen(station *Framework) { - for i := range p.activatedPlugins { - // check if this method exists on our plugin obj, these are optionaly and call it - if pluginObj, ok := p.activatedPlugins[i].(pluginPreListen); ok { - pluginObj.PreListen(station) - p.fire("prelisten") - } - } -} - -// DoPreListenParallel raise all PreListen plugins 'at the same time' -func (p *pluginContainer) DoPreListenParallel(station *Framework) { - var wg sync.WaitGroup - - for _, plugin := range p.activatedPlugins { - wg.Add(1) - // check if this method exists on our plugin obj, these are optionaly and call it - go func(plugin Plugin) { - if pluginObj, ok := plugin.(pluginPreListen); ok { - pluginObj.PreListen(station) - p.fire("prelisten") - } - - wg.Done() - - }(plugin) - } - - wg.Wait() - -} - -// PreListenFired returns true if PreListen or PreListenParallel event/ plugin type is fired at least one time -func (p *pluginContainer) PreListenFired() bool { - return p.Fired("prelisten") > 0 -} - -// PostListen adds a PostListen plugin-function to the plugin flow container -func (p *pluginContainer) PostListen(fn PostListenFunc) { - p.Add(fn) -} - -// DoPostListen raise all plugins which has the DoPostListen method -func (p *pluginContainer) DoPostListen(station *Framework) { - for i := range p.activatedPlugins { - // check if this method exists on our plugin obj, these are optionaly and call it - if pluginObj, ok := p.activatedPlugins[i].(pluginPostListen); ok { - pluginObj.PostListen(station) - p.fire("postlisten") - } - } -} - -// PostListenFired returns true if PostListen event/ plugin type is fired at least one time -func (p *pluginContainer) PostListenFired() bool { - return p.Fired("postlisten") > 0 -} - -// PostInterrupt adds a PostInterrupt plugin-function to the plugin flow container -// -// Read more here: https://github.com/kataras/iris/blob/master/HISTORY.md#608---609 -// Example: https://github.com/iris-contrib/examples/tree/master/os_interrupt -func (p *pluginContainer) PostInterrupt(fn PostInterruptFunc) { - p.Add(fn) -} - -// DoPostInterrupt raise all plugins which has the PostInterrupt method -func (p *pluginContainer) DoPostInterrupt(station *Framework) { - for i := range p.activatedPlugins { - // check if this method exists on our plugin obj, these are optionaly and call it - if pluginObj, ok := p.activatedPlugins[i].(pluginPostInterrupt); ok { - pluginObj.PostInterrupt(station) - p.fire("postinterrupt") - } - } -} - -// PostInterruptFired returns true if PostInterrupt event/ plugin type is fired at least one time -func (p *pluginContainer) PostInterruptFired() bool { - return p.Fired("postinterrupt") > 0 -} - -// PreClose adds a PreClose plugin-function to the plugin flow container -func (p *pluginContainer) PreClose(fn PreCloseFunc) { - p.Add(fn) -} - -// DoPreClose raise all plugins which has the DoPreClose method -func (p *pluginContainer) DoPreClose(station *Framework) { - for i := range p.activatedPlugins { - // check if this method exists on our plugin obj, these are optionaly and call it - if pluginObj, ok := p.activatedPlugins[i].(pluginPreClose); ok { - pluginObj.PreClose(station) - p.fire("preclose") - } - } -} - -// PreCloseFired returns true if PreCLose event/ plugin type is fired at least one time -func (p *pluginContainer) PreCloseFired() bool { - return p.Fired("preclose") > 0 -} - -// PreDownload adds a PreDownload plugin-function to the plugin flow container -func (p *pluginContainer) PreDownload(fn PreDownloadFunc) { - p.Add(fn) -} - -// DoPreDownload raise all plugins which has the DoPreDownload method -func (p *pluginContainer) DoPreDownload(pluginTryToDownload Plugin, downloadURL string) { - for i := range p.activatedPlugins { - // check if this method exists on our plugin obj, these are optionaly and call it - if pluginObj, ok := p.activatedPlugins[i].(pluginPreDownload); ok { - pluginObj.PreDownload(pluginTryToDownload, downloadURL) - p.fire("predownload") - } - } -} - -// PreDownloadFired returns true if PreDownload event/ plugin type is fired at least one time -func (p *pluginContainer) PreDownloadFired() bool { - return p.Fired("predownload") > 0 -} diff --git a/plugin_test.go b/plugin_test.go deleted file mode 100644 index fc14f2ec..00000000 --- a/plugin_test.go +++ /dev/null @@ -1,205 +0,0 @@ -// Black-box Testing -package iris_test - -import ( - "fmt" - "github.com/kataras/iris" - "testing" -) - -const ( - testPluginExDescription = "Description for My test plugin" - testPluginExName = "My test plugin" -) - -type testPluginEx struct { - named, activated, descriptioned bool - prelistenran, postlistenran, precloseran bool -} - -func (t *testPluginEx) GetName() string { - fmt.Println("GetName Struct") - t.named = true - return testPluginExName -} - -func (t *testPluginEx) GetDescription() string { - fmt.Println("GetDescription Struct") - t.descriptioned = true - return testPluginExDescription -} - -func (t *testPluginEx) Activate(p iris.PluginContainer) error { - fmt.Println("Activate Struct") - t.activated = true - return nil -} - -func (t *testPluginEx) PreListen(*iris.Framework) { - fmt.Println("PreListen Struct") - t.prelistenran = true -} - -func (t *testPluginEx) PostListen(*iris.Framework) { - fmt.Println("PostListen Struct") - t.postlistenran = true -} - -func (t *testPluginEx) PreClose(*iris.Framework) { - fmt.Println("PreClose Struct") - t.precloseran = true -} - -func ExamplePlugins_Add() { - iris.ResetDefault() - iris.Default.Set(iris.OptionDisableBanner(true)) - iris.Plugins.Add(iris.PreListenFunc(func(*iris.Framework) { - fmt.Println("PreListen Func") - })) - - iris.Plugins.Add(iris.PostListenFunc(func(*iris.Framework) { - fmt.Println("PostListen Func") - })) - - iris.Plugins.Add(iris.PreCloseFunc(func(*iris.Framework) { - fmt.Println("PreClose Func") - })) - - myplugin := &testPluginEx{} - iris.Plugins.Add(myplugin) - desc := iris.Plugins.GetDescription(myplugin) - fmt.Println(desc) - - // travis have problems if I do that using - // Listen(":8080") and Close() - iris.Plugins.DoPreListen(iris.Default) - iris.Plugins.DoPostListen(iris.Default) - iris.Plugins.DoPreClose(iris.Default) - - // Output: - // GetName Struct - // Activate Struct - // GetDescription Struct - // Description for My test plugin - // PreListen Func - // PreListen Struct - // PostListen Func - // PostListen Struct - // PreClose Func - // PreClose Struct -} - -// if a plugin has GetName, then it should be registered only one time, the name exists for that reason, it's like unique ID -func TestPluginDublicateName(t *testing.T) { - iris.ResetDefault() - var plugins = iris.Default.Plugins - firstNamedPlugin := &testPluginEx{} - sameNamedPlugin := &testPluginEx{} - // err := plugins.Add(firstNamedPlugin, sameNamedPlugin) or - err := plugins.Add(firstNamedPlugin) - if err != nil { - t.Fatalf("Unexpected error when adding a plugin with name: %s", testPluginExName) - } - err = plugins.Add(sameNamedPlugin) - if err == nil { - t.Fatalf("Expected an error because of dublicate named plugin!") - } - if plugins.Len() != 1 { - t.Fatalf("Expected: %d activated plugin but we got: %d", 1, plugins.Len()) - } -} - -type testPluginActivationType struct { - shouldError bool -} - -func (t testPluginActivationType) Activate(p iris.PluginContainer) error { - p.Add(&testPluginEx{}) - if t.shouldError { - return fmt.Errorf("An error happens, this plugin and the added plugins by this plugin should not be registered") - } - return nil -} - -// a plugin may contain children plugins too, but, -// if an error happened then all of them are not activated/added to the plugin container -func AddPluginTo(t *testing.T, plugins iris.PluginContainer, plugin iris.Plugin, expectingCount int) { - plugins.Add(plugin) - if plugins.Len() != expectingCount { // 2 because it registers a second plugin also - t.Fatalf("Expected activated plugins to be: %d but we got: %d", expectingCount, plugins.Len()) - } -} - -// if any error returned from the Activate plugin's method, -// then this plugin and the plugins it registers should not be registered at all -func TestPluginActivate(t *testing.T) { - iris.ResetDefault() - plugins := iris.Plugins - - myValidPluginWithChild := testPluginActivationType{shouldError: false} - AddPluginTo(t, plugins, myValidPluginWithChild, 2) // 2 because its children registered also (its parent is not throwing an error) - - myInvalidPlugin := testPluginActivationType{shouldError: true} - // should stay 2, even if we tried to add a new one, - // because it cancels the registration of its children too (shouldError = true) - AddPluginTo(t, plugins, myInvalidPlugin, 2) -} - -func TestPluginEvents(t *testing.T) { - iris.ResetDefault() - var plugins = iris.Default.Plugins - var prelistenran, postlistenran, precloseran bool - - plugins.Add(iris.PreListenFunc(func(*iris.Framework) { - prelistenran = true - })) - - plugins.Add(iris.PostListenFunc(func(*iris.Framework) { - postlistenran = true - })) - - plugins.Add(iris.PreCloseFunc(func(*iris.Framework) { - precloseran = true - })) - - myplugin := &testPluginEx{} - plugins.Add(myplugin) - if plugins.Len() != 4 { - t.Fatalf("Expected: %d plugins to be registered but we got: %d", 4, plugins.Len()) - } - desc := plugins.GetDescription(myplugin) - if desc != testPluginExDescription { - t.Fatalf("Expected: %s as Description of the plugin but got: %s", testPluginExDescription, desc) - } - - plugins.DoPreListen(nil) - plugins.DoPostListen(nil) - plugins.DoPreClose(nil) - - if !prelistenran { - t.Fatalf("Expected to run PreListen Func but it doesn't!") - } - if !postlistenran { - t.Fatalf("Expected to run PostListen Func but it doesn't!") - } - if !precloseran { - t.Fatalf("Expected to run PostListen Func but it doesn't!") - } - - if !myplugin.named { - t.Fatalf("Plugin should be named with: %s!", testPluginExName) - } - if !myplugin.activated { - t.Fatalf("Plugin should be activated but it's not!") - } - if !myplugin.prelistenran { - t.Fatalf("Expected to run PreListen Struct but it doesn't!") - } - if !myplugin.postlistenran { - t.Fatalf("Expected to run PostListen Struct but it doesn't!") - } - if !myplugin.precloseran { - t.Fatalf("Expected to run PostListen Struct but it doesn't!") - } - -} diff --git a/plugins/README.md b/plugins/README.md deleted file mode 100644 index 20b0f77e..00000000 --- a/plugins/README.md +++ /dev/null @@ -1,19 +0,0 @@ -# Plugins - -Navigate through [iris-contrib/plugin](https://github.com/iris-contrib/plugin) repository to view all available 'plugins'. - -> By the word 'plugin', we mean an event-driven system and not the future go1.8 plugin feature. - - -## Installation - -```sh -$ go get github.com/iris-contrib/plugin/... -``` - -## How can I register a plugin? - -```go -app := iris.New() -app.Plugins.Add(thePlugin) -``` diff --git a/policy.go b/policy.go new file mode 100644 index 00000000..af617a73 --- /dev/null +++ b/policy.go @@ -0,0 +1,432 @@ +package iris + +import ( + "io" + "log" + "net/http" + "strings" + + "github.com/kataras/go-errors" +) + +type ( + // Policy is an interface which should be implemented by all + // modules that can adapt a policy to the Framework. + // With a Policy you can change the behavior of almost each of the existing Iris' features. + Policy interface { + // Adapt receives the main *Policies which the Policy should be attached on. + Adapt(frame *Policies) + } + + // Policies is the main policies list, the rest of the objects that implement the Policy + // are adapted to the object which contains a field of type *Policies. + // + // Policies can have nested policies behaviors too. + // See iris.go field: 'policies' and function 'Adapt' for more. + Policies struct { + LoggerPolicy + EventPolicy + RouterReversionPolicy + RouterBuilderPolicy + RouterWrapperPolicy + RenderPolicy + TemplateFuncsPolicy + } +) + +// Adapt implements the behavior in order to be valid to pass Policies as one +// useful for third-party libraries which can provide more tools in one registration. +func (p Policies) Adapt(frame *Policies) { + + // Adapt the logger (optionally, it defaults to a log.New(...).Printf) + if p.LoggerPolicy != nil { + p.LoggerPolicy.Adapt(frame) + } + + // Adapt the flow callbacks (optionally) + p.EventPolicy.Adapt(frame) + + // Adapt the reverse routing behaviors and policy + p.RouterReversionPolicy.Adapt(frame) + + // Adapt the router builder + if p.RouterBuilderPolicy != nil { + p.RouterBuilderPolicy.Adapt(frame) + } + + // Adapt any Router's wrapper (optionally) + if p.RouterWrapperPolicy != nil { + p.RouterWrapperPolicy.Adapt(frame) + } + + // Adapt the render policy (both templates and rich content) + if p.RenderPolicy != nil { + p.RenderPolicy.Adapt(frame) + } + + // Adapt the template funcs which can be used to register template funcs + // from community's packages, it doesn't matters what template/view engine the user + // uses, and if uses at all. + if p.TemplateFuncsPolicy != nil { + p.TemplateFuncsPolicy.Adapt(frame) + } + +} + +// LogMode is the type for the LoggerPolicy write mode. +// Two modes available: +// - ProdMode (production level mode) +// - DevMode (development level mode) +// +// The ProdMode should output only fatal errors +// The DevMode ouputs the rest of the errors +// +// Iris logs ONLY errors at both cases. +// By-default ONLY ProdMode level messages are printed to the os.Stdout. +type LogMode uint8 + +const ( + // ProdMode the production level logger write mode, + // responsible to fatal errors, errors that happen which + // your app can't continue running. + ProdMode LogMode = iota + // DevMode is the development level logger write mode, + // responsible to the rest of the errors, for example + // if you set a app.Favicon("myfav.ico"..) and that fav doesn't exists + // in your system, then it printed by DevMode and app.Favicon simple doesn't works. + // But the rest of the app can continue running, so it's not 'Fatal error' + DevMode +) + +// LoggerPolicy is a simple interface which is used to log mostly system panics +// exception for general debugging messages is when the `Framework.Config.IsDevelopment = true`. +// It should prints to the logger. +// Arguments should be handled in the manner of fmt.Printf. +type LoggerPolicy func(mode LogMode, log string) + +// Adapt addapts a Logger to the main policies. +func (l LoggerPolicy) Adapt(frame *Policies) { + if l != nil { + // notes for me: comment these in order to remember + // why I choose not to do that: + // It wraps the loggers, so you can use more than one + // when you have multiple print targets. + // No this is not a good idea for loggers + // the user may not expecting this behavior, + // if the user wants multiple targets she/he + // can wrap their loggers or use one logger to print on all targets. + // COMMENT: + // logger := l + // if frame.LoggerPolicy != nil { + // prevLogger := frame.LoggerPolicy + // nextLogger := l + // logger = func(mode LogMode, log string) { + // prevLogger(mode, log) + // nextLogger(mode, log) + // } + // } + frame.LoggerPolicy = l + } +} + +// The write method exists to LoggerPolicy to be able to passed +// as a valid an io.Writer when you need it. +// +// Write writes len(p) bytes from p to the underlying data stream. +// It returns the number of bytes written from p (0 <= n <= len(p)) +// and any error encountered that caused the write to stop early. +// Write must return a non-nil error if it returns n < len(p). +// Write must not modify the slice data, even temporarily. +// +// Implementations must not retain p. +// +// Note: this Write writes as the Production Env, so the default logger should be able to log this messages +// coming from internal http.Server (mostly) +// you can change this behavior too. +func (l LoggerPolicy) Write(p []byte) (n int, err error) { + log := string(p) + l(ProdMode, log) + return len(log), nil +} + +// ToLogger returns a new *log.Logger +// which prints to the the LoggerPolicy function +// this is used when your packages needs explicit an *log.Logger. +// +// Note: Each time you call it, it returns a new *log.Logger. +func (l LoggerPolicy) ToLogger(flag int) *log.Logger { + return log.New(l, "", flag) +} + +type ( + // EventListener is the signature for type of func(*Framework), + // which is used to register events inside an EventPolicy. + // + // Keep note that, inside the policy this is a wrapper + // in order to register more than one listener without the need of slice. + EventListener func(*Framework) + + // EventPolicy contains the available Framework's flow event callbacks. + // Available events: + // - Boot + // - Build + // - Interrupted + // - Recover + EventPolicy struct { + // Boot with a listener type of EventListener. + // Fires when '.Boot' is called (by .Serve functions or manually), + // before the Build of the components and the Listen, + // after VHost and VSCheme configuration has been setted. + Boot EventListener + // Before Listen, after Boot + Build EventListener + // Interrupted with a listener type of EventListener. + // Fires after the terminal is interrupted manually by Ctrl/Cmd + C + // which should be used to release external resources. + // Iris will close and os.Exit at the end of custom interrupted events. + // If you want to prevent the default behavior just block on the custom Interrupted event. + Interrupted EventListener + // Recover with a listener type of func(*Framework, interface{}). + // Fires when an unexpected error(panic) is happening at runtime, + // while the server's net.Listener accepting requests + // or when a '.Must' call contains a filled error. + // Used to release external resources and '.Close' the server. + // Only one type of this callback is allowed. + // + // If not empty then the Framework will skip its internal + // server's '.Close' and panic to its '.Logger' and execute that callback instaed. + // Differences from Interrupted: + // 1. Fires on unexpected errors + // 2. Only one listener is allowed. + Recover func(*Framework, error) + } +) + +var _ Policy = EventPolicy{} + +// Adapt adaps an EventPolicy object to the main *Policies. +func (e EventPolicy) Adapt(frame *Policies) { + + // Boot event listener, before the build (old: PreBuild) + frame.EventPolicy.Boot = + wrapEvtListeners(frame.EventPolicy.Boot, e.Boot) + + // Build event listener, after Boot and before Listen(old: PostBuild & PreListen) + frame.EventPolicy.Build = + wrapEvtListeners(frame.EventPolicy.Build, e.Build) + + // Interrupted event listener, when control+C or manually interrupt by os signal + frame.EventPolicy.Interrupted = + wrapEvtListeners(frame.EventPolicy.Interrupted, e.Interrupted) + + // Recover event listener, when panic on .Must and inside .Listen/ListenTLS/ListenUNIX/ListenLETSENCRYPT/Serve + // only one allowed, no wrapper is used. + if e.Recover != nil { + frame.EventPolicy.Recover = e.Recover + } + +} + +// Fire fires an EventListener with its Framework when listener is not nil. +// Returns true when fired, otherwise false. +func (e EventPolicy) Fire(ln EventListener, s *Framework) bool { + if ln != nil { + ln(s) + return true + } + return false +} + +func wrapEvtListeners(prev EventListener, next EventListener) EventListener { + if next == nil { + return prev + } + listener := next + if prev != nil { + listener = func(s *Framework) { + prev(s) + next(s) + } + } + + return listener +} + +type ( + // RouterReversionPolicy is used for the reverse routing feature on + // which custom routers should create and adapt to the Policies. + RouterReversionPolicy struct { + // StaticPath should return the static part of the route path + // for example, with the default router (: and *): + // /api/user/:userid should return /api/user + // /api/user/:userid/messages/:messageid should return /api/user + // /dynamicpath/*path should return /dynamicpath + // /my/path should return /my/path + StaticPath func(path string) string + // WildcardPath should return a path converted to a 'dynamic' path + // for example, with the default router(wildcard symbol: '*'): + // ("/static", "path") should return /static/*path + // ("/myfiles/assets", "anything") should return /myfiles/assets/*anything + WildcardPath func(path string, paramName string) string + // URLPath used for reverse routing on templates with {{ url }} and {{ path }} funcs. + // Receives the route name and arguments and returns its http path + URLPath func(r RouteInfo, args ...string) string + + // RouteContextLinker should put the route's handlers and named parameters(if any) to the ctx + // it's used to execute virtually an "offline" route + // against a context like it was requested by user, but it is not. + RouteContextLinker func(r RouteInfo, ctx *Context) + } + // RouterBuilderPolicy is the most useful Policy for custom routers. + // A custom router should adapt this policy which is a func + // accepting a route repository (contains all necessary routes information) + // and a context pool which should be used inside router's handlers. + RouterBuilderPolicy func(repo RouteRepository, cPool ContextPool) http.Handler + // RouterWrapperPolicy is the Policy which enables a wrapper on the top of + // the builded Router. Usually it's useful for third-party middleware + // when need to wrap the entire application with a middleware like CORS. + RouterWrapperPolicy func(w http.ResponseWriter, r *http.Request, next http.HandlerFunc) +) + +func normalizePath(path string) string { + path = strings.Replace(path, "//", "/", -1) + if len(path) > 1 && strings.IndexByte(path, '/') == len(path)-1 { + // if it's not "/" and ending with slash remove that slash + path = path[0 : len(path)-2] + } + return path +} + +// Adapt adaps a RouterReversionPolicy object to the main *Policies. +func (r RouterReversionPolicy) Adapt(frame *Policies) { + if r.StaticPath != nil { + staticPathFn := r.StaticPath + frame.RouterReversionPolicy.StaticPath = func(path string) string { + return staticPathFn(normalizePath(path)) + } + } + + if r.WildcardPath != nil { + wildcardPathFn := r.WildcardPath + frame.RouterReversionPolicy.WildcardPath = func(path string, paramName string) string { + return wildcardPathFn(normalizePath(path), paramName) + } + } + + if r.URLPath != nil { + frame.RouterReversionPolicy.URLPath = r.URLPath + } + + if r.RouteContextLinker != nil { + frame.RouterReversionPolicy.RouteContextLinker = r.RouteContextLinker + } +} + +// Adapt adaps a RouterBuilderPolicy object to the main *Policies. +func (r RouterBuilderPolicy) Adapt(frame *Policies) { + // What is this kataras? + // The whole design of this file is brilliant = go's power + my ideas and experience on software architecture. + // + // When the router decides to compile/build this behavior + // then this overload will check for a wrapper too + // if a wrapper exists it will wrap the result of the RouterBuilder (which is http.Handler, the Router.) + // and return that instead. + // I moved the logic here so we don't need a 'compile/build' method inside the routerAdaptor. + frame.RouterBuilderPolicy = RouterBuilderPolicy(func(repo RouteRepository, cPool ContextPool) http.Handler { + handler := r(repo, cPool) + wrapper := frame.RouterWrapperPolicy + if wrapper != nil { + originalHandler := handler.ServeHTTP + + handler = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + wrapper(w, r, originalHandler) + }) + } + return handler + }) +} + +// Adapt adaps a RouterWrapperPolicy object to the main *Policies. +func (r RouterWrapperPolicy) Adapt(frame *Policies) { + frame.RouterWrapperPolicy = r +} + +// RenderPolicy is the type which you can adapt custom renderers +// based on the 'name', simple as that. +// Note that the whole template view system and +// content negotiation works by setting this function via other adaptors. +// +// The functions are wrapped, like any other policy func, the only difference is that +// here the developer has a priority over the defaults: +// - the last registered is trying to be executed first +// - the first registered is executing last. +// So a custom adaptor that the community can create and share with each other +// can override the existing one with just a simple registration. +type RenderPolicy func(out io.Writer, name string, bind interface{}, options ...map[string]interface{}) (error, bool) + +// Adapt adaps a RenderPolicy object to the main *Policies. +func (r RenderPolicy) Adapt(frame *Policies) { + if r != nil { + renderer := r + prevRenderer := frame.RenderPolicy + if prevRenderer != nil { + nextRenderer := r + renderer = func(out io.Writer, name string, binding interface{}, options ...map[string]interface{}) (error, bool) { + // Remember: RenderPolicy works in the opossite order of declaration, + // the last registered is trying to be executed first, + // the first registered is executing last. + err, ok := nextRenderer(out, name, binding, options...) + if !ok { + + prevErr, prevOk := prevRenderer(out, name, binding, options...) + if err != nil { + if prevErr != nil { + err = errors.New(prevErr.Error()).Append(err.Error()) + } + } + if prevOk { + ok = true + } + } + // this renderer is responsible for this name + // but it has an error, so don't continue to the next + return err, ok + + } + } + + frame.RenderPolicy = renderer + } +} + +// TemplateFuncsPolicy sets or overrides template func map. +// Defaults are the iris.URL and iris.Path, all the template engines supports the following: +// {{ url "mynamedroute" "pathParameter_ifneeded"} } +// {{ urlpath "mynamedroute" "pathParameter_ifneeded" }} +// {{ render "header.html" }} +// {{ render_r "header.html" }} // partial relative path to current page +// {{ yield }} +// {{ current }} +// +// Developers can already set the template's func map from the view adaptors, example: view.HTML(...).Funcs(...)), +// this type exists in order to be possible from third-party developers to create packages that bind template functions +// to the Iris without the need of knowing what template engine is used by the user or +// what order of declaration the user should follow. +type TemplateFuncsPolicy map[string]interface{} // interface can be: func(arguments ...string) string {} + +// Adapt adaps a TemplateFuncsPolicy object to the main *Policies. +func (t TemplateFuncsPolicy) Adapt(frame *Policies) { + if len(t) > 0 { + if frame.TemplateFuncsPolicy == nil { + frame.TemplateFuncsPolicy = t + return + } + + if frame.TemplateFuncsPolicy != nil { + for k, v := range t { + // set or replace the existing + frame.TemplateFuncsPolicy[k] = v + } + } + } +} diff --git a/response_recorder.go b/response_recorder.go index caa352fb..c8b3fb2e 100644 --- a/response_recorder.go +++ b/response_recorder.go @@ -148,14 +148,13 @@ func (w *ResponseRecorder) flushResponse() { w.responseWriter.flushResponse() if len(w.chunks) > 0 { + // ignore error w.responseWriter.Write(w.chunks) } - } // Flush sends any buffered data to the client. func (w *ResponseRecorder) Flush() { - w.flushResponse() w.responseWriter.Flush() w.ResetBody() } @@ -250,6 +249,7 @@ func (w *ResponseRecorder) writeTo(res ResponseWriter) { // append the body if len(w.chunks) > 0 { + // ignore error to.Write(w.chunks) } diff --git a/response_writer.go b/response_writer.go index 99f0c151..f19fade8 100644 --- a/response_writer.go +++ b/response_writer.go @@ -202,7 +202,7 @@ func (w *responseWriter) Hijack() (net.Conn, *bufio.ReadWriter, error) { return h.Hijack() } - return nil, nil, errors.New("Hijack is not supported by this ResponseWriter.") + return nil, nil, errors.New("hijack is not supported by this ResponseWriter") } // Flush sends any buffered data to the client. diff --git a/response_writer_test.go b/response_writer_test.go deleted file mode 100644 index 08fd1e6d..00000000 --- a/response_writer_test.go +++ /dev/null @@ -1,158 +0,0 @@ -package iris_test - -import ( - "fmt" - "testing" - - "github.com/kataras/iris" - "github.com/kataras/iris/httptest" -) - -// most tests lives inside context_test.go:Transactions, there lives the response writer's full and coblex tests -func TestResponseWriterBeforeFlush(t *testing.T) { - api := iris.New() - body := "my body" - beforeFlushBody := "body appeneded or setted before callback" - - api.Get("/", func(ctx *iris.Context) { - w := ctx.ResponseWriter - - w.SetBeforeFlush(func() { - w.WriteString(beforeFlushBody) - }) - - w.WriteString(body) - }) - - // recorder can change the status code after write too - // it can also be changed everywhere inside the context's lifetime - api.Get("/recorder", func(ctx *iris.Context) { - w := ctx.Recorder() - - w.SetBeforeFlush(func() { - w.SetBodyString(beforeFlushBody) - w.WriteHeader(iris.StatusForbidden) - }) - - w.WriteHeader(iris.StatusOK) - w.WriteString(body) - }) - - e := httptest.New(api, t) - - e.GET("/").Expect().Status(iris.StatusOK).Body().Equal(body + beforeFlushBody) - e.GET("/recorder").Expect().Status(iris.StatusForbidden).Body().Equal(beforeFlushBody) -} - -func TestResponseWriterToRecorderMiddleware(t *testing.T) { - api := iris.New() - beforeFlushBody := "body appeneded or setted before callback" - api.UseGlobal(iris.Recorder) - - api.Get("/", func(ctx *iris.Context) { - w := ctx.Recorder() - - w.SetBeforeFlush(func() { - w.SetBodyString(beforeFlushBody) - w.WriteHeader(iris.StatusForbidden) - }) - - w.WriteHeader(iris.StatusOK) - w.WriteString("this will not be sent at all because of SetBodyString") - }) - - e := httptest.New(api, t) - - e.GET("/").Expect().Status(iris.StatusForbidden).Body().Equal(beforeFlushBody) -} - -func TestResponseRecorderStatusCodeContentTypeBody(t *testing.T) { - api := iris.New() - firstStatusCode := iris.StatusOK - contentType := "text/html; charset=" + api.Config.Charset - firstBodyPart := "first" - secondBodyPart := "second" - prependedBody := "zero" - expectedBody := prependedBody + firstBodyPart + secondBodyPart - - api.Use(iris.Recorder) - // recorder's status code can change if needed by a middleware or the last handler. - api.UseFunc(func(ctx *iris.Context) { - ctx.SetStatusCode(firstStatusCode) - ctx.Next() - }) - - api.UseFunc(func(ctx *iris.Context) { - ctx.SetContentType(contentType) - ctx.Next() - }) - - api.UseFunc(func(ctx *iris.Context) { - // set a body ( we will append it later, only with response recorder we can set append or remove a body or a part of it*) - ctx.WriteString(firstBodyPart) - ctx.Next() - }) - - api.UseFunc(func(ctx *iris.Context) { - ctx.WriteString(secondBodyPart) - ctx.Next() - }) - - api.Get("/", func(ctx *iris.Context) { - previousStatusCode := ctx.StatusCode() - if previousStatusCode != firstStatusCode { - t.Fatalf("Previous status code should be %d but got %d", firstStatusCode, previousStatusCode) - } - - previousContentType := ctx.ContentType() - if previousContentType != contentType { - t.Fatalf("First content type should be %s but got %d", contentType, previousContentType) - } - // change the status code, this will tested later on (httptest) - ctx.SetStatusCode(iris.StatusForbidden) - prevBody := string(ctx.Recorder().Body()) - if prevBody != firstBodyPart+secondBodyPart { - t.Fatalf("Previous body (first handler + second handler's writes) expected to be: %s but got: %s", firstBodyPart+secondBodyPart, prevBody) - } - // test it on httptest later on - ctx.Recorder().SetBodyString(prependedBody + prevBody) - }) - - e := httptest.New(api, t) - - et := e.GET("/").Expect().Status(iris.StatusForbidden) - et.Header("Content-Type").Equal(contentType) - et.Body().Equal(expectedBody) -} - -func ExampleResponseWriter_WriteHeader() { - // func TestResponseWriterMultipleWriteHeader(t *testing.T) { - iris.ResetDefault() - iris.Default.Set(iris.OptionDisableBanner(true)) - - expectedOutput := "Hey" - iris.Get("/", func(ctx *iris.Context) { - - // here - for i := 0; i < 10; i++ { - ctx.ResponseWriter.WriteHeader(iris.StatusOK) - } - - ctx.Writef(expectedOutput) - - // here - fmt.Println(expectedOutput) - - // here - for i := 0; i < 10; i++ { - ctx.SetStatusCode(iris.StatusOK) - } - }) - - e := httptest.New(iris.Default, nil) - e.GET("/").Expect().Status(iris.StatusOK).Body().Equal(expectedOutput) - // here it shouldn't log an error that status code write multiple times (by the net/http package.) - - // Output: - // Hey -} diff --git a/route.go b/route.go new file mode 100644 index 00000000..9992da29 --- /dev/null +++ b/route.go @@ -0,0 +1,332 @@ +package iris + +import ( + "sort" + "strings" +) + +type ( + // RouteInfo is just the (other idea was : RouteInfo but we needed the Change/SetName visible so...) + // information of the registered routes. + RouteInfo interface { + // ChangeName & AllowOPTIONS are the only one route property + // which can be change without any bad side-affects + // so it's the only setter here. + // + // It's used on iris.Default.Handle() + ChangeName(name string) RouteInfo + + // Name returns the name of the route + Name() string + // Method returns the http method + Method() string + // AllowOPTIONS called when this route is targeting OPTIONS methods too + // it's an alternative way of registring the same route with '.OPTIONS("/routepath", routeMiddleware)' + AllowOPTIONS() RouteInfo + // HasCors returns true if the route is targeting OPTIONS methods too + // or it has a middleware which conflicts with "httpmethod", + // otherwise false + HasCors() bool + // Subdomain returns the subdomain,if any + Subdomain() string + // Path returns the path + Path() string + // Middleware returns the slice of Handler([]Handler) registered to this route + Middleware() Middleware + // IsOnline returns true if the route is marked as "online" (state) + IsOnline() bool + } + + // route holds useful information about route + route struct { + // if no name given then it's the subdomain+path + name string + subdomain string + method string + allowOptionsMethod bool + path string + middleware Middleware + } +) + +var _ RouteInfo = &route{} + +// RouteConflicts checks for route's middleware conflicts +func RouteConflicts(r RouteInfo, with string) bool { + for _, h := range r.Middleware() { + if m, ok := h.(interface { + Conflicts() string + }); ok { + if c := m.Conflicts(); c == with { + return true + } + } + } + return false +} + +// Name returns the name of the route +func (r route) Name() string { + return r.name +} + +// Name returns the name of the route +func (r *route) ChangeName(name string) RouteInfo { + r.name = name + return r +} + +// AllowOPTIONS called when this route is targeting OPTIONS methods too +// it's an alternative way of registring the same route with '.OPTIONS("/routepath", routeMiddleware)' +func (r *route) AllowOPTIONS() RouteInfo { + r.allowOptionsMethod = true + return r +} + +// Method returns the http method +func (r route) Method() string { + return r.method +} + +// Subdomain returns the subdomain,if any +func (r route) Subdomain() string { + return r.subdomain +} + +// Path returns the path +func (r route) Path() string { + return r.path +} + +// Middleware returns the slice of Handler([]Handler) registered to this route +func (r route) Middleware() Middleware { + return r.middleware +} + +// IsOnline returns true if the route is marked as "online" (state) +func (r route) IsOnline() bool { + return r.method != MethodNone +} + +// HasCors returns true if the route is targeting OPTIONS methods too +// or it has a middleware which conflicts with "httpmethod", +// otherwise false +func (r *route) HasCors() bool { + return r.allowOptionsMethod || RouteConflicts(r, "httpmethod") +} + +// MethodChangedListener listener signature fired when route method changes +type MethodChangedListener func(routeInfo RouteInfo, oldMethod string) + +// RouteRepository contains the interface which is used on custom routers +// contains methods and helpers to find a route by its name, +// and change its method, path, middleware. +// +// This is not visible outside except the RouterBuilderPolicy +type RouteRepository interface { // RouteEngine kai ContextEngine mesa sto builder adi gia RouteRepository kai ContextEngine + RoutesInfo + ChangeName(routeInfo RouteInfo, newName string) + ChangeMethod(routeInfo RouteInfo, newMethod string) + ChangePath(routeInfo RouteInfo, newPath string) + ChangeMiddleware(routeInfo RouteInfo, newMiddleware Middleware) +} + +// RoutesInfo is the interface which contains the valid actions +// permitted at RUNTIME +type RoutesInfo interface { // RouteRepository + Lookup(routeName string) RouteInfo + Visit(visitor func(RouteInfo)) + OnMethodChanged(methodChangedListener MethodChangedListener) + Online(routeInfo RouteInfo, HTTPMethod string) bool + Offline(routeInfo RouteInfo) bool +} + +// routeRepository contains all the routes. +// Implements both RouteRepository and RoutesInfo +type routeRepository struct { + routes []*route + // when builded (TODO: move to its own struct) + methodChangedListeners []MethodChangedListener +} + +var _ sort.Interface = &routeRepository{} +var _ RouteRepository = &routeRepository{} + +// Len is the number of elements in the collection. +func (r routeRepository) Len() int { + return len(r.routes) +} + +// Less reports whether the element with +// index i should sort before the element with index j. +func (r routeRepository) Less(i, j int) bool { + return len(r.routes[i].Subdomain()) > len(r.routes[j].Subdomain()) +} + +// Swap swaps the elements with indexes i and j. +func (r routeRepository) Swap(i, j int) { + r.routes[i], r.routes[j] = r.routes[j], r.routes[i] +} + +func (r *routeRepository) register(method, subdomain, path string, + middleware Middleware) *route { + + _route := &route{ + name: method + subdomain + path, + method: method, + subdomain: subdomain, + path: path, + middleware: middleware, + } + + r.routes = append(r.routes, _route) + return _route +} + +func (r *routeRepository) getRouteByName(routeName string) *route { + for i := range r.routes { + _route := r.routes[i] + if _route.name == routeName { + return _route + } + } + return nil +} + +// Lookup returns a route by its name +// used for reverse routing and templates +func (r *routeRepository) Lookup(routeName string) RouteInfo { + route := r.getRouteByName(routeName) + if route == nil { + return nil + } + return route +} + +// ChangeName changes the Name of an existing route +func (r *routeRepository) ChangeName(routeInfo RouteInfo, + newName string) { + + if newName != "" { + route := r.getRouteByName(routeInfo.Name()) + if route != nil { + route.name = newName + } + } +} + +func (r *routeRepository) OnMethodChanged(methodChangedListener MethodChangedListener) { + r.methodChangedListeners = append(r.methodChangedListeners, methodChangedListener) +} + +func (r *routeRepository) fireMethodChangedListeners(routeInfo RouteInfo, oldMethod string) { + for i := 0; i < len(r.methodChangedListeners); i++ { + r.methodChangedListeners[i](routeInfo, oldMethod) + } +} + +// ChangeMethod changes the Method of an existing route +func (r *routeRepository) ChangeMethod(routeInfo RouteInfo, + newMethod string) { + newMethod = strings.ToUpper(newMethod) + valid := false + for _, m := range AllMethods { + if newMethod == m || newMethod == MethodNone { + valid = true + } + } + + if valid { + route := r.getRouteByName(routeInfo.Name()) + if route != nil && route.method != newMethod { + oldMethod := route.method + route.method = newMethod + r.fireMethodChangedListeners(routeInfo, oldMethod) + } + } +} + +// Online sets the state of the route to "online" with a specific http method +// it re-builds the router +// +// returns true if state was actually changed +// +// see context.ExecRoute(routeInfo), +// iris.Default.None(...) and iris.Routes.Online/.Routes.Offline +// For more details look: https://github.com/kataras/iris/issues/585 +// +// Example: https://github.com/iris-contrib/examples/tree/master/route_state +func (r *routeRepository) Online(routeInfo RouteInfo, HTTPMethod string) bool { + return r.changeRouteState(routeInfo, HTTPMethod) +} + +// Offline sets the state of the route to "offline" and re-builds the router +// +// returns true if state was actually changed +// +// see context.ExecRoute(routeInfo), +// iris.Default.None(...) and iris.Routes.Online/.Routes.Offline +// For more details look: https://github.com/kataras/iris/issues/585 +// +// Example: https://github.com/iris-contrib/examples/tree/master/route_state +func (r *routeRepository) Offline(routeInfo RouteInfo) bool { + return r.changeRouteState(routeInfo, MethodNone) +} + +// changeRouteState changes the state of the route. +// iris.MethodNone for offline +// and iris.MethodGet/MethodPost/MethodPut/MethodDelete /MethodConnect/MethodOptions/MethodHead/MethodTrace/MethodPatch for online +// it re-builds the router +// +// returns true if state was actually changed +func (r *routeRepository) changeRouteState(routeInfo RouteInfo, HTTPMethod string) bool { + if routeInfo != nil { + nonSpecificMethod := len(HTTPMethod) == 0 + if routeInfo.Method() != HTTPMethod { + if nonSpecificMethod { + r.ChangeMethod(routeInfo, MethodGet) // if no method given, then do it for "GET" only + } else { + r.ChangeMethod(routeInfo, HTTPMethod) + } + // re-build the router/main handler should be implemented + // on the custom router via OnMethodChanged event. + return true + } + } + return false +} + +// ChangePath changes the Path of an existing route +func (r *routeRepository) ChangePath(routeInfo RouteInfo, + newPath string) { + + if newPath != "" { + route := r.getRouteByName(routeInfo.Name()) + if route != nil { + route.path = newPath + } + } +} + +// ChangeMiddleware changes the Middleware/Handlers of an existing route +func (r *routeRepository) ChangeMiddleware(routeInfo RouteInfo, + newMiddleware Middleware) { + + route := r.getRouteByName(routeInfo.Name()) + if route != nil { + route.middleware = newMiddleware + } +} + +// Visit accepts a visitor func which receives a route(readonly). +// That visitor func accepts the next route of each of the route entries. +func (r *routeRepository) Visit(visitor func(RouteInfo)) { + for i := range r.routes { + visitor(r.routes[i]) + } +} + +// sort sorts routes by subdomain. +func (r *routeRepository) sort() { + sort.Sort(r) +} diff --git a/router.go b/router.go new file mode 100644 index 00000000..fe42ab4d --- /dev/null +++ b/router.go @@ -0,0 +1,676 @@ +package iris + +import ( + "net/http" + "os" + "path" + "strings" + "time" + + "github.com/kataras/go-errors" + "github.com/kataras/go-fs" +) + +const ( + // MethodGet "GET" + MethodGet = "GET" + // MethodPost "POST" + MethodPost = "POST" + // MethodPut "PUT" + MethodPut = "PUT" + // MethodDelete "DELETE" + MethodDelete = "DELETE" + // MethodConnect "CONNECT" + MethodConnect = "CONNECT" + // MethodHead "HEAD" + MethodHead = "HEAD" + // MethodPatch "PATCH" + MethodPatch = "PATCH" + // MethodOptions "OPTIONS" + MethodOptions = "OPTIONS" + // MethodTrace "TRACE" + MethodTrace = "TRACE" + // MethodNone is a Virtual method + // to store the "offline" routes + MethodNone = "NONE" +) + +var ( + // AllMethods contains all the http valid methods: + // "GET", "POST", "PUT", "DELETE", "CONNECT", "HEAD", "PATCH", "OPTIONS", "TRACE" + AllMethods = [...]string{ + MethodGet, + MethodPost, + MethodPut, + MethodDelete, + MethodConnect, + MethodHead, + MethodPatch, + MethodOptions, + MethodTrace, + } +) + +const ( + // subdomainIndicator where './' exists in a registered path then it contains subdomain + subdomainIndicator = "./" + // DynamicSubdomainIndicator where a registered path starts with '*.' then it contains a dynamic subdomain, if subdomain == "*." then its dynamic + DynamicSubdomainIndicator = "*." + // slashByte is just a byte of '/' rune/char + slashByte = byte('/') + // slash is just a string of "/" + slash = "/" +) + +var errRouterIsMissing = errors.New( + ` +fatal error, router is missing! +Please .Adapt one of the available routers inside 'kataras/iris/adaptors'. +By-default Iris supports two routers, httprouter and gorillamux. +Edit your main .go source file to adapt one of these routers and restart your app. + i.e: lines (<---) were missing. + ----------------------------HTTPROUTER---------------------------------- + import ( + "github.com/kataras/iris" + "github.com/kataras/iris/adaptors/httprouter" // <--- this line + ) + + func main(){ + app := iris.New() + // right below the iris.New() + app.Adapt(httprouter.New()) // <--- and this line were missing. + + app.Listen("%s") + } + + + ----------------------------OR GORILLA MUX------------------------------- + + import ( + "github.com/kataras/iris" + "github.com/kataras/iris/adaptors/gorillamux" // <--- or this line + ) + + func main(){ + app := iris.New() + // right below the iris.New() + app.Adapt(gorillamux.New()) // <--- and this line were missing. + + app.Listen("%s") + } + `) + +// Router the visible api for RESTFUL +type Router struct { + + // Ok I thought it very well + // these changes are breaking for sure + // but for the best design I have to risk stability. + // so the router api it's the router + // and new feature aka policies will be responsible + // to build the handler and reverse routing + // from this repo and errors + // the global routes registry + repository *routeRepository + // the global errors registry + Errors *ErrorHandlers + Context ContextPool + handler http.Handler + + // per-party middleware + middleware Middleware + // per-party routes (useful only for done middleware) + apiRoutes []*route + // per-party done middleware + doneMiddleware Middleware + // per-party + relativePath string +} + +var ( + // errDirectoryFileNotFound returns an error with message: 'Directory or file %s couldn't found. Trace: +error trace' + errDirectoryFileNotFound = errors.New("Directory or file %s couldn't found. Trace: %s") +) + +func (router *Router) build(builder RouterBuilderPolicy) { + router.handler = builder(router.repository, router.Context) +} + +func (router *Router) ServeHTTP(w http.ResponseWriter, r *http.Request) { + router.handler.ServeHTTP(w, r) +} + +// Routes returns the routes information, +// some of them can be changed at runtime some others not +// the result of this RoutesInfo is safe to use at RUNTIME. +func (router *Router) Routes() RoutesInfo { + return router.repository +} + +// UseGlobal registers Handler middleware to the beginning, prepends them instead of append +// +// Use it when you want to add a global middleware to all parties, to all routes in all subdomains +// It should be called right before Listen functions +func (router *Router) UseGlobal(handlers ...Handler) { + router.repository.Visit(func(routeInfo RouteInfo) { + router.repository.ChangeMiddleware(routeInfo, append(handlers, routeInfo.Middleware()...)) + }) + + router.Use(handlers...) +} + +// UseGlobalFunc registers HandlerFunc middleware to the beginning, prepends them instead of append +// +// Use it when you want to add a global middleware to all parties, to all routes in all subdomains +// It should be called right before Listen functions +func (router *Router) UseGlobalFunc(handlersFn ...HandlerFunc) { + router.UseGlobal(convertToHandlers(handlersFn)...) +} + +// Party is just a group joiner of routes which have the same prefix and share same middleware(s) also. +// Party can also be named as 'Join' or 'Node' or 'Group' , Party chosen because it has more fun +func (router *Router) Party(relativePath string, handlersFn ...HandlerFunc) *Router { + parentPath := router.relativePath + dot := string(subdomainIndicator[0]) + if len(parentPath) > 0 && parentPath[0] == slashByte && strings.HasSuffix(relativePath, dot) { // if ends with . , example: admin., it's subdomain-> + parentPath = parentPath[1:] // remove first slash + } + + fullpath := parentPath + relativePath + middleware := convertToHandlers(handlersFn) + // append the parent's +child's handlers + middleware = joinMiddleware(router.middleware, middleware) + + return &Router{ + repository: router.repository, + Errors: router.Errors, + Context: router.Context, + handler: router.handler, // not-needed + doneMiddleware: router.doneMiddleware, + apiRoutes: make([]*route, 0), + middleware: middleware, + relativePath: fullpath, + } +} + +// Use registers Handler middleware +// returns itself +func (router *Router) Use(handlers ...Handler) *Router { + router.middleware = append(router.middleware, handlers...) + return router +} + +// UseFunc registers HandlerFunc middleware +// returns itself +func (router *Router) UseFunc(handlersFn ...HandlerFunc) *Router { + return router.Use(convertToHandlers(handlersFn)...) +} + +// Done registers Handler 'middleware' the only difference from .Use is that it +// should be used BEFORE any party route registered or AFTER ALL party's routes have been registered. +// +// returns itself +func (router *Router) Done(handlers ...Handler) *Router { + if len(router.apiRoutes) > 0 { // register these middleware on previous-party-defined routes, it called after the party's route methods (Handle/HandleFunc/Get/Post/Put/Delete/...) + for i, n := 0, len(router.apiRoutes); i < n; i++ { + router.apiRoutes[i].middleware = append(router.apiRoutes[i].middleware, handlers...) + } + } else { + // register them on the doneMiddleware, which will be used on Handle to append these middlweare as the last handler(s) + router.doneMiddleware = append(router.doneMiddleware, handlers...) + } + + return router +} + +// DoneFunc registers HandlerFunc 'middleware' the only difference from .Use is that it +// should be used BEFORE any party route registered or AFTER ALL party's routes have been registered. +// +// returns itself +func (router *Router) DoneFunc(handlersFn ...HandlerFunc) *Router { + return router.Done(convertToHandlers(handlersFn)...) +} + +// Handle registers a route to the server's router +// if empty method is passed then registers handler(s) for all methods, same as .Any, but returns nil as result +func (router *Router) Handle(method string, registeredPath string, handlers ...Handler) RouteInfo { + if method == "" { // then use like it was .Any + for _, k := range AllMethods { + router.Handle(k, registeredPath, handlers...) + } + return nil + } + + fullpath := router.relativePath + registeredPath // for now, keep the last "/" if any, "/xyz/" + + middleware := joinMiddleware(router.middleware, handlers) + + // here we separate the subdomain and relative path + subdomain := "" + path := fullpath + + if dotWSlashIdx := strings.Index(path, subdomainIndicator); dotWSlashIdx > 0 { + subdomain = fullpath[0 : dotWSlashIdx+1] // admin. + path = fullpath[dotWSlashIdx+1:] // / + } + + // we splitted the path and subdomain parts so we're ready to check only the path, + // otherwise we will had problems with subdomains + // if the user wants beta:= iris.Default.Party("/beta"); beta.Get("/") to be registered as + //: /beta/ then should disable the path correction OR register it like: beta.Get("//") + // this is only for the party's roots in order to have expected paths, + // as we do with iris.Default.Get("/") which is localhost:8080 as RFC points, not localhost:8080/ + ///TODO: 31 Jan 2017 -> It does nothing I don't know why I code it but any way' I think it later... + // if router.mux.correctPath && registeredPath == slash { // check the given relative path + // // remove last "/" if any, "/xyz/" + // if len(path) > 1 { // if it's the root, then keep it* + // if path[len(path)-1] == slashByte { + // // ok we are inside /xyz/ + // } + // } + // } + + path = strings.Replace(path, "//", "/", -1) // fix the path if double // + + if len(router.doneMiddleware) > 0 { + middleware = append(middleware, router.doneMiddleware...) // register the done middleware, if any + } + r := router.repository.register(method, subdomain, path, middleware) + + router.apiRoutes = append(router.apiRoutes, r) + // should we remove the router.apiRoutes on the .Party (new children party) ?, No, because the user maybe use this party later + // should we add to the 'inheritance tree' the router.apiRoutes, No, these are for this specific party only, because the user propably, will have unexpected behavior when using Use/UseFunc, Done/DoneFunc + return r +} + +// HandleFunc registers and returns a route with a method string, path string and a handler +// registeredPath is the relative url path +func (router *Router) HandleFunc(method string, registeredPath string, handlersFn ...HandlerFunc) RouteInfo { + return router.Handle(method, registeredPath, convertToHandlers(handlersFn)...) +} + +// None registers an "offline" route +// see context.ExecRoute(routeName), +// iris.Default.None(...) and iris.Default.SetRouteOnline/SetRouteOffline +// For more details look: https://github.com/kataras/iris/issues/585 +// +// Example: https://github.com/iris-contrib/examples/tree/master/route_state +func (router *Router) None(path string, handlersFn ...HandlerFunc) RouteInfo { + return router.HandleFunc(MethodNone, path, handlersFn...) +} + +// Get registers a route for the Get http method +func (router *Router) Get(path string, handlersFn ...HandlerFunc) RouteInfo { + return router.HandleFunc(MethodGet, path, handlersFn...) +} + +// Post registers a route for the Post http method +func (router *Router) Post(path string, handlersFn ...HandlerFunc) RouteInfo { + return router.HandleFunc(MethodPost, path, handlersFn...) +} + +// Put registers a route for the Put http method +func (router *Router) Put(path string, handlersFn ...HandlerFunc) RouteInfo { + return router.HandleFunc(MethodPut, path, handlersFn...) +} + +// Delete registers a route for the Delete http method +func (router *Router) Delete(path string, handlersFn ...HandlerFunc) RouteInfo { + return router.HandleFunc(MethodDelete, path, handlersFn...) +} + +// Connect registers a route for the Connect http method +func (router *Router) Connect(path string, handlersFn ...HandlerFunc) RouteInfo { + return router.HandleFunc(MethodConnect, path, handlersFn...) +} + +// Head registers a route for the Head http method +func (router *Router) Head(path string, handlersFn ...HandlerFunc) RouteInfo { + return router.HandleFunc(MethodHead, path, handlersFn...) +} + +// Options registers a route for the Options http method +func (router *Router) Options(path string, handlersFn ...HandlerFunc) RouteInfo { + return router.HandleFunc(MethodOptions, path, handlersFn...) +} + +// Patch registers a route for the Patch http method +func (router *Router) Patch(path string, handlersFn ...HandlerFunc) RouteInfo { + return router.HandleFunc(MethodPatch, path, handlersFn...) +} + +// Trace registers a route for the Trace http method +func (router *Router) Trace(path string, handlersFn ...HandlerFunc) RouteInfo { + return router.HandleFunc(MethodTrace, path, handlersFn...) +} + +// Any registers a route for ALL of the http methods (Get,Post,Put,Head,Patch,Options,Connect,Delete) +func (router *Router) Any(registeredPath string, handlersFn ...HandlerFunc) { + for _, k := range AllMethods { + router.HandleFunc(k, registeredPath, handlersFn...) + } +} + +// if / then returns /*wildcard or /something then /something/*wildcard +// if empty then returns /*wildcard too +func validateWildcard(reqPath string, paramName string) string { + if reqPath[len(reqPath)-1] != slashByte { + reqPath += slash + } + reqPath += "*" + paramName + return reqPath +} + +func (router *Router) registerResourceRoute(reqPath string, h HandlerFunc) RouteInfo { + router.Head(reqPath, h) + return router.Get(reqPath, h) +} + +// StaticServe serves a directory as web resource +// it's the simpliest form of the Static* functions +// Almost same usage as StaticWeb +// accepts only one required parameter which is the systemPath ( the same path will be used to register the GET&HEAD routes) +// if second parameter is empty, otherwise the requestPath is the second parameter +// it uses gzip compression (compression on each request, no file cache) +func (router *Router) StaticServe(systemPath string, requestPath ...string) RouteInfo { + var reqPath string + + if len(requestPath) == 0 { + reqPath = strings.Replace(systemPath, fs.PathSeparator, slash, -1) // replaces any \ to / + reqPath = strings.Replace(reqPath, "//", slash, -1) // for any case, replaces // to / + reqPath = strings.Replace(reqPath, ".", "", -1) // replace any dots (./mypath -> /mypath) + } else { + reqPath = requestPath[0] + } + + return router.Get(reqPath+"/*file", func(ctx *Context) { + filepath := ctx.Param("file") + + spath := strings.Replace(filepath, "/", fs.PathSeparator, -1) + spath = path.Join(systemPath, spath) + + if !fs.DirectoryExists(spath) { + ctx.NotFound() + return + } + + if err := ctx.ServeFile(spath, true); err != nil { + ctx.EmitError(StatusInternalServerError) + } + }) +} + +// StaticContent serves bytes, memory cached, on the reqPath +// a good example of this is how the websocket server uses that to auto-register the /iris-ws.js +func (router *Router) StaticContent(reqPath string, cType string, content []byte) RouteInfo { // func(string) because we use that on websockets + modtime := time.Now() + h := func(ctx *Context) { + if err := ctx.SetClientCachedBody(StatusOK, content, cType, modtime); err != nil { + ctx.Log(DevMode, "error while serving []byte via StaticContent: ", err.Error()) + } + } + + return router.registerResourceRoute(reqPath, h) +} + +// StaticEmbedded used when files are distributed inside the app executable, using go-bindata mostly +// First parameter is the request path, the path which the files in the vdir will be served to, for example "/static" +// Second parameter is the (virtual) directory path, for example "./assets" +// Third parameter is the Asset function +// Forth parameter is the AssetNames function +// +// For more take a look at the +// example: https://github.com/iris-contrib/examples/tree/master/static_files_embedded +func (router *Router) StaticEmbedded(requestPath string, vdir string, assetFn func(name string) ([]byte, error), namesFn func() []string) RouteInfo { + paramName := "path" + requestPath = router.Context.Framework().policies.RouterReversionPolicy.WildcardPath(requestPath, paramName) + + if len(vdir) > 0 { + if vdir[0] == '.' { // first check for .wrong + vdir = vdir[1:] + } + if vdir[0] == '/' || vdir[0] == os.PathSeparator { // second check for /something, (or ./something if we had dot on 0 it will be removed + vdir = vdir[1:] + } + } + + // collect the names we are care for, because not all Asset used here, we need the vdir's assets. + allNames := namesFn() + + var names []string + for _, path := range allNames { + // check if path is the path name we care for + if !strings.HasPrefix(path, vdir) { + continue + } + + path = strings.Replace(path, "\\", "/", -1) // replace system paths with double slashes + path = strings.Replace(path, "./", "/", -1) // replace ./assets/favicon.ico to /assets/favicon.ico in order to be ready for compare with the reqPath later + path = path[len(vdir):] // set it as the its 'relative' ( we should re-setted it when assetFn will be used) + names = append(names, path) + + } + if len(names) == 0 { + // we don't start the server yet, so: + panic("iris.StaticEmbedded: Unable to locate any embedded files located to the (virtual) directory: " + vdir) + } + + modtime := time.Now() + h := func(ctx *Context) { + + reqPath := ctx.Param(paramName) + + for _, path := range names { + + if path != reqPath { + continue + } + + cType := fs.TypeByExtension(path) + fullpath := vdir + path + + buf, err := assetFn(fullpath) + + if err != nil { + continue + } + + if err := ctx.SetClientCachedBody(StatusOK, buf, cType, modtime); err != nil { + ctx.EmitError(StatusInternalServerError) + ctx.Log(DevMode, "error while serving via StaticEmbedded: ", err.Error()) + } + return + } + + // not found or error + ctx.EmitError(StatusNotFound) + + } + + return router.registerResourceRoute(requestPath, h) +} + +// Favicon serves static favicon +// accepts 2 parameters, second is optional +// favPath (string), declare the system directory path of the __.ico +// requestPath (string), it's the route's path, by default this is the "/favicon.ico" because some browsers tries to get this by default first, +// you can declare your own path if you have more than one favicon (desktop, mobile and so on) +// +// this func will add a route for you which will static serve the /yuorpath/yourfile.ico to the /yourfile.ico (nothing special that you can't handle by yourself) +// Note that you have to call it on every favicon you have to serve automatically (desktop, mobile and so on) +// +// panics on error +func (router *Router) Favicon(favPath string, requestPath ...string) RouteInfo { + f, err := os.Open(favPath) + if err != nil { + panic(errDirectoryFileNotFound.Format(favPath, err.Error())) + } + + // ignore error f.Close() + defer f.Close() + fi, _ := f.Stat() + if fi.IsDir() { // if it's dir the try to get the favicon.ico + fav := path.Join(favPath, "favicon.ico") + f, err = os.Open(fav) + if err != nil { + //we try again with .png + return router.Favicon(path.Join(favPath, "favicon.png")) + } + favPath = fav + fi, _ = f.Stat() + } + + cType := fs.TypeByExtension(favPath) + // copy the bytes here in order to cache and not read the ico on each request. + cacheFav := make([]byte, fi.Size()) + if _, err = f.Read(cacheFav); err != nil { + // Here we are before actually run the server. + // So we could panic but we don't, + // we just interrupt with a message + // to the (user-defined) logger. + router.Context.Framework().Log(DevMode, + errDirectoryFileNotFound. + Format(favPath, "favicon: couldn't read the data bytes for file: "+err.Error()). + Error()) + return nil + } + modtime := "" + h := func(ctx *Context) { + if modtime == "" { + modtime = fi.ModTime().UTC().Format(ctx.framework.Config.TimeFormat) + } + if t, err := time.Parse(ctx.framework.Config.TimeFormat, ctx.RequestHeader(ifModifiedSince)); err == nil && fi.ModTime().Before(t.Add(StaticCacheDuration)) { + + ctx.ResponseWriter.Header().Del(contentType) + ctx.ResponseWriter.Header().Del(contentLength) + ctx.SetStatusCode(StatusNotModified) + return + } + + ctx.ResponseWriter.Header().Set(contentType, cType) + ctx.ResponseWriter.Header().Set(lastModified, modtime) + ctx.SetStatusCode(StatusOK) + if _, err := ctx.Write(cacheFav); err != nil { + ctx.Log(DevMode, "error while trying to serve the favicon: %s", err.Error()) + } + } + + reqPath := "/favicon" + path.Ext(fi.Name()) //we could use the filename, but because standards is /favicon.ico/.png. + if len(requestPath) > 0 { + reqPath = requestPath[0] + } + + return router.registerResourceRoute(reqPath, h) +} + +// StaticHandler returns a new Handler which serves static files +func (router *Router) StaticHandler(reqPath string, systemPath string, showList bool, enableGzip bool, exceptRoutes ...RouteInfo) HandlerFunc { + // here we separate the path from the subdomain (if any), we care only for the path + // fixes a bug when serving static files via a subdomain + fullpath := router.relativePath + reqPath + path := fullpath + if dotWSlashIdx := strings.Index(path, subdomainIndicator); dotWSlashIdx > 0 { + path = fullpath[dotWSlashIdx+1:] + } + + h := NewStaticHandlerBuilder(systemPath). + Path(path). + Listing(showList). + Gzip(enableGzip). + Except(exceptRoutes...). + Build() + + managedStaticHandler := func(ctx *Context) { + h(ctx) + prevStatusCode := ctx.ResponseWriter.StatusCode() + if prevStatusCode >= 400 { // we have an error + // fire the custom error handler + router.Errors.Fire(prevStatusCode, ctx) + } + // go to the next middleware + if ctx.Pos < len(ctx.Middleware)-1 { + ctx.Next() + } + } + return managedStaticHandler +} + +// StaticWeb returns a handler that serves HTTP requests +// with the contents of the file system rooted at directory. +// +// first parameter: the route path +// second parameter: the system directory +// third OPTIONAL parameter: the exception routes +// (= give priority to these routes instead of the static handler) +// for more options look iris.StaticHandler. +// +// iris.StaticWeb("/static", "./static") +// +// As a special case, the returned file server redirects any request +// ending in "/index.html" to the same path, without the final +// "index.html". +// +// StaticWeb calls the StaticHandler(reqPath, systemPath, listingDirectories: false, gzip: false ). +func (router *Router) StaticWeb(reqPath string, systemPath string, exceptRoutes ...RouteInfo) RouteInfo { + h := router.StaticHandler(reqPath, systemPath, false, false, exceptRoutes...) + routePath := validateWildcard(reqPath, "file") + return router.registerResourceRoute(routePath, h) +} + +// Layout oerrides the parent template layout with a more specific layout for this Party +// returns this Party, to continue as normal +// example: +// my := iris.Default.Party("/my").Layout("layouts/mylayout.html") +// { +// my.Get("/", func(ctx *iris.Context) { +// ctx.MustRender("page1.html", nil) +// }) +// } +// +func (router *Router) Layout(tmplLayoutFile string) *Router { + router.UseFunc(func(ctx *Context) { + ctx.Set(TemplateLayoutContextKey, tmplLayoutFile) + ctx.Next() + }) + + return router +} + +// OnError registers a custom http error handler +func (router *Router) OnError(statusCode int, handlerFn HandlerFunc) { + staticPath := router.Context.Framework().policies.RouterReversionPolicy.StaticPath(router.relativePath) + + if staticPath == "/" { + router.Errors.Register(statusCode, handlerFn) // register the user-specific error message, as the global error handler, for now. + return + } + + // after this, we have more than one error handler for one status code, and that's dangerous some times, but use it for non-globals error catching by your own risk + // NOTES: + // subdomains error will not work if same path of a non-subdomain (maybe a TODO for later) + // errors for parties should be registered from the biggest path length to the smaller. + + // get the previous + prevErrHandler := router.Errors.GetOrRegister(statusCode) + + func(statusCode int, staticPath string, prevErrHandler Handler, newHandler Handler) { // to separate the logic + errHandler := HandlerFunc(func(ctx *Context) { + if strings.HasPrefix(ctx.Path(), staticPath) { // yes the user should use OnError from longest to lower static path's length in order this to work, so we can find another way, like a builder on the end. + newHandler.Serve(ctx) + return + } + // serve with the user-specific global ("/") pure iris.OnError receiver Handler or the standar handler if OnError called only from inside a no-relative Party. + prevErrHandler.Serve(ctx) + }) + + router.Errors.Register(statusCode, errHandler) + }(statusCode, staticPath, prevErrHandler, handlerFn) + +} + +// EmitError fires a custom http error handler to the client +// +// if no custom error defined with this statuscode, then iris creates one, and once at runtime +func (router *Router) EmitError(statusCode int, ctx *Context) { + router.Errors.Fire(statusCode, ctx) +} diff --git a/router_policy_test.go b/router_policy_test.go new file mode 100644 index 00000000..0f2d04df --- /dev/null +++ b/router_policy_test.go @@ -0,0 +1,189 @@ +package iris_test + +import ( + "net/http" + "testing" + + . "gopkg.in/kataras/iris.v6" + "gopkg.in/kataras/iris.v6/httptest" +) + +// This will be removed at the final release +// it's here just for my tests +// it will may transfered to the (advanced) examples repository +// in order to show the users how they can adapt any third-party router. +// They can also view the ./adaptors/httprouter and ./adaptors/gorillamux. +func newTestNativeRouter() Policies { + fireMethodNotAllowed := false + return Policies{ + EventPolicy: EventPolicy{ + Boot: func(s *Framework) { + fireMethodNotAllowed = s.Config.FireMethodNotAllowed + }, + }, + RouterReversionPolicy: RouterReversionPolicy{ + // path normalization done on iris' side + StaticPath: func(path string) string { return path }, + WildcardPath: func(requestPath string, paramName string) string { return requestPath }, + URLPath: func(r RouteInfo, args ...string) string { + if r == nil { + return "" + } + // note: + // as we already know, net/http servemux doesn't provides parameterized paths so we will + // use the passed args(if any) to build the url query + path := r.Path() + if len(args) > 0 { + if len(args)%2 != 0 { + // key=value + // so the result of len args should be %2==0. + // if not return just the path. + return path + } + path += "?" + for i := 0; i < len(args)-1; i++ { + path += args[i] + "=" + args[i+1] + i++ + if i != len(args)-1 { + path += "&" + } + + } + } + return path + }, + RouteContextLinker: func(r RouteInfo, ctx *Context) { + if r == nil { + return + } + ctx.Middleware = r.Middleware() + }, + }, + RouterBuilderPolicy: func(repo RouteRepository, context ContextPool) http.Handler { + servemux := http.NewServeMux() + noIndexRegistered := true + servemux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { + if noIndexRegistered { + context.Run(w, r, func(ctx *Context) { + ctx.EmitError(StatusNotFound) + }) + } + }) + repo.Visit(func(route RouteInfo) { + path := route.Path() + if path == "/" { + noIndexRegistered = false // this goes before the handlefunc("/") + } + if path[len(path)-1] != '/' { // append a slash (net/http works this way) + path += "/" + repo.ChangePath(route, path) + } + servemux.HandleFunc(path, func(w http.ResponseWriter, r *http.Request) { + ctx := context.Acquire(w, r) + + if ctx.Method() == route.Method() || ctx.Method() == MethodOptions && route.HasCors() { + ctx.Middleware = route.Middleware() + recorder := ctx.Recorder() + ctx.Do() + + // ok, we can't bypass the net/http server.go's err handlers + // we have two options: + // - create the mux by ourselve, not an ideal because we already done two of them. + // - create a new response writer which will check once if user has registered error handler,if yes write that response instead. + // - on "/" path(which net/http fallbacks if no any registered route handler found) make if requested_path != "/" or "" + // and emit the 404 error, but for the rest of the custom errors...? + // - use our custom context's recorder to record the status code, this will be a bit slower solution(maybe not) + // but it covers all our scenarios. + + statusCode := recorder.StatusCode() + if statusCode >= 400 { // if we have an error status code try to find a custom error handler + errorHandler := ctx.Framework().Errors.Get(statusCode) + if errorHandler != nil { + // it will reset the response and write its own response by the user's handler + errorHandler.Serve(ctx) + } + } + } else if fireMethodNotAllowed { + ctx.EmitError(StatusMethodNotAllowed) // fire method not allowed if enabled + } else { // else fire not found + ctx.EmitError(StatusNotFound) + } + + context.Release(ctx) + }) + }) + + return servemux + }, + } +} + +func h(ctx *Context) { + ctx.WriteString("hello from " + ctx.Path()) +} + +type testNativeRoute struct { + method, path, body string + status int +} + +func TestRouterPolicyAdaptor(t *testing.T) { + expectedWrongMethodStatus := StatusNotFound + app := New(Configuration{FireMethodNotAllowed: true}) + app.Adapt(newTestNativeRouter()) + // 404 or 405 + if app.Config.FireMethodNotAllowed { + expectedWrongMethodStatus = StatusMethodNotAllowed + } + + var testRoutes = []testNativeRoute{ + {"GET", "/hi", "Should be /hi/ \nhello from /hi/", StatusOK}, + {"GET", "/other", "Should be /other/ \nhello from /other/", StatusOK}, + {"GET", "/hi/you", "Should be /hi/you/ \nhello from /hi/you/", StatusOK}, + {"POST", "/hey", "hello from /hey/", StatusOK}, + {"GET", "/hey", "Method Not Allowed", expectedWrongMethodStatus}, + {"GET", "/doesntexists", "Custom 404 page", StatusNotFound}, + } + app.OnError(404, func(ctx *Context) { + ctx.HTML(404, "Custom 404 page") + }) + + app.Get("/hi", func(ctx *Context) { + ctx.Writef("Should be /hi/ \n") + ctx.Next() + }, h) + + app.Get("/other", func(ctx *Context) { + ctx.Writef("Should be /other/ \n") + ctx.Next() + }, h) + + app.Get("/hi/you", func(ctx *Context) { + ctx.Writef("Should be /hi/you/ \n") + ctx.Next() + }, h) + + app.Post("/hey", h) + + app.None("/profile", func(ctx *Context) { + userid, _ := ctx.URLParamInt("user_id") + ref := ctx.URLParam("ref") + ctx.Writef("%s\n%d", userid, ref) + }).ChangeName("profile") + + e := httptest.New(app, t) + expected := "/profile/?user_id=42&ref=iris-go&something=anything" + + if got := app.Path("profile", "user_id", 42, "ref", "iris-go", "something", "anything"); got != expected { + t.Fatalf("URLPath expected %s but got %s", expected, got) + } + + for _, r := range testRoutes { + // post should be passed with ending / here, it's not iris-specific. + if r.method == "POST" { + r.path += "/" + } + e.Request(r.method, r.path).Expect().Status(r.status).Body().Equal(r.body).Raw() + } + +} diff --git a/status.go b/status.go new file mode 100644 index 00000000..ac3376be --- /dev/null +++ b/status.go @@ -0,0 +1,215 @@ +package iris + +import ( + "sync" +) + +// HTTP status codes. +const ( + StatusContinue = 100 // RFC 7231, 6.2.1 + StatusSwitchingProtocols = 101 // RFC 7231, 6.2.2 + StatusProcessing = 102 // RFC 2518, 10.1 + + StatusOK = 200 // RFC 7231, 6.3.1 + StatusCreated = 201 // RFC 7231, 6.3.2 + StatusAccepted = 202 // RFC 7231, 6.3.3 + StatusNonAuthoritativeInfo = 203 // RFC 7231, 6.3.4 + StatusNoContent = 204 // RFC 7231, 6.3.5 + StatusResetContent = 205 // RFC 7231, 6.3.6 + StatusPartialContent = 206 // RFC 7233, 4.1 + StatusMultiStatus = 207 // RFC 4918, 11.1 + StatusAlreadyReported = 208 // RFC 5842, 7.1 + StatusIMUsed = 226 // RFC 3229, 10.4.1 + + StatusMultipleChoices = 300 // RFC 7231, 6.4.1 + StatusMovedPermanently = 301 // RFC 7231, 6.4.2 + StatusFound = 302 // RFC 7231, 6.4.3 + StatusSeeOther = 303 // RFC 7231, 6.4.4 + StatusNotModified = 304 // RFC 7232, 4.1 + StatusUseProxy = 305 // RFC 7231, 6.4.5 + _ = 306 // RFC 7231, 6.4.6 (Unused) + StatusTemporaryRedirect = 307 // RFC 7231, 6.4.7 + StatusPermanentRedirect = 308 // RFC 7538, 3 + + StatusBadRequest = 400 // RFC 7231, 6.5.1 + StatusUnauthorized = 401 // RFC 7235, 3.1 + StatusPaymentRequired = 402 // RFC 7231, 6.5.2 + StatusForbidden = 403 // RFC 7231, 6.5.3 + StatusNotFound = 404 // RFC 7231, 6.5.4 + StatusMethodNotAllowed = 405 // RFC 7231, 6.5.5 + StatusNotAcceptable = 406 // RFC 7231, 6.5.6 + StatusProxyAuthRequired = 407 // RFC 7235, 3.2 + StatusRequestTimeout = 408 // RFC 7231, 6.5.7 + StatusConflict = 409 // RFC 7231, 6.5.8 + StatusGone = 410 // RFC 7231, 6.5.9 + StatusLengthRequired = 411 // RFC 7231, 6.5.10 + StatusPreconditionFailed = 412 // RFC 7232, 4.2 + StatusRequestEntityTooLarge = 413 // RFC 7231, 6.5.11 + StatusRequestURITooLong = 414 // RFC 7231, 6.5.12 + StatusUnsupportedMediaType = 415 // RFC 7231, 6.5.13 + StatusRequestedRangeNotSatisfiable = 416 // RFC 7233, 4.4 + StatusExpectationFailed = 417 // RFC 7231, 6.5.14 + StatusTeapot = 418 // RFC 7168, 2.3.3 + StatusUnprocessableEntity = 422 // RFC 4918, 11.2 + StatusLocked = 423 // RFC 4918, 11.3 + StatusFailedDependency = 424 // RFC 4918, 11.4 + StatusUpgradeRequired = 426 // RFC 7231, 6.5.15 + StatusPreconditionRequired = 428 // RFC 6585, 3 + StatusTooManyRequests = 429 // RFC 6585, 4 + StatusRequestHeaderFieldsTooLarge = 431 // RFC 6585, 5 + StatusUnavailableForLegalReasons = 451 // RFC 7725, 3 + + StatusInternalServerError = 500 // RFC 7231, 6.6.1 + StatusNotImplemented = 501 // RFC 7231, 6.6.2 + StatusBadGateway = 502 // RFC 7231, 6.6.3 + StatusServiceUnavailable = 503 // RFC 7231, 6.6.4 + StatusGatewayTimeout = 504 // RFC 7231, 6.6.5 + StatusHTTPVersionNotSupported = 505 // RFC 7231, 6.6.6 + StatusVariantAlsoNegotiates = 506 // RFC 2295, 8.1 + StatusInsufficientStorage = 507 // RFC 4918, 11.5 + StatusLoopDetected = 508 // RFC 5842, 7.2 + StatusNotExtended = 510 // RFC 2774, 7 + StatusNetworkAuthenticationRequired = 511 // RFC 6585, 6 +) + +var statusText = map[int]string{ + StatusContinue: "Continue", + StatusSwitchingProtocols: "Switching Protocols", + StatusProcessing: "Processing", + + StatusOK: "OK", + StatusCreated: "Created", + StatusAccepted: "Accepted", + StatusNonAuthoritativeInfo: "Non-Authoritative Information", + StatusNoContent: "No Content", + StatusResetContent: "Reset Content", + StatusPartialContent: "Partial Content", + StatusMultiStatus: "Multi-Status", + StatusAlreadyReported: "Already Reported", + StatusIMUsed: "IM Used", + + StatusMultipleChoices: "Multiple Choices", + StatusMovedPermanently: "Moved Permanently", + StatusFound: "Found", + StatusSeeOther: "See Other", + StatusNotModified: "Not Modified", + StatusUseProxy: "Use Proxy", + StatusTemporaryRedirect: "Temporary Redirect", + StatusPermanentRedirect: "Permanent Redirect", + + StatusBadRequest: "Bad Request", + StatusUnauthorized: "Unauthorized", + StatusPaymentRequired: "Payment Required", + StatusForbidden: "Forbidden", + StatusNotFound: "Not Found", + StatusMethodNotAllowed: "Method Not Allowed", + StatusNotAcceptable: "Not Acceptable", + StatusProxyAuthRequired: "Proxy Authentication Required", + StatusRequestTimeout: "Request Timeout", + StatusConflict: "Conflict", + StatusGone: "Gone", + StatusLengthRequired: "Length Required", + StatusPreconditionFailed: "Precondition Failed", + StatusRequestEntityTooLarge: "Request Entity Too Large", + StatusRequestURITooLong: "Request URI Too Long", + StatusUnsupportedMediaType: "Unsupported Media Type", + StatusRequestedRangeNotSatisfiable: "Requested Range Not Satisfiable", + StatusExpectationFailed: "Expectation Failed", + StatusTeapot: "I'm a teapot", + StatusUnprocessableEntity: "Unprocessable Entity", + StatusLocked: "Locked", + StatusFailedDependency: "Failed Dependency", + StatusUpgradeRequired: "Upgrade Required", + StatusPreconditionRequired: "Precondition Required", + StatusTooManyRequests: "Too Many Requests", + StatusRequestHeaderFieldsTooLarge: "Request Header Fields Too Large", + StatusUnavailableForLegalReasons: "Unavailable For Legal Reasons", + + StatusInternalServerError: "Internal Server Error", + StatusNotImplemented: "Not Implemented", + StatusBadGateway: "Bad Gateway", + StatusServiceUnavailable: "Service Unavailable", + StatusGatewayTimeout: "Gateway Timeout", + StatusHTTPVersionNotSupported: "HTTP Version Not Supported", + StatusVariantAlsoNegotiates: "Variant Also Negotiates", + StatusInsufficientStorage: "Insufficient Storage", + StatusLoopDetected: "Loop Detected", + StatusNotExtended: "Not Extended", + StatusNetworkAuthenticationRequired: "Network Authentication Required", +} + +// StatusText returns a text for the HTTP status code. It returns the empty +// string if the code is unknown. +func StatusText(code int) string { + return statusText[code] +} + +// ErrorHandlers contains all custom http errors. +// A custom http error handler is just a handler with its status code. +type ErrorHandlers struct { + // Handlers the map which actually contains the errors. + // Use the declared functions to get, set or fire an error. + handlers map[int]Handler + mu sync.RWMutex +} + +// Register registers a handler to a http status +func (e *ErrorHandlers) Register(statusCode int, handler Handler) { + e.mu.Lock() + if e.handlers == nil { + e.handlers = make(map[int]Handler) + } + func(statusCode int, handler Handler) { + e.handlers[statusCode] = HandlerFunc(func(ctx *Context) { + if w, ok := ctx.IsRecording(); ok { + w.Reset() + } + ctx.SetStatusCode(statusCode) + handler.Serve(ctx) + }) + }(statusCode, handler) + e.mu.Unlock() +} + +// Get returns the handler which is responsible for +// this 'statusCode' http error. +func (e *ErrorHandlers) Get(statusCode int) Handler { + e.mu.RLock() + h := e.handlers[statusCode] + e.mu.RUnlock() + if h == nil { + return nil + } + return h +} + +// GetOrRegister trys to return the handler which is responsible +// for the 'statusCode', if it was nil then it creates +// a new one, registers that to the error list and returns that. +func (e *ErrorHandlers) GetOrRegister(statusCode int) Handler { + h := e.Get(statusCode) + if h == nil { + // create a new one + h = HandlerFunc(func(ctx *Context) { + if w, ok := ctx.IsRecording(); ok { + w.Reset() + } + ctx.SetStatusCode(statusCode) + if _, err := ctx.WriteString(statusText[statusCode]); err != nil { + ctx.Log(DevMode, "error from a pre-defined error handler while trying to send an http error: %s", + err.Error()) + } + }) + e.mu.Lock() + e.handlers[statusCode] = h + e.mu.Unlock() + } + + return h +} + +// Fire fires an error based on the `statusCode` +func (e *ErrorHandlers) Fire(statusCode int, ctx *Context) { + h := e.GetOrRegister(statusCode) + h.Serve(ctx) +} diff --git a/template.go b/template.go deleted file mode 100644 index bcd719e7..00000000 --- a/template.go +++ /dev/null @@ -1,137 +0,0 @@ -package iris - -import ( - "io" - - "github.com/kataras/go-fs" - "github.com/kataras/go-template" -) - -const ( - // NoLayout to disable layout for a particular template file - NoLayout = template.NoLayout - // TemplateLayoutContextKey is the name of the user values which can be used to set a template layout from a middleware and override the parent's - TemplateLayoutContextKey = "templateLayout" -) - -type ( - // RenderOptions is a helper type for the optional runtime options can be passed by user when Render - // an example of this is the "layout" or "gzip" option - // same as Map but more specific name - RenderOptions map[string]interface{} - - // PreRender is typeof func(*iris.Context, string, interface{},...map[string]interface{}) bool - // PreRenders helps developers to pass middleware between - // the route Handler and a context.Render (THAT can render 'rest' types also but the PreRender applies ONLY to template rendering(file or source)) call - // all parameter receivers can be changed before passing it to the actual context's Render - // so, you can change the filenameOrSource, the page binding, the options, and even add cookies, session value or a flash message through ctx - // the return value of a PreRender is a boolean, if returns false then the next PreRender will not be executed, keep note - // that the actual context's Render will be called at any case. - // - // Example: https://github.com/iris-contrib/examples/tree/master/template_engines/template_prerender - PreRender func(ctx *Context, filenameOrSource string, binding interface{}, options ...map[string]interface{}) bool -) - -// templateEngines just a wrapper of template.Mux in order to use it's execute without break the whole of the API -type templateEngines struct { - *template.Mux - prerenders []PreRender -} - -func newTemplateEngines(sharedFuncs map[string]interface{}) *templateEngines { - return &templateEngines{Mux: template.NewMux(sharedFuncs)} -} - -// getGzipOption receives a default value and the render options map and returns if gzip is enabled for this render action -func getGzipOption(defaultValue bool, options map[string]interface{}) bool { - gzipOpt := options["gzip"] // we only need that, so don't create new map to keep the options. - if b, isBool := gzipOpt.(bool); isBool { - return b - } - return defaultValue -} - -// gtCharsetOption receives a default value and the render options map and returns the correct charset for this render action -func getCharsetOption(defaultValue string, options map[string]interface{}) string { - charsetOpt := options["charset"] - if s, isString := charsetOpt.(string); isString { - return s - } - return defaultValue -} - -func (t *templateEngines) usePreRender(pre PreRender) { - t.prerenders = append(t.prerenders, pre) -} - -// render executes a template and write its result to the context's body -// options are the optional runtime options can be passed by user and catched by the template engine when render -// an example of this is the "layout" -// note that gzip option is an iris dynamic option which exists for all template engines -// the gzip and charset options are built'n with iris -// template is passed as file or souce -func (t *templateEngines) render(isFile bool, ctx *Context, filenameOrSource string, binding interface{}, options []map[string]interface{}) error { - if ctx.framework.Config.DisableTemplateEngines { - return errTemplateExecute.Format("Templates are disabled '.Config.DisableTemplatesEngines = true' please turn that to false, as defaulted.") - } - - if len(t.prerenders) > 0 { - - for i := range t.prerenders { - - // I'm not making any checks here for performance reasons, means that - // if binding is pointer it can be changed, otherwise not. - - if shouldContinue := t.prerenders[i](ctx, filenameOrSource, binding, options...); !shouldContinue { - break - } - } - } - - // we do all these because we don't want to initialize a new map for each execution... - gzipEnabled := ctx.framework.Config.Gzip - charset := ctx.framework.Config.Charset - if len(options) > 0 { - gzipEnabled = getGzipOption(gzipEnabled, options[0]) - charset = getCharsetOption(charset, options[0]) - } - - if isFile { - ctxLayout := ctx.GetString(TemplateLayoutContextKey) - if ctxLayout != "" { - if len(options) > 0 { - options[0]["layout"] = ctxLayout - } else { - options = []map[string]interface{}{{"layout": ctxLayout}} - } - } - } - - ctx.SetContentType(contentHTML + "; charset=" + charset) - - var out io.Writer - if gzipEnabled && ctx.clientAllowsGzip() { - ctx.ResponseWriter.Header().Add(varyHeader, acceptEncodingHeader) - ctx.SetHeader(contentEncodingHeader, "gzip") - - gzipWriter := fs.AcquireGzipWriter(ctx.ResponseWriter) - defer fs.ReleaseGzipWriter(gzipWriter) - out = gzipWriter - } else { - out = ctx.ResponseWriter - } - - if isFile { - return t.ExecuteWriter(out, filenameOrSource, binding, options...) - } - return t.ExecuteRaw(filenameOrSource, out, binding) - -} - -func (t *templateEngines) renderFile(ctx *Context, filename string, binding interface{}, options ...map[string]interface{}) error { - return t.render(true, ctx, filename, binding, options) -} - -func (t *templateEngines) renderSource(ctx *Context, src string, binding interface{}, options ...map[string]interface{}) error { - return t.render(false, ctx, src, binding, options) -} diff --git a/transactions.go b/transaction.go similarity index 100% rename from transactions.go rename to transaction.go diff --git a/utils/bytes.go b/utils/bytes.go deleted file mode 100644 index c46e9bd4..00000000 --- a/utils/bytes.go +++ /dev/null @@ -1,40 +0,0 @@ -package utils - -import ( - "bytes" -) - -// BufferPool implements a pool of bytes.Buffers in the form of a bounded channel. -// Pulled from the github.com/oxtoacart/bpool package (Apache licensed). -type BufferPool struct { - c chan *bytes.Buffer -} - -// NewBufferPool creates a new BufferPool bounded to the given size. -func NewBufferPool(size int) (bp *BufferPool) { - return &BufferPool{ - c: make(chan *bytes.Buffer, size), - } -} - -// Get gets a Buffer from the BufferPool, or creates a new one if none are -// available in the pool. -func (bp *BufferPool) Get() (b *bytes.Buffer) { - select { - case b = <-bp.c: - // reuse existing buffer - default: - // create new buffer - b = bytes.NewBuffer([]byte{}) - } - return -} - -// Put returns the given Buffer to the BufferPool. -func (bp *BufferPool) Put(b *bytes.Buffer) { - b.Reset() - select { - case bp.c <- b: - default: // Discard the buffer if the pool is full. - } -} diff --git a/utils/installer.go b/utils/installer.go deleted file mode 100644 index cdb540a3..00000000 --- a/utils/installer.go +++ /dev/null @@ -1,29 +0,0 @@ -package utils - -import ( - "os" - "runtime" -) - -const ( - // ContentBINARY is the string of "application/octet-stream response headers - ContentBINARY = "application/octet-stream" -) - -var ( - // AssetsDirectory is the path which iris saves some assets came from the internet used mostly from iris control plugin (to download the html,css,js) - AssetsDirectory = "" -) - -// init just sets the iris path for assets, used in iris control plugin and GOPATH for iris command line tool(create command) -// the AssetsDirectory path should be like: C:/users/kataras/.iris (for windows) and for linux you can imagine -func init() { - homepath := "" - if runtime.GOOS == "windows" { - homepath = os.Getenv("HOMEDRIVE") + os.Getenv("HOMEPATH") - } else { - homepath = os.Getenv("HOME") - } - AssetsDirectory = homepath + PathSeparator + ".iris" - -} diff --git a/websocket.go b/websocket.go index e6cab4fa..d246f372 100644 --- a/websocket.go +++ b/websocket.go @@ -56,9 +56,9 @@ func (ws *WebsocketServer) init() { clientSideLookupName := "iris-websocket-client-side" ws.station.Get(c.Endpoint, ToHandler(ws.Server.Handler())) // check if client side already exists - if ws.station.Lookup(clientSideLookupName) == nil { + if ws.station.Routes().Lookup(clientSideLookupName) == nil { // serve the client side on domain:port/iris-ws.js - ws.station.StaticContent("/iris-ws.js", contentJavascript, websocket.ClientSource)(clientSideLookupName) + ws.station.StaticContent("/iris-ws.js", contentJavascript, websocket.ClientSource).ChangeName(clientSideLookupName) } if c.CheckOrigin == nil { @@ -78,9 +78,9 @@ func (ws *WebsocketServer) init() { ReadBufferSize: c.ReadBufferSize, WriteBufferSize: c.WriteBufferSize, Error: func(w http.ResponseWriter, r *http.Request, status int, reason error) { - ctx := ws.station.AcquireCtx(w, r) - c.Error(ctx, status, reason) - ws.station.ReleaseCtx(ctx) + ws.station.Context.Run(w, r, func(ctx *Context) { + c.Error(ctx, status, reason) + }) }, CheckOrigin: c.CheckOrigin, IDGenerator: c.IDGenerator,