versioning

package module
v0.0.0-...-94003ff Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Dec 24, 2022 License: MIT Imports: 5 Imported by: 0

README

API Versioning (Go)

build status report card godocs donate on PayPal

Semver versioning for your APIs. It implements all the suggestions written at api-guidelines and more.

The version comparison is done by the go-version package. It supports matching over patterns like ">= 1.0, < 3" and e.t.c.

Getting started

The only requirement is the Go Programming Language.

$ go get github.com/kataras/versioning

Features

  • Per route version matching, an http.Handler with "switch" cases via versioning.Map for version => handler
  • Per group versioned routes and deprecation API
  • Version matching like ">= 1.0, < 2.0" or just "2.0.1" and e.t.c.
  • 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 through request's context key)
  • 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.

Compare Versions

// If reports whether the "version" is a valid match to the "is".
// The "is" can be a version constraint like ">= 1, < 3".
If(version string, is string) bool
// Match reports whether the current version matches the "expectedVersion".
Match(r *http.Request, expectedVersion string) bool

Example

router.HandleFunc("/api/user", func(w http.ResponseWriter, r *http.Request) {
    if versioning.Match(r, ">= 2.2.3") {
        // [logic for >= 2.2.3 version of your handler goes here]
        return
    }
})

Determining The Current Version

Current request version is retrieved by versioning.GetVersion(r *http.Request).

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"
func handler(w http.ResponseWriter, r *http.Request){
    currentVersion := versioning.GetVersion(r)
}

You can also set a custom version to a handler trough a middleware by setting a request context's value. For example:

import (
    "context"
    "net/http"

    "github.com/kataras/versioning"
)

func urlParamVersion(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request){
        version := r.URL.Query().Get("v") // ?v=2.3.5
        if version == "" {
            // set a default version, e.g. 1.0
            version = "1.0"
        }
        r = r.WithContext(versioning.WithVersion(r.Context(), version))
        next.ServeHTTP(w, r)
    })
}

Map Versions to Handlers

The versioning.NewMatcher(versioning.Map) http.Handler creates a single handler which decides what handler need to be executed based on the requested version.

// middleware for all versions.
func myMiddleware(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request){
        // [...]
        next.ServeHTTP(w, r)
    })
}

func myCustomVersionNotFound(w http.ResponseWriter, r *http.Request) {
    w.WriteHeader(404)
    fmt.Fprintf(w, "%s version not found", versioning.GetVersion(r))
}

router := http.NewServeMux()
router.Handle("/", myMiddleware(versioning.NewMatcher(versioning.Map{
    // v1Handler is a handler of yuors that will be executed only on version 1.
    "1.0":               v1Handler, 
    ">= 2, < 3":         v2Handler,
    versioning.NotFound: http.HandlerFunc(myCustomNotVersionFound),
})))
Deprecation

Using the versioning.Deprecated(handler http.Handler, options versioning.DeprecationOptions) http.Handler function you can mark a specific handler version as deprecated.

v1Handler = versioning.Deprecated(v1Handler, versioning.DeprecationOptions{
    // if empty defaults to: "WARNING! You are using a deprecated version of this API."
    WarnMessage string
    DeprecationDate time.Time
    DeprecationInfo string
})

router.Handle("/", versioning.NewMatcher(versioning.Map{
    "1.0": v1Handler,
    // [...]
}))

This will make the handler to send these headers to the client:

  • "X-API-Warn": options.WarnMessage
  • "X-API-Deprecation-Date": 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 *http.ServeMux, versionNotFoundHandler http.Handler, groups ...*versioning.Group) must be called in the end in order to register the routes to a specific StdMux.

router := http.NewServeMux()

// version 1.
usersAPIV1 := versioning.NewGroup(">= 1, < 2")
usersAPIV1.HandleFunc("/api/users", func(w http.ResponseWriter, r *http.Request) {
    if r.Method != http.MethodGet {
        http.Error(w, http.StatusText(http.StatusMethodNotAllowed), http.StatusMethodNotAllowed)
        return
    }

    w.Write([]byte("v1 resource: /api/users handler"))
})
usersAPIV1.HandleFunc("/api/users/new", func(w http.ResponseWriter, r *http.Request) {
    if r.Method != http.MethodPost {
        http.Error(w, http.StatusText(http.StatusMethodNotAllowed), http.StatusMethodNotAllowed)
        return
    }

    w.Write([]byte("v1 resource: /api/users/new post handler"))
})

// version 2.
usersAPIV2 := versioning.NewGroup(">= 2, < 3")
usersAPIV2.HandleFunc("/api/users", func(w http.ResponseWriter, r *http.Request) {
    if r.Method != http.MethodPost {
        http.Error(w, http.StatusText(http.StatusMethodNotAllowed), http.StatusMethodNotAllowed)
        return
    }

    w.Write([]byte("v2 resource: /api/users handler"))
})
usersAPIV2.HandleFunc("/api/users", func(w http.ResponseWriter, r *http.Request) {
    if r.Method != http.MethodPost {
        http.Error(w, http.StatusText(http.StatusMethodNotAllowed), http.StatusMethodNotAllowed)
        return
    }

    w.Write([]byte("v2 resource: /api/users post handler"))
})

