From 9f98bbce2daeeec7ad9f0c9443254b73c7786f12 Mon Sep 17 00:00:00 2001 From: Haelnorr Date: Sun, 11 Jan 2026 23:11:49 +1100 Subject: [PATCH] refactored hws to improve database operability --- hws/.gitignore | 2 + hws/LICENSE | 21 +++++++ hws/README.md | 119 +++++++++++++++++++++++++++++++++++++ hws/server_methods_test.go | 44 +++++++------- 4 files changed, 164 insertions(+), 22 deletions(-) create mode 100644 hws/LICENSE create mode 100644 hws/README.md diff --git a/hws/.gitignore b/hws/.gitignore index d4d2509..18dc548 100644 --- a/hws/.gitignore +++ b/hws/.gitignore @@ -17,3 +17,5 @@ coverage.html # Go workspace file go.work + +.claude/ diff --git a/hws/LICENSE b/hws/LICENSE new file mode 100644 index 0000000..fbf1733 --- /dev/null +++ b/hws/LICENSE @@ -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. diff --git a/hws/README.md b/hws/README.md new file mode 100644 index 0000000..5f60a81 --- /dev/null +++ b/hws/README.md @@ -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 diff --git a/hws/server_methods_test.go b/hws/server_methods_test.go index 8912d18..0eab81a 100644 --- a/hws/server_methods_test.go +++ b/hws/server_methods_test.go @@ -18,7 +18,7 @@ func Test_Server_Addr(t *testing.T) { Port: 8080, }) require.NoError(t, err) - + addr := server.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) { var buf bytes.Buffer server := createTestServer(t, &buf) - + // Add routes first handler := testHandler err := server.AddRoutes(hws.Route{ @@ -35,16 +35,16 @@ func Test_Server_Handler(t *testing.T) { Handler: handler, }) require.NoError(t, err) - + // Get the handler h := server.Handler() require.NotNil(t, h) - + // Test the handler directly with httptest req := httptest.NewRequest("GET", "/test", nil) rr := httptest.NewRecorder() h.ServeHTTP(rr, req) - + assert.Equal(t, 200, rr.Code) 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) { var buf bytes.Buffer server := createTestServer(t, &buf) - + // Add routes err := server.AddRoutes(hws.Route{ Path: "/test", @@ -64,28 +64,28 @@ func Test_LoggerIgnorePaths_Integration(t *testing.T) { Handler: testHandler, }) require.NoError(t, err) - + // Set paths to ignore server.LoggerIgnorePaths("/ignore", "/healthz") - + err = server.AddMiddleware() require.NoError(t, err) - + // Test that ignored path doesn't generate logs buf.Reset() req := httptest.NewRequest("GET", "/ignore", nil) rr := httptest.NewRecorder() server.Handler().ServeHTTP(rr, req) - + // Buffer should be empty for ignored path assert.Empty(t, buf.String()) - + // Test that non-ignored path generates logs buf.Reset() req = httptest.NewRequest("GET", "/test", nil) rr = httptest.NewRecorder() server.Handler().ServeHTTP(rr, req) - + // Buffer should have logs for non-ignored path assert.NotEmpty(t, buf.String()) } @@ -93,12 +93,12 @@ func Test_LoggerIgnorePaths_Integration(t *testing.T) { func Test_WrappedWriter(t *testing.T) { var buf bytes.Buffer server := createTestServer(t, &buf) - + // Add routes with different status codes err := server.AddRoutes( hws.Route{ - Path: "/ok", - Method: hws.MethodGET, + Path: "/ok", + Method: hws.MethodGET, Handler: testHandler, }, hws.Route{ @@ -111,16 +111,16 @@ func Test_WrappedWriter(t *testing.T) { }, ) require.NoError(t, err) - + err = server.AddMiddleware() require.NoError(t, err) - + // Test OK status req := httptest.NewRequest("GET", "/ok", nil) rr := httptest.NewRecorder() server.Handler().ServeHTTP(rr, req) assert.Equal(t, 200, rr.Code) - + // Test Created status req = httptest.NewRequest("POST", "/created", nil) rr = httptest.NewRecorder() @@ -149,7 +149,7 @@ func Test_Start_Errors(t *testing.T) { }) require.NoError(t, err) - err = server.Start(nil) + err = server.Start(t.Context()) assert.Error(t, err) assert.Contains(t, err.Error(), "Context cannot be nil") }) @@ -163,10 +163,10 @@ func Test_Shutdown_Errors(t *testing.T) { startTestServer(t, server) <-server.Ready() - err := server.Shutdown(nil) + err := server.Shutdown(t.Context()) assert.Error(t, err) assert.Contains(t, err.Error(), "Context cannot be nil") - + // Clean up 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 err = server.Start(ctx) - + // The error could be nil if server started very quickly, or context.DeadlineExceeded // This tests the ctx.Err() path in waitUntilReady if err != nil {