├── .hound.yml ├── docs ├── response.md ├── handler_file.json ├── request_form_input.json ├── request.json ├── request_file_upload.json └── request_multiple_file_uploads.json ├── helpers.go ├── app.go ├── httpmethods.go ├── attributes.go ├── handler.go ├── params.go ├── LINCENCE.md ├── README.md ├── response.go ├── uploadedfile.go ├── path.go ├── middleware.go ├── controller.go ├── request.go ├── .gitignore ├── router.go └── tree.go /.hound.yml: -------------------------------------------------------------------------------- 1 | go: 2 | enabled: true 3 | -------------------------------------------------------------------------------- /docs/response.md: -------------------------------------------------------------------------------- 1 | Proto HTTP/1.1 2 | Status Code 200 OK 3 | Content-Type: application/json; charset=UTF-8 4 | Date: Sun, 12 Jul 2015 18:26:51 GMT 5 | Content-Length: 1248 -------------------------------------------------------------------------------- /docs/handler_file.json: -------------------------------------------------------------------------------- 1 | { 2 | "Filename": "middleware.png", 3 | "Header": { 4 | "Content-Disposition": ["form-data; name=\"uploadfile\"; filename=\"middleware.png\""], 5 | "Content-Type": ["image/png"] 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /helpers.go: -------------------------------------------------------------------------------- 1 | package frodo 2 | 3 | import "fmt" 4 | 5 | // Little function to convert type "func(http.ResponseWriter, *Request)" to Frodo.HandleFunc 6 | func makeHandler(h Handler) Handler { 7 | fmt.Println("converting func(http.ResponseWriter, *Request) to Frodo.Handler") 8 | return h 9 | } 10 | -------------------------------------------------------------------------------- /app.go: -------------------------------------------------------------------------------- 1 | package frodo 2 | 3 | // New returns a new initialized Router. 4 | // Path auto-correction, including trailing slashes, is enabled by default. 5 | func New() *Router { 6 | return &Router{ 7 | RedirectTrailingSlash: true, 8 | RedirectFixedPath: true, 9 | HandleMethodNotAllowed: true, 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /httpmethods.go: -------------------------------------------------------------------------------- 1 | package frodo 2 | 3 | // Methods type is used in Match method to get all methods user wants to apply 4 | // that will help in invoking the related handler 5 | type Methods []string 6 | 7 | // MethodsAllowed -- HTTP Methods/Verbs allowed 8 | var MethodsAllowed = Methods{"GET", "POST", "PUT", "DELETE", "PATCH", "HEAD", "OPTIONS"} 9 | -------------------------------------------------------------------------------- /attributes.go: -------------------------------------------------------------------------------- 1 | package frodo 2 | 3 | // Attributes will be used as an optional argument while declaring routes 4 | // it lets you: 5 | // - Name a Controller or Handler 6 | // - define the specific Method to be used in a Controller 7 | // - a list of Middlewares that should run before the specific Controller 8 | type Attributes struct { 9 | Method, Name string 10 | Middleware []string 11 | } 12 | -------------------------------------------------------------------------------- /handler.go: -------------------------------------------------------------------------------- 1 | package frodo 2 | 3 | import ( 4 | "net/http" 5 | ) 6 | 7 | // Handler is a function that can be registered to a route to handle HTTP requests. 8 | type Handler func(http.ResponseWriter, *Request) 9 | 10 | // Next enables Handler types to be treated as Middleware too 11 | func (h Handler) Next(r ...interface{}) { 12 | } 13 | 14 | // ControllerHandle is used to incubate the Controller Methods and it's Attributes 15 | // since the Attributes are lost in the previous ways of attaching them to routes 16 | type ControllerHandle struct { 17 | Handler CRUDController 18 | Attributes Attributes 19 | } 20 | 21 | // Next enables ControllerHandle types to be treated as Middleware too 22 | func (h ControllerHandle) Next(r ...interface{}) { 23 | } 24 | -------------------------------------------------------------------------------- /params.go: -------------------------------------------------------------------------------- 1 | package frodo 2 | 3 | // Params is a Param-slice, as returned by the router. 4 | // The slice is ordered, the first URL parameter is also the first slice value. 5 | // It is therefore safe to read values by the index. 6 | type Params map[string]string 7 | 8 | // GetParam returns the value of the first Param which key matches the given name. 9 | // If no matching Param is found, an empty string is returned. 10 | func (p Params) GetParam(name string) string { 11 | value, ok := p[name] 12 | if ok { 13 | return value 14 | } 15 | return "" 16 | } 17 | 18 | // Param is shorter equivalent of `GetParam` method 19 | func (p Params) Param(name string) string { 20 | return p.GetParam(name) 21 | } 22 | 23 | // SetParam adds a key/value pair to the Request params 24 | func (p Params) SetParam(name, value string) bool { 25 | // 1st check if it has been initialised 26 | if p != nil { 27 | // If not initialise 28 | p = make(map[string]string) 29 | } 30 | 31 | // allow overwriting 32 | p[name] = value 33 | return true 34 | } 35 | -------------------------------------------------------------------------------- /LINCENCE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | Copyright © 2015 Eugene Mutai 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 5 | 6 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 7 | 8 | THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 9 | -------------------------------------------------------------------------------- /docs/request_form_input.json: -------------------------------------------------------------------------------- 1 | { 2 | "Method": "POST", 3 | "URL": { 4 | "Scheme": "", 5 | "Opaque": "", 6 | "User": null, 7 | "Host": "", 8 | "Path": "/login", 9 | "RawQuery": "username=astaxie", 10 | "Fragment": "" 11 | }, 12 | "Proto": "HTTP/1.1", 13 | "ProtoMajor": 1, 14 | "ProtoMinor": 1, 15 | "Header": { 16 | "Accept": ["text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8"], 17 | "Accept-Encoding": ["gzip, deflate"], 18 | "Accept-Language": ["en-US,en;q=0.8,de;q=0.6,sw;q=0.4"], 19 | "Cache-Control": ["max-age=0"], 20 | "Connection": ["keep-alive"], 21 | "Content-Length": ["77"], 22 | "Content-Type": ["application/x-www-form-urlencoded"], 23 | "Cookie": ["_ga=GA1.4.1581308941.1425994740"], 24 | "Dnt": ["1"], 25 | "Origin": ["http://127.0.0.1:9090"], 26 | "Referer": ["http://127.0.0.1:9090/login"], 27 | "User-Agent": ["Mozilla/5.0 (Macintosh; Intel Mac OS X 10_10_4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/43.0.2357.132 Safari/537.36"] 28 | }, 29 | "Body": {}, 30 | "ContentLength": 77, 31 | "TransferEncoding": null, 32 | "Close": false, 33 | "Host": "127.0.0.1:9090", 34 | "Form": { 35 | "password": ["wild1s75"], 36 | "token": ["8bc28d1ef277735ca3ccc5e1ba36a6fd"], 37 | "username": ["eugenemutai", "astaxie"] 38 | }, 39 | "PostForm": { 40 | "password": ["wild1s75"], 41 | "token": ["8bc28d1ef277735ca3ccc5e1ba36a6fd"], 42 | "username": ["eugenemutai"] 43 | }, 44 | "MultipartForm": null, 45 | "Trailer": null, 46 | "RemoteAddr": "127.0.0.1:62119", 47 | "RequestURI": "/login?username=astaxie", 48 | "TLS": null 49 | } 50 | -------------------------------------------------------------------------------- /docs/request.json: -------------------------------------------------------------------------------- 1 | { 2 | "Method": "GET", 3 | "URL": { 4 | "Scheme": "", 5 | "Opaque": "", 6 | "User": null, 7 | "Host": "", 8 | "Path": "/json", 9 | "RawQuery": "", 10 | "Fragment": "" 11 | }, 12 | "Proto": "HTTP/1.1", 13 | "ProtoMajor": 1, 14 | "ProtoMinor": 1, 15 | "Header": { 16 | "Accept": ["text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8"], 17 | "Accept-Encoding": ["gzip, deflate, sdch"], 18 | "Accept-Language": ["en-US,en;q=0.8,de;q=0.6,sw;q=0.4"], 19 | "Connection": ["keep-alive"], 20 | "Cookie": ["visited=yes; _jsuid=3194997667; ajs_anonymous_id=%22026e6744-47cc-40d5-9df2-6ea8ab9f2261%22; mp_ebcdcca3aad447bca3c21744819c0365_mixpanel=%7B%22distinct_id%22%3A%20%2214bbffe90686b1-06ff65766-32677c02-fa000-14bbffe9069b5b%22%2C%22%24initial_referrer%22%3A%20%22%24direct%22%2C%22%24initial_referring_domain%22%3A%20%22%24direct%22%7D; ai_user=82CA416F-0A45-4C27-AA2D-89527BFFA1D7|2015-03-21T13:29:21.733Z; ajs_user_id=null; ajs_group_id=null; _ga=GA1.1.1400103185.1424855307; _hp2_id.3250765848=4998398632575462.1934775901.1006734726; __atuvc=0%7C18%2C0%7C19%2C9%7C20%2C104%7C21%2C68%7C22"], 21 | "Dnt": ["1"], 22 | "User-Agent": ["Mozilla/5.0 (Macintosh; Intel Mac OS X 10_10_3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/43.0.2357.130 Safari/537.36"] 23 | }, 24 | "Body": { 25 | "Closer": { 26 | "Reader": null 27 | } 28 | }, 29 | "ContentLength": 0, 30 | "TransferEncoding": null, 31 | "Close": false, 32 | "Host": "localhost:8080", 33 | "Form": null, 34 | "PostForm": null, 35 | "MultipartForm": null, 36 | "Trailer": null, 37 | "RemoteAddr": "[::1]:54493", 38 | "RequestURI": "/json", 39 | "TLS": null 40 | } 41 | -------------------------------------------------------------------------------- /docs/request_file_upload.json: -------------------------------------------------------------------------------- 1 | { 2 | "Method": "POST", 3 | "URL": { 4 | "Scheme": "", 5 | "Opaque": "", 6 | "User": null, 7 | "Host": "", 8 | "Path": "/upload", 9 | "RawQuery": "", 10 | "Fragment": "" 11 | }, 12 | "Proto": "HTTP/1.1", 13 | "ProtoMajor": 1, 14 | "ProtoMinor": 1, 15 | "Header": { 16 | "Accept": ["text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8"], 17 | "Accept-Encoding": ["gzip, deflate"], 18 | "Accept-Language": ["en-US,en;q=0.8,de;q=0.6,sw;q=0.4"], 19 | "Cache-Control": ["max-age=0"], 20 | "Connection": ["keep-alive"], 21 | "Content-Length": ["304599"], 22 | "Content-Type": ["multipart/form-data; boundary=----WebKitFormBoundarypujW24qVJP1ZcT41"], 23 | "Cookie": ["_ga=GA1.4.1581308941.1425994740"], 24 | "Dnt": ["1"], 25 | "Origin": ["http://localhost:9090"], 26 | "Referer": ["http://localhost:9090/"], 27 | "User-Agent": ["Mozilla/5.0 (Macintosh; Intel Mac OS X 10_10_4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/43.0.2357.132 Safari/537.36"] 28 | }, 29 | "Body": {}, 30 | "ContentLength": 304599, 31 | "TransferEncoding": null, 32 | "Close": false, 33 | "Host": "127.0.0.1:9090", 34 | "Form": { 35 | "submit": ["upload"], 36 | "token": ["a406df3e0954d6fbf3e5e566b3c62158"] 37 | }, 38 | "PostForm": {}, 39 | "MultipartForm": { 40 | "Value": { 41 | "submit": ["upload"], 42 | "token": ["a406df3e0954d6fbf3e5e566b3c62158"] 43 | }, 44 | "File": { 45 | "uploadfile": [{ 46 | "Filename": "materializw.png", 47 | "Header": { 48 | "Content-Disposition": ["form-data; name=\"uploadfile\"; filename=\"materializw.png\""], 49 | "Content-Type": ["image/png"] 50 | } 51 | }] 52 | } 53 | }, 54 | "Trailer": null, 55 | "RemoteAddr": "127.0.0.1:60961", 56 | "RequestURI": "/upload", 57 | "TLS": null 58 | } 59 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Frodo (A Tiny Go Web Framework) 2 | 3 | [Frodo](http://godoc.org/github.com/kn9ts/frodo) is a Go micro web framework inspired by ExpressJS. 4 | 5 | __NOTE:__ _I built it to so as to learn Go, and also how frameworks work. A big thanks to TJ Holowaychuk too 6 | for the inspiration_ 7 | 8 | Are you looking for the **[GoDocs Documentation](http://godoc.org/github.com/kn9ts/frodo)** 9 | 10 | #### Updates 11 | 12 | - Intergrated(actually interweaved into the code base) and using [httprouter](https://github.com/julienschmidt/httprouter) as the framework's routing system 13 | - Accepts handlers as middleware now by default, one or more 14 | 15 | #### "Hello world" example 16 | 17 | The `main.go` file: 18 | 19 | ```go 20 | package main 21 | 22 | import ( 23 | "net/http" 24 | "github.com/kn9ts/frodo" 25 | ) 26 | 27 | func main() { 28 | app := frodo.New() 29 | 30 | app.Get("/", one, two, three) 31 | app.Get("/hello/:name", one, nameFunction) 32 | 33 | app.Serve() 34 | } 35 | ``` 36 | 37 | And the functions passed as middleware would look like: 38 | 39 | ```go 40 | package main 41 | 42 | func one(w http.ResponseWriter, r *frodo.Request) { 43 | fmt.Println("Hello, am the 1st middleware!") 44 | // fmt.Fprint(w, "Hello, I'm 1st!\n") 45 | r.Next() 46 | } 47 | 48 | func two(w http.ResponseWriter, r *frodo.Request) { 49 | fmt.Println("Hello, am function no. 2!") 50 | // fmt.Fprint(w, "Hello, am function no. 2!\n") 51 | r.Next() 52 | } 53 | 54 | func three(w http.ResponseWriter, r *frodo.Request) { 55 | fmt.Println("Hello, am function no 3!") 56 | fmt.Fprint(w, "Hey, am function no. 3!\n") 57 | } 58 | 59 | func nameFunction(w http.ResponseWriter, r *frodo.Request) { 60 | fmt.Println("Hello there, ", r.GetParam("name")) 61 | fmt.Fprintf(w, "Hello there, %s!\n", r.GetParam("name")) 62 | } 63 | ``` 64 | 65 | #### To do (after Go sabitcal is over) 66 | 67 | - Controllers (which will implement a BaseController) 68 | - Controllers can be mixed with the common handlers as middleware 69 | - Ability to detect CRUD requests and run the right controller method, if a controllers are passed as middleware 70 | 71 | ## Release History 72 | 73 | **Version: 0.10.0** 74 | 75 | ## License 76 | 77 | Copyright (c) 2014 **Eugene Mutai** 78 | Licensed under the [MIT license](http://mit-license.org/) 79 | -------------------------------------------------------------------------------- /response.go: -------------------------------------------------------------------------------- 1 | package frodo 2 | 3 | import ( 4 | "bufio" 5 | "errors" 6 | "net" 7 | "net/http" 8 | "time" 9 | 10 | "log" 11 | ) 12 | 13 | const customErrorMessage string = "[ERROR] Headers were already written." 14 | 15 | // ResponseWriter is used to hijack/embed http.ResponseWriter 16 | // thus making it satisfy the ResponseWriter interface, we then add a written boolean property 17 | // to trace when a write made, with a couple of other helpful properties 18 | type ResponseWriter struct { 19 | http.ResponseWriter 20 | headerWritten bool 21 | written bool 22 | timeStart time.Time 23 | timeEnd time.Time 24 | duration float64 25 | statusCode int 26 | size int64 27 | method string 28 | route string 29 | } 30 | 31 | // Write writes data back the client/creates the body 32 | func (w *ResponseWriter) Write(bytes []byte) (int, error) { 33 | if !w.HeaderWritten() { 34 | w.WriteHeader(http.StatusOK) 35 | } 36 | 37 | if w.ResponseSent() { 38 | log.Println(customErrorMessage) 39 | return 1, errors.New(customErrorMessage) 40 | } 41 | 42 | sent, err := w.ResponseWriter.Write(bytes) 43 | if err != nil { 44 | return sent, err 45 | } 46 | w.size += int64(sent) 47 | w.timeEnd = time.Now() 48 | w.duration = time.Since(w.timeEnd).Seconds() 49 | return sent, nil 50 | } 51 | 52 | // WriteHeader writes the Headers out 53 | func (w *ResponseWriter) WriteHeader(code int) { 54 | if w.HeaderWritten() { 55 | log.Println(customErrorMessage) 56 | return 57 | } 58 | w.ResponseWriter.WriteHeader(code) 59 | w.headerWritten = true 60 | w.statusCode = code 61 | } 62 | 63 | // ResponseSent checks if a write has been made 64 | // starts with a header being sent out 65 | func (w *ResponseWriter) ResponseSent() bool { 66 | return w.written 67 | } 68 | 69 | // HeaderWritten checks if a write has been made 70 | // starts with a header being sent out 71 | func (w *ResponseWriter) HeaderWritten() bool { 72 | return w.headerWritten 73 | } 74 | 75 | // Hijack wraps response writer's Hijack function. 76 | func (w *ResponseWriter) Hijack() (net.Conn, *bufio.ReadWriter, error) { 77 | return w.ResponseWriter.(http.Hijacker).Hijack() 78 | } 79 | 80 | // CloseNotify wraps response writer's CloseNotify function. 81 | func (w *ResponseWriter) CloseNotify() <-chan bool { 82 | return w.ResponseWriter.(http.CloseNotifier).CloseNotify() 83 | } 84 | 85 | // Size returns the size of the response 86 | // about to be sent out 87 | func (w *ResponseWriter) Size() int64 { 88 | return w.size 89 | } 90 | -------------------------------------------------------------------------------- /uploadedfile.go: -------------------------------------------------------------------------------- 1 | package frodo 2 | 3 | import ( 4 | "fmt" 5 | "io" 6 | "mime/multipart" 7 | "os" 8 | "path/filepath" 9 | "reflect" 10 | ) 11 | 12 | // FileUploadsPath for now, declares the path to upload the files 13 | var FileUploadsPath = "./assets/uploads/" 14 | 15 | // UploadedFile struct/type is the data that makes up an uploaded file 16 | // once it is recieved and parsed eg. using request.FormFile() 17 | type UploadedFile struct { 18 | multipart.File 19 | *multipart.FileHeader 20 | /* 21 | type FileHeader struct { 22 | Filename string 23 | Header textproto.MIMEHeader 24 | } 25 | */ 26 | } 27 | 28 | // Name returns the name of the file when it was uploaded 29 | func (file *UploadedFile) Name() string { 30 | // found in *multipart.FileHeader 31 | return file.Filename 32 | } 33 | 34 | // Size returns the size of the file in question 35 | func (file *UploadedFile) Size() int64 { 36 | defer file.Close() 37 | return file.Size() 38 | } 39 | 40 | // Extension returns the extension of the file uploaded 41 | func (file *UploadedFile) Extension() string { 42 | // _, header, error := r.FormFile(name) 43 | ext := filepath.Ext(file.Filename) 44 | return ext 45 | } 46 | 47 | // Move basically moves/transfers the uploaded file to the upload folder provided 48 | // 49 | // Using ...interface{} because I want the user to only pass more than one argument 50 | // when changing upload dir and filename, if none is changed then defaults are used 51 | // 52 | // eg. file.Move(true) 53 | // ----- or ----- 54 | // file.Move("../new_upload_path/", "newfilename.png") 55 | // 56 | func (file *UploadedFile) Move(args ...interface{}) bool { 57 | file.Open() 58 | defer file.Close() 59 | name := args[0] 60 | val := reflect.ValueOf(name) 61 | 62 | // If a string was give, then treat is a the FileUploadsPath 63 | if val.Kind().String() == "string" { 64 | FileUploadsPath = name.(string) 65 | } 66 | 67 | var FileName string 68 | // Check to see if a file name was given, 2nd argument 69 | if len(args) > 1 { 70 | FileName = args[1].(string) 71 | } else { 72 | FileName = file.Name() 73 | } 74 | 75 | savedFile, err := os.OpenFile(FileUploadsPath+FileName, os.O_WRONLY|os.O_CREATE, 0666) 76 | if err != nil { 77 | fmt.Println(err) 78 | return false 79 | } 80 | 81 | _, ioerr := io.Copy(savedFile, file) 82 | if ioerr != nil { 83 | fmt.Println(ioerr) 84 | return false 85 | } 86 | 87 | return true 88 | } 89 | 90 | // MimeType returns the mime/type of the file uploaded 91 | func (file *UploadedFile) MimeType() string { 92 | mimetype := file.Header.Get("Content-Type") 93 | return mimetype 94 | } 95 | 96 | // IsValid checks if the file is alright by opening it up 97 | // if errors come up while opening it is an invalid upload 98 | func (file *UploadedFile) IsValid() bool { 99 | _, err := file.Open() 100 | defer file.Close() 101 | if err != nil { 102 | return false 103 | } 104 | return true 105 | } 106 | -------------------------------------------------------------------------------- /path.go: -------------------------------------------------------------------------------- 1 | package frodo 2 | 3 | // CleanPath is the URL version of path.Clean, it returns a canonical URL path 4 | // for p, eliminating . and .. elements. 5 | // 6 | // The following rules are applied iteratively until no further processing can 7 | // be done: 8 | // 1. Replace multiple slashes with a single slash. 9 | // 2. Eliminate each . path name element (the current directory). 10 | // 3. Eliminate each inner .. path name element (the parent directory) 11 | // along with the non-.. element that precedes it. 12 | // 4. Eliminate .. elements that begin a rooted path: 13 | // that is, replace "/.." by "/" at the beginning of a path. 14 | // 15 | // If the result of this process is an empty string, "/" is returned 16 | func CleanPath(p string) string { 17 | // Turn empty string into "/" 18 | if p == "" { 19 | return "/" 20 | } 21 | 22 | n := len(p) 23 | var buf []byte 24 | 25 | // Invariants: 26 | // reading from path; r is index of next byte to process. 27 | // writing to buf; w is index of next byte to write. 28 | 29 | // path must start with '/' 30 | r := 1 31 | w := 1 32 | 33 | if p[0] != '/' { 34 | r = 0 35 | buf = make([]byte, n+1) 36 | buf[0] = '/' 37 | } 38 | 39 | trailing := n > 2 && p[n-1] == '/' 40 | 41 | // A bit more clunky without a 'lazybuf' like the path package, but the loop 42 | // gets completely inlined (bufApp). So in contrast to the path package this 43 | // loop has no expensive function calls (except 1x make) 44 | 45 | for r < n { 46 | switch { 47 | case p[r] == '/': 48 | // empty path element, trailing slash is added after the end 49 | r++ 50 | 51 | case p[r] == '.' && r+1 == n: 52 | trailing = true 53 | r++ 54 | 55 | case p[r] == '.' && p[r+1] == '/': 56 | // . element 57 | r++ 58 | 59 | case p[r] == '.' && p[r+1] == '.' && (r+2 == n || p[r+2] == '/'): 60 | // .. element: remove to last / 61 | r += 2 62 | 63 | if w > 1 { 64 | // can backtrack 65 | w-- 66 | 67 | if buf == nil { 68 | for w > 1 && p[w] != '/' { 69 | w-- 70 | } 71 | } else { 72 | for w > 1 && buf[w] != '/' { 73 | w-- 74 | } 75 | } 76 | } 77 | 78 | default: 79 | // real path element. 80 | // add slash if needed 81 | if w > 1 { 82 | bufApp(&buf, p, w, '/') 83 | w++ 84 | } 85 | 86 | // copy element 87 | for r < n && p[r] != '/' { 88 | bufApp(&buf, p, w, p[r]) 89 | w++ 90 | r++ 91 | } 92 | } 93 | } 94 | 95 | // re-append trailing slash 96 | if trailing && w > 1 { 97 | bufApp(&buf, p, w, '/') 98 | w++ 99 | } 100 | 101 | if buf == nil { 102 | return p[:w] 103 | } 104 | return string(buf[:w]) 105 | } 106 | 107 | // internal helper to lazily create a buffer if necessary 108 | func bufApp(buf *[]byte, s string, w int, c byte) { 109 | if *buf == nil { 110 | if s[w] == c { 111 | return 112 | } 113 | 114 | *buf = make([]byte, len(s)) 115 | copy(*buf, s[:w]) 116 | } 117 | (*buf)[w] = c 118 | } 119 | -------------------------------------------------------------------------------- /docs/request_multiple_file_uploads.json: -------------------------------------------------------------------------------- 1 | { 2 | "Method": "POST", 3 | "URL": { 4 | "Scheme": "", 5 | "Opaque": "", 6 | "User": null, 7 | "Host": "", 8 | "Path": "/receive_multiple", 9 | "RawQuery": "", 10 | "Fragment": "" 11 | }, 12 | "Proto": "HTTP/1.1", 13 | "ProtoMajor": 1, 14 | "ProtoMinor": 1, 15 | "Header": { 16 | "Accept": ["text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8"], 17 | "Accept-Encoding": ["gzip, deflate"], 18 | "Accept-Language": ["en-US,en;q=0.8,de;q=0.6,sw;q=0.4"], 19 | "Cache-Control": ["no-cache"], 20 | "Connection": ["keep-alive"], 21 | "Content-Length": ["761228"], 22 | "Content-Type": ["multipart/form-data; boundary=----WebKitFormBoundaryDHH2AnMHXlUzzAQy"], 23 | "Cookie": ["visited=yes; _jsuid=3194997667; ajs_anonymous_id=%22026e6744-47cc-40d5-9df2-6ea8ab9f2261%22; mp_ebcdcca3aad447bca3c21744819c0365_mixpanel=%7B%22distinct_id%22%3A%20%2214bbffe90686b1-06ff65766-32677c02-fa000-14bbffe9069b5b%22%2C%22%24initial_referrer%22%3A%20%22%24direct%22%2C%22%24initial_referring_domain%22%3A%20%22%24direct%22%7D; ai_user=82CA416F-0A45-4C27-AA2D-89527BFFA1D7|2015-03-21T13:29:21.733Z; ajs_user_id=null; ajs_group_id=null; _ga=GA1.1.1400103185.1424855307; _hp2_id.3250765848=4998398632575462.1934775901.1006734726; __atuvc=0%7C18%2C0%7C19%2C9%7C20%2C104%7C21%2C68%7C22"], 24 | "Dnt": ["1"], 25 | "Origin": ["null"], 26 | "Pragma": ["no-cache"], 27 | "User-Agent": ["Mozilla/5.0 (Macintosh; Intel Mac OS X 10_10_4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/43.0.2357.132 Safari/537.36"] 28 | }, 29 | "Body": {}, 30 | "ContentLength": 761228, 31 | "TransferEncoding": null, 32 | "Close": false, 33 | "Host": "localhost:9090", 34 | "Form": { 35 | "submit": ["Submit"] 36 | }, 37 | "PostForm": {}, 38 | "MultipartForm": { 39 | "Value": { 40 | "submit": ["Submit"] 41 | }, 42 | "File": { 43 | "uploadfiles": [{ 44 | "Filename": "middleware.png", 45 | "Header": { 46 | "Content-Disposition": ["form-data; name=\"multiplefiles\"; filename=\"middleware.png\""], 47 | "Content-Type": ["image/png"] 48 | } 49 | }, { 50 | "Filename": "atom.png", 51 | "Header": { 52 | "Content-Disposition": ["form-data; name=\"multiplefiles\"; filename=\"atom.png\""], 53 | "Content-Type": ["image/png"] 54 | } 55 | }, { 56 | "Filename": "gopher.jpg", 57 | "Header": { 58 | "Content-Disposition": ["form-data; name=\"multiplefiles\"; filename=\"gopher.jpg\""], 59 | "Content-Type": ["image/jpeg"] 60 | } 61 | }] 62 | } 63 | }, 64 | "Trailer": null, 65 | "RemoteAddr": "[::1]:62871", 66 | "RequestURI": "/receive_multiple", 67 | "TLS": null 68 | } 69 | -------------------------------------------------------------------------------- /middleware.go: -------------------------------------------------------------------------------- 1 | package frodo 2 | 3 | import "fmt" 4 | 5 | // Middleware declares the minimum implementation 6 | // necessary for a handlers to be used as Frodo's middleware route Handlers 7 | type Middleware interface { 8 | Next(...interface{}) 9 | } 10 | 11 | // RequestMiddleware is a collection of Middlwares 12 | // this struct will have the method next to invoke the middleware in the chain 13 | // one after the other 14 | type RequestMiddleware struct { 15 | handlers []Middleware // []Handle 16 | ResponseWriter *ResponseWriter 17 | Request *Request 18 | total int 19 | nextPosition int 20 | } 21 | 22 | func (m *RequestMiddleware) chainReaction() { 23 | m.nextPosition++ 24 | m.typeCastAndCall(m.handlers[0]) 25 | } 26 | 27 | // Next will be used to call the next handler in line/queue 28 | func (m *RequestMiddleware) Next(args ...interface{}) { 29 | // 1st check if the next handler position accounts for the number 30 | // of handlers existing in the handlers array 31 | if m.nextPosition < m.total { 32 | // get the next handler 33 | nextHandle := m.handlers[m.nextPosition] 34 | // move the cursor 35 | m.nextPosition++ 36 | // 1st check if a write has happened 37 | // have headers been sent out? 38 | // meaning a response has been issued out to the client 39 | // if not call the next handler in line 40 | if !m.ResponseWriter.HeaderWritten() { 41 | m.typeCastAndCall(nextHandle) 42 | } 43 | } 44 | } 45 | 46 | // typeCastAndCall converts the middleware to it's rightful type then calls it 47 | func (m *RequestMiddleware) typeCastAndCall(run Middleware) { 48 | // 1st check if the route handler is HandleFunc 49 | if handle, hasTypeCasted := run.(Handler); hasTypeCasted { 50 | handle(m.ResponseWriter, m.Request) 51 | } else { 52 | // if not, then is it an implementation of ControllerInterface 53 | if ctrl, ok := run.(CRUDController); ok { 54 | // Yes! it is. 55 | // Ok! check if a method was specified to run 56 | // if name := ctrl.Method; name != "" { 57 | // // if so check that Method exists 58 | // v := reflect.ValueOf(ctrl) 59 | // 60 | // // check for the method by it's name 61 | // fn := v.MethodByName(name) 62 | // 63 | // // if a Method was found, not a Zero value 64 | // if fn != (reflect.Value{}) { 65 | // // Then convert it back to a Handler 66 | // // You have to know which type it is you are converting to 67 | // if value, ok := fn.Interface().(func(http.ResponseWriter, *Request)); ok && fn.Kind().String() == "func" { 68 | // // morph it to it's dynamic data type, and run it 69 | // makeHandler(value)(m.ResponseWriter, m.Request) 70 | // return 71 | // } 72 | // } else { 73 | // // Method given in use does not exist 74 | // err := fmt.Errorf("Error: Method undefined (The Controller has no field or method %s)", name) 75 | // panic(err) 76 | // } 77 | // } else { 78 | // // Nothing like so were found, run internal server error: 500 79 | // fmt.Println("No Method specified to run in Controller, defaulting to Index method") 80 | // ctrl.Index(m.ResponseWriter, m.Request) 81 | // return 82 | // } 83 | 84 | // If no Controller.Attribute.Method was provided, run Index as the default fallback 85 | ctrl.Index(m.ResponseWriter, m.Request) 86 | } else { 87 | // No Handler or Controller was found, run internal server error: 500 88 | m.ResponseWriter.WriteHeader(404) 89 | fmt.Fprintf(m.ResponseWriter, "No Frodo.Handle or Frodo.Controller exists to handle the route.") 90 | } 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /controller.go: -------------------------------------------------------------------------------- 1 | package frodo 2 | 3 | import ( 4 | "net/http" 5 | ) 6 | 7 | // BaseController defines the basic structure of a REST application controller, 8 | // the devs controller should embed this to create their own controllers. 9 | // It then automatically implements CRUDController, and can be passed as a controller in routing 10 | type BaseController struct { 11 | Attributes 12 | } 13 | 14 | // CRUDController can be used to parse back the developer's controller back to it's own type. 15 | // Since we know that a REST controller entails the following methods then any struct 16 | // that implements the Controller methods suffices the CRUDController 17 | type CRUDController interface { 18 | Index(http.ResponseWriter, *Request) 19 | Create(http.ResponseWriter, *Request) 20 | Store(http.ResponseWriter, *Request) 21 | Show(http.ResponseWriter, *Request) 22 | Edit(http.ResponseWriter, *Request) 23 | Update(http.ResponseWriter, *Request) 24 | Patch(http.ResponseWriter, *Request) 25 | Destroy(http.ResponseWriter, *Request) 26 | Head(http.ResponseWriter, *Request) 27 | Options(http.ResponseWriter, *Request) 28 | Next(...interface{}) 29 | } 30 | 31 | // Index is the default handler for any incoming request or route's request that is not matched to it's handler 32 | // It can also be used for specific route, mostly for the root routes("/") 33 | func (c *BaseController) Index(w http.ResponseWriter, r *Request) { 34 | http.Error(w, "Method Not Allowed", 405) 35 | } 36 | 37 | // Create handles a request to Show the form for creating a new resource. 38 | // * GET /posts/create 39 | func (c *BaseController) Create(w http.ResponseWriter, r *Request) { 40 | http.Error(w, "Method Not Allowed", 405) 41 | } 42 | 43 | // Store handles a request to Store a newly created resource in storage. 44 | // * POST /posts 45 | func (c *BaseController) Store(w http.ResponseWriter, r *Request) { 46 | http.Error(w, "Method Not Allowed", 405) 47 | } 48 | 49 | // Show handles a request to Display the specified resource. 50 | // * GET /posts/{id} 51 | func (c *BaseController) Show(w http.ResponseWriter, r *Request) { 52 | http.Error(w, "Method Not Allowed", 405) 53 | } 54 | 55 | // Edit handles a request to Show the form for editing the specified resource. 56 | // * GET /posts/{id}/edit 57 | func (c *BaseController) Edit(w http.ResponseWriter, r *Request) { 58 | http.Error(w, "Method Not Allowed", 405) 59 | } 60 | 61 | // Update handles a request to update the specified resource in storage. 62 | // * PUT /posts/{id} 63 | func (c *BaseController) Update(w http.ResponseWriter, r *Request) { 64 | http.Error(w, "Method Not Allowed", 405) 65 | } 66 | 67 | // Patch is an alternative to Update 68 | func (c *BaseController) Patch(w http.ResponseWriter, r *Request) { 69 | http.Error(w, "Method Not Allowed", 405) 70 | } 71 | 72 | // Destroy handles a request to Remove the specified resource from storage. 73 | // * DELETE /posts/{id} 74 | func (c *BaseController) Destroy(w http.ResponseWriter, r *Request) { 75 | http.Error(w, "Method Not Allowed", 405) 76 | } 77 | 78 | // Head handle HEAD request. 79 | func (c *BaseController) Head(w http.ResponseWriter, r *Request) { 80 | http.Error(w, "Method Not Allowed", 405) 81 | } 82 | 83 | // Options handle OPTIONS request. 84 | func (c *BaseController) Options(w http.ResponseWriter, r *Request) { 85 | http.Error(w, "Method Not Allowed", 405) 86 | } 87 | 88 | // Next will be used to call the next handler in line/queue 89 | // th biggest change is that it requires the Request struct to be passed as parametre 90 | // to call the next handler in line 91 | // 92 | // it also makes the Controller implement the Middleware type 93 | func (c *BaseController) Next(args ...interface{}) { 94 | // We only need the 1st argument 95 | // the Request Object 96 | // r := args[0].(*Request) 97 | } 98 | -------------------------------------------------------------------------------- /request.go: -------------------------------------------------------------------------------- 1 | package frodo 2 | 3 | import ( 4 | "net/http" 5 | "strings" 6 | ) 7 | 8 | // Request will help facilitate the passing of multiple handlers 9 | type Request struct { 10 | files []*UploadedFile 11 | *http.Request 12 | *RequestMiddleware 13 | Params 14 | } 15 | 16 | // Input gets ALL key/values sent via POST from all methods. 17 | // Keep in mind `r.Form == type url.Values map[string][]string` 18 | func (r *Request) Input(name string) []string { 19 | if r.Form == nil { 20 | r.ParseForm() 21 | } 22 | 23 | if value, ok := r.Form[name]; ok { 24 | return value 25 | } 26 | return nil 27 | } 28 | 29 | // HasInput checks for the existence of the given 30 | // input name in the inputs sent from a FORM 31 | func (r *Request) HasInput(name string) bool { 32 | if r.Form == nil { 33 | r.ParseForm() 34 | } 35 | 36 | _, ok := r.Form[name] 37 | return ok 38 | } 39 | 40 | // HasFile mimics FormFile method from `http.Request` 41 | // func (r *Request) FormFile(key string) (multipart.File, *multipart.FileHeader, error) 42 | func (r *Request) HasFile(name string) bool { 43 | _, _, err := r.FormFile(name) 44 | if err != nil { 45 | return false 46 | } 47 | return true 48 | } 49 | 50 | // UploadedFile gets the file requested that was uploaded 51 | func (r *Request) UploadedFile(name string) (*UploadedFile, error) { 52 | file, header, err := r.FormFile(name) 53 | if err == nil { 54 | return &UploadedFile{file, header}, nil 55 | } 56 | return nil, err 57 | } 58 | 59 | // UploadedFiles parses all uploaded files, creates and returns an array of UploadedFile 60 | // type representing each uploaded file 61 | func (r *Request) UploadedFiles(name string) []*UploadedFile { 62 | // Instantiate r.files 63 | if r.files == nil { 64 | r.files = make([]*UploadedFile, len(r.MultipartForm.File[name])) 65 | r.ParseMultipartForm(32 << 20) 66 | } 67 | 68 | for _, header := range r.MultipartForm.File[name] { 69 | file, _ := header.Open() 70 | r.files = append(r.files, &UploadedFile{file, header}) 71 | } 72 | 73 | return r.files 74 | } 75 | 76 | // MoveAll is a neat trick to upload all the files that 77 | // have been parsed. Awesome for bulk uploading, and storage. 78 | func (r *Request) MoveAll(args ...interface{}) (bool, int) { 79 | if r.files == nil { 80 | return false, 0 81 | } 82 | 83 | count := 0 84 | for _, file := range r.files { 85 | moved := file.Move(args...) 86 | if moved { 87 | count++ 88 | } 89 | } 90 | 91 | if count == len(r.files) { 92 | return true, count 93 | } 94 | return false, count 95 | } 96 | 97 | // ClientIP implements a best effort algorithm to return the real client IP, it parses 98 | // X-Real-IP and X-Forwarded-For in order to work properly with reverse-proxies such us: nginx or haproxy. 99 | func (r *Request) ClientIP() string { 100 | if true { 101 | clientIP := strings.TrimSpace(r.Request.Header.Get("X-Real-Ip")) 102 | if len(clientIP) > 0 { 103 | return clientIP 104 | } 105 | clientIP = r.Request.Header.Get("X-Forwarded-For") 106 | if index := strings.IndexByte(clientIP, ','); index >= 0 { 107 | clientIP = clientIP[0:index] 108 | } 109 | clientIP = strings.TrimSpace(clientIP) 110 | if len(clientIP) > 0 { 111 | return clientIP 112 | } 113 | } 114 | return strings.TrimSpace(r.Request.RemoteAddr) 115 | } 116 | 117 | // IsAjax checks if the Request was made via AJAX, 118 | // the XMLHttpRequest will usually be sent with a X-Requested-With HTTP header. 119 | func (r *Request) IsAjax() bool { 120 | if r.Request.Header.Get("X-Request-With") != "" { 121 | return true 122 | } 123 | return false 124 | } 125 | 126 | // IsXhr gives user a choice in whichever way he/she feels okay checking for AJAX Request 127 | // It actually calls r.IsAjax() 128 | func (r *Request) IsXhr() bool { 129 | return r.IsAjax() 130 | } 131 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Custom config 2 | *.log 3 | lab/ 4 | docs/ 5 | tmp/ 6 | frodo-reference/ 7 | 8 | # Created by https://www.gitignore.io/api/node,laravel,bower,justcode,linux,osx,jetbrains,sublimetext,vim,xcode 9 | 10 | ### Node ### 11 | # Logs 12 | logs 13 | *.log 14 | npm-debug.log* 15 | 16 | # Runtime data 17 | pids 18 | *.pid 19 | *.seed 20 | 21 | # Directory for instrumented libs generated by jscoverage/JSCover 22 | lib-cov 23 | 24 | # Coverage directory used by tools like istanbul 25 | coverage 26 | 27 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 28 | .grunt 29 | 30 | # node-waf configuration 31 | .lock-wscript 32 | 33 | # Compiled binary addons (http://nodejs.org/api/addons.html) 34 | build/Release 35 | 36 | # Dependency directory 37 | # https://docs.npmjs.com/misc/faq#should-i-check-my-node-modules-folder-into-git 38 | node_modules 39 | 40 | # Optional npm cache directory 41 | .npm 42 | 43 | # Optional REPL history 44 | .node_repl_history 45 | 46 | 47 | ### Laravel ### 48 | vendor/ 49 | node_modules/ 50 | 51 | # Laravel 4 specific 52 | bootstrap/compiled.php 53 | app/storage/ 54 | 55 | # Laravel 5 & Lumen specific 56 | bootstrap/cache/ 57 | storage/ 58 | .env.*.php 59 | .env.php 60 | .env 61 | .env.example 62 | 63 | ### Bower ### 64 | bower_components 65 | .bower-cache 66 | .bower-registry 67 | .bower-tmp 68 | 69 | 70 | ### JustCode ### 71 | .JustCode 72 | 73 | ### Linux ### 74 | *~ 75 | 76 | # temporary files which can be created if a process still has a handle open of a deleted file 77 | .fuse_hidden* 78 | 79 | # KDE directory preferences 80 | .directory 81 | 82 | # Linux trash folder which might appear on any partition or disk 83 | .Trash-* 84 | 85 | 86 | ### OSX ### 87 | .DS_Store 88 | .AppleDouble 89 | .LSOverride 90 | 91 | # Icon must end with two \r 92 | Icon 93 | 94 | 95 | # Thumbnails 96 | ._* 97 | 98 | # Files that might appear in the root of a volume 99 | .DocumentRevisions-V100 100 | .fseventsd 101 | .Spotlight-V100 102 | .TemporaryItems 103 | .Trashes 104 | .VolumeIcon.icns 105 | 106 | # Directories potentially created on remote AFP share 107 | .AppleDB 108 | .AppleDesktop 109 | Network Trash Folder 110 | Temporary Items 111 | .apdisk 112 | 113 | 114 | ### JetBrains ### 115 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and Webstorm 116 | 117 | *.iml 118 | 119 | ## Directory-based project format: 120 | .idea/ 121 | # if you remove the above rule, at least ignore the following: 122 | 123 | # User-specific stuff: 124 | # .idea/workspace.xml 125 | # .idea/tasks.xml 126 | # .idea/dictionaries 127 | # .idea/shelf 128 | 129 | # Sensitive or high-churn files: 130 | # .idea/dataSources.ids 131 | # .idea/dataSources.xml 132 | # .idea/sqlDataSources.xml 133 | # .idea/dynamic.xml 134 | # .idea/uiDesigner.xml 135 | 136 | # Gradle: 137 | # .idea/gradle.xml 138 | # .idea/libraries 139 | 140 | # Mongo Explorer plugin: 141 | # .idea/mongoSettings.xml 142 | 143 | ## File-based project format: 144 | *.ipr 145 | *.iws 146 | 147 | ## Plugin-specific files: 148 | 149 | # IntelliJ 150 | /out/ 151 | 152 | # mpeltonen/sbt-idea plugin 153 | .idea_modules/ 154 | 155 | # JIRA plugin 156 | atlassian-ide-plugin.xml 157 | 158 | # Crashlytics plugin (for Android Studio and IntelliJ) 159 | com_crashlytics_export_strings.xml 160 | crashlytics.properties 161 | crashlytics-build.properties 162 | fabric.properties 163 | 164 | 165 | ### SublimeText ### 166 | # cache files for sublime text 167 | *.tmlanguage.cache 168 | *.tmPreferences.cache 169 | *.stTheme.cache 170 | 171 | # workspace files are user-specific 172 | *.sublime-workspace 173 | 174 | # project files should be checked into the repository, unless a significant 175 | # proportion of contributors will probably not be using SublimeText 176 | # *.sublime-project 177 | 178 | # sftp configuration file 179 | sftp-config.json 180 | 181 | 182 | ### Vim ### 183 | [._]*.s[a-w][a-z] 184 | [._]s[a-w][a-z] 185 | *.un~ 186 | Session.vim 187 | .netrwhist 188 | *~ 189 | 190 | 191 | ### Xcode ### 192 | # Xcode 193 | # 194 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore 195 | 196 | ## Build generated 197 | build/ 198 | DerivedData 199 | 200 | ## Various settings 201 | *.pbxuser 202 | !default.pbxuser 203 | *.mode1v3 204 | !default.mode1v3 205 | *.mode2v3 206 | !default.mode2v3 207 | *.perspectivev3 208 | !default.perspectivev3 209 | xcuserdata 210 | 211 | ## Other 212 | *.xccheckout 213 | *.moved-aside 214 | *.xcuserstate 215 | -------------------------------------------------------------------------------- /router.go: -------------------------------------------------------------------------------- 1 | package frodo 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | "net/http" 7 | "reflect" 8 | "strconv" 9 | "strings" 10 | "time" 11 | ) 12 | 13 | // Router is a http.Handler which can be used to dispatch requests to different 14 | // handler functions via configurable routes 15 | type Router struct { 16 | trees map[string]*node 17 | 18 | // Enables automatic redirection if the current route can't be matched but a 19 | // handler for the path with (without) the trailing slash exists. 20 | // For example if /foo/ is requested but a route only exists for /foo, the 21 | // client is redirected to /foo with http status code 301 for GET requests 22 | // and 307 for all other request methods. 23 | RedirectTrailingSlash bool 24 | 25 | // If enabled, the router tries to fix the current request path, if no 26 | // handle is registered for it. 27 | // First superfluous path elements like ../ or // are removed. 28 | // Afterwards the router does a case-insensitive lookup of the cleaned path. 29 | // If a handle can be found for this route, the router makes a redirection 30 | // to the corrected path with status code 301 for GET requests and 307 for 31 | // all other request methods. 32 | // For example /FOO and /..//Foo could be redirected to /foo. 33 | // RedirectTrailingSlash is independent of this option. 34 | RedirectFixedPath bool 35 | 36 | // If enabled, the router checks if another method is allowed for the 37 | // current route, if the current request can not be routed. 38 | // If this is the case, the request is answered with 'Method Not Allowed' 39 | // and HTTP status code 405. 40 | // If no other Method is allowed, the request is delegated to the NotFound 41 | // handler. 42 | HandleMethodNotAllowed bool 43 | 44 | // Configurable http.Handler which is called when no matching route is 45 | // found. If it is not set, http.NotFound is used. 46 | NotFoundHandler Handler 47 | 48 | // Configurable http.Handler which is called when a request 49 | // cannot be routed and HandleMethodNotAllowed is true. 50 | // If it is not set, http.Error with http.StatusMethodNotAllowed is used. 51 | MethodNotAllowedHandler Handler 52 | 53 | // Function to handle panics recovered from http handlers. 54 | // It should be used to generate a error page and return the http error code 55 | // 500 (Internal Server Error). 56 | // The handler can be used to keep your server from crashing because of 57 | // unrecovered panics. 58 | PanicHandler Handler 59 | } 60 | 61 | // Make sure the Router conforms with the http.Handler interface 62 | var _ http.Handler = New() 63 | 64 | // Get is a shortcut for router.Handle("GET", path, handle) 65 | func (r *Router) Get(path string, handlers ...interface{}) { 66 | r.Handle("GET", path, handlers...) 67 | } 68 | 69 | // Head is a shortcut for router.Handle("HEAD", path, ...handle) 70 | func (r *Router) Head(path string, handlers ...interface{}) { 71 | r.Handle("HEAD", path, handlers...) 72 | } 73 | 74 | // Options is a shortcut for router.Handle("OPTIONS", path, ...handle) 75 | func (r *Router) Options(path string, handlers ...interface{}) { 76 | r.Handle("OPTIONS", path, handlers...) 77 | } 78 | 79 | // Post is a shortcut for router.Handle("POST", path, ...handle) 80 | func (r *Router) Post(path string, handlers ...interface{}) { 81 | r.Handle("POST", path, handlers...) 82 | } 83 | 84 | // Put is a shortcut for router.Handle("PUT", path, ...handle) 85 | func (r *Router) Put(path string, handlers ...interface{}) { 86 | r.Handle("PUT", path, handlers...) 87 | } 88 | 89 | // Patch is a shortcut for router.Handle("PATCH", path, ...handle) 90 | func (r *Router) Patch(path string, handlers ...interface{}) { 91 | r.Handle("PATCH", path, handlers...) 92 | } 93 | 94 | // Delete is a shortcut for router.Handle("DELETE", path, ...handle) 95 | func (r *Router) Delete(path string, handlers ...interface{}) { 96 | r.Handle("DELETE", path, handlers...) 97 | } 98 | 99 | // Match adds the Handle to the provided Methods/HTTPVerbs for a given route 100 | // EG. GET/POST from /home to have the same Handle 101 | func (r *Router) Match(httpVerbs Methods, path string, handlers ...interface{}) { 102 | if len(httpVerbs) > 0 { 103 | for _, verb := range httpVerbs { 104 | r.Handle(strings.ToUpper(verb), path, handlers...) 105 | } 106 | } 107 | } 108 | 109 | // Any method adds the Handle to all HTTP methods/HTTP verbs for the route given 110 | // it does not add routing Handlers for HEADER and OPTIONS HTTP verbs 111 | func (r *Router) Any(path string, handlers ...interface{}) { 112 | r.Match(Methods{"GET", "POST", "PUT", "DELETE", "PATCH"}, path, handlers...) 113 | } 114 | 115 | // Handle registers a new request handle with the given path and method. 116 | // 117 | // For GET, POST, PUT, PATCH and DELETE requests the respective shortcut 118 | // functions can be used. 119 | // 120 | // This function is intended for bulk loading and to allow the usage of less 121 | // frequently used, non-standardized or custom methods (e.g. for internal 122 | // communication with a proxy). 123 | func (r *Router) Handle(method, path string, handlers ...interface{}) { 124 | // Two things satisfy the Middleware interface 125 | // Controller and a Handler 126 | if path[0] != '/' { 127 | panic("path must begin with '/' in path '" + path + "'") 128 | } 129 | 130 | // this is used to collect all Handlers 131 | var middleware = make([]Middleware, len(handlers)) 132 | 133 | for pos, h := range handlers { 134 | // to recieve the typecasted Controller or Handler 135 | var handle Middleware 136 | 137 | // Check to see if a Handler was provided if not 138 | v := reflect.ValueOf(h).Type() 139 | fmt.Printf("==> Handler provided: %s\n", v) 140 | 141 | // Debug: First of check if it is a Frodo.Handler type 142 | // might have been altered on first/previous loop 143 | // if not check the function if it suffices the Handle type pattern 144 | // If it does -- func(http.ResponseWriter, *Request) 145 | // then convert it to a Frodo.Handler type 146 | if value, isHandler := h.(func(http.ResponseWriter, *Request)); isHandler && v.Kind().String() == "func" { 147 | // morph it to it's dynamic data type 148 | handle = makeHandler(value) 149 | } else { 150 | // It is not a Handler, checked if it is a Controller 151 | if ctrl, isController := h.(CRUDController); isController { 152 | // fmt.Println("converting struct related to Frodo.BaseController") 153 | handle = ctrl 154 | } else { 155 | panic("Error: expected Controller arguement provided to be an extension of " + 156 | "Frodo.BaseController or \"func(http.ResponseWriter, *Frodo.Request)\" type") 157 | } 158 | } 159 | // replace the Middleware with correct Handler 160 | middleware[pos] = handle 161 | } 162 | fmt.Printf("%v and the no %d\n", middleware, len(middleware)) 163 | 164 | if r.trees == nil { 165 | r.trees = make(map[string]*node) 166 | } 167 | 168 | root := r.trees[method] 169 | if root == nil { 170 | root = new(node) 171 | r.trees[method] = root 172 | } 173 | 174 | // store them to it's route node 175 | root.addRoute(path, middleware) 176 | } 177 | 178 | // Handler is an adapter which allows the usage of an 179 | // http.Handler as a request handle. 180 | func (r *Router) Handler(method, path string, handler http.Handler) { 181 | r.Handle(method, path, func(w http.ResponseWriter, req *Request) { 182 | handler.ServeHTTP(w, req.Request) 183 | }) 184 | } 185 | 186 | // HandlerFunc is an adapter which allows the usage of an http.HandlerFunc as a 187 | // request handle. 188 | func (r *Router) HandlerFunc(method, path string, handler http.HandlerFunc) { 189 | r.Handler(method, path, handler) 190 | } 191 | 192 | // ServeFiles serves files from the given file system root. 193 | // The path must end with "/*filepath", files are then served from the local 194 | // path /defined/root/dir/*filepath. 195 | // For example if root is "/etc" and *filepath is "passwd", the local file 196 | // "/etc/passwd" would be served. 197 | // Internally a http.FileServer is used, therefore http.NotFound is used instead 198 | // of the Router's NotFound handler. 199 | // To use the operating system's file system implementation, 200 | // use http.Dir: 201 | // router.ServeFiles("/src/*filepath", http.Dir("/var/www")) 202 | func (r *Router) ServeFiles(path string, root http.FileSystem) { 203 | if len(path) < 10 || path[len(path)-10:] != "/*filepath" { 204 | panic("path must end with /*filepath in path '" + path + "'") 205 | } 206 | 207 | fileServer := http.FileServer(root) 208 | 209 | r.Get(path, func(w http.ResponseWriter, req *Request) { 210 | req.URL.Path = req.GetParam("filepath") 211 | fileServer.ServeHTTP(w, req.Request) 212 | }) 213 | } 214 | 215 | // NotFound can be used to define custom routes to handle NotFound routes 216 | func (r *Router) NotFound(handler Handler) { 217 | r.NotFoundHandler = handler 218 | } 219 | 220 | // MethodNotAllowed can be used to define custom routes 221 | // to handle Methods that are not allowed 222 | func (r *Router) MethodNotAllowed(handler Handler) { 223 | r.MethodNotAllowedHandler = handler 224 | } 225 | 226 | // ServerError can be used to define custom routes to handle OnServerError routes 227 | func (r *Router) ServerError(handler Handler) { 228 | r.PanicHandler = handler 229 | } 230 | 231 | // On404 is shortform for NotFound 232 | func (r *Router) On404(handler Handler) { 233 | r.NotFound(handler) 234 | } 235 | 236 | // On405 is shortform for NotFound 237 | func (r *Router) On405(handler Handler) { 238 | r.MethodNotAllowed(handler) 239 | } 240 | 241 | // On500 is shortform for ServerError 242 | func (r *Router) On500(handler Handler) { 243 | r.ServerError(handler) 244 | } 245 | 246 | func (r *Router) recover(w *ResponseWriter, req *Request) { 247 | if err := recover(); err != nil { 248 | // if a custom panic handler has been defined 249 | // run that instead 250 | if r.PanicHandler != nil { 251 | r.PanicHandler(w, req) 252 | return 253 | } 254 | 255 | // If it doesnt, use original http error function as fallback 256 | http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) 257 | return 258 | } 259 | } 260 | 261 | // Lookup allows the manual lookup of a method + path combo. 262 | // This is e.g. useful to build a framework around this router. 263 | // If the path was found, it returns the handle function and the path parameter 264 | // values. Otherwise the third return value indicates whether a redirection to 265 | // the same path with an extra / without the trailing slash should be performed. 266 | func (r *Router) Lookup(method, path string) ([]Middleware, Params, bool) { 267 | if root := r.trees[method]; root != nil { 268 | return root.getValue(path) 269 | } 270 | return nil, nil, false 271 | } 272 | 273 | // ServeHTTP makes the router implement the http.Handler interface. 274 | func (r *Router) ServeHTTP(w http.ResponseWriter, req *http.Request) { 275 | // 1st things 1st, wrap the response writter 276 | // to add the extra functionality we want basically 277 | // trace when a write happens 278 | FrodoWritter := ResponseWriter{ 279 | ResponseWriter: w, 280 | timeStart: time.Now(), 281 | method: req.Method, 282 | route: req.URL.Path, 283 | } 284 | 285 | // Wrap the supplied http.Request 286 | FrodoRequest := Request{ 287 | Request: req, 288 | // files []*UploadFile 289 | } 290 | 291 | // ---------- Handle 500: Internal Server Error ----------- 292 | // If a panic/error takes place while process, 293 | // recover and run PanicHandle if defined 294 | defer r.recover(&FrodoWritter, &FrodoRequest) 295 | 296 | if root := r.trees[req.Method]; root != nil { 297 | path := req.URL.Path 298 | 299 | // get the Handle of the route path requested 300 | handlers, ps, tsr := root.getValue(path) 301 | 302 | // if []Middleware was found were found, run it! 303 | noOfHandlers := len(handlers) 304 | if noOfHandlers > 0 { 305 | // if the 1st handler is defined, run it 306 | FrodoRequest.Params = ps 307 | FrodoRequest.RequestMiddleware = &RequestMiddleware{ 308 | handlers: handlers[:], 309 | total: noOfHandlers, 310 | nextPosition: 0, 311 | ResponseWriter: &FrodoWritter, 312 | Request: &FrodoRequest, 313 | } 314 | // Trigger the middleware chain of handlers to be triggered one by one 315 | // the rest shall be called to run by m.Next() 316 | FrodoRequest.RequestMiddleware.chainReaction() 317 | return 318 | } 319 | 320 | // if a handle was not found, the method is not a CONNECT request 321 | // and it is not a root path request 322 | if noOfHandlers == 0 && req.Method != "CONNECT" && path != "/" { 323 | code := 301 // Permanent redirect, request with GET method 324 | if req.Method != "GET" { 325 | // Temporary redirect, request with same method 326 | // As of Go 1.3, Go does not support status code 308. 327 | code = 307 328 | } 329 | 330 | if tsr && r.RedirectTrailingSlash { 331 | if len(path) > 1 && path[len(path)-1] == '/' { 332 | req.URL.Path = path[:len(path)-1] 333 | } else { 334 | req.URL.Path = path + "/" 335 | } 336 | 337 | http.Redirect(w, req, req.URL.String(), code) 338 | return 339 | } 340 | 341 | // Try to fix the request path 342 | if r.RedirectFixedPath { 343 | fixedPath, found := root.findCaseInsensitivePath( 344 | CleanPath(path), 345 | r.RedirectTrailingSlash, 346 | ) 347 | if found { 348 | req.URL.Path = string(fixedPath) 349 | http.Redirect(w, req, req.URL.String(), code) 350 | return 351 | } 352 | } 353 | } 354 | } 355 | 356 | // Handle 405 357 | if r.HandleMethodNotAllowed { 358 | for method := range r.trees { 359 | // Skip the requested method - we already tried this one 360 | if method == req.Method { 361 | continue 362 | } 363 | 364 | handle, ps, _ := r.trees[method].getValue(req.URL.Path) 365 | if handle != nil { 366 | if r.MethodNotAllowedHandler != nil { 367 | FrodoRequest.Params = ps 368 | r.MethodNotAllowedHandler(&FrodoWritter, &FrodoRequest) 369 | return 370 | } 371 | // if no MethodNotAllowedHandler found, just throw an error the old way 372 | http.Error(w, http.StatusText(405), http.StatusMethodNotAllowed) 373 | return 374 | } 375 | } 376 | return 377 | } 378 | 379 | // Handle 404 380 | if r.NotFoundHandler != nil { 381 | r.NotFoundHandler(&FrodoWritter, &FrodoRequest) 382 | return 383 | } 384 | 385 | // If there is not Handle for a 404 error use Go's w 386 | http.Error(w, http.StatusText(404), http.StatusNotFound) 387 | return 388 | } 389 | 390 | // Serve deploys the application 391 | // Default port is 3102, inspired by https://en.wikipedia.org/wiki/Fourth_Age 392 | // The "Fourth Age" followed the defeat of Sauron and the destruction of his One Ring, 393 | // but did not officially begin until after the Bearers of the Three Rings left Middle-earth for Valinor, 394 | // the 'Uttermost West' 395 | func (r *Router) Serve() { 396 | r.ServeOnPort(3102) 397 | } 398 | 399 | // ServeOnPort is to used if you plan change the port to serve the application on 400 | func (r *Router) ServeOnPort(portNumber interface{}) { 401 | var portNumberAsString string 402 | // Converting an interface into the data type it should be 403 | if pns, ok := portNumber.(int); ok { 404 | portNumberAsString = strconv.Itoa(pns) 405 | } else { 406 | // if it is not a number/int provided then it must be a string 407 | if pns, ok := portNumber.(string); ok { 408 | if pns == "" { 409 | pns = "3102" 410 | } 411 | portNumberAsString = pns 412 | } else { 413 | log.Fatal("[ERROR] PortNumber can only be a numeral string or integer") 414 | return 415 | } 416 | } 417 | 418 | err := http.ListenAndServe(":"+portNumberAsString, r) 419 | if err != nil { 420 | log.Fatalf("[ERROR] Server failed to initialise: %s", err) 421 | return 422 | } 423 | 424 | // If server successfully Launched 425 | log.Printf("[LOG] Server deployed at: %s", portNumberAsString) 426 | } 427 | -------------------------------------------------------------------------------- /tree.go: -------------------------------------------------------------------------------- 1 | package frodo 2 | 3 | import ( 4 | "strings" 5 | "unicode" 6 | ) 7 | 8 | func min(a, b int) int { 9 | if a <= b { 10 | return a 11 | } 12 | return b 13 | } 14 | 15 | func countParams(path string) uint8 { 16 | var n uint 17 | for i := 0; i < len(path); i++ { 18 | if path[i] != ':' && path[i] != '*' { 19 | continue 20 | } 21 | n++ 22 | } 23 | if n >= 255 { 24 | return 255 25 | } 26 | return uint8(n) 27 | } 28 | 29 | type nodeType uint8 30 | 31 | const ( 32 | static nodeType = iota // default 33 | root 34 | param 35 | catchAll 36 | ) 37 | 38 | type node struct { 39 | path string 40 | wildChild bool 41 | nType nodeType 42 | maxParams uint8 43 | indices string 44 | children []*node 45 | handle []Middleware 46 | priority uint32 47 | } 48 | 49 | // increments priority of the given child and reorders if necessary 50 | func (n *node) incrementChildPrio(pos int) int { 51 | n.children[pos].priority++ 52 | prio := n.children[pos].priority 53 | 54 | // adjust position (move to front) 55 | newPos := pos 56 | for newPos > 0 && n.children[newPos-1].priority < prio { 57 | // swap node positions 58 | tmpN := n.children[newPos-1] 59 | n.children[newPos-1] = n.children[newPos] 60 | n.children[newPos] = tmpN 61 | 62 | newPos-- 63 | } 64 | 65 | // build new index char string 66 | if newPos != pos { 67 | n.indices = n.indices[:newPos] + // unchanged prefix, might be empty 68 | n.indices[pos:pos+1] + // the index char we move 69 | n.indices[newPos:pos] + n.indices[pos+1:] // rest without char at 'pos' 70 | } 71 | 72 | return newPos 73 | } 74 | 75 | // addRoute adds a node with the given handle to the path. 76 | // Not concurrency-safe! 77 | func (n *node) addRoute(path string, handle []Middleware) { 78 | fullPath := path 79 | n.priority++ 80 | numParams := countParams(path) 81 | 82 | // non-empty tree 83 | if len(n.path) > 0 || len(n.children) > 0 { 84 | walk: 85 | for { 86 | // Update maxParams of the current node 87 | if numParams > n.maxParams { 88 | n.maxParams = numParams 89 | } 90 | 91 | // Find the longest common prefix. 92 | // This also implies that the common prefix contains no ':' or '*' 93 | // since the existing key can't contain those chars. 94 | i := 0 95 | max := min(len(path), len(n.path)) 96 | for i < max && path[i] == n.path[i] { 97 | i++ 98 | } 99 | 100 | // Split edge 101 | if i < len(n.path) { 102 | child := node{ 103 | path: n.path[i:], 104 | wildChild: n.wildChild, 105 | indices: n.indices, 106 | children: n.children, 107 | handle: n.handle, 108 | priority: n.priority - 1, 109 | } 110 | 111 | // Update maxParams (max of all children) 112 | for i := range child.children { 113 | if child.children[i].maxParams > child.maxParams { 114 | child.maxParams = child.children[i].maxParams 115 | } 116 | } 117 | 118 | n.children = []*node{&child} 119 | // []byte for proper unicode char conversion, see #65 120 | n.indices = string([]byte{n.path[i]}) 121 | n.path = path[:i] 122 | n.handle = make([]Middleware, 0) 123 | n.wildChild = false 124 | } 125 | 126 | // Make new node a child of this node 127 | if i < len(path) { 128 | path = path[i:] 129 | 130 | if n.wildChild { 131 | n = n.children[0] 132 | n.priority++ 133 | 134 | // Update maxParams of the child node 135 | if numParams > n.maxParams { 136 | n.maxParams = numParams 137 | } 138 | numParams-- 139 | 140 | // Check if the wildcard matches 141 | if len(path) >= len(n.path) && n.path == path[:len(n.path)] { 142 | // check for longer wildcard, e.g. :name and :names 143 | if len(n.path) >= len(path) || path[len(n.path)] == '/' { 144 | continue walk 145 | } 146 | } 147 | 148 | panic("path segment '" + path + 149 | "' conflicts with existing wildcard '" + n.path + 150 | "' in path '" + fullPath + "'") 151 | } 152 | 153 | c := path[0] 154 | 155 | // slash after param 156 | if n.nType == param && c == '/' && len(n.children) == 1 { 157 | n = n.children[0] 158 | n.priority++ 159 | continue walk 160 | } 161 | 162 | // Check if a child with the next path byte exists 163 | for i := 0; i < len(n.indices); i++ { 164 | if c == n.indices[i] { 165 | i = n.incrementChildPrio(i) 166 | n = n.children[i] 167 | continue walk 168 | } 169 | } 170 | 171 | // Otherwise insert it 172 | if c != ':' && c != '*' { 173 | // []byte for proper unicode char conversion, see #65 174 | n.indices += string([]byte{c}) 175 | child := &node{ 176 | maxParams: numParams, 177 | } 178 | n.children = append(n.children, child) 179 | n.incrementChildPrio(len(n.indices) - 1) 180 | n = child 181 | } 182 | n.insertChild(numParams, path, fullPath, handle) 183 | return 184 | 185 | } else if i == len(path) { // Make node a (in-path) leaf 186 | if n.handle != nil { 187 | panic("a handle is already registered for path '" + fullPath + "'") 188 | } 189 | n.handle = handle 190 | } 191 | return 192 | } 193 | } else { // Empty tree 194 | n.insertChild(numParams, path, fullPath, handle) 195 | n.nType = root 196 | } 197 | } 198 | 199 | func (n *node) insertChild(numParams uint8, path, fullPath string, handle []Middleware) { 200 | var offset int // already handled bytes of the path 201 | 202 | // find prefix until first wildcard (beginning with ':'' or '*'') 203 | for i, max := 0, len(path); numParams > 0; i++ { 204 | c := path[i] 205 | if c != ':' && c != '*' { 206 | continue 207 | } 208 | 209 | // find wildcard end (either '/' or path end) 210 | end := i + 1 211 | for end < max && path[end] != '/' { 212 | switch path[end] { 213 | // the wildcard name must not contain ':' and '*' 214 | case ':', '*': 215 | panic("only one wildcard per path segment is allowed, has: '" + 216 | path[i:] + "' in path '" + fullPath + "'") 217 | default: 218 | end++ 219 | } 220 | } 221 | 222 | // check if this Node existing children which would be 223 | // unreachable if we insert the wildcard here 224 | if len(n.children) > 0 { 225 | panic("wildcard route '" + path[i:end] + 226 | "' conflicts with existing children in path '" + fullPath + "'") 227 | } 228 | 229 | // check if the wildcard has a name 230 | if end-i < 2 { 231 | panic("wildcards must be named with a non-empty name in path '" + fullPath + "'") 232 | } 233 | 234 | if c == ':' { // param 235 | // split path at the beginning of the wildcard 236 | if i > 0 { 237 | n.path = path[offset:i] 238 | offset = i 239 | } 240 | 241 | child := &node{ 242 | nType: param, 243 | maxParams: numParams, 244 | } 245 | n.children = []*node{child} 246 | n.wildChild = true 247 | n = child 248 | n.priority++ 249 | numParams-- 250 | 251 | // if the path doesn't end with the wildcard, then there 252 | // will be another non-wildcard subpath starting with '/' 253 | if end < max { 254 | n.path = path[offset:end] 255 | offset = end 256 | 257 | child := &node{ 258 | maxParams: numParams, 259 | priority: 1, 260 | } 261 | n.children = []*node{child} 262 | n = child 263 | } 264 | 265 | } else { // catchAll 266 | if end != max || numParams > 1 { 267 | panic("catch-all routes are only allowed at the end of the path in path '" + fullPath + "'") 268 | } 269 | 270 | if len(n.path) > 0 && n.path[len(n.path)-1] == '/' { 271 | panic("catch-all conflicts with existing handle for the path segment root in path '" + fullPath + "'") 272 | } 273 | 274 | // currently fixed width 1 for '/' 275 | i-- 276 | if path[i] != '/' { 277 | panic("no / before catch-all in path '" + fullPath + "'") 278 | } 279 | 280 | n.path = path[offset:i] 281 | 282 | // first node: catchAll node with empty path 283 | child := &node{ 284 | wildChild: true, 285 | nType: catchAll, 286 | maxParams: 1, 287 | } 288 | n.children = []*node{child} 289 | n.indices = string(path[i]) 290 | n = child 291 | n.priority++ 292 | 293 | // second node: node holding the variable 294 | child = &node{ 295 | path: path[i:], 296 | nType: catchAll, 297 | maxParams: 1, 298 | handle: handle, 299 | priority: 1, 300 | } 301 | n.children = []*node{child} 302 | 303 | return 304 | } 305 | } 306 | 307 | // insert remaining path part and handle to the leaf 308 | n.path = path[offset:] 309 | n.handle = handle 310 | } 311 | 312 | // Returns the handle registered with the given path (key). The values of 313 | // wildcards are saved to a map. 314 | // If no handle can be found, a TSR (trailing slash redirect) recommendation is 315 | // made if a handle exists with an extra (without the) trailing slash for the 316 | // given path. 317 | func (n *node) getValue(path string) (handle []Middleware, p Params, tsr bool) { 318 | walk: // Outer loop for walking the tree 319 | for { 320 | if len(path) > len(n.path) { 321 | if path[:len(n.path)] == n.path { 322 | path = path[len(n.path):] 323 | // If this node does not have a wildcard (param or catchAll) 324 | // child, we can just look up the next child node and continue 325 | // to walk down the tree 326 | if !n.wildChild { 327 | c := path[0] 328 | for i := 0; i < len(n.indices); i++ { 329 | if c == n.indices[i] { 330 | n = n.children[i] 331 | continue walk 332 | } 333 | } 334 | 335 | // Nothing found. 336 | // We can recommend to redirect to the same URL without a 337 | // trailing slash if a leaf exists for that path. 338 | tsr = (path == "/" && n.handle != nil) 339 | return 340 | 341 | } 342 | 343 | // handle wildcard child 344 | n = n.children[0] 345 | switch n.nType { 346 | case param: 347 | // find param end (either '/' or path end) 348 | end := 0 349 | for end < len(path) && path[end] != '/' { 350 | end++ 351 | } 352 | 353 | // save param value 354 | if p == nil { 355 | // lazy allocation 356 | p = make(Params) 357 | } 358 | key := n.path[1:] 359 | p[key] = path[:end] 360 | 361 | // we need to go deeper! 362 | if end < len(path) { 363 | if len(n.children) > 0 { 364 | path = path[end:] 365 | n = n.children[0] 366 | continue walk 367 | } 368 | 369 | // ... but we can't 370 | tsr = (len(path) == end+1) 371 | return 372 | } 373 | 374 | if handle = n.handle; handle != nil { 375 | return 376 | } else if len(n.children) == 1 { 377 | // No handle found. Check if a handle for this path + a 378 | // trailing slash exists for TSR recommendation 379 | n = n.children[0] 380 | tsr = (n.path == "/" && n.handle != nil) 381 | } 382 | 383 | return 384 | 385 | case catchAll: 386 | // save param value 387 | if p == nil { 388 | // lazy allocation 389 | p = make(Params) 390 | } 391 | key := n.path[2:] 392 | p[key] = path 393 | 394 | handle = n.handle 395 | return 396 | 397 | default: 398 | panic("invalid node type") 399 | } 400 | } 401 | } else if path == n.path { 402 | // We should have reached the node containing the handle. 403 | // Check if this node has a handle registered. 404 | if handle = n.handle; handle != nil { 405 | return 406 | } 407 | 408 | if path == "/" && n.wildChild && n.nType != root { 409 | tsr = true 410 | return 411 | } 412 | 413 | // No handle found. Check if a handle for this path + a 414 | // trailing slash exists for trailing slash recommendation 415 | for i := 0; i < len(n.indices); i++ { 416 | if n.indices[i] == '/' { 417 | n = n.children[i] 418 | tsr = (len(n.path) == 1 && n.handle != nil) || 419 | (n.nType == catchAll && n.children[0].handle != nil) 420 | return 421 | } 422 | } 423 | 424 | return 425 | } 426 | 427 | // Nothing found. We can recommend to redirect to the same URL with an 428 | // extra trailing slash if a leaf exists for that path 429 | tsr = (path == "/") || 430 | (len(n.path) == len(path)+1 && n.path[len(path)] == '/' && 431 | path == n.path[:len(n.path)-1] && n.handle != nil) 432 | return 433 | } 434 | } 435 | 436 | // Makes a case-insensitive lookup of the given path and tries to find a handler. 437 | // It can optionally also fix trailing slashes. 438 | // It returns the case-corrected path and a bool indicating whether the lookup 439 | // was successful. 440 | func (n *node) findCaseInsensitivePath(path string, fixTrailingSlash bool) (ciPath []byte, found bool) { 441 | ciPath = make([]byte, 0, len(path)+1) // preallocate enough memory 442 | 443 | // Outer loop for walking the tree 444 | for len(path) >= len(n.path) && strings.ToLower(path[:len(n.path)]) == strings.ToLower(n.path) { 445 | path = path[len(n.path):] 446 | ciPath = append(ciPath, n.path...) 447 | 448 | if len(path) > 0 { 449 | // If this node does not have a wildcard (param or catchAll) child, 450 | // we can just look up the next child node and continue to walk down 451 | // the tree 452 | if !n.wildChild { 453 | r := unicode.ToLower(rune(path[0])) 454 | for i, index := range n.indices { 455 | // must use recursive approach since both index and 456 | // ToLower(index) could exist. We must check both. 457 | if r == unicode.ToLower(index) { 458 | out, found := n.children[i].findCaseInsensitivePath(path, fixTrailingSlash) 459 | if found { 460 | return append(ciPath, out...), true 461 | } 462 | } 463 | } 464 | 465 | // Nothing found. We can recommend to redirect to the same URL 466 | // without a trailing slash if a leaf exists for that path 467 | found = (fixTrailingSlash && path == "/" && n.handle != nil) 468 | return 469 | } 470 | 471 | n = n.children[0] 472 | switch n.nType { 473 | case param: 474 | // find param end (either '/' or path end) 475 | k := 0 476 | for k < len(path) && path[k] != '/' { 477 | k++ 478 | } 479 | 480 | // add param value to case insensitive path 481 | ciPath = append(ciPath, path[:k]...) 482 | 483 | // we need to go deeper! 484 | if k < len(path) { 485 | if len(n.children) > 0 { 486 | path = path[k:] 487 | n = n.children[0] 488 | continue 489 | } 490 | 491 | // ... but we can't 492 | if fixTrailingSlash && len(path) == k+1 { 493 | return ciPath, true 494 | } 495 | return 496 | } 497 | 498 | if n.handle != nil { 499 | return ciPath, true 500 | } else if fixTrailingSlash && len(n.children) == 1 { 501 | // No handle found. Check if a handle for this path + a 502 | // trailing slash exists 503 | n = n.children[0] 504 | if n.path == "/" && n.handle != nil { 505 | return append(ciPath, '/'), true 506 | } 507 | } 508 | return 509 | 510 | case catchAll: 511 | return append(ciPath, path...), true 512 | 513 | default: 514 | panic("invalid node type") 515 | } 516 | } else { 517 | // We should have reached the node containing the handle. 518 | // Check if this node has a handle registered. 519 | if n.handle != nil { 520 | return ciPath, true 521 | } 522 | 523 | // No handle found. 524 | // Try to fix the path by adding a trailing slash 525 | if fixTrailingSlash { 526 | for i := 0; i < len(n.indices); i++ { 527 | if n.indices[i] == '/' { 528 | n = n.children[i] 529 | if (len(n.path) == 1 && n.handle != nil) || 530 | (n.nType == catchAll && n.children[0].handle != nil) { 531 | return append(ciPath, '/'), true 532 | } 533 | return 534 | } 535 | } 536 | } 537 | return 538 | } 539 | } 540 | 541 | // Nothing found. 542 | // Try to fix the path by adding / removing a trailing slash 543 | if fixTrailingSlash { 544 | if path == "/" { 545 | return ciPath, true 546 | } 547 | if len(path)+1 == len(n.path) && n.path[len(path)] == '/' && 548 | strings.ToLower(path) == strings.ToLower(n.path[:len(path)]) && 549 | n.handle != nil { 550 | return append(ciPath, n.path...), true 551 | } 552 | } 553 | return 554 | } 555 | --------------------------------------------------------------------------------