srv API

srv

package

API reference for the srv package.

F
function

Auth

Auth returns middleware that validates a Bearer token using auth.VerifyToken.
A valid token is attached to the context via auth.Payload.

Parameters

secret
[]byte

Returns

pkg/srv/auth.go:12-34
func Auth(secret []byte) Middleware

{
	return func(next HandlerFunc) HandlerFunc {
		return func(ctx *Context) error {
			h := ctx.Request.Header.Get("Authorization")
			if h == "" {
				return ctx.JSON(http.StatusUnauthorized, map[string]string{"error": "missing Authorization header"})
			}

			parts := strings.SplitN(h, " ", 2)
			if len(parts) != 2 || strings.ToLower(parts[0]) != "bearer" {
				return ctx.JSON(http.StatusUnauthorized, map[string]string{"error": "invalid Authorization format"})
			}

			payload, err := auth.VerifyToken(parts[1], secret)
			if err != nil {
				return ctx.JSON(http.StatusUnauthorized, map[string]string{"error": err.Error()})
			}

			ctx.Set("auth.payload", payload)
			return next(ctx)
		}
	}
}
F
function

TestAuth_MissingHeader

Parameters

pkg/srv/auth_test.go:11-25
func TestAuth_MissingHeader(t *testing.T)

{
	s := New()
	s.Use(Auth([]byte("secret")))
	s.MapGet("/api", func(ctx *Context) error {
		return ctx.JSON(200, map[string]string{"ok": "yes"})
	})

	req := httptest.NewRequest("GET", "/api", nil)
	w := httptest.NewRecorder()
	s.ServeHTTP(w, req)

	if w.Code != http.StatusUnauthorized {
		t.Errorf("status: got %d, want 401", w.Code)
	}
}
F
function

TestAuth_InvalidToken

Parameters

pkg/srv/auth_test.go:27-42
func TestAuth_InvalidToken(t *testing.T)

{
	s := New()
	s.Use(Auth([]byte("secret")))
	s.MapGet("/api", func(ctx *Context) error {
		return ctx.JSON(200, map[string]string{"ok": "yes"})
	})

	req := httptest.NewRequest("GET", "/api", nil)
	req.Header.Set("Authorization", "Bearer badtoken")
	w := httptest.NewRecorder()
	s.ServeHTTP(w, req)

	if w.Code != http.StatusUnauthorized {
		t.Errorf("status: got %d, want 401", w.Code)
	}
}
F
function

TestAuth_ValidToken

Parameters

pkg/srv/auth_test.go:44-60
func TestAuth_ValidToken(t *testing.T)

{
	s := New()
	s.Use(Auth([]byte("secret")))
	s.MapGet("/api", func(ctx *Context) error {
		return ctx.JSON(200, map[string]string{"ok": "yes"})
	})

	token, _ := auth.SignToken(auth.Payload{Sub: "user1", Exp: 9999999999}, []byte("secret"))
	req := httptest.NewRequest("GET", "/api", nil)
	req.Header.Set("Authorization", "Bearer "+token)
	w := httptest.NewRecorder()
	s.ServeHTTP(w, req)

	if w.Code != 200 {
		t.Errorf("status: got %d, want 200", w.Code)
	}
}
T
type

methodHandlers

pkg/srv/radix.go:10-10
type methodHandlers map[string]HandlerFunc
S
struct

paramConstraint

pkg/srv/radix.go:12-15
type paramConstraint struct

Fields

Name Type Description
name string
validate func(string) bool
S
struct

radixNode

pkg/srv/radix.go:17-25
type radixNode struct

Fields

Name Type Description
children []*radixNode
segment string
isParam bool
isCatchAll bool
paramName string
constraints []paramConstraint
handlers methodHandlers
S
struct

radixTree

pkg/srv/radix.go:27-30
type radixTree struct

Methods

insert
Method

Parameters

method string
path string
handler HandlerFunc
func (*radixTree) insert(method, path string, handler HandlerFunc)
{
	t.mu.Lock()
	defer t.mu.Unlock()

	segments := splitPath(path)

	if len(segments) > 0 && isCatchAll(segments[len(segments)-1]) {
		segments = segments[:len(segments)-1]
		node := t.walkOrCreate(t.root, segments)
		paramName, _ := parseCatchAll(path)
		catchAllNode := &radixNode{
			isCatchAll: true,
			paramName:  paramName,
			children:   make([]*radixNode, 0),
		}
		node.children = append(node.children, catchAllNode)
		if catchAllNode.handlers == nil {
			catchAllNode.handlers = make(methodHandlers)
		}
		catchAllNode.handlers[method] = handler
		return
	}

	node := t.walkOrCreate(t.root, segments)
	if node.handlers == nil {
		node.handlers = make(methodHandlers)
	}
	node.handlers[method] = handler
}
walkOrCreate
Method

Parameters

node *radixNode
segments []string

Returns

func (*radixTree) walkOrCreate(node *radixNode, segments []string) *radixNode
{
	for _, seg := range segments {
		if isParam(seg) {
			name, constraints := parseParamConstraints(seg)
			node = findOrCreateParam(node, name, constraints)
		} else {
			node = findOrCreateStatic(node, seg)
		}
	}
	return node
}
lookup
Method

Parameters

method string
path string

Returns

map[string]string
func (*radixTree) lookup(method, path string) (HandlerFunc, map[string]string)
{
	t.mu.RLock()
	defer t.mu.RUnlock()

	segments := splitPath(path)
	params := make(map[string]string)
	node := t.root

	segmentIdx := 0
	for segmentIdx < len(segments) {
		seg := segments[segmentIdx]

		var next *radixNode
		for _, child := range node.children {
			if !child.isParam && !child.isCatchAll && child.segment == seg {
				next = child
				break
			}
		}

		if next == nil {
			for _, child := range node.children {
				if child.isCatchAll {
					remaining := strings.Join(segments[segmentIdx:], "/")
					params[child.paramName] = remaining
					handler, ok := child.handlers[method]
					if !ok {
						return nil, nil
					}
					return handler, params
				}
				if child.isParam {
					if len(child.constraints) > 0 {
						allValid := true
						for _, c := range child.constraints {
							if !c.validate(seg) {
								allValid = false
								break
							}
						}
						if !allValid {
							return nil, nil
						}
					}
					params[child.paramName] = seg
					next = child
					break
				}
			}
		}

		if next == nil {
			return nil, nil
		}
		node = next
		segmentIdx++
	}

	if node.handlers == nil {
		for _, child := range node.children {
			if child.isCatchAll {
				params[child.paramName] = ""
				handler, ok := child.handlers[method]
				if !ok {
					return nil, nil
				}
				return handler, params
			}
		}
		return nil, nil
	}

	handler, ok := node.handlers[method]
	if !ok {
		return nil, nil
	}
	return handler, params
}

Fields

Name Type Description
root *radixNode
mu sync.RWMutex
F
function

newRadixTree

Returns

pkg/srv/radix.go:32-36
func newRadixTree() *radixTree

{
	return &radixTree{
		root: &radixNode{children: make([]*radixNode, 0)},
	}
}
F
function

findOrCreateStatic

Parameters

parent
segment
string

Returns

pkg/srv/radix.go:80-93
func findOrCreateStatic(parent *radixNode, segment string) *radixNode

{
	for _, child := range parent.children {
		if !child.isParam && !child.isCatchAll && child.segment == segment {
			return child
		}
	}
	node := &radixNode{
		segment:   segment,
		isParam:   false,
		children:  make([]*radixNode, 0),
	}
	parent.children = append(parent.children, node)
	return node
}
F
function

findOrCreateParam

Parameters

parent
paramName
string
constraints

Returns

pkg/srv/radix.go:95-112
func findOrCreateParam(parent *radixNode, paramName string, constraints []paramConstraint) *radixNode

{
	for _, child := range parent.children {
		if child.isParam && child.paramName == paramName {
			if len(constraints) > 0 && len(child.constraints) == 0 {
				child.constraints = constraints
			}
			return child
		}
	}
	node := &radixNode{
		isParam:    true,
		paramName:  paramName,
		constraints: constraints,
		children:   make([]*radixNode, 0),
	}
	parent.children = append(parent.children, node)
	return node
}
F
function

splitPath

Parameters

path
string

Returns

[]string
pkg/srv/radix.go:193-199
func splitPath(path string) []string

{
	trimmed := strings.Trim(path, "/")
	if trimmed == "" {
		return nil
	}
	return strings.Split(trimmed, "/")
}
F
function

isParam

Parameters

seg
string

Returns

bool
pkg/srv/radix.go:201-203
func isParam(seg string) bool

{
	return len(seg) > 2 && seg[0] == '{' && seg[len(seg)-1] == '}'
}
F
function

isCatchAll

Parameters

seg
string

Returns

bool
pkg/srv/radix.go:205-207
func isCatchAll(seg string) bool

{
	return len(seg) > 3 && seg[0] == '{' && seg[1] == '*' && seg[len(seg)-1] == '}'
}
F
function

parseCatchAll

Parameters

path
string

Returns

string
string
pkg/srv/radix.go:209-216
func parseCatchAll(path string) (string, string)

{
	parts := strings.Split(strings.Trim(path, "/"), "/")
	last := parts[len(parts)-1]
	if isCatchAll(last) {
		return last[2 : len(last)-1], ""
	}
	return "", ""
}
F
function

parseParamConstraints

Parameters

seg
string

Returns

string
pkg/srv/radix.go:218-252
func parseParamConstraints(seg string) (string, []paramConstraint)

{
	inner := seg[1 : len(seg)-1]
	colonIdx := strings.Index(inner, ":")
	if colonIdx < 0 {
		return inner, nil
	}

	name := inner[:colonIdx]
	constraintStr := inner[colonIdx+1:]
	var constraints []paramConstraint

	for _, c := range strings.Split(constraintStr, ",") {
		switch c {
		case "int":
			constraints = append(constraints, paramConstraint{
				name:     "int",
				validate: isIntConstraint,
			})
		case "alpha":
			constraints = append(constraints, paramConstraint{
				name:     "alpha",
				validate: isAlphaConstraint,
			})
		default:
			if strings.HasPrefix(c, "regex(") && strings.HasSuffix(c, ")") {
				pattern := c[6 : len(c)-1]
				constraints = append(constraints, paramConstraint{
					name:     c,
					validate: isRegexConstraint(pattern),
				})
			}
		}
	}
	return name, constraints
}
F
function

isIntConstraint

Parameters

s
string

Returns

bool
pkg/srv/radix.go:254-257
func isIntConstraint(s string) bool

{
	_, err := strconv.Atoi(s)
	return err == nil
}
F
function

isAlphaConstraint

Parameters

s
string

Returns

bool
pkg/srv/radix.go:259-266
func isAlphaConstraint(s string) bool

{
	for _, r := range s {
		if !((r >= 'a' && r <= 'z') || (r >= 'A' && r <= 'Z')) {
			return false
		}
	}
	return true
}
F
function

isRegexConstraint

Parameters

pattern
string

Returns

func(string)
bool
pkg/srv/radix.go:268-273
func isRegexConstraint(pattern string) func(string) bool

{
	re := regexp.MustCompile(pattern)
	return func(s string) bool {
		return re.MatchString(s)
	}
}
F
function

RateLimit

RateLimit returns middleware that limits requests per client using a token bucket.

Parameters

rate
int
burst
int

Returns

pkg/srv/ratelimit.go:10-20
func RateLimit(rate, burst int) Middleware

