Skip to content
This repository has been archived by the owner on Oct 2, 2022. It is now read-only.

Commit

Permalink
Support for content negotiation
Browse files Browse the repository at this point in the history
  • Loading branch information
Janos Pasztor committed Jun 9, 2021
1 parent 3af5bdb commit c524bba
Show file tree
Hide file tree
Showing 8 changed files with 239 additions and 14 deletions.
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
# Changelog

## 1.2.0: Support for content negotiation

This release adds support for creating a handler that performs content negotiation between JSON and text output.

## 1.1.0: Adding support for additional HTTP methods

This release adds support for the Delete, Put, and Patch methods.
Expand Down
10 changes: 10 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,16 @@ func (c *myController) OnRequest(request http.ServerRequest, response http.Serve

In other words, the `ServerRequest` object gives you the ability to decode the request into a struct of your choice. The `ServerResponse`, conversely, encodes a struct into the the response body and provides the ability to enter a status code.

## Content negotiation

If you wish to perform content negotiation on the server side, this library now supports switching between text and JSON output. This can be invoked using the `NewServerHandlerNegotiate` method instead of `NewServerHandler`. This handler will attempt to switch based on the `Accept` header sent by the client. You can marshal objects to text by implementing the following interface:

```go
type TextMarshallable interface {
MarshalText() string
}
```

## Using multiple handlers

This is a very simple handler example. You can use utility like [gorilla/mux](https://github.com/gorilla/mux) as an intermediate handler between the simplified handler and the server itself.
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -20,4 +20,4 @@ replace (
gopkg.in/yaml.v2 v2.2.5 => gopkg.in/yaml.v2 v2.2.8
gopkg.in/yaml.v2 v2.2.6 => gopkg.in/yaml.v2 v2.2.8
gopkg.in/yaml.v2 v2.2.7 => gopkg.in/yaml.v2 v2.2.8
)
)
7 changes: 0 additions & 7 deletions go.sum
Original file line number Diff line number Diff line change
@@ -1,11 +1,4 @@
github.com/containerssh/log v1.0.0 h1:nOSqNqh7cXIa+Iy+Lx2CA+wpkrqDqcQh4EVoEvSaxU8=
github.com/containerssh/log v1.0.0/go.mod h1:7Gy+sx0H1UDtjYBySvK0CnXRRHPHZPXMsa9MYmLBI0I=
github.com/containerssh/log v1.1.0 h1:7xJyBrvFU9BIMEttCfmVkJHg6K8tLxevLZ/hxLiM7Co=
github.com/containerssh/log v1.1.0/go.mod h1:7Gy+sx0H1UDtjYBySvK0CnXRRHPHZPXMsa9MYmLBI0I=
github.com/containerssh/log v1.1.1 h1:82OhjJSPDY6B1p/qZs0wGqP1v2cB2JxYeUTrI/EegeY=
github.com/containerssh/log v1.1.1/go.mod h1:JER/AjoAHhb8arGN6bsAPF1r1S8p6sUAnvBOL4s32ZU=
github.com/containerssh/log v1.1.2 h1:6SQECTGS5gOaqaC597VF6i4WkcT5MpvPn10MdV96GGo=
github.com/containerssh/log v1.1.2/go.mod h1:JER/AjoAHhb8arGN6bsAPF1r1S8p6sUAnvBOL4s32ZU=
github.com/containerssh/log v1.1.3 h1:kadnLiSZW/YAxeaUkHNzEG7iZiGIiyyWEo/SjjJpVwA=
github.com/containerssh/log v1.1.3/go.mod h1:JER/AjoAHhb8arGN6bsAPF1r1S8p6sUAnvBOL4s32ZU=
github.com/containerssh/service v1.0.0 h1:+AcBsmeaR0iMDnemXJ6OgeTEYB3C0YJF3h03MNIo1Ak=
Expand Down
33 changes: 31 additions & 2 deletions handler_factory.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,36 @@ func NewServerHandler(
panic("BUG: no logger provided to http.NewServerHandler")
}
return &handler{
requestHandler: requestHandler,
logger: logger,
requestHandler: requestHandler,
logger: logger,
defaultResponseMarshaller: &jsonMarshaller{},
defaultResponseType: "application/json",
responseMarshallers: []responseMarshaller{
&jsonMarshaller{},
},
}
}

