├── .gitignore ├── .travis.yml ├── CONTRIBUTE.md ├── LICENSE.txt ├── README.md ├── cache.go ├── context.go ├── context_test.go ├── errors.go ├── errors_test.go ├── examples ├── complete │ ├── main.go │ ├── main_test.go │ ├── middleware │ │ ├── hello.go │ │ └── hello_test.go │ └── resource │ │ ├── hello.go │ │ └── hello_test.go ├── customserver │ ├── main.go │ └── main_test.go ├── middleware │ ├── main.go │ ├── main_test.go │ ├── middleware.go │ ├── middleware_test.go │ ├── resource.go │ └── resource_test.go ├── panic │ ├── main.go │ └── main_test.go ├── routegroups │ ├── main.go │ ├── main_test.go │ ├── middleware.go │ ├── middleware_test.go │ ├── resource.go │ └── resource_test.go ├── simple │ ├── main.go │ └── main_test.go └── static │ ├── main.go │ └── main_test.go ├── middleware.go ├── middleware_test.go ├── resource.go ├── resource_test.go ├── router.go ├── router_test.go ├── yarf.go └── yarf_test.go /.gitignore: -------------------------------------------------------------------------------- 1 | .project 2 | .buildpath 3 | .settings 4 | *.iml 5 | .idea 6 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: go 2 | 3 | sudo: false 4 | 5 | go: 6 | - 1.5 7 | - 1.6 8 | - 1.7 9 | - 1.8 10 | - 1.9 11 | - tip 12 | 13 | before_install: 14 | - go get github.com/mattn/goveralls 15 | 16 | script: 17 | - $HOME/gopath/bin/goveralls -service=travis-ci 18 | 19 | 20 | -------------------------------------------------------------------------------- /CONTRIBUTE.md: -------------------------------------------------------------------------------- 1 | 2 | # Contribute 3 | 4 | ### Yes, please! 5 | 6 | - Use and test YARF and/or packages included. 7 | - Implement new web applications based on Yarf. 8 | - Report issues/bugs/comments/suggestions on Github. 9 | - Fork, fix/improve and send your pull requests with full descriptions of changes. 10 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) 2015 Leonel Quinteros 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without 5 | modification, are permitted provided that the following conditions 6 | are met: 7 | 1. Redistributions of source code must retain the above copyright 8 | notice, this list of conditions and the following disclaimer. 9 | 2. Redistributions in binary form must reproduce the above copyright 10 | notice, this list of conditions and the following disclaimer in the 11 | documentation and/or other materials provided with the distribution. 12 | 3. Neither the name of copyright holders nor the names of its 13 | contributors may be used to endorse or promote products derived 14 | from this software without specific prior written permission. 15 | 16 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 17 | ''AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED 18 | TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR 19 | PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL COPYRIGHT HOLDERS OR CONTRIBUTORS 20 | BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR 21 | CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF 22 | SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS 23 | INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN 24 | CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 25 | ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 26 | POSSIBILITY OF SUCH DAMAGE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![GoDoc](https://godoc.org/github.com/yarf-framework/yarf?status.svg)](https://godoc.org/github.com/yarf-framework/yarf) 2 | [![Version](https://badge.fury.io/gh/yarf-framework%2Fyarf.svg)](https://badge.fury.io/gh/yarf-framework%2Fyarf) 3 | [![Build Status](https://travis-ci.org/yarf-framework/yarf.svg?branch=master)](https://travis-ci.org/yarf-framework/yarf) 4 | [![Coverage Status](https://coveralls.io/repos/github/yarf-framework/yarf/badge.svg?branch=master)](https://coveralls.io/github/yarf-framework/yarf?branch=master) 5 | [![Go Report Card](https://goreportcard.com/badge/github.com/yarf-framework/yarf)](https://goreportcard.com/report/github.com/yarf-framework/yarf) 6 | [![Join the chat at https://gitter.im/jinzhu/gorm](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/yarf-framework/yarf) 7 | 8 | 9 | # YARF: Yet Another REST Framework 10 | 11 | YARF is a fast micro-framework designed to build REST APIs and web services in a fast and simple way. 12 | Designed after Go's composition features, takes a new approach to write simple and DRY code. 13 | 14 | 15 | ## Getting started 16 | 17 | Here's a transcription from our examples/simple package. 18 | This is a very simple Hello World web application example. 19 | 20 | 21 | ```go 22 | package main 23 | 24 | import ( 25 | "github.com/yarf-framework/yarf" 26 | ) 27 | 28 | // Define a simple resource 29 | type Hello struct { 30 | yarf.Resource 31 | } 32 | 33 | // Implement the GET method 34 | func (h *Hello) Get(c *yarf.Context) error { 35 | c.Render("Hello world!") 36 | 37 | return nil 38 | } 39 | 40 | // Run app server on http://localhost:8080 41 | func main() { 42 | y := yarf.New() 43 | 44 | y.Add("/", new(Hello)) 45 | 46 | y.Start(":8080") 47 | } 48 | 49 | ``` 50 | 51 | For more code and examples demonstrating all YARF features, please refer to the 'examples' directory. 52 | 53 | 54 | 55 | ## Features 56 | 57 | 58 | ### Struct composition based design 59 | 60 | YARF resources are custom structs that act as handlers for HTTP methods. 61 | Each resource can implement one, several or all HTTP methods needed (GET, POST, DELETE, etc.). 62 | Resources are created using Go's struct composition and you only have to declare the yarf.Resource type into your own struct. 63 | 64 | Example: 65 | 66 | ```go 67 | 68 | import "github.com/yarf-framework/yarf" 69 | 70 | // Define a simple resource 71 | type Hello struct { 72 | yarf.Resource 73 | } 74 | 75 | // Implement the GET method 76 | func (h *Hello) Get(c *yarf.Context) error { 77 | c.Render("Hello world!") 78 | 79 | return nil 80 | } 81 | 82 | ``` 83 | 84 | 85 | ### Simple router 86 | 87 | Using a strict match model, it matches exact URLs against resources for increased performance and clarity during routing. 88 | The routes supports parameters in the form '/:param'. 89 | 90 | The route: 91 | 92 | ```go 93 | /hello/:name 94 | ``` 95 | 96 | Will match: 97 | 98 | ``` 99 | /hello/Joe 100 | /hello/nobody 101 | /hello/somebody/ 102 | /hello/:name 103 | /hello/etc 104 | ``` 105 | 106 | But it won't match: 107 | 108 | ``` 109 | / 110 | /hello 111 | /hello/Joe/AndMark 112 | /Joe/:name 113 | /any/thing 114 | ``` 115 | 116 | You can define optional parameters using multiple routes on the same Resource. 117 | 118 | 119 | ### Route parameters 120 | 121 | At this point you know how to define parameters in your routes using the /:param naming convention. 122 | Now you'll see how easy is to get these parameters by their name from your resources using the Context.Param() method. 123 | 124 | Example: 125 | 126 | For the route: 127 | 128 | ``` 129 | /hello/:name 130 | ``` 131 | 132 | You can have this resource: 133 | 134 | ```go 135 | import "github.com/yarf-framework/yarf" 136 | 137 | type Hello struct { 138 | yarf.Resource 139 | } 140 | 141 | func (h *Hello) Get(c *yarf.Context) error { 142 | name := c.Param("name") 143 | 144 | c.Render("Hello, " + name) 145 | 146 | return nil 147 | } 148 | 149 | ``` 150 | 151 | 152 | ### Route wildcards 153 | 154 | When some extra freedom is needed on your routes, you can use a `*` as part of your routes to match anything where the wildcard is present. 155 | 156 | The route: 157 | 158 | ``` 159 | /something/*/here 160 | ``` 161 | 162 | Will match the routes 163 | 164 | ``` 165 | /something/is/here 166 | /something/happen/here 167 | /something/isnt/here 168 | /something/was/here 169 | ``` 170 | 171 | And so on... 172 | 173 | You can also combine this with parameters inside the routes for extra complexity. 174 | 175 | 176 | ### Catch-All wildcard 177 | 178 | When using the `*` at the end of any route, the router will match everything from the wildcard and forward. 179 | 180 | The route: 181 | 182 | ``` 183 | /match/from/here/* 184 | ``` 185 | 186 | Will match: 187 | 188 | ``` 189 | /match/from/here 190 | /match/from/here/please 191 | /match/from/here/and/forward 192 | /match/from/here/and/forward/for/ever/and/ever 193 | ``` 194 | 195 | And so on... 196 | 197 | 198 | #### Note about the wildcard 199 | 200 | The `*` can only be used by itself and it doesn't works for single character matching like in regex. 201 | 202 | So the route: 203 | 204 | ``` 205 | /match/some* 206 | ``` 207 | 208 | Will **NOT** match: 209 | 210 | ``` 211 | /match/some 212 | /match/something 213 | /match/someone 214 | /match/some/please 215 | ``` 216 | 217 | 218 | ### Context 219 | 220 | The Context object is passed as a parameter to all Resource methods and contains all the information related to the ongoing request. 221 | 222 | Check the Context docs for a reference of the object: [https://godoc.org/github.com/yarf-framework/yarf#Context](https://godoc.org/github.com/yarf-framework/yarf#Context) 223 | 224 | 225 | 226 | ### Middleware support 227 | 228 | Middleware support is implemented in a similar way as Resources, by using composition. 229 | Routes will be pre-filtered and post-filtered by Middleware handlers when they're inserted in the router. 230 | 231 | Example: 232 | 233 | ```go 234 | import "github.com/yarf-framework/yarf" 235 | 236 | // Define your middleware and composite yarf.Middleware 237 | type HelloMiddleware struct { 238 | yarf.Middleware 239 | } 240 | 241 | // Implement only the PreDispatch method, PostDispatch not needed. 242 | func (m *HelloMiddleware) PreDispatch(c *yarf.Context) error { 243 | c.Render("Hello from middleware! \n") // Render to response. 244 | 245 | return nil 246 | } 247 | 248 | // Insert your middlewares to the server 249 | func main() { 250 | y := yarf.New() 251 | 252 | // Add middleware 253 | y.Insert(new(HelloMiddleware)) 254 | 255 | // Define routes 256 | // ... 257 | // ... 258 | 259 | // Start the server 260 | y.Start() 261 | } 262 | ``` 263 | 264 | 265 | ### Route groups 266 | 267 | Routes can be grouped into a route prefix and handle their own middleware. 268 | 269 | 270 | ### Nested groups 271 | 272 | As routes can be grouped into a route prefix, other groups can be also grouped allowing for nested prefixes and middleware layers. 273 | 274 | Example: 275 | 276 | ```go 277 | import "github.com/yarf-framework/yarf" 278 | 279 | // Entry point of the executable application 280 | // It runs a default server listening on http://localhost:8080 281 | // 282 | // URLs after configuration: 283 | // http://localhost:8080 284 | // http://localhost:8080/hello/:name 285 | // http://localhost:8080/v2 286 | // http://localhost:8080/v2/hello/:name 287 | // http://localhost:8080/extra/v2 288 | // http://localhost:8080/extra/v2/hello/:name 289 | // 290 | func main() { 291 | // Create a new empty YARF server 292 | y := yarf.New() 293 | 294 | // Create resources 295 | hello := new(Hello) 296 | hellov2 := new(HelloV2) 297 | 298 | // Add main resource to multiple routes at root level. 299 | y.Add("/", hello) 300 | y.Add("/hello/:name", hello) 301 | 302 | // Create /v2 prefix route group 303 | g := yarf.RouteGroup("/v2") 304 | 305 | // Add /v2/ routes to the group 306 | g.Add("/", hellov2) 307 | g.Add("/hello/:name", hellov2) 308 | 309 | // Use middleware only on the /v2/ group 310 | g.Insert(new(HelloMiddleware)) 311 | 312 | // Add group to Yarf routes 313 | y.AddGroup(g) 314 | 315 | // Create another group for nesting into it. 316 | n := yarf.RouteGroup("/extra") 317 | 318 | // Nest /v2 group into /extra/v2 319 | n.AddGroup(g) 320 | 321 | // Use another middleware only for this /extra/v2 group 322 | n.Insert(new(ExtraMiddleware)) 323 | 324 | // Add group to Yarf 325 | y.AddGroup(n) 326 | 327 | // Start server listening on port 8080 328 | y.Start(":8080") 329 | } 330 | ``` 331 | 332 | Check the ./examples/routegroups demo for the complete working implementation. 333 | 334 | 335 | ### Route caching 336 | 337 | A route cache is enabled by default to improve dispatch speed, but sacrificing memory space. 338 | If you're running out of RAM memory and/or your app has too many possible routes that may not fit, you should disable the route cache. 339 | 340 | To enable/disable the route cache, just set the UseCache flag of the Yarf object: 341 | 342 | ```go 343 | y := yarf.New() 344 | y.UseCache = false 345 | ``` 346 | 347 | 348 | ### Chain and extend 349 | 350 | Just use the Yarf object as any http.Handler on a chain. 351 | Set another http.Handler on the Yarf.Follow property to be followed in case this Yarf router can't match the request. 352 | 353 | Here's an example on how to follow the request to a public file server: 354 | 355 | ```go 356 | package main 357 | 358 | import ( 359 | "github.com/yarf-framework/yarf" 360 | "net/http" 361 | ) 362 | 363 | func main() { 364 | y := yarf.New() 365 | 366 | // Add some routes 367 | y.Add("/hello/:name", new(Hello)) 368 | 369 | //... more routes here 370 | 371 | // Follow to file server 372 | y.Follow = http.FileServer(http.Dir("/var/www/public")) 373 | 374 | // Start the server 375 | y.Start(":8080") 376 | } 377 | ``` 378 | 379 | 380 | ### Custom NotFound error handler 381 | 382 | You can handle all 404 errors returned by any resource/middleware during the request flow of a Yarf server. 383 | To do so, you only have to implement a function with the func(c *yarf.Context) signature and set it to your server's Yarf.NotFound property. 384 | 385 | ```go 386 | y := yarf.New() 387 | 388 | // ... 389 | 390 | y.NotFound = func(c *yarf.Context) { 391 | c.Render("This is a custom Not Found handler") 392 | } 393 | 394 | // ... 395 | ``` 396 | 397 | 398 | ## Performance 399 | 400 | On initial benchmarks, the framework seems to perform very well compared with other similar frameworks. 401 | Even when there are faster frameworks, under high load conditions and thanks to the route caching method, 402 | YARF seems to perform as good or even better than the fastests that work better under simpler conditions. 403 | 404 | Check the benchmarks repository to run your own: 405 | 406 | [https://github.com/yarf-framework/benchmarks](https://github.com/yarf-framework/benchmarks) 407 | 408 | 409 | 410 | ## HTTPS support 411 | 412 | Support for running HTTPS server from the net/http package. 413 | 414 | ### Using the default server 415 | 416 | ```go 417 | func main() { 418 | y := yarf.New() 419 | 420 | // Setup the app 421 | // ... 422 | // ... 423 | 424 | // Start https listening on port 443 425 | y.StartTLS(":443", certFile, keyFile) 426 | } 427 | 428 | ``` 429 | 430 | ### Using a custom server 431 | 432 | ```go 433 | func main() { 434 | y := yarf.New() 435 | 436 | // Setup the app 437 | // ... 438 | // ... 439 | 440 | // Configure custom http server and set the yarf object as Handler. 441 | s := &http.Server{ 442 | Addr: ":443", 443 | Handler: y, 444 | ReadTimeout: 10 * time.Second, 445 | WriteTimeout: 10 * time.Second, 446 | MaxHeaderBytes: 1 << 20, 447 | } 448 | s.ListenAndServeTLS(certFile, keyFile) 449 | } 450 | 451 | ``` 452 | 453 | 454 | ## Why another micro-framework? 455 | 456 | Why not? 457 | 458 | No, seriously, i've researched for small/fast frameworks in the past for a Go project that I was starting. 459 | I found several options, but at the same time, none seemed to suit me. 460 | Some of them make you write weird function wrappers to fit the net/http package style. 461 | Actually, most of them seem to be function-based handlers. 462 | While that's not wrong, I feel more comfortable with the resource-based design, and this I also feel aligns better with the spirit of REST. 463 | 464 | In Yarf you create a resource struct that represents a REST resource and it has all HTTP methods available. 465 | No need to create different routes for GET/POST/DELETE/etc. methods. 466 | 467 | By using composition, you don't need to wrap functions inside functions over and over again to implement simple things like middleware or extension to your methods. 468 | You can abuse composition to create a huge OO-like design for your business model without sacrifying performance and code readability. 469 | 470 | Even while the code style differs from the net/http package, the framework is fully compatible with it and couldn't run without it. 471 | Extensions and utilities from other frameworks or even the net/http package can be easily implemented into Yarf by just wraping them up into a Resource, 472 | just as you would do on any other framework by wrapping functions. 473 | 474 | Context handling also shows some weird designs across frameworks. Some of them rely on reflection to receive any kind of handlers and context types. 475 | Others make you receive some extra parameter in the handler function that actually brokes the net/http compatibility, and you have to carry that context parameter through all middleware/handler-wrapper functions just to make it available. 476 | In Yarf, the Context is automatically sent as a parameter to all Resource methods by the framework. 477 | 478 | For all the reasons above, among some others, there is a new framework in town. 479 | -------------------------------------------------------------------------------- /cache.go: -------------------------------------------------------------------------------- 1 | package yarf 2 | 3 | import ( 4 | "sync" 5 | ) 6 | 7 | // RouteCache stores previously matched and parsed routes 8 | type RouteCache struct { 9 | route []Router 10 | params Params 11 | } 12 | 13 | // Cache is the service handler for route caching 14 | type Cache struct { 15 | // Cache data storage 16 | storage map[string]RouteCache 17 | 18 | // Sync Mutex 19 | sync.RWMutex 20 | } 21 | 22 | // NewCache creates and initializes a new Cache service object. 23 | func NewCache() *Cache { 24 | return &Cache{ 25 | storage: make(map[string]RouteCache), 26 | } 27 | } 28 | 29 | // Get retrieves a routeCache object by key name. 30 | func (c *Cache) Get(k string) (rc RouteCache, ok bool) { 31 | c.RLock() 32 | defer c.RUnlock() 33 | 34 | rc, ok = c.storage[k] 35 | return 36 | } 37 | 38 | // Set stores a routeCache object under a key name. 39 | func (c *Cache) Set(k string, r RouteCache) { 40 | c.Lock() 41 | defer c.Unlock() 42 | 43 | c.storage[k] = r 44 | } 45 | -------------------------------------------------------------------------------- /context.go: -------------------------------------------------------------------------------- 1 | package yarf 2 | 3 | import ( 4 | "compress/gzip" 5 | "encoding/json" 6 | "encoding/xml" 7 | "net/http" 8 | "strings" 9 | ) 10 | 11 | // ContextData interface represents a common get/set/del set of methods to handle data storage. 12 | // Is designed to be used as the Data property of the Context obejct. 13 | // The Data property is a free storage unit that apps using the framework can implement to their convenience 14 | // to share context data during a request life. 15 | // All methods returns an error status that different implementations can design to fulfill their needs. 16 | type ContextData interface { 17 | // Get retrieves a data item by it's key name. 18 | Get(key string) (interface{}, error) 19 | 20 | // Set saves a data item under a key name. 21 | Set(key string, data interface{}) error 22 | 23 | // Del removes the data item and key name for a given key. 24 | Del(key string) error 25 | } 26 | 27 | // Params wraps a map[string]string and adds Get/Set/Del methods to work with it. 28 | // Inspired on url.Values but simpler as it doesn't handles a map[string][]string 29 | type Params map[string]string 30 | 31 | // Get gets the first value associated with the given key. 32 | // If there are no values associated with the key, Get returns 33 | // the empty string. 34 | func (p Params) Get(key string) string { 35 | if p == nil { 36 | return "" 37 | } 38 | 39 | param, _ := p[key] 40 | return param 41 | } 42 | 43 | // Set sets the key to value. It replaces any existing values. 44 | func (p Params) Set(key, value string) { 45 | p[key] = value 46 | } 47 | 48 | // Del deletes the values associated with key. 49 | func (p Params) Del(key string) { 50 | delete(p, key) 51 | } 52 | 53 | // Context is the data/status storage of every YARF request. 54 | // Every request will instantiate a new Context object and fill in with all the request data. 55 | // Each request Context will be shared along the entire request life to ensure accesibility of its data at all levels. 56 | type Context struct { 57 | // The *http.Request object as received by the HandleFunc. 58 | Request *http.Request 59 | 60 | // The http.ResponseWriter object as received by the HandleFunc. 61 | Response http.ResponseWriter 62 | 63 | // Parameters received through URL route 64 | Params Params 65 | 66 | // Free storage to be used freely by apps to their convenience. 67 | Data ContextData 68 | 69 | // Group route storage for dispatch 70 | groupDispatch []Router 71 | } 72 | 73 | // NewContext creates a new *Context object with default values and returns it. 74 | func NewContext(r *http.Request, rw http.ResponseWriter) *Context { 75 | return &Context{ 76 | Request: r, 77 | Response: rw, 78 | Params: Params{}, 79 | } 80 | } 81 | 82 | // Status sets the HTTP status code to be returned on the response. 83 | func (c *Context) Status(code int) { 84 | c.Response.WriteHeader(code) 85 | } 86 | 87 | // Param is a wrapper for c.Params.Get() 88 | func (c *Context) Param(name string) string { 89 | return c.Params.Get(name) 90 | } 91 | 92 | // FormValue is a wrapper for c.Request.Form.Get() and it calls c.Request.ParseForm(). 93 | func (c *Context) FormValue(name string) string { 94 | c.Request.ParseForm() 95 | 96 | return c.Request.Form.Get(name) 97 | } 98 | 99 | // QueryValue is a wrapper for c.Request.URL.Query().Get(). 100 | func (c *Context) QueryValue(name string) string { 101 | return c.Request.URL.Query().Get(name) 102 | } 103 | 104 | // GetClientIP retrieves the client IP address from the request information. 105 | // It detects common proxy headers to return the actual client's IP and not the proxy's. 106 | func (c *Context) GetClientIP() (ip string) { 107 | var pIPs string 108 | var pIPList []string 109 | 110 | if pIPs = c.Request.Header.Get("X-Real-Ip"); pIPs != "" { 111 | pIPList = strings.Split(pIPs, ",") 112 | ip = strings.TrimSpace(pIPList[0]) 113 | 114 | } else if pIPs = c.Request.Header.Get("Real-Ip"); pIPs != "" { 115 | pIPList = strings.Split(pIPs, ",") 116 | ip = strings.TrimSpace(pIPList[0]) 117 | 118 | } else if pIPs = c.Request.Header.Get("X-Forwarded-For"); pIPs != "" { 119 | pIPList = strings.Split(pIPs, ",") 120 | ip = strings.TrimSpace(pIPList[0]) 121 | 122 | } else if pIPs = c.Request.Header.Get("X-Forwarded"); pIPs != "" { 123 | pIPList = strings.Split(pIPs, ",") 124 | ip = strings.TrimSpace(pIPList[0]) 125 | 126 | } else if pIPs = c.Request.Header.Get("Forwarded-For"); pIPs != "" { 127 | pIPList = strings.Split(pIPs, ",") 128 | ip = strings.TrimSpace(pIPList[0]) 129 | 130 | } else if pIPs = c.Request.Header.Get("Forwarded"); pIPs != "" { 131 | pIPList = strings.Split(pIPs, ",") 132 | ip = strings.TrimSpace(pIPList[0]) 133 | 134 | } else { 135 | ip = c.Request.RemoteAddr 136 | } 137 | 138 | return strings.Split(ip, ":")[0] 139 | } 140 | 141 | // Redirect sends the corresponding HTTP redirect response with the provided URL and status code. 142 | // It's just a wrapper for net/http.Redirect() 143 | func (c *Context) Redirect(url string, code int) { 144 | http.Redirect(c.Response, c.Request, url, code) 145 | } 146 | 147 | // Render writes a string to the http.ResponseWriter. 148 | // This is the default renderer that just sends the string to the client. 149 | // Check other Render[Type] functions for different types. 150 | func (c *Context) Render(content string) { 151 | // Write response 152 | c.Response.Write([]byte(content)) 153 | } 154 | 155 | // RenderGzip takes a []byte content and if the client accepts compressed responses, 156 | // writes the compressed version of the content to the response. 157 | // Otherwise it just writes the plain []byte to it. 158 | func (c *Context) RenderGzip(content []byte) error { 159 | // Check if client accepts compression 160 | if !strings.Contains(c.Request.Header.Get("Accept-Encoding"), "gzip") { 161 | c.Response.Write(content) 162 | return nil 163 | } 164 | 165 | // Detect content type 166 | c.Response.Header().Set("Content-Type", http.DetectContentType(content)) 167 | 168 | // Write compressed content 169 | gz := gzip.NewWriter(c.Response) 170 | defer gz.Close() 171 | 172 | c.Response.Header().Set("Content-Encoding", "gzip") 173 | c.Response.Header().Set("Vary", "Accept-Encoding") 174 | c.Response.Header().Del("Content-Length") 175 | 176 | gz.Write(content) 177 | 178 | return nil 179 | } 180 | 181 | // RenderJSON takes a interface{} object and writes the JSON encoded string of it. 182 | func (c *Context) RenderJSON(data interface{}) { 183 | // Set content 184 | encoded, err := json.Marshal(data) 185 | if err != nil { 186 | c.Response.Write([]byte(err.Error())) 187 | } else { 188 | c.Response.Write(encoded) 189 | } 190 | } 191 | 192 | // RenderJSONIndent is the indented (beauty) of RenderJSON 193 | func (c *Context) RenderJSONIndent(data interface{}) { 194 | // Set content 195 | encoded, err := json.MarshalIndent(data, "", " ") 196 | if err != nil { 197 | c.Response.Write([]byte(err.Error())) 198 | } else { 199 | c.Response.Write(encoded) 200 | } 201 | } 202 | 203 | // RenderGzipJSON takes a interface{} object and writes the JSON verion through RenderGzip. 204 | func (c *Context) RenderGzipJSON(data interface{}) { 205 | // Create JSON content 206 | encoded, err := json.Marshal(data) 207 | if err != nil { 208 | c.Response.Write([]byte(err.Error())) 209 | } 210 | 211 | c.RenderGzip(encoded) 212 | } 213 | 214 | // RenderXML takes a interface{} object and writes the XML encoded string of it. 215 | func (c *Context) RenderXML(data interface{}) { 216 | // Set content 217 | encoded, err := xml.Marshal(data) 218 | if err != nil { 219 | c.Response.Write([]byte(err.Error())) 220 | } else { 221 | c.Response.Write(encoded) 222 | } 223 | } 224 | 225 | // RenderXMLIndent is the indented (beauty) of RenderXML 226 | func (c *Context) RenderXMLIndent(data interface{}) { 227 | // Set content 228 | encoded, err := xml.MarshalIndent(data, "", " ") 229 | if err != nil { 230 | c.Response.Write([]byte(err.Error())) 231 | } else { 232 | c.Response.Write(encoded) 233 | } 234 | } 235 | 236 | // RenderGzipXML takes a interface{} object and writes the XML verion through RenderGzip. 237 | func (c *Context) RenderGzipXML(data interface{}) { 238 | // Set XML content 239 | encoded, err := xml.Marshal(data) 240 | if err != nil { 241 | c.Response.Write([]byte(err.Error())) 242 | } 243 | 244 | c.RenderGzip(encoded) 245 | } 246 | -------------------------------------------------------------------------------- /context_test.go: -------------------------------------------------------------------------------- 1 | package yarf 2 | 3 | import ( 4 | "net/http" 5 | "net/http/httptest" 6 | "testing" 7 | ) 8 | 9 | func createRequestResponse() (request *http.Request, response *httptest.ResponseRecorder) { 10 | // Create a dummy request. 11 | request, _ = http.NewRequest( 12 | "GET", 13 | "http://127.0.0.1:8080/", 14 | nil, 15 | ) 16 | 17 | request.RemoteAddr = "200.201.202.203" 18 | request.Header.Set("User-Agent", "yarf/1.0") 19 | request.Header.Set("Content-Type", "application/x-www-form-urlencoded") 20 | 21 | response = httptest.NewRecorder() 22 | 23 | return 24 | } 25 | 26 | func TestNewContext(t *testing.T) { 27 | req, res := createRequestResponse() 28 | 29 | c := NewContext(req, res) 30 | 31 | if c.Request != req { 32 | t.Error("Request object provided to NewContext() wasn't set correctly on Context object") 33 | } 34 | if c.Response != res { 35 | t.Error("Response object provided to NewContext() wasn't set correctly on Context object") 36 | } 37 | } 38 | 39 | func TestStatus(t *testing.T) { 40 | req, res := createRequestResponse() 41 | 42 | c := NewContext(req, res) 43 | c.Status(201) 44 | 45 | if c.Response.(*httptest.ResponseRecorder).Code != 201 { 46 | t.Errorf("Status %d set to Status() method, %d found", 201, c.Response.(*httptest.ResponseRecorder).Code) 47 | } 48 | } 49 | 50 | func TestParam(t *testing.T) { 51 | req, res := createRequestResponse() 52 | 53 | c := NewContext(req, res) 54 | c.Params.Set("name", "Joe") 55 | 56 | if c.Param("name") != "Joe" { 57 | t.Errorf("Param 'name' set to '%s', '%s' retrieved.", "Joe", c.Param("name")) 58 | } 59 | } 60 | 61 | func TestGetClientIP(t *testing.T) { 62 | req, res := createRequestResponse() 63 | 64 | c := NewContext(req, res) 65 | 66 | if c.GetClientIP() != req.RemoteAddr { 67 | t.Errorf("IP %s set to request, %s retrieved by GetClientIP()", req.RemoteAddr, c.GetClientIP()) 68 | } 69 | } 70 | 71 | func TestRender(t *testing.T) { 72 | req, res := createRequestResponse() 73 | 74 | c := NewContext(req, res) 75 | c.Render("TEST") 76 | 77 | if c.Response.(*httptest.ResponseRecorder).Body.String() != "TEST" { 78 | t.Errorf("'%s' sent to Render() method, '%s' found on Response object", "TEST", c.Response.(*httptest.ResponseRecorder).Body.String()) 79 | } 80 | } 81 | 82 | func TestRenderJSON(t *testing.T) { 83 | req, res := createRequestResponse() 84 | 85 | c := NewContext(req, res) 86 | c.RenderJSON("TEST") 87 | 88 | if c.Response.(*httptest.ResponseRecorder).Body.String() != "\"TEST\"" { 89 | t.Errorf("'%s' sent to RenderJSON() method, '%s' found on Response object", "TEST", c.Response.(*httptest.ResponseRecorder).Body.String()) 90 | } 91 | } 92 | 93 | func TestRenderJSONIndent(t *testing.T) { 94 | req, res := createRequestResponse() 95 | 96 | c := NewContext(req, res) 97 | c.RenderJSONIndent("TEST") 98 | 99 | if c.Response.(*httptest.ResponseRecorder).Body.String() != "\"TEST\"" { 100 | t.Errorf("'%s' sent to RenderJSONIndent() method, '%s' found on Response object", "TEST", c.Response.(*httptest.ResponseRecorder).Body.String()) 101 | } 102 | } 103 | 104 | func TestRenderXML(t *testing.T) { 105 | req, res := createRequestResponse() 106 | 107 | c := NewContext(req, res) 108 | c.RenderXML("TEST") 109 | 110 | if c.Response.(*httptest.ResponseRecorder).Body.String() != "TEST" { 111 | t.Errorf("'%s' sent to RenderXML() method, '%s' found on Response object", "TEST", c.Response.(*httptest.ResponseRecorder).Body.String()) 112 | } 113 | } 114 | 115 | func TestRenderXMLIndent(t *testing.T) { 116 | req, res := createRequestResponse() 117 | 118 | c := NewContext(req, res) 119 | c.RenderXMLIndent("TEST") 120 | 121 | if c.Response.(*httptest.ResponseRecorder).Body.String() != "TEST" { 122 | t.Errorf("'%s' sent to RenderXMLIndent() method, '%s' found on Response object", "TEST", c.Response.(*httptest.ResponseRecorder).Body.String()) 123 | } 124 | } 125 | -------------------------------------------------------------------------------- /errors.go: -------------------------------------------------------------------------------- 1 | package yarf 2 | 3 | import ( 4 | "net/http" 5 | ) 6 | 7 | // YError is the interface used to handle error responses inside the framework. 8 | type YError interface { 9 | Code() int // HTTP response code for this error 10 | ID() int // Error code ID. 11 | Msg() string // Error description 12 | Body() string // Error body content to be returned to the client if needed. 13 | } 14 | 15 | // CustomError is the standard error response format used through the framework. 16 | // Implements Error and YError interfaces 17 | type CustomError struct { 18 | HTTPCode int // HTTP status code to be used as this error response. 19 | ErrorCode int // Internal YARF error code for further reference. 20 | ErrorMsg string // YARF error message. 21 | ErrorBody string // Error content to be rendered to the client response. 22 | } 23 | 24 | // Implements the error interface returning the ErrorMsg value of each error. 25 | func (e *CustomError) Error() string { 26 | return e.ErrorMsg 27 | } 28 | 29 | // Code returns the error's HTTP code to be used in the response. 30 | func (e *CustomError) Code() int { 31 | return e.HTTPCode 32 | } 33 | 34 | // ID returns the error's ID for further reference. 35 | func (e *CustomError) ID() int { 36 | return e.ErrorCode 37 | } 38 | 39 | // Msg returns the error's message, used to implement the Error interface. 40 | func (e *CustomError) Msg() string { 41 | return e.ErrorMsg 42 | } 43 | 44 | // Body returns the error's content body, if needed, to be returned in the HTTP response. 45 | func (e *CustomError) Body() string { 46 | return e.ErrorBody 47 | } 48 | 49 | // UnexpectedError is used when the origin of the error can't be discovered 50 | type UnexpectedError struct { 51 | CustomError 52 | } 53 | 54 | // ErrorUnexpected creates UnexpectedError 55 | func ErrorUnexpected() *UnexpectedError { 56 | e := new(UnexpectedError) 57 | e.HTTPCode = http.StatusInternalServerError 58 | e.ErrorCode = 0 59 | e.ErrorMsg = "Unexpected error" 60 | 61 | return e 62 | } 63 | 64 | // MethodNotImplementedError is used to communicate that a specific HTTP method isn't implemented by a resource. 65 | type MethodNotImplementedError struct { 66 | CustomError 67 | } 68 | 69 | // ErrorMethodNotImplemented creates MethodNotImplementedError 70 | func ErrorMethodNotImplemented() *MethodNotImplementedError { 71 | e := new(MethodNotImplementedError) 72 | e.HTTPCode = http.StatusMethodNotAllowed 73 | e.ErrorCode = 1 74 | e.ErrorMsg = "Method not implemented" 75 | 76 | return e 77 | } 78 | 79 | // NotFoundError is the HTTP 404 error equivalent. 80 | type NotFoundError struct { 81 | CustomError 82 | } 83 | 84 | // ErrorNotFound creates NotFoundError 85 | func ErrorNotFound() *NotFoundError { 86 | e := new(NotFoundError) 87 | e.HTTPCode = http.StatusNotFound 88 | e.ErrorCode = 2 89 | e.ErrorMsg = "Not found" 90 | 91 | return e 92 | } 93 | -------------------------------------------------------------------------------- /errors_test.go: -------------------------------------------------------------------------------- 1 | package yarf 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | func TestErrors(t *testing.T) { 8 | var e YError 9 | 10 | e = ErrorUnexpected() 11 | if e == nil { 12 | t.Error("ErrorUnexpected() should return an object. Nil value returned.") 13 | } 14 | 15 | e = ErrorMethodNotImplemented() 16 | if e == nil { 17 | t.Error("ErrorMethodNotImplemented() should return an object. Nil value returned.") 18 | } 19 | 20 | e = ErrorNotFound() 21 | if e == nil { 22 | t.Error("ErrorNotFound() should return an object. Nil value returned.") 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /examples/complete/main.go: -------------------------------------------------------------------------------- 1 | // The complete example implements all possible features from YARF. 2 | package main 3 | 4 | import ( 5 | "github.com/yarf-framework/extras/logger" 6 | "github.com/yarf-framework/yarf" 7 | "github.com/yarf-framework/yarf/examples/complete/middleware" 8 | "github.com/yarf-framework/yarf/examples/complete/resource" 9 | ) 10 | 11 | func main() { 12 | // Create a new empty YARF server 13 | y := yarf.New() 14 | 15 | // Create resource 16 | hello := new(resource.Hello) 17 | 18 | // Add resource to multiple routes 19 | y.Add("/", hello) 20 | y.Add("/hello/:name", hello) 21 | 22 | // Create /extra route group 23 | e := yarf.RouteGroup("/extra") 24 | 25 | // Add custom middleware to /extra 26 | e.Insert(new(middleware.Hello)) 27 | 28 | // Add same routes to /extra group 29 | e.Add("/", hello) 30 | e.Add("/hello/:name", hello) 31 | 32 | // Save group 33 | y.AddGroup(e) 34 | 35 | // Add logger middleware at the end of the chain 36 | y.Insert(new(logger.Logger)) 37 | 38 | // Start server listening on port 8088 39 | y.Start(":8080") 40 | } 41 | -------------------------------------------------------------------------------- /examples/complete/main_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | func TestMain(t *testing.T) { 8 | // Run main method in a goroutine to make sure it runs. 9 | // Then let it die and just capture panics. 10 | go func() { 11 | defer func() { 12 | if r := recover(); r != nil { 13 | t.Errorf("PANIC: %s", r) 14 | } 15 | }() 16 | 17 | main() 18 | }() 19 | } 20 | -------------------------------------------------------------------------------- /examples/complete/middleware/hello.go: -------------------------------------------------------------------------------- 1 | package middleware 2 | 3 | import ( 4 | "github.com/yarf-framework/yarf" 5 | ) 6 | 7 | // Hello composites yarf.Middleware 8 | type Hello struct { 9 | yarf.Middleware 10 | } 11 | 12 | // PreDispatch renders a hardcoded string 13 | func (m *Hello) PreDispatch(c *yarf.Context) error { 14 | c.Render("Hello from middleware! \n\n") 15 | 16 | return nil 17 | } 18 | 19 | // PostDispatch renders a hardcoded string 20 | func (m *Hello) PostDispatch(c *yarf.Context) error { 21 | c.Render("\n\nGoodbye from middleware!") 22 | 23 | return nil 24 | } 25 | -------------------------------------------------------------------------------- /examples/complete/middleware/hello_test.go: -------------------------------------------------------------------------------- 1 | package middleware 2 | 3 | import ( 4 | "github.com/yarf-framework/yarf" 5 | "net/http" 6 | "net/http/httptest" 7 | "testing" 8 | ) 9 | 10 | func TestHello(t *testing.T) { 11 | h := new(Hello) 12 | 13 | c := new(yarf.Context) 14 | c.Request, _ = http.NewRequest("GET", "/", nil) 15 | c.Response = httptest.NewRecorder() 16 | 17 | err := h.PreDispatch(c) 18 | if err != nil { 19 | t.Error(err.Error()) 20 | } 21 | 22 | err = h.PostDispatch(c) 23 | if err != nil { 24 | t.Error(err.Error()) 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /examples/complete/resource/hello.go: -------------------------------------------------------------------------------- 1 | package resource 2 | 3 | import ( 4 | "github.com/yarf-framework/yarf" 5 | ) 6 | 7 | // Hello composites yarf.Resource 8 | type Hello struct { 9 | yarf.Resource 10 | } 11 | 12 | // Get implements the GET handler with optional name parameter 13 | func (h *Hello) Get(c *yarf.Context) error { 14 | name := c.Param("name") 15 | 16 | salute := "Hello" 17 | if name != "" { 18 | salute += ", " + name 19 | } 20 | salute += "!" 21 | 22 | c.Render(salute) 23 | 24 | return nil 25 | } 26 | -------------------------------------------------------------------------------- /examples/complete/resource/hello_test.go: -------------------------------------------------------------------------------- 1 | package resource 2 | 3 | import ( 4 | "github.com/yarf-framework/yarf" 5 | "net/http" 6 | "net/http/httptest" 7 | "testing" 8 | ) 9 | 10 | func TestHello(t *testing.T) { 11 | h := new(Hello) 12 | 13 | c := new(yarf.Context) 14 | c.Request, _ = http.NewRequest("GET", "/", nil) 15 | c.Response = httptest.NewRecorder() 16 | 17 | err := h.Get(c) 18 | if err != nil { 19 | t.Error(err.Error()) 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /examples/customserver/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/yarf-framework/yarf" 5 | "net/http" 6 | "time" 7 | ) 8 | 9 | // Hello defines a simple resource 10 | type Hello struct { 11 | yarf.Resource // Extend the yarf.Resource by composition 12 | } 13 | 14 | // Get implements the GET handler 15 | func (h *Hello) Get(c *yarf.Context) error { 16 | c.Render("Hello world!") 17 | 18 | return nil 19 | } 20 | 21 | // Entry point of the executable application 22 | // This time we setup a custom Go http server and use YARF as a router. 23 | func main() { 24 | // Create a new empty YARF server 25 | y := yarf.New() 26 | 27 | // Add route/resource 28 | y.Add("/", new(Hello)) 29 | 30 | // Configure custom http server and set the yarf object as Handler. 31 | s := &http.Server{ 32 | Addr: ":8080", 33 | Handler: y, 34 | ReadTimeout: 10 * time.Second, 35 | WriteTimeout: 10 * time.Second, 36 | MaxHeaderBytes: 1 << 20, 37 | } 38 | s.ListenAndServe() 39 | } 40 | -------------------------------------------------------------------------------- /examples/customserver/main_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | func TestMain(t *testing.T) { 8 | // Run main method in a goroutine to make sure it runs. 9 | // Then let it die and just capture panics. 10 | go func() { 11 | defer func() { 12 | if r := recover(); r != nil { 13 | t.Errorf("PANIC: %s", r) 14 | } 15 | }() 16 | 17 | main() 18 | }() 19 | } 20 | -------------------------------------------------------------------------------- /examples/middleware/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/yarf-framework/yarf" 5 | ) 6 | 7 | // Entry point of the executable application 8 | // It runs a default server listening on http://localhost:8080 9 | func main() { 10 | // Create a new empty YARF server 11 | y := yarf.New() 12 | 13 | // Add middleware 14 | y.Insert(new(HelloMiddleware)) 15 | 16 | // Create resource 17 | hello := new(Hello) 18 | 19 | // Add resource to multiple routes 20 | y.Add("/", hello) 21 | y.Add("/hello/:name", hello) 22 | 23 | // Start server listening on port 8080 24 | y.Start(":8080") 25 | } 26 | -------------------------------------------------------------------------------- /examples/middleware/main_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | func TestMain(t *testing.T) { 8 | // Run main method in a goroutine to make sure it runs. 9 | // Then let it die and just capture panics. 10 | go func() { 11 | defer func() { 12 | if r := recover(); r != nil { 13 | t.Errorf("PANIC: %s", r) 14 | } 15 | }() 16 | 17 | main() 18 | }() 19 | } 20 | -------------------------------------------------------------------------------- /examples/middleware/middleware.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/yarf-framework/yarf" 5 | ) 6 | 7 | // HelloMiddleware is compositing yarf.Middleware 8 | type HelloMiddleware struct { 9 | yarf.Middleware 10 | } 11 | 12 | // PreDispatch renders a hardcoded string 13 | func (m *HelloMiddleware) PreDispatch(c *yarf.Context) error { 14 | c.Render("Hello from middleware! \n\n") 15 | 16 | return nil 17 | } 18 | 19 | // PostDispatch includes code to be executed after every Resource request. 20 | func (m *HelloMiddleware) PostDispatch(c *yarf.Context) error { 21 | c.Render("\n\nGoodbye from middleware!") 22 | 23 | return nil 24 | } 25 | -------------------------------------------------------------------------------- /examples/middleware/middleware_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/yarf-framework/yarf" 5 | "net/http" 6 | "net/http/httptest" 7 | "testing" 8 | ) 9 | 10 | func TestHello(t *testing.T) { 11 | h := new(HelloMiddleware) 12 | 13 | c := new(yarf.Context) 14 | c.Request, _ = http.NewRequest("GET", "/", nil) 15 | c.Response = httptest.NewRecorder() 16 | 17 | err := h.PreDispatch(c) 18 | if err != nil { 19 | t.Error(err.Error()) 20 | } 21 | 22 | err = h.PostDispatch(c) 23 | if err != nil { 24 | t.Error(err.Error()) 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /examples/middleware/resource.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/yarf-framework/yarf" 5 | ) 6 | 7 | // Hello composites yarf.Resource 8 | type Hello struct { 9 | yarf.Resource 10 | } 11 | 12 | // Get implements the GET handler with optional name parameter 13 | func (h *Hello) Get(c *yarf.Context) error { 14 | name := c.Param("name") 15 | 16 | salute := "Hello" 17 | if name != "" { 18 | salute += ", " + name 19 | } 20 | salute += "!" 21 | 22 | c.Render(salute) 23 | 24 | return nil 25 | } 26 | -------------------------------------------------------------------------------- /examples/middleware/resource_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/yarf-framework/yarf" 5 | "net/http" 6 | "net/http/httptest" 7 | "testing" 8 | ) 9 | 10 | func TestResourceHello(t *testing.T) { 11 | h := new(Hello) 12 | 13 | c := new(yarf.Context) 14 | c.Request, _ = http.NewRequest("GET", "/", nil) 15 | c.Response = httptest.NewRecorder() 16 | 17 | err := h.Get(c) 18 | if err != nil { 19 | t.Error(err.Error()) 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /examples/panic/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "github.com/yarf-framework/yarf" 6 | ) 7 | 8 | // Panic defines a simple resource 9 | type Panic struct { 10 | yarf.Resource // Extend the yarf.Resource by composition 11 | } 12 | 13 | // Get implements a GET handler that panics 14 | func (p *Panic) Get(c *yarf.Context) error { 15 | c.Render("I'm panicking!") 16 | 17 | panic("Totally panicking!") 18 | 19 | // The next line is unreachable (govet) 20 | //return nil 21 | } 22 | 23 | // PanicHandler is used to catch panics and display the error message 24 | func PanicHandler() { 25 | if err := recover(); err != nil { 26 | fmt.Printf("Handling panic: %v \n", err) 27 | } 28 | } 29 | 30 | // Entry point of the executable application 31 | // It runs a default server listening on http://localhost:8080 32 | func main() { 33 | // Create a new empty YARF server 34 | y := yarf.New() 35 | 36 | // Add route/resource 37 | y.Add("/", new(Panic)) 38 | 39 | // Set our custom panic handler 40 | y.PanicHandler = PanicHandler 41 | 42 | // Start server listening on port 8080 43 | y.Start(":8080") 44 | } 45 | -------------------------------------------------------------------------------- /examples/panic/main_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | func TestMain(t *testing.T) { 8 | // Run main method in a goroutine to make sure it runs. 9 | // Then let it die and just capture panics. 10 | go func() { 11 | defer func() { 12 | if r := recover(); r != nil { 13 | t.Errorf("PANIC: %s", r) 14 | } 15 | }() 16 | 17 | main() 18 | }() 19 | } 20 | -------------------------------------------------------------------------------- /examples/routegroups/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/yarf-framework/yarf" 5 | ) 6 | 7 | // Entry point of the executable application 8 | // It runs a default server listening on http://localhost:8080 9 | // 10 | // URLs available: 11 | // http://localhost:8080 12 | // http://localhost:8080/hello/:name 13 | // http://localhost:8080/v2 14 | // http://localhost:8080/v2/hello/:name 15 | // http://localhost:8080/extra/v2 16 | // http://localhost:8080/extra/v2/hello/:name 17 | // 18 | func main() { 19 | // Create a new empty YARF server 20 | y := yarf.New() 21 | 22 | // Create resources 23 | hello := new(Hello) 24 | hellov2 := new(HelloV2) 25 | 26 | // Add main resource to multiple routes 27 | y.Add("/", hello) 28 | y.Add("/hello/:name", hello) 29 | 30 | // Create /v2 route group 31 | g := yarf.RouteGroup("/v2") 32 | 33 | // Add v2 routes to the group 34 | g.Add("/", hellov2) 35 | g.Add("/hello/:name", hellov2) 36 | 37 | // Use middleware only on the group 38 | g.Insert(new(HelloMiddleware)) 39 | 40 | // Add group to Yarf routes 41 | y.AddGroup(g) 42 | 43 | // Create another group for nesting 44 | n := yarf.RouteGroup("/extra") 45 | 46 | // Nest /v2 group into /extra 47 | n.AddGroup(g) 48 | 49 | // Use another middleware for this group 50 | n.Insert(new(ExtraMiddleware)) 51 | 52 | // Add group to Yarf 53 | y.AddGroup(n) 54 | 55 | // Start server listening on port 8080 56 | y.Start(":8080") 57 | } 58 | -------------------------------------------------------------------------------- /examples/routegroups/main_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | func TestMain(t *testing.T) { 8 | // Run main method in a goroutine to make sure it runs. 9 | // Then let it die and just capture panics. 10 | go func() { 11 | defer func() { 12 | if r := recover(); r != nil { 13 | t.Errorf("PANIC: %s", r) 14 | } 15 | }() 16 | 17 | main() 18 | }() 19 | } 20 | -------------------------------------------------------------------------------- /examples/routegroups/middleware.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/yarf-framework/yarf" 5 | ) 6 | 7 | // HelloMiddleware composites yarf.Middleware 8 | type HelloMiddleware struct { 9 | yarf.Middleware 10 | } 11 | 12 | // PreDispatch renders a hardcoded string 13 | func (m *HelloMiddleware) PreDispatch(c *yarf.Context) error { 14 | c.Render("Hello from middleware! \n\n") 15 | 16 | return nil 17 | } 18 | 19 | // PostDispatch renders a hardcoded string 20 | func (m *HelloMiddleware) PostDispatch(c *yarf.Context) error { 21 | c.Render("\n\nGoodbye from middleware!") 22 | 23 | return nil 24 | } 25 | 26 | // ExtraMiddleware also composites yarf.Middleware 27 | type ExtraMiddleware struct { 28 | yarf.Middleware 29 | } 30 | 31 | // PreDispatch renders a hardcoded string 32 | func (m *ExtraMiddleware) PreDispatch(c *yarf.Context) error { 33 | c.Render("Extra from nested middleware! \n\n") 34 | 35 | return nil 36 | } 37 | 38 | // PostDispatch renders a hardcoded string 39 | func (m *ExtraMiddleware) PostDispatch(c *yarf.Context) error { 40 | c.Render("\n\nExtra from nested middleware!") 41 | 42 | return nil 43 | } 44 | -------------------------------------------------------------------------------- /examples/routegroups/middleware_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/yarf-framework/yarf" 5 | "net/http" 6 | "net/http/httptest" 7 | "testing" 8 | ) 9 | 10 | func TestHello(t *testing.T) { 11 | h := new(HelloMiddleware) 12 | 13 | c := new(yarf.Context) 14 | c.Request, _ = http.NewRequest("GET", "/", nil) 15 | c.Response = httptest.NewRecorder() 16 | 17 | err := h.PreDispatch(c) 18 | if err != nil { 19 | t.Error(err.Error()) 20 | } 21 | 22 | err = h.PostDispatch(c) 23 | if err != nil { 24 | t.Error(err.Error()) 25 | } 26 | } 27 | 28 | func TestExtra(t *testing.T) { 29 | e := new(ExtraMiddleware) 30 | 31 | c := new(yarf.Context) 32 | c.Request, _ = http.NewRequest("GET", "/", nil) 33 | c.Response = httptest.NewRecorder() 34 | 35 | err := e.PreDispatch(c) 36 | if err != nil { 37 | t.Error(err.Error()) 38 | } 39 | 40 | err = e.PostDispatch(c) 41 | if err != nil { 42 | t.Error(err.Error()) 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /examples/routegroups/resource.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/yarf-framework/yarf" 5 | ) 6 | 7 | // Hello composites yarf.Resource 8 | type Hello struct { 9 | yarf.Resource 10 | } 11 | 12 | // Get implements the GET handler with optional name parameter 13 | func (h *Hello) Get(c *yarf.Context) error { 14 | name := c.Param("name") 15 | 16 | salute := "Hello" 17 | if name != "" { 18 | salute += ", " + name 19 | } 20 | salute += "!" 21 | 22 | c.Render(salute) 23 | 24 | return nil 25 | } 26 | 27 | // HelloV2 composites yarf.Resource 28 | type HelloV2 struct { 29 | yarf.Resource 30 | } 31 | 32 | // Get implements the GET handler with optional name parameter 33 | func (h *HelloV2) Get(c *yarf.Context) error { 34 | name := c.Param("name") 35 | 36 | salute := "(v2) Hello" 37 | if name != "" { 38 | salute += ", " + name 39 | } 40 | salute += "!" 41 | 42 | c.Render(salute) 43 | 44 | return nil 45 | } 46 | -------------------------------------------------------------------------------- /examples/routegroups/resource_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/yarf-framework/yarf" 5 | "net/http" 6 | "net/http/httptest" 7 | "testing" 8 | ) 9 | 10 | func TestResourceHello(t *testing.T) { 11 | h := new(Hello) 12 | 13 | c := new(yarf.Context) 14 | c.Request, _ = http.NewRequest("GET", "/", nil) 15 | c.Response = httptest.NewRecorder() 16 | 17 | err := h.Get(c) 18 | if err != nil { 19 | t.Error(err.Error()) 20 | } 21 | } 22 | 23 | func TestResourceHelloV2(t *testing.T) { 24 | h := new(HelloV2) 25 | 26 | c := new(yarf.Context) 27 | c.Request, _ = http.NewRequest("GET", "/", nil) 28 | c.Response = httptest.NewRecorder() 29 | 30 | err := h.Get(c) 31 | if err != nil { 32 | t.Error(err.Error()) 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /examples/simple/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/yarf-framework/yarf" 5 | ) 6 | 7 | // Hello defines a simple resource by compositing yarf.Resource 8 | type Hello struct { 9 | yarf.Resource 10 | } 11 | 12 | // Get implements the GET handler 13 | func (h *Hello) Get(c *yarf.Context) error { 14 | c.Render("Hello world!") 15 | 16 | return nil 17 | } 18 | 19 | // Entry point of the executable application 20 | // It runs a default server listening on http://localhost:8080 21 | func main() { 22 | // Create a new empty YARF server 23 | y := yarf.New() 24 | 25 | // Add route/resource 26 | y.Add("/", new(Hello)) 27 | 28 | // Start server listening on port 8080 29 | y.Start(":8080") 30 | } 31 | -------------------------------------------------------------------------------- /examples/simple/main_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/yarf-framework/yarf" 5 | "net/http" 6 | "net/http/httptest" 7 | "testing" 8 | ) 9 | 10 | func TestHello(t *testing.T) { 11 | h := new(Hello) 12 | 13 | c := new(yarf.Context) 14 | c.Request, _ = http.NewRequest("GET", "/", nil) 15 | c.Response = httptest.NewRecorder() 16 | 17 | err := h.Get(c) 18 | if err != nil { 19 | t.Error(err.Error()) 20 | } 21 | } 22 | 23 | func TestMain(t *testing.T) { 24 | // Run main method in a goroutine to make sure it runs. 25 | // Then let it die and just capture panics. 26 | go func() { 27 | defer func() { 28 | if r := recover(); r != nil { 29 | t.Errorf("PANIC: %s", r) 30 | } 31 | }() 32 | 33 | main() 34 | }() 35 | } 36 | -------------------------------------------------------------------------------- /examples/static/main.go: -------------------------------------------------------------------------------- 1 | // static example package demonstrates how to easily implement 2 | // a yarf handler that serves static files using the net/http package. 3 | package main 4 | 5 | import ( 6 | "github.com/yarf-framework/yarf" 7 | "net/http" 8 | ) 9 | 10 | // Static defines a simple resource 11 | type Static struct { 12 | yarf.Resource // Extend the yarf.Resource by composition 13 | 14 | path string // Directory to serve static files from. 15 | } 16 | 17 | // Get implements the static files handler 18 | func (s *Static) Get(c *yarf.Context) error { 19 | http.FileServer(http.Dir(s.path)).ServeHTTP(c.Response, c.Request) 20 | 21 | return nil 22 | } 23 | 24 | // StaticDir constructs a Static handler and sets the path to serve under the route. 25 | func StaticDir(path string) *Static { 26 | s := new(Static) 27 | s.path = path 28 | 29 | return s 30 | } 31 | 32 | // Entry point of the executable application 33 | // It runs a default server listening on http://localhost:8080 34 | func main() { 35 | // Create a new empty YARF server 36 | y := yarf.New() 37 | 38 | // Add routes/resources 39 | y.Add("/", StaticDir("/tmp")) 40 | y.Add("/test", StaticDir("/var/www/test")) 41 | 42 | // Start server listening on port 8080 43 | y.Start(":8080") 44 | } 45 | -------------------------------------------------------------------------------- /examples/static/main_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | func TestMain(t *testing.T) { 8 | // Run main method in a goroutine to make sure it runs. 9 | // Then let it die and just capture panics. 10 | go func() { 11 | defer func() { 12 | if r := recover(); r != nil { 13 | t.Errorf("PANIC: %s", r) 14 | } 15 | }() 16 | 17 | main() 18 | }() 19 | } 20 | -------------------------------------------------------------------------------- /middleware.go: -------------------------------------------------------------------------------- 1 | package yarf 2 | 3 | // MiddlewareHandler interface provides the methods for request filters 4 | // that needs to run before, or after, every request Resource is executed. 5 | type MiddlewareHandler interface { 6 | PreDispatch(*Context) error 7 | PostDispatch(*Context) error 8 | End(*Context) error 9 | } 10 | 11 | // Middleware struct is the default implementation of a Middleware and does nothing. 12 | // Users can either implement both methods or composite this struct into their own. 13 | // Both methods needs to be present to satisfy the MiddlewareHandler interface. 14 | type Middleware struct{} 15 | 16 | // PreDispatch includes code to be executed before every Resource request. 17 | func (m *Middleware) PreDispatch(c *Context) error { 18 | return nil 19 | } 20 | 21 | // PostDispatch includes code to be executed after every Resource request. 22 | func (m *Middleware) PostDispatch(c *Context) error { 23 | return nil 24 | } 25 | 26 | // End will be executed ALWAYS after every request, even if there were errors present. 27 | func (m *Middleware) End(c *Context) error { 28 | return nil 29 | } 30 | -------------------------------------------------------------------------------- /middleware_test.go: -------------------------------------------------------------------------------- 1 | package yarf 2 | 3 | import ( 4 | "net/http" 5 | "net/http/httptest" 6 | "testing" 7 | ) 8 | 9 | func TestMiddlewareInterface(t *testing.T) { 10 | var m interface{} 11 | m = new(Middleware) 12 | 13 | if _, ok := m.(MiddlewareHandler); !ok { 14 | t.Error("Middleware type doesn't implement MiddlewareHandler interface") 15 | } 16 | } 17 | 18 | func TestMiddlewareDefaultResponse(t *testing.T) { 19 | m := new(Middleware) 20 | 21 | // Create a dummy request. 22 | request, _ := http.NewRequest( 23 | "GET", 24 | "http://127.0.0.1:8080/", 25 | nil, 26 | ) 27 | response := httptest.NewRecorder() 28 | 29 | c := NewContext(request, response) 30 | 31 | if m.PreDispatch(c) != nil { 32 | t.Error("Default PreDispatch() implementation should return nil") 33 | } 34 | if m.PostDispatch(c) != nil { 35 | t.Error("Default PostDispatch() implementation should return nil") 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /resource.go: -------------------------------------------------------------------------------- 1 | package yarf 2 | 3 | // The ResourceHandler interface defines how Resources through the application have to be defined. 4 | // Ideally, the developer will composite the Resource struct into their own resources, 5 | // but it's possible to implement each one by their own. 6 | type ResourceHandler interface { 7 | // HTTP methods 8 | Get(*Context) error 9 | Post(*Context) error 10 | Put(*Context) error 11 | Patch(*Context) error 12 | Delete(*Context) error 13 | Options(*Context) error 14 | Head(*Context) error 15 | Trace(*Context) error 16 | Connect(*Context) error 17 | } 18 | 19 | // The Resource type is the representation of each REST resource of the application. 20 | // It implements the ResourceHandler interface and allows the developer to extend the methods needed. 21 | // All resources being used by a YARF application have to composite this Resource struct. 22 | type Resource struct{} 23 | 24 | // Implementations for all HTTP methods. 25 | // The default implementation will return a 405 HTTP error indicating that the method isn't allowed. 26 | // Once a resource composites the Resource type, it will implement/overwrite the methods needed. 27 | 28 | // Get is the default HTTP GET implementation. 29 | // It returns a NotImplementedError 30 | func (r *Resource) Get(c *Context) error { 31 | return ErrorMethodNotImplemented() 32 | } 33 | 34 | // Post is the default HTTP POST implementation. 35 | // It returns a NotImplementedError 36 | func (r *Resource) Post(c *Context) error { 37 | return ErrorMethodNotImplemented() 38 | } 39 | 40 | // Put is the default HTTP PUT implementation. 41 | // It returns a NotImplementedError 42 | func (r *Resource) Put(c *Context) error { 43 | return ErrorMethodNotImplemented() 44 | } 45 | 46 | // Patch is the default HTTP PATCH implementation. 47 | // It returns a NotImplementedError 48 | func (r *Resource) Patch(c *Context) error { 49 | return ErrorMethodNotImplemented() 50 | } 51 | 52 | // Delete is the default HTTP DELETE implementation. 53 | // It returns a NotImplementedError 54 | func (r *Resource) Delete(c *Context) error { 55 | return ErrorMethodNotImplemented() 56 | } 57 | 58 | // Options is the default HTTP OPTIONS implementation. 59 | // It returns a NotImplementedError 60 | func (r *Resource) Options(c *Context) error { 61 | return ErrorMethodNotImplemented() 62 | } 63 | 64 | // Head is the default HTTP HEAD implementation. 65 | // It returns a NotImplementedError 66 | func (r *Resource) Head(c *Context) error { 67 | return ErrorMethodNotImplemented() 68 | } 69 | 70 | // Trace is the default HTTP TRACE implementation. 71 | // It returns a NotImplementedError 72 | func (r *Resource) Trace(c *Context) error { 73 | return ErrorMethodNotImplemented() 74 | } 75 | 76 | // Connect is the default HTTP CONNECT implementation. 77 | // It returns a NotImplementedError 78 | func (r *Resource) Connect(c *Context) error { 79 | return ErrorMethodNotImplemented() 80 | } 81 | -------------------------------------------------------------------------------- /resource_test.go: -------------------------------------------------------------------------------- 1 | package yarf 2 | 3 | import ( 4 | "net/http" 5 | "net/http/httptest" 6 | "testing" 7 | ) 8 | 9 | func TestResourceInterface(t *testing.T) { 10 | var r interface{} 11 | r = new(Resource) 12 | 13 | if _, ok := r.(ResourceHandler); !ok { 14 | t.Error("Resource type doesn't implement ResourceHandler interface") 15 | } 16 | } 17 | 18 | func TestResourceInterfaceMethods(t *testing.T) { 19 | r := new(Resource) 20 | req, _ := http.NewRequest("GET", "http://localhost:8080/test", nil) 21 | res := httptest.NewRecorder() 22 | c := NewContext(req, res) 23 | 24 | if r.Get(c) == nil { 25 | t.Error("Get() should return a MethodNotImplementedError type by default.") 26 | } 27 | if r.Post(c) == nil { 28 | t.Error("Post() should return a MethodNotImplementedError type by default.") 29 | } 30 | if r.Put(c) == nil { 31 | t.Error("Put() should return a MethodNotImplementedError type by default.") 32 | } 33 | if r.Delete(c) == nil { 34 | t.Error("Delete() should return a MethodNotImplementedError type by default.") 35 | } 36 | if r.Patch(c) == nil { 37 | t.Error("Patch() should return a MethodNotImplementedError type by default.") 38 | } 39 | if r.Head(c) == nil { 40 | t.Error("Head() should return a MethodNotImplementedError type by default.") 41 | } 42 | if r.Options(c) == nil { 43 | t.Error("Options() should return a MethodNotImplementedError type by default.") 44 | } 45 | if r.Connect(c) == nil { 46 | t.Error("Connect() should return a MethodNotImplementedError type by default.") 47 | } 48 | if r.Trace(c) == nil { 49 | t.Error("Trace() should return a MethodNotImplementedError type by default.") 50 | } 51 | 52 | } 53 | -------------------------------------------------------------------------------- /router.go: -------------------------------------------------------------------------------- 1 | package yarf 2 | 3 | import ( 4 | "errors" 5 | "strings" 6 | ) 7 | 8 | // Router interface provides the methods used to handle route and GroupRoute objects. 9 | type Router interface { 10 | Match(string, *Context) bool 11 | Dispatch(*Context) error 12 | } 13 | 14 | // GroupRouter interface adds methods to work with children routers 15 | type GroupRouter interface { 16 | Router 17 | Add(string, ResourceHandler) 18 | AddGroup(*GroupRoute) 19 | Insert(MiddlewareHandler) 20 | } 21 | 22 | // route struct stores the expected route path and the ResourceHandler that handles that route. 23 | type route struct { 24 | path string // Original route 25 | 26 | routeParts []string // parsed Route split into parts 27 | 28 | handler ResourceHandler // Handler for the route 29 | } 30 | 31 | // Route returns a new route object initialized with the provided data. 32 | // Params: 33 | // - url string // The route path to handle 34 | // - h ResourceHandler // The ResourceHandler object that will process the requests to the url. 35 | // 36 | func Route(url string, h ResourceHandler) Router { 37 | return &route{ 38 | path: url, 39 | handler: h, 40 | routeParts: prepareURL(url), 41 | } 42 | } 43 | 44 | // Match returns true/false indicating if a request URL matches the route and 45 | // sets the Context Params for matching parts in the original route. 46 | // Route matchs are exact, that means, there are not optional parameters. 47 | // To implement optional parameters you can define different routes handled by the same ResourceHandler. 48 | // When a route matches the request URL, this method will parse and fill 49 | // the parameters parsed during the process into the Context object. 50 | func (r *route) Match(url string, c *Context) bool { 51 | requestParts := prepareURL(url) 52 | 53 | // YARF router only accepts exact route matches, so check for part count. 54 | // Unless it's a catch-all route 55 | if len(r.routeParts) == 0 || (len(r.routeParts) > 0 && r.routeParts[len(r.routeParts)-1] != "*") { 56 | if len(r.routeParts) != len(requestParts) { 57 | return false 58 | } 59 | } 60 | 61 | // check that requestParts matches routeParts 62 | if !matches(r.routeParts, requestParts) { 63 | return false 64 | } 65 | 66 | storeParams(c, r.routeParts, requestParts) 67 | 68 | return true 69 | } 70 | 71 | // Dispatch executes the right ResourceHandler method based on the HTTP request in the Context object. 72 | func (r *route) Dispatch(c *Context) error { 73 | // Method dispatch 74 | switch c.Request.Method { 75 | case "GET": 76 | return r.handler.Get(c) 77 | 78 | case "POST": 79 | return r.handler.Post(c) 80 | 81 | case "PUT": 82 | return r.handler.Put(c) 83 | 84 | case "PATCH": 85 | return r.handler.Patch(c) 86 | 87 | case "DELETE": 88 | return r.handler.Delete(c) 89 | 90 | case "OPTIONS": 91 | return r.handler.Options(c) 92 | 93 | case "HEAD": 94 | return r.handler.Head(c) 95 | 96 | case "TRACE": 97 | return r.handler.Trace(c) 98 | 99 | case "CONNECT": 100 | return r.handler.Connect(c) 101 | 102 | } 103 | 104 | // Return method not implemented 105 | return ErrorMethodNotImplemented() 106 | } 107 | 108 | // GroupRoute stores routes grouped under a single url prefix. 109 | type GroupRoute struct { 110 | prefix string // The url prefix path for all routes in the group 111 | 112 | routeParts []string // parsed Route split into parts 113 | 114 | middleware []MiddlewareHandler // Group middleware resources 115 | 116 | routes []Router // Group routes 117 | } 118 | 119 | // RouteGroup creates a new GroupRoute object and initializes it with the provided url prefix. 120 | // The object implements Router interface to being able to handle groups as routes. 121 | // Groups can be nested into each other, 122 | // so it's possible to add a GroupRoute as a route inside another GroupRoute. 123 | // Includes methods to work with middleware. 124 | func RouteGroup(url string) *GroupRoute { 125 | return &GroupRoute{ 126 | prefix: url, 127 | routeParts: prepareURL(url), 128 | } 129 | } 130 | 131 | // Match loops through all routes inside the group and find for one that matches the request. 132 | // After a match is found, the route matching is stored into Context.groupDispatch 133 | // to being able to dispatch it directly after a match without looping again. 134 | // Outside the box, works exactly the same as route.Match() 135 | func (g *GroupRoute) Match(url string, c *Context) bool { 136 | urlParts := prepareURL(url) 137 | 138 | // check if urlParts matches routeParts 139 | if !matches(g.routeParts, urlParts) { 140 | return false 141 | } 142 | 143 | // Remove prefix part form the request URL 144 | rURL := strings.Join(urlParts[len(g.routeParts):], "/") 145 | 146 | // Now look for a match inside the routes collection 147 | for _, r := range g.routes { 148 | if r.Match(rURL, c) { 149 | // store the matching Router and params after a match is found 150 | c.groupDispatch = append(c.groupDispatch, r) 151 | storeParams(c, g.routeParts, urlParts) 152 | return true 153 | } 154 | } 155 | 156 | return false 157 | } 158 | 159 | // Dispatch loops through all routes inside the group and dispatch the one that matches the request. 160 | // Outside the box, works exactly the same as route.Dispatch(). 161 | func (g *GroupRoute) Dispatch(c *Context) (err error) { 162 | if len(c.groupDispatch) == 0 { 163 | g.endDispatch(c) 164 | return errors.New("No matching route found") 165 | } 166 | 167 | // Pre-dispatch middleware 168 | for _, m := range g.middleware { 169 | // Dispatch 170 | err = m.PreDispatch(c) 171 | if err != nil { 172 | g.endDispatch(c) 173 | return 174 | } 175 | } 176 | 177 | // pop, dispatch last route 178 | n := len(c.groupDispatch) - 1 179 | route := c.groupDispatch[n] 180 | c.groupDispatch = c.groupDispatch[:n] 181 | err = route.Dispatch(c) 182 | if err != nil { 183 | g.endDispatch(c) 184 | return 185 | } 186 | 187 | // Post-dispatch middleware 188 | for _, m := range g.middleware { 189 | // Dispatch 190 | err = m.PostDispatch(c) 191 | if err != nil { 192 | g.endDispatch(c) 193 | return 194 | } 195 | } 196 | 197 | // End dispatch if no errors blocking... 198 | g.endDispatch(c) 199 | 200 | // Return success 201 | return 202 | } 203 | 204 | func (g *GroupRoute) endDispatch(c *Context) (err error) { 205 | // End dispatch middleware 206 | for _, m := range g.middleware { 207 | e := m.End(c) 208 | if e != nil { 209 | // If there are any error, only return the last to be sure we go through all middlewares. 210 | err = e 211 | } 212 | } 213 | 214 | return 215 | } 216 | 217 | // Add inserts a new resource with it's associated route into the group object. 218 | func (g *GroupRoute) Add(url string, h ResourceHandler) { 219 | g.routes = append(g.routes, Route(url, h)) 220 | } 221 | 222 | // AddGroup inserts a GroupRoute into the routes list of the group object. 223 | // This makes possible to nest groups. 224 | func (g *GroupRoute) AddGroup(r *GroupRoute) { 225 | g.routes = append(g.routes, r) 226 | } 227 | 228 | // Insert adds a MiddlewareHandler into the middleware list of the group object. 229 | func (g *GroupRoute) Insert(m MiddlewareHandler) { 230 | g.middleware = append(g.middleware, m) 231 | } 232 | 233 | // prepareUrl trims leading and trailing slahses, splits url parts, and removes empty parts 234 | func prepareURL(url string) []string { 235 | return removeEmpty(strings.Split(url, "/")) 236 | } 237 | 238 | // removeEmpty removes blank strings from parts in one pass, shifting elements 239 | // of the array down, and returns the altered array. 240 | func removeEmpty(parts []string) []string { 241 | x := parts[:0] 242 | 243 | for _, p := range parts { 244 | if p != "" { 245 | x = append(x, p) 246 | } 247 | } 248 | 249 | return x 250 | } 251 | 252 | // matches returns true if requestParts matches routeParts up through len(routeParts) 253 | // ignoring params in routeParts 254 | func matches(routeParts, requestParts []string) bool { 255 | routeCount := len(routeParts) 256 | 257 | // Check for catch-all wildcard 258 | if len(routeParts) > 0 && routeParts[len(routeParts)-1] == "*" { 259 | routeCount-- 260 | } 261 | 262 | if len(requestParts) < routeCount { 263 | return false 264 | } 265 | 266 | // Check for part matching, ignoring params and * wildcards 267 | for i, p := range routeParts { 268 | // Skip wildcard 269 | if p == "*" { 270 | continue 271 | } 272 | if p != requestParts[i] && p[0] != ':' { 273 | return false 274 | } 275 | } 276 | 277 | return true 278 | } 279 | 280 | // storeParams writes parts from requestParts that correspond with param names in 281 | // routeParts into c.Params. 282 | func storeParams(c *Context, routeParts, requestParts []string) { 283 | for i, p := range routeParts { 284 | if p[0] == ':' { 285 | c.Params.Set(p[1:], requestParts[i]) 286 | } 287 | } 288 | } 289 | -------------------------------------------------------------------------------- /router_test.go: -------------------------------------------------------------------------------- 1 | package yarf 2 | 3 | import ( 4 | "net/http" 5 | "testing" 6 | ) 7 | 8 | // Empty handler 9 | type Handler struct { 10 | Resource 11 | } 12 | 13 | func TestRouterRootMatch(t *testing.T) { 14 | // Create empty handler 15 | h := new(Handler) 16 | 17 | // Create empty context 18 | c := new(Context) 19 | c.Params = Params{} 20 | 21 | // Create route 22 | r := Route("/", h) 23 | 24 | // Matching routes 25 | rs := []string{"/", ""} 26 | 27 | // Check 28 | for _, s := range rs { 29 | if !r.Match(s, c) { 30 | t.Errorf("'%s' should match against '/'", s) 31 | } 32 | } 33 | } 34 | 35 | func TestRouterRootCatchAll(t *testing.T) { 36 | // Create empty handler 37 | h := new(Handler) 38 | 39 | // Create empty context 40 | c := new(Context) 41 | c.Params = Params{} 42 | 43 | // Create route 44 | r := Route("/*", h) 45 | 46 | // Matching routes 47 | rs := []string{"/", "", "/something", "/*", "/something/else/more"} 48 | 49 | // Check 50 | for _, s := range rs { 51 | if !r.Match(s, c) { 52 | t.Errorf("'%s' should match against '*'", s) 53 | } 54 | } 55 | } 56 | 57 | func TestRouterRootUnmatch(t *testing.T) { 58 | // Create empty handler 59 | h := new(Handler) 60 | 61 | // Create empty context 62 | c := new(Context) 63 | c.Params = Params{} 64 | 65 | // Create route 66 | r := Route("/", h) 67 | 68 | // Non-matching routes 69 | rs := []string{"/something", "something", "/some/thing", "some/thing"} 70 | 71 | // Check 72 | for _, s := range rs { 73 | if r.Match(s, c) { 74 | t.Errorf("'%s' shouldn't match against '/'", s) 75 | } 76 | } 77 | } 78 | 79 | func TestRouter1LevelMatch(t *testing.T) { 80 | // Create empty handler 81 | h := new(Handler) 82 | 83 | // Create empty context 84 | c := new(Context) 85 | c.Params = Params{} 86 | 87 | // Create route 88 | r := Route("/level", h) 89 | 90 | // Matching routes 91 | rs := []string{"/level", "level"} 92 | 93 | // Check 94 | for _, s := range rs { 95 | if !r.Match(s, c) { 96 | t.Errorf("'%s' should match against '/level'", s) 97 | } 98 | } 99 | } 100 | 101 | func TestRouter1LevelCatchAll(t *testing.T) { 102 | // Create empty handler 103 | h := new(Handler) 104 | 105 | // Create empty context 106 | c := new(Context) 107 | c.Params = Params{} 108 | 109 | // Create route 110 | r := Route("/level/*", h) 111 | 112 | // Matching routes 113 | rs := []string{"/level/something", "level/something", "/level/*", "/level/something/else/and/more/because/this/matches/all"} 114 | 115 | // Check 116 | for _, s := range rs { 117 | if !r.Match(s, c) { 118 | t.Errorf("'%s' should match against '/level/*'", s) 119 | } 120 | } 121 | } 122 | 123 | func TestRouter1LevelUnmatch(t *testing.T) { 124 | // Create empty handler 125 | h := new(Handler) 126 | 127 | // Create empty context 128 | c := new(Context) 129 | c.Params = Params{} 130 | 131 | // Create route 132 | r := Route("/level", h) 133 | 134 | // Non-matching routes 135 | rs := []string{"/", "", "/:level", "/Level", "/some/thing", "some/thing", "/more/levels/to/be/sure/it/shouldn't/matter/", "/with/trailer/"} 136 | 137 | // Check 138 | for _, s := range rs { 139 | if r.Match(s, c) { 140 | t.Errorf("'%s' shouldn't match against '/level'", s) 141 | } 142 | } 143 | } 144 | 145 | func TestRouterMultiLevelMatch(t *testing.T) { 146 | // Create empty handler 147 | h := new(Handler) 148 | 149 | // Create empty context 150 | c := new(Context) 151 | c.Params = Params{} 152 | 153 | // Create route 154 | r := Route("/a/b/c", h) 155 | 156 | // Matching routes 157 | rs := []string{"/a/b/c", "a/b/c", "/a/b/c/", "a/b/c/"} 158 | 159 | // Check 160 | for _, s := range rs { 161 | if !r.Match(s, c) { 162 | t.Errorf("'%s' should match against '/a/b/c", s) 163 | } 164 | } 165 | } 166 | 167 | func TestRouterMultiLevelWildcard(t *testing.T) { 168 | // Create empty handler 169 | h := new(Handler) 170 | 171 | // Create empty context 172 | c := new(Context) 173 | c.Params = Params{} 174 | 175 | // Create route 176 | r := Route("/a/b/*/d", h) 177 | 178 | // Matching routes 179 | rs := []string{"/a/b/c/d", "a/b/c/d", "/a/b/*/d", "a/b/something/d"} 180 | 181 | // Check 182 | for _, s := range rs { 183 | if !r.Match(s, c) { 184 | t.Errorf("'%s' should match against '/a/b/*/d", s) 185 | } 186 | } 187 | } 188 | 189 | func TestRouterMultiLevelCatchAll(t *testing.T) { 190 | // Create empty handler 191 | h := new(Handler) 192 | 193 | // Create empty context 194 | c := new(Context) 195 | c.Params = Params{} 196 | 197 | // Create route 198 | r := Route("/a/b/*", h) 199 | 200 | // Matching routes 201 | rs := []string{"/a/b/c", "a/b/c", "/a/b/c/d/e", "a/b/c/d"} 202 | 203 | // Check 204 | for _, s := range rs { 205 | if !r.Match(s, c) { 206 | t.Errorf("'%s' should match against '/a/b/*", s) 207 | } 208 | } 209 | } 210 | 211 | func TestRouterMultiLevelUnmatch(t *testing.T) { 212 | // Create empty handler 213 | h := new(Handler) 214 | 215 | // Create empty context 216 | c := new(Context) 217 | c.Params = Params{} 218 | 219 | // Create route 220 | r := Route("/a/b/c", h) 221 | 222 | // Non-matching routes 223 | rs := []string{"/", "", "/:a/b/c", "/A/B/C", "/some/thing", "some/thing", "/more/levels/to/be/sure/it/shouldn't/matter", "///", "/almost/trailer/"} 224 | 225 | // Check 226 | for _, s := range rs { 227 | if r.Match(s, c) { 228 | t.Errorf("'%s' shouldn't match against '/a/b/c'", s) 229 | } 230 | } 231 | } 232 | 233 | func TestRouter1LevelParamMatch(t *testing.T) { 234 | // Create empty handler 235 | h := new(Handler) 236 | 237 | // Create empty context 238 | c := new(Context) 239 | c.Params = Params{} 240 | 241 | // Create route 242 | r := Route("/:param", h) 243 | 244 | // Matching routes 245 | rs := []string{"/a", "a", "/cafewafewa", "/:paramStyle", "/trailer/"} 246 | 247 | // Check 248 | for _, s := range rs { 249 | if !r.Match(s, c) { 250 | t.Errorf("'%s' should match against '/:param'", s) 251 | } 252 | } 253 | } 254 | 255 | func TestRouter1LevelParamCatchAll(t *testing.T) { 256 | // Create empty handler 257 | h := new(Handler) 258 | 259 | // Create empty context 260 | c := new(Context) 261 | c.Params = Params{} 262 | 263 | // Create route 264 | r := Route("/:param/*", h) 265 | 266 | // Matching routes 267 | rs := []string{"/a", "a", "/cafewafewa", "/:paramStyle", "/trailer/", "/something/more/to/catch"} 268 | 269 | // Check 270 | for _, s := range rs { 271 | if !r.Match(s, c) { 272 | t.Errorf("'%s' should match against '/:param/*'", s) 273 | } 274 | } 275 | } 276 | 277 | func TestRouter1LevelParamUnmatch(t *testing.T) { 278 | // Create empty handler 279 | h := new(Handler) 280 | 281 | // Create empty context 282 | c := new(Context) 283 | c.Params = Params{} 284 | 285 | // Create route 286 | r := Route("/:param", h) 287 | 288 | // Non-matching routes 289 | rs := []string{"/", "", "/some/thing", "some/thing", "/more/levels/to/be/sure/it/shouldn't/matter/", "/with/trailer/"} 290 | 291 | // Check 292 | for _, s := range rs { 293 | if r.Match(s, c) { 294 | t.Errorf("'%s' shouldn't match against '/:param'", s) 295 | } 296 | } 297 | } 298 | 299 | func TestRouterMultiLevelParamMatch(t *testing.T) { 300 | // Create empty handler 301 | h := new(Handler) 302 | 303 | // Create empty context 304 | c := new(Context) 305 | c.Params = Params{} 306 | 307 | // Create route 308 | r := Route("/a/b/:param", h) 309 | 310 | // Matching routes 311 | rs := []string{"/a/b/c", "a/b/c", "/a/b/c/", "a/b/c/", "/a/b/:c", "/a/b/:param"} 312 | 313 | // Check 314 | for _, s := range rs { 315 | if !r.Match(s, c) { 316 | t.Errorf("'%s' should match against '/a/b/:param'", s) 317 | } 318 | } 319 | } 320 | 321 | func TestRouterMultiLevelParamWildcard(t *testing.T) { 322 | // Create empty handler 323 | h := new(Handler) 324 | 325 | // Create empty context 326 | c := new(Context) 327 | c.Params = Params{} 328 | 329 | // Create route 330 | r := Route("/a/*/:param", h) 331 | 332 | // Matching routes 333 | rs := []string{"/a/b/c", "a/b/c", "/a/b/c/", "a/b/c/", "/a/b/:c", "/a/b/:param"} 334 | 335 | // Check 336 | for _, s := range rs { 337 | if !r.Match(s, c) { 338 | t.Errorf("'%s' should match against '/a/*/:param'", s) 339 | } 340 | } 341 | } 342 | 343 | func TestRouterMultiLevelParamCatchAll(t *testing.T) { 344 | // Create empty handler 345 | h := new(Handler) 346 | 347 | // Create empty context 348 | c := new(Context) 349 | c.Params = Params{} 350 | 351 | // Create route 352 | r := Route("/a/b/:param/*", h) 353 | 354 | // Matching routes 355 | rs := []string{"/a/b/c", "a/b/c", "/a/b/c/", "a/b/c/", "/a/b/:c", "/a/b/:param", "/a/b/c/d/e/f/g", "/a/b/c/d/:param/*"} 356 | 357 | // Check 358 | for _, s := range rs { 359 | if !r.Match(s, c) { 360 | t.Errorf("'%s' should match against '/a/b/:param/*'", s) 361 | } 362 | } 363 | } 364 | 365 | func TestRouterMultiLevelParamUnmatch(t *testing.T) { 366 | // Create empty handler 367 | h := new(Handler) 368 | 369 | // Create empty context 370 | c := new(Context) 371 | c.Params = Params{} 372 | 373 | // Create route 374 | r := Route("/a/b/:param", h) 375 | 376 | // Non-matching routes 377 | rs := []string{"/", "", "/a/b", "a/b", "/a/b/c/d", "/a/b/"} 378 | 379 | // Check 380 | for _, s := range rs { 381 | if r.Match(s, c) { 382 | t.Errorf("'%s' shouldn't match against '/a/b/:param'", s) 383 | } 384 | } 385 | } 386 | 387 | func TestRouteGroupAdd(t *testing.T) { 388 | y := RouteGroup("") 389 | r := new(MockResource) 390 | 391 | y.Add("/test", r) 392 | 393 | if len(y.routes) != 1 { 394 | t.Fatalf("Added 1 route, found %d in the list.", len(y.routes)) 395 | } 396 | if y.routes[0].(*route).path != "/test" { 397 | t.Fatalf("Added /test path. Found %s", y.routes[0].(*route).path) 398 | } 399 | if y.routes[0].(*route).handler != r { 400 | t.Fatal("Added a Handler. Handler found seems to be different") 401 | } 402 | 403 | y.Add("/test/2", r) 404 | 405 | if len(y.routes) != 2 { 406 | t.Fatalf("Added 2 routes, found %d routes in the list.", len(y.routes)) 407 | } 408 | 409 | if y.routes[0].(*route).handler != y.routes[1].(*route).handler { 410 | t.Fatal("Added a Handler to 2 routes. Handlers found seems to be different") 411 | } 412 | } 413 | 414 | func TestRouteGroupAddGroup(t *testing.T) { 415 | y := RouteGroup("") 416 | g := RouteGroup("/group") 417 | 418 | y.AddGroup(g) 419 | 420 | if len(y.routes) != 1 { 421 | t.Fatalf("Added 1 route group, found %d in the list.", len(y.routes)) 422 | } 423 | if y.routes[0].(*GroupRoute).prefix != "/group" { 424 | t.Fatalf("Added a /group route prefix. Found %s", y.routes[0].(*GroupRoute).prefix) 425 | } 426 | } 427 | 428 | func TestRouteGroupInsert(t *testing.T) { 429 | y := RouteGroup("") 430 | m := new(MockMiddleware) 431 | 432 | y.Insert(m) 433 | 434 | if len(y.middleware) != 1 { 435 | t.Fatalf("Added 1 middleware, found %d in the list.", len(y.routes)) 436 | } 437 | if y.middleware[0] != m { 438 | t.Fatal("Added a middleware. Stored one seems to be different") 439 | } 440 | } 441 | 442 | func TestRouterGroupDispatch(t *testing.T) { 443 | g1 := RouteGroup("one") 444 | g2 := RouteGroup("two") 445 | 446 | g1.AddGroup(g2) 447 | g2.Add("test", &Handler{}) 448 | 449 | c := &Context{Params: Params{}, Request: &http.Request{}} 450 | 451 | if !g1.Match("one/two/test", c) { 452 | t.Errorf("Route did not match") 453 | } 454 | 455 | err := g1.Dispatch(c) 456 | if _, ok := err.(*MethodNotImplementedError); !ok { 457 | t.Errorf("Dispatch failed: %s", err) 458 | } 459 | } 460 | 461 | func TestRouterGroupMatch(t *testing.T) { 462 | // Create empty handler 463 | h := new(Handler) 464 | 465 | // Create empty context 466 | c := new(Context) 467 | c.Params = Params{} 468 | 469 | // Create group 470 | g := RouteGroup("/v1") 471 | g.Add("/test/:param", h) 472 | 473 | // Matching routes 474 | rs := []string{"/v1/test/test", "/v1/test/:param/"} 475 | 476 | // Check 477 | for _, s := range rs { 478 | if !g.Match(s, c) { 479 | t.Errorf("'%s' should match", s) 480 | } 481 | } 482 | } 483 | 484 | func TestRouterGroupCatchAll(t *testing.T) { 485 | // Create empty handler 486 | h := new(Handler) 487 | 488 | // Create empty context 489 | c := new(Context) 490 | c.Params = Params{} 491 | 492 | // Create group 493 | g := RouteGroup("/v1") 494 | g.Add("/test/*", h) 495 | 496 | // Matching routes 497 | rs := []string{"/v1/test/test", "/v1/test/:param/", "/v1/test/this/is/a/wild/card"} 498 | 499 | // Check 500 | for _, s := range rs { 501 | if !g.Match(s, c) { 502 | t.Errorf("'%s' should match", s) 503 | } 504 | } 505 | } 506 | 507 | func TestRouterGroupNotMatch(t *testing.T) { 508 | // Create empty handler 509 | h := new(Handler) 510 | 511 | // Create empty context 512 | c := new(Context) 513 | c.Params = Params{} 514 | 515 | // Create group 516 | g := RouteGroup("/v1") 517 | g.Add("/test/:param", h) 518 | 519 | // Non-Matching routes 520 | rs := []string{"/test/test", "/v1/test", "/v1/test/a/b", "/v1", "/"} 521 | 522 | // Check 523 | for _, s := range rs { 524 | if g.Match(s, c) { 525 | t.Errorf("'%s' shouldn't match", s) 526 | } 527 | } 528 | } 529 | 530 | func TestRouterGroupParams(t *testing.T) { 531 | h := &Handler{} 532 | c := &Context{Params: Params{}} 533 | 534 | g := RouteGroup("/test/:param/") 535 | g.Add("/blah", h) 536 | 537 | if g.Match("/test/arg1/nomatch", c) { 538 | t.Errorf("shouldn't match") 539 | } 540 | 541 | if len(c.Params) > 0 { 542 | t.Errorf("RouteGroup should not write params if children did not match: %v", c.Params) 543 | } 544 | } 545 | 546 | func TestRouterNestedGroupMatch(t *testing.T) { 547 | // Create empty handler 548 | h := new(Handler) 549 | 550 | // Create empty context 551 | c := new(Context) 552 | c.Params = Params{} 553 | 554 | // Create groups 555 | l1 := RouteGroup("/level1") 556 | l2 := RouteGroup("/level2") 557 | l3 := RouteGroup("/level3") 558 | 559 | // Add one route 560 | l3.Add("/test/:param", h) 561 | 562 | // Neste into: 563 | // - /level1/level2/level3/test/:param 564 | // - /level2/level3/test/:param 565 | // - /level3/test/:param 566 | l2.AddGroup(l3) 567 | l1.AddGroup(l2) 568 | 569 | // Level 3 matching routes 570 | rs := []string{"/level3/test/test", "/level3/test/:param/"} 571 | 572 | // Check 573 | for _, s := range rs { 574 | if !l3.Match(s, c) { 575 | t.Errorf("'%s' should match", s) 576 | } 577 | } 578 | 579 | // Level 2 matching routes 580 | rs = []string{"/level2/level3/test/test", "/level2/level3/test/:param/"} 581 | 582 | // Check 583 | for _, s := range rs { 584 | if !l2.Match(s, c) { 585 | t.Errorf("'%s' should match", s) 586 | } 587 | } 588 | 589 | // Level 1 matching routes 590 | rs = []string{"/level1/level2/level3/test/test", "/level1/level2/level3/test/:param/"} 591 | 592 | // Check 593 | for _, s := range rs { 594 | if !l1.Match(s, c) { 595 | t.Errorf("'%s' should match", s) 596 | } 597 | } 598 | } 599 | 600 | func BenchmarkRouteMatch_short(b *testing.B) { 601 | h := &Handler{} 602 | c := &Context{} 603 | r := Route("/test", h) 604 | for i := 0; i < b.N; i++ { 605 | r.Match("/test", c) 606 | r.Match("/nomatch", c) 607 | } 608 | } 609 | 610 | func BenchmarkRouteMatch_long(b *testing.B) { 611 | h := &Handler{} 612 | c := &Context{} 613 | routeString := "/very/long/route/with/ten/separate/parts/eight/nine/ten" 614 | r := Route(routeString, h) 615 | for i := 0; i < b.N; i++ { 616 | r.Match(routeString, c) 617 | r.Match("/short/request/url", c) 618 | r.Match("/very/long/route/with/ten/separate/parts/that/do/not/match", c) 619 | } 620 | } 621 | 622 | func BenchmarkRouteMatch_emptyParts(b *testing.B) { 623 | h := &Handler{} 624 | c := &Context{} 625 | r := Route("/route///with//lots////of///empty///parts/", h) 626 | for i := 0; i < b.N; i++ { 627 | r.Match("/route///with/lots/of////empty////parts/", c) 628 | r.Match("/request/////url/////////////with//////////////tons//of/empty///////////parts/////////////test", c) 629 | } 630 | } 631 | 632 | func BenchmarkRouteGroupMatch_short(b *testing.B) { 633 | h := &Handler{} 634 | c := &Context{} 635 | r := RouteGroup("/prefix") 636 | r.Add("/suffix", h) 637 | for i := 0; i < b.N; i++ { 638 | r.Match("/test", c) 639 | r.Match("/nomatch", c) 640 | } 641 | } 642 | 643 | func BenchmarkRouteGroupMatch_nested(b *testing.B) { 644 | h := &Handler{} 645 | c := &Context{} 646 | // create a set of nested RouteGroups 20 levels deep 647 | g := RouteGroup("/test") 648 | g.Add("/router", h) 649 | path := "/test/router" 650 | for i := 0; i < 19; i++ { 651 | r := RouteGroup("/test") 652 | r.AddGroup(g) 653 | g = r 654 | path = "/test" + path 655 | } 656 | for i := 0; i < b.N; i++ { 657 | g.Match(path, c) 658 | g.Match(path+"matchfail", c) 659 | } 660 | } 661 | -------------------------------------------------------------------------------- /yarf.go: -------------------------------------------------------------------------------- 1 | package yarf 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | "net/http" 7 | ) 8 | 9 | // Version string 10 | const Version = "0.8.5" 11 | 12 | // Yarf is the main entry point for the framework and it centralizes most of the functionality. 13 | // All configuration actions are handled by this object. 14 | type Yarf struct { 15 | // UseCache indicates if the route cache should be used. 16 | UseCache bool 17 | 18 | // Debug enables/disables the debug mode. 19 | // On debug mode, extra error information is sent to the client. 20 | Debug bool 21 | 22 | // PanicHandler can store a func() that will be defered by each request to be able to recover(). 23 | // If you need to log, send information or do anything about a panic, this is your place. 24 | PanicHandler func() 25 | 26 | GroupRouter 27 | 28 | // Cached routes storage 29 | cache *Cache 30 | 31 | // Logger object will be used if present 32 | Logger *log.Logger 33 | 34 | // Follow defines a standard http.Handler implementation to follow if no route matches. 35 | Follow http.Handler 36 | 37 | // NotFound defines a function interface to execute when a NotFound (404) error is thrown. 38 | NotFound func(c *Context) 39 | } 40 | 41 | // New creates a new yarf and returns a pointer to it. 42 | // Performs needed initializations 43 | func New() *Yarf { 44 | y := new(Yarf) 45 | 46 | // Init cache 47 | y.UseCache = true 48 | y.cache = NewCache() 49 | y.GroupRouter = RouteGroup("") 50 | 51 | // Return object 52 | return y 53 | } 54 | 55 | // ServeHTTP Implements http.Handler interface into yarf. 56 | // Initializes a Context object and handles middleware and route actions. 57 | // If an error is returned by any of the actions, the flow is stopped and a response is sent. 58 | // If no route matches, tries to forward the request to the Yarf.Follow (http.Handler type) property if set. 59 | // Otherwise it returns a 404 response. 60 | func (y *Yarf) ServeHTTP(res http.ResponseWriter, req *http.Request) { 61 | if y.PanicHandler != nil { 62 | defer y.PanicHandler() 63 | } 64 | 65 | // Set initial context data. 66 | // The Context pointer will be affected by the middleware and resources. 67 | c := NewContext(req, res) 68 | 69 | // Cached routes 70 | if y.UseCache { 71 | if cache, ok := y.cache.Get(req.URL.Path); ok { 72 | // Set context params 73 | c.Params = cache.params 74 | c.groupDispatch = cache.route 75 | 76 | // Dispatch and stop 77 | err := y.Dispatch(c) 78 | y.finish(c, err) 79 | return 80 | } 81 | } 82 | 83 | // Route match 84 | if y.Match(req.URL.Path, c) { 85 | if y.UseCache { 86 | y.cache.Set(req.URL.Path, RouteCache{c.groupDispatch, c.Params}) 87 | } 88 | err := y.Dispatch(c) 89 | y.finish(c, err) 90 | return 91 | } 92 | 93 | // Follow only when route doesn't match. 94 | // Returned 404 errors won't follow. 95 | if y.Follow != nil { 96 | // Log follow 97 | y.finish(c, nil) 98 | 99 | // Follow 100 | y.Follow.ServeHTTP(c.Response, c.Request) 101 | 102 | // End here 103 | return 104 | } 105 | 106 | // Return 404 107 | y.finish(c, ErrorNotFound()) 108 | } 109 | 110 | // Finish handles the end of the execution. 111 | // It checks for errors and follow actions to execute. 112 | // It also handles the custom 404 error handler. 113 | func (y *Yarf) finish(c *Context, err error) { 114 | // If a logger is present, lets log everything. 115 | if y.Logger != nil { 116 | // Construct request host string 117 | req := "http" 118 | if c.Request.TLS != nil { 119 | req += "s" 120 | } 121 | req += "://" + c.Request.Host + c.Request.URL.String() 122 | 123 | // Check for errors 124 | errorMsg := "OK" 125 | if err != nil { 126 | yerr, ok := err.(YError) 127 | if ok { 128 | if yerr.Code() == 404 && y.NotFound != nil { 129 | errorMsg = "FOLLOW NotFound" 130 | } else { 131 | errorMsg = fmt.Sprintf("ERROR: %d - %s | %s", yerr.Code(), yerr.Body(), yerr.Msg()) 132 | } 133 | } else { 134 | errorMsg = "ERROR: " + err.Error() 135 | } 136 | } 137 | 138 | y.Logger.Printf( 139 | "%s - %s | %s | %s => %s", 140 | c.GetClientIP(), 141 | c.Request.UserAgent(), 142 | c.Request.Method, 143 | req, 144 | errorMsg, 145 | ) 146 | } 147 | 148 | // Return if no error 149 | if err == nil { 150 | return 151 | } 152 | 153 | // Check error type 154 | yerr, ok := err.(YError) 155 | if !ok { 156 | // Create default 500 error 157 | yerr = &CustomError{ 158 | HTTPCode: 500, 159 | ErrorCode: 0, 160 | ErrorMsg: err.Error(), 161 | ErrorBody: err.Error(), 162 | } 163 | } 164 | 165 | // Custom 404 166 | if yerr.Code() == 404 && y.NotFound != nil { 167 | y.NotFound(c) 168 | return 169 | } 170 | 171 | // Write error data to response. 172 | c.Response.WriteHeader(yerr.Code()) 173 | c.Render(yerr.Body()) 174 | } 175 | 176 | // Start initiates a new http yarf server and start listening. 177 | // It's a shortcut for http.ListenAndServe(address, y) 178 | func (y *Yarf) Start(address string) { 179 | http.ListenAndServe(address, y) 180 | } 181 | 182 | // StartTLS initiates a new http yarf server and starts listening to HTTPS requests. 183 | // It is a shortcut for http.ListenAndServeTLS(address, cert, key, yarf) 184 | func (y *Yarf) StartTLS(address, cert, key string) { 185 | http.ListenAndServeTLS(address, cert, key, y) 186 | } 187 | -------------------------------------------------------------------------------- /yarf_test.go: -------------------------------------------------------------------------------- 1 | package yarf 2 | 3 | import ( 4 | "net/http" 5 | "net/http/httptest" 6 | "testing" 7 | ) 8 | 9 | type MockResource struct { 10 | Resource 11 | } 12 | 13 | type MockMiddleware struct { 14 | Middleware 15 | } 16 | 17 | func TestYarfCache(t *testing.T) { 18 | y := New() 19 | 20 | if len(y.cache.storage) > 0 { 21 | t.Error("yarf.cache.storage should be empty after initialization") 22 | } 23 | 24 | r := new(MockResource) 25 | y.Add("/test", r) 26 | 27 | req, _ := http.NewRequest("GET", "http://localhost:8080/route/not/match", nil) 28 | res := httptest.NewRecorder() 29 | y.ServeHTTP(res, req) 30 | 31 | if len(y.cache.storage) > 0 { 32 | t.Error("yarf.cache.storage should be empty after non-matching request") 33 | } 34 | 35 | req, _ = http.NewRequest("GET", "http://localhost:8080/test", nil) 36 | y.ServeHTTP(res, req) 37 | 38 | if len(y.cache.storage) != 1 { 39 | t.Error("yarf.cache.storage should have 1 item after matching request") 40 | } 41 | 42 | for i := 0; i < 100; i++ { 43 | y.ServeHTTP(res, req) 44 | } 45 | 46 | if len(y.cache.storage) != 1 { 47 | t.Error("yarf.cache.storage should have 1 item after multiple matching requests to a single route") 48 | } 49 | } 50 | 51 | func TestYarfUseCacheFalse(t *testing.T) { 52 | r := new(MockResource) 53 | y := New() 54 | y.UseCache = false 55 | y.Add("/test", r) 56 | 57 | req, _ := http.NewRequest("GET", "http://localhost:8080/test", nil) 58 | res := httptest.NewRecorder() 59 | y.ServeHTTP(res, req) 60 | 61 | if len(y.cache.storage) > 0 { 62 | t.Error("yarf.cache.storage should be empty after matching request with yarf.UseCache = false") 63 | } 64 | } 65 | 66 | func TestRace(t *testing.T) { 67 | g := RouteGroup("/test") 68 | g.Add("/one/:param", &MockResource{}) 69 | g.Add("/two/:param", &MockResource{}) 70 | 71 | y := New() 72 | y.AddGroup(g) 73 | 74 | one, _ := http.NewRequest("GET", "http://localhost:8080/test/one/1", nil) 75 | two, _ := http.NewRequest("GET", "http://localhost:8080/test/two/2", nil) 76 | 77 | for i := 0; i < 1000; i++ { 78 | res1 := httptest.NewRecorder() 79 | res2 := httptest.NewRecorder() 80 | 81 | go y.ServeHTTP(res1, one) 82 | go y.ServeHTTP(res2, two) 83 | } 84 | } 85 | 86 | func TestNotFoundResponse(t *testing.T) { 87 | y := New() 88 | 89 | r := new(MockResource) 90 | y.Add("/test", r) 91 | 92 | req, _ := http.NewRequest("GET", "http://localhost:8080/route/not/match", nil) 93 | res := httptest.NewRecorder() 94 | y.ServeHTTP(res, req) 95 | 96 | if res.Code != 404 { 97 | t.Error("Non matching route should return 404 response") 98 | } 99 | } 100 | --------------------------------------------------------------------------------