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

Commit c524bba

Browse files
author
Janos Pasztor
committed
Support for content negotiation
1 parent 3af5bdb commit c524bba

File tree

8 files changed

+239
-14
lines changed

8 files changed

+239
-14
lines changed

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
# Changelog
22

3+
## 1.2.0: Support for content negotiation
4+
5+
This release adds support for creating a handler that performs content negotiation between JSON and text output.
6+
37
## 1.1.0: Adding support for additional HTTP methods
48

59
This release adds support for the Delete, Put, and Patch methods.

README.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -162,6 +162,16 @@ func (c *myController) OnRequest(request http.ServerRequest, response http.Serve
162162

163163
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.
164164

165+
## Content negotiation
166+
167+
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:
168+
169+
```go
170+
type TextMarshallable interface {
171+
MarshalText() string
172+
}
173+
```
174+
165175
## Using multiple handlers
166176

167177
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.

go.mod

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,4 +20,4 @@ replace (
2020
gopkg.in/yaml.v2 v2.2.5 => gopkg.in/yaml.v2 v2.2.8
2121
gopkg.in/yaml.v2 v2.2.6 => gopkg.in/yaml.v2 v2.2.8
2222
gopkg.in/yaml.v2 v2.2.7 => gopkg.in/yaml.v2 v2.2.8
23-
)
23+
)

go.sum

Lines changed: 0 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,4 @@
1-
github.com/containerssh/log v1.0.0 h1:nOSqNqh7cXIa+Iy+Lx2CA+wpkrqDqcQh4EVoEvSaxU8=
21
github.com/containerssh/log v1.0.0/go.mod h1:7Gy+sx0H1UDtjYBySvK0CnXRRHPHZPXMsa9MYmLBI0I=
3-
github.com/containerssh/log v1.1.0 h1:7xJyBrvFU9BIMEttCfmVkJHg6K8tLxevLZ/hxLiM7Co=
4-
github.com/containerssh/log v1.1.0/go.mod h1:7Gy+sx0H1UDtjYBySvK0CnXRRHPHZPXMsa9MYmLBI0I=
5-
github.com/containerssh/log v1.1.1 h1:82OhjJSPDY6B1p/qZs0wGqP1v2cB2JxYeUTrI/EegeY=
6-
github.com/containerssh/log v1.1.1/go.mod h1:JER/AjoAHhb8arGN6bsAPF1r1S8p6sUAnvBOL4s32ZU=
7-
github.com/containerssh/log v1.1.2 h1:6SQECTGS5gOaqaC597VF6i4WkcT5MpvPn10MdV96GGo=
8-
github.com/containerssh/log v1.1.2/go.mod h1:JER/AjoAHhb8arGN6bsAPF1r1S8p6sUAnvBOL4s32ZU=
92
github.com/containerssh/log v1.1.3 h1:kadnLiSZW/YAxeaUkHNzEG7iZiGIiyyWEo/SjjJpVwA=
103
github.com/containerssh/log v1.1.3/go.mod h1:JER/AjoAHhb8arGN6bsAPF1r1S8p6sUAnvBOL4s32ZU=
114
github.com/containerssh/service v1.0.0 h1:+AcBsmeaR0iMDnemXJ6OgeTEYB3C0YJF3h03MNIo1Ak=

handler_factory.go

Lines changed: 31 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,36 @@ func NewServerHandler(
1818
panic("BUG: no logger provided to http.NewServerHandler")
1919
}
2020
return &handler{
21-
requestHandler: requestHandler,
22-
logger: logger,
21+
requestHandler: requestHandler,
22+
logger: logger,
23+
defaultResponseMarshaller: &jsonMarshaller{},
24+
defaultResponseType: "application/json",
25+
responseMarshallers: []responseMarshaller{
26+
&jsonMarshaller{},
27+
},
28+
}
29+
}
30+
31+
// NewServerHandlerNegotiate creates a simplified HTTP handler that supports content negotiation for responses.
32+
//goland:noinspection GoUnusedExportedFunction
33+
func NewServerHandlerNegotiate(
34+
requestHandler RequestHandler,
35+
logger log.Logger,
36+
) goHttp.Handler {
37+
if requestHandler == nil {
38+
panic("BUG: no requestHandler provided to http.NewServerHandler")
39+
}
40+
if logger == nil {
41+
panic("BUG: no logger provided to http.NewServerHandler")
42+
}
43+
return &handler{
44+
requestHandler: requestHandler,
45+
logger: logger,
46+
defaultResponseMarshaller: &jsonMarshaller{},
47+
defaultResponseType: "application/json",
48+
responseMarshallers: []responseMarshaller{
49+
&jsonMarshaller{},
50+
&textMarshaller{},
51+
},
2352
}
2453
}

