negotiator

package module
v0.0.0-...-6c16851 Latest Latest
Warning

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

Go to latest
Published: Feb 8, 2017 License: MIT Imports: 9 Imported by: 0

README

negotiator

Negotiator is a content negotiation library aimed to support strong content typing in RESTful HTTP services. This library implements both Accept and Content-Type header parsers and struct variants that are fully compliant with both RFC-6839 and RFC-7231.

Build Status Go Report Card GoDoc

Installation

$ go get github.com/moogar0880/negotiator

Simple Example

This simple example shows how to represent a basic Message resource using the content type registry. It has a single, defined, media type, and a default representation.

import (
  "encoding/json"
  "encoding/xml"
  "net/http"

  "github.com/moogar0880/negotiator"
)

const v1JSONMediaType = "application/vnd.message.v1+json"

var (
  Registry *negotiator.Registry
  defaultMessage = Message{Greeting: "Hello", Name: "World"}
)

type Message struct {
  Name string
  Greeting string
}

// Return a content type for the Message resource type
func (m *Message) ContentType(a *Accept) (string, error) {
  return v1JSONMediaType, nil
}

// handle encoding a message resource into a byte slice
func (m *Message) MarshalMedia(a *Accept) ([]byte, error) {
  data, _ := json.Marshal(m)
  return data, nil
}

// handle unmarshalling a message from an http request body
func (m *Message) UnmarshalMedia(cType string, params ContentTypeParams, body []byte) error {
  json.Unmarshal(body, &tcn)
  return nil
}

func init() {
  Registry = negotiator.NewRegistry()
  Registry.Register("application/vnd.message.v1+json", defaultMessage)
}

func messageHandler(w http.ResponseWriter, req *http.Request) {
  model, accept, err := Registry.Negotiate(req.Header.Get("Accept"))
  if err != nil {
    http.Error(w, "Invalid Accept Header", http.StatusNotAcceptable)
    return
  }

  negotiator.MarshalMedia(accept, model)
}

Example Usage With Versioned Resource

This example shows how to handle versioning resources at the media type level, similar to how the Github API does.

Expanding from our previous example, let's introduce a vnd.message.v2+json resource that contains information about what language the message Greeting is in.

import (
  "encoding/json"
  "net/http"

  "github.com/moogar0880/negotiator"
)

const (
  v1JSONMediaType = "application/vnd.message.v1+json"
  v2JSONMediaType = "application/vnd.message.v2+json"
)

var Registry negotiator.Registry

// the original v1 message resource
type MessageV1 struct {
  Name string
  Greeting string
}

type greeting struct {
  Phrase string
  Language string
}

// The new message resource with a Greeting object instead of a string
type Message struct {
  Name string
  Greeting greeting
}

// Return a content type matching what was requested in the accept header
func (m *Message) ContentType(a *Accept) (string, error) {
  switch a.MediaRange {
  case v1JSONMediaType:
    return v1JSONMediaType, nil
  case v2JSONMediaType:
    return v2JSONMediaType, nil
  }
  return "", errors.New("Unsupported Media Type")
}

// handle marshlling a message resource, including converting to the old
// message resource format, if that's what was requested
func (m *Message) MarshalMedia(a *Accept) ([]byte, error) {
  switch a.MediaRange {
  case v1JSONMediaType:
    data, _ := json.Marshal(MessageV1{Name: m.Name, Greeting: m.Greeting.Phrase})
    return data, nil
  case v2JSONMediaType:
    data, _ := json.Marshal(m)
    return data, nil
  }
  return nil, errors.New("Unsupported Media Type")
}

// handle unmarshalling messages in either format from an HTTP request body as
// would be seen with a POST or PUT
func (m *Message) UnmarshalMedia(cType string, params ContentTypeParams, body []byte) error {
	switch cType {
  case v1JSONMediaType:
    var m1 MessageV1
    json.Unmarshal(body, &m1)
    m.Name = m1.Name
    m.Greeting.Phrase = m1.Greeting
    return nil
	case v2JSONMediaType:
		json.Unmarshal(body, &tcn)
    return nil
	}
	return errors.New("Unsupported Media Type")
}

func init() {
  Registry = negotiator.NewRegistry()
  Registry.Register("application/vnd.message.v1+json", Message{})
  Registry.Register("application/vnd.message.v2+json", Message{})
}

func MessageHandler(w http.ResponseWriter, req *http.Request) {
  model, accept, err := Registry.Negotiate(req.Header.Get("Accept"))
  if err != nil {
    http.Error(w, "Invalid Accept Header", http.StatusNotAcceptable)
    return
  }

  MarshalMedia(w, model, accept)
}

Documentation

Overview

Package negotiator is a content negotiation library aimed to support strong content typing for RESTful HTTP services. This library implements both Accept and Content-Type header parsers and struct variants that are fully compliant with both RFC-6839 and RFC-7231.

Index

Examples

Constants

View Source
const (
	// AcceptParamsQuality is the default quality of a media range with specified
	// accept-params. (e.g text/html;level=1)
	AcceptParamsQuality float64 = 1.0

	// MediaRangeSubTypeQuality is the default quality of a media range with
	// type and subtype defined. (e.g text/html)
	MediaRangeSubTypeQuality float64 = 0.9

	// MediaRangeWildcardSubtypeQuality is the default weight of a media range
	// with a wildcarded subtype. (e.g text/*)
	MediaRangeWildcardSubtypeQuality float64 = 0.8

	// MediaRangeWildcardQuality is the default quality for a wildcarded media
	// range. (e.g */*)
	MediaRangeWildcardQuality float64 = 0.7

	// WildCard is the constant character "*", representing an accept header
	// wildcard character
	WildCard string = "*"
)
View Source
const ContentTypeHeader = "Content-Type"

