Skip to content

Commit

Permalink
httputil, codec, handler, http: expose parser methods for Accept head…
Browse files Browse the repository at this point in the history
…ers, register codecs by MIME types, encode response with negotiated header
  • Loading branch information
lithdew committed May 11, 2020
1 parent 5668eae commit 76e3d4e
Show file tree
Hide file tree
Showing 4 changed files with 76 additions and 47 deletions.
35 changes: 25 additions & 10 deletions codec.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,22 +18,37 @@ type (
type Codec struct {
Encode EncodeFunc
Decode DecodeFunc
Tags []string
}

var Codecs = map[string]*Codec{
"json": reflectCodec(json.Marshal, json.Unmarshal),
"yaml": reflectCodec(yaml.Marshal, yaml.Unmarshal),
"xml": reflectCodec(xml.Marshal, xml.Unmarshal),
"csv": newCodec(csvEncoder, csvDecoder),
"gob": newCodec(gobEncoder, gobDecoder),
func (c Codec) Tag() string {
return c.Tags[0]
}

func reflectCodec(me func(src interface{}) ([]byte, error), md func(buf []byte, dst interface{}) error) *Codec {
return &Codec{Encode: marshalEncoder(me), Decode: unmarshalDecoder(md)}
var Codecs = make(map[string]*Codec)
var CodecTypes []string

func init() {
registerCodec(reflectCodec(json.Marshal, json.Unmarshal, "application/json", "text/json"))
registerCodec(reflectCodec(yaml.Marshal, yaml.Unmarshal, "application/x-yaml", "text/x-yaml"))
registerCodec(reflectCodec(xml.Marshal, xml.Unmarshal, "application/xml", "text/xml"))
registerCodec(newCodec(csvEncoder, csvDecoder, "application/csv", "text/csv"))
registerCodec(newCodec(gobEncoder, gobDecoder, "application/x-gob", "text/x-gob"))
}

func registerCodec(codec *Codec) {
for _, tag := range codec.Tags {
Codecs[tag] = codec
CodecTypes = append(CodecTypes, tag)
}
}

func reflectCodec(me func(src interface{}) ([]byte, error), md func(buf []byte, dst interface{}) error, tags ...string) *Codec {
return &Codec{Encode: marshalEncoder(me), Decode: unmarshalDecoder(md), Tags: tags}
}

func newCodec(encoder EncodeFunc, decoder DecodeFunc) *Codec {
return &Codec{Encode: encoder, Decode: decoder}
func newCodec(encoder EncodeFunc, decoder DecodeFunc, tags ...string) *Codec {
return &Codec{Encode: encoder, Decode: decoder, Tags: tags}
}

func unmarshalDecoder(f func(buf []byte, dst interface{}) error) DecodeFunc {
Expand Down
43 changes: 29 additions & 14 deletions handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,26 +27,26 @@ type Context struct {
type ContentType struct{}

func (h *ContentType) Serve(ctx *Context, w http.ResponseWriter, r *http.Request) {
accept := NegotiateCodec(r, ctx.Config.Codecs, ctx.Config.DefaultCodec)
accept := NegotiateCodec(r, ctx.Config.CodecTypes, ctx.Config.DefaultCodec)

codec, available := ctx.Config.Codecs[accept]
if !available {
httpError(
w, codec,
http.StatusNotAcceptable,
fmt.Errorf("only able to accept %v", ctx.Config.Codecs),
fmt.Errorf("wanted codec %q, only able to accept %v", accept, ctx.Config.Codecs),
)
return
}

w.Header().Set(HeaderContentType, accept)
}

func NegotiateCodec(r *http.Request, codecs map[string]*Codec, defaultCodec string) string {
func NegotiateCodec(r *http.Request, codecs []string, defaultCodec string) string {
specs := httputil.ParseAccept(r.Header, "Accept")
bestCodec, bestQ, bestWild := defaultCodec, -1.0, 3

for codec := range codecs {
for _, codec := range codecs {
for _, spec := range specs {
switch {
case spec.Q == 0.0:
Expand Down Expand Up @@ -83,14 +83,9 @@ type ContentLength struct {
}

func (c *ContentLength) Serve(ctx *Context, w http.ResponseWriter, r *http.Request) {
var codec *Codec
if ctx.Config.Codecs != nil {
codec = ctx.Config.Codecs[w.Header().Get(HeaderContentType)]
}
codec := ctx.Config.Codecs[w.Header().Get(HeaderContentType)]

switch {
case r.ContentLength == 0:
httpError(w, codec, http.StatusNoContent, nil)
case r.ContentLength < c.Min:
httpError(
w, codec,
Expand All @@ -109,10 +104,7 @@ func (c *ContentLength) Serve(ctx *Context, w http.ResponseWriter, r *http.Reque
type ContentDecode struct{}

func (h *ContentDecode) Serve(ctx *Context, w http.ResponseWriter, r *http.Request) {
var codec *Codec
if ctx.Config.Codecs != nil {
codec = ctx.Config.Codecs[w.Header().Get(HeaderContentType)]
}
codec := ctx.Config.Codecs[r.Header.Get(HeaderContentType)]

err := getHeaderParams(r, ctx.In)
if err != nil && !errors.Is(err, http.ErrBodyNotAllowed) {
Expand Down Expand Up @@ -188,3 +180,26 @@ func getBodyParams(r *http.Request, codec *Codec, values map[string]interface{})

return nil
}

type ContentEncode struct{}

func (h *ContentEncode) Serve(ctx *Context, w http.ResponseWriter, _ *http.Request) {
codec := ctx.Config.Codecs[w.Header().Get(HeaderContentType)]

if codec == nil {
httpError(
w, codec,
http.StatusNotAcceptable,
fmt.Errorf("only able to accept %v", ctx.Config.Codecs),
)
return
}

buf, err := codec.Encode(ctx.Out)
if err != nil && !errors.Is(err, http.ErrBodyNotAllowed) {
httpError(w, codec, http.StatusInternalServerError, err)
return
}

w.Write(buf)
}
27 changes: 13 additions & 14 deletions http.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ package flatend

import (
"context"
"crypto/tls"
"net"
"net/http"
"sync"
Expand All @@ -13,6 +12,7 @@ var _ http.Handler = (*Server)(nil)

type Config struct {
Codecs map[string]*Codec
CodecTypes []string
DefaultCodec string

Handlers []Handler
Expand All @@ -21,25 +21,26 @@ type Config struct {
func NewDefaultConfig() *Config {
return &Config{
Codecs: Codecs,
DefaultCodec: "json",
CodecTypes: CodecTypes,
DefaultCodec: CodecTypes[0],

Handlers: []Handler{
&ContentType{},
&ContentLength{Max: 10 * 1024 * 1024},
&ContentDecode{},
&ContentEncode{},
},
}
}

type Server struct {
ReadTimeout time.Duration
WriteTimeout time.Duration
IdleTimeout time.Duration
ReadHeaderTimeout time.Duration
IdleTimeout time.Duration
ReadTimeout time.Duration
WriteTimeout time.Duration

MaxHeaderBytes int
MaxHeaderBytes int
ReadHeaderTimeout time.Duration

TLS *tls.Config
Config *Config

once sync.Once
Expand All @@ -51,14 +52,12 @@ func (s *Server) init() {
s.http = &http.Server{
Handler: s,

TLSConfig: s.TLS,
IdleTimeout: s.IdleTimeout,
ReadTimeout: s.ReadTimeout,
WriteTimeout: s.WriteTimeout,

ReadTimeout: s.ReadTimeout,
WriteTimeout: s.WriteTimeout,
IdleTimeout: s.IdleTimeout,
MaxHeaderBytes: s.MaxHeaderBytes,
ReadHeaderTimeout: s.ReadHeaderTimeout,

MaxHeaderBytes: s.MaxHeaderBytes,
}

if s.Config == nil {
Expand Down
18 changes: 9 additions & 9 deletions httputil/accept.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,39 +18,39 @@ func ParseAccept(header http.Header, key string) (specs []AcceptSpec) {

for _, s := range header[key] {
for {
spec.Value, s = expectTokenSlash(s)
spec.Value, s = ExpectTokenSlash(s)
if spec.Value == "" {
break
}

s = skipSpace(s)
s = SkipSpace(s)

spec.Q = 1.0

if len(s) > 0 && s[0] == ';' {
s = skipSpace(s[1:])
s = SkipSpace(s[1:])
if !strings.HasPrefix(s, "q=") {
break
}
spec.Q, s = expectQuality(s[2:])
spec.Q, s = ExpectQuality(s[2:])
if spec.Q < 0.0 {
break
}
}

specs = append(specs, spec)

s = skipSpace(s)
s = SkipSpace(s)
if len(s) == 0 || s[0] != ',' {
break
}
s = skipSpace(s[1:])
s = SkipSpace(s[1:])
}
}
return specs
}

func skipSpace(s string) (rest string) {
func SkipSpace(s string) (rest string) {
i := 0
for ; i < len(s); i++ {
if octetTypes[s[i]]&isSpace == 0 {
Expand All @@ -60,7 +60,7 @@ func skipSpace(s string) (rest string) {
return s[i:]
}

func expectTokenSlash(s string) (token, rest string) {
func ExpectTokenSlash(s string) (token, rest string) {
i := 0
for ; i < len(s); i++ {
b := s[i]
Expand All @@ -71,7 +71,7 @@ func expectTokenSlash(s string) (token, rest string) {
return s[:i], s[i:]
}

func expectQuality(s string) (q float64, rest string) {
func ExpectQuality(s string) (q float64, rest string) {
switch {
case len(s) == 0:
return -1, ""
Expand Down

0 comments on commit 76e3d4e

Please sign in to comment.