blob: 335cc6f7b71d7bf03bb7dfeae6fe9a170e7ba881 [file] [log] [blame]
package goscaffold
import (
"net/http"
"regexp"
"sort"
"strconv"
"strings"
)
var qParamRe = regexp.MustCompile("^q=([0-9\\.]+)$")
type acceptCriterion struct {
major string
minor string
precedence float32
origOrder int
match string
}
type acceptCriteria []acceptCriterion
/*
SelectMediaType matches a set of candidate media types against the
Accept header in an HTTP request. It does this using the rules from
RFC2616 section 1.4.
If no Accept header is present, then the first choice in the
"choices" array is returned.
If multiple choices match, then we will take the first one specified
in the "Accept" header.
If there are no compatible media types, then an empty string
is returned.
Only the first "Accept" header on the request is considered.
*/
func SelectMediaType(req *http.Request, choices []string) string {
hdr := strings.TrimSpace(req.Header.Get("Accept"))
if hdr == "" || hdr == "*" || hdr == "*/*" {
if len(choices) >= 1 {
return choices[0]
}
return ""
}
// Parse accept header and parse the criteria
candidates := parseAcceptHeader(hdr)
// For each choice, assign the best match (least wildcardy)
matches := make(map[string]*acceptCriterion)
for _, choice := range choices {
for c := range candidates {
crit := candidates[c]
if crit.matches(choice) {
if matches[choice] == nil ||
matches[choice].level() < crit.level() {
matches[choice] = &acceptCriterion{
minor: crit.minor,
major: crit.major,
precedence: crit.precedence,
origOrder: crit.origOrder,
match: choice,
}
}
}
}
}
if len(matches) == 0 {
return ""
}
// Sort the matches now by precedence level and original order
var sortedMatches acceptCriteria
for _, v := range matches {
sortedMatches = append(sortedMatches, *v)
}
sortedMatches.sort()
return sortedMatches[0].match
}
func parseAcceptHeader(hdr string) acceptCriteria {
var ret acceptCriteria
parts := strings.Split(hdr, ",")
for i, part := range parts {
candidate := parseAcceptPart(part, i)
ret = append(ret, candidate)
}
return ret
}
/*
parseAcceptPart parses a single section of the header and extracts the
"q" parameter.
*/
func parseAcceptPart(part string, order int) acceptCriterion {
params := strings.Split(part, ";")
finalType := strings.TrimSpace(params[0])
var precedence float32 = 1.0
for _, param := range params[1:] {
match := qParamRe.FindStringSubmatch(strings.TrimSpace(param))
if match == nil {
finalType += ";" + param
} else {
qVal, err := strconv.ParseFloat(match[1], 32)
if err == nil {
precedence = float32(qVal)
}
}
}
splitType := strings.SplitN(finalType, "/", 2)
return acceptCriterion{
major: splitType[0],
minor: splitType[1],
precedence: precedence,
origOrder: order,
}
}
/*
matches matches a candidate media type with a criterion.
*/
func (a acceptCriterion) matches(t string) bool {
st := strings.SplitN(t, "/", 2)
if a.major != "*" && a.major != st[0] {
return false
}
if a.minor != "*" && a.minor != st[1] {
return false
}
return true
}
func (a acceptCriterion) level() int {
if a.minor == "*" {
if a.major == "*" {
return 0
}
return 1
}
return 2
}
/*
sortCandidates sorts accept header candidates in order of:
1) Precedence (from the "q" parameter)
2) Original order in accept header
3) Stable sort otherwise
*/
func (c acceptCriteria) sort() {
sort.Stable(c)
}
func (c acceptCriteria) Less(i, j int) bool {
// Higher precedence goes first
if c[i].precedence > c[j].precedence {
return true
}
if c[i].precedence == c[j].precedence &&
c[i].origOrder < c[j].origOrder {
return true
}
return false
}
func (c acceptCriteria) Len() int {
return len(c)
}
func (c acceptCriteria) Swap(i, j int) {
tmp := c[i]
c[i] = c[j]
c[j] = tmp
}