github.com/segakazzz/buffalo@v0.16.22-0.20210119082501-1f52048d3feb/route_mappings.go (about)

     1  package buffalo
     2  
     3  import (
     4  	"fmt"
     5  	"net/http"
     6  	"net/url"
     7  	"os"
     8  	"path"
     9  	"reflect"
    10  	"sort"
    11  	"strings"
    12  
    13  	"github.com/gobuffalo/envy"
    14  	"github.com/gobuffalo/flect/name"
    15  	"github.com/gorilla/handlers"
    16  )
    17  
    18  const (
    19  	// AssetsAgeVarName is the ENV variable used to specify max age when ServeFiles is used.
    20  	AssetsAgeVarName = "ASSETS_MAX_AGE"
    21  )
    22  
    23  // GET maps an HTTP "GET" request to the path and the specified handler.
    24  func (a *App) GET(p string, h Handler) *RouteInfo {
    25  	return a.addRoute("GET", p, h)
    26  }
    27  
    28  // POST maps an HTTP "POST" request to the path and the specified handler.
    29  func (a *App) POST(p string, h Handler) *RouteInfo {
    30  	return a.addRoute("POST", p, h)
    31  }
    32  
    33  // PUT maps an HTTP "PUT" request to the path and the specified handler.
    34  func (a *App) PUT(p string, h Handler) *RouteInfo {
    35  	return a.addRoute("PUT", p, h)
    36  }
    37  
    38  // DELETE maps an HTTP "DELETE" request to the path and the specified handler.
    39  func (a *App) DELETE(p string, h Handler) *RouteInfo {
    40  	return a.addRoute("DELETE", p, h)
    41  }
    42  
    43  // HEAD maps an HTTP "HEAD" request to the path and the specified handler.
    44  func (a *App) HEAD(p string, h Handler) *RouteInfo {
    45  	return a.addRoute("HEAD", p, h)
    46  }
    47  
    48  // OPTIONS maps an HTTP "OPTIONS" request to the path and the specified handler.
    49  func (a *App) OPTIONS(p string, h Handler) *RouteInfo {
    50  	return a.addRoute("OPTIONS", p, h)
    51  }
    52  
    53  // PATCH maps an HTTP "PATCH" request to the path and the specified handler.
    54  func (a *App) PATCH(p string, h Handler) *RouteInfo {
    55  	return a.addRoute("PATCH", p, h)
    56  }
    57  
    58  // Redirect from one URL to another URL. Only works for "GET" requests.
    59  func (a *App) Redirect(status int, from, to string) *RouteInfo {
    60  	return a.GET(from, func(c Context) error {
    61  		return c.Redirect(status, to)
    62  	})
    63  }
    64  
    65  // Mount mounts a http.Handler (or Buffalo app) and passes through all requests to it.
    66  //
    67  //	func muxer() http.Handler {
    68  //		f := func(res http.ResponseWriter, req *http.Request) {
    69  //			fmt.Fprintf(res, "%s - %s", req.Method, req.URL.String())
    70  //		}
    71  //		mux := mux.NewRouter()
    72  //		mux.HandleFunc("/foo", f).Methods("GET")
    73  //		mux.HandleFunc("/bar", f).Methods("POST")
    74  //		mux.HandleFunc("/baz/baz", f).Methods("DELETE")
    75  //		return mux
    76  //	}
    77  //
    78  //	a.Mount("/admin", muxer())
    79  //
    80  //	$ curl -X DELETE http://localhost:3000/admin/baz/baz
    81  func (a *App) Mount(p string, h http.Handler) {
    82  	prefix := path.Join(a.Prefix, p)
    83  	path := path.Join(p, "{path:.+}")
    84  	a.ANY(path, WrapHandler(http.StripPrefix(prefix, h)))
    85  }
    86  
    87  // ServeFiles maps an path to a directory on disk to serve static files.
    88  // Useful for JavaScript, images, CSS, etc...
    89  /*
    90  	a.ServeFiles("/assets", http.Dir("path/to/assets"))
    91  */
    92  func (a *App) ServeFiles(p string, root http.FileSystem) {
    93  	path := path.Join(a.Prefix, p)
    94  	a.filepaths = append(a.filepaths, path)
    95  
    96  	h := stripAsset(path, a.fileServer(root), a)
    97  	a.router.PathPrefix(path).Handler(h)
    98  }
    99  
   100  func (a *App) fileServer(fs http.FileSystem) http.Handler {
   101  	fsh := http.FileServer(fs)
   102  	baseHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
   103  		f, err := fs.Open(path.Clean(r.URL.Path))
   104  		if os.IsNotExist(err) {
   105  			eh := a.ErrorHandlers.Get(http.StatusNotFound)
   106  			eh(http.StatusNotFound, fmt.Errorf("could not find %s", r.URL.Path), a.newContext(RouteInfo{}, w, r))
   107  			return
   108  		}
   109  
   110  		stat, _ := f.Stat()
   111  		maxAge := envy.Get(AssetsAgeVarName, "31536000")
   112  		w.Header().Add("ETag", fmt.Sprintf("%x", stat.ModTime().UnixNano()))
   113  		w.Header().Add("Cache-Control", fmt.Sprintf("max-age=%s", maxAge))
   114  		fsh.ServeHTTP(w, r)
   115  	})
   116  
   117  	if a.CompressFiles {
   118  		return handlers.CompressHandler(baseHandler)
   119  	}
   120  
   121  	return baseHandler
   122  }
   123  
   124  type newable interface {
   125  	New(Context) error
   126  }
   127  
   128  type editable interface {
   129  	Edit(Context) error
   130  }
   131  
   132  // Resource maps an implementation of the Resource interface
   133  // to the appropriate RESTful mappings. Resource returns the *App
   134  // associated with this group of mappings so you can set middleware, etc...
   135  // on that group, just as if you had used the a.Group functionality.
   136  /*
   137  	a.Resource("/users", &UsersResource{})
   138  
   139  	// Is equal to this:
   140  
   141  	ur := &UsersResource{}
   142  	g := a.Group("/users")
   143  	g.GET("/", ur.List) // GET /users => ur.List
   144  	g.GET("/new", ur.New) // GET /users/new => ur.New
   145  	g.GET("/{user_id}", ur.Show) // GET /users/{user_id} => ur.Show
   146  	g.GET("/{user_id}/edit", ur.Edit) // GET /users/{user_id}/edit => ur.Edit
   147  	g.POST("/", ur.Create) // POST /users => ur.Create
   148  	g.PUT("/{user_id}", ur.Update) PUT /users/{user_id} => ur.Update
   149  	g.DELETE("/{user_id}", ur.Destroy) DELETE /users/{user_id} => ur.Destroy
   150  */
   151  func (a *App) Resource(p string, r Resource) *App {
   152  	g := a.Group(p)
   153  
   154  	if mw, ok := r.(Middler); ok {
   155  		g.Use(mw.Use()...)
   156  	}
   157  
   158  	p = "/"
   159  
   160  	rv := reflect.ValueOf(r)
   161  	if rv.Kind() == reflect.Ptr {
   162  		rv = rv.Elem()
   163  	}
   164  
   165  	rt := rv.Type()
   166  	resourceName := rt.Name()
   167  	handlerName := fmt.Sprintf("%s.%s", rt.PkgPath(), resourceName) + ".%s"
   168  
   169  	n := strings.TrimSuffix(rt.Name(), "Resource")
   170  	paramName := name.New(n).ParamID().String()
   171  
   172  	type paramKeyable interface {
   173  		ParamKey() string
   174  	}
   175  
   176  	if pk, ok := r.(paramKeyable); ok {
   177  		paramName = pk.ParamKey()
   178  	}
   179  
   180  	spath := path.Join(p, "{"+paramName+"}")
   181  
   182  	setFuncKey(r.List, fmt.Sprintf(handlerName, "List"))
   183  	g.GET(p, r.List).ResourceName = resourceName
   184  
   185  	if n, ok := r.(newable); ok {
   186  		setFuncKey(n.New, fmt.Sprintf(handlerName, "New"))
   187  		g.GET(path.Join(p, "new"), n.New).ResourceName = resourceName
   188  	}
   189  
   190  	setFuncKey(r.Show, fmt.Sprintf(handlerName, "Show"))
   191  	g.GET(path.Join(spath), r.Show).ResourceName = resourceName
   192  
   193  	if n, ok := r.(editable); ok {
   194  		setFuncKey(n.Edit, fmt.Sprintf(handlerName, "Edit"))
   195  		g.GET(path.Join(spath, "edit"), n.Edit).ResourceName = resourceName
   196  	}
   197  
   198  	setFuncKey(r.Create, fmt.Sprintf(handlerName, "Create"))
   199  	g.POST(p, r.Create).ResourceName = resourceName
   200  
   201  	setFuncKey(r.Update, fmt.Sprintf(handlerName, "Update"))
   202  	g.PUT(path.Join(spath), r.Update).ResourceName = resourceName
   203  
   204  	setFuncKey(r.Destroy, fmt.Sprintf(handlerName, "Destroy"))
   205  	g.DELETE(path.Join(spath), r.Destroy).ResourceName = resourceName
   206  
   207  	g.Prefix = path.Join(g.Prefix, spath)
   208  
   209  	return g
   210  }
   211  
   212  // ANY accepts a request across any HTTP method for the specified path
   213  // and routes it to the specified Handler.
   214  func (a *App) ANY(p string, h Handler) {
   215  	a.GET(p, h)
   216  	a.POST(p, h)
   217  	a.PUT(p, h)
   218  	a.PATCH(p, h)
   219  	a.HEAD(p, h)
   220  	a.OPTIONS(p, h)
   221  	a.DELETE(p, h)
   222  }
   223  
   224  // Group creates a new `*App` that inherits from it's parent `*App`.
   225  // This is useful for creating groups of end-points that need to share
   226  // common functionality, like middleware.
   227  /*
   228  	g := a.Group("/api/v1")
   229  	g.Use(AuthorizeAPIMiddleware)
   230  	g.GET("/users, APIUsersHandler)
   231  	g.GET("/users/:user_id, APIUserShowHandler)
   232  */
   233  func (a *App) Group(groupPath string) *App {
   234  	g := New(a.Options)
   235  	g.Prefix = path.Join(a.Prefix, groupPath)
   236  	g.Name = g.Prefix
   237  
   238  	g.router = a.router
   239  	g.RouteNamer = a.RouteNamer
   240  	g.Middleware = a.Middleware.clone()
   241  	g.ErrorHandlers = a.ErrorHandlers
   242  	g.root = a
   243  	if a.root != nil {
   244  		g.root = a.root
   245  	}
   246  	a.children = append(a.children, g)
   247  	return g
   248  }
   249  
   250  // RouteHelpers returns a map of BuildPathHelper() for each route available in the app.
   251  func (a *App) RouteHelpers() map[string]RouteHelperFunc {
   252  	rh := map[string]RouteHelperFunc{}
   253  	for _, route := range a.Routes() {
   254  		cRoute := route
   255  		rh[cRoute.PathName] = cRoute.BuildPathHelper()
   256  	}
   257  	return rh
   258  }
   259  
   260  func (a *App) addRoute(method string, url string, h Handler) *RouteInfo {
   261  	a.moot.Lock()
   262  	defer a.moot.Unlock()
   263  
   264  	url = path.Join(a.Prefix, url)
   265  	url = a.normalizePath(url)
   266  	name := a.RouteNamer.NameRoute(url)
   267  
   268  	hs := funcKey(h)
   269  	r := &RouteInfo{
   270  		Method:      method,
   271  		Path:        url,
   272  		HandlerName: hs,
   273  		Handler:     h,
   274  		App:         a,
   275  		Aliases:     []string{},
   276  	}
   277  
   278  	r.MuxRoute = a.router.Handle(url, r).Methods(method)
   279  	r.Name(name)
   280  
   281  	routes := a.Routes()
   282  	routes = append(routes, r)
   283  	sort.Sort(routes)
   284  
   285  	if a.root != nil {
   286  		a.root.routes = routes
   287  	} else {
   288  		a.routes = routes
   289  	}
   290  
   291  	return r
   292  }
   293  
   294  func stripAsset(path string, h http.Handler, a *App) http.Handler {
   295  	if path == "" {
   296  		return h
   297  	}
   298  
   299  	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
   300  		up := r.URL.Path
   301  		up = strings.TrimPrefix(up, path)
   302  		up = strings.TrimSuffix(up, "/")
   303  
   304  		u, err := url.Parse(up)
   305  		if err != nil {
   306  			eh := a.ErrorHandlers.Get(http.StatusBadRequest)
   307  			eh(http.StatusBadRequest, err, a.newContext(RouteInfo{}, w, r))
   308  			return
   309  		}
   310  
   311  		r.URL = u
   312  		h.ServeHTTP(w, r)
   313  	})
   314  }