Skip to content

Commit c157caa

Browse files
committed
file type registry
1 parent 2c0b4cc commit c157caa

File tree

10 files changed

+334
-32
lines changed

10 files changed

+334
-32
lines changed

pkg/http/Ext.go

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
// =================================================================
2+
//
3+
// Copyright (C) 2019 Spatial Current, Inc. - All Rights Reserved
4+
// Released as open source under the MIT License. See LICENSE file.
5+
//
6+
// =================================================================
7+
8+
package http
9+
10+
import (
11+
"net/http"
12+
"path/filepath"
13+
14+
"github.com/pkg/errors"
15+
)
16+
17+
var (
18+
ErrMissingURL = errors.New("missing URL")
19+
)
20+
21+
// Ext returns the file name extension in the URL path.
22+
// The extension begins after the last period in the file element of the path.
23+
// If no period is in the last element or a period is the last character, then returns a blank string.
24+
func Ext(r *http.Request) (string, error) {
25+
if r.URL == nil {
26+
return "", ErrMissingURL
27+
}
28+
ext := filepath.Ext(r.URL.Path)
29+
if len(ext) == 0 {
30+
return "", nil
31+
}
32+
if ext == "." {
33+
return "", nil
34+
}
35+
return ext[1:], nil
36+
}

pkg/http/NegotiateFormat.go

Lines changed: 13 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -8,28 +8,35 @@
88
package http
99

1010
import (
11-
"github.com/pkg/errors"
12-
"github.com/spatialcurrent/go-simple-serializer/pkg/serializer"
1311
"net/http"
1412
"sort"
1513
"strconv"
1614
"strings"
15+
16+
"github.com/pkg/errors"
17+
18+
"github.com/spatialcurrent/go-simple-serializer/pkg/registry"
1719
)
1820

1921
var (
2022
ErrMissingAcceptHeader = errors.New("missing accept header")
23+
ErrMissingRegistry = errors.New("missing file type registry")
2124
)
2225

