go项目脚手架
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
sorbet/pkg/rsp/negotiator.go

277 lines
6.3 KiB

package rsp
import (
"fmt"
"github.com/labstack/echo/v4"
"sort"
"strconv"
"strings"
)
type cache struct {
hint int
slice AcceptSlice
}
// Negotiator An HTTP content negotiator
//
// Accept: <MIME_type>/<MIME_subtype>
// Accept: <MIME_type>/*
// Accept: */*
// Accept: text/html, application/xhtml+xml, application/xml;q=0.9, image/webp, */*;q=0.8
type Negotiator struct {
capacity int // for cache
caches map[string]*cache
}
func NewNegotiator(capacity int) *Negotiator {
if capacity <= 0 {
capacity = 10
}
return &Negotiator{
capacity: capacity,
caches: make(map[string]*cache),
}
}
func (n *Negotiator) Slice(header string) AcceptSlice {
if c, ok := n.caches[header]; ok {
c.hint++
return c.slice
}
if len(n.caches) >= n.capacity {
var s string
var hint int
for i, x := range n.caches {
if hint == 0 || hint < x.hint {
hint = x.hint
s = i
}
}
delete(n.caches, s)
}
slice := newSlice(header)
n.caches[header] = &cache{1, slice}
return slice
}
func (n *Negotiator) Is(header string, expects ...string) bool {
return n.Slice(header).Is(expects...)
}
func (n *Negotiator) Type(header string, expects ...string) string {
return n.Slice(header).Type(expects...)
}
// Accept represents a parsed `Accept` header.
type Accept struct {
Type, Subtype string
Q float64
mime string
}
func (a *Accept) Mime() string {
if a.mime == "" {
a.mime = a.Type + "/" + a.Subtype
}
return a.mime
}
// AcceptSlice is a slice of accept.
type AcceptSlice []Accept
// newSlice parses an HTTP Accept header and returns AcceptSlice, sorted in
// decreasing order of preference. If the header lists multiple types that
// have the same level of preference (same specificity of a type and subtype,
// same qvalue, and same number of extensions), the type that was listed
// in the header first comes in the returned value.
//
// See http://www.w3.org/Protocols/rfc2616/rfc2616-sec14 for more information.
func newSlice(header string) AcceptSlice {
mediaRanges := strings.Split(header, ",")
accepted := make(AcceptSlice, 0, len(mediaRanges))
for _, mediaRange := range mediaRanges {
rangeParams, typeSubtype, err := parseMediaRange(mediaRange)
if err != nil {
continue
}
item := Accept{
Type: typeSubtype[0],
Subtype: typeSubtype[1],
Q: 1.0,
}
// If there is only one rangeParams, we can stop here.
if len(rangeParams) == 1 {
accepted = append(accepted, item)
continue
}
// Validate the rangeParams.
validParams := true
for _, v := range rangeParams[1:] {
nameVal := strings.SplitN(v, "=", 2)
if len(nameVal) != 2 {
validParams = false
break
}
nameVal[1] = strings.TrimSpace(nameVal[1])
if name := strings.TrimSpace(nameVal[0]); name == "q" {
qval, err := strconv.ParseFloat(nameVal[1], 64)
if err != nil || qval < 0 {
validParams = false
break
}
if qval > 1.0 {
qval = 1.0
}
item.Q = qval
//break // 不跳过,检查 validParams
}
}
if validParams {
accepted = append(accepted, item)
}
}
sort.Sort(accepted)
return accepted
}
// Len implements the Len() method of the Sort interface.
func (a AcceptSlice) Len() int {
return len(a)
}
// Less implements the Less() method of the Sort interface. Elements are
// sorted in order of decreasing preference.
func (a AcceptSlice) Less(i, j int) bool {
// Higher qvalues come first.
if a[i].Q > a[j].Q {
return true
} else if a[i].Q < a[j].Q {
return false
}
// Specific types come before wildcard types.
if a[i].Type != "*" && a[j].Type == "*" {
return true
} else if a[i].Type == "*" && a[j].Type != "*" {
return false
}
// Specific subtypes come before wildcard subtypes.
if a[i].Subtype != "*" && a[j].Subtype == "*" {
return true
} else if a[i].Subtype == "*" && a[j].Subtype != "*" {
return false
}
return false
}
// Swap implements the Swap() method of the Sort interface.
func (a AcceptSlice) Swap(i, j int) {
a[i], a[j] = a[j], a[i]
}
func (a AcceptSlice) Is(expect ...string) bool {
for _, e := range expect {
for _, s := range a {
if e == s.Mime() {
return true
}
}
}
return false
}
func (a AcceptSlice) Type(expects ...string) string {
if len(expects) == 0 {
return ""
}
var fuzzies [][2]string
for _, expect := range expects {
switch expect {
case "html":
if a.Is(echo.MIMETextHTML) {
return expect
}
fuzzies = append(fuzzies, [2]string{expect, "text/*"})
case "json":
if a.Is(echo.MIMEApplicationJSON) {
return expect
}
fuzzies = append(fuzzies, [2]string{expect, "text/*"})
case "jsonp":
if a.Is(echo.MIMEApplicationJavaScript) {
return expect
}
case "xml":
if a.Is(echo.MIMEApplicationXML, echo.MIMETextXML) {
return expect
}
fuzzies = append(fuzzies, [2]string{expect, "text/*"})
case "form":
if a.Is(echo.MIMEMultipartForm, echo.MIMEApplicationForm) {
return expect
}
case "protobuf":
if a.Is(echo.MIMEApplicationProtobuf) {
return expect
}
case "msgpack":
if a.Is(echo.MIMEApplicationMsgpack) {
return expect
}
case "text", "string":
if a.Is(echo.MIMETextPlain) {
return expect
}
default:
_, typeSubtype, err := parseMediaRange(expect)
if err != nil {
continue
}
if a.Is(typeSubtype[0] + "/" + typeSubtype[1]) {
return expect
}
//if typeSubtype[0] == "text" {
// fuzzies = append(fuzzies, [2]string{expect, "text/*"})
//}
fuzzies = append(fuzzies, [2]string{expect, typeSubtype[0] + "/*"})
}
}
if fuzzies != nil {
for _, f := range fuzzies {
if a.Is(f[1]) {
return f[0]
}
}
}
if a.Is("*/*") {
return expects[0]
}
return ""
}
// parseMediaRange parses the provided media range, and on success returns the
// parsed range params and type/subtype pair.
func parseMediaRange(mediaRange string) (rangeParams, typeSubtype []string, err error) {
rangeParams = strings.Split(mediaRange, ";")
typeSubtype = strings.Split(rangeParams[0], "/")
// typeSubtype should have a length of exactly two.
if len(typeSubtype) > 2 {
err = fmt.Errorf("go-slim.dev/slim: invalid accept type '%s'", rangeParams[0])
return
} else {
typeSubtype = append(typeSubtype, "*")
}
// Sanitize typeSubtype.
typeSubtype[0] = strings.TrimSpace(typeSubtype[0])
typeSubtype[1] = strings.TrimSpace(typeSubtype[1])
if typeSubtype[0] == "" {
typeSubtype[0] = "*"
}
if typeSubtype[1] == "" {
typeSubtype[1] = "*"
}
return
}