mirror of
https://github.com/kataras/iris.git
synced 2025-01-23 10:41:03 +01:00
initialize support for versioning as requested, per route -- not finished yet
Former-commit-id: ade66610125f06a0b5ce3e90bcafe349f216a616
This commit is contained in:
parent
e08d0b4be6
commit
b22a18da6b
36
versioning/deprecation.go
Normal file
36
versioning/deprecation.go
Normal file
|
@ -0,0 +1,36 @@
|
||||||
|
package versioning
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/kataras/iris/context"
|
||||||
|
)
|
||||||
|
|
||||||
|
type DeprecationOptions struct {
|
||||||
|
WarnMessage string
|
||||||
|
DeprecationDate time.Time
|
||||||
|
DeprecationInfo string
|
||||||
|
}
|
||||||
|
|
||||||
|
var DefaultDeprecationOptions = DeprecationOptions{
|
||||||
|
WarnMessage: "WARNING! You are using a deprecated version of this API.",
|
||||||
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
33
versioning/deprecation_test.go
Normal file
33
versioning/deprecation_test.go
Normal file
|
@ -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 <this link>",
|
||||||
|
DeprecationDate: time.Now().UTC(),
|
||||||
|
DeprecationInfo: "a bigger version is available, see <this link> 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)
|
||||||
|
}
|
72
versioning/version.go
Normal file
72
versioning/version.go
Normal file
|
@ -0,0 +1,72 @@
|
||||||
|
package versioning
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/kataras/iris/context"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
AcceptVersionHeaderKey = "Accept-Version"
|
||||||
|
AcceptHeaderKey = "Accept"
|
||||||
|
AcceptHeaderVersionValue = "version"
|
||||||
|
|
||||||
|
Key = "iris.api.version" // for use inside the ctx.Values(), not visible by the user.
|
||||||
|
NotFound = Key + ".notfound"
|
||||||
|
)
|
||||||
|
|
||||||
|
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.WriteString("version not found")
|
||||||
|
ctx.StatusCode(501)
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
48
versioning/version_test.go
Normal file
48
versioning/version_test.go
Normal file
|
@ -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")
|
||||||
|
}
|
87
versioning/versioning.go
Normal file
87
versioning/versioning.go
Normal file
|
@ -0,0 +1,87 @@
|
||||||
|
package versioning
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/kataras/iris/context"
|
||||||
|
|
||||||
|
"github.com/hashicorp/go-version"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Map map[string]context.Handler
|
||||||
|
|
||||||
|
func Handler(versions Map) context.Handler {
|
||||||
|
constraintsHandlers, notFoundHandler := buildConstraints(versions)
|
||||||
|
|
||||||
|
return func(ctx context.Context) {
|
||||||
|
versionString := GetVersion(ctx)
|
||||||
|
if versionString == NotFound {
|
||||||
|
notFoundHandler(ctx)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
version, err := version.NewVersion(versionString)
|
||||||
|
if err != nil {
|
||||||
|
notFoundHandler(ctx)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, ch := range constraintsHandlers {
|
||||||
|
if ch.constraints.Check(version) {
|
||||||
|
ctx.Header("X-API-Version", version.String())
|
||||||
|
ch.handler(ctx)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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", Handler( // 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", Handler(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
|
||||||
|
}
|
49
versioning/versioning_test.go
Normal file
49
versioning/versioning_test.go
Normal file
|
@ -0,0 +1,49 @@
|
||||||
|
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 TestHandler(t *testing.T) {
|
||||||
|
app := iris.New()
|
||||||
|
|
||||||
|
userAPI := app.Party("/api/user")
|
||||||
|
userAPI.Get("/", versioning.Handler(versioning.Map{
|
||||||
|
"1.0": sendHandler(v10Response),
|
||||||
|
">= 2, < 3": sendHandler(v2Response),
|
||||||
|
versioning.NotFound: notFoundHandler,
|
||||||
|
}))
|
||||||
|
|
||||||
|
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)
|
||||||
|
|
||||||
|
e.GET("/api/user").WithHeader(versioning.AcceptVersionHeaderKey, "3.0").Expect().
|
||||||
|
Status(iris.StatusNotFound).Body().Equal("Not Found")
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user