{
	rl := resiliency.NewRateLimiter(rate, burst)
	return func(next HandlerFunc) HandlerFunc {
		return func(ctx *Context) error {
			if err := rl.Wait(ctx.Ctx); err != nil {
				return ctx.JSON(http.StatusTooManyRequests, map[string]string{"error": "rate limit exceeded"})
			}
			return next(ctx)
		}
	}
}
S
struct

Context

Context holds the request state for a single HTTP request.

pkg/srv/srv.go:21-27
type Context struct

Methods

Set
Method

Set stores a key-value pair in the request context.

Parameters

key string
val any
func (*Context) Set(key string, val any)
{
	if c.values == nil {
		c.values = make(map[string]any)
	}
	c.values[key] = val
}
Get
Method

Get retrieves a value from the request context by key.

Parameters

key string

Returns

any
bool
func (*Context) Get(key string) (any, bool)
{
	v, ok := c.values[key]
	return v, ok
}
JSON
Method

JSON writes a JSON response with the given status code.

Parameters

code int
v any

Returns

error
func (*Context) JSON(code int, v any) error
{
	c.Response.Header().Set("Content-Type", "application/json; charset=utf-8")
	c.Response.WriteHeader(code)
	return json.NewEncoder(c.Response).Encode(v)
}
String
Method

String writes a plain text response with the given status code.

Parameters

code int
s string
func (*Context) String(code int, s string)
{
	c.Response.Header().Set("Content-Type", "text/plain; charset=utf-8")
	c.Response.WriteHeader(code)
	c.Response.Write([]byte(s))
}
Bind
Method

Bind decodes the request body into v based on the Content-Type header.

Parameters

v any

Returns

error
func (*Context) Bind(v any) error
{
	ct := c.Request.Header.Get("Content-Type")
	if strings.Contains(ct, "application/json") {
		return json.NewDecoder(c.Request.Body).Decode(v)
	}
	if strings.Contains(ct, "application/x-www-form-urlencoded") {
		if err := c.Request.ParseForm(); err != nil {
			return fmt.Errorf("srv: cannot parse form: %w", err)
		}
		return bindForm(c.Request.Form, v)
	}
	return fmt.Errorf("srv: unsupported content type: %s", ct)
}
Validate
Method

Validate runs validation rules on the target object and returns the collected validation errors. Returns nil if there are no errors.

Parameters

target any
func (*Context) Validate(target any) validation.Errors
{
	return validation.New().Validate(target)
}

BindAndValidate decodes the request body into target and then runs validation. Returns a validation.Errors slice if decoding or validation fails.

Parameters

target any
func (*Context) BindAndValidate(target any) validation.Errors
{
	if err := c.Bind(target); err != nil {
		return validation.Errors{{Field: "", Message: err.Error()}}
	}
	return c.Validate(target)
}

Fields

Name Type Description
Request *http.Request
Response http.ResponseWriter
Params map[string]string
Ctx context.Context
values map[string]any
F
function

bindForm

Parameters

form
map[string][]string
v
any

Returns

error
pkg/srv/srv.go:72-96
func bindForm(form map[string][]string, v any) error

{
	val := reflect.ValueOf(v)
	if val.Kind() != reflect.Ptr || val.Elem().Kind() != reflect.Struct {
		return fmt.Errorf("srv: form binding requires pointer to struct")
	}
	elem := val.Elem()
	for i := 0; i < elem.NumField(); i++ {
		field := elem.Field(i)
		if !field.CanSet() {
			continue
		}
		fieldName := elem.Type().Field(i).Name
		jsonTag := elem.Type().Field(i).Tag.Get("json")
		if jsonTag != "" {
			parts := strings.Split(jsonTag, ",")
			if parts[0] != "" && parts[0] != "-" {
				fieldName = parts[0]
			}
		}
		if vals, ok := form[fieldName]; ok && len(vals) > 0 {
			setFieldValue(field, vals[0])
		}
	}
	return nil
}
F
function

setFieldValue

Parameters

value
string
pkg/srv/srv.go:98-115
func setFieldValue(field reflect.Value, value string)

{
	switch field.Kind() {
	case reflect.String:
		field.SetString(value)
	case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
		if n, err := strconv.ParseInt(value, 10, 64); err == nil {
			field.SetInt(n)
		}
	case reflect.Float32, reflect.Float64:
		if n, err := strconv.ParseFloat(value, 64); err == nil {
			field.SetFloat(n)
		}
	case reflect.Bool:
		if b, err := strconv.ParseBool(value); err == nil {
			field.SetBool(b)
		}
	}
}
T
type

HandlerFunc

HandlerFunc is the handler signature for srv routes.

pkg/srv/srv.go:118-118
type HandlerFunc func(*Context) error
T
type

Middleware

Middleware wraps a HandlerFunc, returning a new HandlerFunc.

pkg/srv/srv.go:121-121
type Middleware func(HandlerFunc) HandlerFunc
S
struct

route

pkg/srv/srv.go:123-127
type route struct

Fields

Name Type Description
method string
path string
handler HandlerFunc
S
struct

Server

Server is a minimal API HTTP server with routing and middleware support.

pkg/srv/srv.go:130-137
type Server struct

Methods

Group
Method

Group creates a route group with a common prefix and optional middleware.

Parameters

prefix string
mw ...Middleware

Returns

func (*Server) Group(prefix string, mw ...Middleware) *group
{
	g := &group{prefix: prefix, middleware: mw, server: s}
	s.mu.Lock()
	s.groups = append(s.groups, g)
	s.mu.Unlock()
	return g
}
Use
Method

Use registers global middleware on the server.

Parameters

func (*Server) Use(mw Middleware)
{
	s.mu.Lock()
	s.middleware = append(s.middleware, mw)
	s.mu.Unlock()
}
MapGet
Method

MapGet registers a GET route at the given path.

Parameters

path string
handler HandlerFunc
mw ...Middleware
func (*Server) MapGet(path string, handler HandlerFunc, mw ...Middleware)
{
	s.addRoute("GET", path, handler, mw...)
}
MapPost
Method

MapPost registers a POST route at the given path.

