Dragonfly routes | Dragonfly web framework

Default Routes

Dragonfly's routes are written in pure newLISP code and have no arbitrary constraints placed on them. They can be as simple or complex as you need them to be. In fact, Dragonfly's routes are so flexible that the defaults will often be all you'll need.

What is a 'route'?

A route, in an abstract sense, routes a request to its destination.

For example, if a request is made for the following URL:

http://rundragonfly.com/welcome

Then one of Dragonfly's routes (Route.Static) sends the contents of the welcome.html file to your web browser (after evaluating any newLISP code inside of it).

Dragonfly's routes are represented as FOOP objects and use the convention of prepending "Route." to their name to avoid namespace conflicts.

They are stored in the DF:dragonfly-routes list where they are asked, one-by-one, whether they wish to handle the current request. The first route to say "yes" gets run.

This document describes Dragonfly's two default routes: Route.Static and Route.Resource. A third route included with Dragonfly (but not built into the core) is Route.CGI and is explained in the next topic: Creating your own routes.

Templates with Routes.Static

Route.Static offers a flexible method for serving template files to allow for a PHP-like development pattern, but without the PHP.

It handles requests such as the following:

This route is very flexible, and there are only three configuration parameters associated with it in config.lsp:

ENABLE_STATIC_TEMPLATES

Default value: true

Setting this value to nil will remove Route.Static from Dragonfly's list of routes. If you disable this and you're using Dragonfly with the Apache web server, be sure to also comment out this line in .htaccess:

RewriteCond %{REQUEST_FILENAME} \.html$

STATIC_TRIGGER_EXTENSIONS

Default value: '(".html")

If the request has one of these file extensions (including the dot) then the route matches if and only if the requested file exists.

If the request does not end in one of these extensions then the route will attempt to transform it using the STATIC_TRANSFORMATIONS to see if that results in a matching file (see below).

Any GET parameters appended to the URL will not obscure Dragonfly's ability to detect the file extension.

CONFIGURATION
If you modify this list and are using the Apache server, make sure to keep the .htaccess up-to-date with it. See the comment there and in config.lsp for more info.

STATIC_TRANSFORMATIONS

Default value:

'(
    (string DOCUMENT_ROOT "/" _ "/index.html")
    (string VIEWS_PATH "/" _)
    (string VIEWS_PATH "/" _ VIEW_EXTENSION)
)

If the request does not have one of the STATIC_TRIGGER_EXTENSIONS, then Route.Static will attempt to transform it using this list of possible transformations, where the path is bound to the _ symbol.

As an example, say we have a folder called foo in our site's root, and inside of it is an index.html file. If the user visits the following URL they will be shown the contents of foo/index.html:

http://example-site.com/foo

This is because the first transformation resulted in a match:

(string DOCUMENT_ROOT "/" _ "/index.html")
;=> "/home/www/example-site.com/foo/index.html"

Go ahead and give it a try!

The page you're currently on was matched by the third transformation:

(string VIEWS_PATH "/" _ VIEW_EXTENSION)

Note that this technique is very powerful as the transformations can contain arbitrarily complex newLISP expressions, so long as they eventually evaluate to a string.

RESTful routing with Route.Resource

Dragonfly supports the creation of powerful web applications through the use of clean RESTful routes.

Route.Resource handles URLs that refer to RESTful resources, which are represented as FOOP objects deriving from the Resource context. The configuration parameter RESOURCES_PATH specifies the folder containing the resources as .lsp files, one per resource.

The URL scheme works in a similar manner to twitter's RESTful API:

http://mysite.com/resource[/action][/id][.format][?get params..]

resource - maps to a context name in a special way:

  1. Resource. is prepended to the name
  2. Any underscores are removed
  3. The name is written in title case

The resource may only have the letters A-Z (lowercase or uppercase), 0-9, the underscore, and it must begin with a letter. For example:

my_resource => Resource.MyResource

The name also maps to a real file located in RESOURCES_PATH by appending ".lsp" to the name:

my_resource => load file: RESOURCES_PATH/my_resource.lsp

action (optional) - specifies the function to be called on the resource. Like resource, action may only contain letters, numbers, and the underscore. If no action is specified, then the resource's default function is called instead.

