Serving JSON at Altitude

By: on July 31, 2012

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 ALRelays 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: [
            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":"todayrrt^ self currentr"},
    {"class":"DateAndTime class",
     "source":"todayrrt^ 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.


Leave a Reply

Your email address will not be published.

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <s> <strike> <strong>