Step the picoframework: bigger, smaller
Some time ago I wrote about the toy web framework for scala I had coded to get a feel for the language.
I improved the code a bit after that, but as I did not blog about it and nobody knew it Alan Dipert got the ball and run with it and now the code evolved into a proper project on github, including tests, build management and code that does much more in funny ways :)
Kudos to him who also kindly asked me to keep the name ;)
I had managed to do some things (e.g. path parameters) but he managed to improve it even more, and has already shown me a couple of tricks I did not imagine. For one, I had coded both get and post by hand, and he had done the same adding put and delete, but I just noticed the updated code with the following trick that allows them to be defined programmatically:
val List(get, post, put, delete) = protocols map routeSetter
private def routeSetter(protocol: String): (String) => (=> Any) => Unit = {
def g(path: String, fun: => Any) = Routes(protocol) += new Route(path, x => fun.toString)
(g _).curry
} (If I understand correctly this is a change due to paulp's fork. )
As the namespace between functions and values is (mostly) unified in scala the four functions can be defined with a single step. Notice the nifty usage of pattern matching for variable declarations and of currying to have a two-step operation defining the new Route.
I have the feeling the routeSetter function could be shortened by two lines with
private def routeSetter(protocol: String): (String) => (=> Any) => Unit =
(path: String)=>((fun: => Any) => Routes(protocol) += new Route(path, x => fun.toString))but it seems scala does not accept the definition of a by-name value in a function literal (yet?)
scala> val f= (x: =>int) => x
<console>:1: error: identifier expected but '=>' found.
val f= (x: =>int) => xAnyway, if you have some spare time, maybe you should fork this project and work a bit on it, I know I already did :)
See all commentsStep, a scala web picoframework
So, after playing with scala on Google App Engine two days ago, I spent a bit of time yesterday (while waiting for food from girlfriend) and today (while waiting plane to go eat at my parents' easter breakfast, unrelated) writing my hack for a web framework.
I mostly wanted to try scala's cool abstraction features, so I thought I could try to produce something that had more or less the feeling of immediateness of sinatra.
To get ruby-like blocks in scala you only need to define functions with by-name arguments in this way:
1 2 3 |
def foo(x:String)(fun: =>Any)
foo("something") {block returning something to be evaluated later } |
Of course, what was needed at this point was something that allowed me to match the url and the http method. This is the code to allow that, using a simple Map from path+method to anonymous functions
1 2 3 4 5 6 7 8 9 10 11 12 |
abstract class Step extends HttpServlet {
val Map = new HashMap[(String,String),(Any=>String)]()
override def service(request:HttpServletRequest ,response: HttpServletResponse) {
val method=request.getMethod
val path=request.getRequestURI
response.getWriter.println(Map(method,path)());
}
def get(path:String)(fun: =>Any) = Map.put(("GET",path),x=>fun.toString)
def post(path:String)(fun: =>Any) = Map.put(("POST",path),x=>fun.toString)
} |
I believe the types could be simplified, as I'd just need a ()=>Any function not Any=>String, but I could not come up with the proper definition yet :)
Anyway, this can be used in a pretty simple way, and thanks to Scala's xml literal you can have code like this:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
class ScalaHello extends Step {
get("/") {
<form action="/post" method="POST">
<textarea name="key"/>
<input type="submit"/>
</form>
}
post("/post") {
"received a post"
}
def hello() = "hello scala world!"
get("/hello) {
hello
}
get("/number") { 1 }
} |
Now, obviously we have the issue of passing arguments. The by-name trick does not allow us to pass parameters and we cannot inject a new variable into the closure scope (I believe), nor evaluate the block in a different scope a-la ruby's instance_eval.
It is possible in scala to define implicit variables that have dynamic scope, but I could not find example on how that would be possible with byname blocks.
What we can do though, is define a method params to access such variable.
Yet, we are defining all the "get"s inside the same object, so even if we can access the method to access the parameters we just shifted the problem of having that shared object, and what if different threads access it at the same time?
Yet, scala already provides what we need, namely dynamic scope, in the form of the DynamicVariable class, which can be used like
1 2 3 4 |
private val dynamic = new DynamicVariable[SomeType](init)
dynamic.withValue(aValue) {
block where dynamic contains aValue
} |
Having defined the params method properly we have this code working nicely
1 2 3 4 5 6 7 8 9 10 |
class ScalaHello extends Step {
get("/") {
<form action="/post" method="POST">
<textarea name="key"/>
<input type="submit"/>
</form>
}
post("/post") {
"hello, this is your input "+params("key")
} |
I did not implement the pattern matching in the URL as sinatra does, but that should also be simple.
The code is pretty ugly and having started really reading about scala only few days ago it can be probably improved in many ways. The first things that come to my mind
- I am shuffling around parameters to keep the function closure, but probably cleaning up the types should avoid this
- I am using java.util.Map and java arrays, which means even if the code is well typed it can still go wrong
- I have a cast that could probably be avoided
- there is no error handling
- the code still needs to me mapped via the evil web.xml to the / path. It should be possible to avoid this, but I have no clue on how to do it :)
Anyway, as the code is 30 lines of which about 9 are "real" code, I am fairly impressed :)
The full code
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 |
package step
import javax.servlet._;
import javax.servlet.http._;
import scala.util.DynamicVariable
import scala.collection.mutable.HashMap
abstract class Step extends HttpServlet {
type Params = java.util.Map[String,Array[String]]
val Map = new HashMap[(String,String),(Any=>String)]()
val paramsMap = new DynamicVariable[Params](null)
def params(name:String):String = paramsMap.value.get(name)(0)
override def service(request:HttpServletRequest ,response: HttpServletResponse) {
try {
response.setContentType("text/html")
paramsMap.withValue(request.getParameterMap.asInstanceOf[Params]) {
response.getWriter.println(Map(request.getMethod,request.getRequestURI)());
}
}
catch {
case ex:NoSuchElementException => response.getWriter.println("requesting "+request.getMethod+" "+ request.getRequestURI+" but only have "
+Map)
}
}
def get(path:String)(fun: =>Any) = Map.put(("GET",path),x=>fun.toString)
def post(path:String)(fun: =>Any) = Map.put(("POST",path),x=>fun.toString)
} |
Please post suggestions if you want, but remeber that for heavy lifting it would still better to use a lift ;)
PS this code does not work on google app engine as of now, but I am getting this message
Error: Server Error The server encountered an error and could not complete your request. If the problem persists, please report your problem and mention this error message and the query that caused it.
which may mean this is a bug on their side, probably related to some Thread behaviour as I believe DynamicVariable is defined in terms of ThreadLocal. I'll let you know if I sort it out.
UPDATE
actually, it seems related to scala.collections.mutable.HashMap not being loaded, even thought the jar is in lib as the others. Mh..
UPDATE 2 well, it seems I had just a misconfigured path, everything works like a charm on Google App Engine too, yay!
See all comments