Capstan is a Golang web framework that shares some similarities with others in its segment.
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

589 lines
16KB

  1. package capstan
  2. import (
  3. "net/http"
  4. pth "path"
  5. "reflect"
  6. "strings"
  7. "git.destrealm.org/go/vfs"
  8. "github.com/gorilla/websocket"
  9. )
  10. var validMethods = map[string]struct{}{
  11. "GET": struct{}{},
  12. "HEAD": struct{}{},
  13. "POST": struct{}{},
  14. "PUT": struct{}{},
  15. "PATCH": struct{}{},
  16. "DELETE": struct{}{},
  17. "CONNECT": struct{}{},
  18. "OPTIONS": struct{}{},
  19. "TRACE": struct{}{},
  20. }
  21. // Bind the specified endpoint.
  22. //
  23. // This method is the core of the router and handles all method routes directly
  24. // and farms out other route types to separate functions (also located in this
  25. // file). Bind() will also setup routes and map them.
  26. //
  27. // The use of route.Copy() may appear confusing at first blush, but its purpose
  28. // is to create a copy of (most of) the route and its data, which is then used
  29. // to bind a method/endpoint tuple with go-chi. route.Copy() only performs a
  30. // shallow copy; slicens and map pointers are replicate across all dependents.
  31. func (r *Router) Bind(endpoint Controller) error {
  32. var hasIndex bool
  33. var middleware []func(http.Handler) http.Handler
  34. var rt *Route
  35. routes := make([]*Route, 0)
  36. r.dependencies.Apply(endpoint)
  37. route, props := r.toRoute(endpoint)
  38. r.cnames[route.Name] = struct{}{}
  39. r.pathmap[endpoint.path()] = route
  40. // IndexHandler should come first as it gets special treatment. Note: To
  41. // prevent the likelihood that the GET route might share the same route as
  42. // this method, we prioritize this one.
  43. if e, ok := endpoint.(IndexHandler); ok {
  44. iprops := r.ParseRoute(endpoint.index())
  45. iprops.mergeBase(props)
  46. hasIndex = true
  47. rt = route.Copy()
  48. rt.Method = "GET"
  49. rt.Suffix = "index"
  50. rt.Endpoint = e.Index
  51. rt.Path = iprops.route
  52. rt.OptionalSlash = iprops.optionalSlash
  53. rt.MandatoryNoSlash = iprops.mandatoryNoSlash
  54. rt.MandatorySlash = iprops.mandatorySlash
  55. rt.SlashIsDefault = iprops.slashIsDefault
  56. routes = append(routes, rt)
  57. }
  58. if e, ok := endpoint.(GetHandler); ok {
  59. // Add the GET handler *only* if the defined route has no Index() method
  60. // OR if the handler route and controller baseRoute differ. In other
  61. // words, if the Index() route has a separate path assignment defined by
  62. // BaseController.Index, we'll still accept both handlers since they can
  63. // be mounted at separate locations.
  64. if !hasIndex || props.route != props.baseRoute {
  65. rt = route.Copy()
  66. rt.Method = "GET"
  67. rt.Endpoint = e.Get
  68. if endpoint.options().Get != "" {
  69. iprops := r.ParseRoute(endpoint.options().Get)
  70. rt.Path = pth.Join(rt.Path, iprops.route)
  71. rt.OptionalSlash = iprops.optionalSlash
  72. rt.MandatoryNoSlash = iprops.mandatoryNoSlash
  73. rt.MandatorySlash = iprops.mandatorySlash
  74. rt.SlashIsDefault = iprops.slashIsDefault
  75. }
  76. routes = append(routes, rt)
  77. }
  78. }
  79. if e, ok := endpoint.(WebSocketHandler); ok {
  80. rt = route.Copy()
  81. rt.Method = "GET"
  82. rt.Endpoint = e.WebSocket
  83. rt.Upgrader = endpoint.upgrader()
  84. if rt.Upgrader == nil {
  85. rt.Upgrader = &websocket.Upgrader{}
  86. }
  87. if endpoint.webSocketPath() != "" {
  88. if strings.HasSuffix(rt.Path, "/") {
  89. rt.Path = rt.Path[:len(rt.Path)-1]
  90. }
  91. rt.Path = rt.Path + endpoint.webSocketPath()
  92. }
  93. if _, ok := endpoint.(GetHandler); ok {
  94. if endpoint.webSocketPath() == "" {
  95. panic("controller must set BaseController.WebSocketPath if both Get() and WebSocket() endpoints are present")
  96. }
  97. }
  98. routes = append(routes, rt)
  99. }
  100. if e, ok := endpoint.(ConnectHandler); ok {
  101. rt = route.Copy()
  102. rt.Method = "CONNECT"
  103. rt.Endpoint = e.Connect
  104. routes = append(routes, rt)
  105. }
  106. if e, ok := endpoint.(DeleteHandler); ok {
  107. rt = route.Copy()
  108. rt.Method = "DELETE"
  109. rt.Endpoint = e.Delete
  110. if endpoint.options().Delete != "" {
  111. iprops := r.ParseRoute(endpoint.options().Delete)
  112. rt.Path = pth.Join(rt.Path, iprops.route)
  113. rt.OptionalSlash = iprops.optionalSlash
  114. rt.MandatoryNoSlash = iprops.mandatoryNoSlash
  115. rt.MandatorySlash = iprops.mandatorySlash
  116. rt.SlashIsDefault = iprops.slashIsDefault
  117. }
  118. routes = append(routes, rt)
  119. }
  120. if e, ok := endpoint.(HeadHandler); ok {
  121. rt = route.Copy()
  122. rt.Method = "HEAD"
  123. rt.Endpoint = e.Head
  124. if endpoint.options().Head != "" {
  125. iprops := r.ParseRoute(endpoint.options().Head)
  126. rt.Path = pth.Join(rt.Path, iprops.route)
  127. rt.OptionalSlash = iprops.optionalSlash
  128. rt.MandatoryNoSlash = iprops.mandatoryNoSlash
  129. rt.MandatorySlash = iprops.mandatorySlash
  130. rt.SlashIsDefault = iprops.slashIsDefault
  131. }
  132. routes = append(routes, rt)
  133. }
  134. if e, ok := endpoint.(OptionsHandler); ok {
  135. rt = route.Copy()
  136. rt.Method = "OPTIONS"
  137. rt.Endpoint = e.Options
  138. routes = append(routes, rt)
  139. }
  140. if e, ok := endpoint.(PostHandler); ok {
  141. rt = route.Copy()
  142. rt.Method = "POST"
  143. rt.Endpoint = e.Post
  144. if endpoint.options().Post != "" {
  145. iprops := r.ParseRoute(endpoint.options().Post)
  146. rt.Path = pth.Join(rt.Path, iprops.route)
  147. rt.OptionalSlash = iprops.optionalSlash
  148. rt.MandatoryNoSlash = iprops.mandatoryNoSlash
  149. rt.MandatorySlash = iprops.mandatorySlash
  150. rt.SlashIsDefault = iprops.slashIsDefault
  151. }
  152. routes = append(routes, rt)
  153. }
  154. if e, ok := endpoint.(PutHandler); ok {
  155. rt = route.Copy()
  156. rt.Method = "PUT"
  157. rt.Endpoint = e.Put
  158. if endpoint.options().Put != "" {
  159. iprops := r.ParseRoute(endpoint.options().Put)
  160. rt.Path = pth.Join(rt.Path, iprops.route)
  161. rt.OptionalSlash = iprops.optionalSlash
  162. rt.MandatoryNoSlash = iprops.mandatoryNoSlash
  163. rt.MandatorySlash = iprops.mandatorySlash
  164. rt.SlashIsDefault = iprops.slashIsDefault
  165. }
  166. routes = append(routes, rt)
  167. }
  168. if e, ok := endpoint.(PatchHandler); ok {
  169. rt = route.Copy()
  170. rt.Method = "PATCH"
  171. rt.Endpoint = e.Patch
  172. if endpoint.options().Patch != "" {
  173. iprops := r.ParseRoute(endpoint.options().Patch)
  174. rt.Path = pth.Join(rt.Path, iprops.route)
  175. rt.OptionalSlash = iprops.optionalSlash
  176. rt.MandatoryNoSlash = iprops.mandatoryNoSlash
  177. rt.MandatorySlash = iprops.mandatorySlash
  178. rt.SlashIsDefault = iprops.slashIsDefault
  179. }
  180. routes = append(routes, rt)
  181. }
  182. if e, ok := endpoint.(TraceHandler); ok {
  183. rt = route.Copy()
  184. rt.Method = "TRACE"
  185. rt.Endpoint = e.Trace
  186. routes = append(routes, rt)
  187. }
  188. if e, ok := endpoint.(MiddlewareHandler); ok {
  189. middleware = e.Middleware()
  190. }
  191. if rts := r.bindSpecial(endpoint, route); rts != nil {
  192. routes = append(routes, rts...)
  193. }
  194. // Endpoint instance implements Binder and may handle some routes on its
  195. // own.
  196. if e, ok := endpoint.(Binder); ok {
  197. e.Bind(r)
  198. }
  199. for _, rt := range routes {
  200. if r.name != "" {
  201. rt.Prefix = r.name
  202. }
  203. rt.contextMaker = r.contextMaker
  204. if r.group != nil {
  205. rt.Headers = r.group.Header()
  206. if r.group.options != nil {
  207. rt.ContentType = r.group.options.ContentType
  208. }
  209. }
  210. // Assign renderer if none defined for this route.
  211. if rt.Renderer == nil {
  212. rt.Renderer = r.renderer
  213. }
  214. if rt.Renderer != nil {
  215. rt.Renderer.SetFunc("url", URLForGen(r, false))
  216. rt.Renderer.SetFunc("external", URLForGen(r, true))
  217. rt.Renderer.GlobalContext("_route", rt)
  218. }
  219. // Assign before/after response handlers.
  220. if e, ok := endpoint.(BaseController); ok {
  221. rt.BeforeResponse = append(rt.BeforeResponse, e.beforeResponseHandlers()...)
  222. }
  223. if e, ok := endpoint.(BaseController); ok {
  224. rt.AfterResponse = append(rt.AfterResponse, e.afterResponseHandlers()...)
  225. }
  226. if middleware != nil {
  227. rt.Middleware = append(rt.Middleware, middleware...)
  228. }
  229. bindRoute(r, rt)
  230. // Append route to map for possible rebinding.
  231. r.routes = append(r.routes, rt)
  232. // Map the route after being bound to the muxer. Guarantees route values
  233. // reflect their final state. (Note: bindRoute() also corrects slash
  234. // termination based on trailing syntax.)
  235. r.urls.Map(rt)
  236. }
  237. return nil
  238. }
  239. // ManualBind is used with Bind() routes to manually bind a given `endpoint` to
  240. // the specified `path` with the HTTP request `method` and URLFor `suffix`
  241. // provided. The controller must also be specified to correctly assemble the
  242. // route.
  243. func (r *Router) ManualBind(method, path, suffix string, controller Controller, endpoint Endpoint) {
  244. // Derive middleware globally or from controller (if implementing the
  245. // middleware interface)?
  246. var middleware []func(http.Handler) http.Handler
  247. props := r.ParseRoute(path)
  248. route := &Route{
  249. router: r,
  250. Prefix: controller.prefix(),
  251. Path: props.route,
  252. CapPath: pth.Join(controller.path(), path),
  253. BasePath: r.basePath, // TODO: Support CapBasePath.
  254. MandatoryNoSlash: props.mandatoryNoSlash,
  255. MandatorySlash: props.mandatorySlash,
  256. OptionalSlash: props.optionalSlash,
  257. SlashIsDefault: props.slashIsDefault,
  258. ParamTypes: props.params,
  259. Method: method,
  260. // FIXME: needs to be []MiddlewareFunc
  261. //Middleware: controller.middleware(),
  262. Controller: controller,
  263. Renderer: controller.renderer(),
  264. Suffix: suffix,
  265. Endpoint: endpoint,
  266. AfterResponse: make([]func(Context, error) error, len(r.afterResponse)),
  267. BeforeResponse: make([]func(Context, *Route) error, len(r.beforeResponse)),
  268. }
  269. r.cnames[route.Name] = struct{}{}
  270. r.pathmap[controller.path()] = route
  271. if r.name != "" {
  272. route.Prefix = r.name
  273. }
  274. // Append route to map for possible rebinding.
  275. r.routes = append(r.routes, route)
  276. if r.group != nil {
  277. route.Headers = r.group.Header()
  278. if r.group.options != nil {
  279. route.ContentType = r.group.options.ContentType
  280. }
  281. }
  282. // Assign renderer if none defined for this route.
  283. if route.Renderer == nil {
  284. route.Renderer = r.renderer
  285. }
  286. if route.Renderer != nil {
  287. route.Renderer.SetFunc("url", URLForGen(r, false))
  288. route.Renderer.SetFunc("external", URLForGen(r, true))
  289. route.Renderer.GlobalContext("_route", route)
  290. }
  291. // Assign before/after response handlers.
  292. if e, ok := controller.(BaseController); ok {
  293. route.BeforeResponse = append(route.BeforeResponse, e.beforeResponseHandlers()...)
  294. }
  295. if e, ok := controller.(BaseController); ok {
  296. route.AfterResponse = append(route.AfterResponse, e.afterResponseHandlers()...)
  297. }
  298. if middleware != nil {
  299. route.Middleware = append(route.Middleware, middleware...)
  300. }
  301. bindRoute(r, route)
  302. // Map the route after being bound to the muxer. Guarantees route values
  303. // reflect their final state. (Note: bindRoute() also corrects slash
  304. // termination based on trailing syntax.)
  305. r.urls.Map(route)
  306. }
  307. // Static binds the path described by `urlroot` to the filesystem `path` as a
  308. // source for hosting static assets.
  309. //
  310. // Being as this makes use of the Golang default http.FileServer implementation,
  311. // it's worth considering a reverse proxy for better performance.
  312. func (r *Router) Static(urlroot, path string) {
  313. r.Mux().Get(urlroot, func(w http.ResponseWriter, r *http.Request) {
  314. http.StripPrefix(path, http.FileServer(http.Dir(path))).ServeHTTP(w, r)
  315. })
  316. }
  317. // Static binds the path described by `urlroot` to the filesystem `path` as
  318. // contained in the virtual filesystem implementation `fs` as a source for
  319. // hosting static assets. This should work with any VFS implementation
  320. // compatible with vfs.FileSystem but is primarily intended to work with
  321. // Embedder.
  322. //
  323. // Being as this makes use of the Golang default http.FileServer implementation,
  324. // it's worth considering a reverse proxy for better performance.
  325. func (r *Router) StaticVFS(urlroot, path string, fs vfs.FileSystem) {
  326. if !strings.HasSuffix(urlroot, "*") {
  327. if !strings.HasSuffix(urlroot, "/") {
  328. urlroot += "/"
  329. }
  330. urlroot += "*"
  331. }
  332. if !strings.HasSuffix(path, "/") {
  333. path += "/"
  334. }
  335. r.Mux().Get(urlroot, func(w http.ResponseWriter, r *http.Request) {
  336. http.StripPrefix(path, http.FileServer(vfs.HTTPDir(fs))).ServeHTTP(w, r)
  337. })
  338. }
  339. // Bind special endpoints. These are, for example, endpoints that map their own
  340. // handlers or provide special methods.
  341. func (r *Router) bindSpecial(endpoint Controller, baseRoute *Route) []*Route {
  342. routes := make([]*Route, 0)
  343. basePath := baseRoute.Path
  344. has := make(map[string]struct{})
  345. if strings.HasSuffix(basePath, "/") {
  346. basePath = basePath[:len(basePath)-1]
  347. }
  348. // Check if endpoint handles mapping.
  349. if e, ok := endpoint.(MapperHandler); ok {
  350. for path, mapper := range e.Mapper() {
  351. rt := baseRoute.Copy()
  352. if !strings.HasPrefix(path, "/") {
  353. path = "/" + path
  354. }
  355. rt.Path = path
  356. rt.Endpoint = mapper.Endpoint
  357. rt.Method = strings.ToUpper(mapper.Method)
  358. has[rt.Method+":"+path] = struct{}{}
  359. routes = append(routes, rt)
  360. }
  361. }
  362. // ...endpoint doesn't, so we'll continue examining it ourselves.
  363. typ := reflect.TypeOf(endpoint)
  364. // First, handle values configured from the BaseController provided to
  365. // BindRoute() by the caller (manually configuring routes).
  366. //
  367. // FIXME: Reflection will be broken as below, previously, and should be
  368. // fixed accordingly.
  369. for path, flag := range endpoint.endpoints() {
  370. var field reflect.Method
  371. var ok bool
  372. if field, ok = typ.MethodByName(flag.Endpoint); !ok {
  373. continue
  374. }
  375. if _, ok := r.reserved[field.Name]; ok {
  376. continue
  377. }
  378. if !field.Type.AssignableTo(reflect.TypeOf(Endpoint(nil))) {
  379. continue
  380. }
  381. if !strings.HasPrefix(path, "/") {
  382. path = "/" + path
  383. }
  384. // Already mapped.
  385. if _, ok = has[flag.Method+":"+path]; ok {
  386. continue
  387. }
  388. rt := baseRoute.Copy()
  389. rt.Path = path
  390. rt.Method = strings.ToUpper(flag.Method)
  391. ev := reflect.ValueOf(rt.Endpoint)
  392. ev.Set(reflect.ValueOf(field))
  393. has[rt.Method+":"+path] = struct{}{}
  394. routes = append(routes, rt)
  395. }
  396. // Finally, scan through the custom base paths. These are attached exported
  397. // methods for the pattern <Method>Name or Name. These routes will be
  398. // bound to hyphen-case versions of the same; e.g. GetListUsers would be
  399. // translated to an endpoint of the path "/list-users" accessible via an
  400. // HTTP GET. Slash optionality can be determined by the suffix "O", for
  401. // optional trailing slash; "S", for mandatory trailing slash; or "N" for
  402. // mandatory no-trailing-slash.
  403. //
  404. // Underscores are also converted to hyphens in the final interpolated path.
  405. ev := reflect.ValueOf(endpoint)
  406. et := ev.Type()
  407. for i := 0; i < ev.NumMethod(); i++ {
  408. var method string
  409. var ok bool
  410. var path string
  411. var hasSlash bool
  412. // Copy() doesn't reset defaults and instead inherits these values from
  413. // the base path. Since we're creating a separate route independent from
  414. // the base, we'll reset these values to their original state.
  415. rt := baseRoute.Copy()
  416. rt.OptionalSlash = false
  417. rt.MandatoryNoSlash = false
  418. rt.MandatorySlash = false
  419. rt.SlashIsDefault = false
  420. field := et.Method(i)
  421. // Already handled by other methods.
  422. if _, ok := reserved[field.Name]; ok {
  423. continue
  424. }
  425. // Manually reserved fields.
  426. if _, ok := r.reserved[field.Name]; ok {
  427. continue
  428. }
  429. var fn func(Context) error
  430. fv := ev.Method(i)
  431. fnv := reflect.ValueOf(&fn)
  432. if fnv.Elem().CanSet() && fv.Type() == fnv.Elem().Type() {
  433. fnv.Elem().Set(fv)
  434. } else {
  435. // Don't bother attempting to infer anything further about this
  436. // endpoint; it cannot be converted to our type.
  437. continue
  438. }
  439. for _, c := range field.Name {
  440. if method == "" {
  441. method += string(c)
  442. continue
  443. }
  444. if int32(c) > 122 || int32(c) < 97 {
  445. break
  446. }
  447. method += string(c)
  448. }
  449. // If no method is provided, we default to GET.
  450. if _, ok := validMethods[strings.ToUpper(method)]; !ok {
  451. path = field.Name
  452. method = "GET"
  453. } else {
  454. path = field.Name[len(method):]
  455. }
  456. switch path[len(path)-1:] {
  457. case "S":
  458. path = path[:len(path)-1]
  459. hasSlash = true
  460. rt.MandatorySlash = true
  461. case "O":
  462. path = path[:len(path)-1]
  463. hasSlash = true
  464. rt.OptionalSlash = true
  465. rt.SlashIsDefault = true
  466. case "N":
  467. path = path[:len(path)-1]
  468. rt.MandatoryNoSlash = true
  469. default:
  470. rt.OptionalSlash = true
  471. }
  472. rt.Path = ""
  473. for _, c := range path {
  474. switch {
  475. case int32(c) >= 97 && int32(c) <= 122:
  476. rt.Path += strings.ToLower(string(c))
  477. case c == '_':
  478. break
  479. case int32(c) > 47 && int32(c) < 58:
  480. rt.Path += "-"
  481. rt.Path += string(c)
  482. case strings.ToUpper(string(c)) == string(c):
  483. if rt.Path == "" {
  484. rt.Path = strings.ToLower(string(c))
  485. break
  486. }
  487. rt.Path += "-"
  488. rt.Path += strings.ToLower(string(c))
  489. default:
  490. rt.Path += strings.ToLower(string(c))
  491. }
  492. }
  493. // Already mapped.
  494. if _, ok = has[rt.Method+":"+path]; ok {
  495. continue
  496. }
  497. rt.special = strings.ToLower(method + "-" + rt.Path)
  498. rt.Path = basePath + "/" + rt.Path
  499. rt.Method = strings.ToUpper(method)
  500. rt.Endpoint = Endpoint(fn)
  501. if hasSlash {
  502. rt.Path += "/"
  503. }
  504. routes = append(routes, rt)
  505. }
  506. return routes
  507. }