Skip to content

Instantly share code, notes, and snippets.

@diosmosis
Created December 13, 2011 23:32
Show Gist options
  • Save diosmosis/1474468 to your computer and use it in GitHub Desktop.
Save diosmosis/1474468 to your computer and use it in GitHub Desktop.
Scala Experiment Iteration 0
import scala.collection.mutable.HashMap
/** Represents a segment in a router's tree of valid paths. Describes exactly what
* path segments are allowed to come after this one.
*/
class RouteTreeNode(pathSegment:String) {
/** The segment this node represents. */
private val segment = pathSegment
/** The child nodes representing possible paths this segment can lead to. */
private val children:HashMap[String, RouteTreeNode] = new HashMap[String, RouteTreeNode]()
/** The callback associated with this node.
*
* If this member is not null, then the path that leads to this node is a
* valid path.
*/
var callback:(Array[String]) => Unit = null
/** Adds the child to this node's list of children.
*
* @param child The child node to add. If there already is a child node w/ the
* same segment as this node, it is overwritten.
* @return returns the child node for convenience.
*/
def addChild(child:RouteTreeNode):RouteTreeNode = {
children(child.segment) = child
return child
}
/** Returns the child node with the specified segment, or null if there is none.
*
* @param segment The path segment.
* @return The child node associated with segment or null.
*/
def getChild(segment:String):RouteTreeNode = {
return children.getOrElse(segment, null)
}
}
/** Maintains and traverses a tree of "/my/path/to/something"-like paths.
*
* A Router will associate URL paths, like /this/is/a/path, with callbacks and
* attempt to invoke these callbacks when given an arbitrary path.
*
* Router will allow you to use wildcards in place for path segments. For
* example, /this/:will/match will match both "/this/will/match" and
* "/this/abc/match".
*/
class Router {
/** The root node of the Router's route tree. Holds every valid path & the
* callbacks associated with them.
*/
private var routeTree:RouteTreeNode = new RouteTreeNode("")
/** The callback invoked when routing fails. */
private var onError:(Array[String]) => Unit = null
/** Invokes the callback associated with the given path, or the onError
* callback if the path is invalid.
*
* @param path The path to route.
* @return True if the route was successful, false if otherwise.
*/
def route(path: String):Boolean = {
// get the path segments & remove the empty segments
val segments = path.split("/").filter((s) => s.length > 0)
// travel through the route tree, segment by segment
var node = routeTree
for (segment <- segments) {
// look for a child node that matches the segment exactly
var child = node.getChild(segment)
// if there is no child node that matches the segment exactly, try
// looking for a wildcard
if (child == null) {
child = node.getChild("*")
}
// no match? invoke onError
if (child == null) {
if (onError != null) onError(segments)
return false
}
node = child
}
// if the specific node has no callback, it is not a valid path end
if (node.callback == null) {
if (onError != null) onError(segments)
return false
}
node.callback(segments)
return true
}
/** Associates the supplied path with the supplied callback.
*
* @param path The URL path to associate with. Must be of the format: "/a/sample/path".
* Can use wildcards in the format of "/a/path/:wildcard-name".
* @param callback The callback to run when a matched path is routed. This callback
* will be supplied the path segments when invoked.
* @return The Router instance for convenience.
*/
def addRoute(path: String, callback: (Array[String]) => Unit):Router = {
// get the path segments & remove the empty segments
val segments = path.split("/").filter((s) => s.length > 0)
// travel the route tree and add missing nodes as they come up
var node = routeTree
for (segment <- segments) {
var realSegment = segment
// if segment is a wildcard, replace it with the '*' value. right now,
// we don't care about the wildcard's name
if (realSegment.startsWith(":")) {
realSegment = "*"
}
val child = node.getChild(realSegment)
if (child == null) {
node = node.addChild(new RouteTreeNode(realSegment))
} else {
node = child
}
}
node.callback = callback
return this
}
/** Sets the callback that is run when routing fails.
*
* @param callback The callback.
* @return The Router instance for convenience.
*/
def setOnError(callback: (Array[String]) => Unit):Router = {
onError = callback
return this
}
}
/** Testing singleton. */
object Test {
def main(args: Array[String]) = {
val router = new Router()
router.addRoute("/test/route", (path: Array[String]) => println("At /test/route"))
.addRoute("/pages/:name", (path: Array[String]) => println("At /pages/:name w/ name=" + path(1)))
.addRoute("/things/:with/:widgets",
(path: Array[String]) => println("At /things/:with/:widgets w/ with=" + path(1) + " & widgets=" + path(2)))
.setOnError((path: Array[String]) => println("Route failed: /" + path.reduceLeft(_ + "/" + _)))
val routesToTest = List("/test/route", "/pages/my_page", "/things/w/i", "/pages/my-other-page", "/things/w")
routesToTest.foreach((path) => router.route(path))
}
}
Test.main(args)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment