Skip to content

Commit

Permalink
feat: wip per route options
Browse files Browse the repository at this point in the history
  • Loading branch information
tigerwill90 committed Sep 14, 2024
1 parent 1f1c12a commit 74dab72
Show file tree
Hide file tree
Showing 3 changed files with 128 additions and 37 deletions.
57 changes: 49 additions & 8 deletions fox.go
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,47 @@ func (f ClientIPStrategyFunc) ClientIP(c Context) (*net.IPAddr, error) {
return f(c)
}

// Route represent a registered route in the route tree.
type Route struct {
ipStrategy ClientIPStrategy
handler HandlerFunc
path string
mws []middleware
redirectTrailingSlash bool
ignoreTrailingSlash bool
}

// Handle call the registered handler with the provided Context.
func (r *Route) Handle(c Context) {
r.handler(c)
}

// Path returns the route path.
func (r *Route) Path() string {
return r.path
}

// RedirectTrailingSlashEnabled returns whether the route is configured to automatically
// redirect requests that include or omit a trailing slash.
// This api is EXPERIMENTAL and is likely to change in future release.
func (r *Route) RedirectTrailingSlashEnabled() bool {
return r.redirectTrailingSlash
}

// IgnoreTrailingSlashEnabled returns whether the route is configured to ignore
// trailing slashes in requests when matching routes.
// This api is EXPERIMENTAL and is likely to change in future release.
func (r *Route) IgnoreTrailingSlashEnabled() bool {
return r.ignoreTrailingSlash
}

// ClientIPStrategyEnabled returns whether the route is configured with a ClientIPStrategy.
// This api is EXPERIMENTAL and is likely to change in future release.
func (r *Route) ClientIPStrategyEnabled() bool {
_, ok := r.ipStrategy.(noClientIPStrategy)
return !ok
}

