|
- package capstan
-
- import (
- "net/http"
- pth "path"
- "reflect"
- "strings"
-
- "git.destrealm.org/go/vfs"
- "github.com/gorilla/websocket"
- )
-
- var validMethods = map[string]struct{}{
- "GET": struct{}{},
- "HEAD": struct{}{},
- "POST": struct{}{},
- "PUT": struct{}{},
- "PATCH": struct{}{},
- "DELETE": struct{}{},
- "CONNECT": struct{}{},
- "OPTIONS": struct{}{},
- "TRACE": struct{}{},
- }
-
- // Bind the specified endpoint.
- //
- // This method is the core of the router and handles all method routes directly
- // and farms out other route types to separate functions (also located in this
- // file). Bind() will also setup routes and map them.
- //
- // The use of route.Copy() may appear confusing at first blush, but its purpose
- // is to create a copy of (most of) the route and its data, which is then used
- // to bind a method/endpoint tuple with go-chi. route.Copy() only performs a
- // shallow copy; slicens and map pointers are replicate across all dependents.
- func (r *Router) Bind(endpoint Controller) error {
- var hasIndex bool
- var middleware []func(http.Handler) http.Handler
- var rt *Route
- routes := make([]*Route, 0)
- r.dependencies.Apply(endpoint)
- route, props := r.toRoute(endpoint)
-
- if endpoint.path() == "" {
- panic("BaseController.Path must be set for endpoint")
- }
-
- r.cnames[route.Name] = struct{}{}
- r.pathmap[endpoint.path()] = route
-
- // IndexHandler should come first as it gets special treatment. Note: To
- // prevent the likelihood that the GET route might share the same route as
- // this method, we prioritize this one.
- if e, ok := endpoint.(IndexHandler); ok {
- iprops := r.ParseRoute(endpoint.index())
- iprops.mergeBase(props)
-
- hasIndex = true
- rt = route.Copy()
- rt.Method = "GET"
- rt.Suffix = "index"
- rt.Endpoint = e.Index
- rt.Path = iprops.route
- rt.OptionalSlash = iprops.optionalSlash
- rt.MandatoryNoSlash = iprops.mandatoryNoSlash
- rt.MandatorySlash = iprops.mandatorySlash
- rt.SlashIsDefault = iprops.slashIsDefault
- routes = append(routes, rt)
- }
-
- if e, ok := endpoint.(GetHandler); ok {
- // Add the GET handler *only* if the defined route has no Index() method
- // OR if the handler route and controller baseRoute differ. In other
- // words, if the Index() route has a separate path assignment defined by
- // BaseController.Index, we'll still accept both handlers since they can
- // be mounted at separate locations.
- if !hasIndex || props.route != props.baseRoute {
- rt = route.Copy()
- rt.Method = "GET"
- rt.Endpoint = e.Get
- if endpoint.options().Get != "" {
- iprops := r.ParseRoute(endpoint.options().Get)
- rt.Path = pth.Join(rt.Path, iprops.route)
- rt.OptionalSlash = iprops.optionalSlash
- rt.MandatoryNoSlash = iprops.mandatoryNoSlash
- rt.MandatorySlash = iprops.mandatorySlash
- rt.SlashIsDefault = iprops.slashIsDefault
- }
- routes = append(routes, rt)
- }
- }
-
- if e, ok := endpoint.(WebSocketHandler); ok {
- rt = route.Copy()
- rt.Method = "GET"
- rt.Endpoint = e.WebSocket
-
- rt.Upgrader = endpoint.upgrader()
- if rt.Upgrader == nil {
- rt.Upgrader = &websocket.Upgrader{}
- }
-
- if endpoint.webSocketPath() != "" {
- if strings.HasSuffix(rt.Path, "/") {
- rt.Path = rt.Path[:len(rt.Path)-1]
- }
- rt.Path = rt.Path + endpoint.webSocketPath()
- }
-
- if _, ok := endpoint.(GetHandler); ok {
- if endpoint.webSocketPath() == "" {
- panic("controller must set BaseController.WebSocketPath if both Get() and WebSocket() endpoints are present")
- }
- }
-
- routes = append(routes, rt)
- }
-
- if e, ok := endpoint.(ConnectHandler); ok {
- rt = route.Copy()
- rt.Method = "CONNECT"
- rt.Endpoint = e.Connect
- routes = append(routes, rt)
- }
-
- if e, ok := endpoint.(DeleteHandler); ok {
- rt = route.Copy()
- rt.Method = "DELETE"
- rt.Endpoint = e.Delete
- if endpoint.options().Delete != "" {
- iprops := r.ParseRoute(endpoint.options().Delete)
- rt.Path = pth.Join(rt.Path, iprops.route)
- rt.OptionalSlash = iprops.optionalSlash
- rt.MandatoryNoSlash = iprops.mandatoryNoSlash
- rt.MandatorySlash = iprops.mandatorySlash
- rt.SlashIsDefault = iprops.slashIsDefault
- }
- routes = append(routes, rt)
- }
-
- if e, ok := endpoint.(HeadHandler); ok {
- rt = route.Copy()
- rt.Method = "HEAD"
- rt.Endpoint = e.Head
- if endpoint.options().Head != "" {
- iprops := r.ParseRoute(endpoint.options().Head)
- rt.Path = pth.Join(rt.Path, iprops.route)
- rt.OptionalSlash = iprops.optionalSlash
- rt.MandatoryNoSlash = iprops.mandatoryNoSlash
- rt.MandatorySlash = iprops.mandatorySlash
- rt.SlashIsDefault = iprops.slashIsDefault
- }
- routes = append(routes, rt)
- }
-
- if e, ok := endpoint.(OptionsHandler); ok {
- rt = route.Copy()
- rt.Method = "OPTIONS"
- rt.Endpoint = e.Options
- routes = append(routes, rt)
- }
-
- if e, ok := endpoint.(PostHandler); ok {
- rt = route.Copy()
- rt.Method = "POST"
- rt.Endpoint = e.Post
- if endpoint.options().Post != "" {
- iprops := r.ParseRoute(endpoint.options().Post)
- rt.Path = pth.Join(rt.Path, iprops.route)
- rt.OptionalSlash = iprops.optionalSlash
- rt.MandatoryNoSlash = iprops.mandatoryNoSlash
- rt.MandatorySlash = iprops.mandatorySlash
- rt.SlashIsDefault = iprops.slashIsDefault
- }
- routes = append(routes, rt)
- }
-
- if e, ok := endpoint.(PutHandler); ok {
- rt = route.Copy()
- rt.Method = "PUT"
- rt.Endpoint = e.Put
- if endpoint.options().Put != "" {
- iprops := r.ParseRoute(endpoint.options().Put)
- rt.Path = pth.Join(rt.Path, iprops.route)
- rt.OptionalSlash = iprops.optionalSlash
- rt.MandatoryNoSlash = iprops.mandatoryNoSlash
- rt.MandatorySlash = iprops.mandatorySlash
- rt.SlashIsDefault = iprops.slashIsDefault
- }
- routes = append(routes, rt)
- }
-
- if e, ok := endpoint.(PatchHandler); ok {
- rt = route.Copy()
- rt.Method = "PATCH"
- rt.Endpoint = e.Patch
- if endpoint.options().Patch != "" {
- iprops := r.ParseRoute(endpoint.options().Patch)
- rt.Path = pth.Join(rt.Path, iprops.route)
- rt.OptionalSlash = iprops.optionalSlash
- rt.MandatoryNoSlash = iprops.mandatoryNoSlash
- rt.MandatorySlash = iprops.mandatorySlash
- rt.SlashIsDefault = iprops.slashIsDefault
- }
- routes = append(routes, rt)
- }
-
- if e, ok := endpoint.(TraceHandler); ok {
- rt = route.Copy()
- rt.Method = "TRACE"
- rt.Endpoint = e.Trace
- routes = append(routes, rt)
- }
-
- if e, ok := endpoint.(MiddlewareHandler); ok {
- middleware = e.Middleware()
- }
-
- if rts := r.bindSpecial(endpoint, route); rts != nil {
- routes = append(routes, rts...)
- }
-
- // Endpoint instance implements Binder and may handle some routes on its
- // own.
- if e, ok := endpoint.(Binder); ok {
- e.Bind(r)
- }
-
- for _, rt := range routes {
-
- if r.name != "" {
- rt.Prefix = r.name
- }
-
- rt.contextMaker = r.contextMaker
-
- if r.group != nil {
- rt.Headers = r.group.Header()
- if r.group.options != nil {
- rt.ContentType = r.group.options.ContentType
- }
- }
-
- // Assign renderer if none defined for this route.
- if rt.Renderer == nil {
- rt.Renderer = r.renderer
- }
-
- if rt.Renderer != nil {
- rt.Renderer.SetFunc("url", URLForGen(r, false))
- rt.Renderer.SetFunc("external", URLForGen(r, true))
- rt.Renderer.GlobalContext("_route", rt)
- }
-
- // Assign before/after response handlers.
- if e, ok := endpoint.(BaseController); ok {
- rt.BeforeResponse = append(rt.BeforeResponse, e.beforeResponseHandlers()...)
- }
- if e, ok := endpoint.(BaseController); ok {
- rt.AfterResponse = append(rt.AfterResponse, e.afterResponseHandlers()...)
- }
-
- if middleware != nil {
- rt.Middleware = append(rt.Middleware, middleware...)
- }
-
- bindRoute(r, rt)
-
- // Append route to map for possible rebinding.
- r.routes = append(r.routes, rt)
-
- // Map the route after being bound to the muxer. Guarantees route values
- // reflect their final state. (Note: bindRoute() also corrects slash
- // termination based on trailing syntax.)
- r.urls.Map(rt)
- }
-
- return nil
- }
-
- // ManualBind is used with Bind() routes to manually bind a given `endpoint` to
- // the specified `path` with the HTTP request `method` and URLFor `suffix`
- // provided. The controller must also be specified to correctly assemble the
- // route.
- func (r *Router) ManualBind(method, path, suffix string, controller Controller, endpoint Endpoint) {
- // Derive middleware globally or from controller (if implementing the
- // middleware interface)?
- var middleware []func(http.Handler) http.Handler
-
- props := r.ParseRoute(path)
- route := &Route{
- router: r,
- Prefix: controller.prefix(),
- Path: props.route,
- CapPath: pth.Join(controller.path(), path),
- BasePath: r.basePath, // TODO: Support CapBasePath.
- MandatoryNoSlash: props.mandatoryNoSlash,
- MandatorySlash: props.mandatorySlash,
- OptionalSlash: props.optionalSlash,
- SlashIsDefault: props.slashIsDefault,
- ParamTypes: props.params,
- Method: method,
- // FIXME: needs to be []MiddlewareFunc
- //Middleware: controller.middleware(),
- Controller: controller,
- Renderer: controller.renderer(),
- Suffix: suffix,
- Endpoint: endpoint,
- AfterResponse: make([]func(Context, error) error, len(r.afterResponse)),
- BeforeResponse: make([]func(Context, *Route) error, len(r.beforeResponse)),
- }
-
- r.cnames[route.Name] = struct{}{}
- r.pathmap[controller.path()] = route
-
- if r.name != "" {
- route.Prefix = r.name
- }
-
- // Append route to map for possible rebinding.
- r.routes = append(r.routes, route)
-
- if r.group != nil {
- route.Headers = r.group.Header()
- if r.group.options != nil {
- route.ContentType = r.group.options.ContentType
- }
- }
-
- // Assign renderer if none defined for this route.
- if route.Renderer == nil {
- route.Renderer = r.renderer
- }
-
- if route.Renderer != nil {
- route.Renderer.SetFunc("url", URLForGen(r, false))
- route.Renderer.SetFunc("external", URLForGen(r, true))
- route.Renderer.GlobalContext("_route", route)
- }
-
- // Assign before/after response handlers.
- if e, ok := controller.(BaseController); ok {
- route.BeforeResponse = append(route.BeforeResponse, e.beforeResponseHandlers()...)
- }
- if e, ok := controller.(BaseController); ok {
- route.AfterResponse = append(route.AfterResponse, e.afterResponseHandlers()...)
- }
-
- if middleware != nil {
- route.Middleware = append(route.Middleware, middleware...)
- }
-
- bindRoute(r, route)
-
- // Map the route after being bound to the muxer. Guarantees route values
- // reflect their final state. (Note: bindRoute() also corrects slash
- // termination based on trailing syntax.)
- r.urls.Map(route)
- }
-
- // Static binds the path described by `urlroot` to the filesystem `path` as a
- // source for hosting static assets.
- //
- // Being as this makes use of the Golang default http.FileServer implementation,
- // it's worth considering a reverse proxy for better performance.
- func (r *Router) Static(urlroot, path string) {
- r.Mux().Get(urlroot, func(w http.ResponseWriter, r *http.Request) {
- http.StripPrefix(path, http.FileServer(http.Dir(path))).ServeHTTP(w, r)
- })
- }
-
- // Static binds the path described by `urlroot` to the filesystem `path` as
- // contained in the virtual filesystem implementation `fs` as a source for
- // hosting static assets. This should work with any VFS implementation
- // compatible with vfs.FileSystem but is primarily intended to work with
- // Embedder.
- //
- // Being as this makes use of the Golang default http.FileServer implementation,
- // it's worth considering a reverse proxy for better performance.
- func (r *Router) StaticVFS(urlroot, path string, fs vfs.FileSystem) {
- if !strings.HasSuffix(urlroot, "*") {
- if !strings.HasSuffix(urlroot, "/") {
- urlroot += "/"
- }
- urlroot += "*"
- }
-
- if !strings.HasSuffix(path, "/") {
- path += "/"
- }
-
- r.Mux().Get(urlroot, func(w http.ResponseWriter, r *http.Request) {
- http.StripPrefix(path, http.FileServer(vfs.HTTPDir(fs))).ServeHTTP(w, r)
- })
- }
-
- // Bind special endpoints. These are, for example, endpoints that map their own
- // handlers or provide special methods.
- func (r *Router) bindSpecial(endpoint Controller, baseRoute *Route) []*Route {
- routes := make([]*Route, 0)
- basePath := baseRoute.Path
- has := make(map[string]struct{})
-
- if strings.HasSuffix(basePath, "/") {
- basePath = basePath[:len(basePath)-1]
- }
-
- // Check if endpoint handles mapping.
- if e, ok := endpoint.(MapperHandler); ok {
- for path, mapper := range e.Mapper() {
- rt := baseRoute.Copy()
- if !strings.HasPrefix(path, "/") {
- path = "/" + path
- }
- rt.Path = path
- rt.Endpoint = mapper.Endpoint
- rt.Method = strings.ToUpper(mapper.Method)
- has[rt.Method+":"+path] = struct{}{}
- routes = append(routes, rt)
- }
- }
-
- // ...endpoint doesn't, so we'll continue examining it ourselves.
- typ := reflect.TypeOf(endpoint)
-
- // First, handle values configured from the BaseController provided to
- // BindRoute() by the caller (manually configuring routes).
- //
- // FIXME: Reflection will be broken as below, previously, and should be
- // fixed accordingly.
- for path, flag := range endpoint.endpoints() {
- var field reflect.Method
- var ok bool
-
- if field, ok = typ.MethodByName(flag.Endpoint); !ok {
- continue
- }
-
- if _, ok := r.reserved[field.Name]; ok {
- continue
- }
-
- if !field.Type.AssignableTo(reflect.TypeOf(Endpoint(nil))) {
- continue
- }
-
- if !strings.HasPrefix(path, "/") {
- path = "/" + path
- }
-
- // Already mapped.
- if _, ok = has[flag.Method+":"+path]; ok {
- continue
- }
-
- rt := baseRoute.Copy()
- rt.Path = path
- rt.Method = strings.ToUpper(flag.Method)
-
- ev := reflect.ValueOf(rt.Endpoint)
- ev.Set(reflect.ValueOf(field))
-
- has[rt.Method+":"+path] = struct{}{}
-
- routes = append(routes, rt)
- }
-
- // Finally, scan through the custom base paths. These are attached exported
- // methods for the pattern <Method>Name or Name. These routes will be
- // bound to hyphen-case versions of the same; e.g. GetListUsers would be
- // translated to an endpoint of the path "/list-users" accessible via an
- // HTTP GET. Slash optionality can be determined by the suffix "O", for
- // optional trailing slash; "S", for mandatory trailing slash; or "N" for
- // mandatory no-trailing-slash.
- //
- // Underscores are also converted to hyphens in the final interpolated path.
- ev := reflect.ValueOf(endpoint)
- et := ev.Type()
- for i := 0; i < ev.NumMethod(); i++ {
- var method string
- var ok bool
- var path string
- var hasSlash bool
-
- // Copy() doesn't reset defaults and instead inherits these values from
- // the base path. Since we're creating a separate route independent from
- // the base, we'll reset these values to their original state.
- rt := baseRoute.Copy()
- rt.OptionalSlash = false
- rt.MandatoryNoSlash = false
- rt.MandatorySlash = false
- rt.SlashIsDefault = false
-
- field := et.Method(i)
-
- // Already handled by other methods.
- if _, ok := reserved[field.Name]; ok {
- continue
- }
-
- // Manually reserved fields.
- if _, ok := r.reserved[field.Name]; ok {
- continue
- }
-
- var fn func(Context) error
- fv := ev.Method(i)
- fnv := reflect.ValueOf(&fn)
- if fnv.Elem().CanSet() && fv.Type() == fnv.Elem().Type() {
- fnv.Elem().Set(fv)
- } else {
- // Don't bother attempting to infer anything further about this
- // endpoint; it cannot be converted to our type.
- continue
- }
-
- for _, c := range field.Name {
- if method == "" {
- method += string(c)
- continue
- }
- if int32(c) > 122 || int32(c) < 97 {
- break
- }
- method += string(c)
- }
-
- // If no method is provided, we default to GET.
- if _, ok := validMethods[strings.ToUpper(method)]; !ok {
- path = field.Name
- method = "GET"
- } else {
- path = field.Name[len(method):]
- }
-
- switch path[len(path)-1:] {
- case "S":
- path = path[:len(path)-1]
- hasSlash = true
- rt.MandatorySlash = true
- case "O":
- path = path[:len(path)-1]
- hasSlash = true
- rt.OptionalSlash = true
- rt.SlashIsDefault = true
- case "N":
- path = path[:len(path)-1]
- rt.MandatoryNoSlash = true
- default:
- rt.OptionalSlash = true
- }
-
- rt.Path = ""
- for _, c := range path {
- switch {
- case int32(c) >= 97 && int32(c) <= 122:
- rt.Path += strings.ToLower(string(c))
- case c == '_':
- break
- case int32(c) > 47 && int32(c) < 58:
- rt.Path += "-"
- rt.Path += string(c)
- case strings.ToUpper(string(c)) == string(c):
- if rt.Path == "" {
- rt.Path = strings.ToLower(string(c))
- break
- }
- rt.Path += "-"
- rt.Path += strings.ToLower(string(c))
- default:
- rt.Path += strings.ToLower(string(c))
- }
- }
-
- // Already mapped.
- if _, ok = has[rt.Method+":"+path]; ok {
- continue
- }
-
- rt.special = strings.ToLower(method + "-" + rt.Path)
- rt.Path = basePath + "/" + rt.Path
- rt.Method = strings.ToUpper(method)
- rt.Endpoint = Endpoint(fn)
-
- if hasSlash {
- rt.Path += "/"
- }
-
- routes = append(routes, rt)
- }
-
- return routes
- }
|