Skip to content

Commit a9e5d79

Browse files
committed
http and registry work
1 parent bd3a366 commit a9e5d79

File tree

11 files changed

+616
-0
lines changed

11 files changed

+616
-0
lines changed

pkg/http/ErrNotNegotiable.go

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
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+
"fmt"
12+
)
13+
14+
// ErrNotNegotiable is used when the server cannot negotiate a format given an accept header.
15+
type ErrNotNegotiable struct {
16+
Value string // the name of the unknown format
17+
}
18+
19+
// Error returns the error formatted as a string.
20+
func (e ErrNotNegotiable) Error() string {
21+
return fmt.Sprintf("could not negotiate format from string %q", e.Value)
22+
}

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: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
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+
"sort"
13+
"strconv"
14+
"strings"
15+
16+
"github.com/pkg/errors"
17+
18+
"github.com/spatialcurrent/go-simple-serializer/pkg/registry"
19+
)
20+
21+
var (
22+
ErrMissingAcceptHeader = errors.New("missing accept header")
23+
ErrMissingRegistry = errors.New("missing file type registry")
24+
)
25+
26+
// NegotiateFormat negotitates the format for the response based on the incoming request and the given file type registry.
27+
// Returns the matching content type, followed by the format known to GSS, and then an error if any.
28+
func NegotiateFormat(r *http.Request, reg *registry.Registry) (string, string, error) {
29+
30+
accept := strings.TrimSpace(r.Header.Get(HeaderAccept))
31+
32+
if len(accept) == 0 {
33+
return "", "", ErrMissingAcceptHeader
34+
}
35+
36+
if reg == nil {
37+
return "", "", ErrMissingRegistry
38+
}
39+
40+
// Parse accept header into map of weights to accepted values
41+
values := map[float64][]string{}
42+
for _, str := range strings.SplitN(accept, ",", -1) {
43+
v := strings.TrimSpace(str)
44+
if strings.Contains(v, ";q=") {
45+
parts := strings.SplitN(v, ";q=", 2)
46+
w, err := strconv.ParseFloat(parts[1], 64)
47+
if err != nil {
48+
return "", "", errors.Wrapf(err, "could not parse quality value for value %q", v)
49+
}
50+
if _, ok := values[w]; !ok {
51+
values[w] = make([]string, 0)
52+
}
53+
values[w] = append(values[w], strings.TrimSpace(parts[0]))
54+
55+
} else {
56+
if _, ok := values[1.0]; !ok {
57+
values[1.0] = make([]string, 0)
58+
}
59+
values[1.0] = append(values[1.0], v)
60+
}
61+
}
62+
63+
// Create list of weights
64+
weights := make([]float64, 0, len(values))
65+
for w := range values {
66+
weights = append(weights, w)
67+
}
68+
69+
// Sort by weigt in descending order
70+
sort.SliceStable(weights, func(i, j int) bool {
71+
return weights[i] > weights[j]
72+
})
73+
74+
// Iterate through accepted values in order of highest weight first
75+
for _, w := range weights {
76+
for _, contentType := range values[w] {
77+
if item, ok := reg.LookupContentType(contentType); ok {
78+
return contentType, item.Format, nil
79+
}
80+
}
81+
}
82+
return "", "", &ErrNotNegotiable{Value: accept}
83+
}

pkg/http/NegotiateFormat_test.go

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
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/httptest"
12+
"testing"
13+
14+
"github.com/stretchr/testify/assert"
15+
16+
"github.com/spatialcurrent/go-simple-serializer/pkg/serializer"
17+
)
18+
19+
func TestNegotiateFormatJSON(t *testing.T) {
20+
reg := NewDefaultRegistry()
21+
r := httptest.NewRequest("GET", "https://example.com/foo/bar", nil)
22+
r.Header.Set("Accept", "application/json")
23+
c, f, err := NegotiateFormat(r, reg)
24+
assert.NoError(t, err)
25+
assert.Equal(t, "application/json", c)
26+
assert.Equal(t, serializer.FormatJSON, f)
27+
}
28+
29+
func TestNegotiateFormatBSON(t *testing.T) {
30+
reg := NewDefaultRegistry()
31+
r := httptest.NewRequest("GET", "https://example.com/foo/bar", nil)
32+
r.Header.Set("Accept", "application/ubjson, application/json")
33+
c, f, err := NegotiateFormat(r, reg)
34+
assert.NoError(t, err)
35+
assert.Equal(t, "application/ubjson", c)
36+
assert.Equal(t, serializer.FormatBSON, f)
37+
}
38+
39+
func TestNegotiateFormatWeight(t *testing.T) {
40+
reg := NewDefaultRegistry()
41+
r := httptest.NewRequest("GET", "https://example.com/foo/bar", nil)
42+
r.Header.Set("Accept", "text/csv;q=0.8, application/json;q=0.9")
43+
c, f, err := NegotiateFormat(r, reg)
44+
assert.NoError(t, err)
45+
assert.Equal(t, "application/json", c)
46+
assert.Equal(t, serializer.FormatJSON, f)
47+
}

pkg/http/Respond.go

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
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+
13+
"github.com/pkg/errors"
14+
15+
"github.com/spatialcurrent/go-simple-serializer/pkg/registry"
16+
"github.com/spatialcurrent/go-simple-serializer/pkg/serializer"
17+
)
18+
19+
// Respond writes the given data to the respond writer, and returns an error if any.
20+
// If filename is not empty, then the "Content-Disposition" header is set to "attachment; filename=<FILENAME>".
21+
func Respond(w http.ResponseWriter, r *http.Request, reg *registry.Registry, data interface{}, status int, filename string) error {
22+
23+
contentType, format, err := NegotiateFormat(r, reg)
24+
if err != nil {
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+
}
35+
}
36+
37+
s := serializer.New(format)
38+
39+
body, err := s.Serialize(data)
40+
if err != nil {
41+
return errors.Wrap(err, "error serializing response body")
42+
}
43+
44+
return RespondWithContent(w, body, contentType, status, filename)
45+
}

pkg/http/RespondWithContent.go

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
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+
"fmt"
12+
"net/http"
13+
14+
"github.com/pkg/errors"
15+
)
16+
17+
// RespondWithContent writes the given content to the response writer, and returns an error if any.
18+
// If filename is not empty, then the "Content-Disposition" header is set to "attachment; filename=<FILENAME>".
19+
func RespondWithContent(w http.ResponseWriter, body []byte, contentType string, status int, filename string) error {
20+
21+
if len(filename) > 0 {
22+
w.Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=%s", filename))
23+
}
24+
25+
w.Header().Set("Content-Type", contentType)
26+
27+
if status != http.StatusOK {
28+
w.WriteHeader(status)
29+
}
30+
31+
_, err := w.Write(body)
32+
if err != nil {
33+
return errors.Wrap(err, "error writing response body")
34+
}
35+
36+
return nil
37+
}

0 commit comments

Comments
 (0)