Skip to content

Instantly share code, notes, and snippets.

@oscarrenalias
Created October 5, 2012 20:20
Show Gist options
  • Select an option

  • Save oscarrenalias/3842138 to your computer and use it in GitHub Desktop.

Select an option

Save oscarrenalias/3842138 to your computer and use it in GitHub Desktop.
This is small implementation of a tiny MVC-oriented framework inspired by the design * of the Play Framework 2.0; it is meant as an educational exercise to get more familiar with * the concepts of partial functions, function composition and generic type
/**
* This is small implementation of a tiny MVC-oriented framework inspired by the design
* of the Play Framework 2.0; it is meant as an educational exercise to get more familiar with
* the concepts of partial functions, function composition and generic types in Scala
*
* How to run:
* scalac framework.scala
* scala App
*/
package object Framework {
/**
* Base class that represents a simplified HTTP request, which can be typed based
* the contents of its body
*/
case class Request[+A](uri: String, body: A)
/**
* Base class that represents an HTTP request; ideally headers (like the content type) should be
* implemented differently but this is enough for now.
* The Result class is not typed because everything are strings.
*/
case class Result(body: String, status: Int = 200, contentType: String = "text/plain")
/**
* Base abstract trait that represents actions in an MVC framework. Implemented as a function that takes
* a request and returns a result
*/
trait Action[A] extends (Request[A] => Result) {
def apply(request: Request[A]): Result
}
/**
* Type alias that will be used for testing
*/
type AnyContent = String
/**
* This object contains static methods that help generate Action classes.
*/
object Action {
/**
* Generates an Action class given a request parameter and a function passed as a closure
*/
def apply[A](code: Request[A] => Result):Action[A] = new Action[A] {
def apply(request: Request[A]) = code(request)
}
/**
* Generates an Action class when no request parameter is required
*/
def apply(code: => Result):Action[AnyContent] = Action[AnyContent](_ => code)
}
/**
* A route links a URI to a specific action. Routes are defined as partial functions, whereby
* a route is defined for a specific URI if the route's URI and the given URI are the same and if so,
* then the action associated to the URI is returned.
*
* As they are defined as partial functions which are only defined for a specifc URI/path, they
* will be composed into a single partial function that is defined for all the inputs of the
* chained functions using 'orElse'
*/
case class Route[A](path: String, action: Action[A]) extends PartialFunction[String, Action[A]] {
/**
* If the given URI and the route's URI match, then the function is defined
*/
def isDefinedAt(uri:String) = path == uri
/**
* Returns the given action; please be aware that isDefinedAt must be checked before calling this
* method to make sure that the route can actually process the URI
*/
def apply(path:String) = action
}
/**
* This abstract trait represents a wired and configured application, which in the current implementation
* is only a partial function representing chained routes and the default action that is triggered if
* no route matches
*
* TODO: routes should be of type Route[A] but instead it's a ParticalFunction - check why
*/
trait FrameworkApplication[A] {
val routes: PartialFunction[String, Action[A]]
/**
* This is a default action that will get executed if none of the configured actions matches
*/
val defaultAction = Action[AnyContent] { request =>
Result("No matching route was found for the following request: " + request, 500)
}
}
/**
* This 'runs' an application, given an Application object as well as a request
*
* Internally it 'lifts' the partial function with the routes so that it returns None if no
* route matches (instead of a match error), so that we can execute the default action as configured
* within the application instead.
*/
def runApp[A <: AnyContent](app:FrameworkApplication[A], request: Request[A]) =
app.routes.lift(request.uri).getOrElse(app.defaultAction)(request)
}
package object Application {
import Framework._
/**
* Our toy controller, seen as a bunch of action/methods
*/
object Controller {
def index = Action {
Result("This is the index page")
}
def login = Action {
Result("This is the login page")
}
def test = Action[AnyContent] { request =>
Result("Request body was: " + request.body)
}
}
/**
* Our application's runner, where we wire the application's routes and return a curried version of the
* runApp method instead so that we can reuse it with different requests for testing purposes. This is not
* strictly necessary but it makes testing easier.
*/
val run = runApp(new FrameworkApplication[AnyContent] {
val routes = Route("/", Controller.index) orElse Route("/login", Controller.login) orElse Route("/test", Controller.test)
}, _:Request[AnyContent])
}
object App {
import Framework._
// Our test requests
val indexRequest = Request("/", "index")
val loginRequest = Request("/login", "login")
val failRequest = Request("/asfasdfsf", "foo")
val testRequest = Request("/test", "this is the request body")
def main (args: Array[String]) = {
println(Application.run(indexRequest)) // works
println(Application.run(loginRequest)) // works
println(Application.run(testRequest)) // works, returns the request body
println(Application.run(failRequest)) // returns the default rule
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment