Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
109 changes: 109 additions & 0 deletions http/ant_matcher.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
// Copyright 2025 Codnect
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package http

import (
"regexp"
"strings"
)

type antRoute struct {
method string
pattern string
matcher *regexp.Regexp
placeholders []string
handler Handler
middleware []Middleware
}

// newAntRoute builds an ant-style route with placeholders and wildcard support.
func newAntRoute(method, pattern string, handler Handler, middleware []Middleware) (*antRoute, error) {
regex, placeholders, err := compileAntPattern(pattern)
if err != nil {
return nil, err
}

return &antRoute{
method: strings.ToUpper(method),
pattern: pattern,
matcher: regex,
placeholders: placeholders,
handler: handler,
middleware: middleware,
}, nil
}

func (r *antRoute) match(method, path string) (map[string]string, bool) {
if r.method != method {
return nil, false
}

matches := r.matcher.FindStringSubmatch(path)
if len(matches) == 0 {
return nil, false
}

params := make(map[string]string)
for i, name := range r.placeholders {
params[name] = matches[i+1]
}
return params, true
}

// compileAntPattern compiles an ant-style pattern into a regular expression.
func compileAntPattern(pattern string) (*regexp.Regexp, []string, error) {
var builder strings.Builder
builder.WriteString("^")

placeholders := make([]string, 0)
for i := 0; i < len(pattern); i++ {
ch := pattern[i]

switch ch {
case '*':
if i+1 < len(pattern) && pattern[i+1] == '*' {
builder.WriteString("(.*)")
i++
} else {
builder.WriteString("([^/]*)")
}
case '?':
builder.WriteString("([^/])")
case '{':
end := strings.IndexByte(pattern[i:], '}')
if end == -1 {
return nil, nil, ErrInvalidPattern
}

placeholder := pattern[i+1 : i+end]
placeholders = append(placeholders, placeholder)
builder.WriteString("([^/]+)")
i += end
default:
if strings.ContainsRune(".+()^$|[]\\", rune(ch)) {
builder.WriteString("\\")
}
builder.WriteByte(ch)
}
}

builder.WriteString("$")
regex, err := regexp.Compile(builder.String())
if err != nil {
return nil, nil, err
}

return regex, placeholders, nil
}
67 changes: 67 additions & 0 deletions http/context.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
// Copyright 2025 Codnect
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package http

import "io"

// Handler represents an HTTP handler function.
type Handler func(Context)

// Middleware represents an HTTP middleware component.
type Middleware = Handler

// Context represents the request-scoped context passed to handlers and middleware.
type Context interface {
// Request returns the incoming HTTP request abstraction.
Request() Request
// Response returns the outgoing HTTP response abstraction.
Response() Response
// Security returns the active SecurityContext.
Security() SecurityContext
// SetSecurity replaces the current security context.
SetSecurity(SecurityContext)
// Next executes the next middleware or handler in the chain.
Next()
// Params returns the matched route parameters.
Params() map[string]string
// Param retrieves a specific route parameter by name.
Param(string) string
// Set stores a value in the context.
Set(string, any)
// Get retrieves a stored value and a boolean indicating presence.
Get(string) (any, bool)
}

// Request is an abstract HTTP request representation.
type Request interface {
// Method returns the HTTP method of the request.
Method() string
// Path returns the request path.
Path() string
// Header returns the first value for the named header.
Header(string) string
// Body returns the raw request body reader.
Body() io.ReadCloser
}

// Response is an abstract HTTP response representation.
type Response interface {
// Status writes the HTTP status code.
Status(int)
// Header sets a header value on the response.
Header(string, string)
// Write writes the response body bytes.
Write([]byte) (int, error)
}
126 changes: 126 additions & 0 deletions http/radix_tree.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
// Copyright 2025 Codnect
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package http

import "strings"

type radixNode struct {
segment string
children map[string]*radixNode
paramChild *radixNode
wildcard *radixNode
paramName string
handlers map[string]routeEntry
}

type routeEntry struct {
handler Handler
middleware []Middleware
}

