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 -[![build status](https://img.shields.io/travis/kataras/iris/master.svg?style=flat-square)](https://travis-ci.org/kataras/iris) [![report card](https://img.shields.io/badge/report%20card-a%2B-ff3333.svg?style=flat-square)](http://goreportcard.com/report/kataras/iris) [![vscode-iris](https://img.shields.io/badge/ext%20-vscode-0c77e3.svg?style=flat-square)](https://marketplace.visualstudio.com/items?itemName=kataras2006.iris) [![chat](https://img.shields.io/badge/community-%20chat-00BCD4.svg?style=flat-square)](https://kataras.rocket.chat/channel/iris) [![view examples](https://img.shields.io/badge/routing%20by-example-0077b3.svg?style=flat-square)](https://github.com/kataras/iris/tree/master/_examples/routing) [![release](https://img.shields.io/badge/release%20-v11.0-0077b3.svg?style=flat-square)](https://github.com/kataras/iris/releases) +[![build status](https://img.shields.io/travis/kataras/iris/master.svg?style=flat-square)](https://travis-ci.org/kataras/iris) [![report card](https://img.shields.io/badge/report%20card-a%2B-ff3333.svg?style=flat-square)](http://goreportcard.com/report/kataras/iris) [![vscode-iris](https://img.shields.io/badge/ext%20-vscode-0c77e3.svg?style=flat-square)](https://marketplace.visualstudio.com/items?itemName=kataras2006.iris) [![chat](https://img.shields.io/badge/community-%20chat-00BCD4.svg?style=flat-square)](https://kataras.rocket.chat/channel/iris) [![view examples](https://img.shields.io/badge/routing%20by-example-0077b3.svg?style=flat-square)](https://github.com/kataras/iris/tree/master/_examples/routing) [![release](https://img.shields.io/badge/release%20-v11.1-0077b3.svg?style=flat-square)](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 @@ -[![build status](https://img.shields.io/travis/kataras/iris/master.svg?style=flat-square)](https://travis-ci.org/kataras/iris) [![report card](https://img.shields.io/badge/report%20card-a%2B-ff3333.svg?style=flat-square)](http://goreportcard.com/report/kataras/iris) [![vscode-iris](https://img.shields.io/badge/ext%20-vscode-0c77e3.svg?style=flat-square)](https://marketplace.visualstudio.com/items?itemName=kataras2006.iris) [![chat](https://img.shields.io/badge/community-%20chat-00BCD4.svg?style=flat-square)](https://kataras.rocket.chat/channel/iris) [![view examples](https://img.shields.io/badge/learn%20by-examples-0077b3.svg?style=flat-square)](https://github.com/kataras/iris/tree/master/_examples/routing) [![release](https://img.shields.io/badge/release%20-v11.0-0077b3.svg?style=flat-square)](https://github.com/kataras/iris/releases) +[![build status](https://img.shields.io/travis/kataras/iris/master.svg?style=flat-square)](https://travis-ci.org/kataras/iris) [![report card](https://img.shields.io/badge/report%20card-a%2B-ff3333.svg?style=flat-square)](http://goreportcard.com/report/kataras/iris) [![vscode-iris](https://img.shields.io/badge/ext%20-vscode-0c77e3.svg?style=flat-square)](https://marketplace.visualstudio.com/items?itemName=kataras2006.iris) [![chat](https://img.shields.io/badge/community-%20chat-00BCD4.svg?style=flat-square)](https://kataras.rocket.chat/channel/iris) [![view examples](https://img.shields.io/badge/learn%20by-examples-0077b3.svg?style=flat-square)](https://github.com/kataras/iris/tree/master/_examples/routing) [![release](https://img.shields.io/badge/release%20-v11.1-0077b3.svg?style=flat-square)](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 @@ -[![build status](https://img.shields.io/travis/kataras/iris/master.svg?style=flat-square)](https://travis-ci.org/kataras/iris) [![report card](https://img.shields.io/badge/report%20card-a%2B-ff3333.svg?style=flat-square)](http://goreportcard.com/report/kataras/iris) [![vscode-iris](https://img.shields.io/badge/ext%20-vscode-0c77e3.svg?style=flat-square)](https://marketplace.visualstudio.com/items?itemName=kataras2006.iris) [![chat](https://img.shields.io/badge/community-%20chat-00BCD4.svg?style=flat-square)](https://kataras.rocket.chat/channel/iris) [![view examples](https://img.shields.io/badge/learn%20by-examples-0077b3.svg?style=flat-square)](https://github.com/kataras/iris/tree/master/_examples/routing) [![release](https://img.shields.io/badge/release%20-v11.0-0077b3.svg?style=flat-square)](https://github.com/kataras/iris/releases) +[![build status](https://img.shields.io/travis/kataras/iris/master.svg?style=flat-square)](https://travis-ci.org/kataras/iris) [![report card](https://img.shields.io/badge/report%20card-a%2B-ff3333.svg?style=flat-square)](http://goreportcard.com/report/kataras/iris) [![vscode-iris](https://img.shields.io/badge/ext%20-vscode-0c77e3.svg?style=flat-square)](https://marketplace.visualstudio.com/items?itemName=kataras2006.iris) [![chat](https://img.shields.io/badge/community-%20chat-00BCD4.svg?style=flat-square)](https://kataras.rocket.chat/channel/iris) [![view examples](https://img.shields.io/badge/learn%20by-examples-0077b3.svg?style=flat-square)](https://github.com/kataras/iris/tree/master/_examples/routing) [![release](https://img.shields.io/badge/release%20-v11.1-0077b3.svg?style=flat-square)](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 @@ -[![build status](https://img.shields.io/travis/kataras/iris/master.svg?style=flat-square)](https://travis-ci.org/kataras/iris) [![report card](https://img.shields.io/badge/report%20card-a%2B-ff3333.svg?style=flat-square)](http://goreportcard.com/report/kataras/iris) [![vscode-iris](https://img.shields.io/badge/ext%20-vscode-0c77e3.svg?style=flat-square)](https://marketplace.visualstudio.com/items?itemName=kataras2006.iris) [![chat](https://img.shields.io/badge/community-%20chat-00BCD4.svg?style=flat-square)](https://kataras.rocket.chat/channel/iris) [![view examples](https://img.shields.io/badge/learn%20by-examples-0077b3.svg?style=flat-square)](https://github.com/kataras/iris/tree/master/_examples/routing) [![release](https://img.shields.io/badge/release%20-v11.0-0077b3.svg?style=flat-square)](https://github.com/kataras/iris/releases) +[![build status](https://img.shields.io/travis/kataras/iris/master.svg?style=flat-square)](https://travis-ci.org/kataras/iris) [![report card](https://img.shields.io/badge/report%20card-a%2B-ff3333.svg?style=flat-square)](http://goreportcard.com/report/kataras/iris) [![vscode-iris](https://img.shields.io/badge/ext%20-vscode-0c77e3.svg?style=flat-square)](https://marketplace.visualstudio.com/items?itemName=kataras2006.iris) [![chat](https://img.shields.io/badge/community-%20chat-00BCD4.svg?style=flat-square)](https://kataras.rocket.chat/channel/iris) [![view examples](https://img.shields.io/badge/learn%20by-examples-0077b3.svg?style=flat-square)](https://github.com/kataras/iris/tree/master/_examples/routing) [![release](https://img.shields.io/badge/release%20-v11.1-0077b3.svg?style=flat-square)](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 @@ -[![build status](https://img.shields.io/travis/kataras/iris/master.svg?style=flat-square)](https://travis-ci.org/kataras/iris) [![report card](https://img.shields.io/badge/report%20card-a%2B-ff3333.svg?style=flat-square)](http://goreportcard.com/report/kataras/iris) [![vscode-iris](https://img.shields.io/badge/ext%20-vscode-0c77e3.svg?style=flat-square)](https://marketplace.visualstudio.com/items?itemName=kataras2006.iris) [![chat](https://img.shields.io/badge/community-%20chat-00BCD4.svg?style=flat-square)](https://kataras.rocket.chat/channel/iris) [![view examples](https://img.shields.io/badge/learn%20by-examples-0077b3.svg?style=flat-square)](https://github.com/kataras/iris/tree/master/_examples/routing) [![release](https://img.shields.io/badge/release%20-v11.0-0077b3.svg?style=flat-square)](https://github.com/kataras/iris/releases) +[![build status](https://img.shields.io/travis/kataras/iris/master.svg?style=flat-square)](https://travis-ci.org/kataras/iris) [![report card](https://img.shields.io/badge/report%20card-a%2B-ff3333.svg?style=flat-square)](http://goreportcard.com/report/kataras/iris) [![vscode-iris](https://img.shields.io/badge/ext%20-vscode-0c77e3.svg?style=flat-square)](https://marketplace.visualstudio.com/items?itemName=kataras2006.iris) [![chat](https://img.shields.io/badge/community-%20chat-00BCD4.svg?style=flat-square)](https://kataras.rocket.chat/channel/iris) [![view examples](https://img.shields.io/badge/learn%20by-examples-0077b3.svg?style=flat-square)](https://github.com/kataras/iris/tree/master/_examples/routing) [![release](https://img.shields.io/badge/release%20-v11.1-0077b3.svg?style=flat-square)](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 @@ -[![build status](https://img.shields.io/travis/kataras/iris/master.svg?style=flat-square)](https://travis-ci.org/kataras/iris) [![report card](https://img.shields.io/badge/report%20card-a%2B-ff3333.svg?style=flat-square)](http://goreportcard.com/report/kataras/iris) [![vscode-iris](https://img.shields.io/badge/ext%20-vscode-0c77e3.svg?style=flat-square)](https://marketplace.visualstudio.com/items?itemName=kataras2006.iris) [![chat](https://img.shields.io/badge/community-%20chat-00BCD4.svg?style=flat-square)](https://kataras.rocket.chat/channel/iris) [![view examples](https://img.shields.io/badge/learn%20by-examples-0077b3.svg?style=flat-square)](https://github.com/kataras/iris/tree/master/_examples/routing) [![release](https://img.shields.io/badge/release%20-v11.0-0077b3.svg?style=flat-square)](https://github.com/kataras/iris/releases) +[![build status](https://img.shields.io/travis/kataras/iris/master.svg?style=flat-square)](https://travis-ci.org/kataras/iris) [![report card](https://img.shields.io/badge/report%20card-a%2B-ff3333.svg?style=flat-square)](http://goreportcard.com/report/kataras/iris) [![vscode-iris](https://img.shields.io/badge/ext%20-vscode-0c77e3.svg?style=flat-square)](https://marketplace.visualstudio.com/items?itemName=kataras2006.iris) [![chat](https://img.shields.io/badge/community-%20chat-00BCD4.svg?style=flat-square)](https://kataras.rocket.chat/channel/iris) [![view examples](https://img.shields.io/badge/learn%20by-examples-0077b3.svg?style=flat-square)](https://github.com/kataras/iris/tree/master/_examples/routing) [![release](https://img.shields.io/badge/release%20-v11.1-0077b3.svg?style=flat-square)](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 @@ -[![build status](https://img.shields.io/travis/kataras/iris/master.svg?style=flat-square)](https://travis-ci.org/kataras/iris) [![report card](https://img.shields.io/badge/report%20card-a%2B-ff3333.svg?style=flat-square)](http://goreportcard.com/report/kataras/iris) [![vscode-iris](https://img.shields.io/badge/ext%20-vscode-0c77e3.svg?style=flat-square)](https://marketplace.visualstudio.com/items?itemName=kataras2006.iris) [![chat](https://img.shields.io/badge/community-%20chat-00BCD4.svg?style=flat-square)](https://kataras.rocket.chat/channel/iris) [![view examples](https://img.shields.io/badge/learn%20by-examples-0077b3.svg?style=flat-square)](https://github.com/kataras/iris/tree/master/_examples/routing) [![release](https://img.shields.io/badge/release%20-v11.0-0077b3.svg?style=flat-square)](https://github.com/kataras/iris/releases) +[![build status](https://img.shields.io/travis/kataras/iris/master.svg?style=flat-square)](https://travis-ci.org/kataras/iris) [![report card](https://img.shields.io/badge/report%20card-a%2B-ff3333.svg?style=flat-square)](http://goreportcard.com/report/kataras/iris) [![vscode-iris](https://img.shields.io/badge/ext%20-vscode-0c77e3.svg?style=flat-square)](https://marketplace.visualstudio.com/items?itemName=kataras2006.iris) [![chat](https://img.shields.io/badge/community-%20chat-00BCD4.svg?style=flat-square)](https://kataras.rocket.chat/channel/iris) [![view examples](https://img.shields.io/badge/learn%20by-examples-0077b3.svg?style=flat-square)](https://github.com/kataras/iris/tree/master/_examples/routing) [![release](https://img.shields.io/badge/release%20-v11.1-0077b3.svg?style=flat-square)](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") +}