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