func newRadixNode(segment string) *radixNode {
return &radixNode{
segment: segment,
children: make(map[string]*radixNode),
handlers: make(map[string]routeEntry),
}
}

func (n *radixNode) addRoute(method, pattern string, handler Handler, middleware []Middleware) {
segments := splitPath(pattern)
current := n

for _, segment := range segments {
switch {
case strings.HasPrefix(segment, ":"):
if current.paramChild == nil {
current.paramChild = newRadixNode(segment)
current.paramChild.paramName = strings.TrimPrefix(segment, ":")
}
current = current.paramChild
case segment == "**":
if current.wildcard == nil {
current.wildcard = newRadixNode(segment)
}
current = current.wildcard
default:
child, ok := current.children[segment]
if !ok {
child = newRadixNode(segment)
current.children[segment] = child
}
current = child
}
}

current.handlers[strings.ToUpper(method)] = routeEntry{handler: handler, middleware: middleware}
}

func (n *radixNode) match(method, path string) (map[string]string, routeEntry, bool) {
segments := splitPath(path)
params := make(map[string]string)
if entry, ok := n.matchSegments(strings.ToUpper(method), segments, params); ok {
return params, entry, true
}
return nil, routeEntry{}, false
}

func (n *radixNode) matchSegments(method string, segments []string, params map[string]string) (routeEntry, bool) {
if len(segments) == 0 {
if entry, ok := n.handlers[method]; ok {
return entry, true
}
if n.wildcard != nil {
if entry, ok := n.wildcard.handlers[method]; ok {
return entry, true
}
}
return routeEntry{}, false
}

segment := segments[0]

if child, ok := n.children[segment]; ok {
if entry, ok := child.matchSegments(method, segments[1:], params); ok {
return entry, true
}
}

if n.paramChild != nil {
params[n.paramChild.paramName] = segment
if entry, ok := n.paramChild.matchSegments(method, segments[1:], params); ok {
return entry, true
}
delete(params, n.paramChild.paramName)
}

if n.wildcard != nil {
params["*wildcard"] = strings.Join(segments, "/")
if entry, ok := n.wildcard.matchSegments(method, nil, params); ok {
return entry, true
}
delete(params, "*wildcard")
}

return routeEntry{}, false
}

func splitPath(path string) []string {
cleaned := strings.Trim(path, "/")
if cleaned == "" {
return nil
}
return strings.Split(cleaned, "/")
}
81 changes: 81 additions & 0 deletions http/router.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
// Copyright 2025 Codnect
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package http

import (
"errors"
"strings"
)

// ErrInvalidPattern indicates the provided pattern is malformed.
var ErrInvalidPattern = errors.New("invalid route pattern")

// Router routes HTTP requests using radix-tree and ant-style pattern matching.
type Router struct {
root *radixNode
antRoutes []*antRoute
notFound Handler
}

// NewRouter creates a new Router instance.
func NewRouter() *Router {
return &Router{
root: newRadixNode("/"),
notFound: func(ctx Context) {},
}
}

// Handle registers a route for the given method and pattern.
func (r *Router) Handle(method, pattern string, handler Handler, middleware ...Middleware) error {
pattern = strings.TrimSpace(pattern)
if pattern == "" {
return ErrInvalidPattern
}

if strings.ContainsAny(pattern, "*?{") {
route, err := newAntRoute(method, pattern, handler, middleware)
if err != nil {
return err
}
r.antRoutes = append(r.antRoutes, route)
return nil
}

r.root.addRoute(method, pattern, handler, middleware)
return nil
}

// NotFound sets the handler executed when no routes match.
func (r *Router) NotFound(handler Handler) {
if handler != nil {
r.notFound = handler
}
}

// Resolve resolves the incoming method and path into a handler and middleware chain.
func (r *Router) Resolve(method, path string) (Handler, []Middleware, map[string]string) {
params, entry, ok := r.root.match(strings.ToUpper(method), path)
if ok {
return entry.handler, entry.middleware, params
}

for _, route := range r.antRoutes {
if params, matched := route.match(strings.ToUpper(method), path); matched {
return route.handler, route.middleware, params
}
}

return r.notFound, nil, map[string]string{}
}
Loading