refactored hws to improve database operability

This commit is contained in:
2026-01-11 23:11:49 +11:00
parent 4c5af63ea2
commit 9f98bbce2d
4 changed files with 164 additions and 22 deletions

2
hws/.gitignore vendored
View File

@@ -17,3 +17,5 @@ coverage.html
# Go workspace file # Go workspace file
go.work go.work
.claude/

21
hws/LICENSE Normal file
View File

@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2026 haelnorr
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

119
hws/README.md Normal file
View File

@@ -0,0 +1,119 @@
# hws
[![Go Reference](https://pkg.go.dev/badge/git.haelnorr.com/h/golib/hws.svg)](https://pkg.go.dev/git.haelnorr.com/h/golib/hws)
A lightweight, opinionated HTTP web server framework for Go built on top of the standard library's `net/http`.
## Features
- 🚀 Built on Go 1.22+ routing patterns with method and path matching
- 🎯 Structured error handling with customizable error pages
- 📝 Integrated logging with zerolog via hlog
- 🔧 Middleware support with predictable execution order
- 🗜️ GZIP compression support
- 🔒 Safe static file serving (prevents directory listing)
- ⚙️ Environment variable configuration
- ⏱️ Request timing and logging middleware
- 💚 Graceful shutdown support
- 🏥 Built-in health check endpoint
## Installation
```bash
go get git.haelnorr.com/h/golib/hws
```
## Quick Start
```go
package main
import (
"context"
"git.haelnorr.com/h/golib/hws"
"net/http"
)
func main() {
// Load configuration from environment variables
config, _ := hws.ConfigFromEnv()
// Create server
server, _ := hws.NewServer(config)
// Define routes
routes := []hws.Route{
{
Path: "/",
Method: hws.MethodGET,
Handler: http.HandlerFunc(homeHandler),
},
{
Path: "/api/users/{id}",
Method: hws.MethodGET,
Handler: http.HandlerFunc(getUserHandler),
},
}
// Add routes and middleware
server.AddRoutes(routes...)
server.AddMiddleware()
// Start server
ctx := context.Background()
server.Start(ctx)
// Wait for server to be ready
<-server.Ready()
}
func homeHandler(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("Hello, World!"))
}
func getUserHandler(w http.ResponseWriter, r *http.Request) {
id := r.PathValue("id")
w.Write([]byte("User ID: " + id))
}
```
## Documentation
Comprehensive documentation is available in the [Wiki](https://git.haelnorr.com/h/golib/wiki/hws).
### Key Topics
- [Configuration](https://git.haelnorr.com/h/golib/wiki/hws#configuration)
- [Routing](https://git.haelnorr.com/h/golib/wiki/hws#routing)
- [Middleware](https://git.haelnorr.com/h/golib/wiki/hws#middleware)
- [Error Handling](https://git.haelnorr.com/h/golib/wiki/hws#error-handling)
- [Logging](https://git.haelnorr.com/h/golib/wiki/hws#logging)
- [Static Files](https://git.haelnorr.com/h/golib/wiki/hws#static-files)
- [Graceful Shutdown](https://git.haelnorr.com/h/golib/wiki/hws#graceful-shutdown)
- [Complete Examples](https://git.haelnorr.com/h/golib/wiki/hws#complete-production-example)
## Environment Variables
| Variable | Description | Default |
|----------|-------------|---------|
| `HWS_HOST` | Host to listen on | `127.0.0.1` |
| `HWS_PORT` | Port to listen on | `3000` |
| `HWS_TRUSTED_HOST` | Trusted hostname/domain | Same as Host |
| `HWS_GZIP` | Enable GZIP compression | `false` |
| `HWS_READ_HEADER_TIMEOUT` | Header read timeout (seconds) | `2` |
| `HWS_WRITE_TIMEOUT` | Write timeout (seconds) | `10` |
| `HWS_IDLE_TIMEOUT` | Idle connection timeout (seconds) | `120` |
## License
This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.
## Contributing
Contributions are welcome! Please feel free to submit a Pull Request.
## Related Projects
- [hwsauth](https://git.haelnorr.com/h/golib/hwsauth) - JWT authentication middleware for hws
- [hlog](https://git.haelnorr.com/h/golib/hlog) - Structured logging with zerolog
- [jwt](https://git.haelnorr.com/h/golib/jwt) - JWT token generation and validation

View File

@@ -18,7 +18,7 @@ func Test_Server_Addr(t *testing.T) {
Port: 8080, Port: 8080,
}) })
require.NoError(t, err) require.NoError(t, err)
addr := server.Addr() addr := server.Addr()
assert.Equal(t, "192.168.1.1:8080", addr) assert.Equal(t, "192.168.1.1:8080", addr)
} }
@@ -26,7 +26,7 @@ func Test_Server_Addr(t *testing.T) {
func Test_Server_Handler(t *testing.T) { func Test_Server_Handler(t *testing.T) {
var buf bytes.Buffer var buf bytes.Buffer
server := createTestServer(t, &buf) server := createTestServer(t, &buf)
// Add routes first // Add routes first
handler := testHandler handler := testHandler
err := server.AddRoutes(hws.Route{ err := server.AddRoutes(hws.Route{
@@ -35,16 +35,16 @@ func Test_Server_Handler(t *testing.T) {
Handler: handler, Handler: handler,
}) })
require.NoError(t, err) require.NoError(t, err)
// Get the handler // Get the handler
h := server.Handler() h := server.Handler()
require.NotNil(t, h) require.NotNil(t, h)
// Test the handler directly with httptest // Test the handler directly with httptest
req := httptest.NewRequest("GET", "/test", nil) req := httptest.NewRequest("GET", "/test", nil)
rr := httptest.NewRecorder() rr := httptest.NewRecorder()
h.ServeHTTP(rr, req) h.ServeHTTP(rr, req)
assert.Equal(t, 200, rr.Code) assert.Equal(t, 200, rr.Code)
assert.Equal(t, "hello world", rr.Body.String()) assert.Equal(t, "hello world", rr.Body.String())
} }
@@ -52,7 +52,7 @@ func Test_Server_Handler(t *testing.T) {
func Test_LoggerIgnorePaths_Integration(t *testing.T) { func Test_LoggerIgnorePaths_Integration(t *testing.T) {
var buf bytes.Buffer var buf bytes.Buffer
server := createTestServer(t, &buf) server := createTestServer(t, &buf)
// Add routes // Add routes
err := server.AddRoutes(hws.Route{ err := server.AddRoutes(hws.Route{
Path: "/test", Path: "/test",
@@ -64,28 +64,28 @@ func Test_LoggerIgnorePaths_Integration(t *testing.T) {
Handler: testHandler, Handler: testHandler,
}) })
require.NoError(t, err) require.NoError(t, err)
// Set paths to ignore // Set paths to ignore
server.LoggerIgnorePaths("/ignore", "/healthz") server.LoggerIgnorePaths("/ignore", "/healthz")
err = server.AddMiddleware() err = server.AddMiddleware()
require.NoError(t, err) require.NoError(t, err)
// Test that ignored path doesn't generate logs // Test that ignored path doesn't generate logs
buf.Reset() buf.Reset()
req := httptest.NewRequest("GET", "/ignore", nil) req := httptest.NewRequest("GET", "/ignore", nil)
rr := httptest.NewRecorder() rr := httptest.NewRecorder()
server.Handler().ServeHTTP(rr, req) server.Handler().ServeHTTP(rr, req)
// Buffer should be empty for ignored path // Buffer should be empty for ignored path
assert.Empty(t, buf.String()) assert.Empty(t, buf.String())
// Test that non-ignored path generates logs // Test that non-ignored path generates logs
buf.Reset() buf.Reset()
req = httptest.NewRequest("GET", "/test", nil) req = httptest.NewRequest("GET", "/test", nil)
rr = httptest.NewRecorder() rr = httptest.NewRecorder()
server.Handler().ServeHTTP(rr, req) server.Handler().ServeHTTP(rr, req)
// Buffer should have logs for non-ignored path // Buffer should have logs for non-ignored path
assert.NotEmpty(t, buf.String()) assert.NotEmpty(t, buf.String())
} }
@@ -93,12 +93,12 @@ func Test_LoggerIgnorePaths_Integration(t *testing.T) {
func Test_WrappedWriter(t *testing.T) { func Test_WrappedWriter(t *testing.T) {
var buf bytes.Buffer var buf bytes.Buffer
server := createTestServer(t, &buf) server := createTestServer(t, &buf)
// Add routes with different status codes // Add routes with different status codes
err := server.AddRoutes( err := server.AddRoutes(
hws.Route{ hws.Route{
Path: "/ok", Path: "/ok",
Method: hws.MethodGET, Method: hws.MethodGET,
Handler: testHandler, Handler: testHandler,
}, },
hws.Route{ hws.Route{
@@ -111,16 +111,16 @@ func Test_WrappedWriter(t *testing.T) {
}, },
) )
require.NoError(t, err) require.NoError(t, err)
err = server.AddMiddleware() err = server.AddMiddleware()
require.NoError(t, err) require.NoError(t, err)
// Test OK status // Test OK status
req := httptest.NewRequest("GET", "/ok", nil) req := httptest.NewRequest("GET", "/ok", nil)
rr := httptest.NewRecorder() rr := httptest.NewRecorder()
server.Handler().ServeHTTP(rr, req) server.Handler().ServeHTTP(rr, req)
assert.Equal(t, 200, rr.Code) assert.Equal(t, 200, rr.Code)
// Test Created status // Test Created status
req = httptest.NewRequest("POST", "/created", nil) req = httptest.NewRequest("POST", "/created", nil)
rr = httptest.NewRecorder() rr = httptest.NewRecorder()
@@ -149,7 +149,7 @@ func Test_Start_Errors(t *testing.T) {
}) })
require.NoError(t, err) require.NoError(t, err)
err = server.Start(nil) err = server.Start(t.Context())
assert.Error(t, err) assert.Error(t, err)
assert.Contains(t, err.Error(), "Context cannot be nil") assert.Contains(t, err.Error(), "Context cannot be nil")
}) })
@@ -163,10 +163,10 @@ func Test_Shutdown_Errors(t *testing.T) {
startTestServer(t, server) startTestServer(t, server)
<-server.Ready() <-server.Ready()
err := server.Shutdown(nil) err := server.Shutdown(t.Context())
assert.Error(t, err) assert.Error(t, err)
assert.Contains(t, err.Error(), "Context cannot be nil") assert.Contains(t, err.Error(), "Context cannot be nil")
// Clean up // Clean up
server.Shutdown(t.Context()) server.Shutdown(t.Context())
}) })
@@ -199,7 +199,7 @@ func Test_WaitUntilReady_ContextCancelled(t *testing.T) {
// Start should return with context error since timeout is so short // Start should return with context error since timeout is so short
err = server.Start(ctx) err = server.Start(ctx)
// The error could be nil if server started very quickly, or context.DeadlineExceeded // The error could be nil if server started very quickly, or context.DeadlineExceeded
// This tests the ctx.Err() path in waitUntilReady // This tests the ctx.Err() path in waitUntilReady
if err != nil { if err != nil {