I ended up just using Text.Regex to parse the Symfony routing.yml bullshit. Basically, I’ve got a bit of code which reads in some routing Yaml like
index:
url: /index
param: { module: index }
tag_list:
url: /tag
param: { module: tag, action: list }
tag_view:
url: /tag/:id
param: { module: tag, action: view }
Then as a build step parse and transform it into a couple tables which get statically linked into the application –
module Site.RoutingTable where
import qualified Site.Index
import qualified Site.Tag
import Data.Map (fromAsc)
import Routing (Route (..))
actionTable = fromAscList [
(("Index", "index", Site.Index.index),
(("Tag", "list"), Site.Tag.list),
(("Tag", "view"), Site.Tag.view)]
urlTree = Branch ([Branch ([Literal ("tag",RouteEnd (Just [("module","tag"),
("action","list")]))],Just (Parameter ("id",RouteEnd (Just [("module","tag"),
("action","view")])))),Literal ("index",RouteEnd (Just [("module","index")]))],Nothing)
Basically, the urlTree is a decision tree which contains all the possible paths through the tree –
- index - end
| tag - end
| :id - end
When a request needs to be routed, the URL is split with the '/' delimiter into a list of strings. The pop is popped off the front of that list at each level of the tree until it hits an RouteEnd element or a Branch element with no corresponding Literal and no Parameter or Wildcard part (in the later case, the route fails and a HTTP 404 response should be returned).
Parameter and Wildcard nodes are only taken when there isn’t a competing Literal node. When a Parameter node gets taken, the value it corresponds to gets pushed onto an associative list for processing later on.
When the routing process hits the end, the RouteEnd node contains the parameters specified in the Yaml. Those parameters are merged with the parameters generated by the Parameter nodes in the tree traversal (but will not overwrite — the parameters in the Yaml simply serve as defaults).
Thus we can route a Url -> Parameters where Url :: String and Parameters :: [(String, String)].
Next, we need to resolve what code to execute. Contained within the parameters be a "module" and a "action" parameter — these are resolved in the table to a function pointer within the application’s namespace. The application functions are all existentially quantified as
data Action = forall a. IConnection a => MkAction (RouteParameters -> a -> String)
– they get passed both a HDBC database reference and the URL parameters and return a string which gets output to the browser.
A good use of a weekend, I guess. [source]
1 comment