go-tower: first release v0.1.0
This commit is contained in:
29
.devcontainer/devcontainer.json
Normal file
29
.devcontainer/devcontainer.json
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
// For format details, see https://aka.ms/devcontainer.json. For config options, see the
|
||||||
|
// README at: https://github.com/devcontainers/templates/tree/main/src/go .
|
||||||
|
{
|
||||||
|
"name": "Go",
|
||||||
|
// Or use a Dockerfile or Docker Compose file. More info: https://containers.dev/guide/dockerfile
|
||||||
|
"image": "mcr.microsoft.com/devcontainers/go:2-1.25-trixie",
|
||||||
|
"customizations": {
|
||||||
|
"vscode": {
|
||||||
|
"extensions": [
|
||||||
|
"golang.go"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Features to add to the dev container. More info: https://containers.dev/features.
|
||||||
|
// "features": {},
|
||||||
|
|
||||||
|
// Use 'forwardPorts' to make a list of ports inside the container available locally.
|
||||||
|
// "forwardPorts": [],
|
||||||
|
|
||||||
|
// Use 'postCreateCommand' to run commands after the container is created.
|
||||||
|
// "postCreateCommand": "go version",
|
||||||
|
|
||||||
|
// Configure tool-specific properties.
|
||||||
|
// "customizations": {},
|
||||||
|
|
||||||
|
// Uncomment to connect as root instead. More info: https://aka.ms/dev-containers-non-root.
|
||||||
|
// "remoteUser": "root"
|
||||||
|
}
|
||||||
12
.github/dependabot.yml
vendored
Normal file
12
.github/dependabot.yml
vendored
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
# To get started with Dependabot version updates, you'll need to specify which
|
||||||
|
# package ecosystems to update and where the package manifests are located.
|
||||||
|
# Please see the documentation for more information:
|
||||||
|
# https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates
|
||||||
|
# https://containers.dev/guide/dependabot
|
||||||
|
|
||||||
|
version: 2
|
||||||
|
updates:
|
||||||
|
- package-ecosystem: "devcontainers"
|
||||||
|
directory: "/"
|
||||||
|
schedule:
|
||||||
|
interval: weekly
|
||||||
328
README.md
328
README.md
@@ -1,2 +1,328 @@
|
|||||||
# go-tower
|
# Tower
|
||||||
|
|
||||||
|
Tower is a minimal, composable HTTP web framework for Go built on top of `net/http` and `httprouter`.
|
||||||
|
It provides a small, explicit API for routing, middleware composition, grouping, and structured request/response handling via a custom context.
|
||||||
|
|
||||||
|
The framework favors:
|
||||||
|
- Small surface area
|
||||||
|
- Explicit middleware chaining
|
||||||
|
- Typed handler signatures
|
||||||
|
- Zero magic abstractions
|
||||||
|
- Compatibility with the standard library
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
```bash
|
||||||
|
go get git.trcreatives.at/TR_Creatives/go-tower
|
||||||
|
````
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Quick Start
|
||||||
|
|
||||||
|
```go
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"log"
|
||||||
|
|
||||||
|
"git.trcreatives.at/TR_Creatives/go-tower"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
app := tower.New()
|
||||||
|
|
||||||
|
app.GET("/hello/:name", func(c *tower.Context) error {
|
||||||
|
name := c.Param("name")
|
||||||
|
return c.WriteJSON(200, tower.Map{
|
||||||
|
"message": "Hello " + name,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
log.Fatal(app.Start(":8080"))
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Core Concepts
|
||||||
|
|
||||||
|
### Handler
|
||||||
|
|
||||||
|
All route handlers use a unified signature:
|
||||||
|
|
||||||
|
```go
|
||||||
|
type Handler func(c *Context) error
|
||||||
|
```
|
||||||
|
|
||||||
|
Returning an error forwards it to the configured error handler.
|
||||||
|
|
||||||
|
```go
|
||||||
|
app.GET("/ping", func(c *tower.Context) error {
|
||||||
|
return c.WriteString(200, "pong")
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Middleware
|
||||||
|
|
||||||
|
Middleware wraps handlers and composes into a chain:
|
||||||
|
|
||||||
|
```go
|
||||||
|
type Middleware func(h Handler) Handler
|
||||||
|
```
|
||||||
|
|
||||||
|
Example:
|
||||||
|
|
||||||
|
```go
|
||||||
|
func Logger() tower.Middleware {
|
||||||
|
return func(next tower.Handler) tower.Handler {
|
||||||
|
return func(c *tower.Context) error {
|
||||||
|
err := next(c)
|
||||||
|
log.Printf("%s %s -> %d",
|
||||||
|
c.Request().Method,
|
||||||
|
c.Request().URL.Path,
|
||||||
|
c.Status(),
|
||||||
|
)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Register globally:
|
||||||
|
|
||||||
|
```go
|
||||||
|
app.Use(Logger())
|
||||||
|
```
|
||||||
|
|
||||||
|
Or per route:
|
||||||
|
|
||||||
|
```go
|
||||||
|
app.GET("/secure", handler, AuthMiddleware())
|
||||||
|
```
|
||||||
|
|
||||||
|
Middleware execution order (outer → inner):
|
||||||
|
|
||||||
|
1. Global middleware
|
||||||
|
2. Group middleware
|
||||||
|
3. Route middleware
|
||||||
|
4. Handler
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Routing
|
||||||
|
|
||||||
|
Tower uses `httprouter`, so parameters are declared with `:name`.
|
||||||
|
|
||||||
|
```go
|
||||||
|
app.GET("/users/:id", func(c *tower.Context) error {
|
||||||
|
id := c.Param("id")
|
||||||
|
return c.WriteString(200, "User: "+id)
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
Available helpers:
|
||||||
|
|
||||||
|
```go
|
||||||
|
app.GET(path, handler, mws...)
|
||||||
|
app.POST(path, handler, mws...)
|
||||||
|
app.PUT(path, handler, mws...)
|
||||||
|
app.PATCH(path, handler, mws...)
|
||||||
|
app.DELETE(path, handler, mws...)
|
||||||
|
```
|
||||||
|
|
||||||
|
Or manually:
|
||||||
|
|
||||||
|
```go
|
||||||
|
app.Handle("GET", "/custom", handler)
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Groups
|
||||||
|
|
||||||
|
Groups allow route prefixing and shared middleware.
|
||||||
|
|
||||||
|
```go
|
||||||
|
api := app.Group("/api")
|
||||||
|
|
||||||
|
api.GET("/health", func(c *tower.Context) error {
|
||||||
|
return c.WriteString(200, "ok")
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
Nested groups:
|
||||||
|
|
||||||
|
```go
|
||||||
|
v1 := api.Group("/v1")
|
||||||
|
v1.GET("/users", listUsers)
|
||||||
|
```
|
||||||
|
|
||||||
|
Group-level middleware:
|
||||||
|
|
||||||
|
```go
|
||||||
|
auth := app.Group("/auth", AuthMiddleware())
|
||||||
|
auth.GET("/profile", profileHandler)
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Static Files
|
||||||
|
|
||||||
|
Serve a directory under a URL prefix:
|
||||||
|
|
||||||
|
```go
|
||||||
|
app.Static("/assets", "./public")
|
||||||
|
```
|
||||||
|
|
||||||
|
Group variant:
|
||||||
|
|
||||||
|
```go
|
||||||
|
api.Static("/docs", "./docs")
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Context API
|
||||||
|
|
||||||
|
`Context` wraps `http.Request` and `http.ResponseWriter` and provides helpers.
|
||||||
|
|
||||||
|
### Request Access
|
||||||
|
|
||||||
|
```go
|
||||||
|
c.Request() // *http.Request
|
||||||
|
c.Param("id") // path parameter
|
||||||
|
c.Query("q") // query string value
|
||||||
|
c.Header("X-Token") // request header
|
||||||
|
c.Cookie("session") // cookie
|
||||||
|
c.ClientIP() // best-effort client IP
|
||||||
|
```
|
||||||
|
|
||||||
|
### Response Helpers
|
||||||
|
|
||||||
|
```go
|
||||||
|
c.SetStatus(201)
|
||||||
|
c.SetHeader("X-App", "tower")
|
||||||
|
|
||||||
|
c.WriteString(200, "ok")
|
||||||
|
c.WriteJSON(200, data)
|
||||||
|
c.WriteXML(200, data)
|
||||||
|
c.WriteBytes(200, []byte("raw"), "application/octet-stream")
|
||||||
|
c.Redirect(302, "/login")
|
||||||
|
```
|
||||||
|
|
||||||
|
### Binding Request Bodies
|
||||||
|
|
||||||
|
```go
|
||||||
|
var payload CreateUserRequest
|
||||||
|
if err := c.BindJSON(&payload); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Also supported:
|
||||||
|
|
||||||
|
```go
|
||||||
|
c.BindXML(&payload)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Context Values
|
||||||
|
|
||||||
|
```go
|
||||||
|
type userKey tower.ContextKey // define your own alias if desired
|
||||||
|
|
||||||
|
c.SetValue("userID", "123")
|
||||||
|
id := c.Value("userID")
|
||||||
|
```
|
||||||
|
|
||||||
|
(Values are stored in the underlying `request.Context()`.)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Error Handling
|
||||||
|
|
||||||
|
Handlers return errors which are passed to the global error handler.
|
||||||
|
|
||||||
|
Set a custom handler:
|
||||||
|
|
||||||
|
```go
|
||||||
|
app.SetErrorHandler(func(c *tower.Context, err error) error {
|
||||||
|
return c.WriteJSON(500, tower.Map{
|
||||||
|
"error": err.Error(),
|
||||||
|
})
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
If the error handler itself returns an error, it is logged.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Server Lifecycle
|
||||||
|
|
||||||
|
### Start HTTP
|
||||||
|
|
||||||
|
```go
|
||||||
|
if err := app.Start(":8080"); err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Start HTTPS
|
||||||
|
|
||||||
|
```go
|
||||||
|
log.Fatal(app.StartTLS(":8443", "cert.pem", "key.pem"))
|
||||||
|
```
|
||||||
|
|
||||||
|
### Graceful Shutdown
|
||||||
|
|
||||||
|
```go
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
_ = app.Shutdown(ctx)
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Execution Flow
|
||||||
|
|
||||||
|
For each incoming request:
|
||||||
|
|
||||||
|
1. Route matched via `httprouter`
|
||||||
|
2. `Context` constructed
|
||||||
|
3. Route middleware applied
|
||||||
|
4. Global middleware applied
|
||||||
|
5. Handler executed
|
||||||
|
6. Returned error passed to `ErrorHandler`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Map Helper
|
||||||
|
|
||||||
|
Tower provides a small convenience type:
|
||||||
|
|
||||||
|
```go
|
||||||
|
type Map map[string]any
|
||||||
|
```
|
||||||
|
|
||||||
|
Useful for JSON responses:
|
||||||
|
|
||||||
|
```go
|
||||||
|
return c.WriteJSON(200, tower.Map{
|
||||||
|
"status": "ok",
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Design Goals
|
||||||
|
|
||||||
|
* Minimal abstraction over `net/http`
|
||||||
|
* Deterministic middleware composition
|
||||||
|
* No reflection or hidden state
|
||||||
|
* Explicit error propagation
|
||||||
|
* Fully standard-library compatible
|
||||||
117
context.go
Normal file
117
context.go
Normal file
@@ -0,0 +1,117 @@
|
|||||||
|
package tower
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"encoding/xml"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/julienschmidt/httprouter"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Context struct {
|
||||||
|
request *http.Request
|
||||||
|
response *responseWriter
|
||||||
|
params httprouter.Params
|
||||||
|
}
|
||||||
|
|
||||||
|
type ContextKey string
|
||||||
|
|
||||||
|
func (c *Context) Request() *http.Request {
|
||||||
|
return c.request
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Context) Response() http.ResponseWriter {
|
||||||
|
return c.response
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Context) Query(key string) string {
|
||||||
|
return c.request.URL.Query().Get(key)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Context) Param(key string) string {
|
||||||
|
return c.params.ByName(key)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Context) Value(key ContextKey) any {
|
||||||
|
return c.request.Context().Value(key)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Context) SetValue(key ContextKey, value any) {
|
||||||
|
ctx := context.WithValue(c.request.Context(), key, value)
|
||||||
|
c.request = c.request.WithContext(ctx)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Context) Cookie(key string) (*http.Cookie, error) {
|
||||||
|
return c.request.Cookie(key)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Context) SetCookie(cookie *http.Cookie) {
|
||||||
|
http.SetCookie(c.response, cookie)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Context) Header(key string) string {
|
||||||
|
return c.request.Header.Get(key)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Context) SetHeader(key, value string) {
|
||||||
|
c.response.Header().Set(key, value)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Context) Status() int {
|
||||||
|
return c.response.Status()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Context) SetStatus(code int) {
|
||||||
|
c.response.WriteHeader(code)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Context) ClientIP() string {
|
||||||
|
if ip := c.Header("X-Forwarded-For"); ip != "" {
|
||||||
|
return ip
|
||||||
|
}
|
||||||
|
|
||||||
|
if ip := c.Header("X-Real-IP"); ip != "" {
|
||||||
|
return ip
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.request.RemoteAddr
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Context) Redirect(code int, url string) {
|
||||||
|
http.Redirect(c.response, c.request, url, code)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Context) WriteBytes(code int, b []byte, contentType string) error {
|
||||||
|
c.SetStatus(code)
|
||||||
|
c.SetHeader("Content-Type", contentType)
|
||||||
|
_, err := c.response.Write(b)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Context) WriteString(code int, v string) error {
|
||||||
|
c.SetStatus(code)
|
||||||
|
c.SetHeader("Content-Type", "text/plain; charset=utf-8")
|
||||||
|
_, err := c.response.Write([]byte(v))
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Context) WriteJSON(code int, v any) error {
|
||||||
|
c.SetStatus(code)
|
||||||
|
c.SetHeader("Content-Type", "application/json; charset=utf-8")
|
||||||
|
return json.NewEncoder(c.response).Encode(v)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Context) WriteXML(code int, v any) error {
|
||||||
|
c.SetStatus(code)
|
||||||
|
c.SetHeader("Content-Type", "application/xml; charset=utf-8")
|
||||||
|
return xml.NewEncoder(c.response).Encode(v)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Context) BindJSON(v any) error {
|
||||||
|
return json.NewDecoder(c.request.Body).Decode(v)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Context) BindXML(v any) error {
|
||||||
|
return xml.NewDecoder(c.request.Body).Decode(v)
|
||||||
|
}
|
||||||
5
go.mod
Normal file
5
go.mod
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
module git.trcreatives.at/trcreatives/go-tower
|
||||||
|
|
||||||
|
go 1.25.6
|
||||||
|
|
||||||
|
require github.com/julienschmidt/httprouter v1.3.0
|
||||||
2
go.sum
Normal file
2
go.sum
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
github.com/julienschmidt/httprouter v1.3.0 h1:U0609e9tgbseu3rBINet9P48AI/D3oJs4dN7jwJOQ1U=
|
||||||
|
github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM=
|
||||||
82
group.go
Normal file
82
group.go
Normal file
@@ -0,0 +1,82 @@
|
|||||||
|
package tower
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Group struct {
|
||||||
|
prefix string
|
||||||
|
parent *Tower
|
||||||
|
groupMiddlewares []Middleware
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *Group) Use(groupMiddlewares ...Middleware) {
|
||||||
|
g.groupMiddlewares = append(g.groupMiddlewares, groupMiddlewares...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *Group) Group(prefix string, mws ...Middleware) *Group {
|
||||||
|
child := &Group{
|
||||||
|
parent: g.parent,
|
||||||
|
prefix: g.buildFullPath(prefix),
|
||||||
|
}
|
||||||
|
|
||||||
|
child.groupMiddlewares = make([]Middleware, 0, len(g.groupMiddlewares)+len(mws))
|
||||||
|
child.groupMiddlewares = append(child.groupMiddlewares, g.groupMiddlewares...)
|
||||||
|
child.groupMiddlewares = append(child.groupMiddlewares, mws...)
|
||||||
|
|
||||||
|
return child
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *Group) Static(prefix, dir string) {
|
||||||
|
if prefix == "" || prefix[0] != '/' {
|
||||||
|
prefix = "/" + prefix
|
||||||
|
}
|
||||||
|
|
||||||
|
if prefix[len(prefix)-1] == '/' {
|
||||||
|
prefix = prefix[:len(prefix)-1]
|
||||||
|
}
|
||||||
|
prefix += "/*filepath"
|
||||||
|
|
||||||
|
g.parent.router.ServeFiles(g.buildFullPath(prefix), http.Dir(dir))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *Group) GET(path string, h Handler, routeMiddlewares ...Middleware) {
|
||||||
|
g.Handle(http.MethodGet, path, h, routeMiddlewares...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *Group) POST(path string, h Handler, routeMiddlewares ...Middleware) {
|
||||||
|
g.Handle(http.MethodPost, path, h, routeMiddlewares...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *Group) PUT(path string, h Handler, routeMiddlewares ...Middleware) {
|
||||||
|
g.Handle(http.MethodPut, path, h, routeMiddlewares...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *Group) PATCH(path string, h Handler, routeMiddlewares ...Middleware) {
|
||||||
|
g.Handle(http.MethodPatch, path, h, routeMiddlewares...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *Group) DELETE(path string, h Handler, routeMiddlewares ...Middleware) {
|
||||||
|
g.Handle(http.MethodDelete, path, h, routeMiddlewares...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *Group) Handle(method string, path string, h Handler, routeMiddlewares ...Middleware) {
|
||||||
|
fullPath := g.buildFullPath(path)
|
||||||
|
|
||||||
|
mws := make([]Middleware, 0, len(g.groupMiddlewares)+len(routeMiddlewares))
|
||||||
|
mws = append(mws, g.groupMiddlewares...)
|
||||||
|
mws = append(mws, routeMiddlewares...)
|
||||||
|
|
||||||
|
g.parent.Handle(method, fullPath, h, mws...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *Group) buildFullPath(path string) string {
|
||||||
|
normalizedPrefix := normalizePath(g.prefix)
|
||||||
|
normalizedPath := normalizePath(path)
|
||||||
|
|
||||||
|
if normalizedPrefix == "/" && normalizedPath == "/" {
|
||||||
|
return "/"
|
||||||
|
}
|
||||||
|
|
||||||
|
return normalizePath(g.prefix) + normalizePath(path)
|
||||||
|
}
|
||||||
9
handler.go
Normal file
9
handler.go
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
package tower
|
||||||
|
|
||||||
|
type Handler func(c *Context) error
|
||||||
|
|
||||||
|
type ErrorHandler func(c *Context, err error) error
|
||||||
|
|
||||||
|
func defaultErrorHandler(c *Context, err error) error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
3
middleware.go
Normal file
3
middleware.go
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
package tower
|
||||||
|
|
||||||
|
type Middleware func(h Handler) Handler
|
||||||
14
path.go
Normal file
14
path.go
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
package tower
|
||||||
|
|
||||||
|
import "strings"
|
||||||
|
|
||||||
|
func normalizePath(path string) string {
|
||||||
|
if path == "" {
|
||||||
|
return "/"
|
||||||
|
}
|
||||||
|
|
||||||
|
path = strings.TrimLeft(path, "/")
|
||||||
|
path = strings.TrimRight(path, "/")
|
||||||
|
|
||||||
|
return "/" + path
|
||||||
|
}
|
||||||
33
response_writer.go
Normal file
33
response_writer.go
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
package tower
|
||||||
|
|
||||||
|
import "net/http"
|
||||||
|
|
||||||
|
type responseWriter struct {
|
||||||
|
http.ResponseWriter
|
||||||
|
status int
|
||||||
|
wroteHeader bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *responseWriter) WriteHeader(code int) {
|
||||||
|
if !w.wroteHeader {
|
||||||
|
w.status = code
|
||||||
|
w.wroteHeader = true
|
||||||
|
}
|
||||||
|
|
||||||
|
w.ResponseWriter.WriteHeader(code)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *responseWriter) Write(b []byte) (int, error) {
|
||||||
|
if !w.wroteHeader {
|
||||||
|
w.WriteHeader(http.StatusOK)
|
||||||
|
}
|
||||||
|
|
||||||
|
return w.ResponseWriter.Write(b)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *responseWriter) Status() int {
|
||||||
|
if w.status == 0 {
|
||||||
|
return http.StatusOK
|
||||||
|
}
|
||||||
|
return w.status
|
||||||
|
}
|
||||||
117
tower.go
Normal file
117
tower.go
Normal file
@@ -0,0 +1,117 @@
|
|||||||
|
package tower
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"log"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/julienschmidt/httprouter"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Tower struct {
|
||||||
|
router *httprouter.Router
|
||||||
|
errorHandler ErrorHandler
|
||||||
|
globalMiddlewares []Middleware
|
||||||
|
server *http.Server
|
||||||
|
}
|
||||||
|
|
||||||
|
func New() *Tower {
|
||||||
|
t := &Tower{
|
||||||
|
router: httprouter.New(),
|
||||||
|
errorHandler: defaultErrorHandler,
|
||||||
|
globalMiddlewares: []Middleware{},
|
||||||
|
}
|
||||||
|
|
||||||
|
t.server = &http.Server{
|
||||||
|
Handler: t.router,
|
||||||
|
}
|
||||||
|
|
||||||
|
return t
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *Tower) Start(listenAddr string) error {
|
||||||
|
t.server.Addr = listenAddr
|
||||||
|
return t.server.ListenAndServe()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *Tower) StartTLS(listenAddr, certFile, keyFile string) error {
|
||||||
|
t.server.Addr = listenAddr
|
||||||
|
return t.server.ListenAndServeTLS(certFile, keyFile)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *Tower) Shutdown(ctx context.Context) error {
|
||||||
|
return t.server.Shutdown(ctx)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *Tower) SetErrorHandler(errorHandler ErrorHandler) {
|
||||||
|
t.errorHandler = errorHandler
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *Tower) Use(globalMiddlewares ...Middleware) {
|
||||||
|
t.globalMiddlewares = append(t.globalMiddlewares, globalMiddlewares...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *Tower) Group(prefix string, groupMiddlewares ...Middleware) *Group {
|
||||||
|
return &Group{
|
||||||
|
prefix: normalizePath(prefix),
|
||||||
|
parent: t,
|
||||||
|
groupMiddlewares: groupMiddlewares,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *Tower) Static(prefix, dir string) {
|
||||||
|
prefix = normalizePath(prefix)
|
||||||
|
prefix += "/*filepath"
|
||||||
|
|
||||||
|
t.router.ServeFiles(prefix, http.Dir(dir))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *Tower) GET(path string, h Handler, routeMiddlewares ...Middleware) {
|
||||||
|
t.Handle(http.MethodGet, path, h, routeMiddlewares...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *Tower) POST(path string, h Handler, routeMiddlewares ...Middleware) {
|
||||||
|
t.Handle(http.MethodPost, path, h, routeMiddlewares...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *Tower) PUT(path string, h Handler, routeMiddlewares ...Middleware) {
|
||||||
|
t.Handle(http.MethodPut, path, h, routeMiddlewares...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *Tower) PATCH(path string, h Handler, routeMiddlewares ...Middleware) {
|
||||||
|
t.Handle(http.MethodPatch, path, h, routeMiddlewares...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *Tower) DELETE(path string, h Handler, routeMiddlewares ...Middleware) {
|
||||||
|
t.Handle(http.MethodDelete, path, h, routeMiddlewares...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *Tower) Handle(method string, path string, h Handler, routeMiddlewares ...Middleware) {
|
||||||
|
t.router.Handle(method, path, t.buildHTTPRouterHandle(h, routeMiddlewares...))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *Tower) buildHTTPRouterHandle(h Handler, routeMiddlewares ...Middleware) httprouter.Handle {
|
||||||
|
return func(w http.ResponseWriter, r *http.Request, p httprouter.Params) {
|
||||||
|
ctx := Context{
|
||||||
|
request: r,
|
||||||
|
response: &responseWriter{ResponseWriter: w},
|
||||||
|
params: p,
|
||||||
|
}
|
||||||
|
|
||||||
|
wrappedHandler := h
|
||||||
|
|
||||||
|
for i := len(routeMiddlewares) - 1; i >= 0; i-- {
|
||||||
|
wrappedHandler = routeMiddlewares[i](wrappedHandler)
|
||||||
|
}
|
||||||
|
|
||||||
|
for i := len(t.globalMiddlewares) - 1; i >= 0; i-- {
|
||||||
|
wrappedHandler = t.globalMiddlewares[i](wrappedHandler)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := wrappedHandler(&ctx); err != nil {
|
||||||
|
if err := t.errorHandler(&ctx, err); err != nil {
|
||||||
|
log.Printf("[Error] %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user