handler_impl.go

Lines changed: 55 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,9 @@ import (
66
"fmt"
77
"io/ioutil"
88
goHttp "net/http"
9+
"sort"
10+
"strconv"
11+
"strings"
912

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

3033
type handler struct {
31-
requestHandler RequestHandler
32-
logger log.Logger
34+
requestHandler RequestHandler
35+
logger log.Logger
36+
defaultResponseMarshaller responseMarshaller
37+
defaultResponseType string
38+
responseMarshallers []responseMarshaller
3339
}
3440

3541
var internalErrorResponse = serverResponse{
@@ -60,7 +66,11 @@ func (h *handler) ServeHTTP(goWriter goHttp.ResponseWriter, goRequest *goHttp.Re
6066
response = internalErrorResponse
6167
}
6268
}
63-
bytes, err := json.Marshal(response.body)
69+
marshaller, responseType, statusCode := h.findMarshaller(goWriter, goRequest)
70+
if statusCode != 200 {
71+
goWriter.WriteHeader(statusCode)
72+
}
73+
bytes, err := marshaller.Marshal(response.body)
6474
if err != nil {
6575
h.logger.Error(log.Wrap(err, MServerEncodeFailed, "failed to marshal response %v", response))
6676
response = internalErrorResponse
@@ -71,12 +81,53 @@ func (h *handler) ServeHTTP(goWriter goHttp.ResponseWriter, goRequest *goHttp.Re
7181
}
7282
}
7383
goWriter.WriteHeader(int(response.statusCode))
74-
goWriter.Header().Add("Content-Type", "application/json")
84+
goWriter.Header().Add("Content-Type", responseType)
7585
if _, err := goWriter.Write(bytes); err != nil {
7686
h.logger.Debug(log.Wrap(err, MServerResponseWriteFailed, "Failed to write HTTP response"))
7787
}
7888
}
7989

