Skip to content
9 changes: 4 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# rsvp

net/http middleware with a type-driven Handler interface, handling content negotiation automatically.
net/http middleware providing a Handler interface with type-driven content negotiation.

---

Expand Down Expand Up @@ -29,7 +29,6 @@ ServeHTTP(ResponseWriter, *http.Request) Body
- [x] `text/csv` (by implementing the rsvp.Csv interface)
- [x] `application/octet-stream`
- [x] `application/xml`
- [x] `application/vnd.golang.gob` (Golang's [encoding/gob](https://go.dev/blog/gob))
- [x] `application/vnd.msgpack` (optional extension behind -tags=rsvp_msgpack)
- [ ] Others to be implemented?
- Extension matching on GET requests:
Expand Down Expand Up @@ -97,7 +96,7 @@ func main() {
}

func getUser(w rsvp.ResponseWriter, r *http.Request) rsvp.Body {
return rsvp.Data(User{ID: 123}) // In content negotiation this will be offered as, in order; JSON, XML, and encoding/gob.
return rsvp.Data(User{ID: 123}) // In content negotiation this will be offered as JSON, then XML.
}
```

Expand All @@ -118,7 +117,7 @@ rsvp.Config{

func showUser(w rsvp.ResponseWriter, r *http.Request) rsvp.Body {
w.DefaultTemplateName("user.gotmpl") // Must exist in HtmlTemplate and/or TextTemplate for formats to match
return rsvp.Data(User{ID: 123}) // In content negotiation this will be offered as JSON, XML, HTML, plain text, and encoding/gob.
return rsvp.Data(User{ID: 123}) // In content negotiation this will be offered as JSON, XML, HTML, and plain text.
}
```

Expand Down Expand Up @@ -153,7 +152,7 @@ func (ul UserList) MarshalCsv(w *csv.Writer) error {
}

func userList(w rsvp.ResponseWriter, r *http.Request) rsvp.Body {
return rsvp.Data(users) // In content negotiation this will be offered as JSON, XML, CSV, and encoding/gob.
return rsvp.Data(users) // In content negotiation this will be offered as JSON, XML, and CSV.
}
```

Expand Down
7 changes: 1 addition & 6 deletions api.snap.txt
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@ const (
SupportedMediaTypeBytes string = "application/octet-stream"
SupportedMediaTypeJson string = "application/json"
SupportedMediaTypeXml string = "application/xml"
SupportedMediaTypeGob string = "application/vnd.golang.gob"
)

VARIABLES
Expand All @@ -23,11 +22,7 @@ func AdaptHandler(config Config, next Handler) http.Handler

func AdaptHandlerFunc(cfg Config, next HandlerFunc) http.HandlerFunc

func Write(w io.Writer, cfg Config, wh http.Header, r *http.Request, handler HandlerFunc) (int, error)

func WriteHandler(cfg Config, rw http.ResponseWriter, r *http.Request, handler HandlerFunc) error

func WriteResponse(status int, w http.ResponseWriter, r io.Reader) error
func Write(w http.ResponseWriter, r *http.Request, cfg Config, handler HandlerFunc) error

TYPES

Expand Down
22 changes: 7 additions & 15 deletions content.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,6 @@ const (
SupportedMediaTypeBytes string = "application/octet-stream"
SupportedMediaTypeJson string = "application/json"
SupportedMediaTypeXml string = "application/xml"
SupportedMediaTypeGob string = "application/vnd.golang.gob"
)

var mediaTypeToContentType = map[string]string{
Expand All @@ -30,7 +29,6 @@ var mediaTypeToContentType = map[string]string{
SupportedMediaTypeBytes: "application/octet-stream",
SupportedMediaTypeJson: "application/json",
SupportedMediaTypeXml: "application/xml",
SupportedMediaTypeGob: "application/vnd.golang.gob",
}

var extToProposalMap = map[string]string{
Expand All @@ -41,7 +39,6 @@ var extToProposalMap = map[string]string{
"json": SupportedMediaTypeJson,
"xml": SupportedMediaTypeXml,
"bin": SupportedMediaTypeBytes,
"gob": SupportedMediaTypeGob,
}

var extendedMediaTypes []string = nil
Expand Down Expand Up @@ -107,15 +104,15 @@ func chooseMediaType(ext string, supported []string, accept iter.Seq[string]) st
if ext != "" {
dev.Log("Checking extension: %#v", ext)
if a, ok := extToProposalMap[ext]; ok {
dev.Log("proposing %#v", a)
for _, s := range supported {
dev.Log("offering %#v", s)
if mediaTypesEqual(s, a) {
return s
}
if slices.Contains(supported, a) {
dev.Log("Setting %#v as sole supported type", a)
supported = []string{a}
} else {
supported = []string{}
}
} else {
supported = []string{}
}
return ""
}

dev.Log("Checking accept list")
Expand All @@ -139,7 +136,6 @@ func chooseMediaType(ext string, supported []string, accept iter.Seq[string]) st
// 2. Generic structured (JSON, XML)
// 3. Interface implementations (CSV)
// 4. Template-based (HTML template, text template)
// 5. Golang-native fallback (Gob)
func (res *Body) MediaTypes(cfg Config) iter.Seq[string] {
return func(yield func(string) bool) {
if res.predeterminedMediaType != "" {
Expand Down Expand Up @@ -193,9 +189,5 @@ func (res *Body) MediaTypes(cfg Config) iter.Seq[string] {
return
}
}

if !yield(SupportedMediaTypeGob) {
return
}
}
}
20 changes: 0 additions & 20 deletions mediatypes_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,6 @@ func TestNilResponse(t *testing.T) {
rsvp.SupportedMediaTypeJson,
rsvp.SupportedMediaTypeXml,
rsvp.SupportedMediaTypeMsgpack,
rsvp.SupportedMediaTypeGob,
}

assert.SlicesEq(t, "media types", expected, actual)
Expand All @@ -37,7 +36,6 @@ func TestBlankResponse(t *testing.T) {
rsvp.SupportedMediaTypeJson,
rsvp.SupportedMediaTypeXml,
rsvp.SupportedMediaTypeMsgpack,
rsvp.SupportedMediaTypeGob,
}

assert.SlicesEq(t, "media types", expected, actual)
Expand All @@ -53,7 +51,6 @@ func TestEmptyStringResponse(t *testing.T) {
rsvp.SupportedMediaTypeJson,
rsvp.SupportedMediaTypeXml,
rsvp.SupportedMediaTypeMsgpack,
rsvp.SupportedMediaTypeGob,
}

assert.SlicesEq(t, "media types", expected, actual)
Expand All @@ -68,7 +65,6 @@ func TestStructResponse(t *testing.T) {
rsvp.SupportedMediaTypeJson,
rsvp.SupportedMediaTypeXml,
rsvp.SupportedMediaTypeMsgpack,
rsvp.SupportedMediaTypeGob,
}

assert.SlicesEq(t, "media types", expected, actual)
Expand All @@ -83,7 +79,6 @@ func TestSliceResponse(t *testing.T) {
rsvp.SupportedMediaTypeJson,
rsvp.SupportedMediaTypeXml,
rsvp.SupportedMediaTypeMsgpack,
rsvp.SupportedMediaTypeGob,
}

assert.SlicesEq(t, "media types", expected, actual)
Expand All @@ -98,7 +93,6 @@ func TestMapResponse(t *testing.T) {
rsvp.SupportedMediaTypeJson,
rsvp.SupportedMediaTypeXml,
rsvp.SupportedMediaTypeMsgpack,
rsvp.SupportedMediaTypeGob,
}

assert.SlicesEq(t, "media types", expected, actual)
Expand All @@ -114,7 +108,6 @@ func TestStringResponse(t *testing.T) {
rsvp.SupportedMediaTypeJson,
rsvp.SupportedMediaTypeXml,
rsvp.SupportedMediaTypeMsgpack,
rsvp.SupportedMediaTypeGob,
}

assert.SlicesEq(t, "media types", expected, actual)
Expand All @@ -130,7 +123,6 @@ func TestBytesResponse(t *testing.T) {
rsvp.SupportedMediaTypeJson,
rsvp.SupportedMediaTypeXml,
rsvp.SupportedMediaTypeMsgpack,
rsvp.SupportedMediaTypeGob,
}

assert.SlicesEq(t, "media types", expected, actual)
Expand All @@ -152,7 +144,6 @@ func TestCsvResponse(t *testing.T) {
rsvp.SupportedMediaTypeXml,
rsvp.SupportedMediaTypeCsv,
rsvp.SupportedMediaTypeMsgpack,
rsvp.SupportedMediaTypeGob,
}

assert.SlicesEq(t, "media types", expected, actual)
Expand All @@ -170,7 +161,6 @@ func TestStructResponseWithHtmlTemplate(t *testing.T) {
rsvp.SupportedMediaTypeXml,
rsvp.SupportedMediaTypeHtml,
rsvp.SupportedMediaTypeMsgpack,
rsvp.SupportedMediaTypeGob,
}

assert.SlicesEq(t, "media types", expected, actual)
Expand All @@ -187,7 +177,6 @@ func TestStructWithoutTemplateNameResponseWithHtmlTemplate(t *testing.T) {
rsvp.SupportedMediaTypeJson,
rsvp.SupportedMediaTypeXml,
rsvp.SupportedMediaTypeMsgpack,
rsvp.SupportedMediaTypeGob,
}

assert.SlicesEq(t, "media types", expected, actual)
Expand All @@ -206,7 +195,6 @@ func TestCsvResponseWithHtmlTemplate(t *testing.T) {
rsvp.SupportedMediaTypeCsv,
rsvp.SupportedMediaTypeHtml,
rsvp.SupportedMediaTypeMsgpack,
rsvp.SupportedMediaTypeGob,
}

assert.SlicesEq(t, "media types", expected, actual)
Expand All @@ -225,7 +213,6 @@ func TestCsvResponseWithTextTemplate(t *testing.T) {
rsvp.SupportedMediaTypeCsv,
rsvp.SupportedMediaTypePlaintext,
rsvp.SupportedMediaTypeMsgpack,
rsvp.SupportedMediaTypeGob,
}

assert.SlicesEq(t, "media types", expected, actual)
Expand All @@ -243,7 +230,6 @@ func TestStructResponseWithTextTemplate(t *testing.T) {
rsvp.SupportedMediaTypeXml,
rsvp.SupportedMediaTypePlaintext,
rsvp.SupportedMediaTypeMsgpack,
rsvp.SupportedMediaTypeGob,
}

assert.SlicesEq(t, "media types", expected, actual)
Expand All @@ -260,7 +246,6 @@ func TestStructWithoutTemplateNameResponseWithTextTemplate(t *testing.T) {
rsvp.SupportedMediaTypeJson,
rsvp.SupportedMediaTypeXml,
rsvp.SupportedMediaTypeMsgpack,
rsvp.SupportedMediaTypeGob,
}

assert.SlicesEq(t, "media types", expected, actual)
Expand All @@ -280,7 +265,6 @@ func TestStructResponseWithTextAndHtmlTemplates(t *testing.T) {
rsvp.SupportedMediaTypeHtml,
rsvp.SupportedMediaTypePlaintext,
rsvp.SupportedMediaTypeMsgpack,
rsvp.SupportedMediaTypeGob,
}

assert.SlicesEq(t, "media types", expected, actual)
Expand All @@ -301,7 +285,6 @@ func TestStringResponseWithTextAndHtmlTemplates(t *testing.T) {
rsvp.SupportedMediaTypeHtml,
rsvp.SupportedMediaTypePlaintext,
rsvp.SupportedMediaTypeMsgpack,
rsvp.SupportedMediaTypeGob,
}

assert.SlicesEq(t, "media types", expected, actual)
Expand All @@ -322,7 +305,6 @@ func TestCsvResponseWithTextAndHtmlTemplates(t *testing.T) {
rsvp.SupportedMediaTypeHtml,
rsvp.SupportedMediaTypePlaintext,
rsvp.SupportedMediaTypeMsgpack,
rsvp.SupportedMediaTypeGob,
}

assert.SlicesEq(t, "media types", expected, actual)
Expand All @@ -341,7 +323,6 @@ func TestBytesResponseWithHtmlTemplate(t *testing.T) {
rsvp.SupportedMediaTypeXml,
rsvp.SupportedMediaTypeHtml,
rsvp.SupportedMediaTypeMsgpack,
rsvp.SupportedMediaTypeGob,
}

assert.SlicesEq(t, "media types", expected, actual)
Expand All @@ -360,7 +341,6 @@ func TestStringResponseWithHtmlTemplate(t *testing.T) {
rsvp.SupportedMediaTypeXml,
rsvp.SupportedMediaTypeHtml,
rsvp.SupportedMediaTypeMsgpack,
rsvp.SupportedMediaTypeGob,
}

assert.SlicesEq(t, "media types", expected, actual)
Expand Down
44 changes: 3 additions & 41 deletions middleware.go
Original file line number Diff line number Diff line change
@@ -1,61 +1,23 @@
package rsvp

import (
"bytes"
"fmt"
"io"
"log"
"net/http"

"github.com/Teajey/rsvp/internal/dev"
)

// AdaptHandlerFunc wraps a [HandlerFunc] as an [http.HandlerFunc] with the given config.
//
// This is the primary entrypoint to using rsvp.
func AdaptHandlerFunc(cfg Config, next HandlerFunc) http.HandlerFunc {
return func(rw http.ResponseWriter, r *http.Request) {
err := WriteHandler(cfg, rw, r, next)
err := Write(rw, r, cfg, next)
if err != nil {
log.Printf("[RSVP ERROR]: %s", err)
log.Printf("rsvp failed to write a response: %s", err)
return
}
}
}

// WriteHandler writes the result of handler to rw according to cfg.
//
// NOTE: This function is for advanced lower-level use cases.
func WriteHandler(cfg Config, rw http.ResponseWriter, r *http.Request, handler HandlerFunc) error {
var buf bytes.Buffer
status, err := Write(&buf, cfg, rw.Header(), r, handler)
if err != nil {
http.Error(rw, "RSVP failed to write a response", http.StatusInternalServerError)
return fmt.Errorf("writing response: %w", err)
}
err = WriteResponse(status, rw, &buf)
if err != nil {
return fmt.Errorf("writing header: %w", err)
}
return nil
}

// WriteResponse calls w.WriteHeader(status) and copies r to w.
//
// NOTE: This function is for advanced lower-level use cases.
//
// This function, alongside [Write], should be used to wrap [Handler] in middleware that requires _write_ access to [http.ResponseWriter]. [AdaptHandler] and [AdaptHandlerFunc] may be used for simpler standard middleware that does not write to [http.ResponseWriter].
//
// See this test for an example: https://github.com/Teajey/rsvp/blob/main/middleware_test.go
func WriteResponse(status int, w http.ResponseWriter, r io.Reader) error {
dev.Log("Setting status to %d", status)
w.WriteHeader(status)
_, err := io.Copy(w, r)
if err != nil {
return fmt.Errorf("copying to http.ResponseWriter: %w", err)
}
return nil
}

// AdaptHandler wraps a [Handler] as an [http.Handler] with the given config.
//
// This is the primary entrypoint to using rsvp.
Expand Down
Loading
Loading