Skip to content

Instantly share code, notes, and snippets.

@diosmosis
Created December 29, 2011 02:42
Show Gist options
  • Save diosmosis/1531357 to your computer and use it in GitHub Desktop.
Save diosmosis/1531357 to your computer and use it in GitHub Desktop.
Scala Experiment Iteration 1
import scala.actors.Actor
import scala.actors.Actor._
import scala.collection.immutable.HashMap
import scala.collection.mutable.{HashMap => MutableHashMap}
/**
* The type passed to RouterActionInvokers.
*
* The 'message' member can be anything.
*/
case class InvokerInfo(callbackName: String, path: Array[String], message: Object)
/**
* Actor used to invoke route actions. When path is successfully routed, its
* callback is executed in an Actor.
*/
class RouterActionInvoker(id: String, callbacks: Map[String, (Array[String]) => Unit]) extends Actor {
/**
* Prints out some metadata & calls the necessary callback.
*/
def act() = {
while (true) {
receive {
case InvokerInfo(callbackName, path, message) =>
println()
println("Handling '" + callbackName + "' in Invoker(ID = " + id + ")")
println(" Data passed: " + message.toString())
callbacks(callbackName)(path)
}
}
}
}
/** 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 unique name of 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 callbackName:String = null
/** The child nodes representing possible paths this segment can lead to. */
private val children:MutableHashMap[String, RouteTreeNode] = new MutableHashMap()
/** 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)
}
}
/** The default actor picking algorithm used by Router.
*
* Cycles through the list of actors so each new route callback is handled by
* the next actor.
*/
class ActorCarousel(actorCount:Int) extends Function1[String, Int] {
private var currentActor = 0
def apply(callbackName:String):Int = {
val result = currentActor
currentActor = (currentActor + 1) % actorCount
return result
}
}
/** 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".
*
* The initialize method must be called before any routing is done and after
* all routes are added.
*/
class Router(actorCount:Int) {
/** 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
/** Maps every callback with the string route its associated with. */
private val allCallbacks:MutableHashMap[String, (Array[String]) => Unit] = new MutableHashMap()
/** The array of actors used to invoke callbacks. */
private var actors:Array[Actor] = null
/** The function used to decide which actor to use when handling a route.
* Takes the route path as an argument.
*/
private var actorPicker:(String) => Int = new ActorCarousel(actorCount)
/** Method to call to finish two-phase construction. Must be called before
* doing any routing.
*/
def initialize() = {
actors = new Array[Actor](actorCount)
for (i <- 0 until actorCount) {
val actor = new RouterActionInvoker(i.toString(), allCallbacks.toMap)
actor.start()
actors(i) = actor
}
}
/** 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, data: Object):Boolean = {
if (actors == null || (actors contains null)) {
throw new IllegalStateException("Router must be initialized first!");
}
// 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)
// no match? 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.callbackName == null) {
if (onError != null) onError(segments)
return false
}
// pick the actor & send it a message
val actor = actors(actorPicker(node.callbackName))
actor ! new InvokerInfo(node.callbackName, segments, data)
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.callbackName = path
allCallbacks(path) = 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
}
/** Sets the function used to decide which actor to use when invoking a
* route action.
*/
def setActorPicker(picker: (String) => Int):Router = {
if (picker == null) {
throw new IllegalArgumentException("picker cannot be null")
}
actorPicker = picker
return this
}
}
/** Testing singleton. */
object Test {
def main(args: Array[String]) = {
val router = new Router(3)
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(_ + "/" + _)))
router.initialize()
val routesToTest = List(Array("/test/route", "no-data1"),
Array("/pages/my_page", "no-data2"),
Array("/things/w/i", "no-data3"),
Array("/pages/my-other-page", "no-data4"),
Array("/things/w", "no-data5"))
routesToTest.foreach((pair) => router.route(pair(0), pair(1)))
}
}
Test.main(args)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment