Merge pull request #22 from prometheus/feature/accept-header-discrimination

WIP — Protocol Buffer negotiation support in handler.
This commit is contained in:
Matt T. Proud 2013-07-01 08:16:10 -07:00
commit b6cac8669e
18 changed files with 546 additions and 105 deletions

View File

@ -20,7 +20,10 @@ preparation:
ln -sf "$(PWD)" $(PROMETHEUS_TARGET)
dependencies:
go get code.google.com/p/goprotobuf/proto
go get github.com/matttproud/gocheck
go get github.com/matttproud/golang_protobuf_extensions/ext
go get github.com/prometheus/client_model/go
test: dependencies preparation
$(MAKE) test

View File

@ -33,11 +33,10 @@ func ProcessorForRequestHeader(header http.Header) (Processor, error) {
}
switch mediatype {
case "application/vnd.google.protobuf":
// BUG(matt): Version?
if params["proto"] != "io.prometheus.client.MetricFamily" {
return nil, fmt.Errorf("Unrecognized Protocol Message %s", params["proto"])
}
if params["encoding"] != "varint record length-delimited" {
if params["encoding"] != "delimited" {
return nil, fmt.Errorf("Unsupported Encoding %s", params["encoding"])
}
return MetricFamilyProcessor, nil

View File

@ -57,12 +57,12 @@ func testDiscriminatorHttpHeader(t test.Tester) {
err: nil,
},
{
input: map[string]string{"Content-Type": `application/vnd.google.protobuf; proto="io.prometheus.client.MetricFamily"; encoding="varint record length-delimited"`},
input: map[string]string{"Content-Type": `application/vnd.google.protobuf; proto="io.prometheus.client.MetricFamily"; encoding="delimited"`},
output: MetricFamilyProcessor,
err: nil,
},
{
input: map[string]string{"Content-Type": `application/vnd.google.protobuf; proto="illegal"; encoding="varint record length-delimited"`},
input: map[string]string{"Content-Type": `application/vnd.google.protobuf; proto="illegal"; encoding="delimited"`},
output: nil,
err: fmt.Errorf("Unrecognized Protocol Message illegal"),
},

0
foo Normal file
View File

View File

@ -28,9 +28,11 @@ const (
// The content type and schema information set on telemetry data responses.
TelemetryContentType = `application/json; schema="prometheus/telemetry"; version=` + APIVersion
// The content type and schema information set on telemetry data responses.
DelimitedTelemetryContentType = `application/vnd.google.protobuf; proto="io.prometheus.client.MetricFamily"; encoding="delimited"`
// The customary web services endpoint on which telemetric data is exposed.
ExpositionResource = "/metrics.json"
ExpositionResource = "/metrics"
baseLabelsKey = "baseLabels"
docstringKey = "docstring"

View File

@ -10,6 +10,10 @@ import (
"encoding/json"
"fmt"
"sync"
dto "github.com/prometheus/client_model/go"
"code.google.com/p/goprotobuf/proto"
)
// TODO(matt): Refactor to de-duplicate behaviors.
@ -31,13 +35,13 @@ type counterVector struct {
func NewCounter() Counter {
return &counter{
values: map[string]*counterVector{},
values: map[uint64]*counterVector{},
}
}
type counter struct {
mutex sync.RWMutex
values map[string]*counterVector
values map[uint64]*counterVector
}
func (metric *counter) Set(labels map[string]string, value float64) float64 {
@ -147,3 +151,31 @@ func (metric *counter) MarshalJSON() ([]byte, error) {
typeKey: counterTypeValue,
})
}
func (metric *counter) dumpChildren(f *dto.MetricFamily) {
metric.mutex.RLock()
defer metric.mutex.RUnlock()
f.Type = dto.MetricType_COUNTER.Enum()
for _, child := range metric.values {
c := &dto.Counter{
Value: proto.Float64(child.Value),
}
m := &dto.Metric{
Counter: c,
}
for name, value := range child.Labels {
p := &dto.LabelPair{
Name: proto.String(name),
Value: proto.String(value),
}
m.Label = append(m.Label, p)
}
f.Metric = append(f.Metric, m)
}
}

View File

@ -10,6 +10,10 @@ import (
"encoding/json"
"fmt"
"sync"
"code.google.com/p/goprotobuf/proto"
dto "github.com/prometheus/client_model/go"
)
// A gauge metric merely provides an instantaneous representation of a scalar
@ -28,13 +32,13 @@ type gaugeVector struct {
func NewGauge() Gauge {
return &gauge{
values: map[string]*gaugeVector{},
values: map[uint64]*gaugeVector{},
}
}
type gauge struct {
mutex sync.RWMutex
values map[string]*gaugeVector
values map[uint64]*gaugeVector
}
func (metric *gauge) String() string {
@ -94,3 +98,31 @@ func (metric *gauge) MarshalJSON() ([]byte, error) {
valueKey: values,
})
}
func (metric *gauge) dumpChildren(f *dto.MetricFamily) {
metric.mutex.RLock()
defer metric.mutex.RUnlock()
f.Type = dto.MetricType_GAUGE.Enum()
for _, child := range metric.values {
c := &dto.Gauge{
Value: proto.Float64(child.Value),
}
m := &dto.Metric{
Gauge: c,
}
for name, value := range child.Labels {
p := &dto.LabelPair{
Name: proto.String(name),
Value: proto.String(value),
}
m.Label = append(m.Label, p)
}
f.Metric = append(f.Metric, m)
}
}

View File

@ -13,6 +13,10 @@ import (
"math"
"strconv"
"sync"
dto "github.com/prometheus/client_model/go"
"code.google.com/p/goprotobuf/proto"
)
// This generates count-buckets of equal size distributed along the open
@ -75,7 +79,7 @@ type histogram struct {
// These are the buckets that capture samples as they are emitted to the
// histogram. Please consult the reference interface and its implements for
// further details about behavior expectations.
values map[string]*histogramVector
values map[uint64]*histogramVector
// These are the percentile values that will be reported on marshalling.
reportablePercentiles []float64
}
@ -157,7 +161,7 @@ func prospectiveIndexForPercentile(percentile float64, totalObservations int) in
}
// Determine the next bucket element when interim bucket intervals may be empty.
func (h histogram) nextNonEmptyBucketElement(signature string, currentIndex, bucketCount int, observationsByBucket []int) (*Bucket, int) {
func (h histogram) nextNonEmptyBucketElement(signature uint64, currentIndex, bucketCount int, observationsByBucket []int) (*Bucket, int) {
for i := currentIndex; i < bucketCount; i++ {
if observationsByBucket[i] == 0 {
continue
@ -176,7 +180,7 @@ func (h histogram) nextNonEmptyBucketElement(signature string, currentIndex, buc
// longer contained by the bucket, the index of the last item is returned. This
// may occur if the underlying bucket catalogs values and employs an eviction
// strategy.
func (h histogram) bucketForPercentile(signature string, percentile float64) (*Bucket, int) {
func (h histogram) bucketForPercentile(signature uint64, percentile float64) (*Bucket, int) {
bucketCount := len(h.bucketStarts)
// This captures the quantity of samples in a given bucket's range.
@ -229,7 +233,7 @@ func (h histogram) bucketForPercentile(signature string, percentile float64) (*B
// Return the histogram's estimate of the value for a given percentile of
// collected samples. The requested percentile is expected to be a real
// value within (0, 1.0].
func (h histogram) percentile(signature string, percentile float64) float64 {
func (h histogram) percentile(signature uint64, percentile float64) float64 {
bucket, index := h.bucketForPercentile(signature, percentile)
return (*bucket).ValueForIndex(index)
@ -284,7 +288,7 @@ func NewHistogram(specification *HistogramSpecification) Histogram {
bucketMaker: specification.BucketBuilder,
bucketStarts: specification.Starts,
reportablePercentiles: specification.ReportablePercentiles,
values: map[string]*histogramVector{},
values: map[uint64]*histogramVector{},
}
return metric
@ -301,3 +305,38 @@ func NewDefaultHistogram() Histogram {
},
)
}
func (metric *histogram) dumpChildren(f *dto.MetricFamily) {
metric.mutex.RLock()
defer metric.mutex.RUnlock()
f.Type = dto.MetricType_SUMMARY.Enum()
for signature, child := range metric.values {
c := &dto.Summary{}
m := &dto.Metric{
Summary: c,
}
for name, value := range child.labels {
p := &dto.LabelPair{
Name: proto.String(name),
Value: proto.String(value),
}
m.Label = append(m.Label, p)
}
for _, percentile := range metric.reportablePercentiles {
q := &dto.Quantile{
Quantile: proto.Float64(percentile),
Value: proto.Float64(metric.percentile(signature, percentile)),
}
c.Quantile = append(c.Quantile, q)
}
f.Metric = append(f.Metric, m)
}
}

View File

@ -6,7 +6,11 @@
package prometheus
import "encoding/json"
import (
"encoding/json"
dto "github.com/prometheus/client_model/go"
)
// A Metric is something that can be exposed via the registry framework.
type Metric interface {
@ -16,4 +20,6 @@ type Metric interface {
ResetAll()
// Produce a human-consumable representation of the metric.
String() string
// dumpChildren populates the child metrics of the given family.
dumpChildren(*dto.MetricFamily)
}

View File

@ -19,25 +19,26 @@ import (
"strings"
"sync"
"time"
dto "github.com/prometheus/client_model/go"
"code.google.com/p/goprotobuf/proto"
"github.com/matttproud/golang_protobuf_extensions/ext"
"github.com/prometheus/client_golang/vendor/goautoneg"
)
const (
acceptEncodingHeader = "Accept-Encoding"
authorization = "Authorization"
authorizationHeader = "WWW-Authenticate"
authorizationHeaderValue = "Basic"
acceptEncodingHeader = "Accept-Encoding"
contentEncodingHeader = "Content-Encoding"
contentTypeHeader = "Content-Type"
gzipAcceptEncodingValue = "gzip"
gzipContentEncodingValue = "gzip"
jsonContentType = "application/json"
jsonSuffix = ".json"
)
var (
abortOnMisuse bool
debugRegistration bool
useAggressiveSanityChecks bool
)
// container represents a top-level registered metric that encompasses its
@ -49,9 +50,23 @@ type container struct {
name string
}
type containers []*container
func (c containers) Len() int {
return len(c)
}
func (c containers) Swap(i, j int) {
c[i], c[j] = c[j], c[i]
}
func (c containers) Less(i, j int) bool {
return c[i].name < c[j].name
}
type registry struct {
mutex sync.RWMutex
signatureContainers map[string]container
signatureContainers map[uint64]*container
}
// Registry is a registrar where metrics are listed.
@ -72,8 +87,8 @@ type Registry interface {
// This builds a new metric registry. It is not needed in the majority of
// cases.
func NewRegistry() Registry {
return registry{
signatureContainers: make(map[string]container),
return &registry{
signatureContainers: make(map[uint64]*container),
}
}
@ -83,33 +98,28 @@ func Register(name, docstring string, baseLabels map[string]string, metric Metri
}
// Implements json.Marshaler
func (r registry) MarshalJSON() (_ []byte, err error) {
metrics := make([]interface{}, 0, len(r.signatureContainers))
func (r *registry) MarshalJSON() ([]byte, error) {
containers := make(containers, 0, len(r.signatureContainers))
keys := make([]string, 0, len(metrics))
for key := range r.signatureContainers {
keys = append(keys, key)
for _, container := range r.signatureContainers {
containers = append(containers, container)
}
sort.Strings(keys)
sort.Sort(containers)
for _, key := range keys {
metrics = append(metrics, r.signatureContainers[key])
}
return json.Marshal(metrics)
return json.Marshal(containers)
}
// isValidCandidate returns true if the candidate is acceptable for use. In the
// event of any apparent incorrect use it will report the problem, invalidate
// the candidate, or outright abort.
func (r registry) isValidCandidate(name string, baseLabels map[string]string) (signature string, err error) {
func (r *registry) isValidCandidate(name string, baseLabels map[string]string) (signature uint64, err error) {
if len(name) == 0 {
err = fmt.Errorf("unnamed metric named with baseLabels %s is invalid", baseLabels)
if abortOnMisuse {
if *abortOnMisuse {
panic(err)
} else if debugRegistration {
} else if *debugRegistration {
log.Println(err)
}
}
@ -117,13 +127,13 @@ func (r registry) isValidCandidate(name string, baseLabels map[string]string) (s
if _, contains := baseLabels[nameLabel]; contains {
err = fmt.Errorf("metric named %s with baseLabels %s contains reserved label name %s in baseLabels", name, baseLabels, nameLabel)
if abortOnMisuse {
if *abortOnMisuse {
panic(err)
} else if debugRegistration {
} else if *debugRegistration {
log.Println(err)
}
return
return signature, err
}
baseLabels[nameLabel] = name
@ -131,34 +141,34 @@ func (r registry) isValidCandidate(name string, baseLabels map[string]string) (s
if _, contains := r.signatureContainers[signature]; contains {
err = fmt.Errorf("metric named %s with baseLabels %s is already registered", name, baseLabels)
if abortOnMisuse {
if *abortOnMisuse {
panic(err)
} else if debugRegistration {
} else if *debugRegistration {
log.Println(err)
}
return
return signature, err
}
if useAggressiveSanityChecks {
if *useAggressiveSanityChecks {
for _, container := range r.signatureContainers {
if container.name == name {
err = fmt.Errorf("metric named %s with baseLabels %s is already registered as %s and risks causing confusion", name, baseLabels, container.BaseLabels)
if abortOnMisuse {
if *abortOnMisuse {
panic(err)
} else if debugRegistration {
} else if *debugRegistration {
log.Println(err)
}
return
return signature, err
}
}
}
return
return signature, err
}
func (r registry) Register(name, docstring string, baseLabels map[string]string, metric Metric) (err error) {
func (r *registry) Register(name, docstring string, baseLabels map[string]string, metric Metric) error {
r.mutex.Lock()
defer r.mutex.Unlock()
@ -168,25 +178,26 @@ func (r registry) Register(name, docstring string, baseLabels map[string]string,
signature, err := r.isValidCandidate(name, baseLabels)
if err != nil {
return
return err
}
r.signatureContainers[signature] = container{
r.signatureContainers[signature] = &container{
BaseLabels: baseLabels,
Docstring: docstring,
Metric: metric,
name: name,
}
return
return nil
}
// YieldBasicAuthExporter creates a http.HandlerFunc that is protected by HTTP's
// basic authentication.
func (register registry) YieldBasicAuthExporter(username, password string) http.HandlerFunc {
func (register *registry) YieldBasicAuthExporter(username, password string) http.HandlerFunc {
// XXX: Work with Daniel to get this removed from the library, as it is really
// superfluous and can be much more elegantly accomplished via
// delegation.
log.Println("Registry.YieldBasicAuthExporter is deprecated.")
exporter := register.YieldExporter()
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
@ -225,22 +236,46 @@ func decorateWriter(request *http.Request, writer http.ResponseWriter) io.Writer
return gziper
}
func (registry registry) YieldExporter() http.HandlerFunc {
func (registry *registry) YieldExporter() http.HandlerFunc {
log.Println("Registry.YieldExporter is deprecated in favor of Registry.Handler.")
return registry.Handler()
}
func (registry registry) Handler() http.HandlerFunc {
func (r *registry) dumpDelimitedPB(w io.Writer) {
r.mutex.RLock()
defer r.mutex.RUnlock()
f := new(dto.MetricFamily)
for _, container := range r.signatureContainers {
f.Reset()
f.Name = proto.String(container.name)
f.Help = proto.String(container.Docstring)
container.Metric.dumpChildren(f)
for name, value := range container.BaseLabels {
p := &dto.LabelPair{
Name: proto.String(name),
Value: proto.String(value),
}
for _, child := range f.Metric {
child.Label = append(child.Label, p)
}
}
ext.WriteDelimited(w, f)
}
}
func (registry *registry) Handler() http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
defer requestLatencyAccumulator(time.Now())
requestCount.Increment(nil)
url := r.URL
if strings.HasSuffix(url.Path, jsonSuffix) {
header := w.Header()
header.Set(contentTypeHeader, TelemetryContentType)
writer := decorateWriter(r, w)
@ -248,15 +283,34 @@ func (registry registry) Handler() http.HandlerFunc {
defer closer.Close()
}
json.NewEncoder(writer).Encode(registry)
} else {
w.WriteHeader(http.StatusNotFound)
accepts := goautoneg.ParseAccept(r.Header.Get("Accept"))
for _, accept := range accepts {
if accept.Type != "application" {
continue
}
if accept.SubType == "vnd.google.protobuf" {
if accept.Params["proto"] != "io.prometheus.client.MetricFamily" {
continue
}
if accept.Params["encoding"] != "delimited" {
continue
}
header.Set(contentTypeHeader, DelimitedTelemetryContentType)
registry.dumpDelimitedPB(writer)
return
}
}
func init() {
flag.BoolVar(&abortOnMisuse, FlagNamespace+"abortonmisuse", false, "abort if a semantic misuse is encountered (bool).")
flag.BoolVar(&debugRegistration, FlagNamespace+"debugregistration", false, "display information about the metric registration process (bool).")
flag.BoolVar(&useAggressiveSanityChecks, FlagNamespace+"useaggressivesanitychecks", false, "perform expensive validation of metrics (bool).")
header.Set(contentTypeHeader, TelemetryContentType)
json.NewEncoder(writer).Encode(registry)
}
}
var (
abortOnMisuse = flag.Bool(FlagNamespace+"abortonmisuse", false, "abort if a semantic misuse is encountered (bool).")
debugRegistration = flag.Bool(FlagNamespace+"debugregistration", false, "display information about the metric registration process (bool).")
useAggressiveSanityChecks = flag.Bool(FlagNamespace+"useaggressivesanitychecks", false, "perform expensive validation of metrics (bool).")
)

View File

@ -13,6 +13,8 @@ import (
"io"
"net/http"
"testing"
"code.google.com/p/goprotobuf/proto"
)
func testRegister(t tester) {
@ -21,14 +23,14 @@ func testRegister(t tester) {
debugRegistration bool
useAggressiveSanityChecks bool
}{
abortOnMisuse: abortOnMisuse,
debugRegistration: debugRegistration,
useAggressiveSanityChecks: useAggressiveSanityChecks,
abortOnMisuse: *abortOnMisuse,
debugRegistration: *debugRegistration,
useAggressiveSanityChecks: *useAggressiveSanityChecks,
}
defer func() {
abortOnMisuse = oldState.abortOnMisuse
debugRegistration = oldState.debugRegistration
useAggressiveSanityChecks = oldState.useAggressiveSanityChecks
abortOnMisuse = &(oldState.abortOnMisuse)
debugRegistration = &(oldState.debugRegistration)
useAggressiveSanityChecks = &(oldState.useAggressiveSanityChecks)
}()
type input struct {
@ -139,9 +141,9 @@ func testRegister(t tester) {
t.Fatalf("%d. expected scenario output length %d, got %d", i, len(scenario.inputs), len(scenario.outputs))
}
abortOnMisuse = false
debugRegistration = false
useAggressiveSanityChecks = true
abortOnMisuse = proto.Bool(false)
debugRegistration = proto.Bool(false)
useAggressiveSanityChecks = proto.Bool(true)
registry := NewRegistry()
@ -297,7 +299,7 @@ func testDumpToWriter(t tester) {
}
for i, scenario := range scenarios {
registry := NewRegistry().(registry)
registry := NewRegistry().(*registry)
for name, metric := range scenario.in.metrics {
err := registry.Register(name, fmt.Sprintf("metric %s", name), map[string]string{fmt.Sprintf("label_%s", name): name}, metric)

View File

@ -7,35 +7,28 @@
package prometheus
import (
"bytes"
"fmt"
"hash/fnv"
"sort"
)
const (
delimiter = "|"
"github.com/prometheus/client_golang/model"
)
// LabelsToSignature provides a way of building a unique signature
// (i.e., fingerprint) for a given label set sequence.
func labelsToSignature(labels map[string]string) string {
// TODO(matt): This is a wart, and we'll want to validate that collisions
// do not occur in less-than-diligent environments.
cardinality := len(labels)
keys := make([]string, 0, cardinality)
for label := range labels {
keys = append(keys, label)
func labelsToSignature(labels map[string]string) uint64 {
names := make(model.LabelNames, 0, len(labels))
for name := range labels {
names = append(names, model.LabelName(name))
}
sort.Strings(keys)
sort.Sort(names)
buffer := bytes.Buffer{}
hasher := fnv.New64a()
for _, label := range keys {
buffer.WriteString(label)
buffer.WriteString(delimiter)
buffer.WriteString(labels[label])
for _, name := range names {
fmt.Fprintf(hasher, string(name), labels[string(name)])
}
return buffer.String()
return hasher.Sum64()
}

View File

@ -13,13 +13,16 @@ import (
func testLabelsToSignature(t tester) {
var scenarios = []struct {
in map[string]string
out string
out uint64
}{
{
in: map[string]string{},
out: "",
out: 14695981039346656037,
},
{
in: map[string]string{"name": "garland, briggs", "fear": "love is not enough"},
out: 15753083015552662396,
},
{},
}
for i, scenario := range scenarios {

1
vendor/goautoneg/MANIFEST vendored Normal file
View File

@ -0,0 +1 @@
Imported at 75cd24fc2f2c from https://bitbucket.org/ww/goautoneg.

13
vendor/goautoneg/Makefile vendored Normal file
View File

@ -0,0 +1,13 @@
include $(GOROOT)/src/Make.inc
TARG=bitbucket.org/ww/goautoneg
GOFILES=autoneg.go
include $(GOROOT)/src/Make.pkg
format:
gofmt -w *.go
docs:
gomake clean
godoc ${TARG} > README.txt

67
vendor/goautoneg/README.txt vendored Normal file
View File

@ -0,0 +1,67 @@
PACKAGE
package goautoneg
import "bitbucket.org/ww/goautoneg"
HTTP Content-Type Autonegotiation.
The functions in this package implement the behaviour specified in
http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html
Copyright (c) 2011, Open Knowledge Foundation Ltd.
All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are
met:
Redistributions of source code must retain the above copyright
notice, this list of conditions and the following disclaimer.
Redistributions in binary form must reproduce the above copyright
notice, this list of conditions and the following disclaimer in
the documentation and/or other materials provided with the
distribution.
Neither the name of the Open Knowledge Foundation Ltd. nor the
names of its contributors may be used to endorse or promote
products derived from this software without specific prior written
permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
FUNCTIONS
func Negotiate(header string, alternatives []string) (content_type string)
Negotiate the most appropriate content_type given the accept header
and a list of alternatives.
func ParseAccept(header string) (accept []Accept)
Parse an Accept Header string returning a sorted list
of clauses
TYPES
type Accept struct {
Type, SubType string
Q float32
Params map[string]string
}
Structure to represent a clause in an HTTP Accept Header
SUBDIRECTORIES
.hg

162
vendor/goautoneg/autoneg.go vendored Normal file
View File

@ -0,0 +1,162 @@
/*
HTTP Content-Type Autonegotiation.
The functions in this package implement the behaviour specified in
http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html
Copyright (c) 2011, Open Knowledge Foundation Ltd.
All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are
met:
Redistributions of source code must retain the above copyright
notice, this list of conditions and the following disclaimer.
Redistributions in binary form must reproduce the above copyright
notice, this list of conditions and the following disclaimer in
the documentation and/or other materials provided with the
distribution.
Neither the name of the Open Knowledge Foundation Ltd. nor the
names of its contributors may be used to endorse or promote
products derived from this software without specific prior written
permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
package goautoneg
import (
"sort"
"strconv"
"strings"
)
// Structure to represent a clause in an HTTP Accept Header
type Accept struct {
Type, SubType string
Q float64
Params map[string]string
}
// For internal use, so that we can use the sort interface
type accept_slice []Accept
func (accept accept_slice) Len() int {
slice := []Accept(accept)
return len(slice)
}
func (accept accept_slice) Less(i, j int) bool {
slice := []Accept(accept)
ai, aj := slice[i], slice[j]
if ai.Q > aj.Q {
return true
}
if ai.Type != "*" && aj.Type == "*" {
return true
}
if ai.SubType != "*" && aj.SubType == "*" {
return true
}
return false
}
func (accept accept_slice) Swap(i, j int) {
slice := []Accept(accept)
slice[i], slice[j] = slice[j], slice[i]
}
// Parse an Accept Header string returning a sorted list
// of clauses
func ParseAccept(header string) (accept []Accept) {
parts := strings.Split(header, ",")
accept = make([]Accept, 0, len(parts))
for _, part := range parts {
part := strings.Trim(part, " ")
a := Accept{}
a.Params = make(map[string]string)
a.Q = 1.0
mrp := strings.Split(part, ";")
media_range := mrp[0]
sp := strings.Split(media_range, "/")
a.Type = strings.Trim(sp[0], " ")
switch {
case len(sp) == 1 && a.Type == "*":
a.SubType = "*"
case len(sp) == 2:
a.SubType = strings.Trim(sp[1], " ")
default:
continue
}
if len(mrp) == 1 {
accept = append(accept, a)
continue
}
for _, param := range mrp[1:] {
sp := strings.SplitN(param, "=", 2)
if len(sp) != 2 {
continue
}
token := strings.Trim(sp[0], " ")
if token == "q" {
a.Q, _ = strconv.ParseFloat(sp[1], 32)
} else {
a.Params[token] = strings.Trim(sp[1], " ")
}
}
accept = append(accept, a)
}
slice := accept_slice(accept)
sort.Sort(slice)
return
}
// Negotiate the most appropriate content_type given the accept header
// and a list of alternatives.
func Negotiate(header string, alternatives []string) (content_type string) {
asp := make([][]string, 0, len(alternatives))
for _, ctype := range alternatives {
asp = append(asp, strings.SplitN(ctype, "/", 2))
}
for _, clause := range ParseAccept(header) {
for i, ctsp := range asp {
if clause.Type == ctsp[0] && clause.SubType == ctsp[1] {
content_type = alternatives[i]
return
}
if clause.Type == ctsp[0] && clause.SubType == "*" {
content_type = alternatives[i]
return
}
if clause.Type == "*" && clause.SubType == "*" {
content_type = alternatives[i]
return
}
}
}
return
}

33
vendor/goautoneg/autoneg_test.go vendored Normal file
View File

@ -0,0 +1,33 @@
package goautoneg
import (
"testing"
)
var chrome = "application/xml,application/xhtml+xml,text/html;q=0.9,text/plain;q=0.8,image/png,*/*;q=0.5"
func TestParseAccept(t *testing.T) {
alternatives := []string{"text/html", "image/png"}
content_type := Negotiate(chrome, alternatives)
if content_type != "image/png" {
t.Errorf("got %s expected image/png", content_type)
}
alternatives = []string{"text/html", "text/plain", "text/n3"}
content_type = Negotiate(chrome, alternatives)
if content_type != "text/html" {
t.Errorf("got %s expected text/html", content_type)
}
alternatives = []string{"text/n3", "text/plain"}
content_type = Negotiate(chrome, alternatives)
if content_type != "text/plain" {
t.Errorf("got %s expected text/plain", content_type)
}
alternatives = []string{"text/n3", "application/rdf+xml"}
content_type = Negotiate(chrome, alternatives)
if content_type != "text/n3" {
t.Errorf("got %s expected text/n3", content_type)
}
}