pinned

package module
v1.1.0 Latest Latest
Warning

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

Go to latest
Published: Jan 3, 2020 License: MIT Imports: 6 Imported by: 0

README

pinned

This is a proof-of-concept, date based versioning system for APIs inspired by Stripe's API versioning.

GoDoc Go Report Card Join the community on Spectrum

Overview

This package enables reverse compatibility for a Go API by defining versions and their associated changes. Consequently, API versions can be maintained for long periods of time without minimal effort.

Usage

See the included example project for detailed usage.

Versioning is done at a resource/struct level. If a type implements Versionable it can take advantage of this package.

  1. To start, create a new VersionManager.
vm := &pinned.VersionManager{
  Layout: "2006-01-02",
  Header: "API Version",
}
  1. Then add Versions.
// Initial version.
vm.Add(&pinned.Version{
  Date: "2018-02-10",
})

// New version.
vm.Add(&pinned.Version{
  Date: "2018-02-11",
  Changes: []*pinned.Change{
    &pinned.Change{
      Description: "New things",
      Actions: map[string]pinned.Action{
        "Object": someMethod,
      }
    }
  }
})

someMethod is applied to all type Object, and has the signature func(map[string]interface{}) map[string]interface{}.

  1. Handle an incoming request.
func handler(w http.ResponseWriter, r *http.Request) {
  // Get version from request.
  v, _ := vm.Parse(r)

  // Set version in context.
  ctx = pinned.NewContext(r.Context(), v)
  
  // ...Fetch resources...

  // Apply version changes to resources.
  body, _ := vm.Apply(v, data)

  // Write response.
  data, err := json.Marshal(body)
  if err != nil {
    panic(err)
  }

  w.Header().Set("Content-Type", "application/json")
  w.Write(data)
}

Example

Consider the following simple example. An API has a User struct which looks like this:

type User struct {
  ID       uint64
  FullName string
}

Now we decide we want to rename FullName to Name. However, this is a breaking change. To ensure stability, prior to change we set a version 2018-02-10.

After the change, we set a version 2018-02-11. This version has a change associated with it. This Change has an Action to be taken on the User resource.

This Action is a func that reverses the change made in the new version.

func userNameFieldChange(mapping map[string]interface{}) map[string]interface{} {
  mapping["full_name"] = mapping["name"]
  delete(mapping, "name")
  return mapping
}

There are now two versions, 2018-02-11 and 2018-02-10. To support the client that requested version 2018-02-10, the "changes" made in version 2018-02-11 are undone, and the User resource now reflects the requested version.

As versions are added, these changes are sequentially undone. This enables a version to be supported for a long period of time, and allows the developer to focus on new feature development without much concern towards legacy versions.

Documentation

Overview

Package pinned is a proof-of-concept, date based versioning system for Go APIs.

Index

Examples

Constants

This section is empty.

Variables

View Source
var (
	// ErrInvalidVersion means the version supplied is not included
	// in the versions supplied to the VersionManager or it is malformed.
	ErrInvalidVersion = errors.New("invalid version")

	// ErrNoVersionSupplied means no version was supplied.
	ErrNoVersionSupplied = errors.New("no version supplied")

	// ErrVersionDeprecated means the version is deprecated.
	ErrVersionDeprecated = errors.New("version is deprecated")
)

Functions

func NewContext

func NewContext(ctx context.Context, v *Version) context.Context

NewContext returns a new Context carrying a Version.

Types

type Action

type Action func(map[string]interface{}) map[string]interface{}

Action represents an action to take on a object in order to make it compatible. An action takes an interface as input and returns an updated interface.

type Change

type Change struct {
	// Description of the change made. Used for documentation.
	Description string

	// Actions are a map of object type to Action. The object type
	// is determined using the reflect pkg.
	Actions map[string]Action
}

Change represents a backwards-incompatible change and the actions required to make it compatible.

type Version

type Version struct {
	Date       string
	Changes    []*Change
	Deprecated bool
	// contains filtered or unexported fields
}

Version represents a version change. It is pinned to a specific time. It contains a list of Changes. Changes are executed in-order.

Example
package main

import (
	"github.com/sjkaliski/pinned"
)

var version = &pinned.Version{}

func main() {
	// userNameFieldChange is an action which "undoes" the change
	// made in this new version.
	userNameFieldChange := func(mapping map[string]interface{}) map[string]interface{} {
		mapping["full_name"] = mapping["name"]
		delete(mapping, "name")
		return mapping
	}

	// Version sets the date and lists a set of changes to be executed to
	// make the current API compatible with requests pinned to this version.
	version = &pinned.Version{
		Date: "2018-02-11",
		Changes: []*pinned.Change{
			{
				Description: "Renames `user.full_name` to `user.name`",
				// Actions are executed by type. In this example "User"
				// references any object of type User.
				Actions: map[string]pinned.Action{
					"User": userNameFieldChange,
				},
			},
		},
	}
}
Output:

func FromContext

func FromContext(ctx context.Context) *Version

FromContext returns the Version in the context.

func (*Version) String

func (v *Version) String() string

type VersionManager

type VersionManager struct {
	Layout string
	Header string
	Query  string
	// contains filtered or unexported fields
}

VersionManager represents a list of versions.

Example
package main

import (
	"github.com/sjkaliski/pinned"
)

var vm = pinned.VersionManager{}

func main() {
	vm = pinned.VersionManager{
		Header: "Example API",
	}
}
Output:

func (*VersionManager) Add

func (vm *VersionManager) Add(v *Version) error

Add adds a version.

func (*VersionManager) Apply

func (vm *VersionManager) Apply(version *Version, obj Versionable) (map[string]interface{}, error)

Apply processes a Versionable object by applying all changes between the latest version and the version requested. The altered object is returned.

Concretely, if the supplied version is two versions behind the latest, the changes in those two versions are applied sequentially to the object. This essentially "undoes" the changes made to the API so that the object is structured according to the specified version.

func (*VersionManager) Latest

func (vm *VersionManager) Latest() *Version

Latest returns the most current active version.

func (*VersionManager) Parse

func (vm *VersionManager) Parse(r *http.Request) (*Version, error)

Parse evaluates an http.Request object to determine an API version. It inspects the query parameters and request headers. Whichever is most recent is the version to use.

func (*VersionManager) Versions

func (vm *VersionManager) Versions() []string

Versions returns a list of all versions as strings.

type Versionable

type Versionable interface {
	// Data returns the object as it stands in the latest version.
	Data() map[string]interface{}
}

Versionable represents an object that is subject to versioning. An object must implement Versionable in order to be affected.

Directories

Path Synopsis

Jump to

Keyboard shortcuts

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