From 43203c2223b5dcb7915639e14e846a7537b5cce9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=9D=8E=E4=BC=9F=E4=B9=90?= Date: Fri, 23 Dec 2022 14:33:06 +0800 Subject: [PATCH] x --- go.mod | 2 +- http/path.go | 150 -------- http/path_test.go | 149 -------- http/router.go | 540 ----------------------------- http/router_test.go | 699 ------------------------------------- http/tree.go | 687 ------------------------------------- http/tree_test.go | 721 --------------------------------------- main.go | 22 +- proto/auth/auth.proto | 2 +- proto/v1/auth/auth.pb.go | 11 +- proto/v1/user.pb.go | 2 +- response/response.go | 2 +- run/main.go | 23 +- service/service.go | 18 +- 14 files changed, 32 insertions(+), 2996 deletions(-) delete mode 100644 http/path.go delete mode 100644 http/path_test.go delete mode 100644 http/router.go delete mode 100644 http/router_test.go delete mode 100644 http/tree.go delete mode 100644 http/tree_test.go diff --git a/go.mod b/go.mod index 5a537da..6019776 100644 --- a/go.mod +++ b/go.mod @@ -1,4 +1,4 @@ -module github.com/mogfee/protoc-gen-kit +module git.echinacities.com/mogfee/protoc-gen-kit go 1.18 diff --git a/http/path.go b/http/path.go deleted file mode 100644 index dcc4d2d..0000000 --- a/http/path.go +++ /dev/null @@ -1,150 +0,0 @@ -// Copyright 2013 Julien Schmidt. All rights reserved. -// Based on the path package, Copyright 2009 The Go Authors. -// Use of this source code is governed by a BSD-style license that can be found -// in the LICENSE file. - -package http - -// CleanPath is the URL version of path.Clean, it returns a canonical URL path -// for p, eliminating . and .. elements. -// -// The following rules are applied iteratively until no further processing can -// be done: -// 1. Replace multiple slashes with a single slash. -// 2. Eliminate each . path name element (the current directory). -// 3. Eliminate each inner .. path name element (the parent directory) -// along with the non-.. element that precedes it. -// 4. Eliminate .. elements that begin a rooted path: -// that is, replace "/.." by "/" at the beginning of a path. -// -// If the result of this process is an empty string, "/" is returned -func CleanPath(p string) string { - const stackBufSize = 128 - - // Turn empty string into "/" - if p == "" { - return "/" - } - - // Reasonably sized buffer on stack to avoid allocations in the common case. - // If a larger buffer is required, it gets allocated dynamically. - buf := make([]byte, 0, stackBufSize) - - n := len(p) - - // Invariants: - // reading from path; r is index of next byte to process. - // writing to buf; w is index of next byte to write. - - // path must start with '/' - r := 1 - w := 1 - - if p[0] != '/' { - r = 0 - - if n+1 > stackBufSize { - buf = make([]byte, n+1) - } else { - buf = buf[:n+1] - } - buf[0] = '/' - } - - trailing := n > 1 && p[n-1] == '/' - - // A bit more clunky without a 'lazybuf' like the path package, but the loop - // gets completely inlined (bufApp calls). - // So in contrast to the path package this loop has no expensive function - // calls (except make, if needed). - - for r < n { - switch { - case p[r] == '/': - // empty path element, trailing slash is added after the end - r++ - - case p[r] == '.' && r+1 == n: - trailing = true - r++ - - case p[r] == '.' && p[r+1] == '/': - // . element - r += 2 - - case p[r] == '.' && p[r+1] == '.' && (r+2 == n || p[r+2] == '/'): - // .. element: remove to last / - r += 3 - - if w > 1 { - // can backtrack - w-- - - if len(buf) == 0 { - for w > 1 && p[w] != '/' { - w-- - } - } else { - for w > 1 && buf[w] != '/' { - w-- - } - } - } - - default: - // Real path element. - // Add slash if needed - if w > 1 { - bufApp(&buf, p, w, '/') - w++ - } - - // Copy element - for r < n && p[r] != '/' { - bufApp(&buf, p, w, p[r]) - w++ - r++ - } - } - } - - // Re-append trailing slash - if trailing && w > 1 { - bufApp(&buf, p, w, '/') - w++ - } - - // If the original string was not modified (or only shortened at the end), - // return the respective substring of the original string. - // Otherwise return a new string from the buffer. - if len(buf) == 0 { - return p[:w] - } - return string(buf[:w]) -} - -// Internal helper to lazily create a buffer if necessary. -// Calls to this function get inlined. -func bufApp(buf *[]byte, s string, w int, c byte) { - b := *buf - if len(b) == 0 { - // No modification of the original string so far. - // If the next character is the same as in the original string, we do - // not yet have to allocate a buffer. - if s[w] == c { - return - } - - // Otherwise use either the stack buffer, if it is large enough, or - // allocate a new buffer on the heap, and copy all previous characters. - if l := len(s); l > cap(b) { - *buf = make([]byte, len(s)) - } else { - *buf = (*buf)[:l] - } - b = *buf - - copy(b, s[:w]) - } - b[w] = c -} diff --git a/http/path_test.go b/http/path_test.go deleted file mode 100644 index 981ffb9..0000000 --- a/http/path_test.go +++ /dev/null @@ -1,149 +0,0 @@ -// Copyright 2013 Julien Schmidt. All rights reserved. -// Based on the path package, Copyright 2009 The Go Authors. -// Use of this source code is governed by a BSD-style license that can be found -// in the LICENSE file. - -package http - -import ( - "strings" - "testing" -) - -type cleanPathTest struct { - path, result string -} - -var cleanTests = []cleanPathTest{ - // Already clean - {"/", "/"}, - {"/abc", "/abc"}, - {"/a/b/c", "/a/b/c"}, - {"/abc/", "/abc/"}, - {"/a/b/c/", "/a/b/c/"}, - - // missing root - {"", "/"}, - {"a/", "/a/"}, - {"abc", "/abc"}, - {"abc/def", "/abc/def"}, - {"a/b/c", "/a/b/c"}, - - // Remove doubled slash - {"//", "/"}, - {"/abc//", "/abc/"}, - {"/abc/def//", "/abc/def/"}, - {"/a/b/c//", "/a/b/c/"}, - {"/abc//def//ghi", "/abc/def/ghi"}, - {"//abc", "/abc"}, - {"///abc", "/abc"}, - {"//abc//", "/abc/"}, - - // Remove . elements - {".", "/"}, - {"./", "/"}, - {"/abc/./def", "/abc/def"}, - {"/./abc/def", "/abc/def"}, - {"/abc/.", "/abc/"}, - - // Remove .. elements - {"..", "/"}, - {"../", "/"}, - {"../../", "/"}, - {"../..", "/"}, - {"../../abc", "/abc"}, - {"/abc/def/ghi/../jkl", "/abc/def/jkl"}, - {"/abc/def/../ghi/../jkl", "/abc/jkl"}, - {"/abc/def/..", "/abc"}, - {"/abc/def/../..", "/"}, - {"/abc/def/../../..", "/"}, - {"/abc/def/../../..", "/"}, - {"/abc/def/../../../ghi/jkl/../../../mno", "/mno"}, - - // Combinations - {"abc/./../def", "/def"}, - {"abc//./../def", "/def"}, - {"abc/../../././../def", "/def"}, -} - -func TestPathClean(t *testing.T) { - for _, test := range cleanTests { - if s := CleanPath(test.path); s != test.result { - t.Errorf("CleanPath(%q) = %q, want %q", test.path, s, test.result) - } - if s := CleanPath(test.result); s != test.result { - t.Errorf("CleanPath(%q) = %q, want %q", test.result, s, test.result) - } - } -} - -func TestPathCleanMallocs(t *testing.T) { - if testing.Short() { - t.Skip("skipping malloc count in short mode") - } - - for _, test := range cleanTests { - test := test - allocs := testing.AllocsPerRun(100, func() { CleanPath(test.result) }) - if allocs > 0 { - t.Errorf("CleanPath(%q): %v allocs, want zero", test.result, allocs) - } - } -} - -func BenchmarkPathClean(b *testing.B) { - b.ReportAllocs() - - for i := 0; i < b.N; i++ { - for _, test := range cleanTests { - CleanPath(test.path) - } - } -} - -func genLongPaths() (testPaths []cleanPathTest) { - for i := 1; i <= 1234; i++ { - ss := strings.Repeat("a", i) - - correctPath := "/" + ss - testPaths = append(testPaths, cleanPathTest{ - path: correctPath, - result: correctPath, - }, cleanPathTest{ - path: ss, - result: correctPath, - }, cleanPathTest{ - path: "//" + ss, - result: correctPath, - }, cleanPathTest{ - path: "/" + ss + "/b/..", - result: correctPath, - }) - } - return testPaths -} - -func TestPathCleanLong(t *testing.T) { - cleanTests := genLongPaths() - - for _, test := range cleanTests { - if s := CleanPath(test.path); s != test.result { - t.Errorf("CleanPath(%q) = %q, want %q", test.path, s, test.result) - } - if s := CleanPath(test.result); s != test.result { - t.Errorf("CleanPath(%q) = %q, want %q", test.result, s, test.result) - } - } -} - -func BenchmarkPathCleanLong(b *testing.B) { - cleanTests := genLongPaths() - b.ResetTimer() - b.ReportAllocs() - - for i := 0; i < b.N; i++ { - for _, test := range cleanTests { - CleanPath(test.path) - } - } -} diff --git a/http/router.go b/http/router.go deleted file mode 100644 index a1b78b6..0000000 --- a/http/router.go +++ /dev/null @@ -1,540 +0,0 @@ -// Copyright 2013 Julien Schmidt. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be found -// in the LICENSE file. - -// Package httprouter is a trie based high performance HTTP request router. -// -// A trivial example is: -// -// package main -// -// import ( -// "fmt" -// "github.com/julienschmidt/httprouter" -// "net/http" -// "log" -// ) -// -// func Index(w http.ResponseWriter, r *http.Request, _ httprouter.Params) { -// fmt.Fprint(w, "Welcome!\n") -// } -// -// func Hello(w http.ResponseWriter, r *http.Request, ps httprouter.Params) { -// fmt.Fprintf(w, "hello, %s!\n", ps.ByName("name")) -// } -// -// func main() { -// router := httprouter.New() -// router.GET("/", Index) -// router.GET("/hello/:name", Hello) -// -// log.Fatal(http.ListenAndServe(":8080", router)) -// } -// -// The router matches incoming requests by the request method and the path. -// If a handle is registered for this path and method, the router delegates the -// request to that function. -// For the methods GET, POST, PUT, PATCH, DELETE and OPTIONS shortcut functions exist to -// register handles, for all other methods router.Handle can be used. -// -// The registered path, against which the router matches incoming requests, can -// contain two types of parameters: -// Syntax Type -// :name named parameter -// *name catch-all parameter -// -// Named parameters are dynamic path segments. They match anything until the -// next '/' or the path end: -// Path: /blog/:category/:post -// -// Requests: -// /blog/go/request-routers match: category="go", post="request-routers" -// /blog/go/request-routers/ no match, but the router would redirect -// /blog/go/ no match -// /blog/go/request-routers/comments no match -// -// Catch-all parameters match anything until the path end, including the -// directory index (the '/' before the catch-all). Since they match anything -// until the end, catch-all parameters must always be the final path element. -// Path: /files/*filepath -// -// Requests: -// /files/ match: filepath="/" -// /files/LICENSE match: filepath="/LICENSE" -// /files/templates/article.html match: filepath="/templates/article.html" -// /files no match, but the router would redirect -// -// The value of parameters is saved as a slice of the Param struct, consisting -// each of a key and a value. The slice is passed to the Handle func as a third -// parameter. -// There are two ways to retrieve the value of a parameter: -// // by the name of the parameter -// user := ps.ByName("user") // defined by :user or *user -// -// // by the index of the parameter. This way you can also get the name (key) -// thirdKey := ps[2].Key // the name of the 3rd parameter -// thirdValue := ps[2].Value // the value of the 3rd parameter -package http - -import ( - "context" - "net/http" - "strings" - "sync" -) - -// Handle is a function that can be registered to a route to handle HTTP -// requests. Like http.HandlerFunc, but has a third parameter for the values of -// wildcards (path variables). -type Handle func(http.ResponseWriter, *http.Request, Params) - -// Param is a single URL parameter, consisting of a key and a value. -type Param struct { - Key string - Value string -} - -// Params is a Param-slice, as returned by the router. -// The slice is ordered, the first URL parameter is also the first slice value. -// It is therefore safe to read values by the index. -type Params []Param - -// ByName returns the value of the first Param which key matches the given name. -// If no matching Param is found, an empty string is returned. -func (ps Params) ByName(name string) string { - for _, p := range ps { - if p.Key == name { - return p.Value - } - } - return "" -} - -type paramsKey struct{} - -// ParamsKey is the request context key under which URL params are stored. -var ParamsKey = paramsKey{} - -// ParamsFromContext pulls the URL parameters from a request context, -// or returns nil if none are present. -func ParamsFromContext(ctx context.Context) Params { - p, _ := ctx.Value(ParamsKey).(Params) - return p -} - -// MatchedRoutePathParam is the Param name under which the path of the matched -// route is stored, if Router.SaveMatchedRoutePath is set. -var MatchedRoutePathParam = "$matchedRoutePath" - -// MatchedRoutePath retrieves the path of the matched route. -// Router.SaveMatchedRoutePath must have been enabled when the respective -// handler was added, otherwise this function always returns an empty string. -func (ps Params) MatchedRoutePath() string { - return ps.ByName(MatchedRoutePathParam) -} - -// Router is a http.Handler which can be used to dispatch requests to different -// handler functions via configurable routes -type Router struct { - trees map[string]*node - - paramsPool sync.Pool - maxParams uint16 - - // If enabled, adds the matched route path onto the http.Request context - // before invoking the handler. - // The matched route path is only added to handlers of routes that were - // registered when this option was enabled. - SaveMatchedRoutePath bool - - // Enables automatic redirection if the current route can't be matched but a - // handler for the path with (without) the trailing slash exists. - // For example if /foo/ is requested but a route only exists for /foo, the - // client is redirected to /foo with http status code 301 for GET requests - // and 308 for all other request methods. - RedirectTrailingSlash bool - - // If enabled, the router tries to fix the current request path, if no - // handle is registered for it. - // First superfluous path elements like ../ or // are removed. - // Afterwards the router does a case-insensitive lookup of the cleaned path. - // If a handle can be found for this route, the router makes a redirection - // to the corrected path with status code 301 for GET requests and 308 for - // all other request methods. - // For example /FOO and /..//Foo could be redirected to /foo. - // RedirectTrailingSlash is independent of this option. - RedirectFixedPath bool - - // If enabled, the router checks if another method is allowed for the - // current route, if the current request can not be routed. - // If this is the case, the request is answered with 'Method Not Allowed' - // and HTTP status code 405. - // If no other Method is allowed, the request is delegated to the NotFound - // handler. - HandleMethodNotAllowed bool - - // If enabled, the router automatically replies to OPTIONS requests. - // Custom OPTIONS handlers take priority over automatic replies. - HandleOPTIONS bool - - // An optional http.Handler that is called on automatic OPTIONS requests. - // The handler is only called if HandleOPTIONS is true and no OPTIONS - // handler for the specific path was set. - // The "Allowed" header is set before calling the handler. - GlobalOPTIONS http.Handler - - // Cached value of global (*) allowed methods - globalAllowed string - - // Configurable http.Handler which is called when no matching route is - // found. If it is not set, http.NotFound is used. - NotFound http.Handler - - // Configurable http.Handler which is called when a request - // cannot be routed and HandleMethodNotAllowed is true. - // If it is not set, http.Error with http.StatusMethodNotAllowed is used. - // The "Allow" header with allowed request methods is set before the handler - // is called. - MethodNotAllowed http.Handler - - // Function to handle panics recovered from http handlers. - // It should be used to generate a error page and return the http error code - // 500 (Internal Server Error). - // The handler can be used to keep your server from crashing because of - // unrecovered panics. - PanicHandler func(http.ResponseWriter, *http.Request, interface{}) -} - -// Make sure the Router conforms with the http.Handler interface -var _ http.Handler = New() - -// New returns a new initialized Router. -// Path auto-correction, including trailing slashes, is enabled by default. -func New() *Router { - return &Router{ - RedirectTrailingSlash: true, - RedirectFixedPath: true, - HandleMethodNotAllowed: true, - HandleOPTIONS: true, - } -} - -func (r *Router) getParams() *Params { - ps, _ := r.paramsPool.Get().(*Params) - *ps = (*ps)[0:0] // reset slice - return ps -} - -func (r *Router) putParams(ps *Params) { - if ps != nil { - r.paramsPool.Put(ps) - } -} - -func (r *Router) saveMatchedRoutePath(path string, handle Handle) Handle { - return func(w http.ResponseWriter, req *http.Request, ps Params) { - if ps == nil { - psp := r.getParams() - ps = (*psp)[0:1] - ps[0] = Param{Key: MatchedRoutePathParam, Value: path} - handle(w, req, ps) - r.putParams(psp) - } else { - ps = append(ps, Param{Key: MatchedRoutePathParam, Value: path}) - handle(w, req, ps) - } - } -} - -// GET is a shortcut for router.Handle(http.MethodGet, path, handle) -func (r *Router) GET(path string, handle Handle) { - r.Handle(http.MethodGet, path, handle) -} - -// HEAD is a shortcut for router.Handle(http.MethodHead, path, handle) -func (r *Router) HEAD(path string, handle Handle) { - r.Handle(http.MethodHead, path, handle) -} - -// OPTIONS is a shortcut for router.Handle(http.MethodOptions, path, handle) -func (r *Router) OPTIONS(path string, handle Handle) { - r.Handle(http.MethodOptions, path, handle) -} - -// POST is a shortcut for router.Handle(http.MethodPost, path, handle) -func (r *Router) POST(path string, handle Handle) { - r.Handle(http.MethodPost, path, handle) -} - -// PUT is a shortcut for router.Handle(http.MethodPut, path, handle) -func (r *Router) PUT(path string, handle Handle) { - r.Handle(http.MethodPut, path, handle) -} - -// PATCH is a shortcut for router.Handle(http.MethodPatch, path, handle) -func (r *Router) PATCH(path string, handle Handle) { - r.Handle(http.MethodPatch, path, handle) -} - -// DELETE is a shortcut for router.Handle(http.MethodDelete, path, handle) -func (r *Router) DELETE(path string, handle Handle) { - r.Handle(http.MethodDelete, path, handle) -} - -// Handle registers a new request handle with the given path and method. -// -// For GET, POST, PUT, PATCH and DELETE requests the respective shortcut -// functions can be used. -// -// This function is intended for bulk loading and to allow the usage of less -// frequently used, non-standardized or custom methods (e.g. for internal -// communication with a proxy). -func (r *Router) Handle(method, path string, handle Handle) { - varsCount := uint16(0) - - if method == "" { - panic("method must not be empty") - } - if len(path) < 1 || path[0] != '/' { - panic("path must begin with '/' in path '" + path + "'") - } - if handle == nil { - panic("handle must not be nil") - } - - if r.SaveMatchedRoutePath { - varsCount++ - handle = r.saveMatchedRoutePath(path, handle) - } - - if r.trees == nil { - r.trees = make(map[string]*node) - } - - root := r.trees[method] - if root == nil { - root = new(node) - r.trees[method] = root - - r.globalAllowed = r.allowed("*", "") - } - - root.addRoute(path, handle) - - // Update maxParams - if paramsCount := countParams(path); paramsCount+varsCount > r.maxParams { - r.maxParams = paramsCount + varsCount - } - - // Lazy-init paramsPool alloc func - if r.paramsPool.New == nil && r.maxParams > 0 { - r.paramsPool.New = func() interface{} { - ps := make(Params, 0, r.maxParams) - return &ps - } - } -} - -// Handler is an adapter which allows the usage of an http.Handler as a -// request handle. -// The Params are available in the request context under ParamsKey. -func (r *Router) Handler(method, path string, handler http.Handler) { - r.Handle(method, path, - func(w http.ResponseWriter, req *http.Request, p Params) { - if len(p) > 0 { - ctx := req.Context() - ctx = context.WithValue(ctx, ParamsKey, p) - req = req.WithContext(ctx) - } - handler.ServeHTTP(w, req) - }, - ) -} - -// HandlerFunc is an adapter which allows the usage of an http.HandlerFunc as a -// request handle. -func (r *Router) HandlerFunc(method, path string, handler http.HandlerFunc) { - r.Handler(method, path, handler) -} - -// ServeFiles serves files from the given file system root. -// The path must end with "/*filepath", files are then served from the local -// path /defined/root/dir/*filepath. -// For example if root is "/etc" and *filepath is "passwd", the local file -// "/etc/passwd" would be served. -// Internally a http.FileServer is used, therefore http.NotFound is used instead -// of the Router's NotFound handler. -// To use the operating system's file system implementation, -// use http.Dir: -// router.ServeFiles("/src/*filepath", http.Dir("/var/www")) -func (r *Router) ServeFiles(path string, root http.FileSystem) { - if len(path) < 10 || path[len(path)-10:] != "/*filepath" { - panic("path must end with /*filepath in path '" + path + "'") - } - - fileServer := http.FileServer(root) - - r.GET(path, func(w http.ResponseWriter, req *http.Request, ps Params) { - req.URL.Path = ps.ByName("filepath") - fileServer.ServeHTTP(w, req) - }) -} - -func (r *Router) recv(w http.ResponseWriter, req *http.Request) { - if rcv := recover(); rcv != nil { - r.PanicHandler(w, req, rcv) - } -} - -// Lookup allows the manual lookup of a method + path combo. -// This is e.g. useful to build a framework around this router. -// If the path was found, it returns the handle function and the path parameter -// values. Otherwise the third return value indicates whether a redirection to -// the same path with an extra / without the trailing slash should be performed. -func (r *Router) Lookup(method, path string) (Handle, Params, bool) { - if root := r.trees[method]; root != nil { - handle, ps, tsr := root.getValue(path, r.getParams) - if handle == nil { - r.putParams(ps) - return nil, nil, tsr - } - if ps == nil { - return handle, nil, tsr - } - return handle, *ps, tsr - } - return nil, nil, false -} - -func (r *Router) allowed(path, reqMethod string) (allow string) { - allowed := make([]string, 0, 9) - - if path == "*" { // server-wide - // empty method is used for internal calls to refresh the cache - if reqMethod == "" { - for method := range r.trees { - if method == http.MethodOptions { - continue - } - // Add request method to list of allowed methods - allowed = append(allowed, method) - } - } else { - return r.globalAllowed - } - } else { // specific path - for method := range r.trees { - // Skip the requested method - we already tried this one - if method == reqMethod || method == http.MethodOptions { - continue - } - - handle, _, _ := r.trees[method].getValue(path, nil) - if handle != nil { - // Add request method to list of allowed methods - allowed = append(allowed, method) - } - } - } - - if len(allowed) > 0 { - // Add request method to list of allowed methods - allowed = append(allowed, http.MethodOptions) - - // Sort allowed methods. - // sort.Strings(allowed) unfortunately causes unnecessary allocations - // due to allowed being moved to the heap and interface conversion - for i, l := 1, len(allowed); i < l; i++ { - for j := i; j > 0 && allowed[j] < allowed[j-1]; j-- { - allowed[j], allowed[j-1] = allowed[j-1], allowed[j] - } - } - - // return as comma separated list - return strings.Join(allowed, ", ") - } - - return allow -} - -// ServeHTTP makes the router implement the http.Handler interface. -func (r *Router) ServeHTTP(w http.ResponseWriter, req *http.Request) { - if r.PanicHandler != nil { - defer r.recv(w, req) - } - - path := req.URL.Path - - if root := r.trees[req.Method]; root != nil { - if handle, ps, tsr := root.getValue(path, r.getParams); handle != nil { - if ps != nil { - handle(w, req, *ps) - r.putParams(ps) - } else { - handle(w, req, nil) - } - return - } else if req.Method != http.MethodConnect && path != "/" { - // Moved Permanently, request with GET method - code := http.StatusMovedPermanently - if req.Method != http.MethodGet { - // Permanent Redirect, request with same method - code = http.StatusPermanentRedirect - } - - if tsr && r.RedirectTrailingSlash { - if len(path) > 1 && path[len(path)-1] == '/' { - req.URL.Path = path[:len(path)-1] - } else { - req.URL.Path = path + "/" - } - http.Redirect(w, req, req.URL.String(), code) - return - } - - // Try to fix the request path - if r.RedirectFixedPath { - fixedPath, found := root.findCaseInsensitivePath( - CleanPath(path), - r.RedirectTrailingSlash, - ) - if found { - req.URL.Path = fixedPath - http.Redirect(w, req, req.URL.String(), code) - return - } - } - } - } - - if req.Method == http.MethodOptions && r.HandleOPTIONS { - // Handle OPTIONS requests - if allow := r.allowed(path, http.MethodOptions); allow != "" { - w.Header().Set("Allow", allow) - if r.GlobalOPTIONS != nil { - r.GlobalOPTIONS.ServeHTTP(w, req) - } - return - } - } else if r.HandleMethodNotAllowed { // Handle 405 - if allow := r.allowed(path, req.Method); allow != "" { - w.Header().Set("Allow", allow) - if r.MethodNotAllowed != nil { - r.MethodNotAllowed.ServeHTTP(w, req) - } else { - http.Error(w, - http.StatusText(http.StatusMethodNotAllowed), - http.StatusMethodNotAllowed, - ) - } - return - } - } - - // Handle 404 - if r.NotFound != nil { - r.NotFound.ServeHTTP(w, req) - } else { - http.NotFound(w, req) - } -} diff --git a/http/router_test.go b/http/router_test.go deleted file mode 100644 index e28d2cb..0000000 --- a/http/router_test.go +++ /dev/null @@ -1,699 +0,0 @@ -// Copyright 2013 Julien Schmidt. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be found -// in the LICENSE file. - -package http - -import ( - "errors" - "fmt" - "net/http" - "net/http/httptest" - "reflect" - "testing" -) - -type mockResponseWriter struct{} - -func (m *mockResponseWriter) Header() (h http.Header) { - return http.Header{} -} - -func (m *mockResponseWriter) Write(p []byte) (n int, err error) { - return len(p), nil -} - -func (m *mockResponseWriter) WriteString(s string) (n int, err error) { - return len(s), nil -} - -func (m *mockResponseWriter) WriteHeader(int) {} - -func TestParams(t *testing.T) { - ps := Params{ - Param{"param1", "value1"}, - Param{"param2", "value2"}, - Param{"param3", "value3"}, - } - for i := range ps { - if val := ps.ByName(ps[i].Key); val != ps[i].Value { - t.Errorf("Wrong value for %s: Got %s; Want %s", ps[i].Key, val, ps[i].Value) - } - } - if val := ps.ByName("noKey"); val != "" { - t.Errorf("Expected empty string for not found key; got: %s", val) - } -} - -func TestRouter(t *testing.T) { - router := New() - - routed := false - router.Handle(http.MethodGet, "/user/:name", func(w http.ResponseWriter, r *http.Request, ps Params) { - routed = true - want := Params{Param{"name", "gopher"}} - if !reflect.DeepEqual(ps, want) { - t.Fatalf("wrong wildcard values: want %v, got %v", want, ps) - } - }) - - w := new(mockResponseWriter) - - req, _ := http.NewRequest(http.MethodGet, "/user/gopher", nil) - router.ServeHTTP(w, req) - - if !routed { - t.Fatal("routing failed") - } -} - -type handlerStruct struct { - handled *bool -} - -func (h handlerStruct) ServeHTTP(w http.ResponseWriter, r *http.Request) { - *h.handled = true -} - -func TestRouterAPI(t *testing.T) { - var get, head, options, post, put, patch, delete, handler, handlerFunc bool - - httpHandler := handlerStruct{&handler} - - router := New() - router.GET("/GET", func(w http.ResponseWriter, r *http.Request, _ Params) { - get = true - }) - router.HEAD("/GET", func(w http.ResponseWriter, r *http.Request, _ Params) { - head = true - }) - router.OPTIONS("/GET", func(w http.ResponseWriter, r *http.Request, _ Params) { - options = true - }) - router.POST("/POST", func(w http.ResponseWriter, r *http.Request, _ Params) { - post = true - }) - router.PUT("/PUT", func(w http.ResponseWriter, r *http.Request, _ Params) { - put = true - }) - router.PATCH("/PATCH", func(w http.ResponseWriter, r *http.Request, _ Params) { - patch = true - }) - router.DELETE("/DELETE", func(w http.ResponseWriter, r *http.Request, _ Params) { - delete = true - }) - router.Handler(http.MethodGet, "/Handler", httpHandler) - router.HandlerFunc(http.MethodGet, "/HandlerFunc", func(w http.ResponseWriter, r *http.Request) { - handlerFunc = true - }) - - w := new(mockResponseWriter) - - r, _ := http.NewRequest(http.MethodGet, "/GET", nil) - router.ServeHTTP(w, r) - if !get { - t.Error("routing GET failed") - } - - r, _ = http.NewRequest(http.MethodHead, "/GET", nil) - router.ServeHTTP(w, r) - if !head { - t.Error("routing HEAD failed") - } - - r, _ = http.NewRequest(http.MethodOptions, "/GET", nil) - router.ServeHTTP(w, r) - if !options { - t.Error("routing OPTIONS failed") - } - - r, _ = http.NewRequest(http.MethodPost, "/POST", nil) - router.ServeHTTP(w, r) - if !post { - t.Error("routing POST failed") - } - - r, _ = http.NewRequest(http.MethodPut, "/PUT", nil) - router.ServeHTTP(w, r) - if !put { - t.Error("routing PUT failed") - } - - r, _ = http.NewRequest(http.MethodPatch, "/PATCH", nil) - router.ServeHTTP(w, r) - if !patch { - t.Error("routing PATCH failed") - } - - r, _ = http.NewRequest(http.MethodDelete, "/DELETE", nil) - router.ServeHTTP(w, r) - if !delete { - t.Error("routing DELETE failed") - } - - r, _ = http.NewRequest(http.MethodGet, "/Handler", nil) - router.ServeHTTP(w, r) - if !handler { - t.Error("routing Handler failed") - } - - r, _ = http.NewRequest(http.MethodGet, "/HandlerFunc", nil) - router.ServeHTTP(w, r) - if !handlerFunc { - t.Error("routing HandlerFunc failed") - } -} - -func TestRouterInvalidInput(t *testing.T) { - router := New() - - handle := func(_ http.ResponseWriter, _ *http.Request, _ Params) {} - - recv := catchPanic(func() { - router.Handle("", "/", handle) - }) - if recv == nil { - t.Fatal("registering empty method did not panic") - } - - recv = catchPanic(func() { - router.GET("", handle) - }) - if recv == nil { - t.Fatal("registering empty path did not panic") - } - - recv = catchPanic(func() { - router.GET("noSlashRoot", handle) - }) - if recv == nil { - t.Fatal("registering path not beginning with '/' did not panic") - } - - recv = catchPanic(func() { - router.GET("/", nil) - }) - if recv == nil { - t.Fatal("registering nil handler did not panic") - } -} - -func TestRouterChaining(t *testing.T) { - router1 := New() - router2 := New() - router1.NotFound = router2 - - fooHit := false - router1.POST("/foo", func(w http.ResponseWriter, req *http.Request, _ Params) { - fooHit = true - w.WriteHeader(http.StatusOK) - }) - - barHit := false - router2.POST("/bar", func(w http.ResponseWriter, req *http.Request, _ Params) { - barHit = true - w.WriteHeader(http.StatusOK) - }) - - r, _ := http.NewRequest(http.MethodPost, "/foo", nil) - w := httptest.NewRecorder() - router1.ServeHTTP(w, r) - if !(w.Code == http.StatusOK && fooHit) { - t.Errorf("Regular routing failed with router chaining.") - t.FailNow() - } - - r, _ = http.NewRequest(http.MethodPost, "/bar", nil) - w = httptest.NewRecorder() - router1.ServeHTTP(w, r) - if !(w.Code == http.StatusOK && barHit) { - t.Errorf("Chained routing failed with router chaining.") - t.FailNow() - } - - r, _ = http.NewRequest(http.MethodPost, "/qax", nil) - w = httptest.NewRecorder() - router1.ServeHTTP(w, r) - if !(w.Code == http.StatusNotFound) { - t.Errorf("NotFound behavior failed with router chaining.") - t.FailNow() - } -} - -func BenchmarkAllowed(b *testing.B) { - handlerFunc := func(_ http.ResponseWriter, _ *http.Request, _ Params) {} - - router := New() - router.POST("/path", handlerFunc) - router.GET("/path", handlerFunc) - - b.Run("Global", func(b *testing.B) { - b.ReportAllocs() - for i := 0; i < b.N; i++ { - _ = router.allowed("*", http.MethodOptions) - } - }) - b.Run("Path", func(b *testing.B) { - b.ReportAllocs() - for i := 0; i < b.N; i++ { - _ = router.allowed("/path", http.MethodOptions) - } - }) -} - -func TestRouterOPTIONS(t *testing.T) { - handlerFunc := func(_ http.ResponseWriter, _ *http.Request, _ Params) {} - - router := New() - router.POST("/path", handlerFunc) - - // test not allowed - // * (server) - r, _ := http.NewRequest(http.MethodOptions, "*", nil) - w := httptest.NewRecorder() - router.ServeHTTP(w, r) - if !(w.Code == http.StatusOK) { - t.Errorf("OPTIONS handling failed: Code=%d, Header=%v", w.Code, w.Header()) - } else if allow := w.Header().Get("Allow"); allow != "OPTIONS, POST" { - t.Error("unexpected Allow header value: " + allow) - } - - // path - r, _ = http.NewRequest(http.MethodOptions, "/path", nil) - w = httptest.NewRecorder() - router.ServeHTTP(w, r) - if !(w.Code == http.StatusOK) { - t.Errorf("OPTIONS handling failed: Code=%d, Header=%v", w.Code, w.Header()) - } else if allow := w.Header().Get("Allow"); allow != "OPTIONS, POST" { - t.Error("unexpected Allow header value: " + allow) - } - - r, _ = http.NewRequest(http.MethodOptions, "/doesnotexist", nil) - w = httptest.NewRecorder() - router.ServeHTTP(w, r) - if !(w.Code == http.StatusNotFound) { - t.Errorf("OPTIONS handling failed: Code=%d, Header=%v", w.Code, w.Header()) - } - - // add another method - router.GET("/path", handlerFunc) - - // set a global OPTIONS handler - router.GlobalOPTIONS = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - // Adjust status code to 204 - w.WriteHeader(http.StatusNoContent) - }) - - // test again - // * (server) - r, _ = http.NewRequest(http.MethodOptions, "*", nil) - w = httptest.NewRecorder() - router.ServeHTTP(w, r) - if !(w.Code == http.StatusNoContent) { - t.Errorf("OPTIONS handling failed: Code=%d, Header=%v", w.Code, w.Header()) - } else if allow := w.Header().Get("Allow"); allow != "GET, OPTIONS, POST" { - t.Error("unexpected Allow header value: " + allow) - } - - // path - r, _ = http.NewRequest(http.MethodOptions, "/path", nil) - w = httptest.NewRecorder() - router.ServeHTTP(w, r) - if !(w.Code == http.StatusNoContent) { - t.Errorf("OPTIONS handling failed: Code=%d, Header=%v", w.Code, w.Header()) - } else if allow := w.Header().Get("Allow"); allow != "GET, OPTIONS, POST" { - t.Error("unexpected Allow header value: " + allow) - } - - // custom handler - var custom bool - router.OPTIONS("/path", func(w http.ResponseWriter, r *http.Request, _ Params) { - custom = true - }) - - // test again - // * (server) - r, _ = http.NewRequest(http.MethodOptions, "*", nil) - w = httptest.NewRecorder() - router.ServeHTTP(w, r) - if !(w.Code == http.StatusNoContent) { - t.Errorf("OPTIONS handling failed: Code=%d, Header=%v", w.Code, w.Header()) - } else if allow := w.Header().Get("Allow"); allow != "GET, OPTIONS, POST" { - t.Error("unexpected Allow header value: " + allow) - } - if custom { - t.Error("custom handler called on *") - } - - // path - r, _ = http.NewRequest(http.MethodOptions, "/path", nil) - w = httptest.NewRecorder() - router.ServeHTTP(w, r) - if !(w.Code == http.StatusOK) { - t.Errorf("OPTIONS handling failed: Code=%d, Header=%v", w.Code, w.Header()) - } - if !custom { - t.Error("custom handler not called") - } -} - -func TestRouterNotAllowed(t *testing.T) { - handlerFunc := func(_ http.ResponseWriter, _ *http.Request, _ Params) {} - - router := New() - router.POST("/path", handlerFunc) - - // test not allowed - r, _ := http.NewRequest(http.MethodGet, "/path", nil) - w := httptest.NewRecorder() - router.ServeHTTP(w, r) - if !(w.Code == http.StatusMethodNotAllowed) { - t.Errorf("NotAllowed handling failed: Code=%d, Header=%v", w.Code, w.Header()) - } else if allow := w.Header().Get("Allow"); allow != "OPTIONS, POST" { - t.Error("unexpected Allow header value: " + allow) - } - - // add another method - router.DELETE("/path", handlerFunc) - router.OPTIONS("/path", handlerFunc) // must be ignored - - // test again - r, _ = http.NewRequest(http.MethodGet, "/path", nil) - w = httptest.NewRecorder() - router.ServeHTTP(w, r) - if !(w.Code == http.StatusMethodNotAllowed) { - t.Errorf("NotAllowed handling failed: Code=%d, Header=%v", w.Code, w.Header()) - } else if allow := w.Header().Get("Allow"); allow != "DELETE, OPTIONS, POST" { - t.Error("unexpected Allow header value: " + allow) - } - - // test custom handler - w = httptest.NewRecorder() - responseText := "custom method" - router.MethodNotAllowed = http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { - w.WriteHeader(http.StatusTeapot) - w.Write([]byte(responseText)) - }) - router.ServeHTTP(w, r) - if got := w.Body.String(); !(got == responseText) { - t.Errorf("unexpected response got %q want %q", got, responseText) - } - if w.Code != http.StatusTeapot { - t.Errorf("unexpected response code %d want %d", w.Code, http.StatusTeapot) - } - if allow := w.Header().Get("Allow"); allow != "DELETE, OPTIONS, POST" { - t.Error("unexpected Allow header value: " + allow) - } -} - -func TestRouterNotFound(t *testing.T) { - handlerFunc := func(_ http.ResponseWriter, _ *http.Request, _ Params) {} - - router := New() - router.GET("/path", handlerFunc) - router.GET("/dir/", handlerFunc) - router.GET("/", handlerFunc) - - testRoutes := []struct { - route string - code int - location string - }{ - {"/path/", http.StatusMovedPermanently, "/path"}, // TSR -/ - {"/dir", http.StatusMovedPermanently, "/dir/"}, // TSR +/ - {"", http.StatusMovedPermanently, "/"}, // TSR +/ - {"/PATH", http.StatusMovedPermanently, "/path"}, // Fixed Case - {"/DIR/", http.StatusMovedPermanently, "/dir/"}, // Fixed Case - {"/PATH/", http.StatusMovedPermanently, "/path"}, // Fixed Case -/ - {"/DIR", http.StatusMovedPermanently, "/dir/"}, // Fixed Case +/ - {"/../path", http.StatusMovedPermanently, "/path"}, // CleanPath - {"/nope", http.StatusNotFound, ""}, // NotFound - } - for _, tr := range testRoutes { - r, _ := http.NewRequest(http.MethodGet, tr.route, nil) - w := httptest.NewRecorder() - router.ServeHTTP(w, r) - if !(w.Code == tr.code && (w.Code == http.StatusNotFound || fmt.Sprint(w.Header().Get("Location")) == tr.location)) { - t.Errorf("NotFound handling route %s failed: Code=%d, Header=%v", tr.route, w.Code, w.Header().Get("Location")) - } - } - - // Test custom not found handler - var notFound bool - router.NotFound = http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) { - rw.WriteHeader(http.StatusNotFound) - notFound = true - }) - r, _ := http.NewRequest(http.MethodGet, "/nope", nil) - w := httptest.NewRecorder() - router.ServeHTTP(w, r) - if !(w.Code == http.StatusNotFound && notFound == true) { - t.Errorf("Custom NotFound handler failed: Code=%d, Header=%v", w.Code, w.Header()) - } - - // Test other method than GET (want 308 instead of 301) - router.PATCH("/path", handlerFunc) - r, _ = http.NewRequest(http.MethodPatch, "/path/", nil) - w = httptest.NewRecorder() - router.ServeHTTP(w, r) - if !(w.Code == http.StatusPermanentRedirect && fmt.Sprint(w.Header()) == "map[Location:[/path]]") { - t.Errorf("Custom NotFound handler failed: Code=%d, Header=%v", w.Code, w.Header()) - } - - // Test special case where no node for the prefix "/" exists - router = New() - router.GET("/a", handlerFunc) - r, _ = http.NewRequest(http.MethodGet, "/", nil) - w = httptest.NewRecorder() - router.ServeHTTP(w, r) - if !(w.Code == http.StatusNotFound) { - t.Errorf("NotFound handling route / failed: Code=%d", w.Code) - } -} - -func TestRouterPanicHandler(t *testing.T) { - router := New() - panicHandled := false - - router.PanicHandler = func(rw http.ResponseWriter, r *http.Request, p interface{}) { - panicHandled = true - } - - router.Handle(http.MethodPut, "/user/:name", func(_ http.ResponseWriter, _ *http.Request, _ Params) { - panic("oops!") - }) - - w := new(mockResponseWriter) - req, _ := http.NewRequest(http.MethodPut, "/user/gopher", nil) - - defer func() { - if rcv := recover(); rcv != nil { - t.Fatal("handling panic failed") - } - }() - - router.ServeHTTP(w, req) - - if !panicHandled { - t.Fatal("simulating failed") - } -} - -func TestRouterLookup(t *testing.T) { - routed := false - wantHandle := func(_ http.ResponseWriter, _ *http.Request, _ Params) { - routed = true - } - wantParams := Params{Param{"name", "gopher"}} - - router := New() - - // try empty router first - handle, _, tsr := router.Lookup(http.MethodGet, "/nope") - if handle != nil { - t.Fatalf("Got handle for unregistered pattern: %v", handle) - } - if tsr { - t.Error("Got wrong TSR recommendation!") - } - - // insert route and try again - router.GET("/user/:name", wantHandle) - handle, params, _ := router.Lookup(http.MethodGet, "/user/gopher") - if handle == nil { - t.Fatal("Got no handle!") - } else { - handle(nil, nil, nil) - if !routed { - t.Fatal("Routing failed!") - } - } - if !reflect.DeepEqual(params, wantParams) { - t.Fatalf("Wrong parameter values: want %v, got %v", wantParams, params) - } - routed = false - - // route without param - router.GET("/user", wantHandle) - handle, params, _ = router.Lookup(http.MethodGet, "/user") - if handle == nil { - t.Fatal("Got no handle!") - } else { - handle(nil, nil, nil) - if !routed { - t.Fatal("Routing failed!") - } - } - if params != nil { - t.Fatalf("Wrong parameter values: want %v, got %v", nil, params) - } - - handle, _, tsr = router.Lookup(http.MethodGet, "/user/gopher/") - if handle != nil { - t.Fatalf("Got handle for unregistered pattern: %v", handle) - } - if !tsr { - t.Error("Got no TSR recommendation!") - } - - handle, _, tsr = router.Lookup(http.MethodGet, "/nope") - if handle != nil { - t.Fatalf("Got handle for unregistered pattern: %v", handle) - } - if tsr { - t.Error("Got wrong TSR recommendation!") - } -} - -func TestRouterParamsFromContext(t *testing.T) { - routed := false - - wantParams := Params{Param{"name", "gopher"}} - handlerFunc := func(_ http.ResponseWriter, req *http.Request) { - // get params from request context - params := ParamsFromContext(req.Context()) - - if !reflect.DeepEqual(params, wantParams) { - t.Fatalf("Wrong parameter values: want %v, got %v", wantParams, params) - } - - routed = true - } - - var nilParams Params - handlerFuncNil := func(_ http.ResponseWriter, req *http.Request) { - // get params from request context - params := ParamsFromContext(req.Context()) - - if !reflect.DeepEqual(params, nilParams) { - t.Fatalf("Wrong parameter values: want %v, got %v", nilParams, params) - } - - routed = true - } - router := New() - router.HandlerFunc(http.MethodGet, "/user", handlerFuncNil) - router.HandlerFunc(http.MethodGet, "/user/:name", handlerFunc) - - w := new(mockResponseWriter) - r, _ := http.NewRequest(http.MethodGet, "/user/gopher", nil) - router.ServeHTTP(w, r) - if !routed { - t.Fatal("Routing failed!") - } - - routed = false - r, _ = http.NewRequest(http.MethodGet, "/user", nil) - router.ServeHTTP(w, r) - if !routed { - t.Fatal("Routing failed!") - } -} - -func TestRouterMatchedRoutePath(t *testing.T) { - route1 := "/user/:name" - routed1 := false - handle1 := func(_ http.ResponseWriter, req *http.Request, ps Params) { - route := ps.MatchedRoutePath() - if route != route1 { - t.Fatalf("Wrong matched route: want %s, got %s", route1, route) - } - routed1 = true - } - - route2 := "/user/:name/details" - routed2 := false - handle2 := func(_ http.ResponseWriter, req *http.Request, ps Params) { - route := ps.MatchedRoutePath() - if route != route2 { - t.Fatalf("Wrong matched route: want %s, got %s", route2, route) - } - routed2 = true - } - - route3 := "/" - routed3 := false - handle3 := func(_ http.ResponseWriter, req *http.Request, ps Params) { - route := ps.MatchedRoutePath() - if route != route3 { - t.Fatalf("Wrong matched route: want %s, got %s", route3, route) - } - routed3 = true - } - - router := New() - router.SaveMatchedRoutePath = true - router.Handle(http.MethodGet, route1, handle1) - router.Handle(http.MethodGet, route2, handle2) - router.Handle(http.MethodGet, route3, handle3) - - w := new(mockResponseWriter) - r, _ := http.NewRequest(http.MethodGet, "/user/gopher", nil) - router.ServeHTTP(w, r) - if !routed1 || routed2 || routed3 { - t.Fatal("Routing failed!") - } - - w = new(mockResponseWriter) - r, _ = http.NewRequest(http.MethodGet, "/user/gopher/details", nil) - router.ServeHTTP(w, r) - if !routed2 || routed3 { - t.Fatal("Routing failed!") - } - - w = new(mockResponseWriter) - r, _ = http.NewRequest(http.MethodGet, "/", nil) - router.ServeHTTP(w, r) - if !routed3 { - t.Fatal("Routing failed!") - } -} - -type mockFileSystem struct { - opened bool -} - -func (mfs *mockFileSystem) Open(name string) (http.File, error) { - mfs.opened = true - return nil, errors.New("this is just a mock") -} - -func TestRouterServeFiles(t *testing.T) { - router := New() - mfs := &mockFileSystem{} - - recv := catchPanic(func() { - router.ServeFiles("/noFilepath", mfs) - }) - if recv == nil { - t.Fatal("registering path not ending with '*filepath' did not panic") - } - - router.ServeFiles("/*filepath", mfs) - w := new(mockResponseWriter) - r, _ := http.NewRequest(http.MethodGet, "/favicon.ico", nil) - router.ServeHTTP(w, r) - if !mfs.opened { - t.Error("serving file failed") - } -} diff --git a/http/tree.go b/http/tree.go deleted file mode 100644 index f97f653..0000000 --- a/http/tree.go +++ /dev/null @@ -1,687 +0,0 @@ -// Copyright 2013 Julien Schmidt. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be found -// in the LICENSE file. - -package http - -import ( - "strings" - "unicode" - "unicode/utf8" -) - -func min(a, b int) int { - if a <= b { - return a - } - return b -} - -func longestCommonPrefix(a, b string) int { - i := 0 - max := min(len(a), len(b)) - for i < max && a[i] == b[i] { - i++ - } - return i -} - -// Search for a wildcard segment and check the name for invalid characters. -// Returns -1 as index, if no wildcard was found. -func findWildcard(path string) (wilcard string, i int, valid bool) { - // Find start - for start, c := range []byte(path) { - // A wildcard starts with ':' (param) or '*' (catch-all) - if c != ':' && c != '*' { - continue - } - - // Find end and check for invalid characters - valid = true - for end, c := range []byte(path[start+1:]) { - switch c { - case '/': - return path[start : start+1+end], start, valid - case ':', '*': - valid = false - } - } - return path[start:], start, valid - } - return "", -1, false -} - -func countParams(path string) uint16 { - var n uint - for i := range []byte(path) { - switch path[i] { - case ':', '*': - n++ - } - } - return uint16(n) -} - -type nodeType uint8 - -const ( - static nodeType = iota // default - root - param //: - catchAll //* -) - -type node struct { - path string - indices string - //是否是: *通配符 - wildChild bool - nType nodeType - priority uint32 - children []*node - handle Handle -} - -// Increments priority of the given child and reorders if necessary -func (n *node) incrementChildPrio(pos int) int { - cs := n.children - cs[pos].priority++ - prio := cs[pos].priority - - // Adjust position (move to front) - newPos := pos - for ; newPos > 0 && cs[newPos-1].priority < prio; newPos-- { - // Swap node positions - cs[newPos-1], cs[newPos] = cs[newPos], cs[newPos-1] - } - - // Build new index char string - if newPos != pos { - n.indices = n.indices[:newPos] + // Unchanged prefix, might be empty - n.indices[pos:pos+1] + // The index char we move - n.indices[newPos:pos] + n.indices[pos+1:] // Rest without char at 'pos' - } - - return newPos -} - -// addRoute adds a node with the given handle to the path. -// Not concurrency-safe! -func (n *node) addRoute(path string, handle Handle) { - fullPath := path - n.priority++ - - // Empty tree - if n.path == "" && n.indices == "" { - n.insertChild(path, fullPath, handle) - n.nType = root - return - } - -walk: - for { - // Find the longest common prefix. - // This also implies that the common prefix contains no ':' or '*' - // since the existing key can't contain those chars. - i := longestCommonPrefix(path, n.path) - - // Split edge - // 比现有路径短 - if i < len(n.path) { - child := node{ - path: n.path[i:], - wildChild: n.wildChild, - nType: static, - indices: n.indices, - children: n.children, - handle: n.handle, - priority: n.priority - 1, - } - - n.children = []*node{&child} - // []byte for proper unicode char conversion, see #65 - n.indices = string([]byte{n.path[i]}) - n.path = path[:i] - n.handle = nil - n.wildChild = false - } - - // Make new node a child of this node - if i < len(path) { - path = path[i:] - - if n.wildChild { - n = n.children[0] - n.priority++ - - // Check if the wildcard matches - if len(path) >= len(n.path) && n.path == path[:len(n.path)] && - // Adding a child to a catchAll is not possible - n.nType != catchAll && - // Check for longer wildcard, e.g. :name and :names - (len(n.path) >= len(path) || path[len(n.path)] == '/') { - continue walk - } else { - // Wildcard conflict - pathSeg := path - if n.nType != catchAll { - pathSeg = strings.SplitN(pathSeg, "/", 2)[0] - } - prefix := fullPath[:strings.Index(fullPath, pathSeg)] + n.path - panic("'" + pathSeg + - "' in new path '" + fullPath + - "' conflicts with existing wildcard '" + n.path + - "' in existing prefix '" + prefix + - "'") - } - } - - idxc := path[0] - - // '/' after param - if n.nType == param && idxc == '/' && len(n.children) == 1 { - n = n.children[0] - n.priority++ - continue walk - } - - // Check if a child with the next path byte exists - for i, c := range []byte(n.indices) { - if c == idxc { - i = n.incrementChildPrio(i) - n = n.children[i] - continue walk - } - } - - // Otherwise insert it - if idxc != ':' && idxc != '*' { - // []byte for proper unicode char conversion, see #65 - n.indices += string([]byte{idxc}) - child := &node{} - n.children = append(n.children, child) - n.incrementChildPrio(len(n.indices) - 1) - n = child - } - n.insertChild(path, fullPath, handle) - return - } - - // Otherwise add handle to current node - if n.handle != nil { - panic("a handle is already registered for path '" + fullPath + "'") - } - n.handle = handle - return - } -} - -func (n *node) insertChild(path, fullPath string, handle Handle) { - for { - // Find prefix until first wildcard - wildcard, i, valid := findWildcard(path) - if i < 0 { // No wilcard found - break - } - - // The wildcard name must not contain ':' and '*' - if !valid { - panic("only one wildcard per path segment is allowed, has: '" + - wildcard + "' in path '" + fullPath + "'") - } - - // Check if the wildcard has a name - if len(wildcard) < 2 { - panic("wildcards must be named with a non-empty name in path '" + fullPath + "'") - } - - // Check if this node has existing children which would be - // unreachable if we insert the wildcard here - if len(n.children) > 0 { - panic("wildcard segment '" + wildcard + - "' conflicts with existing children in path '" + fullPath + "'") - } - - // param - if wildcard[0] == ':' { - if i > 0 { - // Insert prefix before the current wildcard - n.path = path[:i] - path = path[i:] - } - - n.wildChild = true - child := &node{ - nType: param, - path: wildcard, - } - n.children = []*node{child} - n = child - n.priority++ - - // If the path doesn't end with the wildcard, then there - // will be another non-wildcard subpath starting with '/' - if len(wildcard) < len(path) { - path = path[len(wildcard):] - child := &node{ - priority: 1, - } - n.children = []*node{child} - n = child - continue - } - - // Otherwise we're done. Insert the handle in the new leaf - n.handle = handle - return - } - - // catchAll - if i+len(wildcard) != len(path) { - panic("catch-all routes are only allowed at the end of the path in path '" + fullPath + "'") - } - - if len(n.path) > 0 && n.path[len(n.path)-1] == '/' { - panic("catch-all conflicts with existing handle for the path segment root in path '" + fullPath + "'") - } - - // Currently fixed width 1 for '/' - i-- - if path[i] != '/' { - panic("no / before catch-all in path '" + fullPath + "'") - } - - n.path = path[:i] - - // First node: catchAll node with empty path - child := &node{ - wildChild: true, - nType: catchAll, - } - n.children = []*node{child} - n.indices = string('/') - n = child - n.priority++ - - // Second node: node holding the variable - child = &node{ - path: path[i:], - nType: catchAll, - handle: handle, - priority: 1, - } - n.children = []*node{child} - - return - } - - // If no wildcard was found, simply insert the path and handle - n.path = path - n.handle = handle -} - -// Returns the handle registered with the given path (key). The values of -// wildcards are saved to a map. -// If no handle can be found, a TSR (trailing slash redirect) recommendation is -// made if a handle exists with an extra (without the) trailing slash for the -// given path. -func (n *node) getValue(path string, params func() *Params) (handle Handle, ps *Params, tsr bool) { -walk: // Outer loop for walking the tree - for { - prefix := n.path - if len(path) > len(prefix) { - if path[:len(prefix)] == prefix { - path = path[len(prefix):] - - // If this node does not have a wildcard (param or catchAll) - // child, we can just look up the next child node and continue - // to walk down the tree - if !n.wildChild { - idxc := path[0] - for i, c := range []byte(n.indices) { - if c == idxc { - n = n.children[i] - continue walk - } - } - - // Nothing found. - // We can recommend to redirect to the same URL without a - // trailing slash if a leaf exists for that path. - tsr = (path == "/" && n.handle != nil) - return - } - - // Handle wildcard child - n = n.children[0] - switch n.nType { - case param: - // Find param end (either '/' or path end) - end := 0 - for end < len(path) && path[end] != '/' { - end++ - } - - // Save param value - if params != nil { - if ps == nil { - ps = params() - } - // Expand slice within preallocated capacity - i := len(*ps) - *ps = (*ps)[:i+1] - (*ps)[i] = Param{ - Key: n.path[1:], - Value: path[:end], - } - } - - // We need to go deeper! - if end < len(path) { - if len(n.children) > 0 { - path = path[end:] - n = n.children[0] - continue walk - } - - // ... but we can't - tsr = (len(path) == end+1) - return - } - - if handle = n.handle; handle != nil { - return - } else if len(n.children) == 1 { - // No handle found. Check if a handle for this path + a - // trailing slash exists for TSR recommendation - n = n.children[0] - tsr = (n.path == "/" && n.handle != nil) || (n.path == "" && n.indices == "/") - } - - return - - case catchAll: - // Save param value - if params != nil { - if ps == nil { - ps = params() - } - // Expand slice within preallocated capacity - i := len(*ps) - *ps = (*ps)[:i+1] - (*ps)[i] = Param{ - Key: n.path[2:], - Value: path, - } - } - - handle = n.handle - return - - default: - panic("invalid node type") - } - } - } else if path == prefix { - // We should have reached the node containing the handle. - // Check if this node has a handle registered. - if handle = n.handle; handle != nil { - return - } - - // If there is no handle for this route, but this route has a - // wildcard child, there must be a handle for this path with an - // additional trailing slash - if path == "/" && n.wildChild && n.nType != root { - tsr = true - return - } - - if path == "/" && n.nType == static { - tsr = true - return - } - - // No handle found. Check if a handle for this path + a - // trailing slash exists for trailing slash recommendation - for i, c := range []byte(n.indices) { - if c == '/' { - n = n.children[i] - tsr = (len(n.path) == 1 && n.handle != nil) || - (n.nType == catchAll && n.children[0].handle != nil) - return - } - } - return - } - - // Nothing found. We can recommend to redirect to the same URL with an - // extra trailing slash if a leaf exists for that path - tsr = (path == "/") || - (len(prefix) == len(path)+1 && prefix[len(path)] == '/' && - path == prefix[:len(prefix)-1] && n.handle != nil) - return - } -} - -// Makes a case-insensitive lookup of the given path and tries to find a handler. -// It can optionally also fix trailing slashes. -// It returns the case-corrected path and a bool indicating whether the lookup -// was successful. -func (n *node) findCaseInsensitivePath(path string, fixTrailingSlash bool) (fixedPath string, found bool) { - const stackBufSize = 128 - - // Use a static sized buffer on the stack in the common case. - // If the path is too long, allocate a buffer on the heap instead. - buf := make([]byte, 0, stackBufSize) - if l := len(path) + 1; l > stackBufSize { - buf = make([]byte, 0, l) - } - - ciPath := n.findCaseInsensitivePathRec( - path, - buf, // Preallocate enough memory for new path - [4]byte{}, // Empty rune buffer - fixTrailingSlash, - ) - - return string(ciPath), ciPath != nil -} - -// Shift bytes in array by n bytes left -func shiftNRuneBytes(rb [4]byte, n int) [4]byte { - switch n { - case 0: - return rb - case 1: - return [4]byte{rb[1], rb[2], rb[3], 0} - case 2: - return [4]byte{rb[2], rb[3]} - case 3: - return [4]byte{rb[3]} - default: - return [4]byte{} - } -} - -// Recursive case-insensitive lookup function used by n.findCaseInsensitivePath -func (n *node) findCaseInsensitivePathRec(path string, ciPath []byte, rb [4]byte, fixTrailingSlash bool) []byte { - npLen := len(n.path) - -walk: // Outer loop for walking the tree - for len(path) >= npLen && (npLen == 0 || strings.EqualFold(path[1:npLen], n.path[1:])) { - // Add common prefix to result - oldPath := path - path = path[npLen:] - ciPath = append(ciPath, n.path...) - - if len(path) > 0 { - // If this node does not have a wildcard (param or catchAll) child, - // we can just look up the next child node and continue to walk down - // the tree - if !n.wildChild { - // Skip rune bytes already processed - rb = shiftNRuneBytes(rb, npLen) - - if rb[0] != 0 { - // Old rune not finished - idxc := rb[0] - for i, c := range []byte(n.indices) { - if c == idxc { - // continue with child node - n = n.children[i] - npLen = len(n.path) - continue walk - } - } - } else { - // Process a new rune - var rv rune - - // Find rune start. - // Runes are up to 4 byte long, - // -4 would definitely be another rune. - var off int - for max := min(npLen, 3); off < max; off++ { - if i := npLen - off; utf8.RuneStart(oldPath[i]) { - // read rune from cached path - rv, _ = utf8.DecodeRuneInString(oldPath[i:]) - break - } - } - - // Calculate lowercase bytes of current rune - lo := unicode.ToLower(rv) - utf8.EncodeRune(rb[:], lo) - - // Skip already processed bytes - rb = shiftNRuneBytes(rb, off) - - idxc := rb[0] - for i, c := range []byte(n.indices) { - // Lowercase matches - if c == idxc { - // must use a recursive approach since both the - // uppercase byte and the lowercase byte might exist - // as an index - if out := n.children[i].findCaseInsensitivePathRec( - path, ciPath, rb, fixTrailingSlash, - ); out != nil { - return out - } - break - } - } - - // If we found no match, the same for the uppercase rune, - // if it differs - if up := unicode.ToUpper(rv); up != lo { - utf8.EncodeRune(rb[:], up) - rb = shiftNRuneBytes(rb, off) - - idxc := rb[0] - for i, c := range []byte(n.indices) { - // Uppercase matches - if c == idxc { - // Continue with child node - n = n.children[i] - npLen = len(n.path) - continue walk - } - } - } - } - - // Nothing found. We can recommend to redirect to the same URL - // without a trailing slash if a leaf exists for that path - if fixTrailingSlash && path == "/" && n.handle != nil { - return ciPath - } - return nil - } - - n = n.children[0] - switch n.nType { - case param: - // Find param end (either '/' or path end) - end := 0 - for end < len(path) && path[end] != '/' { - end++ - } - - // Add param value to case insensitive path - ciPath = append(ciPath, path[:end]...) - - // We need to go deeper! - if end < len(path) { - if len(n.children) > 0 { - // Continue with child node - n = n.children[0] - npLen = len(n.path) - path = path[end:] - continue - } - - // ... but we can't - if fixTrailingSlash && len(path) == end+1 { - return ciPath - } - return nil - } - - if n.handle != nil { - return ciPath - } else if fixTrailingSlash && len(n.children) == 1 { - // No handle found. Check if a handle for this path + a - // trailing slash exists - n = n.children[0] - if n.path == "/" && n.handle != nil { - return append(ciPath, '/') - } - } - return nil - - case catchAll: - return append(ciPath, path...) - - default: - panic("invalid node type") - } - } else { - // We should have reached the node containing the handle. - // Check if this node has a handle registered. - if n.handle != nil { - return ciPath - } - - // No handle found. - // Try to fix the path by adding a trailing slash - if fixTrailingSlash { - for i, c := range []byte(n.indices) { - if c == '/' { - n = n.children[i] - if (len(n.path) == 1 && n.handle != nil) || - (n.nType == catchAll && n.children[0].handle != nil) { - return append(ciPath, '/') - } - return nil - } - } - } - return nil - } - } - - // Nothing found. - // Try to fix the path by adding / removing a trailing slash - if fixTrailingSlash { - if path == "/" { - return ciPath - } - if len(path)+1 == npLen && n.path[len(path)] == '/' && - strings.EqualFold(path[1:], n.path[1:len(path)]) && n.handle != nil { - return append(ciPath, n.path...) - } - } - return nil -} diff --git a/http/tree_test.go b/http/tree_test.go deleted file mode 100644 index 6560e2f..0000000 --- a/http/tree_test.go +++ /dev/null @@ -1,721 +0,0 @@ -// Copyright 2013 Julien Schmidt. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be found -// in the LICENSE file. - -package http - -import ( - "fmt" - "net/http" - "reflect" - "regexp" - "strings" - "testing" -) - -// func printChildren(n *node, prefix string) { -// fmt.Printf(" %02d %s%s[%d] %v %t %d \r\n", n.priority, prefix, n.path, len(n.children), n.handle, n.wildChild, n.nType) -// for l := len(n.path); l > 0; l-- { -// prefix += " " -// } -// for _, child := range n.children { -// printChildren(child, prefix) -// } -// } - -// Used as a workaround since we can't compare functions or their addresses -var fakeHandlerValue string - -func fakeHandler(val string) Handle { - return func(http.ResponseWriter, *http.Request, Params) { - fakeHandlerValue = val - } -} - -type testRequests []struct { - path string - nilHandler bool - route string - ps Params -} - -func getParams() *Params { - ps := make(Params, 0, 20) - return &ps -} - -func checkRequests(t *testing.T, tree *node, requests testRequests) { - for _, request := range requests { - handler, psp, _ := tree.getValue(request.path, getParams) - - switch { - case handler == nil: - if !request.nilHandler { - t.Errorf("handle mismatch for route '%s': Expected non-nil handle", request.path) - } - case request.nilHandler: - t.Errorf("handle mismatch for route '%s': Expected nil handle", request.path) - default: - handler(nil, nil, nil) - if fakeHandlerValue != request.route { - t.Errorf("handle mismatch for route '%s': Wrong handle (%s != %s)", request.path, fakeHandlerValue, request.route) - } - } - - var ps Params - if psp != nil { - ps = *psp - } - - if !reflect.DeepEqual(ps, request.ps) { - t.Errorf("Params mismatch for route '%s'", request.path) - } - } -} - -func checkPriorities(t *testing.T, n *node) uint32 { - var prio uint32 - for i := range n.children { - prio += checkPriorities(t, n.children[i]) - } - - if n.handle != nil { - prio++ - } - - if n.priority != prio { - t.Errorf( - "priority mismatch for node '%s': is %d, should be %d", - n.path, n.priority, prio, - ) - } - - return prio -} - -func TestCountParams(t *testing.T) { - if countParams("/path/:param1/static/*catch-all") != 2 { - t.Fail() - } - if countParams(strings.Repeat("/:param", 256)) != 256 { - t.Fail() - } -} - -func TestTreeAddAndGet(t *testing.T) { - tree := &node{} - - routes := [...]string{ - "/hi", - "/contact", - "/co", - "/c", - "/a", - "/ab", - "/doc/", - "/doc/go_faq.html", - "/doc/go1.html", - "/α", - "/β", - } - for _, route := range routes { - tree.addRoute(route, fakeHandler(route)) - } - - // printChildren(tree, "") - - checkRequests(t, tree, testRequests{ - {"/a", false, "/a", nil}, - {"/", true, "", nil}, - {"/hi", false, "/hi", nil}, - {"/contact", false, "/contact", nil}, - {"/co", false, "/co", nil}, - {"/con", true, "", nil}, // key mismatch - {"/cona", true, "", nil}, // key mismatch - {"/no", true, "", nil}, // no matching child - {"/ab", false, "/ab", nil}, - {"/α", false, "/α", nil}, - {"/β", false, "/β", nil}, - }) - - checkPriorities(t, tree) -} - -func TestTreeWildcard(t *testing.T) { - tree := &node{} - - routes := [...]string{ - "/", - "/cmd/:tool/:sub", - "/cmd/:tool/", - "/src/*filepath", - "/search/", - "/search/:query", - "/user_:name", - "/user_:name/about", - "/files/:dir/*filepath", - "/doc/", - "/doc/go_faq.html", - "/doc/go1.html", - "/info/:user/public", - "/info/:user/project/:project", - } - for _, route := range routes { - tree.addRoute(route, fakeHandler(route)) - } - - // printChildren(tree, "") - - checkRequests(t, tree, testRequests{ - {"/", false, "/", nil}, - {"/cmd/test/", false, "/cmd/:tool/", Params{Param{"tool", "test"}}}, - {"/cmd/test", true, "", Params{Param{"tool", "test"}}}, - {"/cmd/test/3", false, "/cmd/:tool/:sub", Params{Param{"tool", "test"}, Param{"sub", "3"}}}, - {"/src/", false, "/src/*filepath", Params{Param{"filepath", "/"}}}, - {"/src/some/file.png", false, "/src/*filepath", Params{Param{"filepath", "/some/file.png"}}}, - {"/search/", false, "/search/", nil}, - {"/search/someth!ng+in+ünìcodé", false, "/search/:query", Params{Param{"query", "someth!ng+in+ünìcodé"}}}, - {"/search/someth!ng+in+ünìcodé/", true, "", Params{Param{"query", "someth!ng+in+ünìcodé"}}}, - {"/user_gopher", false, "/user_:name", Params{Param{"name", "gopher"}}}, - {"/user_gopher/about", false, "/user_:name/about", Params{Param{"name", "gopher"}}}, - {"/files/js/inc/framework.js", false, "/files/:dir/*filepath", Params{Param{"dir", "js"}, Param{"filepath", "/inc/framework.js"}}}, - {"/info/gordon/public", false, "/info/:user/public", Params{Param{"user", "gordon"}}}, - {"/info/gordon/project/go", false, "/info/:user/project/:project", Params{Param{"user", "gordon"}, Param{"project", "go"}}}, - }) - - checkPriorities(t, tree) -} - -func catchPanic(testFunc func()) (recv interface{}) { - defer func() { - recv = recover() - }() - - testFunc() - return -} - -type testRoute struct { - path string - conflict bool -} - -func testRoutes(t *testing.T, routes []testRoute) { - tree := &node{} - - for i := range routes { - route := routes[i] - recv := catchPanic(func() { - tree.addRoute(route.path, nil) - }) - - if route.conflict { - if recv == nil { - t.Errorf("no panic for conflicting route '%s'", route.path) - } - } else if recv != nil { - t.Errorf("unexpected panic for route '%s': %v", route.path, recv) - } - } - - // printChildren(tree, "") -} - -func TestTreeWildcardConflict(t *testing.T) { - routes := []testRoute{ - {"/cmd/:tool/:sub", false}, - {"/cmd/vet", true}, - {"/src/*filepath", false}, - {"/src/*filepathx", true}, - {"/src/", true}, - {"/src1/", false}, - {"/src1/*filepath", true}, - {"/src2*filepath", true}, - {"/search/:query", false}, - {"/search/invalid", true}, - {"/user_:name", false}, - {"/user_x", true}, - {"/user_:name", false}, - {"/id:id", false}, - {"/id/:id", true}, - } - testRoutes(t, routes) -} - -func TestTreeChildConflict(t *testing.T) { - routes := []testRoute{ - {"/cmd/vet", false}, - {"/cmd/:tool/:sub", true}, - {"/src/AUTHORS", false}, - {"/src/*filepath", true}, - {"/user_x", false}, - {"/user_:name", true}, - {"/id/:id", false}, - {"/id:id", true}, - {"/:id", true}, - {"/*filepath", true}, - } - testRoutes(t, routes) -} - -func TestTreeDupliatePath(t *testing.T) { - tree := &node{} - - routes := [...]string{ - "/", - "/doc/", - "/src/*filepath", - "/search/:query", - "/user_:name", - } - for i := range routes { - route := routes[i] - recv := catchPanic(func() { - tree.addRoute(route, fakeHandler(route)) - }) - if recv != nil { - t.Fatalf("panic inserting route '%s': %v", route, recv) - } - - // Add again - recv = catchPanic(func() { - tree.addRoute(route, nil) - }) - if recv == nil { - t.Fatalf("no panic while inserting duplicate route '%s", route) - } - } - - // printChildren(tree, "") - - checkRequests(t, tree, testRequests{ - {"/", false, "/", nil}, - {"/doc/", false, "/doc/", nil}, - {"/src/some/file.png", false, "/src/*filepath", Params{Param{"filepath", "/some/file.png"}}}, - {"/search/someth!ng+in+ünìcodé", false, "/search/:query", Params{Param{"query", "someth!ng+in+ünìcodé"}}}, - {"/user_gopher", false, "/user_:name", Params{Param{"name", "gopher"}}}, - }) -} - -func TestEmptyWildcardName(t *testing.T) { - tree := &node{} - - routes := [...]string{ - "/user:", - "/user:/", - "/cmd/:/", - "/src/*", - } - for i := range routes { - route := routes[i] - recv := catchPanic(func() { - tree.addRoute(route, nil) - }) - if recv == nil { - t.Fatalf("no panic while inserting route with empty wildcard name '%s", route) - } - } -} - -func TestTreeCatchAllConflict(t *testing.T) { - routes := []testRoute{ - {"/src/*filepath/x", true}, - {"/src2/", false}, - {"/src2/*filepath/x", true}, - {"/src3/*filepath", false}, - {"/src3/*filepath/x", true}, - } - testRoutes(t, routes) -} - -func TestTreeCatchAllConflictRoot(t *testing.T) { - routes := []testRoute{ - {"/", false}, - {"/*filepath", true}, - } - testRoutes(t, routes) -} - -func TestTreeCatchMaxParams(t *testing.T) { - tree := &node{} - var route = "/cmd/*filepath" - tree.addRoute(route, fakeHandler(route)) -} - -func TestTreeDoubleWildcard(t *testing.T) { - const panicMsg = "only one wildcard per path segment is allowed" - - routes := [...]string{ - "/:foo:bar", - "/:foo:bar/", - "/:foo*bar", - } - - for i := range routes { - route := routes[i] - tree := &node{} - recv := catchPanic(func() { - tree.addRoute(route, nil) - }) - - if rs, ok := recv.(string); !ok || !strings.HasPrefix(rs, panicMsg) { - t.Fatalf(`"Expected panic "%s" for route '%s', got "%v"`, panicMsg, route, recv) - } - } -} - -func TestTreeTrailingSlashRedirect(t *testing.T) { - tree := &node{} - - routes := [...]string{ - "/hi", - "/b/", - "/search/:query", - "/cmd/:tool/", - "/src/*filepath", - "/x", - "/x/y", - "/y/", - "/y/z", - "/0/:id", - "/0/:id/1", - "/1/:id/", - "/1/:id/2", - "/aa", - "/a/", - "/admin", - "/admin/:category", - "/admin/:category/:page", - "/doc", - "/doc/go_faq.html", - "/doc/go1.html", - "/no/a", - "/no/b", - "/api/hello/:name", - "/vendor/:x/*y", - } - for i := range routes { - route := routes[i] - recv := catchPanic(func() { - tree.addRoute(route, fakeHandler(route)) - }) - if recv != nil { - t.Fatalf("panic inserting route '%s': %v", route, recv) - } - } - - // printChildren(tree, "") - - tsrRoutes := [...]string{ - "/hi/", - "/b", - "/search/gopher/", - "/cmd/vet", - "/src", - "/x/", - "/y", - "/0/go/", - "/1/go", - "/a", - "/admin/", - "/admin/config/", - "/admin/config/permissions/", - "/doc/", - "/vendor/x", - } - for _, route := range tsrRoutes { - handler, _, tsr := tree.getValue(route, nil) - if handler != nil { - t.Fatalf("non-nil handler for TSR route '%s", route) - } else if !tsr { - t.Errorf("expected TSR recommendation for route '%s'", route) - } - } - - noTsrRoutes := [...]string{ - "/", - "/no", - "/no/", - "/_", - "/_/", - "/api/world/abc", - } - for _, route := range noTsrRoutes { - handler, _, tsr := tree.getValue(route, nil) - if handler != nil { - t.Fatalf("non-nil handler for No-TSR route '%s", route) - } else if tsr { - t.Errorf("expected no TSR recommendation for route '%s'", route) - } - } -} - -func TestTreeRootTrailingSlashRedirect(t *testing.T) { - tree := &node{} - - recv := catchPanic(func() { - tree.addRoute("/:test", fakeHandler("/:test")) - }) - if recv != nil { - t.Fatalf("panic inserting test route: %v", recv) - } - - handler, _, tsr := tree.getValue("/", nil) - if handler != nil { - t.Fatalf("non-nil handler") - } else if tsr { - t.Errorf("expected no TSR recommendation") - } -} - -func TestTreeFindCaseInsensitivePath(t *testing.T) { - tree := &node{} - - longPath := "/l" + strings.Repeat("o", 128) + "ng" - lOngPath := "/l" + strings.Repeat("O", 128) + "ng/" - - routes := [...]string{ - "/hi", - "/b/", - "/ABC/", - "/search/:query", - "/cmd/:tool/", - "/src/*filepath", - "/x", - "/x/y", - "/y/", - "/y/z", - "/0/:id", - "/0/:id/1", - "/1/:id/", - "/1/:id/2", - "/aa", - "/a/", - "/doc", - "/doc/go_faq.html", - "/doc/go1.html", - "/doc/go/away", - "/no/a", - "/no/b", - "/Π", - "/u/apfêl/", - "/u/äpfêl/", - "/u/öpfêl", - "/v/Äpfêl/", - "/v/Öpfêl", - "/w/♬", // 3 byte - "/w/♭/", // 3 byte, last byte differs - "/w/𠜎", // 4 byte - "/w/𠜏/", // 4 byte - longPath, - } - - for i := range routes { - route := routes[i] - recv := catchPanic(func() { - tree.addRoute(route, fakeHandler(route)) - }) - if recv != nil { - t.Fatalf("panic inserting route '%s': %v", route, recv) - } - } - - // Check out == in for all registered routes - // With fixTrailingSlash = true - for i := range routes { - route := routes[i] - out, found := tree.findCaseInsensitivePath(route, true) - if !found { - t.Errorf("Route '%s' not found!", route) - } else if out != route { - t.Errorf("Wrong result for route '%s': %s", route, out) - } - } - // With fixTrailingSlash = false - for i := range routes { - route := routes[i] - out, found := tree.findCaseInsensitivePath(route, false) - if !found { - t.Errorf("Route '%s' not found!", route) - } else if out != route { - t.Errorf("Wrong result for route '%s': %s", route, out) - } - } - - tests := []struct { - in string - out string - found bool - slash bool - }{ - {"/HI", "/hi", true, false}, - {"/HI/", "/hi", true, true}, - {"/B", "/b/", true, true}, - {"/B/", "/b/", true, false}, - {"/abc", "/ABC/", true, true}, - {"/abc/", "/ABC/", true, false}, - {"/aBc", "/ABC/", true, true}, - {"/aBc/", "/ABC/", true, false}, - {"/abC", "/ABC/", true, true}, - {"/abC/", "/ABC/", true, false}, - {"/SEARCH/QUERY", "/search/QUERY", true, false}, - {"/SEARCH/QUERY/", "/search/QUERY", true, true}, - {"/CMD/TOOL/", "/cmd/TOOL/", true, false}, - {"/CMD/TOOL", "/cmd/TOOL/", true, true}, - {"/SRC/FILE/PATH", "/src/FILE/PATH", true, false}, - {"/x/Y", "/x/y", true, false}, - {"/x/Y/", "/x/y", true, true}, - {"/X/y", "/x/y", true, false}, - {"/X/y/", "/x/y", true, true}, - {"/X/Y", "/x/y", true, false}, - {"/X/Y/", "/x/y", true, true}, - {"/Y/", "/y/", true, false}, - {"/Y", "/y/", true, true}, - {"/Y/z", "/y/z", true, false}, - {"/Y/z/", "/y/z", true, true}, - {"/Y/Z", "/y/z", true, false}, - {"/Y/Z/", "/y/z", true, true}, - {"/y/Z", "/y/z", true, false}, - {"/y/Z/", "/y/z", true, true}, - {"/Aa", "/aa", true, false}, - {"/Aa/", "/aa", true, true}, - {"/AA", "/aa", true, false}, - {"/AA/", "/aa", true, true}, - {"/aA", "/aa", true, false}, - {"/aA/", "/aa", true, true}, - {"/A/", "/a/", true, false}, - {"/A", "/a/", true, true}, - {"/DOC", "/doc", true, false}, - {"/DOC/", "/doc", true, true}, - {"/NO", "", false, true}, - {"/DOC/GO", "", false, true}, - {"/π", "/Π", true, false}, - {"/π/", "/Π", true, true}, - {"/u/ÄPFÊL/", "/u/äpfêl/", true, false}, - {"/u/ÄPFÊL", "/u/äpfêl/", true, true}, - {"/u/ÖPFÊL/", "/u/öpfêl", true, true}, - {"/u/ÖPFÊL", "/u/öpfêl", true, false}, - {"/v/äpfêL/", "/v/Äpfêl/", true, false}, - {"/v/äpfêL", "/v/Äpfêl/", true, true}, - {"/v/öpfêL/", "/v/Öpfêl", true, true}, - {"/v/öpfêL", "/v/Öpfêl", true, false}, - {"/w/♬/", "/w/♬", true, true}, - {"/w/♭", "/w/♭/", true, true}, - {"/w/𠜎/", "/w/𠜎", true, true}, - {"/w/𠜏", "/w/𠜏/", true, true}, - {lOngPath, longPath, true, true}, - } - // With fixTrailingSlash = true - for _, test := range tests { - out, found := tree.findCaseInsensitivePath(test.in, true) - if found != test.found || (found && (out != test.out)) { - t.Errorf("Wrong result for '%s': got %s, %t; want %s, %t", - test.in, out, found, test.out, test.found) - return - } - } - // With fixTrailingSlash = false - for _, test := range tests { - out, found := tree.findCaseInsensitivePath(test.in, false) - if test.slash { - if found { // test needs a trailingSlash fix. It must not be found! - t.Errorf("Found without fixTrailingSlash: %s; got %s", test.in, out) - } - } else { - if found != test.found || (found && (out != test.out)) { - t.Errorf("Wrong result for '%s': got %s, %t; want %s, %t", - test.in, out, found, test.out, test.found) - return - } - } - } -} - -func TestTreeInvalidNodeType(t *testing.T) { - const panicMsg = "invalid node type" - - tree := &node{} - tree.addRoute("/", fakeHandler("/")) - tree.addRoute("/:page", fakeHandler("/:page")) - - // set invalid node type - tree.children[0].nType = 42 - - // normal lookup - recv := catchPanic(func() { - tree.getValue("/test", nil) - }) - if rs, ok := recv.(string); !ok || rs != panicMsg { - t.Fatalf("Expected panic '"+panicMsg+"', got '%v'", recv) - } - - // case-insensitive lookup - recv = catchPanic(func() { - tree.findCaseInsensitivePath("/test", true) - }) - if rs, ok := recv.(string); !ok || rs != panicMsg { - t.Fatalf("Expected panic '"+panicMsg+"', got '%v'", recv) - } -} - -func TestTreeWildcardConflictEx(t *testing.T) { - conflicts := [...]struct { - route string - segPath string - existPath string - existSegPath string - }{ - {"/who/are/foo", "/foo", `/who/are/\*you`, `/\*you`}, - {"/who/are/foo/", "/foo/", `/who/are/\*you`, `/\*you`}, - {"/who/are/foo/bar", "/foo/bar", `/who/are/\*you`, `/\*you`}, - {"/conxxx", "xxx", `/con:tact`, `:tact`}, - {"/conooo/xxx", "ooo", `/con:tact`, `:tact`}, - } - - for i := range conflicts { - conflict := conflicts[i] - - // I have to re-create a 'tree', because the 'tree' will be - // in an inconsistent state when the loop recovers from the - // panic which threw by 'addRoute' function. - tree := &node{} - routes := [...]string{ - "/con:tact", - "/who/are/*you", - "/who/foo/hello", - } - - for i := range routes { - route := routes[i] - tree.addRoute(route, fakeHandler(route)) - } - - recv := catchPanic(func() { - tree.addRoute(conflict.route, fakeHandler(conflict.route)) - }) - - if !regexp.MustCompile(fmt.Sprintf("'%s' in new path .* conflicts with existing wildcard '%s' in existing prefix '%s'", conflict.segPath, conflict.existSegPath, conflict.existPath)).MatchString(fmt.Sprint(recv)) { - t.Fatalf("invalid wildcard conflict error (%v)", recv) - } - } -} - -func TestRedirectTrailingSlash(t *testing.T) { - var data = []struct { - path string - }{ - {"/hello/:name"}, - {"/hello/:name/123"}, - {"/hello/:name/234"}, - } - - node := &node{} - for _, item := range data { - node.addRoute(item.path, fakeHandler("test")) - } - - _, _, tsr := node.getValue("/hello/abx/", nil) - if tsr != true { - t.Fatalf("want true, is false") - } -} diff --git a/main.go b/main.go index 392537b..6be23b5 100644 --- a/main.go +++ b/main.go @@ -1,8 +1,8 @@ package main import ( + "git.echinacities.com/mogfee/protoc-gen-kit/proto/v1/auth" "github.com/golang/protobuf/proto" - "github.com/mogfee/protoc-gen-kit/proto/v1/auth" "google.golang.org/genproto/googleapis/api/annotations" "google.golang.org/protobuf/compiler/protogen" "google.golang.org/protobuf/types/descriptorpb" @@ -12,24 +12,24 @@ import ( //go build && protoc --unknow_out=./proto --go_out=./proto/ --go-grpc_out=./proto proto/user.proto -I ./proto // protoc --unknow_out=./proto proto/user.proto func main() { - u := &Unknow{ + u := &Kit{ imports: map[string]string{}, } protogen.Options{}.Run(u.Generate) } -type Unknow struct { +type Kit struct { imports map[string]string } -func (u *Unknow) addImports(imp string) { +func (u *Kit) addImports(imp string) { u.imports[imp] = imp } -func (u *Unknow) Generate(plugin *protogen.Plugin) error { +func (u *Kit) Generate(plugin *protogen.Plugin) error { if len(plugin.Files) < 1 { return nil } - u.addImports("github.com/mogfee/protoc-gen-kit/response") + u.addImports("git.echinacities.com/mogfee/protoc-gen-kit/response") u.addImports("github.com/gin-gonic/gin") for _, f := range plugin.Files { if len(f.Services) == 0 { @@ -84,7 +84,7 @@ func (u *Unknow) Generate(plugin *protogen.Plugin) error { return nil } -func (Unknow) genGet(t *protogen.GeneratedFile, m *protogen.Method) { +func (Kit) genGet(t *protogen.GeneratedFile, m *protogen.Method) { t.P("func _http_", method_get, `_`, m.GoName, "(router *gin.Engine, method string, srv UserServer){") t.P(`router.`, method_get, `(method, func(c *gin.Context) { post := `, m.Input.GoIdent.GoName, `{} @@ -102,7 +102,7 @@ func (Unknow) genGet(t *protogen.GeneratedFile, m *protogen.Method) { })`) t.P("}") } -func (Unknow) genPost(t *protogen.GeneratedFile, m *protogen.Method) { +func (Kit) genPost(t *protogen.GeneratedFile, m *protogen.Method) { t.P("func _http_", method_post, `_`, m.GoName, "(router *gin.Engine, method string, srv UserServer){") t.P(`router.`, method_post, `(method, func(c *gin.Context) { post := `, m.Input.GoIdent.GoName, `{} @@ -120,7 +120,7 @@ func (Unknow) genPost(t *protogen.GeneratedFile, m *protogen.Method) { })`) t.P("}") } -func (Unknow) genDelete(t *protogen.GeneratedFile, m *protogen.Method) { +func (Kit) genDelete(t *protogen.GeneratedFile, m *protogen.Method) { t.P("func _http_", method_delete, `_`, m.GoName, "(router *gin.Engine, method string, srv UserServer){") t.P(`router.`, method_delete, `(method, func(c *gin.Context) { post := `, m.Input.GoIdent.GoName, `{} @@ -139,7 +139,7 @@ func (Unknow) genDelete(t *protogen.GeneratedFile, m *protogen.Method) { t.P("}") } -func (Unknow) getAuth(m *protogen.Method) (bool, string) { +func (Kit) getAuth(m *protogen.Method) (bool, string) { if op, ok := m.Desc.Options().(*descriptorpb.MethodOptions); ok { if opts, err := proto.GetExtension(op, auth.E_Auth); err != nil { log.Println(err) @@ -151,7 +151,7 @@ func (Unknow) getAuth(m *protogen.Method) (bool, string) { } return false, "" } -func (Unknow) getMethod(m *protogen.Method) (method string, path string) { +func (Kit) getMethod(m *protogen.Method) (method string, path string) { if op, ok := m.Desc.Options().(*descriptorpb.MethodOptions); ok { if opts, err := proto.GetExtension(op, annotations.E_Http); err != nil { log.Println(err) diff --git a/proto/auth/auth.proto b/proto/auth/auth.proto index f4d6f0e..2d5c4b2 100644 --- a/proto/auth/auth.proto +++ b/proto/auth/auth.proto @@ -1,6 +1,6 @@ syntax = "proto3"; package auth; -option go_package = "github.com/mogfee/protoc-gen-kit/proto/v1/auth;auth"; +option go_package = "git.echinacities.com/mogfee/protoc-gen-kit/proto/v1/auth;auth"; import "google/protobuf/descriptor.proto"; diff --git a/proto/v1/auth/auth.pb.go b/proto/v1/auth/auth.pb.go index fd142b3..ae6aaff 100644 --- a/proto/v1/auth/auth.pb.go +++ b/proto/v1/auth/auth.pb.go @@ -107,11 +107,12 @@ var file_auth_auth_proto_rawDesc = []byte{ 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x4d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x4f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x18, 0xd7, 0x08, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x0e, 0x2e, 0x61, 0x75, 0x74, 0x68, 0x2e, 0x41, 0x75, 0x74, 0x68, 0x49, - 0x6e, 0x66, 0x6f, 0x52, 0x04, 0x61, 0x75, 0x74, 0x68, 0x42, 0x35, 0x5a, 0x33, 0x67, 0x69, 0x74, - 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x6d, 0x6f, 0x67, 0x66, 0x65, 0x65, 0x2f, 0x70, - 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x2d, 0x67, 0x65, 0x6e, 0x2d, 0x6b, 0x69, 0x74, 0x2f, 0x70, 0x72, - 0x6f, 0x74, 0x6f, 0x2f, 0x76, 0x31, 0x2f, 0x61, 0x75, 0x74, 0x68, 0x3b, 0x61, 0x75, 0x74, 0x68, - 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, + 0x6e, 0x66, 0x6f, 0x52, 0x04, 0x61, 0x75, 0x74, 0x68, 0x42, 0x3f, 0x5a, 0x3d, 0x67, 0x69, 0x74, + 0x2e, 0x65, 0x63, 0x68, 0x69, 0x6e, 0x61, 0x63, 0x69, 0x74, 0x69, 0x65, 0x73, 0x2e, 0x63, 0x6f, + 0x6d, 0x2f, 0x6d, 0x6f, 0x67, 0x66, 0x65, 0x65, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x2d, + 0x67, 0x65, 0x6e, 0x2d, 0x6b, 0x69, 0x74, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2f, 0x76, 0x31, + 0x2f, 0x61, 0x75, 0x74, 0x68, 0x3b, 0x61, 0x75, 0x74, 0x68, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, + 0x6f, 0x33, } var ( diff --git a/proto/v1/user.pb.go b/proto/v1/user.pb.go index d9fd488..a8dc05b 100644 --- a/proto/v1/user.pb.go +++ b/proto/v1/user.pb.go @@ -7,8 +7,8 @@ package user import ( + _ "git.echinacities.com/mogfee/protoc-gen-kit/proto/v1/auth" _ "github.com/envoyproxy/protoc-gen-validate/validate" - _ "github.com/mogfee/protoc-gen-kit/proto/v1/auth" _ "google.golang.org/genproto/googleapis/api/annotations" protoreflect "google.golang.org/protobuf/reflect/protoreflect" protoimpl "google.golang.org/protobuf/runtime/protoimpl" diff --git a/response/response.go b/response/response.go index 94dee91..1070792 100644 --- a/response/response.go +++ b/response/response.go @@ -2,9 +2,9 @@ package response import ( "encoding/json" + "git.echinacities.com/mogfee/protoc-gen-kit/xerrors" "github.com/gin-gonic/gin" "github.com/go-playground/form" - "github.com/mogfee/protoc-gen-kit/xerrors" "net/http" ) diff --git a/run/main.go b/run/main.go index 8a7671a..402412d 100644 --- a/run/main.go +++ b/run/main.go @@ -1,32 +1,13 @@ package main import ( + user "git.echinacities.com/mogfee/protoc-gen-kit/proto/v1" + "git.echinacities.com/mogfee/protoc-gen-kit/service" "github.com/gin-gonic/gin" - user "github.com/mogfee/protoc-gen-kit/proto/v1" - "github.com/mogfee/protoc-gen-kit/response" - "github.com/mogfee/protoc-gen-kit/service" ) func main() { app := gin.Default() - app.GET("/", func(c *gin.Context) { - resp := response.New(c) - post := user.LoginRequest{} - if err := resp.BindQuery(&post); err != nil { - resp.Error(err) - return - } - c.JSON(200, post) - }) - app.POST("/", func(c *gin.Context) { - resp := response.New(c) - post := user.LoginRequest{} - if err := resp.BindJSON(&post); err != nil { - resp.Error(err) - return - } - c.JSON(200, post) - }) srv := service.UserService{} user.NewGin(app, &srv) app.Run("localhost:8888") diff --git a/service/service.go b/service/service.go index f69a12e..819f825 100644 --- a/service/service.go +++ b/service/service.go @@ -4,30 +4,30 @@ import ( "context" "encoding/json" "errors" - user2 "github.com/mogfee/protoc-gen-kit/proto/v1" - "github.com/mogfee/protoc-gen-kit/xerrors" + user "git.echinacities.com/mogfee/protoc-gen-kit/proto/v1" + "git.echinacities.com/mogfee/protoc-gen-kit/xerrors" ) type UserService struct { - user2.UnimplementedUserServer + user.UnimplementedUserServer } -func (*UserService) Login(ctx context.Context, req *user2.LoginRequest) (*user2.LoginResponse, error) { +func (*UserService) Login(ctx context.Context, req *user.LoginRequest) (*user.LoginResponse, error) { return nil, xerrors.BadRequest("BadRequest", "B") b, _ := json.Marshal(req) - return &user2.LoginResponse{Token: string(b)}, nil + return &user.LoginResponse{Token: string(b)}, nil } -func (*UserService) List(ctx context.Context, req *user2.LoginRequest) (*user2.LoginResponse, error) { +func (*UserService) List(ctx context.Context, req *user.LoginRequest) (*user.LoginResponse, error) { return nil, xerrors.InternalServer("InternalServer", "B") b, _ := json.Marshal(req) - return &user2.LoginResponse{Token: string(b)}, nil + return &user.LoginResponse{Token: string(b)}, nil } -func (*UserService) Delete(ctx context.Context, req *user2.LoginRequest) (*user2.LoginResponse, error) { +func (*UserService) Delete(ctx context.Context, req *user.LoginRequest) (*user.LoginResponse, error) { return nil, errors.New("bad err") b, _ := json.Marshal(req) - return &user2.LoginResponse{Token: string(b)}, nil + return &user.LoginResponse{Token: string(b)}, nil }