If an action is specified but it's not defined in the resource, then the resource's catch-all function is called, which by default displays an error 500 page.
Whichever function gets called, the id and format are passed in as parameters, in that order, except in the case of the catch-all function, in which case the first parameter is the name of the requested action, then followed by the id and format.

id (optional) - may only contain numbers and can be used to specify a specific object out of a collection.

format (optional) - may only contain letters and can be used to specify the response format. (i.e. xml, json, etc.)

Example resource in resources/wings.lsp:

(DF:activate-plugin "artfulcode/json")

(new Resource 'Resource.Wings)
(context 'Resource.Wings)

(set 'my-data
  '((wings (left right))
    (wings-condition ("good" "excellent"))
    (wings-opacity 0.5))
)

(define (Resource.Wings:Resource.Wings id response-format)
    ; defaults to calling show
    (show id response-format)
)

(define (show id response-format)
    ; in this situation we can't use newLISP's default
    ; parameter values to do this for us.
    (if-not id (set 'id 0))

    ; uh-oh! No range checking on 'resource-id' ...
    (if (= response-format "json")
        (begin
            (Response:content-type Response:json-type)
            (print (Json:lisp->json (my-data id)))
        )
        (begin
            (Response:content-type Response:text-type)
            (print (my-data id))
        )
    )
)

(context MAIN)

Try it out:

 
(request will be displayed here...)

Q: Where are the HTTP verbs GET/POST/PUT?

Dragonfly's built-in RESTful route doesn't use them.

There are two reasons for this decision:

  1. newLISP's built-in server doesn't set REQUEST_METHOD (currently)
  2. Their use is redundant and can lead to confusion

To elaborate on point #2, let's take a look at this table (taken from the Ruby on Rails routing guide):

It's not at all clear when a verb should be added to the URL or when an HTTP verb needs to be changed.

The problem is that two RESTful abstractions are carelessly mixed with each other: HTTP verbs, and verbs in the URL.

Whether the RESTful verb in GET /photos/1/edit is "GET" or "edit" is left to philosophical inquiry.

Ironically, the "action" column lists quite clearly which verb is being used:

We can now greatly simplify this table:


URL controller used for
/photos Resource.Photos display a list of all images
/photos/new Resource.Photos return an HTML form for creating a new image
/photos/create Resource.Photos create a new image
/photos/show/1 Resource.Photos display a specific image
/photos/edit/1 Resource.Photos return an HTML form for editing an image
/photos/update/1 Resource.Photos update a specific image
/photos/destroy/1 Resource.Photos delete a specific image

Q: What about nested resources?

Route.Resource is designed to support the creation of clean and efficient APIs (like twitter's). Therefore it does not support nesting because:

1. Nested resources are often unnecessary

You could get comment #5 out of thread #6 in forum #3 like this:

/forums/3/threads/6/comments/5

But that is a convoluted method of going about it. Implementing a generic system to support that is possible, but it would be complex and limiting. It's far simpler, faster, and clearer to do this instead:

/comments/5?forum=3&thread=6

Or by passing those parameters in via POST so that the URL is simply:

/comments/5

Or, if you need it, implement your own specialized route to support such nesting.

2. Can lead to poor design and confusion

This is especially true for implementing some sort of API. Consider the following nested URL (taken from the Rails routing guide):

/magazines/1/ads/1/edit

Ignoring that this request can be rewritten to take a different form (as shown above), consider the situation where both resources magazines and ads take GET parameters and a request such as the following is made:

/magazines/1/ads/1/edit?cat=5

How can you tell whether cat=5 refers to a category of magazines or ads? Of course there's no way to know by looking at the URL, you'd have to check the API. But what if both magazines and ads use cat to refer to their own internal categories? Then you have a real problem.

Also, nested URLs, especially long ones, are a convoluted way of getting what you want. Traversing them results in extra levels of indirection that is ultimately unnecessary and can be inefficient:

Think about it. If you only want to view a specific comment, you shouldn’t have to specify the account, person, and note for the comment in the URL. -- Jamis Buck

Because of these considerations, as well as the complexities of supporting nested resources in a generic fashion, Dragonfly does not encourage this sort of design pattern by supporting it out-of-the-box. However, if you need such behavior, you've got everything you need to create it. :-)

CONTINUE »

Rendered in 1 milliseconds. Used 84 KB of memory, 59 KB for Lisp Cells.