ContentTypeHeader is the constant value for the key indicating the Content-Type header

Variables

View Source
var (
	// ErrInvalidMediaRange is the error returned when an invalid accept
	// media range is parsed
	ErrInvalidMediaRange = errors.New("Invalid Accept Media Range")

	// ErrInvalidAcceptParam is the error returned when an invalid accept
	// parameter is parsed
	ErrInvalidAcceptParam = errors.New("Invalid Accept Parameter")
)
View Source
var (
	// ErrNoContentType is the error returned if an accept header cannot be matched
	// in the current registry
	ErrNoContentType = errors.New("No Acceptable Content Type")
)

Functions

func MarshalMedia

func MarshalMedia(w io.Writer, cn ContentNegotiator, acpt *Accept) error

MarshalMedia marshals the ContentNegotiator to the provided io.Writer, based on an Accept. An error is returned if the ContentNegotiator's MarshalMedia call fails, or if the data can't be written to the io.Writer

func UnmarshalMedia

func UnmarshalMedia(req *http.Request, cn ContentNegotiator) error

UnmarshalMedia handles unmarshalling an http.Request body, using a ContentNegotiator instance. An error is returned if no Content-Type header was provided, if the provided Content-Type header was poorly formatted, or if the body of the http.Request could not be read.

Types

type Accept

type Accept struct {
	MediaRange   mediaRange
	AcceptParams mediaParams
	Quality      float64
	AcceptExt    acceptExt
}

Accept is the struct representation of a single accept header value

func NewAccept

func NewAccept() *Accept

NewAccept returns a zero-valued Accept instance

func ParseAccept

func ParseAccept(header string) (*Accept, error)

ParseAccept parses the provided accept header and returns a newly created Accept struct, and a conditional error

func (*Accept) Parse

func (a *Accept) Parse(accept string) error

Parse parses the provided string argument into an Accept instance, returning an error if the provided value is not properly formatted

type AcceptHeader

type AcceptHeader []*Accept

AcceptHeader is a slice of individual Accept instances representing an entire accept header

func ParseHeader

func ParseHeader(header string) (AcceptHeader, error)

ParseHeader parses an entire Accept header into an AcceptHeader instance and sorts it according to the relative quality of the accept headers provided

type ContentNegotiator

type ContentNegotiator interface {
	// ContentType accepts the provided Accept header struct and returns the
	// matched content type, or an error
	ContentType(*Accept) (string, error)

	// MarshalMedia returns a raw byte slice containing an appropriately rendered
	// representation of the provided resource, or an error.
	MarshalMedia(*Accept) ([]byte, error)

	// UnmarshalMedia accepts the content type and content type parameters
	// provided in a request, as well as the raw request body, and unmarshals it
	// into the ContentNegotiator implementation struct
	UnmarshalMedia(string, ContentTypeParams, []byte) error
}

The ContentNegotiator interface defines the mechanism through which arbitrary interfaces can be provided information about the provided Accept and Content-Type headers, to control marshalling and unmarshalling request/response data as correctly as possible. Optionally, requests may be rejected if provided arguments are invalid, unacceptable, or otherwise erronenous for a given resource.

type ContentTypeParams

type ContentTypeParams map[string]string

ContentTypeParams is a type alias for a map of string to strings, representing any parameters passed to the Content-Type header

type Registry

type Registry map[string]interface{}

Registry is a content type registry used for managing a mapping of media ranges to the interfaces that represent those resources

Example
package main

import (
	"bytes"
	"encoding/json"
	"fmt"

	"github.com/moogar0880/negotiator"
)

func main() {
	type Message struct {
		Name     string
		Greeting string
	}

	type greeting struct {
		Phrase   string
		Language string
	}

	type MessageV2 struct {
		Name     string
		Greeting greeting
	}

	// define a registry that can handle the media types for our message structs
	registry := negotiator.NewRegistry()
	registry.Register("application/vnd.message.v1+json", Message{})
	registry.Register("application/vnd.message.v2+json", MessageV2{})

	// negotiate a predefined accept header, erroring if we don't support any of
	// the provided media types. Spoiler Alert - We do.
	acptHeader := "application/json, application/vnd.message.v1+json"
	model, accept, err := registry.Negotiate(acptHeader)
	if err != nil {
		fmt.Printf("Invalid Accept Header: %s", accept.MediaRange)
		return
	}

	// Dump our response (json encoded) into a buffer and print the result
	w := bytes.NewBuffer([]byte{})
	switch model.(type) {
	default:
		msg := MessageV2{Name: "John Doe",
			Greeting: greeting{
				Phrase:   "Hello",
				Language: "English"},
		}
		json.NewEncoder(w).Encode(&msg)
	case Message:
		msg := Message{Name: "John Doe", Greeting: "Hello"}
		json.NewEncoder(w).Encode(&msg)
	}
	fmt.Println(w.String())
}
Output:

{"Name":"John Doe","Greeting":"Hello"}

func NewRegistry

func NewRegistry() *Registry

NewRegistry returns an empty Registry

func (Registry) ContentType

func (r Registry) ContentType(header string) (interface{}, ContentTypeParams, error)

ContentType parses the provided Content-Type header and attempts to find an interface which implements the specified content type

func (Registry) Negotiate

func (r Registry) Negotiate(header string) (interface{}, *Accept, error)

Negotiate attempts to negotiate the proper interface for the provided accept header. Negotiate returns a copy of the default interface that best matches the provided accept header, if a match is found

func (Registry) Register

func (r Registry) Register(contentType string, defaultValue interface{})

Register registers the default struct value for a content type in the registry. when requested, a copy of the default value will be provided as the result of a call to Negotiate

Jump to

Keyboard shortcuts

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