initialize support for versioning as requested, per route -- not finished yet

Former-commit-id: ade66610125f06a0b5ce3e90bcafe349f216a616
This commit is contained in:
Gerasimos (Makis) Maropoulos 2018-11-10 03:49:32 +02:00
parent e08d0b4be6
commit b22a18da6b
6 changed files with 325 additions and 0 deletions

36
versioning/deprecation.go Normal file
View 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)
}
}
}

View 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
View 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
}

View 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
View 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
}

View 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")
}