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.

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