23-
// NegotiateFormat negotitates the format for the response based on the incoming request.
26+
// NegotiateFormat negotitates the format for the response based on the incoming request and the given file type registry.
2427
// Returns the matching content type, followed by the format known to GSS, and then an error if any.
25-
func NegotiateFormat(r *http.Request) (string, string, error) {
28+
func NegotiateFormat(r *http.Request, reg *registry.Registry) (string, string, error) {
2629

2730
accept := strings.TrimSpace(r.Header.Get(HeaderAccept))
2831

2932
if len(accept) == 0 {
3033
return "", "", ErrMissingAcceptHeader
3134
}
3235

36+
if reg == nil {
37+
return "", "", ErrMissingRegistry
38+
}
39+
3340
// Parse accept header into map of weights to accepted values
3441
values := map[float64][]string{}
3542
for _, str := range strings.SplitN(accept, ",", -1) {
@@ -67,19 +74,8 @@ func NegotiateFormat(r *http.Request) (string, string, error) {
6774
// Iterate through accepted values in order of highest weight first
6875
for _, w := range weights {
6976
for _, contentType := range values[w] {
70-
switch contentType {
71-
case "application/ubjson":
72-
return contentType, serializer.FormatBSON, nil
73-
case "application/json":
74-
return contentType, serializer.FormatJSON, nil
75-
case "application/toml":
76-
return contentType, serializer.FormatTOML, nil
77-
case "text/csv":
78-
return contentType, serializer.FormatCSV, nil
79-
case "text/tab-separated-values":
80-
return contentType, serializer.FormatTSV, nil
81-
case "text/yaml":
82-
return contentType, serializer.FormatYAML, nil
77+
if item, ok := reg.LookupContentType(contentType); ok {
78+
return contentType, item.Format, nil
8379
}
8480
}
8581
}

pkg/http/NegotiateFormat_test.go

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,32 +11,36 @@ import (
1111
"net/http/httptest"
1212
"testing"
1313

14-
"github.com/spatialcurrent/go-simple-serializer/pkg/serializer"
1514
"github.com/stretchr/testify/assert"
15+
16+
"github.com/spatialcurrent/go-simple-serializer/pkg/serializer"
1617
)
1718

1819
func TestNegotiateFormatJSON(t *testing.T) {
20+
reg := NewDefaultRegistry()
1921
r := httptest.NewRequest("GET", "https://example.com/foo/bar", nil)
2022
r.Header.Set("Accept", "application/json")
21-
c, f, err := NegotiateFormat(r)
23+
c, f, err := NegotiateFormat(r, reg)
2224
assert.NoError(t, err)
2325
assert.Equal(t, "application/json", c)
2426
assert.Equal(t, serializer.FormatJSON, f)
2527
}
2628

2729
func TestNegotiateFormatBSON(t *testing.T) {
30+
reg := NewDefaultRegistry()
2831
r := httptest.NewRequest("GET", "https://example.com/foo/bar", nil)
2932
r.Header.Set("Accept", "application/ubjson, application/json")
30-
c, f, err := NegotiateFormat(r)
33+
c, f, err := NegotiateFormat(r, reg)
3134
assert.NoError(t, err)
3235
assert.Equal(t, "application/ubjson", c)
3336
assert.Equal(t, serializer.FormatBSON, f)
3437
}
3538

3639
func TestNegotiateFormatWeight(t *testing.T) {
40+
reg := NewDefaultRegistry()
3741
r := httptest.NewRequest("GET", "https://example.com/foo/bar", nil)
3842
r.Header.Set("Accept", "text/csv;q=0.8, application/json;q=0.9")
39-
c, f, err := NegotiateFormat(r)
43+
c, f, err := NegotiateFormat(r, reg)
4044
assert.NoError(t, err)
4145
assert.Equal(t, "application/json", c)
4246
assert.Equal(t, serializer.FormatJSON, f)

pkg/http/Respond.go

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,18 +8,30 @@
88
package http
99

1010
import (
11+
"net/http"
12+
1113
"github.com/pkg/errors"
14+
15+
"github.com/spatialcurrent/go-simple-serializer/pkg/registry"
1216
"github.com/spatialcurrent/go-simple-serializer/pkg/serializer"
13-
"net/http"
1417
)
1518

1619
// Respond writes the given data to the respond writer, and returns an error if any.
1720
// If filename is not empty, then the "Content-Disposition" header is set to "attachment; filename=<FILENAME>".
18-
func Respond(w http.ResponseWriter, r *http.Request, data interface{}, status int, filename string) error {
21+
func Respond(w http.ResponseWriter, r *http.Request, reg *registry.Registry, data interface{}, status int, filename string) error {
1922

20-
contentType, format, err := NegotiateFormat(r)
23+
contentType, format, err := NegotiateFormat(r, reg)
2124
if err != nil {
22-
return err
25+
ext, err := Ext(r)
26+
if err != nil || len(ext) == 0 {
27+
return errors.Errorf("could not negotiate format or parse file extension from %#v", r)
28+
}
29+
if item, ok := reg.LookupExtension(ext); ok {
30+
contentType = item.ContentTypes[0]
31+
format = item.Format
32+
} else {
33+
return errors.Errorf("could not negotiate format or parse file extension from %#v", r)
34+
}
2335
}
2436

2537
s := serializer.New(format)

pkg/http/RespondWithContent.go

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,11 +9,12 @@ package http
99

1010
import (
1111
"fmt"
12-
"github.com/pkg/errors"
1312
"net/http"
13+
14+
"github.com/pkg/errors"
1415
)
1516

16-
// RespondWithContent writes the given content to the respond writer, and returns an error if any.
17+
// RespondWithContent writes the given content to the response writer, and returns an error if any.
1718
// If filename is not empty, then the "Content-Disposition" header is set to "attachment; filename=<FILENAME>".
1819
func RespondWithContent(w http.ResponseWriter, body []byte, contentType string, status int, filename string) error {
1920

pkg/http/Respond_test.go

Lines changed: 85 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -12,17 +12,39 @@ import (
1212
"net/http/httptest"
1313
"testing"
1414

15-
"github.com/stretchr/testify/assert"
1615
"io/ioutil"
16+
17+
"github.com/stretchr/testify/assert"
1718
)
1819

19-
func TestRespondJSON(t *testing.T) {
20+
func TestRespondAcceptCSV(t *testing.T) {
21+
reg := NewDefaultRegistry()
22+
w := httptest.NewRecorder()
23+
r := httptest.NewRequest("GET", "https://example.com/foo/bar", nil)
24+
r.Header.Set("Accept", "text/csv")
25+
data := map[string]interface{}{"foo": "bar"}
26+
status := http.StatusOK
27+
err := Respond(w, r, reg, data, status, "")
28+
if !assert.NoError(t, err) {
29+
return
30+
}
31+
resp := w.Result()
32+
assert.Equal(t, http.StatusOK, resp.StatusCode)
33+
body, err := ioutil.ReadAll(resp.Body)
34+
if !assert.NoError(t, err) {
35+
return
36+
}
37+
assert.Equal(t, "foo\nbar\n", string(body))
38+
}
39+
40+
func TestRespondAcceptJSON(t *testing.T) {
41+
reg := NewDefaultRegistry()
2042
w := httptest.NewRecorder()
2143
r := httptest.NewRequest("GET", "https://example.com/foo/bar", nil)
2244
r.Header.Set("Accept", "application/json")
2345
data := map[string]interface{}{"foo": "bar"}
2446
status := http.StatusOK
25-
err := Respond(w, r, data, status, "")
47+
err := Respond(w, r, reg, data, status, "")
2648
if !assert.NoError(t, err) {
2749
return
2850
}
@@ -35,13 +57,71 @@ func TestRespondJSON(t *testing.T) {
3557
assert.Equal(t, "{\"foo\":\"bar\"}", string(body))
3658
}
3759

38-
func TestRespondYAML(t *testing.T) {
60+
func TestRespondAcceptYAML(t *testing.T) {
61+
reg := NewDefaultRegistry()
3962
w := httptest.NewRecorder()
4063
r := httptest.NewRequest("GET", "https://example.com/foo/bar", nil)
4164
r.Header.Set("Accept", "application/json;q=0.8, text/yaml;q=0.9")
4265
data := map[string]interface{}{"foo": "bar"}
4366
status := http.StatusOK
44-
err := Respond(w, r, data, status, "")
67+
err := Respond(w, r, reg, data, status, "")
68+
if !assert.NoError(t, err) {
69+
return
70+
}
71+
resp := w.Result()
72+
assert.Equal(t, http.StatusOK, resp.StatusCode)
73+
body, err := ioutil.ReadAll(resp.Body)
74+
if !assert.NoError(t, err) {
75+
return
76+
}
77+
assert.Equal(t, "foo: bar\n", string(body))
78+
}
79+
80+
func TestRespondExtensionCSV(t *testing.T) {
81+
reg := NewDefaultRegistry()
82+
w := httptest.NewRecorder()
83+
r := httptest.NewRequest("GET", "https://example.com/foo/bar.csv", nil)
84+
data := map[string]interface{}{"foo": "bar"}
85+
status := http.StatusOK
86+
err := Respond(w, r, reg, data, status, "")
87+
if !assert.NoError(t, err) {
88+
return
89+
}
90+
resp := w.Result()
91+
assert.Equal(t, http.StatusOK, resp.StatusCode)
92+
body, err := ioutil.ReadAll(resp.Body)
93+
if !assert.NoError(t, err) {
94+
return
95+
}
96+
assert.Equal(t, "foo\nbar\n", string(body))
97+
}
98+
99+
func TestRespondExtensionJSON(t *testing.T) {
100+
reg := NewDefaultRegistry()
101+
w := httptest.NewRecorder()
102+
r := httptest.NewRequest("GET", "https://example.com/foo/bar.json", nil)
103+
data := map[string]interface{}{"foo": "bar"}
104+
status := http.StatusOK
105+
err := Respond(w, r, reg, data, status, "")
106+
if !assert.NoError(t, err) {
107+
return
108+
}
109+
resp := w.Result()
110+
assert.Equal(t, http.StatusOK, resp.StatusCode)
111+
body, err := ioutil.ReadAll(resp.Body)
112+
if !assert.NoError(t, err) {
113+
return
114+
}
115+
assert.Equal(t, "{\"foo\":\"bar\"}", string(body))
116+
}
117+
118+
func TestRespondExtensionYAML(t *testing.T) {
119+
reg := NewDefaultRegistry()
120+
w := httptest.NewRecorder()
121+
r := httptest.NewRequest("GET", "https://example.com/foo/bar.yml", nil)
122+
data := map[string]interface{}{"foo": "bar"}
123+
status := http.StatusOK
124+
err := Respond(w, r, reg, data, status, "")
45125
if !assert.NoError(t, err) {
46126
return
47127
}

pkg/http/http.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,8 +31,10 @@ var (
3131

3232
var (
3333
ContentTypeBSON = "application/ubjson"
34+
ContentTypeCSV = "text/csv"
3435
ContentTypeJSON = "application/json"
3536
ContentTypeTOML = "application/toml"
37+
ContentTypeTSV = "text/tab-separated-values"
3638
ContentTypeYAML = "text/yaml"
3739
ContentTypePlain = "text/plain; charset=utf-8"
3840
)

pkg/http/http_test.go

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
// =================================================================
2+
//
3+
// Copyright (C) 2019 Spatial Current, Inc. - All Rights Reserved
4+
// Released as open source under the MIT License. See LICENSE file.
5+
//
6+
// =================================================================
7+
8+
package http
9+
10+
import (
11+
"github.com/spatialcurrent/go-simple-serializer/pkg/registry"
12+
"github.com/spatialcurrent/go-simple-serializer/pkg/serializer"
13+
)
14+
15+
func NewDefaultRegistry() *registry.Registry {
16+
r := registry.New()
17+
r.Add(registry.Item{
18+
Format: serializer.FormatBSON,
19+
ContentTypes: []string{"application/ubjson"},
20+
Extensions: []string{},
21+
})
22+
r.Add(registry.Item{
23+
Format: serializer.FormatJSON,
24+
ContentTypes: []string{"application/json", "text/json"},
25+
Extensions: []string{"json"},
26+
})
27+
r.Add(registry.Item{
28+
Format: serializer.FormatCSV,
29+
ContentTypes: []string{"text/csv"},
30+
Extensions: []string{"csv"},
31+
})
32+
r.Add(registry.Item{
33+
Format: serializer.FormatYAML,
34+
ContentTypes: []string{"text/yaml"},
35+
Extensions: []string{"yaml", "yml"},
36+
})
37+
return r
38+
}

0 commit comments

Comments
 (0)