Parameters

path string
handler HandlerFunc
mw ...Middleware
func (*Server) MapPost(path string, handler HandlerFunc, mw ...Middleware)
{
	s.addRoute("POST", path, handler, mw...)
}
MapPut
Method

MapPut registers a PUT route at the given path.

Parameters

path string
handler HandlerFunc
mw ...Middleware
func (*Server) MapPut(path string, handler HandlerFunc, mw ...Middleware)
{
	s.addRoute("PUT", path, handler, mw...)
}
MapDelete
Method

MapDelete registers a DELETE route at the given path.

Parameters

path string
handler HandlerFunc
mw ...Middleware
func (*Server) MapDelete(path string, handler HandlerFunc, mw ...Middleware)
{
	s.addRoute("DELETE", path, handler, mw...)
}
addRoute
Method

Parameters

method string
path string
handler HandlerFunc
mw ...Middleware
func (*Server) addRoute(method, path string, handler HandlerFunc, mw ...Middleware)
{
	chained := chainMiddleware(handler, mw...)
	s.mu.Lock()
	s.routes = append(s.routes, route{method: method, path: path, handler: chained})
	s.tree.insert(method, path, chained)
	s.mu.Unlock()
}
ServeHTTP
Method

ServeHTTP implements http.Handler, dispatching requests through global middleware and routes.

func (*Server) ServeHTTP(w http.ResponseWriter, r *http.Request)
{
	s.mu.RLock()
	globalMW := make([]Middleware, len(s.middleware))
	copy(globalMW, s.middleware)
	s.mu.RUnlock()

	ctx := &Context{Request: r, Response: w, Ctx: r.Context()}
	ctx.values = make(map[string]any)

	handler := func(c *Context) error {
		s.serveRoutesWithContext(c)
		return nil
	}

	wrapped := chainMiddleware(handler, globalMW...)
	if err := wrapped(ctx); err != nil {
		ctx.Response.Header().Set("Content-Type", "application/json; charset=utf-8")
		ctx.Response.WriteHeader(statusFromError(err))
		json.NewEncoder(ctx.Response).Encode(map[string]string{"error": err.Error()})
	}
}

Parameters

ctx *Context
func (*Server) serveRoutesWithContext(ctx *Context)
{
	handler, params := s.tree.lookup(ctx.Request.Method, ctx.Request.URL.Path)
	if handler == nil {
		ctx.Response.Header().Set("Content-Type", "application/json; charset=utf-8")
		ctx.Response.WriteHeader(http.StatusNotFound)
		json.NewEncoder(ctx.Response).Encode(map[string]string{"error": "not found"})
		return
	}

	ctx.Params = params

	if err := handler(ctx); err != nil {
		ctx.Response.Header().Set("Content-Type", "application/json; charset=utf-8")
		ctx.Response.WriteHeader(statusFromError(err))
		json.NewEncoder(ctx.Response).Encode(map[string]string{"error": err.Error()})
	}
}

ListenAndServe starts the server on the given address.

Parameters

addr string

Returns

error
func (*Server) ListenAndServe(addr string) error
{
	s.mu.Lock()
	s.server = &http.Server{Addr: addr, Handler: s}
	s.mu.Unlock()
	return s.server.ListenAndServe()
}

ListenAndServeTLS starts the server with TLS on the given address.

Parameters

addr string
certFile string
keyFile string

Returns

error
func (*Server) ListenAndServeTLS(addr, certFile, keyFile string) error
{
	s.mu.Lock()
	s.server = &http.Server{Addr: addr, Handler: s}
	s.mu.Unlock()
	return s.server.ListenAndServeTLS(certFile, keyFile)
}
Shutdown
Method

Shutdown gracefully shuts down the server.

Parameters

Returns

error
func (*Server) Shutdown(ctx context.Context) error
{
	s.mu.RLock()
	srv := s.server
	s.mu.RUnlock()
	if srv == nil {
		return fmt.Errorf("srv: server not started")
	}
	return srv.Shutdown(ctx)
}

RegisterHandler registers a struct that implements Handler as an endpoint. The struct must have method and path tags: type MyEndpoint struct { Pattern `method:"GET" path:"/api/v1/ping"` Times int `query:"times" default:"1"` } The container is used for dependency injection and bind for field population.

Parameters

prototype Handler
container *di.Container
func (*Server) RegisterHandler(prototype Handler, container *di.Container)
{
	meta := &handlerMeta{
		prototype: reflect.TypeOf(prototype).Elem(),
		container: container,
	}

	typ := meta.prototype
	var method, path string
	for i := 0; i < typ.NumField(); i++ {
		sf := typ.Field(i)
		if m := sf.Tag.Get("method"); m != "" {
			method = m
		}
		if p := sf.Tag.Get("path"); p != "" {
			path = p
		}
	}

	if method == "" || path == "" {
		panic("RegisterHandler: struct must have method and path struct tags")
	}

	meta.method = method
	meta.path = path

	handler := s.buildHandler(meta)
	s.tree.insert(method, path, handler)
}
buildHandler
Method

Parameters

meta *handlerMeta

Returns

func (*Server) buildHandler(meta *handlerMeta) HandlerFunc
{
	return func(ctx *Context) error {
		newVal := reflect.New(meta.prototype)

		if meta.container != nil {
			meta.container.Inject(newVal.Interface())
		}

		b := bind.New()
		if len(ctx.Params) > 0 {
			b.FromPath(func(key string) string {
				return ctx.Params[key]
			})
		}
		b.FromQuery(ctx.Request)
		b.FromHeader(ctx.Request)
		b.Bind(newVal.Interface())

		if ctx.Request.Body != nil {
			if ct := ctx.Request.Header.Get("Content-Type"); ct == "application/json" {
				body, err := io.ReadAll(ctx.Request.Body)
				if err == nil && len(body) > 0 {
					_ = bind.New().BindJSON(newVal.Interface(), body)
				}
			}
		}

		result, err := newVal.Interface().(Handler).Handle(ctx.Request.Context())
		if err != nil {
			return err
		}
		if result == nil {
			ctx.Response.WriteHeader(http.StatusNoContent)
			return nil
		}
		return ctx.JSON(http.StatusOK, result)
	}
}

