Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 22 additions & 0 deletions pkg/http/ErrNotNegotiable.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
// =================================================================
//
// Copyright (C) 2019 Spatial Current, Inc. - All Rights Reserved
// Released as open source under the MIT License. See LICENSE file.
//
// =================================================================

package http

import (
"fmt"
)

// ErrNotNegotiable is used when the server cannot negotiate a format given an accept header.
type ErrNotNegotiable struct {
Value string // the name of the unknown format
}

// Error returns the error formatted as a string.
func (e ErrNotNegotiable) Error() string {
return fmt.Sprintf("could not negotiate format from string %q", e.Value)
}
36 changes: 36 additions & 0 deletions pkg/http/Ext.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
// =================================================================
//
// Copyright (C) 2019 Spatial Current, Inc. - All Rights Reserved
// Released as open source under the MIT License. See LICENSE file.
//
// =================================================================

package http

import (
"net/http"
"path/filepath"

"github.com/pkg/errors"
)

var (
ErrMissingURL = errors.New("missing URL")
)

// Ext returns the file name extension in the URL path.
// The extension begins after the last period in the file element of the path.
// If no period is in the last element or a period is the last character, then returns a blank string.
func Ext(r *http.Request) (string, error) {
if r.URL == nil {
return "", ErrMissingURL
}
ext := filepath.Ext(r.URL.Path)
if len(ext) == 0 {
return "", nil
}
if ext == "." {
return "", nil
}
return ext[1:], nil
}
83 changes: 83 additions & 0 deletions pkg/http/NegotiateFormat.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
// =================================================================
//
// Copyright (C) 2019 Spatial Current, Inc. - All Rights Reserved
// Released as open source under the MIT License. See LICENSE file.
//
// =================================================================

package http

import (
"net/http"
"sort"
"strconv"
"strings"

"github.com/pkg/errors"

"github.com/spatialcurrent/go-simple-serializer/pkg/registry"
)

var (
ErrMissingAcceptHeader = errors.New("missing accept header")
ErrMissingRegistry = errors.New("missing file type registry")
)

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

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

if len(accept) == 0 {
return "", "", ErrMissingAcceptHeader
}

if reg == nil {
return "", "", ErrMissingRegistry
}

// Parse accept header into map of weights to accepted values
values := map[float64][]string{}
for _, str := range strings.SplitN(accept, ",", -1) {
v := strings.TrimSpace(str)
if strings.Contains(v, ";q=") {
parts := strings.SplitN(v, ";q=", 2)
w, err := strconv.ParseFloat(parts[1], 64)
if err != nil {
return "", "", errors.Wrapf(err, "could not parse quality value for value %q", v)
}
if _, ok := values[w]; !ok {
values[w] = make([]string, 0)
}
values[w] = append(values[w], strings.TrimSpace(parts[0]))

} else {
if _, ok := values[1.0]; !ok {
values[1.0] = make([]string, 0)
}
values[1.0] = append(values[1.0], v)
}
}

// Create list of weights
weights := make([]float64, 0, len(values))
for w := range values {
weights = append(weights, w)
}

// Sort by weigt in descending order
sort.SliceStable(weights, func(i, j int) bool {
return weights[i] > weights[j]
})

// Iterate through accepted values in order of highest weight first
for _, w := range weights {
for _, contentType := range values[w] {
if item, ok := reg.LookupContentType(contentType); ok {
return contentType, item.Format, nil
}
}
}
return "", "", &ErrNotNegotiable{Value: accept}
}
47 changes: 47 additions & 0 deletions pkg/http/NegotiateFormat_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
// =================================================================
//
// Copyright (C) 2019 Spatial Current, Inc. - All Rights Reserved
// Released as open source under the MIT License. See LICENSE file.
//
// =================================================================

package http

import (
"net/http/httptest"
"testing"

"github.com/stretchr/testify/assert"

"github.com/spatialcurrent/go-simple-serializer/pkg/serializer"
)

func TestNegotiateFormatJSON(t *testing.T) {
reg := NewDefaultRegistry()
r := httptest.NewRequest("GET", "https://example.com/foo/bar", nil)
r.Header.Set("Accept", "application/json")
c, f, err := NegotiateFormat(r, reg)
assert.NoError(t, err)
assert.Equal(t, "application/json", c)
assert.Equal(t, serializer.FormatJSON, f)
}

func TestNegotiateFormatBSON(t *testing.T) {
reg := NewDefaultRegistry()
r := httptest.NewRequest("GET", "https://example.com/foo/bar", nil)
r.Header.Set("Accept", "application/ubjson, application/json")
c, f, err := NegotiateFormat(r, reg)
assert.NoError(t, err)
assert.Equal(t, "application/ubjson", c)
assert.Equal(t, serializer.FormatBSON, f)
}

func TestNegotiateFormatWeight(t *testing.T) {
reg := NewDefaultRegistry()
r := httptest.NewRequest("GET", "https://example.com/foo/bar", nil)
r.Header.Set("Accept", "text/csv;q=0.8, application/json;q=0.9")
c, f, err := NegotiateFormat(r, reg)
assert.NoError(t, err)
assert.Equal(t, "application/json", c)
assert.Equal(t, serializer.FormatJSON, f)
}
45 changes: 45 additions & 0 deletions pkg/http/Respond.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
// =================================================================
//
// Copyright (C) 2019 Spatial Current, Inc. - All Rights Reserved
// Released as open source under the MIT License. See LICENSE file.
//
// =================================================================

package http

import (
"net/http"

"github.com/pkg/errors"

"github.com/spatialcurrent/go-simple-serializer/pkg/registry"
"github.com/spatialcurrent/go-simple-serializer/pkg/serializer"
)

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

contentType, format, err := NegotiateFormat(r, reg)
if err != nil {
ext, err := Ext(r)
if err != nil || len(ext) == 0 {
return errors.Errorf("could not negotiate format or parse file extension from %#v", r)
}
if item, ok := reg.LookupExtension(ext); ok {
contentType = item.ContentTypes[0]
format = item.Format
} else {
return errors.Errorf("could not negotiate format or parse file extension from %#v", r)
}
}

s := serializer.New(format)

body, err := s.Serialize(data)
if err != nil {
return errors.Wrap(err, "error serializing response body")
}

return RespondWithContent(w, body, contentType, status, filename)
}
37 changes: 37 additions & 0 deletions pkg/http/RespondWithContent.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
// =================================================================
//
// Copyright (C) 2019 Spatial Current, Inc. - All Rights Reserved
// Released as open source under the MIT License. See LICENSE file.
//
// =================================================================

package http

import (
"fmt"
"net/http"

"github.com/pkg/errors"
)

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

if len(filename) > 0 {
w.Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=%s", filename))
}

w.Header().Set("Content-Type", contentType)

if status != http.StatusOK {
w.WriteHeader(status)
}

_, err := w.Write(body)
if err != nil {
return errors.Wrap(err, "error writing response body")
}

return nil
}
Loading