-
Notifications
You must be signed in to change notification settings - Fork 40
/
accept.go
160 lines (129 loc) · 3.95 KB
/
accept.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
// Copyright (c) HashiCorp Inc. All rights reserved.
// Licensed under the MPL-2.0 License. See NOTICE.txt in the project root for license information.
package accept
import (
"fmt"
"sort"
"strconv"
"strings"
)
// Header represents an HTTP Accept header value
// See https://httpwg.org/specs/rfc9110.html#field.accept
type Header struct {
types []PreferredType
}
func (h Header) FirstChoice() *PreferredType {
if len(h.types) == 0 {
return nil
}
return &h.types[0]
}
func (h Header) String() string {
out := make([]string, 0)
for _, typ := range h.types {
out = append(out, fmt.Sprintf("%s", typ))
}
return strings.Join(out, ", ")
}
type PreferredType struct {
// ContentType is a singular media type (e.g. text/plain), or one partially containing
// wildcards (e.g. text/*), or entirely wildcards (e.g. */*)
ContentType string
// Parameters is a map of media type parameters, e.g. charset=utf-8
Parameters map[string]string
// Weight is the integer-normalized quality value representing the relative weight/preference
Weight qValue
}
func (t PreferredType) String() string {
out := []string{t.ContentType}
for p, v := range t.Parameters {
out = append(out, fmt.Sprintf("%s=%s", p, v))
}
out = append(out, fmt.Sprintf("q=%s", t.Weight))
return strings.Join(out, "; ")
}
// qValue is an integer-normalized representation of a quality value, which has a minimum value
// of 0.001, a maximum value of 1 and a maximum precision of 3 decimal places.
// See https://httpwg.org/specs/rfc9110.html#quality.values
type qValue uint16
func (q qValue) String() string {
return fmt.Sprintf("%d.%d", q/1000, q%1000)
}
func FromString(in string) (Header, error) {
contentTypes := strings.Split(in, ",")
if len(contentTypes) == 0 {
return Header{}, fmt.Errorf("empty header value provided")
}
types := make([]PreferredType, 0)
for _, typeRaw := range contentTypes {
// separate the parameters from the media type
split := strings.Split(strings.ToLower(typeRaw), ";")
if len(split) == 0 {
continue
}
typ := PreferredType{
ContentType: strings.TrimSpace(split[0]),
Parameters: make(map[string]string),
Weight: 1000, // default weight of 1.000
}
if len(split) > 1 {
params := split[1:]
for _, param := range params {
param = strings.TrimSpace(param)
if len(param) < 3 || strings.Index(param, "=") < 1 {
return Header{}, fmt.Errorf("invalid parameter for %q: %q", typ.ContentType, param)
}
p := strings.Split(param, "=")
if len(p) > 2 {
return Header{}, fmt.Errorf("parameter contains multiple `=` for %q: %q", typ.ContentType, param)
}
for i, v := range p {
p[i] = strings.TrimSpace(v)
}
// handle quality values and store other parameters in a map
switch p[0] {
case "q":
q := p[1]
// determine whether it contains useful decimal point
pIndex := strings.Index(q, ".")
if pIndex == len(q) {
q = q[0 : len(q)-1]
pIndex = -1
}
if pIndex > 0 {
zIndex := len(q)
// pad out the value to three decimal places
if zIndex-pIndex < 3 {
q = q + strings.Repeat("0", zIndex-pIndex)
zIndex = len(q)
}
// trim any precision > 3 decimal places
if zIndex-pIndex > 4 {
zIndex = pIndex + 4
}
// strip out the decimal point, so we are left with thousandths
q = q[0:pIndex] + q[pIndex+1:zIndex]
}
// convert the thousandths value to an int
weight, err := strconv.Atoi(q)
if err != nil {
return Header{}, fmt.Errorf("invalid weight for %q: %q", typ.ContentType, q)
}
// if an integer was supplied, just multiply it for normalized value
if pIndex < 1 {
typ.Weight = qValue(weight * 1000)
} else {
typ.Weight = qValue(weight)
}
default:
typ.Parameters[p[0]] = p[1]
}
}
}
types = append(types, typ)
}
sort.Slice(types, func(i, j int) bool {
return types[i].Weight > types[j].Weight
})
return Header{types: types}, nil
}