// NewServerHandlerNegotiate creates a simplified HTTP handler that supports content negotiation for responses.
//goland:noinspection GoUnusedExportedFunction
func NewServerHandlerNegotiate(
requestHandler RequestHandler,
logger log.Logger,
) goHttp.Handler {
if requestHandler == nil {
panic("BUG: no requestHandler provided to http.NewServerHandler")
}
if logger == nil {
panic("BUG: no logger provided to http.NewServerHandler")
}
return &handler{
requestHandler: requestHandler,
logger: logger,
defaultResponseMarshaller: &jsonMarshaller{},
defaultResponseType: "application/json",
responseMarshallers: []responseMarshaller{
&jsonMarshaller{},
&textMarshaller{},
},
}
}
59 changes: 55 additions & 4 deletions handler_impl.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@ import (
"fmt"
"io/ioutil"
goHttp "net/http"
"sort"
"strconv"
"strings"

"github.com/containerssh/log"
)
Expand All @@ -28,8 +31,11 @@ func (s *serverResponse) SetBody(body interface{}) {
}

type handler struct {
requestHandler RequestHandler
logger log.Logger
requestHandler RequestHandler
logger log.Logger
defaultResponseMarshaller responseMarshaller
defaultResponseType string
responseMarshallers []responseMarshaller
}

var internalErrorResponse = serverResponse{
Expand Down Expand Up @@ -60,7 +66,11 @@ func (h *handler) ServeHTTP(goWriter goHttp.ResponseWriter, goRequest *goHttp.Re
response = internalErrorResponse
}
}
bytes, err := json.Marshal(response.body)
marshaller, responseType, statusCode := h.findMarshaller(goWriter, goRequest)
if statusCode != 200 {
goWriter.WriteHeader(statusCode)
}
bytes, err := marshaller.Marshal(response.body)
if err != nil {
h.logger.Error(log.Wrap(err, MServerEncodeFailed, "failed to marshal response %v", response))
response = internalErrorResponse
Expand All @@ -71,12 +81,53 @@ func (h *handler) ServeHTTP(goWriter goHttp.ResponseWriter, goRequest *goHttp.Re
}
}
goWriter.WriteHeader(int(response.statusCode))
goWriter.Header().Add("Content-Type", "application/json")
goWriter.Header().Add("Content-Type", responseType)
if _, err := goWriter.Write(bytes); err != nil {
h.logger.Debug(log.Wrap(err, MServerResponseWriteFailed, "Failed to write HTTP response"))
}
}

func (h *handler) findMarshaller(_ goHttp.ResponseWriter, request *goHttp.Request) (responseMarshaller, string, int) {
acceptHeader := request.Header.Get("Accept")
if acceptHeader == "" {
return h.defaultResponseMarshaller, h.defaultResponseType, 200
}

accepted := strings.Split(acceptHeader, ",")
acceptMap := make(map[string]float64, len(accepted))
acceptList := make([]string, len(accepted))
for i, accept := range accepted {
acceptParts := strings.SplitN(strings.TrimSpace(accept), ";", 2)
q := 1.0
if len(acceptParts) == 2 {
acceptParts2 := strings.SplitN(acceptParts[1], "=", 2)
if acceptParts2[0] == "q" && len(acceptParts2) == 2 {
var err error
q, err = strconv.ParseFloat(acceptParts2[1], 64)
if err != nil {
return nil, h.defaultResponseType, 400
}
} else {
return nil, h.defaultResponseType, 400
}
}
acceptMap[acceptParts[0]] = q
acceptList[i] = acceptParts[0]
}
sort.SliceStable(acceptList, func(i, j int) bool {
return acceptMap[acceptList[i]] > acceptMap[acceptList[j]]
})

for _, a := range acceptList {
for _, marshaller := range h.responseMarshallers {
if marshaller.SupportsMIME(a) {
return marshaller, a, 200
}
}
}
return nil, h.defaultResponseType, 406
}

