diff --git a/Gopkg.lock b/Gopkg.lock
index 112390bc..09d3684c 100644
--- a/Gopkg.lock
+++ b/Gopkg.lock
@@ -289,6 +289,12 @@
packages = ["internal","redis"]
revision = "2cd21d9966bf7ff9ae091419744f0b3fb0fecace"
+[[projects]]
+ branch = "master"
+ name = "github.com/hashicorp/go-version"
+ packages = ["."]
+ revision = "b5a281d3160aa11950a6182bd9a9dc2cb1e02d50"
+
[solve-meta]
analyzer-name = "dep"
analyzer-version = 1
diff --git a/Gopkg.toml b/Gopkg.toml
index c757d7a7..899be2c4 100644
--- a/Gopkg.toml
+++ b/Gopkg.toml
@@ -89,3 +89,7 @@
[[constraint]]
branch = "master"
name = "github.com/gomodule/redigo"
+
+[[constraint]]
+ name = "github.com/hashicorp/go-version"
+ version = "1.0.0"
\ No newline at end of file
diff --git a/HISTORY.md b/HISTORY.md
index 6a21f38c..ef56dacc 100644
--- a/HISTORY.md
+++ b/HISTORY.md
@@ -17,6 +17,155 @@ Developers are not forced to upgrade if they don't really need it. Upgrade whene
**How to upgrade**: Open your command-line and execute this command: `go get -u github.com/kataras/iris` or let the automatic updater do that for you.
+# Su, 18 November 2018 | v11.1.0
+
+PR: https://github.com/kataras/iris/pull/1130
+
+This release contains a new feature for versioning your Iris APIs. The initial motivation and feature request came by https://github.com/kataras/iris/issues/1129.
+
+The [versioning](https://github.com/kataras/iris/tree/master/versioning) package provides [semver](https://semver.org/) versioning for your APIs. It implements all the suggestions written at [api-guidelines](https://github.com/byrondover/api-guidelines/blob/master/Guidelines.md#versioning) and more.
+
+
+The version comparison is done by the [go-version](https://github.com/hashicorp/go-version) package. It supports matching over patterns like `">= 1.0, < 3"` and etc.
+
+## Features
+
+- per route version matching, a normal iris handler with "switch" cases via Map for version => handler
+- per group versioned routes and deprecation API
+- version matching like ">= 1.0, < 2.0" or just "2.0.1" and etc.
+- version not found handler (can be customized by simply adding the versioning.NotFound: customNotMatchVersionHandler on the Map)
+- version is retrieved from the "Accept" and "Accept-Version" headers (can be customized via middleware)
+- respond with "X-API-Version" header, if version found.
+- deprecation options with customizable "X-API-Warn", "X-API-Deprecation-Date", "X-API-Deprecation-Info" headers via `Deprecated` wrapper.
+
+## Get version
+
+Current request version is retrieved by `versioning.GetVersion(ctx)`.
+
+By default the `GetVersion` will try to read from:
+- `Accept` header, i.e `Accept: "application/json; version=1.0"`
+- `Accept-Version` header, i.e `Accept-Version: "1.0"`
+
+You can also set a custom version for a handler via a middleware by using the context's store values.
+For example:
+```go
+func(ctx iris.Context) {
+ ctx.Values().Set(versioning.Key, ctx.URLParamDefault("version", "1.0"))
+ ctx.Next()
+}
+```
+
+## Match version to handler
+
+The `versioning.NewMatcher(versioning.Map) iris.Handler` creates a single handler which decides what handler need to be executed based on the requested version.
+
+```go
+app := iris.New()
+
+// middleware for all versions.
+myMiddleware := func(ctx iris.Context) {
+ // [...]
+ ctx.Next()
+}
+
+myCustomNotVersionFound := func(ctx iris.Context) {
+ ctx.StatusCode(404)
+ ctx.Writef("%s version not found", versioning.GetVersion(ctx))
+}
+
+userAPI := app.Party("/api/user")
+userAPI.Get("/", myMiddleware, versioning.NewMatcher(versioning.Map{
+ "1.0": sendHandler(v10Response),
+ ">= 2, < 3": sendHandler(v2Response),
+ versioning.NotFound: myCustomNotVersionFound,
+}))
+```
+
+### Deprecation
+
+Using the `versioning.Deprecated(handler iris.Handler, options versioning.DeprecationOptions) iris.Handler` function you can mark a specific handler version as deprecated.
+
+
+```go
+v10Handler := versioning.Deprecated(sendHandler(v10Response), versioning.DeprecationOptions{
+ // if empty defaults to: "WARNING! You are using a deprecated version of this API."
+ WarnMessage string
+ DeprecationDate time.Time
+ DeprecationInfo string
+})
+
+userAPI.Get("/", versioning.NewMatcher(versioning.Map{
+ "1.0": v10Handler,
+ // [...]
+}))
+```
+
+This will make the handler to send these headers to the client:
+
+- `"X-API-Warn": options.WarnMessage`
+- `"X-API-Deprecation-Date": context.FormatTime(ctx, options.DeprecationDate))`
+- `"X-API-Deprecation-Info": options.DeprecationInfo`
+
+> versioning.DefaultDeprecationOptions can be passed instead if you don't care about Date and Info.
+
+## Grouping routes by version
+
+Grouping routes by version is possible as well.
+
+Using the `versioning.NewGroup(version string) *versioning.Group` function you can create a group to register your versioned routes.
+The `versioning.RegisterGroups(r iris.Party, versionNotFoundHandler iris.Handler, groups ...*versioning.Group)` must be called in the end in order to register the routes to a specific `Party`.
+
+```go
+app := iris.New()
+
+userAPI := app.Party("/api/user")
+// [... static serving, middlewares and etc goes here].
+
+userAPIV10 := versioning.NewGroup("1.0")
+userAPIV10.Get("/", sendHandler(v10Response))
+
+userAPIV2 := versioning.NewGroup(">= 2, < 3")
+userAPIV2.Get("/", sendHandler(v2Response))
+userAPIV2.Post("/", sendHandler(v2Response))
+userAPIV2.Put("/other", sendHandler(v2Response))
+
+versioning.RegisterGroups(userAPI, versioning.NotFoundHandler, userAPIV10, userAPIV2)
+```
+
+> A middleware can be registered to the actual `iris.Party` only, using the methods we learnt above, i.e by using the `versioning.Match` in order to detect what code/handler you want to be executed when "x" or no version is requested.
+
+### Deprecation for Group
+
+Just call the `Deprecated(versioning.DeprecationOptions)` on the group you want to notify your API consumers that this specific version is deprecated.
+
+```go
+userAPIV10 := versioning.NewGroup("1.0").Deprecated(versioning.DefaultDeprecationOptions)
+```
+
+## Compare version manually from inside your handlers
+
+```go
+// reports if the "version" is matching to the "is".
+// the "is" can be a constraint like ">= 1, < 3".
+If(version string, is string) bool
+```
+
+```go
+// same as `If` but expects a Context to read the requested version.
+Match(ctx iris.Context, expectedVersion string) bool
+```
+
+```go
+app.Get("/api/user", func(ctx iris.Context) {
+ if versioning.Match(ctx, ">= 2.2.3") {
+ // [logic for >= 2.2.3 version of your handler goes here]
+ return
+ }
+})
+```
+
+Example can be found [here](_examples/versioning/main.go).
+
# Fr, 09 November 2018 | v11.0.4
Add `Configuration.DisablePathCorrectionRedirection` - `iris.WithoutPathCorrectionRedirection` to support
@@ -119,7 +268,7 @@ For the craziest of us, click [here](https://github.com/kataras/iris/compare/v10
## Routing
-I wrote a [new router implementation](https://github.com/kataras/muxie#philosophy) for our Iris internal(low-level) routing mechanism, it is good to know that this was the second time we have updated the router internals without a single breaking change after the v6, thanks to the very well-writen and designed-first code we have for the high-level path syntax component called [macro interpreter](macro/interpreter).
+I wrote a [new router implementation](https://github.com/kataras/muxie#philosophy) for our Iris internal(low-level) routing mechanism, it is good to know that this was the second time we have updated the router internals without a single breaking change after the v6, thanks to the very well-written and designed-first code we have for the high-level path syntax component called [macro interpreter](macro/interpreter).
The new router supports things like **closest wildcard resolution**.
diff --git a/HISTORY_GR.md b/HISTORY_GR.md
index c16bbf7b..021ee5ad 100644
--- a/HISTORY_GR.md
+++ b/HISTORY_GR.md
@@ -17,6 +17,10 @@
**Πώς να αναβαθμίσετε**: Ανοίξτε την γραμμή εντολών σας και εκτελέστε αυτήν την εντολή: `go get -u github.com/kataras/iris` ή αφήστε το αυτόματο updater να το κάνει αυτό για σας.
+# Su, 18 November 2018 | v11.1.0
+
+Πατήστε [εδώ](https://github.com/kataras/iris/blob/master/HISTORY.md#su-18-november-2018--v1110) για να διαβάσετε στα αγγλικά για το νέο "versioning" feature.
+
# Fr, 09 November 2018 | v11.0.4
Πατήστε [εδώ](https://github.com/kataras/iris/blob/master/HISTORY.md#fr-09-november-2018--v1104) για να διαβάσετε στα αγγλικά τις αλλαγές που φέρνει το τελευταίο patch για την έκδοση 11.
diff --git a/HISTORY_ID.md b/HISTORY_ID.md
index ed48b3a2..414dc195 100644
--- a/HISTORY_ID.md
+++ b/HISTORY_ID.md
@@ -17,6 +17,10 @@ Developers tidak diwajibkan untuk melakukan upgrade apabila mereka tidak membutu
**Cara Upgrade**: Bukan command-line anda dan eksekuis perintah ini: `go get -u github.com/kataras/iris` atau biarkan updater otomatis melakukannya untuk anda.
+# Su, 18 November 2018 | v11.1.0
+
+This history entry is not translated yet to the Indonesian language yet, please refer to the english version of the [HISTORY entry](https://github.com/kataras/iris/blob/master/HISTORY.md#su-18-november-2018--v1110) instead.
+
# Fr, 09 November 2018 | v11.0.4
This history entry is not translated yet to the Indonesian language yet, please refer to the english version of the [HISTORY entry](https://github.com/kataras/iris/blob/master/HISTORY.md#fr-09-november-2018--v1104) instead.
diff --git a/HISTORY_ZH.md b/HISTORY_ZH.md
index 6ad12ef6..4b5af7d1 100644
--- a/HISTORY_ZH.md
+++ b/HISTORY_ZH.md
@@ -17,6 +17,10 @@
**如何升级**: 打开命令行执行以下命令: `go get -u github.com/kataras/iris` 或者等待自动更新。
+# Su, 18 November 2018 | v11.1.0
+
+This history entry is not translated yet to the Chinese language yet, please refer to the english version of the [HISTORY entry](https://github.com/kataras/iris/blob/master/HISTORY.md#su-18-november-2018--v1110) instead.
+
# Fr, 09 November 2018 | v11.0.4
This history entry is not translated yet to the Chinese language yet, please refer to the english version of the [HISTORY entry](https://github.com/kataras/iris/blob/master/HISTORY.md#fr-09-november-2018--v1104) instead.
diff --git a/README.md b/README.md
index db03b61f..9c29b829 100644
--- a/README.md
+++ b/README.md
@@ -1,8 +1,12 @@
+# ⚡️ Update: community-driven version 11.1.0
+
+Click [here](HISTORY.md#su-18-november-2018--v1110) to read about the versioning API that the most recent version of Iris brings to you.
+
# Iris Web Framework
-[](https://travis-ci.org/kataras/iris) [](http://goreportcard.com/report/kataras/iris) [](https://marketplace.visualstudio.com/items?itemName=kataras2006.iris) [](https://kataras.rocket.chat/channel/iris) [](https://github.com/kataras/iris/tree/master/_examples/routing) [](https://github.com/kataras/iris/releases)
+[](https://travis-ci.org/kataras/iris) [](http://goreportcard.com/report/kataras/iris) [](https://marketplace.visualstudio.com/items?itemName=kataras2006.iris) [](https://kataras.rocket.chat/channel/iris) [](https://github.com/kataras/iris/tree/master/_examples/routing) [](https://github.com/kataras/iris/releases)
Iris is a fast, simple yet fully featured and very efficient web framework for Go.
@@ -1013,7 +1017,7 @@ Iris, unlike others, is 100% compatible with the standards and that's why the ma
## Support
-- [HISTORY](HISTORY.md#fr-09-november-2018--v1104) file is your best friend, it contains information about the latest features and changes
+- [HISTORY](HISTORY.md#su-18-november-2018--v1110) file is your best friend, it contains information about the latest features and changes
- Did you happen to find a bug? Post it at [github issues](https://github.com/kataras/iris/issues)
- Do you have any questions or need to speak with someone experienced to solve a problem at real-time? Join us to the [community chat](https://chat.iris-go.com)
- Complete our form-based user experience report by clicking [here](https://docs.google.com/forms/d/e/1FAIpQLSdCxZXPANg_xHWil4kVAdhmh7EBBHQZ_4_xSZVDL-oCC_z5pA/viewform?usp=sf_link)
diff --git a/README_GR.md b/README_GR.md
index f824e03a..d5c5321a 100644
--- a/README_GR.md
+++ b/README_GR.md
@@ -2,7 +2,7 @@
-[](https://travis-ci.org/kataras/iris) [](http://goreportcard.com/report/kataras/iris) [](https://marketplace.visualstudio.com/items?itemName=kataras2006.iris) [](https://kataras.rocket.chat/channel/iris) [](https://github.com/kataras/iris/tree/master/_examples/routing) [](https://github.com/kataras/iris/releases)
+[](https://travis-ci.org/kataras/iris) [](http://goreportcard.com/report/kataras/iris) [](https://marketplace.visualstudio.com/items?itemName=kataras2006.iris) [](https://kataras.rocket.chat/channel/iris) [](https://github.com/kataras/iris/tree/master/_examples/routing) [](https://github.com/kataras/iris/releases)
Το Iris είναι ένα γρήγορο, απλό αλλά και πλήρως λειτουργικό και πολύ αποδοτικό web framework για τη Go.
@@ -108,7 +108,7 @@ _Η τελευταία ενημέρωση έγινε την [Τρίτη, 21 Νο
## Υποστήριξη
-- To [HISTORY](HISTORY_GR.md#fr-09-november-2018--v1104) αρχείο είναι ο καλύτερος σας φίλος, περιέχει πληροφορίες σχετικά με τις τελευταίες λειτουργίες(features) και αλλαγές
+- To [HISTORY](HISTORY_GR.md#su-18-november-2018--v1110) αρχείο είναι ο καλύτερος σας φίλος, περιέχει πληροφορίες σχετικά με τις τελευταίες λειτουργίες(features) και αλλαγές
- Μήπως τυχαίνει να βρήκατε κάποιο bug; Δημοσιεύστε το στα [github issues](https://github.com/kataras/iris/issues)
- Έχετε οποιεσδήποτε ερωτήσεις ή πρέπει να μιλήσετε με κάποιον έμπειρο για την επίλυση ενός προβλήματος σε πραγματικό χρόνο; Ελάτε μαζί μας στην [συνομιλία κοινότητας](https://chat.iris-go.com)
- Συμπληρώστε την αναφορά εμπειρίας χρήστη κάνοντας κλικ [εδώ](https://docs.google.com/forms/d/e/1FAIpQLSdCxZXPANg_xHWil4kVAdhmh7EBBHQZ_4_xSZVDL-oCC_z5pA/viewform?usp=sf_link)
diff --git a/README_ID.md b/README_ID.md
index 085c421c..30a7075b 100644
--- a/README_ID.md
+++ b/README_ID.md
@@ -2,7 +2,7 @@
-[](https://travis-ci.org/kataras/iris) [](http://goreportcard.com/report/kataras/iris) [](https://marketplace.visualstudio.com/items?itemName=kataras2006.iris) [](https://kataras.rocket.chat/channel/iris) [](https://github.com/kataras/iris/tree/master/_examples/routing) [](https://github.com/kataras/iris/releases)
+[](https://travis-ci.org/kataras/iris) [](http://goreportcard.com/report/kataras/iris) [](https://marketplace.visualstudio.com/items?itemName=kataras2006.iris) [](https://kataras.rocket.chat/channel/iris) [](https://github.com/kataras/iris/tree/master/_examples/routing) [](https://github.com/kataras/iris/releases)
Iris adalah web framework yang cepat, sederhana namun berfitur lengkap dan sangat efisien untuk Go.
@@ -106,7 +106,7 @@ _Diperbarui pada: [Tuesday, 21 November 2017](_benchmarks/README_UNIX.md)_
## Dukungan
-- File [HISTORY](HISTORY_ID.md#fr-09-november-2018--v1104) adalah sahabat anda, file tersebut memiliki informasi terkait fitur dan perubahan terbaru
+- File [HISTORY](HISTORY_ID.md#su-18-november-2018--v1110) adalah sahabat anda, file tersebut memiliki informasi terkait fitur dan perubahan terbaru
- Apakah anda menemukan bug? Laporkan itu melalui [github issues](https://github.com/kataras/iris/issues)
- Apakah anda memiliki pertanyaan atau butuh untuk bicara kepada seseorang yang sudah berpengalaman untuk menyelesaikan masalah secara langsung? Gabung bersama kami di [community chat](https://chat.iris-go.com)
- Lengkapi laporan user-experience berbasis formulir kami dengan tekan [disini](https://docs.google.com/forms/d/e/1FAIpQLSdCxZXPANg_xHWil4kVAdhmh7EBBHQZ_4_xSZVDL-oCC_z5pA/viewform?usp=sf_link)
diff --git a/README_JPN.md b/README_JPN.md
index 151a904f..c46c95d4 100644
--- a/README_JPN.md
+++ b/README_JPN.md
@@ -2,7 +2,7 @@
-[](https://travis-ci.org/kataras/iris) [](http://goreportcard.com/report/kataras/iris) [](https://marketplace.visualstudio.com/items?itemName=kataras2006.iris) [](https://kataras.rocket.chat/channel/iris) [](https://github.com/kataras/iris/tree/master/_examples/routing) [](https://github.com/kataras/iris/releases)
+[](https://travis-ci.org/kataras/iris) [](http://goreportcard.com/report/kataras/iris) [](https://marketplace.visualstudio.com/items?itemName=kataras2006.iris) [](https://kataras.rocket.chat/channel/iris) [](https://github.com/kataras/iris/tree/master/_examples/routing) [](https://github.com/kataras/iris/releases)
Irisはシンプルで高速、それにも関わらず充実した機能を有する効率的なGo言語のウェブフレームワークです。
@@ -106,7 +106,7 @@ _Updated at: [Tuesday, 21 November 2017](_benchmarks/README_UNIX.md)_
## 支援
-- [HISTORY](HISTORY.md#fr-09-november-2018--v1104)ファイルはあなたの友人です。このファイルには、機能に関する最新の情報や変更点が記載されています。
+- [HISTORY](HISTORY.md#su-18-november-2018--v1110)ファイルはあなたの友人です。このファイルには、機能に関する最新の情報や変更点が記載されています。
- バグを発見しましたか?[github issues](https://github.com/kataras/iris/issues)に投稿をお願い致します。
- 質問がありますか?または問題を即時に解決するため、熟練者に相談する必要がありますか?[community chat](https://chat.iris-go.com)に参加しましょう。
- [here](https://docs.google.com/forms/d/e/1FAIpQLSdCxZXPANg_xHWil4kVAdhmh7EBBHQZ_4_xSZVDL-oCC_z5pA/viewform?usp=sf_link)をクリックしてユーザーとしての体験を報告しましょう。
diff --git a/README_PT_BR.md b/README_PT_BR.md
index e2068d8a..4b637fea 100644
--- a/README_PT_BR.md
+++ b/README_PT_BR.md
@@ -2,7 +2,7 @@
-[](https://travis-ci.org/kataras/iris) [](http://goreportcard.com/report/kataras/iris) [](https://marketplace.visualstudio.com/items?itemName=kataras2006.iris) [](https://kataras.rocket.chat/channel/iris) [](https://github.com/kataras/iris/tree/master/_examples/routing) [](https://github.com/kataras/iris/releases)
+[](https://travis-ci.org/kataras/iris) [](http://goreportcard.com/report/kataras/iris) [](https://marketplace.visualstudio.com/items?itemName=kataras2006.iris) [](https://kataras.rocket.chat/channel/iris) [](https://github.com/kataras/iris/tree/master/_examples/routing) [](https://github.com/kataras/iris/releases)
Iris é um framework rápido, simples porém completo e muito eficiente para a linguagem Go.
@@ -106,7 +106,7 @@ _Atualizado em : [Terça, 21 de Novembro de 2017](_benchmarks/README_UNIX.md)_
## Apoie
-- [HISTORY](HISTORY.md#fr-09-november-2018--v1104) o arquivo HISTORY é o seu melhor amigo, ele contém informações sobre as últimas features e mudanças.
+- [HISTORY](HISTORY.md#su-18-november-2018--v1110) o arquivo HISTORY é o seu melhor amigo, ele contém informações sobre as últimas features e mudanças.
- Econtrou algum bug ? Poste-o nas [issues](https://github.com/kataras/iris/issues)
- Possui alguma dúvida ou gostaria de falar com alguém experiente para resolver seu problema em tempo real ? Junte-se ao [chat da nossa comunidade](https://chat.iris-go.com).
- Complete nosso formulário de experiência do usuário clicando [aqui](https://docs.google.com/forms/d/e/1FAIpQLSdCxZXPANg_xHWil4kVAdhmh7EBBHQZ_4_xSZVDL-oCC_z5pA/viewform?usp=sf_link)
diff --git a/README_RU.md b/README_RU.md
index f791bb72..4a8e9287 100644
--- a/README_RU.md
+++ b/README_RU.md
@@ -2,7 +2,7 @@
-[](https://travis-ci.org/kataras/iris) [](http://goreportcard.com/report/kataras/iris) [](https://marketplace.visualstudio.com/items?itemName=kataras2006.iris) [](https://kataras.rocket.chat/channel/iris) [](https://github.com/kataras/iris/tree/master/_examples/routing) [](https://github.com/kataras/iris/releases)
+[](https://travis-ci.org/kataras/iris) [](http://goreportcard.com/report/kataras/iris) [](https://marketplace.visualstudio.com/items?itemName=kataras2006.iris) [](https://kataras.rocket.chat/channel/iris) [](https://github.com/kataras/iris/tree/master/_examples/routing) [](https://github.com/kataras/iris/releases)
Iris - это быстрая, простая, но полнофункциональная и очень эффективная веб-платформа для Go.
@@ -106,7 +106,7 @@ _Обновлено: [Вторник, 21 ноября 2017 г.](_benchmarks/READ
## Поддержка
-- Файл [HISTORY](HISTORY.md#fr-09-november-2018--v1104) - ваш лучший друг, он содержит информацию о последних особенностях и всех изменениях
+- Файл [HISTORY](HISTORY.md#su-18-november-2018--v1110) - ваш лучший друг, он содержит информацию о последних особенностях и всех изменениях
- Вы случайно обнаружили ошибку? Опубликуйте ее на [Github вопросы](https://github.com/kataras/iris/issues)
- У Вас есть какие-либо вопросы или Вам нужно поговорить с кем-то, кто бы смог решить Вашу проблему в режиме реального времени? Присоединяйтесь к нам в [чате сообщества](https://chat.iris-go.com)
- Заполните наш отчет о пользовательском опыте на основе формы, нажав [здесь](https://docs.google.com/forms/d/e/1FAIpQLSdCxZXPANg_xHWil4kVAdhmh7EBBHQZ_4_xSZVDL-oCC_z5pA/viewform?usp=sf_link)
diff --git a/README_ZH.md b/README_ZH.md
index b8140b84..4d8be6f8 100644
--- a/README_ZH.md
+++ b/README_ZH.md
@@ -2,7 +2,7 @@
-[](https://travis-ci.org/kataras/iris) [](http://goreportcard.com/report/kataras/iris) [](https://marketplace.visualstudio.com/items?itemName=kataras2006.iris) [](https://kataras.rocket.chat/channel/iris) [](https://github.com/kataras/iris/tree/master/_examples/routing) [](https://github.com/kataras/iris/releases)
+[](https://travis-ci.org/kataras/iris) [](http://goreportcard.com/report/kataras/iris) [](https://marketplace.visualstudio.com/items?itemName=kataras2006.iris) [](https://kataras.rocket.chat/channel/iris) [](https://github.com/kataras/iris/tree/master/_examples/routing) [](https://github.com/kataras/iris/releases)
Iris 是一款超快、简洁高效的 Go 语言 Web开发框架。
@@ -102,7 +102,7 @@ _更新于: [2017年11月21日星期二](_benchmarks/README_UNIX.md)_
## 支持
-- [更新记录](HISTORY_ZH.md#fr-09-november-2018--v1104) 是您最好的朋友,它包含有关最新功能和更改的信息
+- [更新记录](HISTORY_ZH.md#su-18-november-2018--v1110) 是您最好的朋友,它包含有关最新功能和更改的信息
- 你碰巧找到了一个错误? 请提交 [github issues](https://github.com/kataras/iris/issues)
- 您是否有任何疑问或需要与有经验的人士交谈以实时解决问题? [加入我们的聊天](https://chat.iris-go.com)
- [点击这里完成我们基于表单的用户体验报告](https://docs.google.com/forms/d/e/1FAIpQLSdCxZXPANg_xHWil4kVAdhmh7EBBHQZ_4_xSZVDL-oCC_z5pA/viewform?usp=sf_link)
diff --git a/VERSION b/VERSION
index 6c5393d1..59226bc2 100644
--- a/VERSION
+++ b/VERSION
@@ -1 +1 @@
-11.0.4:https://github.com/kataras/iris/blob/master/HISTORY.md#fr-09-november-2018--v1104
\ No newline at end of file
+11.1.0:https://github.com/kataras/iris/blob/master/HISTORY.md#su-18-november-2018--v1110
\ No newline at end of file
diff --git a/_examples/README.md b/_examples/README.md
index fd0f4f7c..38a3faa2 100644
--- a/_examples/README.md
+++ b/_examples/README.md
@@ -158,6 +158,11 @@ Navigate through examples for a better understanding.
* [per-route](routing/writing-a-middleware/per-route/main.go)
* [globally](routing/writing-a-middleware/globally/main.go)
+### Versioning
+
+- [How it works](https://github.com/kataras/iris/blob/master/versioning/README.md)
+- [Example](versioning/main.go)
+
### hero
- [Basic](hero/basic/main.go)
diff --git a/_examples/routing/dynamic-path/main.go b/_examples/routing/dynamic-path/main.go
index d94b0c18..d2399839 100644
--- a/_examples/routing/dynamic-path/main.go
+++ b/_examples/routing/dynamic-path/main.go
@@ -28,30 +28,65 @@ func main() {
// otherwise it pre-compiles the regexp and adds the necessary middleware(s).
//
// Standard macro types for parameters:
- // +------------------------+
- // | {param:string} |
- // +------------------------+
+ // +------------------------+
+ // | {param:string} |
+ // +------------------------+
// string type
- // anything
+ // anything (single path segmnent)
//
- // +-------------------------------+
- // | {param:int} or {param:int} |
- // +-------------------------------+
+ // +-------------------------------+
+ // | {param:int} |
+ // +-------------------------------+
// int type
- // both positive and negative numbers, any number of digits (ctx.Params().GetInt will limit the digits based on the host arch)
+ // -9223372036854775808 to 9223372036854775807 (x64) or -2147483648 to 2147483647 (x32), depends on the host arch
//
- // +-------------------------------+
- // | {param:int64} or {param:long} |
- // +-------------------------------+
+ // +------------------------+
+ // | {param:int8} |
+ // +------------------------+
+ // int8 type
+ // -128 to 127
+ //
+ // +------------------------+
+ // | {param:int16} |
+ // +------------------------+
+ // int16 type
+ // -32768 to 32767
+ //
+ // +------------------------+
+ // | {param:int32} |
+ // +------------------------+
+ // int32 type
+ // -2147483648 to 2147483647
+ //
+ // +------------------------+
+ // | {param:int64} |
+ // +------------------------+
// int64 type
// -9223372036854775808 to 9223372036854775807
//
// +------------------------+
+ // | {param:uint} |
+ // +------------------------+
+ // uint type
+ // 0 to 18446744073709551615 (x64) or 0 to 4294967295 (x32)
+ //
+ // +------------------------+
// | {param:uint8} |
// +------------------------+
// uint8 type
// 0 to 255
//
+ // +------------------------+
+ // | {param:uint16} |
+ // +------------------------+
+ // uint16 type
+ // 0 to 65535
+ //
+ // +------------------------+
+ // | {param:uint32} |
+ // +------------------------+
+ // uint32 type
+ // 0 to 4294967295
//
// +------------------------+
// | {param:uint64} |
@@ -66,15 +101,15 @@ func main() {
// only "1" or "t" or "T" or "TRUE" or "true" or "True"
// or "0" or "f" or "F" or "FALSE" or "false" or "False"
//
- // +------------------------+
- // | {param:alphabetical} |
- // +------------------------+
+ // +------------------------+
+ // | {param:alphabetical} |
+ // +------------------------+
// alphabetical/letter type
// letters only (upper or lowercase)
//
- // +------------------------+
- // | {param:file} |
- // +------------------------+
+ // +------------------------+
+ // | {param:file} |
+ // +------------------------+
// file type
// letters (upper or lowercase)
// numbers (0-9)
@@ -83,12 +118,12 @@ func main() {
// point (.)
// no spaces ! or other character
//
- // +------------------------+
- // | {param:path} |
- // +------------------------+
+ // +------------------------+
+ // | {param:path} |
+ // +------------------------+
// path type
- // anything, should be the last part, more than one path segment,
- // i.e: /path1/path2/path3 , ctx.Params().Get("param") == "/path1/path2/path3"
+ // anything, should be the last part, can be more than one path segment,
+ // i.e: "/test/{param:path}" and request: "/test/path1/path2/path3" , ctx.Params().Get("param") == "path1/path2/path3"
//
// if type is missing then parameter's type is defaulted to string, so
// {param} == {param:string}.
diff --git a/_examples/versioning/README.md b/_examples/versioning/README.md
new file mode 100644
index 00000000..d45f42d3
--- /dev/null
+++ b/_examples/versioning/README.md
@@ -0,0 +1 @@
+Head over to the [kataras/iris/versioning/README.md](https://github.com/kataras/iris/blob/master/versioning/README.md) instead.
\ No newline at end of file
diff --git a/_examples/versioning/main.go b/_examples/versioning/main.go
new file mode 100644
index 00000000..8516b190
--- /dev/null
+++ b/_examples/versioning/main.go
@@ -0,0 +1,73 @@
+package main
+
+import (
+ "github.com/kataras/iris"
+ "github.com/kataras/iris/versioning"
+)
+
+func main() {
+ app := iris.New()
+
+ examplePerRoute(app)
+ examplePerParty(app)
+
+ // Read the README.md before any action.
+ app.Run(iris.Addr(":8080"))
+}
+
+// How to test:
+// Open Postman
+// GET: localhost:8080/api/cats
+// Headers[1] = Accept-Version: "1" and repeat with
+// Headers[1] = Accept-Version: "2.5"
+// or even "Accept": "application/json; version=2.5"
+func examplePerRoute(app *iris.Application) {
+ app.Get("/api/cats", versioning.NewMatcher(versioning.Map{
+ "1": catsVersionExactly1Handler,
+ ">= 2, < 3": catsV2Handler,
+ versioning.NotFound: versioning.NotFoundHandler,
+ }))
+}
+
+// How to test:
+// Open Postman
+// GET: localhost:8080/api/users
+// Headers[1] = Accept-Version: "1.9.9" and repeat with
+// Headers[1] = Accept-Version: "2.5"
+//
+// POST: localhost:8080/api/users/new
+// Headers[1] = Accept-Version: "1.8.3"
+//
+// POST: localhost:8080/api/users
+// Headers[1] = Accept-Version: "2"
+func examplePerParty(app *iris.Application) {
+ usersAPI := app.Party("/api/users")
+
+ // version 1.
+ usersAPIV1 := versioning.NewGroup(">= 1, < 2")
+ usersAPIV1.Get("/", func(ctx iris.Context) {
+ ctx.Writef("v1 resource: /api/users handler")
+ })
+ usersAPIV1.Post("/new", func(ctx iris.Context) {
+ ctx.Writef("v1 resource: /api/users/new post handler")
+ })
+
+ // version 2.
+ usersAPIV2 := versioning.NewGroup(">= 2, < 3")
+ usersAPIV2.Get("/", func(ctx iris.Context) {
+ ctx.Writef("v2 resource: /api/users handler")
+ })
+ usersAPIV2.Post("/", func(ctx iris.Context) {
+ ctx.Writef("v2 resource: /api/users post handler")
+ })
+
+ versioning.RegisterGroups(usersAPI, versioning.NotFoundHandler, usersAPIV1, usersAPIV2)
+}
+
+func catsVersionExactly1Handler(ctx iris.Context) {
+ ctx.Writef("v1 exactly resource: /api/cats handler")
+}
+
+func catsV2Handler(ctx iris.Context) {
+ ctx.Writef("v2 resource: /api/cats handler")
+}
diff --git a/doc.go b/doc.go
index 9c6fe387..81aa5854 100644
--- a/doc.go
+++ b/doc.go
@@ -35,7 +35,7 @@ Source code and other details for the project are available at GitHub:
Current Version
-11.0.4
+11.1.0
Installation
@@ -800,7 +800,7 @@ Standard macro types for parameters:
+------------------------+
path type
anything, should be the last part, can be more than one path segment,
- i.e: "/test/*param" and request: "/test/path1/path2/path3" , ctx.Params().Get("param") == "path1/path2/path3"
+ i.e: "/test/{param:path}" and request: "/test/path1/path2/path3" , ctx.Params().Get("param") == "path1/path2/path3"
if type is missing then parameter's type is defaulted to string, so
{param} == {param:string}.
diff --git a/go.mod b/go.mod
index 5ab96780..723b0774 100644
--- a/go.mod
+++ b/go.mod
@@ -20,6 +20,7 @@ require (
github.com/google/go-querystring v1.0.0 // indirect
github.com/gopherjs/gopherjs v0.0.0-20180825215210-0210a2f0f73c // indirect
github.com/gorilla/websocket v1.4.0
+ github.com/hashicorp/go-version v1.0.0
github.com/imkira/go-interpol v1.1.0 // indirect
github.com/iris-contrib/blackfriday v2.0.0+incompatible
github.com/iris-contrib/formBinder v0.0.0-20171010160137-ad9fb86c356f
diff --git a/go.sum b/go.sum
index 9f5d64ca..fa58ee46 100644
--- a/go.sum
+++ b/go.sum
@@ -42,6 +42,8 @@ github.com/gopherjs/gopherjs v0.0.0-20180825215210-0210a2f0f73c h1:16eHWuMGvCjSf
github.com/gopherjs/gopherjs v0.0.0-20180825215210-0210a2f0f73c/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
github.com/gorilla/websocket v1.4.0 h1:WDFjx/TMzVgy9VdMMQi2K2Emtwi2QcUQsztZ/zLaH/Q=
github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ=
+github.com/hashicorp/go-version v1.0.0 h1:21MVWPKDphxa7ineQQTrCU5brh7OuVVAzGOCnnCPtE8=
+github.com/hashicorp/go-version v1.0.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA=
github.com/hpcloud/tail v1.0.0 h1:nfCOvKYfkgYP8hkirhJocXT2+zOD8yUNjXaWfTlyFKI=
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
github.com/imkira/go-interpol v1.1.0 h1:KIiKr0VSG2CUW1hl1jpiyuzuJeKUUpC8iM1AIE7N1Vk=
diff --git a/iris.go b/iris.go
index e785d016..0459d360 100644
--- a/iris.go
+++ b/iris.go
@@ -33,7 +33,7 @@ import (
var (
// Version is the current version number of the Iris Web Framework.
- Version = "11.0.4"
+ Version = "11.1.0"
)
// HTTP status codes as registered with IANA.
diff --git a/versioning/AUTHORS b/versioning/AUTHORS
new file mode 100644
index 00000000..848245bb
--- /dev/null
+++ b/versioning/AUTHORS
@@ -0,0 +1 @@
+Gerasimos Maropoulos
diff --git a/versioning/LICENSE b/versioning/LICENSE
new file mode 100644
index 00000000..469fb44d
--- /dev/null
+++ b/versioning/LICENSE
@@ -0,0 +1,27 @@
+Copyright (c) 2018 Gerasimos Maropoulos. All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are
+met:
+
+ * Redistributions of source code must retain the above copyright
+notice, this list of conditions and the following disclaimer.
+ * Redistributions in binary form must reproduce the above
+copyright notice, this list of conditions and the following disclaimer
+in the documentation and/or other materials provided with the
+distribution.
+ * Neither the name of Iris nor the names of its
+contributor, Gerasimos Maropoulos, may be used to endorse or promote products
+derived from this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
\ No newline at end of file
diff --git a/versioning/README.md b/versioning/README.md
new file mode 100644
index 00000000..01a629ad
--- /dev/null
+++ b/versioning/README.md
@@ -0,0 +1,142 @@
+# Versioning
+
+The [versioning](https://github.com/kataras/iris/tree/master/versioning) package provides [semver](https://semver.org/) versioning for your APIs. It implements all the suggestions written at [api-guidelines](https://github.com/byrondover/api-guidelines/blob/master/Guidelines.md#versioning) and more.
+
+
+The version comparison is done by the [go-version](https://github.com/hashicorp/go-version) package. It supports matching over patterns like `">= 1.0, < 3"` and etc.
+
+## Features
+
+- per route version matching, a normal iris handler with "switch" cases via Map for version => handler
+- per group versioned routes and deprecation API
+- version matching like ">= 1.0, < 2.0" or just "2.0.1" and etc.
+- version not found handler (can be customized by simply adding the versioning.NotFound: customNotMatchVersionHandler on the Map)
+- version is retrieved from the "Accept" and "Accept-Version" headers (can be customized via middleware)
+- respond with "X-API-Version" header, if version found.
+- deprecation options with customizable "X-API-Warn", "X-API-Deprecation-Date", "X-API-Deprecation-Info" headers via `Deprecated` wrapper.
+
+## Get version
+
+Current request version is retrieved by `versioning.GetVersion(ctx)`.
+
+By default the `GetVersion` will try to read from:
+- `Accept` header, i.e `Accept: "application/json; version=1.0"`
+- `Accept-Version` header, i.e `Accept-Version: "1.0"`
+
+You can also set a custom version for a handler via a middleware by using the context's store values.
+For example:
+```go
+func(ctx iris.Context) {
+ ctx.Values().Set(versioning.Key, ctx.URLParamDefault("version", "1.0"))
+ ctx.Next()
+}
+```
+
+## Match version to handler
+
+The `versioning.NewMatcher(versioning.Map) iris.Handler` creates a single handler which decides what handler need to be executed based on the requested version.
+
+```go
+app := iris.New()
+
+// middleware for all versions.
+myMiddleware := func(ctx iris.Context) {
+ // [...]
+ ctx.Next()
+}
+
+myCustomNotVersionFound := func(ctx iris.Context) {
+ ctx.StatusCode(404)
+ ctx.Writef("%s version not found", versioning.GetVersion(ctx))
+}
+
+userAPI := app.Party("/api/user")
+userAPI.Get("/", myMiddleware, versioning.NewMatcher(versioning.Map{
+ "1.0": sendHandler(v10Response),
+ ">= 2, < 3": sendHandler(v2Response),
+ versioning.NotFound: myCustomNotVersionFound,
+}))
+```
+
+### Deprecation
+
+Using the `versioning.Deprecated(handler iris.Handler, options versioning.DeprecationOptions) iris.Handler` function you can mark a specific handler version as deprecated.
+
+
+```go
+v10Handler := versioning.Deprecated(sendHandler(v10Response), versioning.DeprecationOptions{
+ // if empty defaults to: "WARNING! You are using a deprecated version of this API."
+ WarnMessage string
+ DeprecationDate time.Time
+ DeprecationInfo string
+})
+
+userAPI.Get("/", versioning.NewMatcher(versioning.Map{
+ "1.0": v10Handler,
+ // [...]
+}))
+```
+
+This will make the handler to send these headers to the client:
+
+- `"X-API-Warn": options.WarnMessage`
+- `"X-API-Deprecation-Date": context.FormatTime(ctx, options.DeprecationDate))`
+- `"X-API-Deprecation-Info": options.DeprecationInfo`
+
+> versioning.DefaultDeprecationOptions can be passed instead if you don't care about Date and Info.
+
+## Grouping routes by version
+
+Grouping routes by version is possible as well.
+
+Using the `versioning.NewGroup(version string) *versioning.Group` function you can create a group to register your versioned routes.
+The `versioning.RegisterGroups(r iris.Party, versionNotFoundHandler iris.Handler, groups ...*versioning.Group)` must be called in the end in order to register the routes to a specific `Party`.
+
+```go
+app := iris.New()
+
+userAPI := app.Party("/api/user")
+// [... static serving, middlewares and etc goes here].
+
+userAPIV10 := versioning.NewGroup("1.0")
+userAPIV10.Get("/", sendHandler(v10Response))
+
+userAPIV2 := versioning.NewGroup(">= 2, < 3")
+userAPIV2.Get("/", sendHandler(v2Response))
+userAPIV2.Post("/", sendHandler(v2Response))
+userAPIV2.Put("/other", sendHandler(v2Response))
+
+versioning.RegisterGroups(userAPI, versioning.NotFoundHandler, userAPIV10, userAPIV2)
+```
+
+> A middleware can be registered to the actual `iris.Party` only, using the methods we learnt above, i.e by using the `versioning.Match` in order to detect what code/handler you want to be executed when "x" or no version is requested.
+
+### Deprecation for Group
+
+Just call the `Deprecated(versioning.DeprecationOptions)` on the group you want to notify your API consumers that this specific version is deprecated.
+
+```go
+userAPIV10 := versioning.NewGroup("1.0").Deprecated(versioning.DefaultDeprecationOptions)
+```
+
+## Compare version manually from inside your handlers
+
+```go
+// reports if the "version" is matching to the "is".
+// the "is" can be a constraint like ">= 1, < 3".
+If(version string, is string) bool
+```
+
+```go
+// same as `If` but expects a Context to read the requested version.
+Match(ctx iris.Context, expectedVersion string) bool
+```
+
+```go
+app.Get("/api/user", func(ctx iris.Context) {
+ if versioning.Match(ctx, ">= 2.2.3") {
+ // [logic for >= 2.2.3 version of your handler goes here]
+ return
+ }
+})
+```
\ No newline at end of file
diff --git a/versioning/deprecation.go b/versioning/deprecation.go
new file mode 100644
index 00000000..c7b8cd06
--- /dev/null
+++ b/versioning/deprecation.go
@@ -0,0 +1,50 @@
+package versioning
+
+import (
+ "time"
+
+ "github.com/kataras/iris/context"
+)
+
+// DeprecationOptions describes the deprecation headers key-values.
+// - "X-API-Warn": options.WarnMessage
+// - "X-API-Deprecation-Date": context.FormatTime(ctx, options.DeprecationDate))
+// - "X-API-Deprecation-Info": options.DeprecationInfo
+type DeprecationOptions struct {
+ WarnMessage string
+ DeprecationDate time.Time
+ DeprecationInfo string
+}
+
+// ShouldHandle reports whether the deprecation headers should be present or no.
+func (opts DeprecationOptions) ShouldHandle() bool {
+ return opts.WarnMessage != "" || !opts.DeprecationDate.IsZero() || opts.DeprecationInfo != ""
+}
+
+// DefaultDeprecationOptions are the default deprecation options,
+// it defaults the "X-API-Warn" header to a generic message.
+var DefaultDeprecationOptions = DeprecationOptions{
+ WarnMessage: "WARNING! You are using a deprecated version of this API.",
+}
+
+// Deprecated marks a specific handler as a deprecated.
+// Deprecated can be used to tell the clients that
+// a newer version of that specific resource is available instead.
+func Deprecated(handler context.Handler, options DeprecationOptions) context.Handler {
+ if options.WarnMessage == "" {
+ options.WarnMessage = DefaultDeprecationOptions.WarnMessage
+ }
+
+ return func(ctx context.Context) {
+ handler(ctx)
+ ctx.Header("X-API-Warn", options.WarnMessage)
+
+ if !options.DeprecationDate.IsZero() {
+ ctx.Header("X-API-Deprecation-Date", context.FormatTime(ctx, options.DeprecationDate))
+ }
+
+ if options.DeprecationInfo != "" {
+ ctx.Header("X-API-Deprecation-Info", options.DeprecationInfo)
+ }
+ }
+}
diff --git a/versioning/deprecation_test.go b/versioning/deprecation_test.go
new file mode 100644
index 00000000..4cdcf83e
--- /dev/null
+++ b/versioning/deprecation_test.go
@@ -0,0 +1,33 @@
+package versioning_test
+
+import (
+ "testing"
+ "time"
+
+ "github.com/kataras/iris"
+ "github.com/kataras/iris/httptest"
+ "github.com/kataras/iris/versioning"
+)
+
+func TestDeprecated(t *testing.T) {
+ app := iris.New()
+
+ writeVesion := func(ctx iris.Context) {
+ ctx.WriteString(versioning.GetVersion(ctx))
+ }
+
+ opts := versioning.DeprecationOptions{
+ WarnMessage: "deprecated, see ",
+ DeprecationDate: time.Now().UTC(),
+ DeprecationInfo: "a bigger version is available, see for more information",
+ }
+ app.Get("/", versioning.Deprecated(writeVesion, opts))
+
+ e := httptest.New(t, app)
+
+ ex := e.GET("/").WithHeader(versioning.AcceptVersionHeaderKey, "1.0").Expect()
+ ex.Status(iris.StatusOK).Body().Equal("1.0")
+ ex.Header("X-API-Warn").Equal(opts.WarnMessage)
+ expectedDateStr := opts.DeprecationDate.Format(app.ConfigurationReadOnly().GetTimeFormat())
+ ex.Header("X-API-Deprecation-Date").Equal(expectedDateStr)
+}
diff --git a/versioning/group.go b/versioning/group.go
new file mode 100644
index 00000000..3ee4ef4c
--- /dev/null
+++ b/versioning/group.go
@@ -0,0 +1,185 @@
+package versioning
+
+import (
+ "net/http"
+
+ "github.com/kataras/iris/context"
+ "github.com/kataras/iris/core/router"
+)
+
+type (
+ vroute struct {
+ method string
+ path string
+ versions Map
+ }
+
+ // Group is a group of version-based routes.
+ // One version per one or more routes.
+ Group struct {
+ version string
+ extraMethods []string
+ routes []vroute
+
+ deprecation DeprecationOptions
+ }
+)
+
+// NewGroup returns a ptr to Group based on the given "version".
+//
+// See `Handle` and `RegisterGroups` for more.
+func NewGroup(version string) *Group {
+ return &Group{
+ version: version,
+ }
+}
+
+// Deprecated marks this group and all its versioned routes
+// as deprecated versions of that endpoint.
+// It can be called in the end just before `RegisterGroups`
+// or first by `NewGroup(...).Deprecated(...)`. It returns itself.
+func (g *Group) Deprecated(options DeprecationOptions) *Group {
+ // if `Deprecated` is called in the end.
+ for _, r := range g.routes {
+ r.versions[g.version] = Deprecated(r.versions[g.version], options)
+ }
+
+ // store the options if called before registering any versioned routes.
+ g.deprecation = options
+
+ return g
+}
+
+// AllowMethods can be called before `Handle/Get/Post...`
+// to tell the underline router that all routes should be registered
+// to these "methods" as well.
+func (g *Group) AllowMethods(methods ...string) *Group {
+ g.extraMethods = append(g.extraMethods, methods...)
+ return g
+}
+
+func (g *Group) addVRoute(method, path string, handler context.Handler) {
+ for _, r := range g.routes { // check if route already exists.
+ if r.method == method && r.path == path {
+ return
+ }
+ }
+
+ g.routes = append(g.routes, vroute{
+ method: method,
+ path: path,
+ versions: Map{g.version: handler},
+ })
+}
+
+// Handle registers a versioned route to the group.
+// A call of `RegisterGroups` is necessary in order to register the actual routes
+// when the group is complete.
+//
+// `RegisterGroups` for more.
+func (g *Group) Handle(method string, path string, handler context.Handler) {
+ if g.deprecation.ShouldHandle() { // if `Deprecated` called first.
+ handler = Deprecated(handler, g.deprecation)
+ }
+
+ methods := append(g.extraMethods, method)
+
+ for _, method := range methods {
+ g.addVRoute(method, path, handler)
+ }
+}
+
+// None registers an "offline" versioned route
+// see `context#ExecRoute(routeName)` and routing examples.
+func (g *Group) None(path string, handler context.Handler) {
+ g.Handle(router.MethodNone, path, handler)
+}
+
+// Get registers a versioned route for the Get http method.
+func (g *Group) Get(path string, handler context.Handler) {
+ g.Handle(http.MethodGet, path, handler)
+}
+
+// Post registers a versioned route for the Post http method.
+func (g *Group) Post(path string, handler context.Handler) {
+ g.Handle(http.MethodPost, path, handler)
+}
+
+// Put registers a versioned route for the Put http method
+func (g *Group) Put(path string, handler context.Handler) {
+ g.Handle(http.MethodPut, path, handler)
+}
+
+// Delete registers a versioned route for the Delete http method.
+func (g *Group) Delete(path string, handler context.Handler) {
+ g.Handle(http.MethodDelete, path, handler)
+}
+
+// Connect registers a versioned route for the Connect http method.
+func (g *Group) Connect(path string, handler context.Handler) {
+ g.Handle(http.MethodConnect, path, handler)
+}
+
+// Head registers a versioned route for the Head http method.
+func (g *Group) Head(path string, handler context.Handler) {
+ g.Handle(http.MethodHead, path, handler)
+}
+
+// Options registers a versioned route for the Options http method.
+func (g *Group) Options(path string, handler context.Handler) {
+ g.Handle(http.MethodOptions, path, handler)
+}
+
+// Patch registers a versioned route for the Patch http method.
+func (g *Group) Patch(path string, handler context.Handler) {
+ g.Handle(http.MethodPatch, path, handler)
+}
+
+// Trace registers a versioned route for the Trace http method.
+func (g *Group) Trace(path string, handler context.Handler) {
+ g.Handle(http.MethodTrace, path, handler)
+}
+
+// Any registers a versioned route for ALL of the http methods
+// (Get,Post,Put,Head,Patch,Options,Connect,Delete).
+func (g *Group) Any(registeredPath string, handler context.Handler) {
+ g.Get(registeredPath, handler)
+ g.Post(registeredPath, handler)
+ g.Put(registeredPath, handler)
+ g.Delete(registeredPath, handler)
+ g.Connect(registeredPath, handler)
+ g.Head(registeredPath, handler)
+ g.Options(registeredPath, handler)
+ g.Patch(registeredPath, handler)
+ g.Trace(registeredPath, handler)
+}
+
+// RegisterGroups registers one or more groups to an `iris.Party` or to the root router.
+// See `NewGroup` and `NotFoundHandler` too.
+func RegisterGroups(r router.Party, notFoundHandler context.Handler, groups ...*Group) (actualRoutes []*router.Route) {
+ var total []vroute
+ for _, g := range groups {
+ inner:
+ for _, r := range g.routes {
+ for i, tr := range total {
+ if tr.method == r.method && tr.path == r.path {
+ total[i].versions[g.version] = r.versions[g.version]
+ continue inner
+ }
+ }
+
+ total = append(total, r)
+ }
+ }
+
+ for _, vr := range total {
+ if notFoundHandler != nil {
+ vr.versions[NotFound] = notFoundHandler
+ }
+
+ route := r.Handle(vr.method, vr.path, NewMatcher(vr.versions))
+ actualRoutes = append(actualRoutes, route)
+ }
+
+ return
+}
diff --git a/versioning/version.go b/versioning/version.go
new file mode 100644
index 00000000..75d814ba
--- /dev/null
+++ b/versioning/version.go
@@ -0,0 +1,95 @@
+package versioning
+
+import (
+ "strings"
+
+ "github.com/kataras/iris/context"
+)
+
+const (
+ // AcceptVersionHeaderKey is the header key of "Accept-Version".
+ AcceptVersionHeaderKey = "Accept-Version"
+ // AcceptHeaderKey is the header key of "Accept".
+ AcceptHeaderKey = "Accept"
+ // AcceptHeaderVersionValue is the Accept's header value search term the requested version.
+ AcceptHeaderVersionValue = "version"
+
+ // Key is the context key of the version, can be used to manually modify the "requested" version.
+ // Example of how you can change the default behavior to extract a requested version (which is by headers)
+ // from a "version" url parameter instead:
+ // func(ctx iris.Context) { // &version=1
+ // ctx.Values().Set(versioning.Key, ctx.URLParamDefault("version", "1"))
+ // ctx.Next()
+ // }
+ Key = "iris.api.version" // for use inside the ctx.Values(), not visible by the user.
+ // NotFound is the key that can be used inside a `Map` or inside `ctx.Values().Set(versioning.Key, versioning.NotFound)`
+ // to tell that a version wasn't found, therefore the not found handler should handle the request instead.
+ NotFound = Key + ".notfound"
+)
+
+// NotFoundHandler is the default version not found handler that
+// is executed from `NewMatcher` when no version is registered as available to dispatch a resource.
+var NotFoundHandler = func(ctx context.Context) {
+ // 303 is an option too,
+ // end-dev has the chance to change that behavior by using the NotFound in the map:
+ //
+ // https://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html
+ /*
+ 10.5.2 501 Not Implemented
+
+ The server does not support the functionality required to fulfill the request.
+ This is the appropriate response when the server does not
+ recognize the request method and is not capable of supporting it for any resource.
+ */
+
+ ctx.StatusCode(501)
+ ctx.WriteString("version not found")
+}
+
+// GetVersion returns the current request version.
+//
+// By default the `GetVersion` will try to read from:
+// - "Accept" header, i.e Accept: "application/json; version=1.0"
+// - "Accept-Version" header, i.e Accept-Version: "1.0"
+//
+// However, the end developer can also set a custom version for a handler via a middleware by using the context's store key
+// for versions (see `Key` for further details on that).
+func GetVersion(ctx context.Context) string {
+ // firstly by context store, if manually set-ed by a middleware.
+ if version := ctx.Values().GetString(Key); version != "" {
+ return version
+ }
+
+ // secondly by the "Accept-Version" header.
+ if version := ctx.GetHeader(AcceptVersionHeaderKey); version != "" {
+ return version
+ }
+
+ // thirdly by the "Accept" header which is like"...; version=1.0"
+ acceptValue := ctx.GetHeader(AcceptHeaderKey)
+ if acceptValue != "" {
+ if idx := strings.Index(acceptValue, AcceptHeaderVersionValue); idx != -1 {
+ rem := acceptValue[idx:]
+ startVersion := strings.Index(rem, "=")
+ if startVersion == -1 || len(rem) < startVersion+1 {
+ return NotFound
+ }
+
+ rem = rem[startVersion+1:]
+
+ end := strings.Index(rem, " ")
+ if end == -1 {
+ end = strings.Index(rem, ";")
+ }
+ if end == -1 {
+ end = len(rem)
+ }
+
+ if version := rem[:end]; version != "" {
+ return version
+ }
+ }
+ }
+
+ return NotFound
+}
diff --git a/versioning/version_test.go b/versioning/version_test.go
new file mode 100644
index 00000000..059fb310
--- /dev/null
+++ b/versioning/version_test.go
@@ -0,0 +1,48 @@
+package versioning_test
+
+import (
+ "testing"
+
+ "github.com/kataras/iris"
+ "github.com/kataras/iris/httptest"
+ "github.com/kataras/iris/versioning"
+)
+
+func TestGetVersion(t *testing.T) {
+ app := iris.New()
+
+ writeVesion := func(ctx iris.Context) {
+ ctx.WriteString(versioning.GetVersion(ctx))
+ }
+
+ app.Get("/", writeVesion)
+ app.Get("/manual", func(ctx iris.Context) {
+ ctx.Values().Set(versioning.Key, "11.0.5")
+ ctx.Next()
+ }, writeVesion)
+
+ e := httptest.New(t, app)
+
+ e.GET("/").WithHeader(versioning.AcceptVersionHeaderKey, "1.0").Expect().
+ Status(iris.StatusOK).Body().Equal("1.0")
+ e.GET("/").WithHeader(versioning.AcceptHeaderKey, "application/vnd.api+json; version=2.1").Expect().
+ Status(iris.StatusOK).Body().Equal("2.1")
+ e.GET("/").WithHeader(versioning.AcceptHeaderKey, "application/vnd.api+json; version=2.1 ;other=dsa").Expect().
+ Status(iris.StatusOK).Body().Equal("2.1")
+ e.GET("/").WithHeader(versioning.AcceptHeaderKey, "version=2.1").Expect().
+ Status(iris.StatusOK).Body().Equal("2.1")
+ e.GET("/").WithHeader(versioning.AcceptHeaderKey, "version=1").Expect().
+ Status(iris.StatusOK).Body().Equal("1")
+
+ // unknown versions.
+ e.GET("/").WithHeader(versioning.AcceptVersionHeaderKey, "").Expect().
+ Status(iris.StatusOK).Body().Equal(versioning.NotFound)
+ e.GET("/").WithHeader(versioning.AcceptHeaderKey, "application/vnd.api+json; version=").Expect().
+ Status(iris.StatusOK).Body().Equal(versioning.NotFound)
+ e.GET("/").WithHeader(versioning.AcceptHeaderKey, "application/vnd.api+json; version= ;other=dsa").Expect().
+ Status(iris.StatusOK).Body().Equal(versioning.NotFound)
+ e.GET("/").WithHeader(versioning.AcceptHeaderKey, "version=").Expect().
+ Status(iris.StatusOK).Body().Equal(versioning.NotFound)
+
+ e.GET("/manual").Expect().Status(iris.StatusOK).Body().Equal("11.0.5")
+}
diff --git a/versioning/versioning.go b/versioning/versioning.go
new file mode 100644
index 00000000..ca280d1e
--- /dev/null
+++ b/versioning/versioning.go
@@ -0,0 +1,120 @@
+package versioning
+
+import (
+ "github.com/kataras/iris/context"
+
+ "github.com/hashicorp/go-version"
+)
+
+// If reports whether the "version" is matching to the "is".
+// the "is" can be a constraint like ">= 1, < 3".
+func If(v string, is string) bool {
+ ver, err := version.NewVersion(v)
+ if err != nil {
+ return false
+ }
+
+ constraints, err := version.NewConstraint(is)
+ if err != nil {
+ return false
+ }
+
+ return constraints.Check(ver)
+}
+
+// Match acts exactly the same as `If` does but instead it accepts
+// a Context, so it can be called by a handler to determinate the requested version.
+func Match(ctx context.Context, expectedVersion string) bool {
+ return If(GetVersion(ctx), expectedVersion)
+}
+
+// Map is a map of versions targets to a handlers,
+// a handler per version or constraint, the key can be something like ">1, <=2" or just "1".
+type Map map[string]context.Handler
+
+// NewMatcher creates a single handler which decides what handler
+// should be executed based on the requested version.
+//
+// Use the `NewGroup` if you want to add many routes under a specific version.
+//
+// See `Map` and `NewGroup` too.
+func NewMatcher(versions Map) context.Handler {
+ constraintsHandlers, notFoundHandler := buildConstraints(versions)
+
+ return func(ctx context.Context) {
+ versionString := GetVersion(ctx)
+ if versionString == NotFound {
+ notFoundHandler(ctx)
+ return
+ }
+
+ ver, err := version.NewVersion(versionString)
+ if err != nil {
+ notFoundHandler(ctx)
+ return
+ }
+
+ for _, ch := range constraintsHandlers {
+ if ch.constraints.Check(ver) {
+ ctx.Header("X-API-Version", ver.String())
+ ch.handler(ctx)
+ return
+ }
+ }
+
+ // pass the not matched version so the not found handler can have knowedge about it.
+ // ctx.Values().Set(Key, versionString)
+ // or let a manual cal of GetVersion(ctx) do that instead.
+ notFoundHandler(ctx)
+ }
+}
+
+type constraintsHandler struct {
+ constraints version.Constraints
+ handler context.Handler
+}
+
+func buildConstraints(versionsHandler Map) (constraintsHandlers []*constraintsHandler, notfoundHandler context.Handler) {
+ for v, h := range versionsHandler {
+ if v == NotFound {
+ notfoundHandler = h
+ continue
+ }
+
+ constraints, err := version.NewConstraint(v)
+ if err != nil {
+ panic(err)
+ }
+
+ constraintsHandlers = append(constraintsHandlers, &constraintsHandler{
+ constraints: constraints,
+ handler: h,
+ })
+ }
+
+ if notfoundHandler == nil {
+ notfoundHandler = NotFoundHandler
+ }
+
+ // no sort, the end-dev should declare
+ // all version constraint, i.e < 4.0 may be catch 1.0 if not something like
+ // >= 3.0, < 4.0.
+ // I can make it ordered but I do NOT like the final API of it:
+ /*
+ app.Get("/api/user", NewMatcher( // accepts an array, ordered, see last elem.
+ V("1.0", vHandler("v1 here")),
+ V("2.0", vHandler("v2 here")),
+ V("< 4.0", vHandler("v3.x here")),
+ ))
+ instead we have:
+
+ app.Get("/api/user", NewMatcher(Map{ // accepts a map, unordered, see last elem.
+ "1.0": Deprecated(vHandler("v1 here")),
+ "2.0": vHandler("v2 here"),
+ ">= 3.0, < 4.0": vHandler("v3.x here"),
+ VersionUnknown: customHandlerForNotMatchingVersion,
+ }))
+ */
+
+ return
+}
diff --git a/versioning/versioning_test.go b/versioning/versioning_test.go
new file mode 100644
index 00000000..4f7d5600
--- /dev/null
+++ b/versioning/versioning_test.go
@@ -0,0 +1,136 @@
+package versioning_test
+
+import (
+ "testing"
+
+ "github.com/kataras/iris"
+ "github.com/kataras/iris/httptest"
+ "github.com/kataras/iris/versioning"
+)
+
+func notFoundHandler(ctx iris.Context) {
+ ctx.NotFound()
+}
+
+const (
+ v10Response = "v1.0 handler"
+ v2Response = "v2.x handler"
+)
+
+func sendHandler(contents string) iris.Handler {
+ return func(ctx iris.Context) {
+ ctx.WriteString(contents)
+ }
+}
+
+func TestIf(t *testing.T) {
+ if expected, got := true, versioning.If("1.0", ">=1"); expected != got {
+ t.Fatalf("expected %s to be %s", "1.0", ">= 1")
+ }
+ if expected, got := true, versioning.If("1.2.3", "> 1.2"); expected != got {
+ t.Fatalf("expected %s to be %s", "1.2.3", "> 1.2")
+ }
+}
+func TestNewMatcher(t *testing.T) {
+ app := iris.New()
+
+ userAPI := app.Party("/api/user")
+ userAPI.Get("/", versioning.NewMatcher(versioning.Map{
+ "1.0": sendHandler(v10Response),
+ ">= 2, < 3": sendHandler(v2Response),
+ versioning.NotFound: notFoundHandler,
+ }))
+
+ // middleware as usual.
+ myMiddleware := func(ctx iris.Context) {
+ ctx.Header("X-Custom", "something")
+ ctx.Next()
+ }
+ myVersions := versioning.Map{
+ "1.0": sendHandler(v10Response),
+ }
+
+ userAPI.Get("/with_middleware", myMiddleware, versioning.NewMatcher(myVersions))
+
+ e := httptest.New(t, app)
+
+ e.GET("/api/user").WithHeader(versioning.AcceptVersionHeaderKey, "1").Expect().
+ Status(iris.StatusOK).Body().Equal(v10Response)
+ e.GET("/api/user").WithHeader(versioning.AcceptVersionHeaderKey, "2.0").Expect().
+ Status(iris.StatusOK).Body().Equal(v2Response)
+ e.GET("/api/user").WithHeader(versioning.AcceptVersionHeaderKey, "2.1").Expect().
+ Status(iris.StatusOK).Body().Equal(v2Response)
+ e.GET("/api/user").WithHeader(versioning.AcceptVersionHeaderKey, "2.9.9").Expect().
+ Status(iris.StatusOK).Body().Equal(v2Response)
+
+ // middleware as usual.
+ ex := e.GET("/api/user/with_middleware").WithHeader(versioning.AcceptVersionHeaderKey, "1.0").Expect()
+ ex.Status(iris.StatusOK).Body().Equal(v10Response)
+ ex.Header("X-Custom").Equal("something")
+
+ e.GET("/api/user").WithHeader(versioning.AcceptVersionHeaderKey, "3.0").Expect().
+ Status(iris.StatusNotFound).Body().Equal("Not Found")
+}
+
+func TestNewGroup(t *testing.T) {
+ app := iris.New()
+
+ userAPI := app.Party("/api/user")
+ // [... static serving, middlewares and etc goes here].
+
+ userAPIV10 := versioning.NewGroup("1.0").Deprecated(versioning.DefaultDeprecationOptions)
+ // V10middlewareResponse := "m1"
+ // userAPIV10.Use(func(ctx iris.Context) {
+ // println("exec userAPIV10.Use - midl1")
+ // sendHandler(V10middlewareResponse)(ctx)
+ // ctx.Next()
+ // })
+ // userAPIV10.Use(func(ctx iris.Context) {
+ // println("exec userAPIV10.Use - midl2")
+ // sendHandler(V10middlewareResponse + "midl2")(ctx)
+ // ctx.Next()
+ // })
+ // userAPIV10.Use(func(ctx iris.Context) {
+ // println("exec userAPIV10.Use - midl3")
+ // ctx.Next()
+ // })
+
+ userAPIV10.Get("/", sendHandler(v10Response))
+ userAPIV2 := versioning.NewGroup(">= 2, < 3")
+ // V2middlewareResponse := "m2"
+ // userAPIV2.Use(func(ctx iris.Context) {
+ // println("exec userAPIV2.Use - midl1")
+ // sendHandler(V2middlewareResponse)(ctx)
+ // ctx.Next()
+ // })
+ // userAPIV2.Use(func(ctx iris.Context) {
+ // println("exec userAPIV2.Use - midl2")
+ // ctx.Next()
+ // })
+
+ userAPIV2.Get("/", sendHandler(v2Response))
+ userAPIV2.Post("/", sendHandler(v2Response))
+ userAPIV2.Put("/other", sendHandler(v2Response))
+
+ versioning.RegisterGroups(userAPI, versioning.NotFoundHandler, userAPIV10, userAPIV2)
+
+ e := httptest.New(t, app)
+
+ ex := e.GET("/api/user").WithHeader(versioning.AcceptVersionHeaderKey, "1").Expect()
+ ex.Status(iris.StatusOK).Body().Equal(v10Response)
+ ex.Header("X-API-Warn").Equal(versioning.DefaultDeprecationOptions.WarnMessage)
+
+ e.GET("/api/user").WithHeader(versioning.AcceptVersionHeaderKey, "2.0").Expect().
+ Status(iris.StatusOK).Body().Equal(v2Response)
+ e.GET("/api/user").WithHeader(versioning.AcceptVersionHeaderKey, "2.1").Expect().
+ Status(iris.StatusOK).Body().Equal(v2Response)
+ e.GET("/api/user").WithHeader(versioning.AcceptVersionHeaderKey, "2.9.9").Expect().
+ Status(iris.StatusOK).Body().Equal(v2Response)
+ e.POST("/api/user").WithHeader(versioning.AcceptVersionHeaderKey, "2").Expect().
+ Status(iris.StatusOK).Body().Equal(v2Response)
+ e.PUT("/api/user/other").WithHeader(versioning.AcceptVersionHeaderKey, "2.9").Expect().
+ Status(iris.StatusOK).Body().Equal(v2Response)
+
+ e.GET("/api/user").WithHeader(versioning.AcceptVersionHeaderKey, "3.0").Expect().
+ Status(iris.StatusNotImplemented).Body().Equal("version not found")
+}