├── .github └── workflows │ └── coverage.yaml ├── .gitignore ├── LICENSE ├── README.md ├── error.go ├── go.mod ├── go.sum ├── handler.go ├── handler_test.go ├── internal ├── json │ └── error_json.go └── resources │ ├── constants.go │ ├── errors.go │ └── testing.go ├── logger.go ├── logger_test.go ├── middleware └── middleware.go ├── query.go ├── query_test.go ├── router.go ├── router_test.go ├── waggy.go └── waggy_test.go /.github/workflows/coverage.yaml: -------------------------------------------------------------------------------- 1 | on: [push, pull_request] 2 | name: Test Go Coverage 3 | jobs: 4 | test: 5 | strategy: 6 | matrix: 7 | go-version: [1.18.x, 1.19.x] 8 | os: [macos-latest, windows-latest] 9 | runs-on: ${{ matrix.os }} 10 | steps: 11 | - uses: actions/setup-go@v3 12 | with: 13 | go-version: ${{ matrix.go-version }} 14 | - uses: actions/checkout@v3 15 | - run: | 16 | go test -v -cover ./... -coverprofile coverage.out -coverpkg ./... 17 | - name: Go Coverage Badge 18 | uses: tj-actions/coverage-badge-go@v1 19 | if: ${{ runner.os == 'Linux' && matrix.go == '1.18' }} # Runs this on only one of the ci builds. 20 | with: 21 | green: 80 22 | filename: coverage.out 23 | 24 | - uses: stefanzweifel/git-auto-commit-action@v4 25 | id: auto-commit-action 26 | with: 27 | commit_message: Apply Code Coverage Badge 28 | skip_fetch: true 29 | skip_checkout: true 30 | file_pattern: ./README.md 31 | 32 | - name: Push Changes 33 | if: steps.auto-commit-action.outputs.changes_detected == 'true' 34 | uses: ad-m/github-push-action@master 35 | with: 36 | github_token: ${{ github.token }} 37 | branch: ${{ github.ref }} 38 | 39 | - name: Upload coverage to Codecov 40 | uses: codecov/codecov-action@v3 41 | with: 42 | files: ./coverage.out -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Binaries for programs and plugins 2 | *.exe 3 | *.exe~ 4 | *.dll 5 | *.so 6 | *.dylib 7 | 8 | # Test binary, built with `go test -c` 9 | *.test 10 | 11 | # Output of the go coverage tool, specifically when used with LiteIDE 12 | *.out 13 | 14 | # Dependency directories (remove the comment below to include it) 15 | # vendor/ 16 | 17 | .idea -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | BSD 3-Clause License 2 | 3 | Copyright (c) 2022, Quinn Millican 4 | All rights reserved. 5 | 6 | Redistribution and use in source and binary forms, with or without 7 | modification, are permitted provided that the following conditions are met: 8 | 9 | 1. Redistributions of source code must retain the above copyright notice, this 10 | list of conditions and the following disclaimer. 11 | 12 | 2. Redistributions in binary form must reproduce the above copyright notice, 13 | this list of conditions and the following disclaimer in the documentation 14 | and/or other materials provided with the distribution. 15 | 16 | 3. Neither the name of the copyright holder nor the names of its 17 | contributors may be used to endorse or promote products derived from 18 | this software without specific prior written permission. 19 | 20 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 21 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 22 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 23 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 24 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 25 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 26 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 27 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 28 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 29 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 30 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Waggy 2 | [![Go Reference](https://pkg.go.dev/badge/github.com/syke99/waggy.svg)](https://pkg.go.dev/github.com/syke99/waggy) 3 | [![Go Reportcard](https://goreportcard.com/badge/github.com/syke99/waggy)](https://goreportcard.com/report/github.com/syke99/waggy) 4 | [![Codecov](https://codecov.io/gh/syke99/waggy/branch/main/graph/badge.svg?token=KDYH3JO1QI)](https://codecov.io/gh/syke99/waggy) 5 | [![LICENSE](https://img.shields.io/github/license/syke99/waggy)](https://pkg.go.dev/github.com/syke99/waggy/blob/master/LICENSE) 6 | 7 | The dead simple, easy-to-use library for writing HTTP handlers and routers in Go that can be used in standard HTTP server environments or in WAGI (Web Assembly Gateway Interface) environments 8 | 9 | What problems do Waggy solve? 10 | ===== 11 | With WAGI (Web Assembly Gateway Interface), HTTP requests are routed to specific handlers as defined in a modules.toml file 12 | (more information can be found [here](https://github.com/deislabs/wagi)) to specific functions in a WASM module or WAT file. 13 | It accomplishes this by passing in the HTTP request information via os.Stdin and os.Args, and returning HTTP responses via 14 | os.Stdout. To remove considerable amounts of boilerplate code and to provide a familiar API for handling these WAGI HTTP 15 | requests, Waggy was created. It provides path parameter access functionality that will feel very reminiscent to those who 16 | have used [gorilla/mux](https://github.com/gorilla/mux). Additionally, you can also map multiple handlers to a specific route 17 | based on the specific HTTP method that was used in the incoming request. Waggy also allows users to compile an entire server's 18 | worth of routes into a single WASM module and bypass setting up their routes via a modules.toml file if they so choose by 19 | handling mapping the route to the correct entry point (handler). But don't worry, you can also compile individual routes into 20 | their own WASM modules, too, so you can use the conventional modules.toml file for routing. 21 | 22 |
23 | 24 | #### v0.5.0 improvements: 25 | * **Restricting Methods**: With v0.5.0, you have the ability to restrict HTTP methods on your handlers to make sure your endpoints 26 | are more secure 27 | 28 | * **No Matched Routes**: Another added feature in v0.5.0 is handling cases where no routes are matches to the route of the incoming 29 | request with generic 405 responses, as well as the ability to set custom handlers for the same edge case 30 | 31 | How do I use Waggy? 32 | ==== 33 | 34 | ### Installing 35 | To install Waggy in a repo, simply run 36 | 37 | ```bash 38 | $ go get github.com/syke99/waggy 39 | ``` 40 | 41 | Then you can import the package in any go file you'd like 42 | 43 | ```go 44 | import "github.com/syke99/waggy" 45 | ``` 46 | 47 | ### Basic usage 48 | 49 | Examples of using both Routers and Handlers for compiling WASM modules for WAGI can be found in the [examples](https://github.com/syke99/waggy-examples) repo. 50 | 51 | 52 | 53 | **!!NOTE!!** 54 | 55 | To learn more about configuring, routing, compiling, and deploying WAGI routes, as well as 56 | the limitations of WAGI routes, please consult the [WAGI](https://github.com/deislabs/wagi/tree/main/docs) docs 57 | and the [TinyGo](https://tinygo.org/docs/guides/webassembly/) WASM docs 58 | 59 | Who? 60 | ==== 61 | 62 | This library was developed by Quinn Millican ([@syke99](https://github.com/syke99)) 63 | 64 | 65 | ## License 66 | 67 | This repo is under the BSD 3 license, see [LICENSE](../LICENSE) for details. 68 | -------------------------------------------------------------------------------- /error.go: -------------------------------------------------------------------------------- 1 | package waggy 2 | 3 | // WaggyError is provided to help simplify composing the body of 4 | // an HTTP error response 5 | type WaggyError struct { 6 | Type string `json:"type"` 7 | Title string `json:"title"` 8 | Detail string `json:"detail"` 9 | Status int `json:"status"` 10 | Instance string `json:"instance"` 11 | Field string `json:"field"` 12 | } 13 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/syke99/waggy 2 | 3 | go 1.18 4 | 5 | require github.com/stretchr/testify v1.8.1 6 | 7 | require ( 8 | github.com/davecgh/go-spew v1.1.1 // indirect 9 | github.com/pmezard/go-difflib v1.0.0 // indirect 10 | gopkg.in/yaml.v3 v3.0.1 // indirect 11 | ) 12 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 2 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 3 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 4 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 5 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 6 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 7 | github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= 8 | github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= 9 | github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 10 | github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= 11 | github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk= 12 | github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= 13 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= 14 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 15 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 16 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 17 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 18 | -------------------------------------------------------------------------------- /handler.go: -------------------------------------------------------------------------------- 1 | package waggy 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "github.com/syke99/waggy/internal/json" 7 | "github.com/syke99/waggy/middleware" 8 | "net/http" 9 | "net/url" 10 | "os" 11 | "strconv" 12 | "strings" 13 | 14 | "github.com/syke99/waggy/internal/resources" 15 | ) 16 | 17 | // Handler is used to handling various http.HandlerFuncs 18 | // mapped by HTTP methods for an individual route 19 | type Handler struct { 20 | route string 21 | defResp []byte 22 | defRespContType string 23 | defErrResp WaggyError 24 | defErrRespCode int 25 | handlerMap map[string]http.HandlerFunc 26 | restrictedMethods map[string]struct{} 27 | restrictedMethodFunc http.HandlerFunc 28 | logger *Logger 29 | parentLogger *Logger 30 | parentLoggerOverride bool 31 | FullServer bool 32 | middleWare []middleware.MiddleWare 33 | } 34 | 35 | // NewHandler initialized a new Handler and returns 36 | // a pointer to it 37 | func NewHandler(cgi *FullServer) *Handler { 38 | var o bool 39 | var err error 40 | 41 | if cgi != nil { 42 | o, err = strconv.ParseBool(string(*cgi)) 43 | if err != nil { 44 | o = false 45 | } 46 | } 47 | 48 | w := Handler{ 49 | route: "", 50 | defResp: make([]byte, 0), 51 | defRespContType: "", 52 | defErrResp: WaggyError{}, 53 | defErrRespCode: 0, 54 | handlerMap: make(map[string]http.HandlerFunc), 55 | restrictedMethods: make(map[string]struct{}), 56 | logger: nil, 57 | parentLogger: nil, 58 | FullServer: o, 59 | } 60 | 61 | return &w 62 | } 63 | 64 | // NewHandlerWithRoute initialized a new Handler with the provided 65 | // route and returns a pointer to it. It is intended to be used whenever 66 | // only compiling an individual *Handler instead of a full *Router 67 | func NewHandlerWithRoute(route string, cgi *FullServer) *Handler { 68 | if len(route) >= 1 && route[:1] == "/" { 69 | route = route[1:] 70 | } 71 | var o bool 72 | var err error 73 | 74 | if cgi != nil { 75 | o, err = strconv.ParseBool(string(*cgi)) 76 | if err != nil { 77 | o = false 78 | } 79 | } 80 | 81 | w := Handler{ 82 | route: route, 83 | defResp: make([]byte, 0), 84 | defErrResp: WaggyError{}, 85 | defErrRespCode: 0, 86 | handlerMap: make(map[string]http.HandlerFunc), 87 | restrictedMethods: make(map[string]struct{}), 88 | logger: nil, 89 | parentLogger: nil, 90 | FullServer: o, 91 | } 92 | 93 | return &w 94 | } 95 | 96 | // Logger returns the Handler's Logger. If no parent logger is 97 | // inherited from a Router, or you provided a OverrideParentLogger 98 | // whenever adding a Logger to the Handler, then the Handler's 99 | // Logger will be returned. If no logger has been set, then this method 100 | // will return nil 101 | func (wh *Handler) Logger() *Logger { 102 | if wh.parentLogger == nil || 103 | (wh.logger != nil && wh.parentLoggerOverride) { 104 | return wh.logger 105 | } 106 | return wh.parentLogger 107 | } 108 | 109 | // Route returns the route currently set for wh. It is a convenience 110 | // function that greatly eases looping over Handlers and adding 111 | // them to a Router 112 | func (wh *Handler) Route() string { 113 | return fmt.Sprintf("/%s", wh.route) 114 | } 115 | 116 | // UpdateRoute allows you to update the Handler's route 117 | func (wh *Handler) UpdateRoute(route string) { 118 | if len(route) >= 1 && route[:1] == "/" { 119 | route = route[1:] 120 | } 121 | wh.route = route 122 | } 123 | 124 | // Methods returns all HTTP methods that currently have a handler 125 | // set 126 | func (wh *Handler) Methods() []string { 127 | methods := make([]string, 0) 128 | 129 | for k, _ := range wh.handlerMap { 130 | methods = append(methods, k) 131 | } 132 | 133 | return methods 134 | } 135 | 136 | func (wh *Handler) Handler(method string) http.HandlerFunc { 137 | return wh.handlerMap[method] 138 | } 139 | 140 | // WithLogger allows you to set a logger for wh 141 | func (wh *Handler) WithLogger(logger *Logger, parentOverride ParentLoggerOverrider) *Handler { 142 | wh.logger = logger 143 | if parentOverride == nil { 144 | wh.parentLoggerOverride = false 145 | return wh 146 | } 147 | 148 | wh.parentLoggerOverride = parentOverride() 149 | 150 | return wh 151 | } 152 | 153 | // WithDefaultLogger sets wh's logger to the default Logger 154 | func (wh *Handler) WithDefaultLogger() *Handler { 155 | l := Logger{ 156 | logLevel: Info.level(), 157 | key: "", 158 | message: "", 159 | err: "", 160 | vals: make(map[string]interface{}), 161 | log: os.Stderr, 162 | } 163 | 164 | wh.logger = &l 165 | 166 | return wh 167 | } 168 | 169 | func (wh *Handler) inheritLogger(lp *Logger) { 170 | wh.parentLogger = lp 171 | } 172 | 173 | func (wh *Handler) inheritFullServerFlag(cgi bool) { 174 | wh.FullServer = cgi 175 | } 176 | 177 | // WithDefaultResponse allows you to set a default response for 178 | // individual handlers 179 | func (wh *Handler) WithDefaultResponse(contentType string, body []byte) *Handler { 180 | wh.defResp = body 181 | wh.defRespContType = contentType 182 | 183 | return wh 184 | } 185 | 186 | // WithDefaultErrorResponse allows you to set a default error response for 187 | // individual handlers 188 | func (wh *Handler) WithDefaultErrorResponse(err WaggyError, statusCode int) *Handler { 189 | wh.defErrResp = err 190 | wh.defErrRespCode = statusCode 191 | 192 | return wh 193 | } 194 | 195 | // AllHTTPMethods allows you to easily set a 196 | // handler all HTTP Methods 197 | var AllHTTPMethods = func() string { 198 | return "ALL" 199 | } 200 | 201 | // WithMethodHandler allows you to map a different handler to each HTTP Method 202 | // for a single route. 203 | func (wh *Handler) WithMethodHandler(method string, handler http.HandlerFunc) *Handler { 204 | if _, ok := resources.AllHTTPMethods()[method]; !ok { 205 | return wh 206 | } 207 | 208 | if method == "ALL" { 209 | for k, _ := range resources.AllHTTPMethods() { 210 | wh.handlerMap[k] = handler 211 | } 212 | } else { 213 | wh.handlerMap[method] = handler 214 | } 215 | 216 | return wh 217 | } 218 | 219 | // RestrictMethods is a variadic function for restricting a handler from being able 220 | // to be executed on the given methods 221 | func (wh *Handler) RestrictMethods(methods ...string) *Handler { 222 | for _, method := range methods { 223 | if _, ok := resources.AllHTTPMethods()[method]; !ok { 224 | continue 225 | } 226 | wh.restrictedMethods[method] = struct{}{} 227 | } 228 | 229 | return wh 230 | } 231 | 232 | // WithRestrictedMethodHandler allows you to set an http.HandlerFunc to be used 233 | // whenever a request with a restricted HTTP Method is hit. Whenever ServeHTTP is 234 | // called, if this method has not been called and a restricted method has been set 235 | // and is hit by the incoming request, it will return a generic 405 error, instead 236 | func (wh *Handler) WithRestrictedMethodHandler(fn http.HandlerFunc) *Handler { 237 | wh.restrictedMethodFunc = fn 238 | 239 | return wh 240 | } 241 | 242 | // Use allows you to set inline Middleware http.Handlers for a specific *Handler 243 | func (wh *Handler) Use(middleWare ...middleware.MiddleWare) { 244 | for _, mw := range middleWare { 245 | wh.middleWare = append(wh.middleWare, mw) 246 | } 247 | } 248 | 249 | func (wh *Handler) Middleware() []middleware.MiddleWare { 250 | return wh.middleWare 251 | } 252 | 253 | // ServeHTTP serves the route 254 | func (wh *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) { 255 | if _, ok := wh.restrictedMethods[r.Method]; ok { 256 | if wh.restrictedMethodFunc != nil { 257 | wh.restrictedMethodFunc(w, r) 258 | return 259 | } 260 | 261 | r.URL.Opaque = "" 262 | rRoute := r.URL.Path 263 | 264 | methodNotAllowed := WaggyError{ 265 | Title: "Method Not Allowed", 266 | Detail: "method not allowed", 267 | Status: 405, 268 | Instance: rRoute, 269 | } 270 | 271 | wh.defErrResp = methodNotAllowed 272 | 273 | w.WriteHeader(http.StatusMethodNotAllowed) 274 | w.Header().Set("Content-Type", "application/problem+json") 275 | fmt.Fprintln(w, json.BuildJSONStringFromWaggyError(wh.defErrResp.Type, wh.defErrResp.Title, wh.defErrResp.Detail, wh.defErrResp.Status, wh.defErrResp.Instance, wh.defErrResp.Field)) 276 | return 277 | } 278 | 279 | if len(wh.route) >= 1 && wh.route[:1] == "/" { 280 | wh.route = wh.route[1:] 281 | } 282 | 283 | if rr := r.Context().Value(resources.RootRoute); rr != nil { 284 | ctx := context.WithValue(r.Context(), resources.MatchedRoute, "/") 285 | 286 | r = r.Clone(ctx) 287 | } 288 | 289 | splitRoute := strings.Split(wh.route, "/") 290 | 291 | vars := make(map[string]string) 292 | 293 | route := "" 294 | 295 | if mr := r.Context().Value(resources.MatchedRoute); mr != nil { 296 | route = r.Context().Value(resources.MatchedRoute).(string) 297 | } else { 298 | r.URL.Opaque = "" 299 | 300 | route = r.URL.Path 301 | } 302 | 303 | splitRequestRoute := []string{"/"} 304 | 305 | if route != "/" { 306 | splitRequestRoute = strings.Split(route, "/") 307 | 308 | if route[:1] == "/" { 309 | splitRequestRoute = strings.Split(route[1:], "/") 310 | } 311 | } 312 | 313 | for i, section := range splitRoute { 314 | if section == "" || section == "/" { 315 | continue 316 | } 317 | 318 | beginning := section[:1] 319 | middle := section[1 : len(section)-1] 320 | end := section[len(section)-1:] 321 | if beginning == "{" && 322 | end == "}" { 323 | vars[middle] = splitRequestRoute[i] 324 | } 325 | } 326 | 327 | ctx := r.Context() 328 | 329 | queryParams := make(map[string][]string) 330 | 331 | q, _ := url.QueryUnescape(r.URL.RawQuery) 332 | 333 | qp := strings.Split(q, "&") 334 | 335 | if !wh.FullServer { 336 | qp = os.Args[1:] 337 | } 338 | 339 | if len(qp) != 0 && qp[0] != "" { 340 | for _, _qp := range qp { 341 | sqp := strings.Split(_qp, "=") 342 | 343 | key := "" 344 | value := "" 345 | 346 | if len(sqp) == 2 { 347 | key = sqp[0] 348 | value = sqp[1] 349 | 350 | queryParams[key] = append(queryParams[key], value) 351 | } 352 | } 353 | } 354 | 355 | if len(wh.defResp) != 0 { 356 | ctx = context.WithValue(ctx, resources.DefResp, func(w http.ResponseWriter) { 357 | w.Header().Set("Content-Type", wh.defRespContType) 358 | 359 | fmt.Fprintln(w, string(wh.defResp)) 360 | }) 361 | } 362 | 363 | if wh.defErrResp.Detail != "" { 364 | w.Header().Set("Content-Type", "application/problem+json") 365 | 366 | ctx = context.WithValue(ctx, resources.DefErr, func(w http.ResponseWriter) { 367 | fmt.Fprintln(w, json.BuildJSONStringFromWaggyError(wh.defErrResp.Type, wh.defErrResp.Title, wh.defErrResp.Detail, wh.defErrResp.Status, wh.defErrResp.Instance, wh.defErrResp.Field)) 368 | }) 369 | } 370 | 371 | if len(vars) != 0 { 372 | ctx = context.WithValue(ctx, resources.PathParams, vars) 373 | } 374 | 375 | if len(queryParams) != 0 { 376 | ctx = context.WithValue(ctx, resources.QueryParams, queryParams) 377 | } 378 | 379 | if wh.logger != nil { 380 | ctx = context.WithValue(ctx, resources.Logger, wh.logger) 381 | } 382 | 383 | r = r.Clone(ctx) 384 | 385 | handler, _ := wh.handlerMap[r.Method] 386 | 387 | handler(w, r) 388 | } 389 | -------------------------------------------------------------------------------- /handler_test.go: -------------------------------------------------------------------------------- 1 | package waggy 2 | 3 | import ( 4 | "fmt" 5 | "net/http" 6 | "net/http/httptest" 7 | "os" 8 | "testing" 9 | 10 | "github.com/stretchr/testify/assert" 11 | "github.com/syke99/waggy/internal/resources" 12 | ) 13 | 14 | func TestAllHTTPMethods(t *testing.T) { 15 | // Act 16 | all := AllHTTPMethods() 17 | 18 | // Assert 19 | assert.Equal(t, "ALL", all) 20 | } 21 | 22 | func TestNewHandler(t *testing.T) { 23 | // Act 24 | w := NewHandler(nil) 25 | 26 | // Assert 27 | assert.IsType(t, &Handler{}, w) 28 | assert.Equal(t, "", w.route) 29 | assert.IsType(t, []byte{}, w.defResp) 30 | assert.Equal(t, 0, len(w.defResp)) 31 | assert.IsType(t, WaggyError{}, w.defErrResp) 32 | assert.IsType(t, map[string]http.HandlerFunc{}, w.handlerMap) 33 | } 34 | 35 | func TestNewHandlerWithRoute(t *testing.T) { 36 | // Act 37 | w := NewHandlerWithRoute(resources.TestRoute, nil) 38 | 39 | // Assert 40 | assert.IsType(t, &Handler{}, w) 41 | assert.Equal(t, resources.TestRoute[1:], w.route) 42 | assert.IsType(t, []byte{}, w.defResp) 43 | assert.Equal(t, 0, len(w.defResp)) 44 | assert.IsType(t, WaggyError{}, w.defErrResp) 45 | assert.IsType(t, map[string]http.HandlerFunc{}, w.handlerMap) 46 | } 47 | 48 | func TestHandler_WithDefaultResponse(t *testing.T) { 49 | // Arrange 50 | w := NewHandler(nil) 51 | 52 | // Act 53 | w.WithDefaultResponse(resources.TestContentType, []byte(resources.HelloWorld)) 54 | 55 | // Assert 56 | assert.IsType(t, &Handler{}, w) 57 | assert.Equal(t, "", w.route) 58 | assert.IsType(t, []byte{}, w.defResp) 59 | assert.Equal(t, len(resources.HelloWorld), len(w.defResp)) 60 | assert.Equal(t, resources.HelloWorld, string(w.defResp)) 61 | assert.IsType(t, WaggyError{}, w.defErrResp) 62 | assert.IsType(t, map[string]http.HandlerFunc{}, w.handlerMap) 63 | } 64 | 65 | func TestHandler_WithDefaultErrorResponse(t *testing.T) { 66 | // Arrange 67 | w := NewHandler(nil) 68 | testErr := WaggyError{ 69 | Type: resources.TestRoute, 70 | Title: "", 71 | Detail: resources.TestError.Error(), 72 | Status: 0, 73 | } 74 | 75 | // Act 76 | w.WithDefaultErrorResponse(testErr, http.StatusInternalServerError) 77 | 78 | // Assert 79 | assert.IsType(t, &Handler{}, w) 80 | assert.Equal(t, "", w.route) 81 | assert.IsType(t, []byte{}, w.defResp) 82 | assert.Equal(t, 0, len(w.defResp)) 83 | assert.IsType(t, WaggyError{}, w.defErrResp) 84 | assert.Equal(t, testErr, w.defErrResp) 85 | assert.Equal(t, resources.TestRoute, w.defErrResp.Type) 86 | assert.Equal(t, resources.TestError.Error(), w.defErrResp.Detail) 87 | assert.Equal(t, http.StatusInternalServerError, w.defErrRespCode) 88 | assert.IsType(t, map[string]http.HandlerFunc{}, w.handlerMap) 89 | } 90 | 91 | func TestHandler_MethodHandler(t *testing.T) { 92 | // Arrange 93 | helloHandler := func(w http.ResponseWriter, r *http.Request) { 94 | fmt.Fprintln(w, resources.Hello) 95 | } 96 | goodbyeHandler := func(w http.ResponseWriter, r *http.Request) { 97 | fmt.Fprintln(w, resources.Goodbye) 98 | } 99 | w := NewHandler(nil) 100 | 101 | // Act 102 | w.WithMethodHandler(http.MethodGet, helloHandler) 103 | w.WithMethodHandler(http.MethodDelete, goodbyeHandler) 104 | 105 | // Assert 106 | assert.IsType(t, &Handler{}, w) 107 | assert.Equal(t, "", w.route) 108 | assert.IsType(t, []byte{}, w.defResp) 109 | assert.Equal(t, 0, len(w.defResp)) 110 | assert.IsType(t, WaggyError{}, w.defErrResp) 111 | assert.IsType(t, map[string]http.HandlerFunc{}, w.handlerMap) 112 | 113 | for k, v := range w.handlerMap { 114 | switch k { 115 | case http.MethodGet: 116 | assert.Equal(t, resources.GetFunctionPtr(helloHandler), resources.GetFunctionPtr(v)) 117 | case http.MethodDelete: 118 | assert.Equal(t, resources.GetFunctionPtr(goodbyeHandler), resources.GetFunctionPtr(v)) 119 | } 120 | } 121 | } 122 | 123 | func TestHandler_RestrictMethods(t *testing.T) { 124 | // Arrange 125 | w := NewHandler(nil) 126 | 127 | // Act 128 | w = w.RestrictMethods(http.MethodGet, 129 | http.MethodPut, 130 | http.MethodPost, 131 | http.MethodPatch, 132 | http.MethodDelete, 133 | http.MethodConnect, 134 | http.MethodHead, 135 | http.MethodTrace, 136 | http.MethodOptions) 137 | 138 | // Assert 139 | for k := range resources.AllHTTPMethods() { 140 | _, ok := w.restrictedMethods[k] 141 | assert.True(t, ok) 142 | } 143 | } 144 | 145 | func TestHandler_RestrictMethods_NotHTTPMethod(t *testing.T) { 146 | // Arrange 147 | w := NewHandler(nil) 148 | test := "this isn't an http method" 149 | 150 | // Act 151 | w = w.RestrictMethods(test) 152 | 153 | _, ok := w.restrictedMethods[test] 154 | 155 | // Assert 156 | assert.Equal(t, 0, len(w.restrictedMethods)) 157 | assert.False(t, ok) 158 | } 159 | 160 | func TestHandler_WithRestrictedMethodHandler(t *testing.T) { 161 | // Arrange 162 | w := NewHandler(nil) 163 | testHandler := func(w http.ResponseWriter, r *http.Request) {} 164 | 165 | // Act 166 | w.WithRestrictedMethodHandler(testHandler) 167 | 168 | // Asset 169 | assert.NotNil(t, w.restrictedMethodFunc) 170 | } 171 | 172 | func TestHandler_WithRestrictedMethodHandler_NoHandler(t *testing.T) { 173 | // Arrange 174 | w := NewHandler(nil) 175 | 176 | // Act 177 | w.WithRestrictedMethodHandler(nil) 178 | 179 | // Asset 180 | assert.Nil(t, w.restrictedMethodFunc) 181 | } 182 | 183 | func TestHandler_WithLogger(t *testing.T) { 184 | // Arrange 185 | testLog := resources.TestLogFile 186 | testLogLevel := Info 187 | testLogger := NewLogger(testLogLevel, testLog) 188 | 189 | w := NewHandler(nil) 190 | 191 | // Act 192 | w.WithLogger(testLogger, nil) 193 | 194 | // Assert 195 | assert.IsType(t, &Logger{}, w.logger) 196 | assert.Equal(t, Info.level(), w.logger.logLevel) 197 | assert.Equal(t, "", w.logger.key) 198 | assert.Equal(t, "", w.logger.message) 199 | assert.Equal(t, "", w.logger.err) 200 | assert.Equal(t, 0, len(w.logger.vals)) 201 | assert.Equal(t, resources.TestLogFile, w.logger.log) 202 | } 203 | 204 | func TestHandler_WithLogger_ParentOverride(t *testing.T) { 205 | // Assert 206 | r := NewRouter(nil). 207 | WithDefaultLogger() 208 | 209 | testLog := resources.TestLogFile 210 | testLogLevel := Info 211 | testLogger := NewLogger(testLogLevel, testLog) 212 | 213 | w := NewHandler(nil) 214 | 215 | // Act 216 | w.WithLogger(testLogger, OverrideParentLogger()) 217 | 218 | r.Handle(resources.TestRoute, w) 219 | 220 | // Assert 221 | assert.IsType(t, &Logger{}, w.logger) 222 | assert.Equal(t, Info.level(), w.logger.logLevel) 223 | assert.Equal(t, "", w.logger.key) 224 | assert.Equal(t, "", w.logger.message) 225 | assert.Equal(t, "", w.logger.err) 226 | assert.Equal(t, 0, len(w.logger.vals)) 227 | assert.Equal(t, resources.TestLogFile, w.logger.log) 228 | } 229 | 230 | func TestHandler_WithDefaultLogger(t *testing.T) { 231 | // Arrange 232 | w := NewHandler(nil) 233 | 234 | // Act 235 | w.WithDefaultLogger() 236 | 237 | // Assert 238 | assert.IsType(t, &Logger{}, w.logger) 239 | assert.Equal(t, Info.level(), w.logger.logLevel) 240 | assert.Equal(t, "", w.logger.key) 241 | assert.Equal(t, "", w.logger.message) 242 | assert.Equal(t, "", w.logger.err) 243 | assert.Equal(t, 0, len(w.logger.vals)) 244 | assert.Equal(t, os.Stderr, w.logger.log) 245 | } 246 | 247 | func TestHandler_Logger(t *testing.T) { 248 | // Arrange 249 | testLog := resources.TestLogFile 250 | testLogLevel := Info 251 | testLogger := NewLogger(testLogLevel, testLog) 252 | 253 | w := NewHandler(nil). 254 | WithLogger(testLogger, nil) 255 | 256 | // Act 257 | l := w.Logger() 258 | 259 | // Assert 260 | assert.IsType(t, &Logger{}, l) 261 | assert.Equal(t, Info.level(), l.logLevel) 262 | assert.Equal(t, "", l.key) 263 | assert.Equal(t, "", l.message) 264 | assert.Equal(t, "", l.err) 265 | assert.Equal(t, 0, len(l.vals)) 266 | assert.Equal(t, resources.TestLogFile, l.log) 267 | } 268 | 269 | func TestHandler_Logger_Inherited_NoParentOverride(t *testing.T) { 270 | // Assert 271 | r := NewRouter(nil). 272 | WithDefaultLogger() 273 | 274 | testLog := resources.TestLogFile 275 | testLogLevel := Info 276 | testLogger := NewLogger(testLogLevel, testLog) 277 | 278 | w := NewHandler(nil) 279 | 280 | w.WithLogger(testLogger, nil) 281 | 282 | r.Handle(resources.TestRoute, w) 283 | 284 | // Act 285 | l := w.Logger() 286 | 287 | // Assert 288 | assert.IsType(t, &Logger{}, l) 289 | assert.Equal(t, Info.level(), l.logLevel) 290 | assert.Equal(t, "", l.key) 291 | assert.Equal(t, "", l.message) 292 | assert.Equal(t, "", l.err) 293 | assert.Equal(t, 0, len(l.vals)) 294 | assert.Equal(t, os.Stderr, l.log) 295 | } 296 | 297 | func TestHandler_Logger_Inherited_ParentOverride(t *testing.T) { 298 | // Assert 299 | r := NewRouter(nil). 300 | WithDefaultLogger() 301 | 302 | testLog := resources.TestLogFile 303 | testLogLevel := Info 304 | testLogger := NewLogger(testLogLevel, testLog) 305 | 306 | w := NewHandler(nil) 307 | 308 | w.WithLogger(testLogger, OverrideParentLogger()) 309 | 310 | r.Handle(resources.TestRoute, w) 311 | 312 | // Act 313 | l := w.Logger() 314 | 315 | // Assert 316 | assert.IsType(t, &Logger{}, l) 317 | assert.Equal(t, Info.level(), l.logLevel) 318 | assert.Equal(t, "", l.key) 319 | assert.Equal(t, "", l.message) 320 | assert.Equal(t, "", l.err) 321 | assert.Equal(t, 0, len(l.vals)) 322 | assert.Equal(t, resources.TestLogFile, l.log) 323 | } 324 | 325 | func TestHandler_ServeHTTP_RestrictedMethod_NoHandler(t *testing.T) { 326 | // Arrange 327 | w := NewHandler(nil). 328 | RestrictMethods(http.MethodGet) 329 | 330 | r, _ := http.NewRequest(http.MethodGet, resources.TestRoute, nil) 331 | 332 | wr := httptest.NewRecorder() 333 | 334 | // Act 335 | w.ServeHTTP(wr, r) 336 | 337 | // Assert 338 | assert.Equal(t, http.StatusMethodNotAllowed, wr.Code) 339 | assert.Equal(t, resources.TestMethodNotAllowed, wr.Body.String()) 340 | } 341 | 342 | func TestHandler_ServeHTTP_RestrictedMethod_Handler(t *testing.T) { 343 | // Arrange 344 | noRouteHandler := func(w http.ResponseWriter, r *http.Request) { 345 | w.WriteHeader(http.StatusMethodNotAllowed) 346 | fmt.Fprintln(w, "this method isn't allowed, sorry") 347 | } 348 | 349 | w := NewHandler(nil). 350 | RestrictMethods(http.MethodGet). 351 | WithRestrictedMethodHandler(noRouteHandler) 352 | 353 | r, _ := http.NewRequest(http.MethodGet, resources.TestRoute, nil) 354 | 355 | wr := httptest.NewRecorder() 356 | 357 | // Act 358 | w.ServeHTTP(wr, r) 359 | 360 | // Assert 361 | assert.Equal(t, http.StatusMethodNotAllowed, wr.Code) 362 | assert.Equal(t, resources.TestMethodNotAllowedHandlerResp, wr.Body.String()) 363 | } 364 | 365 | func TestHandler_ServeHTTP_MethodGet(t *testing.T) { 366 | // Arrange 367 | helloHandler := func(w http.ResponseWriter, r *http.Request) { 368 | fmt.Fprintln(w, resources.Hello) 369 | } 370 | 371 | goodbyeHandler := func(w http.ResponseWriter, r *http.Request) { 372 | fmt.Fprintln(w, resources.Goodbye) 373 | } 374 | 375 | w := NewHandlerWithRoute(resources.TestRoute, nil) 376 | 377 | w.WithMethodHandler(http.MethodGet, helloHandler) 378 | w.WithMethodHandler(http.MethodDelete, goodbyeHandler) 379 | 380 | w.WithDefaultResponse(resources.TestContentType, []byte(resources.HelloWorld)) 381 | 382 | testErr := WaggyError{ 383 | Type: resources.TestRoute, 384 | Title: "", 385 | Detail: resources.TestError.Error(), 386 | Status: 0, 387 | } 388 | 389 | w.WithDefaultErrorResponse(testErr, http.StatusInternalServerError) 390 | 391 | r, _ := http.NewRequest(http.MethodGet, resources.TestRoute, nil) 392 | 393 | wr := httptest.NewRecorder() 394 | 395 | // Act 396 | w.ServeHTTP(wr, r) 397 | 398 | // Assert 399 | assert.IsType(t, &Handler{}, w) 400 | assert.Equal(t, resources.TestRoute[1:], w.route) 401 | assert.IsType(t, []byte{}, w.defResp) 402 | assert.Equal(t, len(resources.HelloWorld), len(w.defResp)) 403 | assert.Equal(t, resources.HelloWorld, string(w.defResp)) 404 | assert.Equal(t, testErr, w.defErrResp) 405 | assert.Equal(t, resources.TestRoute, w.defErrResp.Type) 406 | assert.Equal(t, resources.TestError.Error(), w.defErrResp.Detail) 407 | assert.Equal(t, http.StatusInternalServerError, w.defErrRespCode) 408 | assert.IsType(t, map[string]http.HandlerFunc{}, w.handlerMap) 409 | 410 | for k, v := range w.handlerMap { 411 | switch k { 412 | case http.MethodGet: 413 | assert.Equal(t, resources.GetFunctionPtr(helloHandler), resources.GetFunctionPtr(v)) 414 | case http.MethodDelete: 415 | assert.Equal(t, resources.GetFunctionPtr(goodbyeHandler), resources.GetFunctionPtr(v)) 416 | } 417 | } 418 | 419 | assert.Equal(t, fmt.Sprintf("%s\n", resources.Hello), wr.Body.String()) 420 | } 421 | 422 | func TestHandler_ServeHTTP_MethodDelete(t *testing.T) { 423 | // Arrange 424 | 425 | helloHandler := func(w http.ResponseWriter, r *http.Request) { 426 | fmt.Fprintln(w, resources.Hello) 427 | } 428 | goodbyeHandler := func(w http.ResponseWriter, r *http.Request) { 429 | fmt.Fprintln(w, resources.Goodbye) 430 | } 431 | 432 | w := NewHandlerWithRoute(resources.TestRoute, nil) 433 | 434 | w.WithMethodHandler(http.MethodGet, helloHandler) 435 | w.WithMethodHandler(http.MethodDelete, goodbyeHandler) 436 | 437 | w.WithDefaultResponse(resources.TestContentType, []byte(resources.HelloWorld)) 438 | 439 | testErr := WaggyError{ 440 | Type: resources.TestRoute, 441 | Title: "", 442 | Detail: resources.TestError.Error(), 443 | Status: 0, 444 | } 445 | 446 | w.WithDefaultErrorResponse(testErr, http.StatusInternalServerError) 447 | 448 | r, _ := http.NewRequest(http.MethodDelete, resources.TestRoute, nil) 449 | 450 | wr := httptest.NewRecorder() 451 | 452 | // Act 453 | w.ServeHTTP(wr, r) 454 | 455 | // Assert 456 | assert.IsType(t, &Handler{}, w) 457 | assert.Equal(t, resources.TestRoute[1:], w.route) 458 | assert.IsType(t, []byte{}, w.defResp) 459 | assert.Equal(t, len(resources.HelloWorld), len(w.defResp)) 460 | assert.Equal(t, resources.HelloWorld, string(w.defResp)) 461 | assert.Equal(t, testErr, w.defErrResp) 462 | assert.Equal(t, resources.TestRoute, w.defErrResp.Type) 463 | assert.Equal(t, resources.TestError.Error(), w.defErrResp.Detail) 464 | assert.Equal(t, http.StatusInternalServerError, w.defErrRespCode) 465 | assert.IsType(t, map[string]http.HandlerFunc{}, w.handlerMap) 466 | 467 | for k, v := range w.handlerMap { 468 | switch k { 469 | case http.MethodGet: 470 | assert.Equal(t, resources.GetFunctionPtr(helloHandler), resources.GetFunctionPtr(v)) 471 | case http.MethodDelete: 472 | assert.Equal(t, resources.GetFunctionPtr(goodbyeHandler), resources.GetFunctionPtr(v)) 473 | } 474 | } 475 | 476 | assert.Equal(t, fmt.Sprintf("%s\n", resources.Goodbye), wr.Body.String()) 477 | } 478 | -------------------------------------------------------------------------------- /internal/json/error_json.go: -------------------------------------------------------------------------------- 1 | package json 2 | 3 | import "fmt" 4 | 5 | func BuildJSONStringFromWaggyError(typ string, ttl string, dtl string, sts int, inst string, fld string) string { 6 | 7 | errStr := "{" 8 | 9 | if typ != "" { 10 | errStr = fmt.Sprintf("%[1]s \"type\": \"%[2]s\",", errStr, typ) 11 | } 12 | 13 | if ttl != "" { 14 | if errStr[:1] != "{" { 15 | errStr = fmt.Sprintf("%[1]s,", errStr) 16 | } 17 | 18 | errStr = fmt.Sprintf("%[1]s \"title\": \"%[2]s\",", errStr, ttl) 19 | } 20 | 21 | if dtl != "" { 22 | if errStr[:1] != "{" { 23 | errStr = fmt.Sprintf("%[1]s,", errStr) 24 | } 25 | 26 | errStr = fmt.Sprintf("%[1]s \"detail\": \"%[2]s\",", errStr, dtl) 27 | } 28 | 29 | if sts != 0 { 30 | if errStr[:1] != "{" { 31 | errStr = fmt.Sprintf("%[1]s,", errStr) 32 | } 33 | 34 | errStr = fmt.Sprintf("%[1]s \"status\": \"%[2]d\",", errStr, sts) 35 | } 36 | 37 | if inst != "" { 38 | if errStr[:1] != "{" { 39 | errStr = fmt.Sprintf("%[1]s,", errStr) 40 | } 41 | 42 | errStr = fmt.Sprintf("%[1]s \"instance\": \"%[2]s\",", errStr, inst) 43 | } 44 | 45 | if fld != "" { 46 | if errStr[:1] != "{" { 47 | errStr = fmt.Sprintf("%[1]s,", errStr) 48 | } 49 | 50 | errStr = fmt.Sprintf("%[1]s \"field\": \"%[2]s\"", errStr, fld) 51 | } 52 | 53 | if errStr[len(errStr)-1:] == "," { 54 | errStr = errStr[:len(errStr)-1] 55 | } 56 | 57 | return fmt.Sprintf("%[1]s }", errStr) 58 | } 59 | -------------------------------------------------------------------------------- /internal/resources/constants.go: -------------------------------------------------------------------------------- 1 | package resources 2 | 3 | import "net/http" 4 | 5 | type ContextKey int 6 | 7 | const ( 8 | DefResp ContextKey = iota 9 | DefErr 10 | MatchedRoute 11 | RootRoute 12 | PathParams 13 | QueryParams 14 | Logger 15 | ) 16 | 17 | var AllHTTPMethods = func() map[string]struct{} { 18 | m := make(map[string]struct{}) 19 | m[http.MethodGet] = struct{}{} 20 | m[http.MethodPut] = struct{}{} 21 | m[http.MethodPost] = struct{}{} 22 | m[http.MethodPatch] = struct{}{} 23 | m[http.MethodOptions] = struct{}{} 24 | m[http.MethodConnect] = struct{}{} 25 | m[http.MethodDelete] = struct{}{} 26 | m[http.MethodTrace] = struct{}{} 27 | m[http.MethodHead] = struct{}{} 28 | 29 | return m 30 | } 31 | -------------------------------------------------------------------------------- /internal/resources/errors.go: -------------------------------------------------------------------------------- 1 | package resources 2 | 3 | import "errors" 4 | 5 | var ( 6 | NoDefaultResponse = errors.New("no default response set") 7 | NoDefaultErrorResponse = errors.New("no default error response set") 8 | NoWaggyEntryPointProvided = errors.New("no entrypoint provided to serve") 9 | ) 10 | -------------------------------------------------------------------------------- /internal/resources/testing.go: -------------------------------------------------------------------------------- 1 | package resources 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "net/http" 7 | "os" 8 | "reflect" 9 | ) 10 | 11 | func GetFunctionPtr(i interface{}) uintptr { 12 | return reflect.ValueOf(i).Pointer() 13 | } 14 | 15 | const ( 16 | Hello = "hello" 17 | Goodbye = "goodbye" 18 | WhereAmI = "where am I?" 19 | HelloWorld = "hello world" 20 | TestFilePath = "./internal/resources/testing.go" 21 | TestContentType = "application/json" 22 | TestRoute = "/test/route" 23 | TestRouteTwo = "/test/route/two" 24 | TestRouteThree = "/test/route/three" 25 | TestRoutePathParams = "/test/route/{param}" 26 | TestRoutePathParamHello = "/test/route/hello" 27 | TestRoutePathParamGoodbye = "/test/route/goodbye" 28 | TestErrorResponse = "{ \"title\": \"Resource Not Found\", \"detail\": \"no path to file provided\", \"status\": \"404\" }" 29 | TestMethodNotAllowed = "{ \"title\": \"Method Not Allowed\", \"detail\": \"method not allowed\", \"status\": \"405\", \"instance\": \"/test/route\" }\n" 30 | TestMethodNotAllowedHandlerResp = "this method isn't allowed, sorry\n" 31 | ) 32 | 33 | var ( 34 | TestError = errors.New("this is a test Waggy Error") 35 | TestLogFile = &os.File{} 36 | TestKey = "testKey" 37 | TestMapKey1 = "testMapKey1" 38 | TestMapKey2 = "testMapKey2" 39 | TestMapKey3 = "testMapKey3" 40 | TestMapValue1 = []string{Hello} 41 | TestMapValue2 = []string{Hello, Goodbye} 42 | TestMapValue3 = make([]string, 0) 43 | TestMessage = "testMessage" 44 | TestValue = "testValue" 45 | TestQueryMap = func() map[string][]string { 46 | m := make(map[string][]string) 47 | m[TestMapKey1] = TestMapValue1 48 | m[TestMapKey2] = TestMapValue2 49 | m[TestMapKey3] = TestMapValue3 50 | 51 | return m 52 | } 53 | TestListenAndServeAddr = fmt.Sprintf("localhost:3000") 54 | TestMethods = []string{http.MethodDelete, http.MethodGet} 55 | TestRoutes = []string{TestRoute, TestRouteTwo} 56 | ) 57 | -------------------------------------------------------------------------------- /logger.go: -------------------------------------------------------------------------------- 1 | package waggy 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "os" 7 | ) 8 | 9 | // ParentLoggerOverrider overrides the parent *Logger in a Handler 10 | type ParentLoggerOverrider = func() bool 11 | 12 | // OverrideParentLogger 13 | func OverrideParentLogger() ParentLoggerOverrider { 14 | return func() bool { 15 | return true 16 | } 17 | } 18 | 19 | // Logger is used for writing to a log 20 | type Logger struct { 21 | logLevel string 22 | key string 23 | message string 24 | err string 25 | vals map[string]interface{} 26 | log *os.File 27 | } 28 | 29 | // LogLevel allows you to set the level 30 | // to be used in a *Logger 31 | type LogLevel int 32 | 33 | const ( 34 | Info LogLevel = iota 35 | Debug 36 | Warning 37 | Fatal 38 | Error 39 | Warn 40 | All 41 | Off 42 | ) 43 | 44 | func (l LogLevel) level() string { 45 | return []string{ 46 | "INFO", 47 | "DEBUG", 48 | "WARNING", 49 | "FATAL", 50 | "ERROR", 51 | "WARN", 52 | "ALL", 53 | "OFF", 54 | }[l] 55 | } 56 | 57 | // NewLogger returns a new *Logger with the provided log file (if 58 | // log is not nil) and the provided logLevel. 59 | func NewLogger(logLevel LogLevel, log *os.File) *Logger { 60 | l := Logger{ 61 | logLevel: logLevel.level(), 62 | key: "", 63 | message: "", 64 | err: "", 65 | vals: make(map[string]interface{}), 66 | log: log, 67 | } 68 | 69 | return &l 70 | } 71 | 72 | // SetLogFile set a specific file for the logger to Write to. 73 | // You must mount the volume that this file resides in whenever 74 | // you configure your WAGI server via your modules.toml file 75 | // for a *Logger to be able to write to the provided file 76 | func (l *Logger) SetLogFile(log *os.File) error { 77 | if log == nil { 78 | return errors.New("no log file provided") 79 | } 80 | 81 | l.log = log 82 | 83 | return nil 84 | } 85 | 86 | // Level update the level of a *Logger 87 | func (l *Logger) Level(level LogLevel) *Logger { 88 | l.logLevel = level.level() 89 | 90 | return l 91 | } 92 | 93 | // Err provide an error to the *Logger to be logged 94 | func (l *Logger) Err(err error) *Logger { 95 | if err == nil { 96 | return l 97 | } 98 | 99 | l.err = err.Error() 100 | 101 | return l 102 | } 103 | 104 | // Val add a value with the corresponding key to be logged by the *Logger 105 | func (l *Logger) Val(key string, val any) *Logger { 106 | l.vals[key] = val 107 | 108 | return l 109 | } 110 | 111 | // Msg provide a message with a key to be logged and then 112 | // logs the constructed log messed to the set *os.File (or default to os.Stderr) 113 | func (l *Logger) Msg(key string, msg string) (int, error) { 114 | l.key = key 115 | l.message = msg 116 | 117 | lm := make(map[string]string) 118 | 119 | lm["level"] = l.logLevel 120 | 121 | if l.key != "" { 122 | lm[l.key] = l.message 123 | } 124 | 125 | for k, v := range l.vals { 126 | if k != "" { 127 | lm[k] = fmt.Sprintf("%s,%v", lm[k], v) 128 | } 129 | } 130 | 131 | if l.err != "" { 132 | lm["error"] = l.err 133 | } 134 | 135 | return l.log.Write([]byte(buildLogJSON(lm))) 136 | } 137 | 138 | func buildLogJSON(log map[string]string) string { 139 | logJSON := "{" 140 | 141 | for key, value := range log { 142 | if key != "" { 143 | if logJSON[:1] != "{" { 144 | logJSON = fmt.Sprintf("%s,", logJSON) 145 | } 146 | 147 | logJSON = fmt.Sprintf("%[1]s \"%[2]s\": \"%[3]s\"", logJSON, key, value) 148 | } 149 | } 150 | 151 | return fmt.Sprintf("%s }", logJSON) 152 | } 153 | -------------------------------------------------------------------------------- /logger_test.go: -------------------------------------------------------------------------------- 1 | package waggy 2 | 3 | import ( 4 | "os" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/assert" 8 | "github.com/syke99/waggy/internal/resources" 9 | ) 10 | 11 | func TestOverrideParentLogger(t *testing.T) { 12 | // Act 13 | o := OverrideParentLogger() 14 | 15 | // Assert 16 | assert.Equal(t, true, o()) 17 | } 18 | 19 | func TestNewLogger(t *testing.T) { 20 | // Arrange 21 | level := Info 22 | testLogFile := resources.TestLogFile 23 | 24 | // Act 25 | l := NewLogger(level, testLogFile) 26 | 27 | // Assert 28 | assert.IsType(t, resources.TestLogFile, l.log) 29 | assert.Equal(t, Info.level(), l.logLevel) 30 | } 31 | 32 | func TestLogger_SetLogFile(t *testing.T) { 33 | // Arrange 34 | l := Logger{ 35 | logLevel: "", 36 | key: "", 37 | message: "", 38 | err: "", 39 | vals: make(map[string]interface{}), 40 | log: nil, 41 | } 42 | testLogFile := resources.TestLogFile 43 | 44 | // Act 45 | err := l.SetLogFile(testLogFile) 46 | 47 | // Assert 48 | assert.NoError(t, err) 49 | assert.IsType(t, resources.TestLogFile, l.log) 50 | } 51 | 52 | func TestLogger_SetLogFile_Error(t *testing.T) { 53 | // Arrange 54 | l := Logger{ 55 | logLevel: "", 56 | key: "", 57 | message: "", 58 | err: "", 59 | vals: make(map[string]interface{}), 60 | log: nil, 61 | } 62 | 63 | // Act 64 | err := l.SetLogFile(nil) 65 | 66 | // Assert 67 | assert.Error(t, err) 68 | assert.Equal(t, "no log file provided", err.Error()) 69 | } 70 | 71 | func TestLogger_Level(t *testing.T) { 72 | // Arrange 73 | l := Logger{ 74 | logLevel: "", 75 | key: "", 76 | message: "", 77 | err: "", 78 | vals: make(map[string]interface{}), 79 | log: nil, 80 | } 81 | testLogLevel := Info 82 | 83 | // Act 84 | l.Level(testLogLevel) 85 | 86 | // Assert 87 | assert.Equal(t, Info.level(), l.logLevel) 88 | } 89 | 90 | func TestLogger_Err(t *testing.T) { 91 | // Arrange 92 | l := Logger{ 93 | logLevel: "", 94 | key: "", 95 | message: "", 96 | err: "", 97 | vals: make(map[string]interface{}), 98 | log: nil, 99 | } 100 | testErr := resources.TestError 101 | 102 | // Act 103 | l.Err(testErr) 104 | 105 | // Assert 106 | assert.Equal(t, testErr.Error(), l.err) 107 | } 108 | 109 | func TestLogger_Err_Nil(t *testing.T) { 110 | // Arrange 111 | l := Logger{ 112 | logLevel: "", 113 | key: "", 114 | message: "", 115 | err: "", 116 | vals: make(map[string]interface{}), 117 | log: nil, 118 | } 119 | 120 | // Act 121 | l.Err(nil) 122 | 123 | // Assert 124 | assert.Equal(t, "", l.err) 125 | } 126 | 127 | func TestLogger_Msg(t *testing.T) { 128 | // Arrange 129 | l := Logger{ 130 | logLevel: "", 131 | key: "", 132 | message: "", 133 | err: "", 134 | vals: make(map[string]interface{}), 135 | log: os.Stdout, 136 | } 137 | testKey := resources.TestKey 138 | testMsg := resources.TestMessage 139 | 140 | // Act 141 | _, err := l.Msg(testKey, testMsg) 142 | 143 | // Assert 144 | assert.NoError(t, err) 145 | assert.Equal(t, resources.TestKey, l.key) 146 | assert.Equal(t, resources.TestMessage, l.message) 147 | } 148 | 149 | func TestLogger_Msg_Error(t *testing.T) { 150 | // Arrange 151 | l := Logger{ 152 | logLevel: "", 153 | key: "", 154 | message: "", 155 | err: "", 156 | vals: make(map[string]interface{}), 157 | log: nil, 158 | } 159 | testKey := resources.TestKey 160 | testMsg := resources.TestMessage 161 | 162 | // Act 163 | _, err := l.Msg(testKey, testMsg) 164 | 165 | // Assert 166 | assert.Error(t, err) 167 | assert.Equal(t, resources.TestKey, l.key) 168 | assert.Equal(t, resources.TestMessage, l.message) 169 | } 170 | 171 | func TestLogger_Val(t *testing.T) { 172 | // Arrange 173 | l := Logger{ 174 | logLevel: "", 175 | key: "", 176 | message: "", 177 | err: "", 178 | vals: make(map[string]interface{}), 179 | log: nil, 180 | } 181 | testKey := resources.TestKey 182 | testValue := resources.TestValue 183 | 184 | //Act 185 | l.Val(testKey, testValue) 186 | 187 | assert.Equal(t, 1, len(l.vals)) 188 | for k, v := range l.vals { 189 | assert.Equal(t, k, resources.TestKey) 190 | assert.Equal(t, v, resources.TestValue) 191 | } 192 | } 193 | -------------------------------------------------------------------------------- /middleware/middleware.go: -------------------------------------------------------------------------------- 1 | package middleware 2 | 3 | import "net/http" 4 | 5 | type MiddleWare func(handler http.Handler) http.Handler 6 | 7 | func PassThroughMiddleWare(middle []MiddleWare, handler http.HandlerFunc) http.HandlerFunc { 8 | for _, mw := range middle { 9 | handler = mw(handler).ServeHTTP 10 | } 11 | 12 | return handler 13 | } 14 | -------------------------------------------------------------------------------- /query.go: -------------------------------------------------------------------------------- 1 | package waggy 2 | 3 | import ( 4 | "github.com/syke99/waggy/internal/resources" 5 | "net/http" 6 | ) 7 | 8 | // QueryParams contains a map containing all the query params from the provided *http.Request 9 | type QueryParams struct { 10 | qp map[string][]string 11 | } 12 | 13 | // Query returns QueryParams from the provided *http.Request 14 | func Query(r *http.Request) *QueryParams { 15 | if rv := r.Context().Value(resources.QueryParams); rv != nil { 16 | qp := rv.(map[string][]string) 17 | 18 | q := QueryParams{ 19 | qp: qp, 20 | } 21 | 22 | return &q 23 | } 24 | return nil 25 | } 26 | 27 | // Get returns the first value stored in q with the provided key 28 | func (q *QueryParams) Get(key string) string { 29 | v, ok := q.qp[key] 30 | if !ok { 31 | return "" 32 | } 33 | 34 | return v[0] 35 | } 36 | 37 | // Set sets the provided val in q with the provided key. If an existing 38 | // value/set of values is already stored at the provided key, then Set 39 | // will override that value 40 | func (q *QueryParams) Set(key string, val string) { 41 | if _, ok := q.qp[key]; ok { 42 | delete(q.qp, key) 43 | } 44 | 45 | newSlice := append(make([]string, 0), val) 46 | 47 | q.qp[key] = newSlice 48 | } 49 | 50 | // Add either appends the provided val at the provided key stored in q, or, 51 | // if no value is currently stored at the provided key, then a new slice will 52 | // be stored at the provided key and the provided val appended to it 53 | func (q *QueryParams) Add(key string, val string) { 54 | if _, ok := q.qp[key]; !ok { 55 | q.qp[key] = make([]string, 0) 56 | } 57 | 58 | q.qp[key] = append(q.qp[key], val) 59 | } 60 | 61 | // Del deletes the value(s) stored in q at the provided key 62 | func (q *QueryParams) Del(key string) { 63 | delete(q.qp, key) 64 | } 65 | 66 | // Values returns a slice of all values stored in q with the provided key 67 | func (q *QueryParams) Values(key string) []string { 68 | if _, ok := q.qp[key]; !ok { 69 | return make([]string, 0) 70 | } 71 | 72 | return q.qp[key] 73 | } 74 | -------------------------------------------------------------------------------- /query_test.go: -------------------------------------------------------------------------------- 1 | package waggy 2 | 3 | import ( 4 | "context" 5 | "github.com/stretchr/testify/assert" 6 | "github.com/syke99/waggy/internal/resources" 7 | "net/http" 8 | "testing" 9 | ) 10 | 11 | func TestQuery(t *testing.T) { 12 | // Arrange 13 | r, _ := http.NewRequest(http.MethodGet, resources.TestRoute, nil) 14 | 15 | ctx := context.WithValue(r.Context(), resources.QueryParams, resources.TestQueryMap()) 16 | 17 | r = r.Clone(ctx) 18 | 19 | // Act 20 | q := Query(r) 21 | 22 | // Assert 23 | assert.IsType(t, &QueryParams{}, q) 24 | assert.Equal(t, 3, len(q.qp)) 25 | assert.Equal(t, 1, len(q.qp[resources.TestMapKey1])) 26 | assert.Equal(t, resources.TestMapValue1, q.qp[resources.TestMapKey1]) 27 | assert.Equal(t, 2, len(q.qp[resources.TestMapKey2])) 28 | assert.Equal(t, resources.TestMapValue2, q.qp[resources.TestMapKey2]) 29 | assert.Equal(t, 0, len(q.qp[resources.TestMapKey3])) 30 | assert.Equal(t, resources.TestMapValue3, q.qp[resources.TestMapKey3]) 31 | } 32 | 33 | func TestQueryParams_Get(t *testing.T) { 34 | // Arrange 35 | r, _ := http.NewRequest(http.MethodGet, resources.TestRoute, nil) 36 | 37 | ctx := context.WithValue(r.Context(), resources.QueryParams, resources.TestQueryMap()) 38 | 39 | r = r.Clone(ctx) 40 | 41 | q := Query(r) 42 | 43 | // Act 44 | v := q.Get(resources.TestMapKey1) 45 | 46 | // Assert 47 | assert.Equal(t, resources.TestMapValue1[0], v) 48 | } 49 | 50 | func TestQueryParams_Get_NoValue(t *testing.T) { 51 | // Arrange 52 | r, _ := http.NewRequest(http.MethodGet, resources.TestRoute, nil) 53 | 54 | ctx := context.WithValue(r.Context(), resources.QueryParams, resources.TestQueryMap()) 55 | 56 | r = r.Clone(ctx) 57 | 58 | q := Query(r) 59 | 60 | // Act 61 | v := q.Get("agaga") 62 | 63 | // Assert 64 | assert.Equal(t, "", v) 65 | } 66 | 67 | func TestQueryParams_Set(t *testing.T) { 68 | // Arrange 69 | r, _ := http.NewRequest(http.MethodGet, resources.TestRoute, nil) 70 | 71 | ctx := context.WithValue(r.Context(), resources.QueryParams, resources.TestQueryMap()) 72 | 73 | r = r.Clone(ctx) 74 | 75 | q := Query(r) 76 | 77 | // Act 78 | q.Set(resources.TestMapKey2, resources.Hello) 79 | 80 | // Assert 81 | assert.Equal(t, 1, len(q.qp[resources.TestMapKey2])) 82 | assert.Equal(t, resources.Hello, q.qp[resources.TestMapKey2][0]) 83 | } 84 | 85 | func TestQueryParams_Add(t *testing.T) { 86 | // Arrange 87 | r, _ := http.NewRequest(http.MethodGet, resources.TestRoute, nil) 88 | 89 | ctx := context.WithValue(r.Context(), resources.QueryParams, resources.TestQueryMap()) 90 | 91 | r = r.Clone(ctx) 92 | 93 | q := Query(r) 94 | 95 | // Act 96 | q.Add(resources.TestMapKey2, resources.WhereAmI) 97 | 98 | // Asset 99 | assert.Equal(t, 3, len(q.qp[resources.TestMapKey2])) 100 | assert.Equal(t, resources.WhereAmI, q.qp[resources.TestMapKey2][2]) 101 | } 102 | 103 | func TestQueryParams_Del(t *testing.T) { 104 | // Arrange 105 | r, _ := http.NewRequest(http.MethodGet, resources.TestRoute, nil) 106 | 107 | ctx := context.WithValue(r.Context(), resources.QueryParams, resources.TestQueryMap()) 108 | 109 | r = r.Clone(ctx) 110 | 111 | q := Query(r) 112 | 113 | // Act 114 | q.Del(resources.TestMapKey2) 115 | 116 | // Act 117 | assert.Equal(t, 0, len(q.qp[resources.TestMapKey2])) 118 | } 119 | 120 | func TestQueryParams_Values(t *testing.T) { 121 | // Arrange 122 | r, _ := http.NewRequest(http.MethodGet, resources.TestRoute, nil) 123 | 124 | ctx := context.WithValue(r.Context(), resources.QueryParams, resources.TestQueryMap()) 125 | 126 | r = r.Clone(ctx) 127 | 128 | q := Query(r) 129 | 130 | // Act 131 | v := q.Values(resources.TestMapKey2) 132 | 133 | // Assert 134 | assert.Equal(t, 2, len(v)) 135 | assert.Equal(t, resources.Hello, v[0]) 136 | assert.Equal(t, resources.Goodbye, v[1]) 137 | } 138 | -------------------------------------------------------------------------------- /router.go: -------------------------------------------------------------------------------- 1 | package waggy 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "github.com/syke99/waggy/internal/json" 7 | "github.com/syke99/waggy/middleware" 8 | "net/http" 9 | "os" 10 | "strconv" 11 | "strings" 12 | 13 | "github.com/syke99/waggy/internal/resources" 14 | ) 15 | 16 | // Router is used for routing incoming HTTP requests to 17 | // specific *Handlers by the route provided whenever you call 18 | // Handle on the return router and provide a route for the *Handler 19 | // you provide 20 | type Router struct { 21 | logger *Logger 22 | router map[string]*Handler 23 | handlerOrder map[int]string 24 | noRoute WaggyError 25 | noRouteFunc http.HandlerFunc 26 | FullServer bool 27 | middleWare []middleware.MiddleWare 28 | } 29 | 30 | // NewRouter initializes a new Router and returns a pointer 31 | // to it 32 | func NewRouter(cgi *FullServer) *Router { 33 | var o bool 34 | var err error 35 | 36 | if cgi != nil { 37 | o, err = strconv.ParseBool(string(*cgi)) 38 | if err != nil { 39 | o = false 40 | } 41 | } 42 | 43 | r := Router{ 44 | logger: nil, 45 | router: make(map[string]*Handler), 46 | handlerOrder: make(map[int]string), 47 | noRoute: WaggyError{ 48 | Title: "Resource not found", 49 | Detail: "route not found", 50 | Status: 404, 51 | Instance: "/", 52 | }, 53 | FullServer: o, 54 | } 55 | 56 | return &r 57 | } 58 | 59 | // Handle allows you to map a *Handler for a specific route. Just 60 | // in the popular gorilla/mux router, you can specify path parameters 61 | // by wrapping them with {} and they can later be accessed by calling 62 | // Vars(r) 63 | func (wr *Router) Handle(route string, handler *Handler) *Router { 64 | handler.route = route 65 | handler.inheritLogger(wr.logger) 66 | handler.inheritFullServerFlag(wr.FullServer) 67 | wr.router[route] = handler 68 | wr.handlerOrder[len(wr.router)] = route 69 | 70 | return wr 71 | } 72 | 73 | // Routes returns all the routes that a *Router has 74 | // *Handlers set for in the order that they were added 75 | func (wr *Router) Routes() []string { 76 | r := make([]string, 0) 77 | 78 | for i := 0; i <= len(wr.router); i++ { 79 | r = append(r, wr.handlerOrder[i]) 80 | } 81 | 82 | return r 83 | } 84 | 85 | // WithLogger allows you to set a Logger for the entire router. Whenever 86 | // Handle is called, this logger will be passed to the *Handler 87 | // being handled for the given route. 88 | func (wr *Router) WithLogger(logger *Logger) *Router { 89 | wr.logger = logger 90 | 91 | return wr 92 | } 93 | 94 | // WithDefaultLogger sets wr's logger to the default Logger 95 | func (wr *Router) WithDefaultLogger() *Router { 96 | l := Logger{ 97 | logLevel: Info.level(), 98 | key: "", 99 | message: "", 100 | err: "", 101 | vals: make(map[string]interface{}), 102 | log: os.Stderr, 103 | } 104 | 105 | wr.logger = &l 106 | 107 | return wr 108 | } 109 | 110 | // WithNoRouteHandler allows you to set an http.HandlerFunc to be used whenever 111 | // no route is found. If this method is not called and the ServeHTTP method 112 | // has been called, then it will return a generic 404 response, instead 113 | func (wr *Router) WithNoRouteHandler(fn http.HandlerFunc) *Router { 114 | wr.noRouteFunc = fn 115 | 116 | return wr 117 | } 118 | 119 | // Logger returns the Router's logger 120 | func (wr *Router) Logger() *Logger { 121 | return wr.logger 122 | } 123 | 124 | // Use allows you to set Middleware http.Handlers for a *Router 125 | func (wr *Router) Use(middleWare ...middleware.MiddleWare) { 126 | for _, mw := range middleWare { 127 | wr.middleWare = append(wr.middleWare, mw) 128 | } 129 | } 130 | 131 | func (wr *Router) Middleware() []middleware.MiddleWare { 132 | return wr.middleWare 133 | } 134 | 135 | // ServeHTTP satisfies the http.Handler interface and calls the stored 136 | // handler at the route of the incoming HTTP request 137 | func (wr *Router) ServeHTTP(w http.ResponseWriter, r *http.Request) { 138 | rt := "" 139 | 140 | r.URL.Opaque = "" 141 | rRoute := r.URL.Path 142 | 143 | var handler *Handler 144 | 145 | var ok bool 146 | 147 | if rRoute == "" || rRoute == "/" { 148 | if handler, ok = wr.router["/"]; !ok { 149 | w.WriteHeader(http.StatusMethodNotAllowed) 150 | wr.noRouteResponse(w, r) 151 | return 152 | } else { 153 | ctx := context.WithValue(r.Context(), resources.RootRoute, true) 154 | 155 | r = r.Clone(ctx) 156 | } 157 | } 158 | 159 | if handler == nil { 160 | var key string 161 | var h *Handler 162 | 163 | for key, h = range wr.router { 164 | if key == "/" { 165 | continue 166 | } 167 | 168 | if rRoute[:1] == "/" { 169 | rRoute = rRoute[1:] 170 | } 171 | 172 | splitRoute := strings.Split(rRoute, "/") 173 | 174 | if key[:1] == "/" { 175 | key = key[1:] 176 | } 177 | 178 | splitKey := strings.Split(key, "/") 179 | 180 | for i, section := range splitKey { 181 | if len(section) == 0 { 182 | continue 183 | } 184 | 185 | beginning := section[:1] 186 | end := section[len(section)-1:] 187 | 188 | // check if this section is a query param 189 | if (beginning == "{" && 190 | end == "}") && (len(splitRoute) != len(splitKey)) { 191 | continue 192 | } 193 | 194 | if (beginning == "{" && 195 | end == "}") && (len(splitRoute) == len(splitKey)) { 196 | rt = key 197 | } 198 | 199 | // if the route sections don't match and aren't query 200 | // params, break out as these are not the correctly matched 201 | // routes 202 | if i > len(splitRoute) || splitRoute[i] != section && rt == "" { 203 | break 204 | } 205 | 206 | if len(splitKey) > len(splitRoute) && 207 | i == len(splitRoute) && 208 | rt == "" { 209 | rt = key 210 | } 211 | 212 | // if the end of splitRoute is reached, and we haven't 213 | // broken out of the loop to move on to the next route, 214 | // then the routes match 215 | if (i == len(splitKey)-1 || i == len(splitRoute)) && 216 | rt == "" { 217 | rt = key 218 | } 219 | } 220 | 221 | if rt != "" { 222 | ctx := context.WithValue(r.Context(), resources.MatchedRoute, rRoute) 223 | 224 | r = r.Clone(ctx) 225 | 226 | handler = h 227 | } 228 | } 229 | 230 | if handler == nil { 231 | wr.noRouteResponse(w, r) 232 | } 233 | } 234 | 235 | handler.ServeHTTP(w, r) 236 | return 237 | } 238 | 239 | func (wr *Router) noRouteResponse(w http.ResponseWriter, r *http.Request) { 240 | if wr.noRouteFunc != nil { 241 | wr.noRouteFunc(w, r) 242 | return 243 | } 244 | 245 | w.WriteHeader(http.StatusNotFound) 246 | w.Header().Set("Content-Type", "application/problem+json") 247 | fmt.Fprintln(w, json.BuildJSONStringFromWaggyError(wr.noRoute.Type, wr.noRoute.Title, wr.noRoute.Detail, wr.noRoute.Status, wr.noRoute.Instance, wr.noRoute.Field)) 248 | } 249 | 250 | // Walk accepts a *Router and a walkFunc to execute on each route that has been 251 | // added to the *Router. It walks the *Router in the order that each 252 | // *Handler was added to the *Router with *Router.Handle(). It returns the first 253 | // error encountered 254 | func (wr *Router) Walk(walkFunc func(method string, route string) error) error { 255 | for i := 1; i <= len(wr.router); i++ { 256 | route, _ := wr.handlerOrder[i] 257 | 258 | handler := wr.router[route] 259 | 260 | if err := walkMethods(route, handler, walkFunc); err != nil { 261 | return err 262 | } 263 | } 264 | return nil 265 | } 266 | 267 | func walkMethods(route string, handler *Handler, walkFunc func(method string, route string) error) error { 268 | for _, method := range handler.Methods() { 269 | if err := walkFunc(method, route); err != nil { 270 | return err 271 | } 272 | } 273 | return nil 274 | } 275 | -------------------------------------------------------------------------------- /router_test.go: -------------------------------------------------------------------------------- 1 | package waggy 2 | 3 | import ( 4 | "fmt" 5 | "net/http" 6 | "net/http/httptest" 7 | "os" 8 | "testing" 9 | 10 | "github.com/stretchr/testify/assert" 11 | "github.com/syke99/waggy/internal/resources" 12 | ) 13 | 14 | func TestNewRouter(t *testing.T) { 15 | // Act 16 | w := NewRouter(nil) 17 | 18 | // Assert 19 | assert.IsType(t, &Router{}, w) 20 | assert.IsType(t, map[string]*Handler{}, w.router) 21 | assert.Equal(t, 0, len(w.router)) 22 | } 23 | 24 | func TestNewRouter_Flg_Parsable(t *testing.T) { 25 | // Act 26 | var flg FullServer = "1" 27 | w := NewRouter(&flg) 28 | 29 | // Assert 30 | assert.IsType(t, &Router{}, w) 31 | assert.IsType(t, map[string]*Handler{}, w.router) 32 | assert.Equal(t, 0, len(w.router)) 33 | assert.True(t, w.FullServer) 34 | } 35 | 36 | func TestNewRouter_Flg_NotParsable(t *testing.T) { 37 | // Act 38 | var flg FullServer = "adsf" 39 | w := NewRouter(&flg) 40 | 41 | // Assert 42 | assert.IsType(t, &Router{}, w) 43 | assert.IsType(t, map[string]*Handler{}, w.router) 44 | assert.Equal(t, 0, len(w.router)) 45 | assert.False(t, w.FullServer) 46 | } 47 | 48 | func TestRouter_Handle(t *testing.T) { 49 | // Arrange 50 | w := NewRouter(nil) 51 | 52 | helloHandler := NewHandler(nil) 53 | goodbyeHandler := NewHandler(nil) 54 | 55 | // Act 56 | w.Handle("/hello", helloHandler) 57 | w.Handle("/goodbye", goodbyeHandler) 58 | 59 | // Assert 60 | assert.IsType(t, &Handler{}, w.router["/hello"]) 61 | assert.IsType(t, &Handler{}, w.router["/goodbye"]) 62 | } 63 | 64 | func TestRouter_WithLogger(t *testing.T) { 65 | // Arrange 66 | w := NewRouter(nil) 67 | 68 | testLog := resources.TestLogFile 69 | testLogLevel := Info 70 | l := NewLogger(testLogLevel, testLog) 71 | 72 | // Act 73 | w.WithLogger(l) 74 | 75 | // Assert 76 | assert.IsType(t, &Logger{}, w.logger) 77 | assert.Equal(t, Info.level(), w.logger.logLevel) 78 | assert.Equal(t, "", w.logger.key) 79 | assert.Equal(t, "", w.logger.message) 80 | assert.Equal(t, "", w.logger.err) 81 | assert.Equal(t, 0, len(w.logger.vals)) 82 | assert.Equal(t, resources.TestLogFile, w.logger.log) 83 | } 84 | 85 | func TestRouter_WithDefaultLogger(t *testing.T) { 86 | // Arrange 87 | w := NewRouter(nil) 88 | 89 | // Act 90 | w.WithDefaultLogger() 91 | 92 | // Assert 93 | assert.IsType(t, &Logger{}, w.logger) 94 | assert.Equal(t, Info.level(), w.logger.logLevel) 95 | assert.Equal(t, "", w.logger.key) 96 | assert.Equal(t, "", w.logger.message) 97 | assert.Equal(t, "", w.logger.err) 98 | assert.Equal(t, 0, len(w.logger.vals)) 99 | assert.Equal(t, os.Stderr, w.logger.log) 100 | } 101 | 102 | func TestRouter_Logger(t *testing.T) { 103 | // Arrange 104 | testLog := resources.TestLogFile 105 | testLogLevel := Info 106 | testLogger := NewLogger(testLogLevel, testLog) 107 | 108 | w := NewRouter(nil). 109 | WithLogger(testLogger) 110 | 111 | // Act 112 | l := w.Logger() 113 | 114 | // Assert 115 | assert.IsType(t, &Logger{}, l) 116 | assert.Equal(t, Info.level(), l.logLevel) 117 | assert.Equal(t, "", l.key) 118 | assert.Equal(t, "", l.message) 119 | assert.Equal(t, "", l.err) 120 | assert.Equal(t, 0, len(l.vals)) 121 | assert.Equal(t, resources.TestLogFile, l.log) 122 | } 123 | 124 | func TestRouter_Logger_Default(t *testing.T) { 125 | // Arrange 126 | w := NewRouter(nil). 127 | WithDefaultLogger() 128 | 129 | // Act 130 | l := w.Logger() 131 | 132 | // Assert 133 | assert.IsType(t, &Logger{}, l) 134 | assert.Equal(t, Info.level(), l.logLevel) 135 | assert.Equal(t, "", l.key) 136 | assert.Equal(t, "", l.message) 137 | assert.Equal(t, "", l.err) 138 | assert.Equal(t, 0, len(l.vals)) 139 | assert.Equal(t, os.Stderr, l.log) 140 | } 141 | 142 | func TestRouter_ServeHTTP_NoBaseRoute(t *testing.T) { 143 | // Arrange 144 | goodbyeHandler := func(w http.ResponseWriter, r *http.Request) { 145 | fmt.Fprintln(w, resources.Goodbye) 146 | } 147 | 148 | wr := NewRouter(nil).WithNoRouteHandler(goodbyeHandler) 149 | 150 | helloHandler := func(w http.ResponseWriter, r *http.Request) { 151 | fmt.Fprintln(w, resources.Hello) 152 | } 153 | 154 | wh := NewHandler(nil). 155 | WithMethodHandler(http.MethodGet, helloHandler) 156 | 157 | wr.Handle(resources.TestRoute, wh) 158 | 159 | r, _ := http.NewRequest(http.MethodGet, "/", nil) 160 | 161 | rr := httptest.NewRecorder() 162 | 163 | // Act 164 | wr.ServeHTTP(rr, r) 165 | 166 | // Assert 167 | assert.IsType(t, &Router{}, wr) 168 | assert.Equal(t, http.StatusMethodNotAllowed, rr.Code) 169 | assert.Equal(t, fmt.Sprintf("%s\n", resources.Goodbye), rr.Body.String()) 170 | } 171 | 172 | func TestRouter_ServeHTTP_BaseRoute(t *testing.T) { 173 | // Arrange 174 | goodbyeHandler := func(w http.ResponseWriter, r *http.Request) { 175 | fmt.Fprintln(w, resources.Goodbye) 176 | } 177 | 178 | wr := NewRouter(nil) 179 | 180 | helloHandler := func(w http.ResponseWriter, r *http.Request) { 181 | fmt.Fprintln(w, resources.Hello) 182 | } 183 | 184 | wh := NewHandler(nil). 185 | WithMethodHandler(http.MethodGet, helloHandler) 186 | 187 | wh2 := NewHandler(nil).WithMethodHandler(http.MethodGet, goodbyeHandler) 188 | 189 | wr.Handle(resources.TestRoute, wh).Handle("/", wh2) 190 | 191 | r, _ := http.NewRequest(http.MethodGet, "/", nil) 192 | 193 | rr := httptest.NewRecorder() 194 | 195 | // Act 196 | wr.ServeHTTP(rr, r) 197 | 198 | // Assert 199 | assert.IsType(t, &Router{}, wr) 200 | assert.Equal(t, http.StatusOK, rr.Code) 201 | assert.Equal(t, fmt.Sprintf("%s\n", resources.Goodbye), rr.Body.String()) 202 | } 203 | 204 | func TestRouter_ServeHTTP_MethodGet(t *testing.T) { 205 | // Arrange 206 | wr := NewRouter(nil) 207 | 208 | helloHandler := func(w http.ResponseWriter, r *http.Request) { 209 | fmt.Fprintln(w, resources.Hello) 210 | } 211 | 212 | wh := NewHandler(nil). 213 | WithMethodHandler(http.MethodGet, helloHandler) 214 | 215 | wr.Handle(resources.TestRoute, wh) 216 | 217 | r, _ := http.NewRequest(http.MethodGet, resources.TestRoute, nil) 218 | 219 | rr := httptest.NewRecorder() 220 | 221 | // Act 222 | wr.ServeHTTP(rr, r) 223 | 224 | // Assert 225 | assert.IsType(t, &Router{}, wr) 226 | assert.IsType(t, &Handler{}, wr.router[resources.TestRoute]) 227 | assert.Equal(t, fmt.Sprintf("%s\n", resources.Hello), rr.Body.String()) 228 | } 229 | 230 | func TestRouter_ServeHTTP_MethodDelete(t *testing.T) { 231 | // Arrange 232 | wr := NewRouter(nil) 233 | 234 | goodbyeHandler := func(w http.ResponseWriter, r *http.Request) { 235 | fmt.Fprintln(w, resources.Goodbye) 236 | } 237 | 238 | wh := NewHandler(nil). 239 | WithMethodHandler(http.MethodDelete, goodbyeHandler) 240 | 241 | wr.Handle(resources.TestRoute, wh) 242 | 243 | r, _ := http.NewRequest(http.MethodDelete, resources.TestRoute, nil) 244 | 245 | rr := httptest.NewRecorder() 246 | 247 | // Act 248 | wr.ServeHTTP(rr, r) 249 | 250 | // Assert 251 | assert.IsType(t, &Router{}, wr) 252 | assert.IsType(t, &Handler{}, wr.router[resources.TestRoute]) 253 | assert.Equal(t, fmt.Sprintf("%s\n", resources.Goodbye), rr.Body.String()) 254 | } 255 | 256 | func TestRouter_Walk(t *testing.T) { 257 | // Arrange 258 | wr := NewRouter(nil) 259 | 260 | helloHandler := func(w http.ResponseWriter, r *http.Request) { 261 | fmt.Fprintln(w, resources.Hello) 262 | } 263 | 264 | goodbyeHandler := func(w http.ResponseWriter, r *http.Request) { 265 | fmt.Fprintln(w, resources.Goodbye) 266 | } 267 | 268 | wh := NewHandler(nil). 269 | WithMethodHandler(http.MethodDelete, goodbyeHandler) 270 | 271 | wr.Handle(resources.TestRoute, wh) 272 | 273 | wh2 := NewHandler(nil). 274 | WithMethodHandler(http.MethodGet, helloHandler) 275 | 276 | wr.Handle(resources.TestRouteTwo, wh2) 277 | 278 | methods := []string{http.MethodDelete, http.MethodGet} 279 | routes := []string{resources.TestRoute, resources.TestRouteTwo} 280 | 281 | // Act 282 | _ = wr.Walk(func(method string, route string) error { 283 | index := func(m string, methods []string) int { 284 | for i, mthd := range methods { 285 | if mthd == m { 286 | return i 287 | } 288 | } 289 | return 0 290 | } 291 | 292 | i := index(method, methods) 293 | 294 | // Assert 295 | assert.Equal(t, route, routes[i]) 296 | 297 | return nil 298 | }) 299 | } 300 | 301 | func TestRouter_Walk_Error(t *testing.T) { 302 | // Arrange 303 | wr := NewRouter(nil) 304 | 305 | helloHandler := func(w http.ResponseWriter, r *http.Request) { 306 | fmt.Fprintln(w, resources.Hello) 307 | } 308 | 309 | goodbyeHandler := func(w http.ResponseWriter, r *http.Request) { 310 | fmt.Fprintln(w, resources.Goodbye) 311 | } 312 | 313 | wh := NewHandler(nil). 314 | WithMethodHandler(http.MethodDelete, goodbyeHandler) 315 | 316 | wr.Handle(resources.TestRoute, wh) 317 | 318 | wh2 := NewHandler(nil). 319 | WithMethodHandler(http.MethodGet, helloHandler) 320 | 321 | wr.Handle(resources.TestRouteThree, wh2) 322 | 323 | // Act 324 | err := wr.Walk(func(method string, route string) error { 325 | index := func(m string, methods []string) int { 326 | for i, mthd := range methods { 327 | if mthd == m { 328 | return i 329 | } 330 | } 331 | return 0 332 | } 333 | 334 | if index(method, resources.TestMethods) == 0 { 335 | return resources.TestError 336 | } 337 | 338 | return nil 339 | }) 340 | 341 | // Assert 342 | assert.Error(t, err) 343 | assert.Equal(t, resources.TestError, err) 344 | } 345 | -------------------------------------------------------------------------------- /waggy.go: -------------------------------------------------------------------------------- 1 | package waggy 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "github.com/syke99/waggy/middleware" 7 | "io" 8 | "net/http" 9 | "net/http/cgi" 10 | "os" 11 | 12 | "github.com/syke99/waggy/internal/resources" 13 | ) 14 | 15 | type FullServer string 16 | 17 | // WaggyEntryPoint is used as a type constraint whenever calling 18 | // Serve so that only a *Router or *Handler can 19 | // be used and not a bare http.Handler 20 | type WaggyEntryPoint interface { 21 | *Router | *Handler 22 | ServeHTTP(w http.ResponseWriter, r *http.Request) 23 | Middleware() []middleware.MiddleWare 24 | } 25 | 26 | // WriteDefaultResponse returns the result (number of bytes written 27 | // and a nil value, or the error of that write) of writing the set 28 | // default response inside the handler it is being used inside of. 29 | // If no default response has been set, this function will return 30 | // an error. 31 | func WriteDefaultResponse(w http.ResponseWriter, r *http.Request) { 32 | rv := r.Context().Value(resources.DefResp) 33 | if rv == nil { 34 | fmt.Fprintln(w, resources.NoDefaultResponse.Error()) 35 | } 36 | 37 | fn := rv.(func(wr http.ResponseWriter)) 38 | 39 | fn(w) 40 | } 41 | 42 | // WriteDefaultErrorResponse returns the result of writing the set 43 | // default error response inside the handler it is being used inside of. 44 | // If no default error response has been set, this function will return 45 | // an error. 46 | func WriteDefaultErrorResponse(w http.ResponseWriter, r *http.Request) { 47 | rv := r.Context().Value(resources.DefErr) 48 | if rv == nil { 49 | fmt.Fprintln(w, resources.NoDefaultErrorResponse.Error()) 50 | } 51 | 52 | fn := rv.(func(wr http.ResponseWriter)) 53 | 54 | fn(w) 55 | } 56 | 57 | // Log returns the *Logger for the current request handler 58 | // to log a message to the set *os.File (defautls to os.Stderr) 59 | // if one exists, otherwise nil 60 | func Log(r *http.Request) *Logger { 61 | if rv := r.Context().Value(resources.Logger); rv != nil { 62 | return rv.(*Logger) 63 | } 64 | return nil 65 | } 66 | 67 | // Vars returns the values matching any path parameters if they exist, 68 | // otherwise returns nil 69 | func Vars(r *http.Request) map[string]string { 70 | if rv := r.Context().Value(resources.PathParams); rv != nil { 71 | return rv.(map[string]string) 72 | } 73 | return nil 74 | } 75 | 76 | // Serve wraps a call to cgi.serve and also uses a type constraint of 77 | // WaggyEntryPoint so that only a *Router or *Handler can be 78 | // used in the call to Serve and not accidentally allow calling 79 | // a bare http.Handler 80 | func Serve[W WaggyEntryPoint](entryPoint W) error { 81 | mw := entryPoint.Middleware() 82 | 83 | if mw != nil { 84 | var hf http.HandlerFunc 85 | 86 | switch any(entryPoint).(type) { 87 | case *Handler: 88 | h := any(entryPoint).(*Handler) 89 | 90 | hf = h.ServeHTTP 91 | 92 | hf = middleware.PassThroughMiddleWare(mw, hf) 93 | case *Router: 94 | r := any(entryPoint).(*Router) 95 | 96 | hf = r.ServeHTTP 97 | 98 | hf = middleware.PassThroughMiddleWare(mw, hf) 99 | } 100 | 101 | handler := http.Handler(hf) 102 | 103 | return cgi.Serve(handler) 104 | } 105 | 106 | return cgi.Serve(entryPoint) 107 | } 108 | 109 | // ListenAndServe wraps a call to http.ListenAndServe and also uses 110 | // a type constraint of WaggyEntryPoint so that only a *waggy.Router or 111 | // *waggy.Handler can be used 112 | func ListenAndServe[W WaggyEntryPoint](addr string, entryPoint W) error { 113 | if entryPoint == nil { 114 | return resources.NoWaggyEntryPointProvided 115 | } 116 | 117 | mw := entryPoint.Middleware() 118 | 119 | if mw != nil { 120 | var hf http.HandlerFunc 121 | 122 | switch any(entryPoint).(type) { 123 | case *Handler: 124 | h := any(entryPoint).(*Handler) 125 | 126 | hf = h.ServeHTTP 127 | 128 | hf = middleware.PassThroughMiddleWare(mw, hf) 129 | case *Router: 130 | r := any(entryPoint).(*Router) 131 | 132 | hf = r.ServeHTTP 133 | 134 | hf = middleware.PassThroughMiddleWare(mw, hf) 135 | } 136 | 137 | handler := http.Handler(hf) 138 | 139 | return http.ListenAndServe(addr, handler) 140 | } 141 | 142 | return http.ListenAndServe(addr, entryPoint) 143 | } 144 | 145 | // ServeFile is a convenience function for serving the file at the given filePath to the given 146 | // http.ResponseWriter (w). If Waggy cannot find a file at the given path (if it doesn't exist 147 | // or the volume was incorrectly mounted), this function will return a status 404. If any other 148 | // error occurs, this function will return a 500. If no contentType is given, this function will 149 | // set the Content-Type header to "application/octet-stream" 150 | func ServeFile(w http.ResponseWriter, contentType string, filePath string) { 151 | var err error 152 | 153 | errMsg := WaggyError{ 154 | Title: "", 155 | Status: 0, 156 | } 157 | 158 | if filePath == "" { 159 | err = errors.New("no path to file provided") 160 | errMsg.Title = "Resource Not Found" 161 | errMsg.Status = http.StatusNotFound 162 | } 163 | 164 | file := new(os.File) 165 | if err == nil { 166 | file, err = os.Open(filePath) 167 | } 168 | 169 | if err == nil { 170 | if contentType == "" { 171 | contentType = "application/octet-stream" 172 | } 173 | w.Header().Set("content-type", contentType) 174 | _, err = io.Copy(w, file) 175 | } 176 | 177 | if err != nil { 178 | w.WriteHeader(http.StatusNotFound) 179 | if errMsg.Status == 0 { 180 | errMsg.Status = http.StatusInternalServerError 181 | errMsg.Title = "Internal Server Error" 182 | w.WriteHeader(http.StatusInternalServerError) 183 | } 184 | 185 | errJSON := fmt.Sprintf("{ \"title\": \"%[1]s\", \"detail\": \"%[2]s\", \"status\": \"%[3]d\" }", errMsg.Title, err.Error(), errMsg.Status) 186 | 187 | w.Header().Set("content-type", "application/problem+json") 188 | fmt.Fprint(w, errJSON) 189 | } 190 | } 191 | -------------------------------------------------------------------------------- /waggy_test.go: -------------------------------------------------------------------------------- 1 | package waggy 2 | 3 | import ( 4 | "fmt" 5 | "github.com/syke99/waggy/internal/json" 6 | "net/http" 7 | "net/http/httptest" 8 | "os" 9 | "testing" 10 | 11 | "github.com/stretchr/testify/assert" 12 | "github.com/syke99/waggy/internal/resources" 13 | ) 14 | 15 | func TestLog(t *testing.T) { 16 | // Arrange 17 | logger := Logger{ 18 | logLevel: "", 19 | key: "", 20 | message: "", 21 | err: "", 22 | vals: make(map[string]interface{}), 23 | log: os.Stdout, 24 | } 25 | 26 | greetingHandler := func(w http.ResponseWriter, r *http.Request) { 27 | l := Log(r) 28 | 29 | // Assert 30 | assert.NotNil(t, l) 31 | } 32 | 33 | handler := NewHandlerWithRoute(resources.TestRoutePathParams, nil). 34 | WithLogger(&logger, nil) 35 | 36 | handler.WithMethodHandler(http.MethodGet, greetingHandler) 37 | 38 | r, _ := http.NewRequest(http.MethodGet, resources.TestRoutePathParamGoodbye, nil) 39 | 40 | wr := httptest.NewRecorder() 41 | 42 | handler.ServeHTTP(wr, r) 43 | } 44 | 45 | func TestLog_Error(t *testing.T) { 46 | // Arrange 47 | greetingHandler := func(w http.ResponseWriter, r *http.Request) { 48 | l := Log(r) 49 | 50 | // Assert 51 | assert.Nil(t, l) 52 | } 53 | 54 | handler := NewHandlerWithRoute(resources.TestRoutePathParams, nil). 55 | WithLogger(nil, nil) 56 | 57 | handler.WithMethodHandler(http.MethodGet, greetingHandler) 58 | 59 | r, _ := http.NewRequest(http.MethodGet, resources.TestRoutePathParamGoodbye, nil) 60 | 61 | wr := httptest.NewRecorder() 62 | 63 | handler.ServeHTTP(wr, r) 64 | } 65 | 66 | func TestVars_Hello(t *testing.T) { 67 | // Arrange 68 | 69 | greetingHandler := func(w http.ResponseWriter, r *http.Request) { 70 | params := Vars(r) 71 | 72 | switch params["param"] { 73 | case resources.Hello: 74 | fmt.Fprintln(w, resources.Hello) 75 | case resources.Goodbye: 76 | fmt.Fprintln(w, resources.Goodbye) 77 | case "": 78 | fmt.Fprintln(w, resources.WhereAmI) 79 | } 80 | } 81 | 82 | handler := NewHandlerWithRoute(resources.TestRoutePathParams, nil) 83 | 84 | handler.WithMethodHandler(http.MethodGet, greetingHandler) 85 | 86 | r, _ := http.NewRequest(http.MethodGet, resources.TestRoutePathParamHello, nil) 87 | 88 | wr := httptest.NewRecorder() 89 | 90 | handler.ServeHTTP(wr, r) 91 | 92 | assert.Equal(t, fmt.Sprintf("%s\n", resources.Hello), wr.Body.String()) 93 | assert.Equal(t, http.StatusOK, wr.Code) 94 | } 95 | 96 | func TestVars_Goodbye(t *testing.T) { 97 | // Arrange 98 | greetingHandler := func(w http.ResponseWriter, r *http.Request) { 99 | params := Vars(r) 100 | 101 | switch params["param"] { 102 | case resources.Hello: 103 | fmt.Fprintln(w, resources.Hello) 104 | case resources.Goodbye: 105 | fmt.Fprintln(w, resources.Goodbye) 106 | case "": 107 | fmt.Fprintln(w, resources.WhereAmI) 108 | } 109 | } 110 | 111 | handler := NewHandlerWithRoute(resources.TestRoutePathParams, nil) 112 | 113 | handler.WithMethodHandler(http.MethodGet, greetingHandler) 114 | 115 | r, _ := http.NewRequest(http.MethodGet, resources.TestRoutePathParamGoodbye, nil) 116 | 117 | wr := httptest.NewRecorder() 118 | 119 | // Act 120 | handler.ServeHTTP(wr, r) 121 | 122 | // Assert 123 | assert.Equal(t, fmt.Sprintf("%s\n", resources.Goodbye), wr.Body.String()) 124 | } 125 | 126 | func TestVars_NoPathParams(t *testing.T) { 127 | // Arrange 128 | 129 | greetingHandler := func(w http.ResponseWriter, r *http.Request) { 130 | params := Vars(r) 131 | 132 | switch params["param"] { 133 | case resources.Hello: 134 | fmt.Fprintln(w, resources.Hello) 135 | case resources.Goodbye: 136 | fmt.Fprintln(w, resources.Goodbye) 137 | case "": 138 | fmt.Fprintln(w, resources.WhereAmI) 139 | } 140 | } 141 | 142 | handler := NewHandlerWithRoute(resources.TestRoute, nil) 143 | 144 | handler.WithMethodHandler(http.MethodGet, greetingHandler) 145 | 146 | r, _ := http.NewRequest(http.MethodGet, resources.TestRoute, nil) 147 | 148 | wr := httptest.NewRecorder() 149 | 150 | // Act 151 | handler.ServeHTTP(wr, r) 152 | 153 | // Assert 154 | assert.Equal(t, fmt.Sprintf("%s\n", resources.WhereAmI), wr.Body.String()) 155 | } 156 | 157 | func TestWriteDefaultResponse(t *testing.T) { 158 | // Arrange 159 | 160 | defRespHandler := func(w http.ResponseWriter, r *http.Request) { 161 | WriteDefaultResponse(w, r) 162 | } 163 | 164 | handler := NewHandlerWithRoute(resources.TestRoute, nil). 165 | WithMethodHandler(http.MethodGet, defRespHandler). 166 | WithDefaultResponse(resources.TestContentType, []byte(resources.Hello)) 167 | 168 | r, _ := http.NewRequest(http.MethodGet, resources.TestRoutePathParamHello, nil) 169 | 170 | wr := httptest.NewRecorder() 171 | 172 | // Act 173 | handler.ServeHTTP(wr, r) 174 | 175 | // Assault 176 | assert.Equal(t, fmt.Sprintf("%s\n", resources.Hello), wr.Body.String()) 177 | } 178 | 179 | func TestWriteDefaultErrorResponse(t *testing.T) { 180 | // Arrange 181 | testErr := WaggyError{ 182 | Type: resources.TestRoute, 183 | Title: "", 184 | Detail: resources.TestError.Error(), 185 | Status: 0, 186 | } 187 | expectedResult := fmt.Sprintf("%s\n", json.BuildJSONStringFromWaggyError(testErr.Type, "", testErr.Detail, 0, "", "")) 188 | 189 | defRespHandler := func(w http.ResponseWriter, r *http.Request) { 190 | WriteDefaultErrorResponse(w, r) 191 | } 192 | 193 | handler := NewHandlerWithRoute(resources.TestRoute, nil). 194 | WithMethodHandler(http.MethodGet, defRespHandler). 195 | WithDefaultErrorResponse(testErr, http.StatusInternalServerError) 196 | 197 | r, _ := http.NewRequest(http.MethodGet, resources.TestRoutePathParamHello, nil) 198 | 199 | wr := httptest.NewRecorder() 200 | 201 | // Act 202 | handler.ServeHTTP(wr, r) 203 | 204 | // Assert 205 | assert.Equal(t, expectedResult, wr.Body.String()) 206 | } 207 | 208 | func TestServeFile(t *testing.T) { 209 | // Arrange 210 | wr := httptest.NewRecorder() 211 | 212 | // Act 213 | ServeFile(wr, resources.TestContentType, resources.TestFilePath) 214 | 215 | // Assert 216 | assert.Equal(t, http.StatusOK, wr.Code) 217 | assert.Equal(t, "application/json", wr.Header().Get("content-type")) 218 | } 219 | 220 | func TestServeFile_NoContentType(t *testing.T) { 221 | // Arrange 222 | wr := httptest.NewRecorder() 223 | 224 | // Act 225 | ServeFile(wr, "", resources.TestFilePath) 226 | 227 | // Assert 228 | assert.Equal(t, http.StatusOK, wr.Code) 229 | assert.Equal(t, "application/octet-stream", wr.Header().Get("content-type")) 230 | } 231 | 232 | func TestServeFile_NoPathToFile(t *testing.T) { 233 | // Arrange 234 | wr := httptest.NewRecorder() 235 | 236 | // Act 237 | ServeFile(wr, resources.TestContentType, "") 238 | 239 | // Assert 240 | assert.Equal(t, http.StatusNotFound, wr.Code) 241 | assert.Equal(t, "application/problem+json", wr.Header().Get("content-type")) 242 | assert.Equal(t, resources.TestErrorResponse, wr.Body.String()) 243 | } 244 | 245 | func TestServe_Router(t *testing.T) { 246 | // Arrange 247 | w := NewRouter(nil) 248 | 249 | // Act 250 | err := Serve(w) 251 | 252 | // Assert 253 | assert.Error(t, err) 254 | } 255 | 256 | func TestServe_Handler(t *testing.T) { 257 | // Arrange 258 | w := NewHandler(nil) 259 | 260 | // Act 261 | err := Serve(w) 262 | 263 | // Assert 264 | assert.Error(t, err) 265 | } 266 | 267 | func TestEmptyRoute(t *testing.T) { 268 | handler := NewHandlerWithRoute("/", nil) 269 | handler.WithMethodHandler(http.MethodGet, func(writer http.ResponseWriter, request *http.Request) { 270 | fmt.Fprintln(writer, "Don't panic! 🐿️") 271 | }) 272 | r, _ := http.NewRequest(http.MethodGet, resources.TestRoutePathParamGoodbye, nil) 273 | wr := httptest.NewRecorder() 274 | handler.ServeHTTP(wr, r) 275 | } 276 | 277 | func TestListenAndServe_Router(t *testing.T) { // Act 278 | err := ListenAndServe[*Router](resources.TestListenAndServeAddr, nil) 279 | 280 | // Assert 281 | assert.Error(t, err) 282 | } 283 | 284 | func TestListenAndServe_Handler(t *testing.T) { 285 | // Act 286 | err := ListenAndServe[*Handler](resources.TestListenAndServeAddr, nil) 287 | 288 | // Assert 289 | assert.Error(t, err) 290 | } 291 | --------------------------------------------------------------------------------