Skip to content

Commit 4599aa0

Browse files
committed
breaking change: ContentType now follows mime package practice: Type and Subtype are not now split
1 parent 80e25bc commit 4599aa0

17 files changed

+146
-181
lines changed

echo4/echo4_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,6 @@ func TestRenderBestMatch_should_give_JSON_response_for_ajax_requests(t *testing.
7979
expect.Error(err).Not().ToHaveOccurred(t)
8080
expect.Number(w.Code).ToBe(t, 201)
8181
expect.Map(w.HeaderMap).ToHaveLength(t, 2)
82-
expect.String(w.Header().Get(ContentType)).ToBe(t, "application/json;charset=utf-8")
82+
expect.String(w.Header().Get(ContentType)).ToBe(t, "application/json")
8383
expect.String(w.Header().Get(ContentLanguage)).ToBe(t, "en")
8484
}

example_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -99,7 +99,7 @@ func Example() {
9999
// GET /request2 200
100100
// 4 headers
101101
// Content-Language: fr
102-
// Content-Type: application/json;charset=utf-8
102+
// Content-Type: application/json
103103
// Etag: "hash1"
104104
// Vary: Accept, Accept-Language
105105
//

header/contenttype.go

Lines changed: 39 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,11 @@ package header
22

33
import (
44
"io"
5+
"mime"
56
"net/http"
67
"strings"
78

89
"github.com/rickb777/acceptable/headername"
9-
"github.com/rickb777/acceptable/internal"
1010
)
1111

1212
// ContentType is a media type as defined in RFC-2045, RFC-2046, RFC-2231
@@ -15,11 +15,26 @@ import (
1515
// There may also be parameters (e.g. "charset=utf-8") and extension values.
1616
type ContentType struct {
1717
// Type and Subtype carry the media type, e.g. "text" and "html"
18-
Type, Subtype string
18+
MediaType string
1919
// Params and Extensions hold optional parameter information
2020
Params []KV
2121
}
2222

23+
func (ct ContentType) Split() (string, string) {
24+
t, s, _ := strings.Cut(ct.MediaType, "/")
25+
return t, s
26+
}
27+
28+
func (ct ContentType) Type() string {
29+
t, _ := ct.Split()
30+
return t
31+
}
32+
33+
func (ct ContentType) Subtype() string {
34+
_, s := ct.Split()
35+
return s
36+
}
37+
2338
// AsMediaRange converts this ContentType to a MediaRange.
2439
// The default quality should be 1.
2540
func (ct ContentType) AsMediaRange(quality float64) MediaRange {
@@ -44,33 +59,32 @@ func (ct ContentType) AsMediaRange(quality float64) MediaRange {
4459
//
4560
// where "*" is a wildcard.
4661
func (ct ContentType) IsTextual() bool {
47-
if ct.Type == "text" {
62+
t, s := ct.Split()
63+
if t == "text" {
4864
return true
4965
}
5066

51-
if ct.Type == "application" {
52-
return ct.Subtype == "json" ||
53-
ct.Subtype == "xml" ||
54-
strings.HasSuffix(ct.Subtype, "+xml") ||
55-
strings.HasSuffix(ct.Subtype, "+json")
67+
if t == "application" {
68+
return s == "json" ||
69+
s == "xml" ||
70+
strings.HasSuffix(s, "+xml") ||
71+
strings.HasSuffix(s, "+json")
5672
}
5773

58-
if ct.Type == "model" {
59-
return strings.HasSuffix(ct.Subtype, "+xml") ||
60-
strings.HasSuffix(ct.Subtype, "+json")
74+
if t == "model" {
75+
return strings.HasSuffix(s, "+xml") ||
76+
strings.HasSuffix(s, "+json")
6177
}
6278

63-
if ct.Type == "image" || ct.Type == "message" {
64-
return strings.HasSuffix(ct.Subtype, "+xml")
79+
if t == "image" || t == "message" {
80+
return strings.HasSuffix(s, "+xml")
6581
}
6682

6783
return false
6884
}
6985

7086
func (ct ContentType) writeTo(w io.StringWriter) {
71-
w.WriteString(ct.Type)
72-
w.WriteString("/")
73-
w.WriteString(ct.Subtype)
87+
w.WriteString(ct.MediaType)
7488
for _, p := range ct.Params {
7589
w.WriteString(";")
7690
w.WriteString(p.Key)
@@ -85,7 +99,7 @@ func (ct ContentType) String() string {
8599
return buf.String()
86100
}
87101

88-
var starStar = ContentType{Type: "*", Subtype: "*"}
102+
var starStar = ContentType{MediaType: "*/*"}
89103

90104
// ParseContentTypeFromHeaders gets the "Content-Type" header and returns
91105
// its parsed value.
@@ -103,34 +117,17 @@ func ParseContentType(ct string) ContentType {
103117
return starStar
104118
}
105119

106-
valueAndParams := Split(ct, ";").TrimSpace()
107-
t, s := internal.Split1(valueAndParams[0], '/')
108-
return contentTypeOf(t, s, valueAndParams[1:])
109-
}
110-
111-
// contentTypeOf builds a content type value with optional parameters.
112-
// The parameters are passed in as literal strings, e.g. "charset=utf-8".
113-
func contentTypeOf(typ, subtype string, paramKV []string) ContentType {
114-
if typ == "" {
120+
mt, params, err := mime.ParseMediaType(ct)
121+
if err != nil {
115122
return starStar
116123
}
117124

118-
if subtype == "" {
119-
subtype = "*"
120-
}
121-
122-
var params []KV
123-
if len(paramKV) > 0 {
124-
params = make([]KV, 0, len(paramKV))
125-
for _, p := range paramKV {
126-
k, v := internal.Split1(p, '=')
127-
params = append(params, KV{Key: k, Value: v})
125+
var paramsKV []KV
126+
if len(params) > 0 {
127+
paramsKV = make([]KV, 0, len(params))
128+
for k, v := range params {
129+
paramsKV = append(paramsKV, KV{Key: k, Value: v})
128130
}
129131
}
130-
131-
return ContentType{
132-
Type: typ,
133-
Subtype: subtype,
134-
Params: params,
135-
}
132+
return ContentType{MediaType: mt, Params: paramsKV}
136133
}

header/contenttype_test.go

Lines changed: 19 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -14,54 +14,32 @@ func TestParseContentTypeFromHeaders(t *testing.T) {
1414

1515
ct1 := ParseContentTypeFromHeaders(hdrs)
1616

17-
expect.Any(ct1).ToBe(t, ContentType{
18-
Type: "*",
19-
Subtype: "*",
20-
})
17+
expect.Any(ct1).ToBe(t, ContentType{MediaType: "*/*"})
2118

2219
hdrs.Set(headername.ContentType, "text/plain")
2320

2421
ct2 := ParseContentTypeFromHeaders(hdrs)
2522

26-
expect.Any(ct2).ToBe(t, ContentType{
27-
Type: "text",
28-
Subtype: "plain",
29-
})
23+
expect.Any(ct2).ToBe(t, ContentType{MediaType: "text/plain"})
3024
}
3125

3226
func TestParseContentType(t *testing.T) {
3327
// blank value is treated as star-star
34-
expect.Any(ParseContentType("")).ToBe(t, ContentType{
35-
Type: "*",
36-
Subtype: "*",
37-
})
28+
expect.Any(ParseContentType("")).ToBe(t, ContentType{MediaType: "*/*"})
3829

3930
// illegal value is treated as star-star
40-
expect.Any(ParseContentType("/")).ToBe(t, ContentType{
41-
Type: "*",
42-
Subtype: "*",
43-
})
31+
expect.Any(ParseContentType("/")).ToBe(t, ContentType{MediaType: "*/*"})
4432

4533
// illegal value is treated as star-star
46-
expect.Any(ParseContentType("/plain")).ToBe(t, ContentType{
47-
Type: "*",
48-
Subtype: "*",
49-
})
34+
expect.Any(ParseContentType("/plain")).ToBe(t, ContentType{MediaType: "*/*"})
5035

5136
// error case handled silently
52-
expect.Any(ParseContentType("text/")).ToBe(t, ContentType{
53-
Type: "text",
54-
Subtype: "*",
55-
})
37+
expect.Any(ParseContentType("text/")).ToBe(t, ContentType{MediaType: "*/*"})
5638

57-
expect.Any(ParseContentType("text/plain")).ToBe(t, ContentType{
58-
Type: "text",
59-
Subtype: "plain",
60-
})
39+
expect.Any(ParseContentType("text/plain")).ToBe(t, ContentType{MediaType: "text/plain"})
6140

6241
expect.Any(ParseContentType("text/html; charset=utf-8")).ToBe(t, ContentType{
63-
Type: "text",
64-
Subtype: "html",
42+
MediaType: "text/html",
6543
Params: []KV{{
6644
Key: "charset",
6745
Value: "utf-8",
@@ -71,26 +49,25 @@ func TestParseContentType(t *testing.T) {
7149

7250
func TestContentType_IsTextual(t *testing.T) {
7351
cases := []ContentType{
74-
{Type: "text", Subtype: "plain"},
75-
{Type: "application", Subtype: "json"},
76-
{Type: "application", Subtype: "geo+json"},
77-
{Type: "application", Subtype: "xml"},
78-
{Type: "application", Subtype: "xv+xml"},
79-
{Type: "image", Subtype: "svg+xml"},
80-
{Type: "message", Subtype: "imdn+xml"},
81-
{Type: "model", Subtype: "x3d+xml"},
82-
{Type: "model", Subtype: "gltf+json"},
52+
{MediaType: "text/plain"},
53+
{MediaType: "application/json"},
54+
{MediaType: "application/geo+json"},
55+
{MediaType: "application/xml"},
56+
{MediaType: "application/xv+xml"},
57+
{MediaType: "image/svg+xml"},
58+
{MediaType: "message/imdn+xml"},
59+
{MediaType: "model/x3d+xml"},
60+
{MediaType: "model/gltf+json"},
8361
}
8462
for _, c := range cases {
8563
expect.Bool(c.IsTextual()).I(c.String).ToBeTrue(t)
8664
}
87-
expect.Bool(ContentType{Type: "video", Subtype: "mp4"}.IsTextual()).ToBeFalse(t)
65+
expect.Bool(ContentType{MediaType: "video/mp4"}.IsTextual()).ToBeFalse(t)
8866
}
8967

9068
func TestContentType_String(t *testing.T) {
9169
ct := ContentType{
92-
Type: "text",
93-
Subtype: "html",
70+
MediaType: "text/html",
9471
Params: []KV{
9572
{Key: "charset", Value: "utf-8"},
9673
{Key: "level", Value: "1"},

header/mediarange.go

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -48,17 +48,19 @@ func (mr MediaRange) StrongerThan(other MediaRange) bool {
4848
return false
4949
}
5050

51-
if mr.Type != "*" {
52-
if other.Type == "*" {
51+
t1, s1 := mr.Split()
52+
t2, s2 := other.Split()
53+
if t1 != "*" {
54+
if t2 == "*" {
5355
return true
5456
}
55-
if mr.Subtype != "*" && other.Subtype == "*" {
57+
if s1 != "*" && s2 == "*" {
5658
return true
5759
}
5860
}
5961

60-
if mr.Type == other.Type {
61-
if mr.Subtype == other.Subtype {
62+
if t1 == t2 {
63+
if s1 == s2 {
6264
return len(mr.Params) > len(other.Params)
6365
}
6466
}
@@ -69,7 +71,7 @@ func (mr MediaRange) StrongerThan(other MediaRange) bool {
6971
// It does not include the quality value nor any of the extensions.
7072
func (mr MediaRange) Value() string {
7173
buf := &strings.Builder{}
72-
fmt.Fprintf(buf, "%s/%s", mr.Type, mr.Subtype)
74+
buf.WriteString(mr.MediaType)
7375
for _, p := range mr.Params {
7476
fmt.Fprintf(buf, ";%s=%s", p.Key, p.Value)
7577
}
@@ -91,7 +93,7 @@ func (mr MediaRange) String() string {
9193
// list is empty, the result holds a wildcard entry ("*/*").
9294
func (mrs MediaRanges) WithDefault() MediaRanges {
9395
if len(mrs) == 0 {
94-
return []MediaRange{{ContentType: ContentType{Type: "*", Subtype: "*"}, Quality: DefaultQuality}}
96+
return []MediaRange{{ContentType: ContentType{MediaType: "*/*"}, Quality: DefaultQuality}}
9597
}
9698
return mrs
9799
}

header/parse_mediarange.go

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -35,8 +35,10 @@ func parseMediaRangeHeader(acceptHeader string) MediaRanges {
3535
for _, part := range parts {
3636
valueAndParams := Split(part, ";").TrimSpace()
3737
if len(valueAndParams) == 1 {
38-
t, s := internal.Split1(strings.TrimSpace(valueAndParams[0]), '/')
39-
wvs = append(wvs, MediaRange{ContentType: ContentType{Type: t, Subtype: s}, Quality: DefaultQuality})
38+
wvs = append(wvs, MediaRange{
39+
ContentType: ContentType{MediaType: strings.TrimSpace(valueAndParams[0])},
40+
Quality: DefaultQuality,
41+
})
4042
} else {
4143
wvs = append(wvs, handleMediaRangeWithParams(valueAndParams[0], valueAndParams[1:]))
4244
}
@@ -47,7 +49,7 @@ func parseMediaRangeHeader(acceptHeader string) MediaRanges {
4749

4850
func handleMediaRangeWithParams(value string, acceptParams []string) MediaRange {
4951
wv := new(MediaRange)
50-
wv.Type, wv.Subtype = internal.Split1(value, '/')
52+
wv.MediaType = value
5153
wv.Quality = DefaultQuality
5254

5355
for _, ap := range acceptParams {

0 commit comments

Comments
 (0)