90+
func (h *handler) findMarshaller(_ goHttp.ResponseWriter, request *goHttp.Request) (responseMarshaller, string, int) {
91+
acceptHeader := request.Header.Get("Accept")
92+
if acceptHeader == "" {
93+
return h.defaultResponseMarshaller, h.defaultResponseType, 200
94+
}
95+
96+
accepted := strings.Split(acceptHeader, ",")
97+
acceptMap := make(map[string]float64, len(accepted))
98+
acceptList := make([]string, len(accepted))
99+
for i, accept := range accepted {
100+
acceptParts := strings.SplitN(strings.TrimSpace(accept), ";", 2)
101+
q := 1.0
102+
if len(acceptParts) == 2 {
103+
acceptParts2 := strings.SplitN(acceptParts[1], "=", 2)
104+
if acceptParts2[0] == "q" && len(acceptParts2) == 2 {
105+
var err error
106+
q, err = strconv.ParseFloat(acceptParts2[1], 64)
107+
if err != nil {
108+
return nil, h.defaultResponseType, 400
109+
}
110+
} else {
111+
return nil, h.defaultResponseType, 400
112+
}
113+
}
114+
acceptMap[acceptParts[0]] = q
115+
acceptList[i] = acceptParts[0]
116+
}
117+
sort.SliceStable(acceptList, func(i, j int) bool {
118+
return acceptMap[acceptList[i]] > acceptMap[acceptList[j]]
119+
})
120+
121+
for _, a := range acceptList {
122+
for _, marshaller := range h.responseMarshallers {
123+
if marshaller.SupportsMIME(a) {
124+
return marshaller, a, 200
125+
}
126+
}
127+
}
128+
return nil, h.defaultResponseType, 406
129+
}
130+
80131
type internalRequest struct {
81132
writer goHttp.ResponseWriter
82133
request *goHttp.Request

marshaller.go

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
package http
2+
3+
import (
4+
"encoding/json"
5+
"fmt"
6+
"reflect"
7+
)
8+
9+
// responseMarshaller is an interface to cover all encoders for HTTP response bodies.
10+
type responseMarshaller interface {
11+
SupportsMIME(mime string) bool
12+
Marshal(body interface{}) ([]byte, error)
13+
}
14+
15+
//region JSON
16+
17+
type jsonMarshaller struct {
18+
}
19+
20+
func (j *jsonMarshaller) SupportsMIME(mime string) bool {
21+
return mime == "application/json" || mime == "application/*" || mime == "*/*"
22+
}
23+
24+
func (j *jsonMarshaller) Marshal(body interface{}) ([]byte, error) {
25+
return json.Marshal(body)
26+
}
27+
28+
func (j *jsonMarshaller) Unmarshal(body []byte, target interface{}) error {
29+
return json.Unmarshal(body, target)
30+
}
31+
32+
// endregion
33+
34+
// region Text
35+
type TextMarshallable interface {
36+
MarshalText() string
37+
}
38+
39+
type textMarshaller struct {
40+
}
41+
42+
func (t *textMarshaller) SupportsMIME(mime string) bool {
43+
// HTML output might be better suited to piping through a templating engine.
44+
return mime == "text/html" || mime == "text/plain" || mime == "text/*" || mime == "*/*"
45+
}
46+
47+
func (t *textMarshaller) Marshal(body interface{}) ([]byte, error) {
48+
switch assertedBody := body.(type) {
49+
case TextMarshallable:
50+
return []byte(assertedBody.MarshalText()), nil
51+
case string:
52+
return []byte(assertedBody), nil
53+
case int:
54+
return t.marshalNumber(body)
55+
case int8:
56+
return t.marshalNumber(body)
57+
case int16:
58+
return t.marshalNumber(body)
59+
case int32:
60+
return t.marshalNumber(body)
61+
case int64:
62+
return t.marshalNumber(body)
63+
case uint:
64+
return t.marshalNumber(body)
65+
case uint8:
66+
return t.marshalNumber(body)
67+
case uint16:
68+
return t.marshalNumber(body)
69+
case uint32:
70+
return t.marshalNumber(body)
71+
case uint64:
72+
return t.marshalNumber(body)
73+
case bool:
74+
if body.(bool) {
75+
return []byte("true"), nil
76+
} else {
77+
return []byte("false"), nil
78+
}
79+
case uintptr:
80+
return t.marshalPointer(body)
81+
default:
82+
return nil, fmt.Errorf("cannot marshal unknown type: %v", body)
83+
}
84+
}
85+
86+
func (t *textMarshaller) marshalNumber(body interface{}) ([]byte, error) {
87+
return []byte(fmt.Sprintf("%d", body)), nil
88+
}
89+
90+
func (t *textMarshaller) marshalPointer(body interface{}) ([]byte, error) {
91+
ptr := body.(uintptr)
92+
return t.Marshal(reflect.ValueOf(ptr).Elem())
93+
}
94+
95+
// endregion

marshaller_test.go

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
package http
2+
3+
import (
4+
"reflect"
5+
"testing"
6+
)
7+
8+
func TestTextMarshal(t *testing.T) {
9+
dataSet := []interface{}{
10+
42,
11+
int8(42),
12+
int16(42),
13+
int32(42),
14+
int64(42),
15+
uint(42),
16+
uint8(42),
17+
uint16(42),
18+
uint32(42),
19+
uint64(42),
20+
"42",
21+
testData{},
22+
&testData{},
23+
}
24+
25+
marshaller := &textMarshaller{}
26+
for _, v := range dataSet {
27+
t.Run(reflect.TypeOf(v).Name(), func(t *testing.T) {
28+
result, err := marshaller.Marshal(v)
29+
if err != nil {
30+
t.Fatal(err)
31+
}
32+
if string(result) != "42" {
33+
t.Fatalf("unexpected marshal result: %s", result)
34+
}
35+
})
36+
}
37+
}
38+
39+
type testData struct{}
40+
41+
func (t testData) MarshalText() string {
42+
return "42"
43+
}

0 commit comments

Comments
 (0)