// Router is a lightweight high performance HTTP request router that support mutation on its routing tree
// while handling request concurrently.
type Router struct {
Expand All @@ -91,7 +132,7 @@ type middleware struct {
var _ http.Handler = (*Router)(nil)

// New returns a ready to use instance of Fox router.
func New(opts ...Option) *Router {
func New(opts ...GlobalOption) *Router {
r := new(Router)

r.noRoute = DefaultNotFoundHandler()
Expand All @@ -100,7 +141,7 @@ func New(opts ...Option) *Router {
r.ipStrategy = noClientIPStrategy{}

for _, opt := range opts {
opt.apply(r)
opt.applyGlob(r)
}

r.noRoute = applyMiddleware(NoRouteHandler, r.mws, r.noRoute)
Expand Down Expand Up @@ -190,20 +231,20 @@ func (fox *Router) Swap(new *Tree) (old *Tree) {
// is already registered or conflict with another. It's perfectly safe to add a new handler while the tree is in use
// for serving requests. This function is safe for concurrent use by multiple goroutine.
// To override an existing route, use Update.
func (fox *Router) Handle(method, path string, handler HandlerFunc) error {
func (fox *Router) Handle(method, path string, handler HandlerFunc, opts ...PathOption) error {
t := fox.Tree()
t.Lock()
defer t.Unlock()
return t.Handle(method, path, handler)
return t.Handle(method, path, handler, opts...)

Check failure on line 238 in fox.go

View workflow job for this annotation

GitHub Actions / Test Fox (>=1.21)

cannot use ... in call to non-variadic t.Handle

Check failure on line 238 in fox.go

View workflow job for this annotation

GitHub Actions / Lint Fox (>=1.21)

cannot use ... in call to non-variadic t.Handle

Check failure on line 238 in fox.go

View workflow job for this annotation

GitHub Actions / Lint Fox (>=1.21)

cannot use ... in call to non-variadic t.Handle
}

// MustHandle registers a new handler for the given method and path. This function is a convenience
// wrapper for the Handle function. It will panic if the route is already registered or conflicts
// with another route. It's perfectly safe to add a new handler while the tree is in use for serving
// requests. This function is safe for concurrent use by multiple goroutines.
// To override an existing route, use Update.
func (fox *Router) MustHandle(method, path string, handler HandlerFunc) {
if err := fox.Handle(method, path, handler); err != nil {
func (fox *Router) MustHandle(method, path string, handler HandlerFunc, opts ...PathOption) {
if err := fox.Handle(method, path, handler, opts...); err != nil {
panic(err)
}
}
Expand All @@ -212,11 +253,11 @@ func (fox *Router) MustHandle(method, path string, handler HandlerFunc) {
// the function return an ErrRouteNotFound. It's perfectly safe to update a handler while the tree is in use for
// serving requests. This function is safe for concurrent use by multiple goroutine.
// To add new handler, use Handle method.
func (fox *Router) Update(method, path string, handler HandlerFunc) error {
func (fox *Router) Update(method, path string, handler HandlerFunc, opts ...PathOption) error {
t := fox.Tree()
t.Lock()
defer t.Unlock()
return t.Update(method, path, handler)
return t.Update(method, path, handler, opts...)

Check failure on line 260 in fox.go

View workflow job for this annotation

GitHub Actions / Test Fox (>=1.21)

cannot use ... in call to non-variadic t.Update

Check failure on line 260 in fox.go

View workflow job for this annotation

GitHub Actions / Lint Fox (>=1.21)

cannot use ... in call to non-variadic t.Update (typecheck)

Check failure on line 260 in fox.go

View workflow job for this annotation

GitHub Actions / Lint Fox (>=1.21)

cannot use ... in call to non-variadic t.Update) (typecheck)
}

// Remove delete an existing handler for the given method and path. If the route does not exist, the function
Expand Down
106 changes: 78 additions & 28 deletions options.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,19 +23,44 @@ const (
)

type Option interface {
apply(*Router)
GlobalOption
PathOption
}

type optionFunc func(*Router)
type GlobalOption interface {
applyGlob(*Router)
}

type PathOption interface {
applyPath(*Route)
}

func (o optionFunc) apply(r *Router) {
type globOptionFunc func(*Router)

func (o globOptionFunc) applyGlob(r *Router) {
o(r)
}

type pathOptionFunc func(*Route)

func (o pathOptionFunc) applyPath(r *Route) {
o(r)
}

type optionFunc func(*Router, *Route)

func (o optionFunc) applyGlob(r *Router) {
o(r, nil)
}

func (o optionFunc) applyPath(r *Route) {
o(nil, r)
}

// WithNoRouteHandler register an HandlerFunc which is called when no matching route is found.
// By default, the DefaultNotFoundHandler is used.
func WithNoRouteHandler(handler HandlerFunc) Option {
return optionFunc(func(r *Router) {
func WithNoRouteHandler(handler HandlerFunc) GlobalOption {
return globOptionFunc(func(r *Router) {
if handler != nil {
r.noRoute = handler
}
Expand All @@ -46,8 +71,8 @@ func WithNoRouteHandler(handler HandlerFunc) Option {
// but the same route exist for other methods. The "Allow" header it automatically set before calling the
// handler. By default, the DefaultMethodNotAllowedHandler is used. Note that this option automatically
// enable WithNoMethod.
func WithNoMethodHandler(handler HandlerFunc) Option {
return optionFunc(func(r *Router) {
func WithNoMethodHandler(handler HandlerFunc) GlobalOption {
return globOptionFunc(func(r *Router) {
if handler != nil {
r.noMethod = handler
r.handleMethodNotAllowed = true
Expand All @@ -60,29 +85,39 @@ func WithNoMethodHandler(handler HandlerFunc) Option {
// handler take priority over automatic replies. By default, DefaultOptionsHandler is used. Note that this option
// automatically enable WithAutoOptions.
// This api is EXPERIMENTAL and is likely to change in future release.
func WithOptionsHandler(handler HandlerFunc) Option {
return optionFunc(func(r *Router) {
func WithOptionsHandler(handler HandlerFunc) GlobalOption {
return globOptionFunc(func(r *Router) {
if handler != nil {
r.autoOptions = handler
r.handleOptions = true
}
})
}

// WithMiddleware attaches a global middleware to the router. Middlewares provided will be chained
// in the order they were added. Note that this option apply middleware to all handler, including NotFound,
// MethodNotAllowed and the internal redirect handler.
// WithMiddleware attaches a middleware to the router or a path. Middlewares provided will be chained in the order they
// were added. Note that this option, when used globally, apply middleware to all handler, including NotFound, MethodNotAllowed,
// AutoOption and the internal redirect handler.
func WithMiddleware(m ...MiddlewareFunc) Option {
return WithMiddlewareFor(AllHandlers, m...)
return optionFunc(func(router *Router, route *Route) {
if router != nil {
for i := range m {
router.mws = append(router.mws, middleware{m[i], AllHandlers})
}
}
if route != nil {
for i := range m {
route.mws = append(route.mws, middleware{m[i], RouteHandlers})
}
}
})
}

// WithMiddlewareFor attaches middleware to the router for a specified scope. Middlewares provided will be chained
// in the order they were added. The scope parameter determines which types of handlers the middleware will be applied to.
// Possible scopes include RouteHandlers (regular routes), NoRouteHandler, NoMethodHandler, RedirectHandler, OptionsHandler,
// and any combination of these. Use this option when you need fine-grained control over where the middleware is applied.
// This api is EXPERIMENTAL and is likely to change in future release.
func WithMiddlewareFor(scope MiddlewareScope, m ...MiddlewareFunc) Option {
return optionFunc(func(r *Router) {
func WithMiddlewareFor(scope MiddlewareScope, m ...MiddlewareFunc) GlobalOption {
return globOptionFunc(func(r *Router) {
for i := range m {
r.mws = append(r.mws, middleware{m[i], scope})
}
Expand All @@ -93,8 +128,8 @@ func WithMiddlewareFor(scope MiddlewareScope, m ...MiddlewareFunc) Option {
// when the route exist for another http verb. The "Allow" header it automatically set before calling the
// handler. Note that this option is automatically enabled when providing a custom handler with the
// option WithNoMethodHandler.
func WithNoMethod(enable bool) Option {
return optionFunc(func(r *Router) {
func WithNoMethod(enable bool) GlobalOption {
return globOptionFunc(func(r *Router) {
r.handleMethodNotAllowed = enable
})
}
Expand All @@ -104,8 +139,8 @@ func WithNoMethod(enable bool) Option {
// determines the "Allow" header value based on the methods registered for the given route. Note that custom OPTIONS
// handler take priority over automatic replies. This option is automatically enabled when providing a custom handler with
// the option WithOptionsHandler. This api is EXPERIMENTAL and is likely to change in future release.
func WithAutoOptions(enable bool) Option {
return optionFunc(func(r *Router) {
func WithAutoOptions(enable bool) GlobalOption {
return globOptionFunc(func(r *Router) {
r.handleOptions = enable
})
}
Expand All @@ -116,8 +151,13 @@ func WithAutoOptions(enable bool) Option {
// all other methods. Note that this option is mutually exclusive with WithIgnoreTrailingSlash, and if both are
// enabled, WithIgnoreTrailingSlash takes precedence.
func WithRedirectTrailingSlash(enable bool) Option {
return optionFunc(func(r *Router) {
r.redirectTrailingSlash = enable
return optionFunc(func(router *Router, route *Route) {
if router != nil {
router.redirectTrailingSlash = enable
}
if route != nil {
route.redirectTrailingSlash = enable
}
})
}

Expand All @@ -127,8 +167,13 @@ func WithRedirectTrailingSlash(enable bool) Option {
// WithRedirectTrailingSlash, and if both are enabled, WithIgnoreTrailingSlash takes precedence.
// This api is EXPERIMENTAL and is likely to change in future release.
func WithIgnoreTrailingSlash(enable bool) Option {
return optionFunc(func(r *Router) {
r.ignoreTrailingSlash = enable
return optionFunc(func(router *Router, route *Route) {
if router != nil {
router.ignoreTrailingSlash = enable
}
if route != nil {
route.ignoreTrailingSlash = enable
}
})
}

Expand All @@ -139,18 +184,23 @@ func WithIgnoreTrailingSlash(enable bool) Option {
// There is no sane default, so if no strategy is configured, Context.ClientIP returns ErrNoClientIPStrategy.
// This API is EXPERIMENTAL and is likely to change in future releases.
func WithClientIPStrategy(strategy ClientIPStrategy) Option {
return optionFunc(func(r *Router) {
return optionFunc(func(router *Router, route *Route) {
if strategy != nil {
r.ipStrategy = strategy
if router != nil {
router.ipStrategy = strategy
}
if route != nil {
route.ipStrategy = strategy
}
}
})
}

// DefaultOptions configure the router to use the Recovery middleware for the RouteHandlers scope, the Logger middleware
// for AllHandlers scope and enable automatic OPTIONS response. Note that DefaultOptions push the Recovery and Logger middleware
// respectively to the first and second position of the middleware chains.
func DefaultOptions() Option {
return optionFunc(func(r *Router) {
func DefaultOptions() GlobalOption {
return globOptionFunc(func(r *Router) {
r.mws = append([]middleware{
{Recovery(), RouteHandlers},
{Logger(), AllHandlers},
Expand Down
2 changes: 1 addition & 1 deletion strategy/strategy.go
Original file line number Diff line number Diff line change
Expand Up @@ -292,8 +292,8 @@ func (s RightmostTrustedCount) ClientIP(c fox.Context) (*net.IPAddr, error) {
// attacker creates a CF distribution that points at your origin server. The attacker uses Lambda@Edge to spoof the Host
// and X-Forwarded-For headers. Now your "trusted" reverse proxy is no longer trustworthy.
type RightmostTrustedRange struct {
headerName string
resolver TrustedIPRange
headerName string
}

// NewRightmostTrustedRange creates a RightmostTrustedRange strategy. headerName must be "X-Forwarded-For"
Expand Down

0 comments on commit 74dab72

Please sign in to comment.