Fields

Name Type Description
middleware []Middleware
routes []route
groups []*group
mu sync.RWMutex
server *http.Server
tree *radixTree
T
type

Option

Option configures a Server.

pkg/srv/srv.go:140-140
type Option options.Option[Server]
F
function

New

New creates a new Server with optional configuration.

Parameters

opts
...Option

Returns

pkg/srv/srv.go:143-151
func New(opts ...Option) *Server

{
	s := &Server{
		tree: newRadixTree(),
	}
	for _, opt := range opts {
		opt(s)
	}
	return s
}
S
struct

group

pkg/srv/srv.go:153-157
type group struct

Methods

MapGet
Method

MapGet registers a GET route in the group.

Parameters

path string
handler HandlerFunc
func (*group) MapGet(path string, handler HandlerFunc)
{
	g.server.MapGet(g.prefix+path, handler, g.middleware...)
}
MapPost
Method

MapPost registers a POST route in the group.

Parameters

path string
handler HandlerFunc
func (*group) MapPost(path string, handler HandlerFunc)
{
	g.server.MapPost(g.prefix+path, handler, g.middleware...)
}
MapPut
Method

MapPut registers a PUT route in the group.

Parameters

path string
handler HandlerFunc
func (*group) MapPut(path string, handler HandlerFunc)
{
	g.server.MapPut(g.prefix+path, handler, g.middleware...)
}
MapDelete
Method

MapDelete registers a DELETE route in the group.

Parameters

path string
handler HandlerFunc
func (*group) MapDelete(path string, handler HandlerFunc)
{
	g.server.MapDelete(g.prefix+path, handler, g.middleware...)
}

Fields

Name Type Description
prefix string
middleware []Middleware
server *Server
F
function

chainMiddleware

Parameters

mw
...Middleware

Returns

pkg/srv/srv.go:223-228
func chainMiddleware(h HandlerFunc, mw ...Middleware) HandlerFunc

{
	for i := len(mw) - 1; i >= 0; i-- {
		h = mw[i](h)
	}
	return h
}
F
function

matchRoute

Parameters

pattern
string
path
string

Returns

map[string]string
bool
pkg/srv/srv.go:271-289
func matchRoute(pattern, path string) (map[string]string, bool)

{
	patternParts := strings.Split(strings.Trim(pattern, "/"), "/")
	pathParts := strings.Split(strings.Trim(path, "/"), "/")

	if len(patternParts) != len(pathParts) {
		return nil, false
	}

	params := make(map[string]string)
	for i := 0; i < len(patternParts); i++ {
		if strings.HasPrefix(patternParts[i], "{") && strings.HasSuffix(patternParts[i], "}") {
			paramName := patternParts[i][1 : len(patternParts[i])-1]
			params[paramName] = pathParts[i]
		} else if patternParts[i] != pathParts[i] {
			return nil, false
		}
	}
	return params, true
}
F
function

JSON

JSON writes a JSON response using the context.

Parameters

ctx
code
int
v
any

Returns

error
pkg/srv/srv.go:319-321
func JSON(ctx *Context, code int, v any) error

{
	return ctx.JSON(code, v)
}
F
function

Error

Error creates an HTTP error with a status code and message.

Parameters

code
int
msg
string

Returns

error
pkg/srv/srv.go:324-326
func Error(code int, msg string) error

{
	return &httpError{Code: code, Message: msg}
}
S
struct

httpError

pkg/srv/srv.go:328-331
type httpError struct

Methods

Error
Method

Returns

string
func (*httpError) Error() string
{
	return e.Message
}
HTTPCode
Method

HTTPCode returns the HTTP status code from the error.

Returns

int
func (*httpError) HTTPCode() int
{
	return e.Code
}

Fields

Name Type Description
Code int
Message string
S
struct

panicError

pkg/srv/srv.go:342-345
type panicError struct

Methods

Error
Method

Returns

string
func (*panicError) Error() string
{
	return fmt.Sprintf("panic: %v\n%s", e.recovered, e.stack)
}
HTTPCode
Method

Returns

int
func (*panicError) HTTPCode() int
{
	return http.StatusInternalServerError
}
Unwrap
Method

Returns

error
func (*panicError) Unwrap() error
{
	if err, ok := e.recovered.(error); ok {
		return err
	}
	return nil
}

Fields

Name Type Description
recovered any
stack string
F
function

Recovery

Recovery returns middleware that catches panics and converts them to errors with stack traces.

Returns

pkg/srv/srv.go:363-377
func Recovery() Middleware

{
	return func(next HandlerFunc) HandlerFunc {
		return func(ctx *Context) (err error) {
			defer func() {
				if r := recover(); r != nil {
					err = &panicError{
						recovered: r,
						stack:     string(debug.Stack()),
					}
				}
			}()
			return next(ctx)
		}
	}
}
F
function

Logger

Logger returns middleware that logs request method, path, duration, and status.

Returns

pkg/srv/srv.go:380-389
func Logger() Middleware

{
	return func(next HandlerFunc) HandlerFunc {
		return func(ctx *Context) error {
			start := time.Now()
			err := next(ctx)
			fmt.Printf("%s %s %v %d\n", ctx.Request.Method, ctx.Request.URL.Path, time.Since(start), statusFromError(err))
			return err
		}
	}
}
F
function

CORS

CORS returns middleware that sets CORS headers. Preflight OPTIONS requests get 204.

Parameters

allowOrigins
...string

Returns

pkg/srv/srv.go:392-409
func CORS(allowOrigins ...string) Middleware

