Serving JSON at Altitude
Colin Putney recently released a preview version of a new Smalltalk web framework, Altitude.
Altitude seeks to be a RESTful Seaside: an HTTP framework that uses RESTful URLs, ubiquitous use of Xtreams, and learning the lessons of years of Seaside development.
Let’s take it for a spin!
The basic architecture of an Altitude application looks like this:
-
ALApplication: The centrepiece of the show, the hub of an application. -
ALResource: something that reacts to an HTTP message. Its subclasses include things that return HTML, files, JSON, and so on. -
ALPath: the path portion of a URL, so not much more than an ordered sequence of string tokens. -
ALLocator: something that uses an ALPath to find an ALResource. -
ALRelay: something that affects a request and its response: a relay might switch on Keep-Alive, or log messages, or switch on chunking, and so on.
I have a need for inspecting an image: I’d like to be able to make use of the extensive reflection capabilities of a Smalltalk image to query a running image about interesting things, and I’d like to be able to consume JSON. Let’s walk through a simple Altitude application to serve this information. First, the hub of the whole affair:
ALApplication subclass: #ImageReflector
instanceVariableNames: ''
classVariableNames: ''
poolDictionaries: ''
category: 'Reflect-Core'.
ALApplication class >> new
^ self withLocator: (ParamLocator new).
ALApplication >> initializeHandler
handler := ALChunkingRelay destination: handler.
handler := ALKeepAliveRelay destination: handler.
handler := ALRequestDecodingRelay destination: handler.
handler := ALLimitingRelay destination: handler.
We don’t know what a
ParamLocator
is yet, but that’s OK.
#initializeHandler
sets up a chain of
ALRelay
s each of which adds capabilities or limitations to the process started by receiving a request, respectively: handle chunked transfer encoding, switch on Keep-Alive, decode the body of the request according to the charset parameter (if present) on the Content-Type header, and ignore any content beyond that defined by the Content-Length header (if present).
ALLocator subclass: #ParamLocator
instanceVariableNames: ''
classVariableNames: ''
poolDictionaries: ''
category: 'Reflect-Core'.
ParamLocator >> keyForResource: aResource
^ aResource selector asString.
ParamLocator >> resourceForPath: aPath
^ aPath first
caseOf: {
['senders-of'] ->
[(ImplementorsOf reflecting: aPath second asSymbol) asResource]}.
otherwise: [ALNotFound signal: aPath printString].
Right. So a
ParamLocator
takes some
ALPath
, deconstructs it a bit, and dispatches to some other things that presumably do something interesting. If we don’t recognise the path structure,
ALNotFound
will ensure that we return a 404 Not Found response.
Now, for our demo application we want to ask an image for all the implementors of a particular message. This is a standard tool in a Smalltalk image, found as a button on any self-respecting
Browser
.
Object subclass: #ImplementorsOf
instanceVariableNames: 'selector'
classVariableNames: ''
poolDictionaries: ''
category: 'Reflect-Core'.
SendersOf class >> reflecting: aSelector
^ self new initializeWithSelector: aSelector.
SendersOf >> initializeWithSelector: aSelector
selector := aSelector.
Nothing special so far. How do we tell the framework that we want to return JSON?
SendersOf >> asResource
^ ALJsonResource endpoint: self
That’s all that’s needed: no setting of MIME types or similar nonsense. OK, so how do we actually generate this JSON?
ImplementorsOf >> renderOn: json
json object: [
json
name: setName
array: [(SystemNavigation default allImplementorsOf: selector) do: [:mref |
json object: [
json name: 'class' value: mref actualClass name.
json name: 'source' value: mref sourceCode asString]]]]
The DSL should look familiar to Seaside developers:
#object:
takes a block that produces a JSON object,
#name:value:
sets a field of an object, and so on.
To start up the application, we need just invoke:
| serv |
serv := ALServer on: 9090 application: (ImageReflector new).
serv start.
Finally, we can fire up a web browser and go to
http://localhost:9090/implementors-of/today
and see:
{"implementors-of": [
{"class":"Date class",
"source":"today\r\r\t^ self current\r"},
{"class":"DateAndTime class",
"source":"today\r\r\t^ self midnight"}]}
Update: Chris Cunnington kindly pointed out several mistakes in the above code. That came from copying snippets of code without their full context: superclasses, and so on. I’ve edited the post to be self-sufficient.
