├── LICENSE ├── README.md ├── doc.go ├── error_types.go ├── travel.go └── travel_test.go /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2015 Benjamen Keroack 2 | 3 | Apache License 4 | Version 2.0, January 2004 5 | http://www.apache.org/licenses/ 6 | 7 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 8 | 9 | 1. Definitions. 10 | 11 | "License" shall mean the terms and conditions for use, reproduction, 12 | and distribution as defined by Sections 1 through 9 of this document. 13 | 14 | "Licensor" shall mean the copyright owner or entity authorized by 15 | the copyright owner that is granting the License. 16 | 17 | "Legal Entity" shall mean the union of the acting entity and all 18 | other entities that control, are controlled by, or are under common 19 | control with that entity. For the purposes of this definition, 20 | "control" means (i) the power, direct or indirect, to cause the 21 | direction or management of such entity, whether by contract or 22 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 23 | outstanding shares, or (iii) beneficial ownership of such entity. 24 | 25 | "You" (or "Your") shall mean an individual or Legal Entity 26 | exercising permissions granted by this License. 27 | 28 | "Source" form shall mean the preferred form for making modifications, 29 | including but not limited to software source code, documentation 30 | source, and configuration files. 31 | 32 | "Object" form shall mean any form resulting from mechanical 33 | transformation or translation of a Source form, including but 34 | not limited to compiled object code, generated documentation, 35 | and conversions to other media types. 36 | 37 | "Work" shall mean the work of authorship, whether in Source or 38 | Object form, made available under the License, as indicated by a 39 | copyright notice that is included in or attached to the work 40 | (an example is provided in the Appendix below). 41 | 42 | "Derivative Works" shall mean any work, whether in Source or Object 43 | form, that is based on (or derived from) the Work and for which the 44 | editorial revisions, annotations, elaborations, or other modifications 45 | represent, as a whole, an original work of authorship. For the purposes 46 | of this License, Derivative Works shall not include works that remain 47 | separable from, or merely link (or bind by name) to the interfaces of, 48 | the Work and Derivative Works thereof. 49 | 50 | "Contribution" shall mean any work of authorship, including 51 | the original version of the Work and any modifications or additions 52 | to that Work or Derivative Works thereof, that is intentionally 53 | submitted to Licensor for inclusion in the Work by the copyright owner 54 | or by an individual or Legal Entity authorized to submit on behalf of 55 | the copyright owner. For the purposes of this definition, "submitted" 56 | means any form of electronic, verbal, or written communication sent 57 | to the Licensor or its representatives, including but not limited to 58 | communication on electronic mailing lists, source code control systems, 59 | and issue tracking systems that are managed by, or on behalf of, the 60 | Licensor for the purpose of discussing and improving the Work, but 61 | excluding communication that is conspicuously marked or otherwise 62 | designated in writing by the copyright owner as "Not a Contribution." 63 | 64 | "Contributor" shall mean Licensor and any individual or Legal Entity 65 | on behalf of whom a Contribution has been received by Licensor and 66 | subsequently incorporated within the Work. 67 | 68 | 2. Grant of Copyright License. Subject to the terms and conditions of 69 | this License, each Contributor hereby grants to You a perpetual, 70 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 71 | copyright license to reproduce, prepare Derivative Works of, 72 | publicly display, publicly perform, sublicense, and distribute the 73 | Work and such Derivative Works in Source or Object form. 74 | 75 | 3. Grant of Patent License. Subject to the terms and conditions of 76 | this License, each Contributor hereby grants to You a perpetual, 77 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 78 | (except as stated in this section) patent license to make, have made, 79 | use, offer to sell, sell, import, and otherwise transfer the Work, 80 | where such license applies only to those patent claims licensable 81 | by such Contributor that are necessarily infringed by their 82 | Contribution(s) alone or by combination of their Contribution(s) 83 | with the Work to which such Contribution(s) was submitted. If You 84 | institute patent litigation against any entity (including a 85 | cross-claim or counterclaim in a lawsuit) alleging that the Work 86 | or a Contribution incorporated within the Work constitutes direct 87 | or contributory patent infringement, then any patent licenses 88 | granted to You under this License for that Work shall terminate 89 | as of the date such litigation is filed. 90 | 91 | 4. Redistribution. You may reproduce and distribute copies of the 92 | Work or Derivative Works thereof in any medium, with or without 93 | modifications, and in Source or Object form, provided that You 94 | meet the following conditions: 95 | 96 | (a) You must give any other recipients of the Work or 97 | Derivative Works a copy of this License; and 98 | 99 | (b) You must cause any modified files to carry prominent notices 100 | stating that You changed the files; and 101 | 102 | (c) You must retain, in the Source form of any Derivative Works 103 | that You distribute, all copyright, patent, trademark, and 104 | attribution notices from the Source form of the Work, 105 | excluding those notices that do not pertain to any part of 106 | the Derivative Works; and 107 | 108 | (d) If the Work includes a "NOTICE" text file as part of its 109 | distribution, then any Derivative Works that You distribute must 110 | include a readable copy of the attribution notices contained 111 | within such NOTICE file, excluding those notices that do not 112 | pertain to any part of the Derivative Works, in at least one 113 | of the following places: within a NOTICE text file distributed 114 | as part of the Derivative Works; within the Source form or 115 | documentation, if provided along with the Derivative Works; or, 116 | within a display generated by the Derivative Works, if and 117 | wherever such third-party notices normally appear. The contents 118 | of the NOTICE file are for informational purposes only and 119 | do not modify the License. You may add Your own attribution 120 | notices within Derivative Works that You distribute, alongside 121 | or as an addendum to the NOTICE text from the Work, provided 122 | that such additional attribution notices cannot be construed 123 | as modifying the License. 124 | 125 | You may add Your own copyright statement to Your modifications and 126 | may provide additional or different license terms and conditions 127 | for use, reproduction, or distribution of Your modifications, or 128 | for any such Derivative Works as a whole, provided Your use, 129 | reproduction, and distribution of the Work otherwise complies with 130 | the conditions stated in this License. 131 | 132 | 5. Submission of Contributions. Unless You explicitly state otherwise, 133 | any Contribution intentionally submitted for inclusion in the Work 134 | by You to the Licensor shall be under the terms and conditions of 135 | this License, without any additional terms or conditions. 136 | Notwithstanding the above, nothing herein shall supersede or modify 137 | the terms of any separate license agreement you may have executed 138 | with Licensor regarding such Contributions. 139 | 140 | 6. Trademarks. This License does not grant permission to use the trade 141 | names, trademarks, service marks, or product names of the Licensor, 142 | except as required for reasonable and customary use in describing the 143 | origin of the Work and reproducing the content of the NOTICE file. 144 | 145 | 7. Disclaimer of Warranty. Unless required by applicable law or 146 | agreed to in writing, Licensor provides the Work (and each 147 | Contributor provides its Contributions) on an "AS IS" BASIS, 148 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 149 | implied, including, without limitation, any warranties or conditions 150 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 151 | PARTICULAR PURPOSE. You are solely responsible for determining the 152 | appropriateness of using or redistributing the Work and assume any 153 | risks associated with Your exercise of permissions under this License. 154 | 155 | 8. Limitation of Liability. In no event and under no legal theory, 156 | whether in tort (including negligence), contract, or otherwise, 157 | unless required by applicable law (such as deliberate and grossly 158 | negligent acts) or agreed to in writing, shall any Contributor be 159 | liable to You for damages, including any direct, indirect, special, 160 | incidental, or consequential damages of any character arising as a 161 | result of this License or out of the use or inability to use the 162 | Work (including but not limited to damages for loss of goodwill, 163 | work stoppage, computer failure or malfunction, or any and all 164 | other commercial damages or losses), even if such Contributor 165 | has been advised of the possibility of such damages. 166 | 167 | 9. Accepting Warranty or Additional Liability. While redistributing 168 | the Work or Derivative Works thereof, You may choose to offer, 169 | and charge a fee for, acceptance of support, warranty, indemnity, 170 | or other liability obligations and/or rights consistent with this 171 | License. However, in accepting such obligations, You may act only 172 | on Your own behalf and on Your sole responsibility, not on behalf 173 | of any other Contributor, and only if You agree to indemnify, 174 | defend, and hold each Contributor harmless for any liability 175 | incurred by, or claims asserted against, such Contributor by reason 176 | of your accepting any such warranty or additional liability. 177 | 178 | END OF TERMS AND CONDITIONS 179 | 180 | APPENDIX: How to apply the Apache License to your work. 181 | 182 | To apply the Apache License to your work, attach the following 183 | boilerplate notice, with the fields enclosed by brackets "{}" 184 | replaced with your own identifying information. (Don't include 185 | the brackets!) The text should be enclosed in the appropriate 186 | comment syntax for the file format. We also recommend that a 187 | file or class name and description of purpose be included on the 188 | same "printed page" as the copyright notice for easier 189 | identification within third-party archives. 190 | 191 | Copyright {yyyy} {name of copyright owner} 192 | 193 | Licensed under the Apache License, Version 2.0 (the "License"); 194 | you may not use this file except in compliance with the License. 195 | You may obtain a copy of the License at 196 | 197 | http://www.apache.org/licenses/LICENSE-2.0 198 | 199 | Unless required by applicable law or agreed to in writing, software 200 | distributed under the License is distributed on an "AS IS" BASIS, 201 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 202 | See the License for the specific language governing permissions and 203 | limitations under the License. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Traversal-like Dynamic HTTP Routing in Go 2 | ================================= 3 | 4 | * For usage/details: https://godoc.org/github.com/bkeroack/travel 5 | * For example usage: https://github.com/bkeroack/travel-examples 6 | 7 | Travel is an HTTP router that provides dynamic routing functionality similar to the "traversal" system from the Pyramid web framework in Python. 8 | 9 | For details on the original traversal system see: http://docs.pylonsproject.org/docs/pyramid/en/latest/narr/traversal.html 10 | 11 | Simply put, traversal allows you to route HTTP requests by providing a nested ``map[string]interface{}`` object called the 12 | "root tree". Request URLs are tokenized and recursive lookup is performed on the root tree object. 13 | 14 | Example: 15 | 16 | If the request URL is ``/foo/bar/baz/123``, it is tokenized to the following: 17 | 18 | ```json 19 | ["foo", "bar", "baz", "123"] 20 | ``` 21 | 22 | Then the equivalent of the following lookup is performed: 23 | 24 | ```go 25 | root_tree["foo"]["bar"]["baz"]["123"] 26 | ``` 27 | 28 | The object that results from this lookup is the "current object". If traversal succeeds, a named handler is then invoked (looked up via the "handler map" provided to the router), otherwise the router returns an appropriate error (404, etc). 29 | 30 | For details on how lookup translates to handler names, see the godoc documentation linked above. Travel allows users to emulate traditional 31 | traversal mechanics while also providing several ways to modify behavior, such as handler name overrides within the root tree object and limitations on subpath length. -------------------------------------------------------------------------------- /doc.go: -------------------------------------------------------------------------------- 1 | /* 2 | Travel is an HTTP router that provides routing similar to the "traversal" system from the Pyramid web framework in Python. 3 | 4 | For details on the original traversal system please read: http://docs.pylonsproject.org/docs/pyramid/en/latest/narr/traversal.html 5 | 6 | Usage: 7 | 8 | func defaultHandler(w http.ResponseWriter, r *http.Request, c *travel.Context) { 9 | // handler code here 10 | } 11 | 12 | func errorHandler(w http.ResponseWriter, r *http.Request, err travel.TraversalError) { 13 | // HTTP error handler code here 14 | http.Error(w, err.Error(), err.Code()) 15 | } 16 | 17 | func getRootTree() { 18 | // Fetch root tree here 19 | } 20 | 21 | handlerMap := map[string]TravelHandler { 22 | "": defaultHandler, 23 | } 24 | 25 | options := travel.TravelOptions{ 26 | StrictTraversal: true, 27 | SubpathMaxLength: map[string]int{ 28 | "GET": travel.UnlimitedSubpath, 29 | "PUT": 0, 30 | "POST": 0, 31 | "DELETE": 0, 32 | }, 33 | } 34 | r, err := travel.NewRouter(getRootTree, handlerMap, errorHandler, &options) 35 | if err != nil { 36 | log.Fatalf("Error creating Travel router: %v\n", err) 37 | } 38 | http.Handle("/", r) 39 | http.ListenAndServe("127.0.0.1:8000", nil) 40 | 41 | Travel provides additional options to modify normal traversal semantincs: 42 | 43 | Strict vs Permissive 44 | 45 | "Strict" means to follow Pyramid traversal semantics -- handler name can only be "" (empty string) or the latest token in path when 46 | root tree lookup failed (everything beyond that is the subpath). Note that this can be modified with handler name overrides in the 47 | root tree object. 48 | 49 | Non-strict (permissive) means that the handler name is always the latest token in the path (regardless if lookup fully succeeds). 50 | 51 | Strict setting has no effect on the following options (they can be used to modify "strict" traversal as needed). Handler names 52 | can always be overridden by embedding handler keys within the root tree ('%handler' key within the object, value must be a string). 53 | 54 | Handler Overrides 55 | 56 | Any level of the root tree can contain a special key "%handler", mapping to a handler name string that will be invoked instead of 57 | whatever traversal would otherwise dictate. Handler overrides take precedence over strict/permissive mode rules. 58 | 59 | Default Handler 60 | 61 | The optional DefaultHandler is used to execute a fallback handler when traversal succeeds but the handler name returned is not 62 | found within the handler map. Otherwise a 501 Not Implemented error is returned. As with handler overrides, the DefaultHandler 63 | setting is respected regardless of strict/permissive setting. 64 | 65 | Subpath Max Length 66 | 67 | SubpathMaxLength is a map of method verb (all caps) to an integer representing the allowed number of subpath tokens. If the subpath 68 | length is less than or equal to this limit, the request succeeds and the handler is executed per traversal semantics. If the subpath 69 | exceeds this limit a 404 Not Found is returned. Traditional Pyramid traversal has an unlimited subpath max length. That can be emulated by setting SubpathMaxLength[verb] to 70 | UnlimitedSubpath. 71 | 72 | */ 73 | package travel 74 | -------------------------------------------------------------------------------- /error_types.go: -------------------------------------------------------------------------------- 1 | package travel 2 | 3 | import ( 4 | "fmt" 5 | "net/http" 6 | ) 7 | 8 | type TraversalError interface { 9 | Error() string 10 | Code() int 11 | } 12 | 13 | type TraversalNotFoundError struct { 14 | path []string 15 | code int 16 | } 17 | type TraversalRootTreeError struct { 18 | err error 19 | code int 20 | } 21 | type TraversalInternalError struct { 22 | msg string 23 | code int 24 | } 25 | type TraversalUnknownHandlerError struct { 26 | path []string 27 | code int 28 | } 29 | 30 | func (t TraversalNotFoundError) Error() string { 31 | return fmt.Sprintf("404 Not Found: %v", t.path) 32 | } 33 | 34 | func (t TraversalNotFoundError) Code() int { 35 | return t.code 36 | } 37 | 38 | func (t TraversalUnknownHandlerError) Error() string { 39 | return fmt.Sprintf("Handler not found for route: %v\n", t.path) 40 | } 41 | 42 | func (t TraversalUnknownHandlerError) Code() int { 43 | return t.code 44 | } 45 | 46 | func (t TraversalRootTreeError) Error() string { 47 | return t.err.Error() 48 | } 49 | 50 | func (t TraversalRootTreeError) Code() int { 51 | return t.code 52 | } 53 | 54 | func (t TraversalInternalError) Error() string { 55 | return fmt.Sprintf("Internal traversal error (bug?): %v", t.msg) 56 | } 57 | 58 | func (t TraversalInternalError) Code() int { 59 | return t.code 60 | } 61 | 62 | func NotFoundError(r []string) TraversalError { 63 | return TraversalNotFoundError{ 64 | path: r, 65 | code: http.StatusNotFound, 66 | } 67 | } 68 | 69 | func UnknownHandlerError(r []string) TraversalError { 70 | return TraversalUnknownHandlerError{ 71 | path: r, 72 | code: http.StatusNotImplemented, 73 | } 74 | } 75 | 76 | func RootTreeError(err error) TraversalError { 77 | return TraversalRootTreeError{ 78 | err: err, 79 | code: http.StatusInternalServerError, 80 | } 81 | } 82 | 83 | func InternalError(m string) TraversalError { 84 | return TraversalInternalError{ 85 | msg: m, 86 | code: http.StatusInternalServerError, 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /travel.go: -------------------------------------------------------------------------------- 1 | package travel 2 | 3 | import ( 4 | "net/http" 5 | "strings" 6 | ) 7 | 8 | const ( 9 | UnlimitedSubpath = -1 // Emulate traditional traversal with unlimited subpath lengths 10 | h_token = "%handler" 11 | ) 12 | 13 | type TravelHandler func(http.ResponseWriter, *http.Request, *Context) 14 | type TravelErrorHandler func(http.ResponseWriter, *http.Request, TraversalError) 15 | type RootTreeFunc func() (map[string]interface{}, error) 16 | type HandlerMap map[string]TravelHandler 17 | 18 | // Options for Travel router 19 | type TravelOptions struct { 20 | SubpathMaxLength map[string]int // Map of method verb to subpath length limit for requests of that type 21 | StrictTraversal bool // Obey Pyramid traversal semantics (do not enforce subpath limits, use handler names from path only) 22 | UseDefaultHandler bool // If handler name is not found in handler map, execute this instead of returning http.StatusNotImplemented 23 | DefaultHandler string // Default handler name (must exist in handler map) 24 | } 25 | 26 | // Request context passed to request handler 27 | type Context struct { 28 | RootTree map[string]interface{} // Root tree as processed by this request (thread-local) 29 | CurrentObj interface{} // Current object from root tree 30 | Path []string // tokenized URL path 31 | Subpath []string // Tokenized subpath for this request (everything beyond the last token that succeeded traversal) 32 | options *TravelOptions // Options passed to router 33 | req *http.Request 34 | rtf RootTreeFunc 35 | } 36 | 37 | // Travel router 38 | type Router struct { 39 | rtf RootTreeFunc 40 | hm HandlerMap 41 | eh TravelErrorHandler 42 | options *TravelOptions 43 | } 44 | 45 | // Result of running traversal algorithm 46 | type TraversalResult struct { 47 | h string // handler name 48 | co interface{} // current object 49 | sp []string // tokenized subpath 50 | } 51 | 52 | // Create a new Travel router. Parameters: callback function to fetch root tree, map of handler names to functions, 53 | // default request error handler, options 54 | func NewRouter(rtf RootTreeFunc, hm HandlerMap, eh TravelErrorHandler, o *TravelOptions) (*Router, error) { 55 | if o == nil { 56 | o = &TravelOptions{ 57 | SubpathMaxLength: map[string]int{}, 58 | } 59 | } 60 | if o.UseDefaultHandler { 61 | if _, ok := hm[o.DefaultHandler]; !ok { 62 | return &Router{}, InternalError("Default handler not found in handler map") 63 | } 64 | } 65 | return &Router{ 66 | rtf: rtf, 67 | hm: hm, 68 | eh: eh, 69 | options: o, 70 | }, nil 71 | } 72 | 73 | func doTraversal(rt map[string]interface{}, tokens []string, spl int, strict bool) (*TraversalResult, TraversalError) { 74 | var cur_obj interface{} 75 | var ok bool 76 | 77 | get_hn := func(token string, found bool) string { 78 | if strict { 79 | if found { 80 | return "" 81 | } else { 82 | return token 83 | } 84 | } else { 85 | return token 86 | } 87 | } 88 | 89 | cur_obj = rt 90 | for i := range tokens { 91 | t := tokens[i] 92 | switch co := cur_obj.(type) { 93 | case map[string]interface{}: 94 | if cur_obj, ok = co[t]; ok { 95 | if i == len(tokens)-1 { 96 | switch co2 := cur_obj.(type) { 97 | case map[string]interface{}: 98 | if hn, ok := co2[h_token]; ok { 99 | hns := hn.(string) 100 | return &TraversalResult{ // last token, token lookup success, cur_obj is map, explicit handler found 101 | h: hns, 102 | co: co2, 103 | sp: []string{}, 104 | }, nil 105 | } else { 106 | return &TraversalResult{ // last token, token lookup success, cur_obj is map, no handler key 107 | h: get_hn(t, true), 108 | co: co2, 109 | sp: []string{}, 110 | }, nil 111 | } 112 | default: 113 | return &TraversalResult{ // last token, token lookup success, cur_obj is not a map 114 | h: get_hn(t, true), 115 | co: cur_obj, 116 | sp: []string{}, 117 | }, nil 118 | } 119 | } // next iteration 120 | } else { 121 | // not found 122 | sp := tokens[i:len(tokens)] 123 | if len(sp) <= spl || len(tokens) == 1 || spl == UnlimitedSubpath { 124 | return &TraversalResult{ // token not found, subpath_limit not exceeded 125 | h: get_hn(t, false), 126 | co: co, 127 | sp: sp, 128 | }, nil 129 | } else { 130 | return &TraversalResult{}, NotFoundError(tokens) // token not found, subpath limit exceeded 131 | } 132 | } 133 | default: 134 | if i == len(tokens)-1 { 135 | return &TraversalResult{ // last token, current object is not a map 136 | h: "", 137 | co: cur_obj, 138 | sp: []string{}, 139 | }, nil 140 | } else { 141 | return &TraversalResult{ // tokens remaining but cur_obj is not a map so traversal cannot continue 142 | h: get_hn(t, false), 143 | co: cur_obj, 144 | sp: tokens[i : len(tokens)-1], 145 | }, nil 146 | } 147 | } 148 | } 149 | return &TraversalResult{}, InternalError("received empty path") 150 | } 151 | 152 | // Fetch the root tree, re-run traversal and update Context fields. 153 | func (c *Context) Refresh() TraversalError { 154 | rt, err := c.rtf() 155 | if err != nil { 156 | return RootTreeError(err) 157 | } 158 | 159 | var spl int 160 | if v, ok := c.options.SubpathMaxLength[c.req.Method]; ok { 161 | spl = v 162 | } else { 163 | spl = 0 164 | } 165 | 166 | tr, err := doTraversal(rt, c.Path, spl, c.options.StrictTraversal) 167 | if err != nil { 168 | return err.(TraversalError) 169 | } 170 | c.CurrentObj = tr.co 171 | c.RootTree = rt 172 | c.Subpath = tr.sp 173 | return nil 174 | } 175 | 176 | // Walk back n nodes in tokenized path, return root tree object at that node. 177 | func (c *Context) WalkBack(n uint) (map[string]interface{}, error) { 178 | new_path := c.Path[0 : len(c.Path)-int(n)] 179 | if len(new_path) == 0 { 180 | new_path = []string{""} 181 | } 182 | tr, err := doTraversal(c.RootTree, new_path, 0, c.options.StrictTraversal) 183 | if err != nil { 184 | return map[string]interface{}{}, err 185 | } 186 | return tr.co.(map[string]interface{}), nil 187 | } 188 | 189 | func (r *Router) ServeHTTP(w http.ResponseWriter, req *http.Request) { 190 | 191 | if req.URL.Path[0] == '/' { 192 | req.URL.Path = strings.TrimLeft(req.URL.Path, "/") 193 | } 194 | if len(req.URL.Path) > 0 { 195 | if req.URL.Path[len(req.URL.Path)-1] == '/' { 196 | req.URL.Path = strings.TrimRight(req.URL.Path, "/") 197 | } 198 | } 199 | 200 | c := &Context{} 201 | c.Path = strings.Split(req.URL.Path, "/") 202 | 203 | rt, err := r.rtf() 204 | if err != nil { 205 | r.eh(w, req, RootTreeError(err)) 206 | return 207 | } 208 | 209 | buildContext := func(tr *TraversalResult) { 210 | c.RootTree = rt 211 | c.CurrentObj = tr.co 212 | c.Subpath = tr.sp 213 | c.options = r.options 214 | c.rtf = r.rtf 215 | c.req = req 216 | } 217 | 218 | var spl int 219 | if v, ok := r.options.SubpathMaxLength[req.Method]; ok { 220 | spl = v 221 | } else { 222 | spl = 0 223 | } 224 | 225 | tr, terr := doTraversal(rt, c.Path, spl, r.options.StrictTraversal) 226 | if terr != nil { 227 | r.eh(w, req, terr) 228 | return 229 | } 230 | if h, ok := r.hm[tr.h]; ok { 231 | buildContext(tr) 232 | h(w, req, c) 233 | return 234 | } else { 235 | if r.options.UseDefaultHandler { 236 | h := r.hm[r.options.DefaultHandler] // guaranteed to exist by NewRouter 237 | buildContext(tr) 238 | h(w, req, c) 239 | return 240 | } 241 | r.eh(w, req, UnknownHandlerError(c.Path)) 242 | } 243 | } 244 | -------------------------------------------------------------------------------- /travel_test.go: -------------------------------------------------------------------------------- 1 | package travel 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "log" 7 | "net/http" 8 | "testing" 9 | ) 10 | 11 | var test_response []byte 12 | 13 | func load_roottree(d []byte) (map[string]interface{}, error) { 14 | var rt interface{} 15 | err := json.Unmarshal(d, &rt) 16 | if err != nil { 17 | return map[string]interface{}{}, err 18 | } 19 | 20 | switch v := rt.(type) { 21 | case map[string]interface{}: 22 | return v, nil 23 | default: 24 | return map[string]interface{}{}, fmt.Errorf("incorrect json in root tree") 25 | } 26 | } 27 | 28 | func new_request(verb string, url string) *http.Request { 29 | r, err := http.NewRequest(verb, url, nil) 30 | if err != nil { 31 | log.Fatalf("error creating new request: %v\n", err) 32 | } 33 | return r 34 | } 35 | 36 | type test_response_writer struct { 37 | hh http.Header 38 | status int 39 | } 40 | 41 | func (tw test_response_writer) Header() http.Header { 42 | return http.Header(tw.hh) 43 | } 44 | 45 | func (tw test_response_writer) Write(b []byte) (int, error) { 46 | test_response = b 47 | return len(b), nil 48 | } 49 | 50 | func (tw test_response_writer) WriteHeader(c int) { 51 | tw.status = c 52 | } 53 | 54 | func new_responsewriter() test_response_writer { 55 | return test_response_writer{ 56 | hh: make(http.Header), 57 | status: 0, 58 | } 59 | } 60 | 61 | func unmarshal_response() (map[string]interface{}, error) { 62 | var d interface{} 63 | err := json.Unmarshal(test_response, &d) 64 | if err != nil { 65 | return map[string]interface{}{}, err 66 | } 67 | switch v := d.(type) { 68 | case map[string]interface{}: 69 | return v, nil 70 | default: 71 | return map[string]interface{}{}, fmt.Errorf("incorrect json in response") 72 | } 73 | } 74 | 75 | func test_handler(w http.ResponseWriter, r *http.Request, c *Context, v string) { 76 | resp := map[string]interface{}{ 77 | "resp": v, 78 | "context": c.CurrentObj, 79 | } 80 | b, err := json.Marshal(resp) 81 | if err != nil { 82 | log.Fatalf("error marshalling json response: %v\n", err) 83 | } 84 | w.Write(b) 85 | } 86 | 87 | func test_error_handler(w http.ResponseWriter, r *http.Request, err TraversalError) { 88 | log.Printf("test_error_handler called\n") 89 | test_handler(w, r, &Context{}, err.Error()) 90 | } 91 | 92 | func test_request(r *Router, v string, p string) map[string]interface{} { 93 | req := new_request(v, p) 94 | rw := new_responsewriter() 95 | r.ServeHTTP(rw, req) 96 | resp, err := unmarshal_response() 97 | if err != nil { 98 | return map[string]interface{}{} 99 | } 100 | return resp 101 | } 102 | 103 | func TestSimpleTraversal(t *testing.T) { 104 | rtf := func() (map[string]interface{}, error) { 105 | rt := ` 106 | { 107 | "foo": { 108 | "bar": { 109 | "baz": {}, 110 | "%handler": "bar" 111 | } 112 | } 113 | }` 114 | return load_roottree([]byte(rt)) 115 | } 116 | 117 | var bar_handler TravelHandler 118 | bar_handler = func(w http.ResponseWriter, r *http.Request, c *Context) { 119 | test_handler(w, r, c, "bar") 120 | } 121 | 122 | baz_handler := func(w http.ResponseWriter, r *http.Request, c *Context) { 123 | test_handler(w, r, c, "baz") 124 | } 125 | 126 | def_handler := func(w http.ResponseWriter, r *http.Request, c *Context) { 127 | test_handler(w, r, c, "default") 128 | } 129 | 130 | hm := map[string]TravelHandler{ 131 | "bar": bar_handler, 132 | "baz": baz_handler, 133 | "": def_handler, 134 | } 135 | 136 | o := TravelOptions{ 137 | StrictTraversal: true, 138 | } 139 | 140 | r, err := NewRouter(rtf, hm, test_error_handler, &o) 141 | if err != nil { 142 | t.Errorf("NewRouter error: %v\n", err) 143 | } 144 | resp := test_request(r, "GET", "/foo/bar") 145 | if resp["resp"] != "bar" { 146 | t.Errorf("Incorrect response: %v\n", resp) 147 | return 148 | } 149 | 150 | resp = test_request(r, "GET", "/foo/bar/baz") 151 | if resp["resp"] != "default" { 152 | t.Errorf("Incorrect response: %v\n", resp) 153 | return 154 | } 155 | } 156 | 157 | func TestPermissiveTraversal(t *testing.T) { 158 | rtf := func() (map[string]interface{}, error) { 159 | rt := ` 160 | { 161 | "accounts": { 162 | "users": { 163 | "mary": { 164 | "%handler": "user" 165 | } 166 | } 167 | } 168 | }` 169 | return load_roottree([]byte(rt)) 170 | } 171 | 172 | accounts_handler := func(w http.ResponseWriter, r *http.Request, c *Context) { 173 | test_handler(w, r, c, "accounts") 174 | } 175 | 176 | users_handler := func(w http.ResponseWriter, r *http.Request, c *Context) { 177 | test_handler(w, r, c, "users") 178 | } 179 | 180 | user_handler := func(w http.ResponseWriter, r *http.Request, c *Context) { 181 | test_handler(w, r, c, "user") 182 | } 183 | 184 | hm := map[string]TravelHandler{ 185 | "accounts": accounts_handler, 186 | "users": users_handler, 187 | "user": user_handler, 188 | } 189 | 190 | r, err := NewRouter(rtf, hm, test_error_handler, nil) 191 | if err != nil { 192 | t.Errorf("NewRouter error: %v\n", err) 193 | } 194 | resp := test_request(r, "GET", "/accounts") 195 | if resp["resp"] != "accounts" { 196 | t.Errorf("Incorrect response: %v\n", resp) 197 | return 198 | } 199 | 200 | resp = test_request(r, "GET", "/accounts/users") 201 | if resp["resp"] != "users" { 202 | t.Errorf("Incorrect response: %v\n", resp) 203 | return 204 | } 205 | 206 | resp = test_request(r, "GET", "/accounts/users/mary") 207 | if resp["resp"] != "user" { 208 | t.Errorf("Incorrect response: %v\n", resp) 209 | return 210 | } 211 | } 212 | --------------------------------------------------------------------------------