{
	origins := "*"
	if len(allowOrigins) > 0 {
		origins = strings.Join(allowOrigins, ", ")
	}
	return func(next HandlerFunc) HandlerFunc {
		return func(ctx *Context) error {
			ctx.Response.Header().Set("Access-Control-Allow-Origin", origins)
			ctx.Response.Header().Set("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS")
			ctx.Response.Header().Set("Access-Control-Allow-Headers", "Content-Type, Authorization")
			if ctx.Request.Method == "OPTIONS" {
				ctx.Response.WriteHeader(http.StatusNoContent)
				return nil
			}
			return next(ctx)
		}
	}
}
F
function

RequestID

RequestID returns middleware that sets and propagates X-Request-ID.

Returns

pkg/srv/srv.go:412-424
func RequestID() Middleware

{
	return func(next HandlerFunc) HandlerFunc {
		return func(ctx *Context) error {
			id := ctx.Request.Header.Get("X-Request-ID")
			if id == "" {
				id = fmt.Sprintf("%d", time.Now().UnixNano())
			}
			ctx.Set("request_id", id)
			ctx.Response.Header().Set("X-Request-ID", id)
			return next(ctx)
		}
	}
}
F
function

Compress

Compress returns middleware that sets gzip content encoding.

Returns

pkg/srv/srv.go:427-434
func Compress() Middleware

{
	return func(next HandlerFunc) HandlerFunc {
		return func(ctx *Context) error {
			ctx.Response.Header().Set("Content-Encoding", "gzip")
			return next(ctx)
		}
	}
}
F
function

HealthEndpoint

HealthEndpoint returns a handler that reports health status based on the checker function.

Parameters

checker
func(ctx context.Context) error

Returns

pkg/srv/srv.go:437-444
func HealthEndpoint(checker func(ctx context.Context) error) HandlerFunc

{
	return func(ctx *Context) error {
		if err := checker(ctx.Ctx); err != nil {
			return ctx.JSON(http.StatusServiceUnavailable, map[string]string{"status": "unhealthy", "error": err.Error()})
		}
		return ctx.JSON(http.StatusOK, map[string]string{"status": "healthy"})
	}
}
F
function

statusFromError

Parameters

err
error

Returns

int
pkg/srv/srv.go:446-461
func statusFromError(err error) int

{
	if err == nil {
		return http.StatusOK
	}
	for {
		if he, ok := err.(interface{ HTTPCode() int }); ok {
			return he.HTTPCode()
		}
		if e := errors.Unwrap(err); e != nil {
			err = e
			continue
		}
		break
	}
	return http.StatusInternalServerError
}
I
interface

Handler

Handler is the interface for declarative struct-tagged endpoints.

pkg/srv/handler.go:14-16
type Handler interface

Methods

Handle
Method

Parameters

Returns

any
error
func Handle(...)
S
struct

handlerMeta

pkg/srv/handler.go:56-61
type handlerMeta struct

Fields

Name Type Description
prototype reflect.Type
method string
path string
container *di.Container
S
struct

PingEndpoint

pkg/srv/handler_test.go:12-15
type PingEndpoint struct

Methods

Handle
Method

Parameters

Returns

any
error
func (*PingEndpoint) Handle(_ context.Context) (any, error)
{
	out := make([]string, e.Times)
	for i := range out {
		out[i] = "pong"
	}
	return pingResponse{Message: out}, nil
}

Fields

Name Type Description
Meta struct{} method:"GET" path:"/ping"
Times int query:"times" default:"1"
S
struct

pingResponse

pkg/srv/handler_test.go:17-19
type pingResponse struct

Fields

Name Type Description
Message []string json:"message"
F
function

TestRegisterHandler_Basic

Parameters

pkg/srv/handler_test.go:29-49
func TestRegisterHandler_Basic(t *testing.T)

{
	s := New()
	b := di.NewBuilder()
	container, _ := b.Build()

	s.RegisterHandler(&PingEndpoint{}, container)

	req := httptest.NewRequest("GET", "/ping", nil)
	w := httptest.NewRecorder()
	s.ServeHTTP(w, req)

	if w.Code != 200 {
		t.Errorf("status = %d, want 200, body: %s", w.Code, w.Body.String())
	}

	var resp pingResponse
	json.NewDecoder(w.Body).Decode(&resp)
	if len(resp.Message) != 1 {
		t.Errorf("messages = %d, want 1", len(resp.Message))
	}
}
F
function

TestRegisterHandler_WithQuery

Parameters

pkg/srv/handler_test.go:51-71
func TestRegisterHandler_WithQuery(t *testing.T)

{
	s := New()
	b := di.NewBuilder()
	container, _ := b.Build()

	s.RegisterHandler(&PingEndpoint{}, container)

	req := httptest.NewRequest("GET", "/ping?times=3", nil)
	w := httptest.NewRecorder()
	s.ServeHTTP(w, req)

	if w.Code != 200 {
		t.Errorf("status = %d, want 200", w.Code)
	}

	var resp pingResponse
	json.NewDecoder(w.Body).Decode(&resp)
	if len(resp.Message) != 3 {
		t.Errorf("messages = %d, want 3", len(resp.Message))
	}
}
S
struct

DiEndpoint

pkg/srv/handler_test.go:73-77
type DiEndpoint struct

Methods

Handle
Method

Parameters

Returns

any
error
func (*DiEndpoint) Handle(_ context.Context) (any, error)
{
	return map[string]string{"greeting": "hello " + e.Name}, nil
}

Fields

Name Type Description
Meta struct{} method:"GET" path:"/greet"
Name string query:"name" default:"world"
Greet string
F
function

TestRegisterHandler_WithDI

Parameters

pkg/srv/handler_test.go:83-98
func TestRegisterHandler_WithDI(t *testing.T)

{
	s := New()
	b := di.NewBuilder()
	b.Provide("Greet", "custom-greeting")
	container, _ := b.Build()

	s.RegisterHandler(&DiEndpoint{}, container)

	req := httptest.NewRequest("GET", "/greet?name=alice", nil)
	w := httptest.NewRecorder()
	s.ServeHTTP(w, req)

	if w.Code != 200 {
		t.Errorf("status = %d, want 200", w.Code)
	}
}
S
struct

NoReturnEndpoint

pkg/srv/handler_test.go:100-102
type NoReturnEndpoint struct

Methods

Handle
Method

Parameters

Returns

any
error
func (*NoReturnEndpoint) Handle(_ context.Context) (any, error)
{
	return nil, nil
}

Fields

Name Type Description
Meta struct{} method:"DELETE" path:"/item"
F
function

TestRegisterHandler_NilReturn

Parameters

pkg/srv/handler_test.go:108-122
func TestRegisterHandler_NilReturn(t *testing.T)

{
	s := New()
	b := di.NewBuilder()
	container, _ := b.Build()

	s.RegisterHandler(&NoReturnEndpoint{}, container)

	req := httptest.NewRequest("DELETE", "/item", nil)
	w := httptest.NewRecorder()
	s.ServeHTTP(w, req)

	if w.Code != 204 {
		t.Errorf("status = %d, want 204", w.Code)
	}
}
F
function

TestServer_MapGet

Parameters

pkg/srv/srv_test.go:11-30
func TestServer_MapGet(t *testing.T)

{
	s := New()
	s.MapGet("/hello", func(ctx *Context) error {
		return ctx.JSON(200, map[string]string{"message": "hello"})
	})

	req := httptest.NewRequest("GET", "/hello", nil)
	w := httptest.NewRecorder()
	s.ServeHTTP(w, req)

	if w.Code != 200 {
		t.Errorf("status: got %d, want 200", w.Code)
	}

	var resp map[string]string
	json.NewDecoder(w.Body).Decode(&resp)
	if resp["message"] != "hello" {
		t.Errorf("body: got %q, want %q", resp["message"], "hello")
	}
}
F
function

TestServer_Params

Parameters

pkg/srv/srv_test.go:32-47
func TestServer_Params(t *testing.T)

{
	s := New()
	s.MapGet("/users/{id}", func(ctx *Context) error {
		return ctx.JSON(200, map[string]string{"id": ctx.Params["id"]})
	})

	req := httptest.NewRequest("GET", "/users/42", nil)
	w := httptest.NewRecorder()
	s.ServeHTTP(w, req)

	var resp map[string]string
	json.NewDecoder(w.Body).Decode(&resp)
	if resp["id"] != "42" {
		t.Errorf("param: got %q, want %q", resp["id"], "42")
	}
}
F
function

TestServer_NotFound

Parameters

pkg/srv/srv_test.go:49-58
func TestServer_NotFound(t *testing.T)

{
	s := New()
	req := httptest.NewRequest("GET", "/missing", nil)
	w := httptest.NewRecorder()
	s.ServeHTTP(w, req)

	if w.Code != 404 {
		t.Errorf("status: got %d, want 404", w.Code)
	}
}
F
function

TestServer_Middleware

Parameters

pkg/srv/srv_test.go:60-79
func TestServer_Middleware(t *testing.T)

{
	s := New()
	s.Use(RequestID())

	s.MapGet("/test", func(ctx *Context) error {
		id, _ := ctx.Get("request_id")
		return ctx.JSON(200, map[string]any{"request_id": id})
	})

	req := httptest.NewRequest("GET", "/test", nil)
	req.Header.Set("X-Request-ID", "test-123")
	w := httptest.NewRecorder()
	s.ServeHTTP(w, req)

	var resp map[string]any
	json.NewDecoder(w.Body).Decode(&resp)
	if resp["request_id"] != "test-123" {
		t.Errorf("middleware: got %v, want test-123", resp["request_id"])
	}
}
F
function

TestServer_Group

Parameters

pkg/srv/srv_test.go:81-95
func TestServer_Group(t *testing.T)

{
	s := New()
	api := s.Group("/api")
	api.MapGet("/users", func(ctx *Context) error {
		return ctx.JSON(200, map[string]string{"path": "/api/users"})
	})

	req := httptest.NewRequest("GET", "/api/users", nil)
	w := httptest.NewRecorder()
	s.ServeHTTP(w, req)

	if w.Code != 200 {
		t.Errorf("group route: got %d, want 200", w.Code)
	}
}
F
function

TestServer_CORS

Parameters

pkg/srv/srv_test.go:97-115
func TestServer_CORS(t *testing.T)

{
	s := New()
	s.Use(CORS("*"))
	s.MapGet("/test", func(ctx *Context) error {
		ctx.String(200, "ok")
		return nil
	})

	req := httptest.NewRequest("OPTIONS", "/test", nil)
	w := httptest.NewRecorder()
	s.ServeHTTP(w, req)

	if w.Code != 204 {
		t.Errorf("CORS preflight: got %d, want 204", w.Code)
	}
	if w.Header().Get("Access-Control-Allow-Origin") != "*" {
		t.Errorf("CORS header: got %q", w.Header().Get("Access-Control-Allow-Origin"))
	}
}
F
function

TestServer_Bind

Parameters

pkg/srv/srv_test.go:117-135
func TestServer_Bind(t *testing.T)

{
	s := New()
	s.MapPost("/data", func(ctx *Context) error {
		var body map[string]string
		if err := ctx.Bind(&body); err != nil {
			return err
		}
		return ctx.JSON(200, body)
	})

	req := httptest.NewRequest("POST", "/data", strings.NewReader(`{"key":"value"}`))
	req.Header.Set("Content-Type", "application/json")
	w := httptest.NewRecorder()
	s.ServeHTTP(w, req)

	if w.Code != 200 {
		t.Errorf("bind: got %d, want 200", w.Code)
	}
}
F
function

TestHealthEndpoint

Parameters

pkg/srv/srv_test.go:137-150
func TestHealthEndpoint(t *testing.T)

{
	s := New()
	s.MapGet("/health", HealthEndpoint(func(ctx context.Context) error {
		return nil
	}))

	req := httptest.NewRequest("GET", "/health", nil)
	w := httptest.NewRecorder()
	s.ServeHTTP(w, req)

	if w.Code != 200 {
		t.Errorf("health: got %d, want 200", w.Code)
	}
}
F
function

TestServer_IntConstraint

Parameters

pkg/srv/srv_test.go:154-173
func TestServer_IntConstraint(t *testing.T)

{
	s := New()
	s.MapGet("/users/{id:int}", func(ctx *Context) error {
		return ctx.JSON(200, map[string]string{"id": ctx.Params["id"]})
	})

	req := httptest.NewRequest("GET", "/users/42", nil)
	w := httptest.NewRecorder()
	s.ServeHTTP(w, req)
	if w.Code != 200 {
		t.Errorf("int constraint valid: got %d, want 200", w.Code)
	}

	req = httptest.NewRequest("GET", "/users/abc", nil)
	w = httptest.NewRecorder()
	s.ServeHTTP(w, req)
	if w.Code != 404 {
		t.Errorf("int constraint invalid: got %d, want 404", w.Code)
	}
}
F
function

TestServer_AlphaConstraint

Parameters

pkg/srv/srv_test.go:175-194
func TestServer_AlphaConstraint(t *testing.T)

{
	s := New()
	s.MapGet("/items/{slug:alpha}", func(ctx *Context) error {
		return ctx.JSON(200, map[string]string{"slug": ctx.Params["slug"]})
	})

	req := httptest.NewRequest("GET", "/items/hello", nil)
	w := httptest.NewRecorder()
	s.ServeHTTP(w, req)
	if w.Code != 200 {
		t.Errorf("alpha constraint valid: got %d, want 200", w.Code)
	}

	req = httptest.NewRequest("GET", "/items/hello123", nil)
	w = httptest.NewRecorder()
	s.ServeHTTP(w, req)
	if w.Code != 404 {
		t.Errorf("alpha constraint invalid: got %d, want 404", w.Code)
	}
}
F
function

TestServer_CatchAll

Parameters

pkg/srv/srv_test.go:196-224
func TestServer_CatchAll(t *testing.T)

{
	s := New()
	s.MapGet("/static/{*filepath}", func(ctx *Context) error {
		return ctx.JSON(200, map[string]string{"path": ctx.Params["filepath"]})
	})

	req := httptest.NewRequest("GET", "/static/css/main.css", nil)
	w := httptest.NewRecorder()
	s.ServeHTTP(w, req)
	if w.Code != 200 {
		t.Fatalf("catch-all: got %d, want 200", w.Code)
	}
	var resp map[string]string
	json.NewDecoder(w.Body).Decode(&resp)
	if resp["path"] != "css/main.css" {
		t.Errorf("catch-all path: got %q, want %q", resp["path"], "css/main.css")
	}

	req = httptest.NewRequest("GET", "/static/a/b/c/d", nil)
	w = httptest.NewRecorder()
	s.ServeHTTP(w, req)
	if w.Code != 200 {
		t.Fatalf("deep catch-all: got %d, want 200", w.Code)
	}
	json.NewDecoder(w.Body).Decode(&resp)
	if resp["path"] != "a/b/c/d" {
		t.Errorf("deep catch-all: got %q, want %q", resp["path"], "a/b/c/d")
	}
}
F
function

TestServer_MethodMultiplex

Parameters

pkg/srv/srv_test.go:226-265
func TestServer_MethodMultiplex(t *testing.T)

{
	s := New()
	s.MapGet("/items/{id}", func(ctx *Context) error {
		return ctx.JSON(200, map[string]string{"method": "GET"})
	})
	s.MapPost("/items/{id}", func(ctx *Context) error {
		return ctx.JSON(201, map[string]string{"method": "POST"})
	})
	s.MapDelete("/items/{id}", func(ctx *Context) error {
		return ctx.JSON(200, map[string]string{"method": "DELETE"})
	})

	req := httptest.NewRequest("GET", "/items/1", nil)
	w := httptest.NewRecorder()
	s.ServeHTTP(w, req)
	if w.Code != 200 {
		t.Errorf("GET: got %d, want 200", w.Code)
	}

	req = httptest.NewRequest("POST", "/items/1", nil)
	w = httptest.NewRecorder()
	s.ServeHTTP(w, req)
	if w.Code != 201 {
		t.Errorf("POST: got %d, want 201", w.Code)
	}

	req = httptest.NewRequest("DELETE", "/items/1", nil)
	w = httptest.NewRecorder()
	s.ServeHTTP(w, req)
	if w.Code != 200 {
		t.Errorf("DELETE: got %d, want 200", w.Code)
	}

	req = httptest.NewRequest("PUT", "/items/1", nil)
	w = httptest.NewRecorder()
	s.ServeHTTP(w, req)
	if w.Code != 404 {
		t.Errorf("PUT not registered: got %d, want 404", w.Code)
	}
}
F
function

TestServer_RegexConstraint

Parameters

pkg/srv/srv_test.go:267-286
func TestServer_RegexConstraint(t *testing.T)

{
	s := New()
	s.MapGet("/files/{name:regex(^[a-z]+\\.txt$)}", func(ctx *Context) error {
		return ctx.JSON(200, map[string]string{"name": ctx.Params["name"]})
	})

	req := httptest.NewRequest("GET", "/files/readme.txt", nil)
	w := httptest.NewRecorder()
	s.ServeHTTP(w, req)
	if w.Code != 200 {
		t.Errorf("regex valid: got %d, want 200, body: %s", w.Code, w.Body.String())
	}

	req = httptest.NewRequest("GET", "/files/README.TXT", nil)
	w = httptest.NewRecorder()
	s.ServeHTTP(w, req)
	if w.Code != 404 {
		t.Errorf("regex invalid: got %d, want 404", w.Code)
	}
}