type internalRequest struct {
writer goHttp.ResponseWriter
request *goHttp.Request
Expand Down
95 changes: 95 additions & 0 deletions marshaller.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
package http

import (
"encoding/json"
"fmt"
"reflect"
)

// responseMarshaller is an interface to cover all encoders for HTTP response bodies.
type responseMarshaller interface {
SupportsMIME(mime string) bool
Marshal(body interface{}) ([]byte, error)
}

//region JSON

type jsonMarshaller struct {
}

func (j *jsonMarshaller) SupportsMIME(mime string) bool {
return mime == "application/json" || mime == "application/*" || mime == "*/*"
}

func (j *jsonMarshaller) Marshal(body interface{}) ([]byte, error) {
return json.Marshal(body)
}

func (j *jsonMarshaller) Unmarshal(body []byte, target interface{}) error {
return json.Unmarshal(body, target)
}

// endregion

// region Text
type TextMarshallable interface {
MarshalText() string
}

type textMarshaller struct {
}

func (t *textMarshaller) SupportsMIME(mime string) bool {
// HTML output might be better suited to piping through a templating engine.
return mime == "text/html" || mime == "text/plain" || mime == "text/*" || mime == "*/*"
}

func (t *textMarshaller) Marshal(body interface{}) ([]byte, error) {
switch assertedBody := body.(type) {
case TextMarshallable:
return []byte(assertedBody.MarshalText()), nil
case string:
return []byte(assertedBody), nil
case int:
return t.marshalNumber(body)
case int8:
return t.marshalNumber(body)
case int16:
return t.marshalNumber(body)
case int32:
return t.marshalNumber(body)
case int64:
return t.marshalNumber(body)
case uint:
return t.marshalNumber(body)
case uint8:
return t.marshalNumber(body)
case uint16:
return t.marshalNumber(body)
case uint32:
return t.marshalNumber(body)
case uint64:
return t.marshalNumber(body)
case bool:
if body.(bool) {
return []byte("true"), nil
} else {
return []byte("false"), nil
}
case uintptr:
return t.marshalPointer(body)
default:
return nil, fmt.Errorf("cannot marshal unknown type: %v", body)
}
}

func (t *textMarshaller) marshalNumber(body interface{}) ([]byte, error) {
return []byte(fmt.Sprintf("%d", body)), nil
}

func (t *textMarshaller) marshalPointer(body interface{}) ([]byte, error) {
ptr := body.(uintptr)
return t.Marshal(reflect.ValueOf(ptr).Elem())
}

// endregion
43 changes: 43 additions & 0 deletions marshaller_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
package http

import (
"reflect"
"testing"
)

func TestTextMarshal(t *testing.T) {
dataSet := []interface{}{
42,
int8(42),
int16(42),
int32(42),
int64(42),
uint(42),
uint8(42),
uint16(42),
uint32(42),
uint64(42),
"42",
testData{},
&testData{},
}

marshaller := &textMarshaller{}
for _, v := range dataSet {
t.Run(reflect.TypeOf(v).Name(), func(t *testing.T) {
result, err := marshaller.Marshal(v)
if err != nil {
t.Fatal(err)
}
if string(result) != "42" {
t.Fatalf("unexpected marshal result: %s", result)
}
})
}
}

type testData struct{}

func (t testData) MarshalText() string {
return "42"
}

0 comments on commit c524bba

Please sign in to comment.