Skip to content

Commit

Permalink
WIP Context
Browse files Browse the repository at this point in the history
  • Loading branch information
tigerwill90 committed Apr 8, 2023
1 parent 4df69c6 commit 7b1cb7a
Show file tree
Hide file tree
Showing 11 changed files with 346 additions and 176 deletions.
204 changes: 147 additions & 57 deletions context.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,57 @@
package fox

import (
"fmt"
"io"
"net/http"
"net/url"
)

// Context holds request-related information and allows interaction with the ResponseWriter.
type Context struct {
Writer ResponseWriter
Request *http.Request
// ContextCloser extends Context for manually created instances, adding a Close method
// to release resources after use.
type ContextCloser interface {
Context
Close()
}

// Context represents the context of the current HTTP request.
// It provides methods to access request data and to write a response.
type Context interface {
// Request returns the current *http.Request.
Request() *http.Request
// SetRequest sets the *http.Request.
SetRequest(r *http.Request)
// Writer returns the ResponseWriter.
Writer() ResponseWriter
// Path returns the registered path for the handler.
Path() string
// Params returns a Params slice containing the matched
// wildcard parameters.
Params() Params
// Param retrieve a matching wildcard parameter by name.
Param(name string) string
// QueryParams parses the Request RawQuery and returns the corresponding values.
QueryParams() url.Values
// QueryParam returns the first query value associated with the given key.
QueryParam(name string) string
// String sends a formatted string with the specified status code.
String(code int, format string, values ...any) error
// Blob sends a byte slice with the specified status code and content type.
Blob(code int, contentType string, buf []byte) error
// Stream sends data from an io.Reader with the specified status code and content type.
Stream(code int, contentType string, r io.Reader) error
// Clone returns a copy of the Context that is safe to use after the Handler returns.
Clone() Context
// Tree is a local copy of the Tree in use to serve the request.
Tree() *Tree
// Fox returns the Router in use to serve the request.
Fox() *Router
}

// context holds request-related information and allows interaction with the ResponseWriter.
type context struct {
w ResponseWriter
req *http.Request
params *Params
skipNds *skippedNodes

Expand All @@ -21,7 +64,7 @@ type Context struct {
r *Router

cachedQuery url.Values
route string
path string
rec recorder

// maxParams cap at allocation (read-only, no reset)
Expand All @@ -30,38 +73,53 @@ type Context struct {
maxD int
}

func (c *Context) reset(fox *Router, w http.ResponseWriter, r *http.Request) {
func (c *context) reset(fox *Router, w http.ResponseWriter, r *http.Request) {
c.rec.reset(w)
c.Request = r
c.req = r
if r.ProtoMajor == 2 {
c.Writer = h2Writer{&c.rec}
c.w = h2Writer{&c.rec}
} else {
c.Writer = h1Writer{&c.rec}
c.w = h1Writer{&c.rec}
}
c.r = fox
c.route = ""
c.path = ""
c.cachedQuery = nil
*c.params = (*c.params)[:0]
}

func (c *Context) resetNil() {
c.Request = nil
c.Writer = nil
func (c *context) resetNil() {
c.req = nil
c.w = nil
c.r = nil
c.route = ""
c.path = ""
c.cachedQuery = nil
*c.params = (*c.params)[:0]
}

// Request returns the *http.Request.
func (c *context) Request() *http.Request {
return c.req
}

// SetRequest sets the *http.Request.
func (c *context) SetRequest(r *http.Request) {
c.req = r
}

// Writer returns the ResponseWriter
func (c *context) Writer() ResponseWriter {
return c.w
}

// Params returns a Params slice containing the matched
// wildcard parameters.
func (c *Context) Params() Params {
func (c *context) Params() Params {
return *c.params
}

// Param retrieve a matching wildcard segment by name.
// It's a helper for c.Params.Get(name).
func (c *Context) Param(name string) string {
func (c *context) Param(name string) string {
for _, p := range c.Params() {
if p.Key == name {
return p.Value
Expand All @@ -70,66 +128,98 @@ func (c *Context) Param(name string) string {
return ""
}

// Query returns the first value associated with the given key.
// It's a helper for c.QueryValues().Get(name)
func (c *Context) Query(name string) string {
return c.getQueries().Get(name)
}

// QueryValues parses RawQuery and returns the corresponding values.
// QueryParams parses RawQuery and returns the corresponding values.
// It's a helper for c.Request.URL.Query(). Note that the parsed
// result is cached.
func (c *Context) QueryValues() url.Values {
c.Request.URL.Query()
func (c *context) QueryParams() url.Values {
c.req.URL.Query()
return c.getQueries()
}

// QueryParam returns the first value associated with the given key.
// It's a helper for c.QueryParams().Get(name)
func (c *context) QueryParam(name string) string {
return c.getQueries().Get(name)
}

// Path returns the registered path for the handler.
func (c *Context) Path() string {
return c.route
func (c *context) Path() string {
return c.path
}

// String sends a formatted string with the specified status code.
func (c *context) String(code int, format string, values ...any) (err error) {
c.w.Header().Set(HeaderContentType, MIMETextPlainCharsetUTF8)
c.w.WriteHeader(code)
_, err = fmt.Fprintf(c.w, format, values...)
return
}

// Blob sends a byte slice with the specified status code and content type.
func (c *context) Blob(code int, contentType string, buf []byte) (err error) {
c.w.Header().Set(HeaderContentType, contentType)
c.w.WriteHeader(code)
_, err = c.w.Write(buf)
return
}

// Stream sends data from an io.Reader with the specified status code and content type.
func (c *context) Stream(code int, contentType string, r io.Reader) (err error) {
c.w.Header().Set(HeaderContentType, contentType)
c.w.WriteHeader(code)
_, err = io.Copy(c.w, r)
return
}

func (c *context) Redirect(code int, url string) error {
if code < http.StatusMultipleChoices || code > http.StatusPermanentRedirect {
return ErrInvalidRedirectCode
}
http.Redirect(c.w, c.req, url, code)
return nil
}

// Tree is a local copy of the Tree in use to serve the request.
func (c *Context) Tree() *Tree {
func (c *context) Tree() *Tree {
return c.tree
}

// Fox returns the Router in use to serve the request.
func (c *Context) Fox() *Router {
func (c *context) Fox() *Router {
return c.r
}

// Free releases the context to be reused later. You only need to call Free for manually
// created Context instances, as the router will automatically manage the Context
// within a Handler.
func (c *Context) Free() {
if cap(*c.params) > c.maxP || cap(*c.skipNds) > c.maxD {
return
}
c.tree.ctx.Put(c)
}

// Clone returns a copy of the Context that is safe to use after the Handler returns.
func (c *Context) Clone() *Context {
cp := Context{
rec: c.rec,
Request: c.Request.Clone(c.Request.Context()),
r: c.r,
tree: c.tree,
func (c *context) Clone() Context {
cp := context{
rec: c.rec,
req: c.req.Clone(c.req.Context()),
r: c.r,
tree: c.tree,
}
cp.rec.ResponseWriter = noopWriter{}
cp.Writer = &cp.rec
cp.w = &cp.rec
params := make(Params, len(*c.params))
copy(params, *c.params)
cp.params = &params
cp.cachedQuery = nil
return &cp
}

func (c *Context) getQueries() url.Values {
// Close releases the context to be reused later. You only need to call Close for manually
// created Context instances, as the router will automatically manage the Context
// within a Handler.
func (c *context) Close() {
if cap(*c.params) > c.maxP || cap(*c.skipNds) > c.maxD {
return
}
c.tree.ctx.Put(c)
}

func (c *context) getQueries() url.Values {
if c.cachedQuery == nil {
if c.Request != nil {
c.cachedQuery = c.Request.URL.Query()
if c.req != nil {
c.cachedQuery = c.req.URL.Query()
} else {
c.cachedQuery = url.Values{}
}
Expand All @@ -139,29 +229,29 @@ func (c *Context) getQueries() url.Values {

// WrapF is a helper function for wrapping http.HandlerFunc and returns a Handler function.
func WrapF(f http.HandlerFunc) Handler {
return func(c *Context) {
f.ServeHTTP(c.Writer, c.Request)
return func(c Context) {
f.ServeHTTP(c.Writer(), c.Request())
}
}

// WrapH is a helper function for wrapping http.Handler and returns a Handler function.
func WrapH(h http.Handler) Handler {
return func(c *Context) {
h.ServeHTTP(c.Writer, c.Request)
return func(c Context) {
h.ServeHTTP(c.Writer(), c.Request())
}
}

// WrapM is a helper function for wrapping http.Handler middleware and returns a
// Middleware function.
func WrapM(m func(handler http.Handler) http.Handler) Middleware {
return func(next Handler) Handler {
return func(c *Context) {
return func(c Context) {
adapter := m(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
ctx := c.Tree().NewContext(c.r, w, r)
defer ctx.Free()
ctx := c.Tree().NewContext(c.Fox(), w, r)
defer ctx.Close()
next(ctx)
}))
adapter.ServeHTTP(c.Writer, c.Request)
adapter.ServeHTTP(c.Writer(), c.Request())
}
}
}
18 changes: 9 additions & 9 deletions context_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ func TestContext_Queries(t *testing.T) {
req.URL.RawQuery = wantValues.Encode()

c := NewTestContext(tree, httptest.NewRecorder(), req)
values := c.QueryValues()
values := c.QueryParams()
assert.Equal(t, wantValues, values)
assert.Equal(t, wantValues, c.cachedQuery)
}
Expand All @@ -36,8 +36,8 @@ func TestContext_Query(t *testing.T) {
req.URL.RawQuery = wantValues.Encode()

c := NewTestContext(tree, httptest.NewRecorder(), req)
assert.Equal(t, "b", c.Query("a"))
assert.Equal(t, "d", c.Query("c"))
assert.Equal(t, "b", c.QueryParam("a"))
assert.Equal(t, "d", c.QueryParam("c"))
assert.Equal(t, wantValues, c.cachedQuery)
}

Expand All @@ -52,16 +52,16 @@ func TestContext_Clone(t *testing.T) {
req.URL.RawQuery = wantValues.Encode()

c := NewHttpTestContext(tree)
c.Request = req
c.req = req

buf := []byte("foo bar")
_, err := c.Writer.Write(buf)
_, err := c.w.Write(buf)
require.NoError(t, err)

cc := c.Clone()
assert.Equal(t, http.StatusOK, cc.Writer.Status())
assert.Equal(t, len(buf), cc.Writer.Size())
assert.Equal(t, wantValues, c.QueryValues())
_, err = cc.Writer.Write([]byte("invalid"))
assert.Equal(t, http.StatusOK, cc.Writer().Status())
assert.Equal(t, len(buf), cc.Writer().Size())
assert.Equal(t, wantValues, c.QueryParams())
_, err = cc.Writer().Write([]byte("invalid"))
assert.ErrorIs(t, err, ErrDiscardedResponseWriter)
}
10 changes: 6 additions & 4 deletions error.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,12 @@ import (
)

var (
ErrRouteNotFound = errors.New("route not found")
ErrRouteExist = errors.New("route already registered")
ErrRouteConflict = errors.New("route conflict")
ErrInvalidRoute = errors.New("invalid route")
ErrRouteNotFound = errors.New("route not found")
ErrRouteExist = errors.New("route already registered")
ErrRouteConflict = errors.New("route conflict")
ErrInvalidRoute = errors.New("invalid route")
ErrDiscardedResponseWriter = errors.New("discarded response writer")
ErrInvalidRedirectCode = errors.New("invalid redirect code")
)

type RouteConflictError struct {
Expand Down
Loading

0 comments on commit 7b1cb7a

Please sign in to comment.