versioning.RegisterGroups(router, versioning.NotFoundHandler, usersAPIV1, usersAPIV2)

A middleware can be registered, 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 Group#Deprecated(versioning.DeprecationOptions) on the group you want to notify your API consumers that this specific version is deprecated.

userAPIV1 := versioning.NewGroup("1.0").Deprecated(versioning.DefaultDeprecationOptions)

For a more detailed technical documentation you can head over to our godocs. And for executable code you can always visit the _examples repository's subdirectory.

License

kataras/versioning is free and open-source software licensed under the MIT License.

Documentation

Index

Constants

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

Variables

View Source
var DefaultDeprecationOptions = DeprecationOptions{
	WarnMessage: "WARNING! You are using a deprecated version of this API.",
}

DefaultDeprecationOptions are the default deprecation options, it defaults the "X-API-Warn" header to a generic message.

View Source
var HeaderTimeFormat = "Mon, 02 Jan 2006 15:04:05 GMT"

HeaderTimeFormat is the time format that will be used to send DeprecationOptions's DeprectationDate time.

View Source
var (

	// NotFound is the key that can be used inside a `Map` or inside `context.WithValue(r.Context(), versioning.contextKey, versioning.NotFound)`
	// to tell that a version wasn't found, therefore the not found handler should handle the request instead.
	NotFound = contextKey.(string) + ".notfound"
)
View Source
var NotFoundHandler = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {

	w.WriteHeader(http.StatusNotImplemented)
	w.Write(versionNotFoundText)
})

NotFoundHandler is the default version not found handler that is executed from `NewMatcher` when no version is registered as available to dispatch a resource.

Functions

func Deprecated

func Deprecated(handler http.Handler, options DeprecationOptions) http.Handler

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 GetVersion

func GetVersion(r *http.Request) string

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 trough a middleware by using the request's context's value for versions (see `WithVersion` for further details on that).

func If

func If(v string, is string) bool

If reports whether the "version" is a valid match to the "is". The "is" should be a version constraint like ">= 1, < 3".

func Match

func Match(r *http.Request, expectedVersion string) bool

Match reports whether the current version matches the "expectedVersion".

func NewMatcher

func NewMatcher(versions Map) http.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 RegisterGroups

func RegisterGroups(mux StdMux, notFoundHandler http.Handler, groups ...*Group) map[string]http.Handler

RegisterGroups registers one or more groups to an `net/http#ServeMux` if not nil, and returns the routes. Map's key is the request path from `Group#Handle` and value is the `http.Handler`. See `NewGroup` and `NotFoundHandler` too.

func WithVersion

func WithVersion(ctx context.Context, version string) context.Context

WithVersion creates the new context that contains a passed 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(w http.ResponseWriter, r *http.Request) { // &version=1

	r = r.WithContext(versioning.WithVersion(r.Context(), r.URL.Query().Get("version")))
	nextHandler.ServeHTTP(w,r)
}

Types

type DeprecationOptions

type DeprecationOptions struct {
	WarnMessage     string
	DeprecationDate time.Time
	DeprecationInfo string
}

DeprecationOptions describes the deprecation headers key-values. - "X-API-Warn": options.WarnMessage - "X-API-Deprecation-Date": time.Now().Format("Mon, 02 Jan 2006 15:04:05 GMT") - "X-API-Deprecation-Info": options.DeprecationInfo

func (DeprecationOptions) ShouldHandle

func (opts DeprecationOptions) ShouldHandle() bool

ShouldHandle reports whether the deprecation headers should be present or no.

type Group

type Group struct {
	// contains filtered or unexported fields
}

Group is a group of version-based routes. One version per one or more routes.

func NewGroup

func NewGroup(version string) *Group

NewGroup returns a ptr to Group based on the given "version".

See `Handle` and `RegisterGroups` for more.

func (*Group) Deprecated

func (g *Group) Deprecated(options DeprecationOptions) *Group

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 (*Group) Handle

func (g *Group) Handle(path string, handler http.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.

See `RegisterGroups` for more.

func (*Group) HandleFunc

func (g *Group) HandleFunc(path string, handlerFn func(w http.ResponseWriter, r *http.Request))

HandleFunc 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.

See `RegisterGroups` for more.

type Map

type Map map[string]http.Handler

Map is a map of version to handler. A handler per version or constraint, the key can be something like ">1, <=2" or just "1".

type StdMux

type StdMux interface{ Handle(string, http.Handler) }

StdMux is an interface which types like `net/http#ServeMux` implements in order to register handlers per path.

See `RegisterGroups`.

Directories

Path Synopsis
_examples

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL