Skip to content

Commit

Permalink
Polishing API (#42)
Browse files Browse the repository at this point in the history
* feat: returns registered route on handler & update

* docs: update README.md
  • Loading branch information
tigerwill90 authored Oct 14, 2024
1 parent dcc925a commit 0c0e6fd
Show file tree
Hide file tree
Showing 10 changed files with 304 additions and 279 deletions.
99 changes: 40 additions & 59 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ routing structure based on user input, configuration changes, or other runtime e
The current api is not yet stabilize. Breaking changes may occur before `v1.0.0` and will be noted on the release note.

## Features
**Runtime updates:** Register, update and remove route handler safely at any time without impact on performance. Fox never block while serving
**Runtime updates:** Register, update and delete route handler safely at any time without impact on performance. Fox never block while serving
request!

**Wildcard pattern:** Route can be registered using wildcard parameters. The matched path segment can then be easily retrieved by
Expand Down Expand Up @@ -53,33 +53,22 @@ go get -u github.com/tigerwill90/fox
package main

import (
"errors"
"github.com/tigerwill90/fox"
"log"
"net/http"
)

type Greeting struct {
Say string
}

func (h *Greeting) Greet(c fox.Context) {
_ = c.String(http.StatusOK, "%s %s\n", h.Say, c.Param("name"))
}

func main() {
f := fox.New(fox.DefaultOptions())

err := f.Handle(http.MethodGet, "/", func(c fox.Context) {
_ = c.String(http.StatusOK, "Welcome\n")
f.MustHandle(http.MethodGet, "/hello/{name}", func(c fox.Context) {
_ = c.String(http.StatusOK, "hello %s\n", c.Param("name"))
})
if err != nil {
panic(err)
}

h := Greeting{Say: "Hello"}
f.MustHandle(http.MethodGet, "/hello/{name}", h.Greet)

log.Fatalln(http.ListenAndServe(":8080", f))
if err := http.ListenAndServe(":8080", f); err != nil && !errors.Is(err, http.ErrServerClosed) {
log.Fatalln(err)
}
}
````
#### Error handling
Expand Down Expand Up @@ -109,10 +98,10 @@ to retrieve directly the value of a parameter using the placeholder name.
````
Pattern /avengers/{name}
/avengers/ironman match
/avengers/thor match
/avengers/hulk/angry no match
/avengers/ no match
/avengers/ironman match
/avengers/thor match
/avengers/hulk/angry no match
/avengers/ no match
Pattern /users/uuid:{id}
Expand Down Expand Up @@ -206,14 +195,15 @@ As such threads that route requests should never encounter latency due to ongoin

### Managing routes a runtime
#### Routing mutation
In this example, the handler for `routes/{action}` allow to dynamically register, update and remove handler for the
In this example, the handler for `routes/{action}` allow to dynamically register, update and delete handler for the
given route and method. Thanks to Fox's design, those actions are perfectly safe and may be executed concurrently.

````go
package main

import (
"encoding/json"
"errors"
"fmt"
"github.com/tigerwill90/fox"
"log"
Expand Down Expand Up @@ -241,15 +231,15 @@ func Action(c fox.Context) {
action := c.Param("action")
switch action {
case "add":
err = c.Fox().Handle(method, path, func(c fox.Context) {
_, err = c.Fox().Handle(method, path, func(c fox.Context) {
_ = c.String(http.StatusOK, text)
})
case "update":
err = c.Fox().Update(method, path, func(c fox.Context) {
_, err = c.Fox().Update(method, path, func(c fox.Context) {
_ = c.String(http.StatusOK, text)
})
case "delete":
err = c.Fox().Remove(method, path)
err = c.Fox().Delete(method, path)
default:
http.Error(c.Writer(), fmt.Sprintf("action %q is not allowed", action), http.StatusBadRequest)
return
Expand All @@ -263,9 +253,12 @@ func Action(c fox.Context) {
}

func main() {
f := fox.New()
f := fox.New(fox.DefaultOptions())
f.MustHandle(http.MethodPost, "/routes/{action}", Action)
log.Fatalln(http.ListenAndServe(":8080", f))

if err := http.ListenAndServe(":8080", f); err != nil && !errors.Is(err, http.ErrServerClosed) {
log.Fatalln(err)
}
}
````

Expand All @@ -277,48 +270,36 @@ Note that router's options apply automatically on the new tree.
package main

import (
"fox-by-example/db"
"errors"
"github.com/tigerwill90/fox"
"html/template"
"log"
"net/http"
"strings"
"time"
)

type HtmlRenderer struct {
Template template.HTML
}

func (h *HtmlRenderer) Render(c fox.Context) {
log.Printf("matched handler path: %s", c.Path())
_ = c.Stream(
http.StatusOK,
fox.MIMETextHTMLCharsetUTF8,
strings.NewReader(string(h.Template)),
)
}

func main() {
f := fox.New()
go Reload(f)
log.Fatalln(http.ListenAndServe(":8080", f))
}

func Reload(r *fox.Router) {
for ; true; <-time.Tick(10 * time.Second) {
routes := db.GetRoutes()
routes := GetRoutes()
tree := r.NewTree()
for _, rte := range routes {
h := HtmlRenderer{Template: rte.Template}
if err := tree.Handle(rte.Method, rte.Path, h.Render); err != nil {
log.Printf("error reloading route: %s\n", err)
if _, err := tree.Handle(rte.Method, rte.Path, rte.Handler); err != nil {
log.Printf("failed to register route: %s\n", err)
continue
}
}
// Swap the currently in-use routing tree with the new provided.
r.Swap(tree)
log.Println("route reloaded")
log.Println("route reloaded!")
}
}

func main() {
f := fox.New(fox.DefaultOptions())

go Reload(f)

if err := http.ListenAndServe(":8080", f); err != nil && !errors.Is(err, http.ErrServerClosed) {
log.Fatalln(err)
}
}
````
Expand All @@ -333,7 +314,7 @@ is already registered for the provided method and path. By locking the `Tree`, t
atomicity, as it prevents other threads from modifying the tree between the lookup and the write operation.
Note that all read operation on the tree remain lock-free.
````go
func Upsert(t *fox.Tree, method, path string, handler fox.HandlerFunc) error {
func Upsert(t *fox.Tree, method, path string, handler fox.HandlerFunc) (*fox.Route, error) {
t.Lock()
defer t.Unlock()
if t.Has(method, path) {
Expand Down Expand Up @@ -446,9 +427,9 @@ only for 404 or 405 handlers. Possible scopes include `fox.RouteHandlers` (regul

````go
f := fox.New(
fox.WithNoMethod(true),
fox.WithMiddlewareFor(fox.RouteHandlers, fox.Recovery(), Logger),
fox.WithMiddlewareFor(fox.NoRouteHandler|fox.NoMethodHandler, SpecialLogger),
fox.WithNoMethod(true),
fox.WithMiddlewareFor(fox.RouteHandler, fox.Recovery(), Logger),
fox.WithMiddlewareFor(fox.NoRouteHandler|fox.NoMethodHandler, SpecialLogger),
)
````

Expand Down
48 changes: 22 additions & 26 deletions context.go
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ type Context interface {
ClientIP() (*net.IPAddr, error)
// Path returns the registered path or an empty string if the handler is called in a scope other than [RouteHandler].
Path() string
// Route returns the registered route or nil if the handler is called in a scope other than [RouteHandler].
// Route returns the registered [Route] or nil if the handler is called in a scope other than [RouteHandler].
Route() *Route
// Params returns a range iterator over the matched wildcard parameters for the current route.
Params() iter.Seq[Param]
Expand Down Expand Up @@ -101,7 +101,7 @@ type Context interface {
Rehydrate(route *Route) bool
}

// cTx holds request-related information and allows interaction with the ResponseWriter.
// cTx holds request-related information and allows interaction with the [ResponseWriter].
type cTx struct {
w ResponseWriter
req *http.Request
Expand All @@ -120,7 +120,7 @@ type cTx struct {
tsr bool
}

// Reset resets the Context to its initial state, attaching the provided ResponseWriter and http.Request.
// Reset resets the [Context] to its initial state, attaching the provided [ResponseWriter] and [http.Request].
func (c *cTx) Reset(w ResponseWriter, r *http.Request) {
c.req = r
c.w = w
Expand All @@ -131,8 +131,8 @@ func (c *cTx) Reset(w ResponseWriter, r *http.Request) {
*c.params = (*c.params)[:0]
}

// Rehydrate updates the current Context to serve the provided Route, bypassing the need for a full tree lookup.
// It succeeds only if the Request's URL path strictly matches the given Route. If successful, the internal state
// Rehydrate updates the current [Context] to serve the provided [Route], bypassing the need for a full tree lookup.
// It succeeds only if the [http.Request]'s URL path strictly matches the given [Route]. If successful, the internal state
// of the context is updated, allowing the context to serve the route directly, regardless of whether the route
// still exists in the routing tree. This provides a key advantage in concurrent scenarios where routes may be
// modified by other threads, as Rehydrate guarantees success if the path matches, without requiring serial execution
Expand Down Expand Up @@ -167,9 +167,9 @@ func (c *cTx) Rehydrate(route *Route) bool {
return true
}

// reset resets the Context to its initial state, attaching the provided http.ResponseWriter and http.Request.
// Caution: always pass the original http.ResponseWriter to this method, not the ResponseWriter itself, to
// avoid wrapping the ResponseWriter within itself. Use wisely!
// reset resets the [Context] to its initial state, attaching the provided [http.ResponseWriter] and [http.Request].
// Caution: always pass the original [http.ResponseWriter] to this method, not the [ResponseWriter] itself, to
// avoid wrapping the [ResponseWriter] within itself. Use wisely!
func (c *cTx) reset(w http.ResponseWriter, r *http.Request) {
c.rec.reset(w)
c.req = r
Expand All @@ -188,27 +188,27 @@ func (c *cTx) resetNil() {
*c.params = (*c.params)[:0]
}

// Request returns the *http.Request.
// Request returns the [http.Request].
func (c *cTx) Request() *http.Request {
return c.req
}

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

// Writer returns the ResponseWriter.
// Writer returns the [ResponseWriter].
func (c *cTx) Writer() ResponseWriter {
return c.w
}

// SetWriter sets the ResponseWriter.
// SetWriter sets the [ResponseWriter].
func (c *cTx) SetWriter(w ResponseWriter) {
c.w = w
}

// RemoteIP parses the IP from Request.RemoteAddr, normalizes it, and returns a *net.IPAddr.
// RemoteIP parses the IP from [http.Request.RemoteAddr], normalizes it, and returns a [net.IPAddr].
// It never returns nil, even if parsing the IP fails.
func (c *cTx) RemoteIP() *net.IPAddr {
ipStr, _, _ := net.SplitHostPort(c.req.RemoteAddr)
Expand All @@ -226,9 +226,9 @@ func (c *cTx) RemoteIP() *net.IPAddr {
return ipAddr
}

// ClientIP returns the "real" client IP address based on the configured ClientIPStrategy.
// The strategy is set using the WithClientIPStrategy option. If no strategy is configured,
// the method returns error ErrNoClientIPStrategy.
// ClientIP returns the "real" client IP address based on the configured [ClientIPStrategy].
// The strategy is set using the [WithClientIPStrategy] option. If no strategy is configured,
// the method returns error [ErrNoClientIPStrategy].
//
// The strategy used must be chosen and tuned for your network configuration. This should result
// in the strategy never returning an error -- i.e., never failing to find a candidate for the "real" IP.
Expand Down Expand Up @@ -264,7 +264,6 @@ func (c *cTx) Params() iter.Seq[Param] {
}

// Param retrieve a matching wildcard segment by name.
// It's a helper for c.Params.Get(name).
func (c *cTx) Param(name string) string {
for p := range c.Params() {
if p.Key == name {
Expand All @@ -274,15 +273,12 @@ func (c *cTx) Param(name string) string {
return ""
}

// QueryParams parses RawQuery and returns the corresponding values.
// It's a helper for c.Request.URL.Query(). Note that the parsed
// result is cached.
// QueryParams parses the [http.Request] raw query and returns the corresponding values.
func (c *cTx) QueryParams() url.Values {
return c.getQueries()
}

// QueryParam returns the first value associated with the given key.
// It's a helper for c.QueryParams().Get(name).
func (c *cTx) QueryParam(name string) string {
return c.getQueries().Get(name)
}
Expand All @@ -297,15 +293,15 @@ func (c *cTx) Header(key string) string {
return c.req.Header.Get(key)
}

// Path returns the registered path or an empty string if the handler is called in a scope other than RouteHandler.
// Path returns the registered path or an empty string if the handler is called in a scope other than [RouteHandler].
func (c *cTx) Path() string {
if c.route == nil {
return ""
}
return c.route.path
}

// Route returns the registered route or nil if the handler is called in a scope other than RouteHandler.
// Route returns the registered [Route] or nil if the handler is called in a scope other than [RouteHandler].
func (c *cTx) Route() *Route {
return c.route
}
Expand All @@ -328,7 +324,7 @@ func (c *cTx) Blob(code int, contentType string, buf []byte) (err error) {
return
}

// Stream sends data from an io.Reader with the specified status code and content type.
// Stream sends data from an [io.Reader] with the specified status code and content type.
func (c *cTx) Stream(code int, contentType string, r io.Reader) (err error) {
c.w.Header().Set(HeaderContentType, contentType)
c.w.WriteHeader(code)
Expand All @@ -345,12 +341,12 @@ func (c *cTx) Redirect(code int, url string) error {
return nil
}

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

// Fox returns the Router instance.
// Fox returns the [Router] instance.
func (c *cTx) Fox() *Router {
return c.fox
}
Expand Down
Loading

0 comments on commit 0c0e6fd

Please sign in to comment.