Monday, August 17, 2009

Introducing SHP 0.1: Scheme Hypertext Processor - A Tutorial

SHP v0.1 has just been released, and this is a tutorial for using SHP.

Introduction

SHP is a PHP/JSP-like framework for PLT Scheme web-server. Instead of developing in servlets, you develop in shp scripts that are dynamically loaded and evaluated. The main benefits for developing in shp scripts instead of servlets are:
  • instant refresh of changes - you do not need to reload servlets manually anymore
  • file-based dispatching and URL mapping - you get URL mapping to the underlying script location, which takes out a big chunk of URL mapping work if you like to have "pretty" URLs
As SHP's underlying platform is PLT Scheme and web-server, you get retain all of the benefits of developing in scheme instead of in php as well, including:
  • writing in xexpr and use quasiquotes instead of html snippets and PHP SSIs
  • scope safety - no global or superglobal variables roaming around somewhere, and every script is compiled into its own functions, so all variables will either need to be parameters or explicitly passed as arguments.
  • much safer language than PHP - you'll never have a mysterious variable magically appearing due to typos


License

SHP is released under LGPL.

Prerequisites

SHP depends on PLT Scheme version 4.2.1 or later. Make sure you have it installed.

Installation

SHP is easy to install within PLT Scheme, just open either DrScheme or mzscheme and type the following in REPL:

(require (planet bzlib/shp))

PLT Scheme automatically takes care of downloading the latest version. If you want to download a specific version, just do

(require (planet bzlib/shp:<major>:<minor>))

and substitute <major> & <minor> for the appropriate planet version numbers.

Example Site

SHP comes with an example site - you can browse and take a look at the source code to get a sense of how to develop in SHP. In general, you can just think of developing SHP as developing with PHP (if you have such experiences).

The example is located under the example/ subpath of the installed location of SHP package. If you go to the example directory you will find the following:
  • web.ss - for starting a single servlet web server
  • servlet.ss - the servlet wrapper for SHP
  • test-servlet.ss - an example servlet that is being "wrapped" by SHP
  • shp - the example shp script directory; contains the scripts


You can study all of the source code in the example directory for developing your own sites. The rest of the tutorial goes through the process and introduce the currently available features of SHP.

To start the example site, just do the following:

(require (planet bzlib/shp/example/web))

And point your browser to http://<site>:8001/.

Setting Up a Site

If you have been developing web-server servlets then you already know how to get started. Use the following as your start procedure:

(define start
(make-shp-handler ))


make-shp-handler generates your start procedure for you. You can specify the following parameters:

  • path: the main path of your shp script directory
  • #:default: the path to the "default" script, which by default points to "index.shp" (like index.html or index.php).
  • #:not-found: the path to the "not found" script. Unlike #:default, there is currently only one "not found" script per site (not per directory).
  • #:required: an optional script for holding a list of common required modules
  • #:topfilter: an optional script for handling pre & post processing for all of the requests.


We'll discuss the rest of the parameters shortly.

Developing Your First Script

It is easy to develop a hello world example - the following is your first script, call it index.shp:

;; index.shp
;; the first hello world example:
`(p "Hello world - this is an shp script")

Save the above to your SHP script directory, and refresh your browser.

Quasiquotes and Script Evaluations

Since this is a scheme xexpr - you can use quasiquotes to include snippets of scheme code for dynamic execution. Below we add the current timestamp to our hello world example:

;; index.shp
;; the first hello world example:
`(p "Hello world - this is an shp script and the timestamp is: "
,(number->string (current-seconds)))

Each script gets compiled into one scheme procedure.

Requiring External Modules & #:required

The goals for SHP is to simplify the writing of presentational layers in web-server, not to replace modules, which is probably one of the most advanced module systems available on the market. Hence, the best practice is to do most of your logic and business layers in PLT Scheme modules, and then require the modules from within SHP scripts. The following requires scheme/string into the hello world example so we can use string-join.

;; index.shp
;; the first hello world example:
(require scheme/string)
`(p "Hello world - this is an shp script and the timestamp is: "
,(number->string (current-seconds))
"."
(br)
"The pathinfo are: "
,(string-join '("abc" "def" "ghi") "/"))

All of the required modules are available to all of the scripts. But the evaluation order still applies, so if you call a script that's expecting a module is already available, without first calling the script that loads the module, then you'll get a compilation error. To solve the issue of evaluation order, you can put all of the require statements into a single script, and pass that into make-shp-handler via the #:required parameter.

(make-shp-handler <path> #:required <required-script-path> ...)

The require statement differ from regular PLT Scheme in one regard - it does not handle the extra require options such as only-in, prefix-in, etc. So make sure you only have require statement with module paths.

Handling Not Found

The #:not-found option for make-shp-handler takes in a script that'll get executed when user enters in an invalid URL that does not resolve into any particular script. This option is currently required. Below is an example of a "not found" script:

;; this script gets executed when user enters in an invalid URL
`(p "Sorry we did not find what you are looking for. Please check your URL and try again")
Accessing Environment Variables

The following environment (request, response, cookies, etc) are available for this release:
  • ($uri) - returns the url of the request
  • ($pathinfo) - return the extra path attached to the script.
  • ($header <key>) - return the header value in string, or false if the header is not found
  • ($query <key>) - return the first value (either in uri query or form post) in string, or false if not found
  • ($query* <key>) - return the value as a list - null if no value found. This is useful for retrieve multiple values
  • ($status) or ($status <new-status>) - retrieve or sets the status, which must be one of the following values - 'continue, 'switching-protocols, 'ok, 'created, 'accepted, 'non-authortative-information, 'no-content, 'reset-content, 'partial-content, 'multiple-choice, 'moved-permanently, 'found, 'see-other, 'not-modified, 'use-proxy, 'temporary-redirect, 'bad-request, 'unauthorized, 'payment-required, 'forbidden, 'not-found, 'method-not-allowed, 'not-acceptable, 'proxy-authentication-required, 'request-timeout, 'conflict, 'length-required, 'precondition-failed, 'request-entity-too-large, 'request-uri-too-long, 'unsupported-media-type, 'request-range-not-satisfied, 'expectation-failed, 'internal-server-error, 'not-implemented, 'bad-gateway, 'service-unavailable, 'gateway-timeout, 'version-not-supported
  • (header! <key> <value>) - sets a response header with key & value
  • ($content-type) or ($content-type <new-content-type>) - get or sets the content-type of the response, defaults to text/html
  • ($redirect <new-url> <status>) - redirect to the new url. The status should be one of the following - 'found, 'see-other, or temporary-redirect (default). The headers are automatically passed along with the redirect.
  • ($headers) or ($headers <new-headers>) - this parameter holds the underlying headers. Since we already have ($header <key>) and (header! <key> <val>), direct use of ($headers) is discouraged, except to set it to empty list.
  • (cookie-ref <key>), (cookie-set! <key> <val>), (cookie-del! <key>) - for manipulating cookies
  • ($cookies) - this is the underlying parameter that holds all of the cookies. Similar to ($headers) its direct use is generally discouraged


Including Other Scripts

An important feature in PHP is the ability to include other scripts for execution, and in SHP we can also do the same. To do so, let's include a secondary script called foo.shp in our hello world example:

;; foo.shp
;; returns the value of a + b
(:args a b)
(number->string (+ a b))

As you can see - foo.shp takes in two arguments, created via the :args directive, a and b, add the two values and return the results. Let's call foo.shp from within index.shp:

;; index.shp
;; the first hello world example:
(require scheme/string)
`(p "Hello world - this is an shp script and the timestamp is: "
,(number->string (current-seconds))
"."
(br)
"The pathinfo are: "
,(string-join '("abc" "def" "ghi") "/")
(br)
;; call /foo.shp
,(include! "/foo.shp" 5 10))

include! handles the magic of calling the script (which takes an absolute path as if mapped from the URL). Since each script gets compiled into its own function and has its own scope, there are no shared variables and variables must be either created as parameters (so they are available to all scripts) or passed as parameters in the include! call.

SHP automatically ensures that any scripts that requires arguments (like foo.shp) cannot be called directly by the user, and this provides an added bonus of hiding library scripts that PHP cannot do!

Toplevel Filters for Managing Global Parameters

The best place to hold any global parameterizations is with the #:topfilter option of make-shp-handler, which takes a filter script path.

A filter is a function that takes one parameter, which is a compiled form of the script. The following is a template for a filter:

;; example filter template
(:args inner) ;; inner is the handler
(parameterize ((parameter-a (some-value)) ...)
(inner))

Thus you can use #:toplevel to initialize and control any parameters (let's say a database connection handle) so all of the scripts have access to them. Currently make-shp-handler can only handle one filter, but in the future we can have multiple filters, which will open up the possibilities of using filters!

Interoperating with web-server and servlets

Since the idea of SHP is to be a script language for rapidly developing UIs, it's important that it can operate well within the context of the existing infrastructures. Besides hooking into web-server's servlet system, it currently have a couple of other features for interacting with web-server.

Punting the request back to web-server

If you would like the web-server instead of SHP to handle a particular request - just issue (punt!). This comes in handy if a particular URL maps to a static file, and you want to serve it through web-server.

Example - we want to punt all of the /scripts/ path to web-server, then put the following scripts to /scripts/index.shp:

;; /scripts/index.shp
(punt!)

Now if you have a scripts directory that maps to /scripts/ in your web-server document root, those files will be served instead.

Wrapping Around Servlets

If you already have substantial development done in servlets, you can call it through shp as well, so you do not have to rewrite everything to shp scripts. To do so is also simple, just have a particular script call (servlet <module-path>), like what we have in the example site:

;; servlet.shp
(servlet! "test-servlet.ss")

The <module-path> value can be a path that's resolvable by the underlying dynamic-require. And as long as the result value can be wrapped (i.e. an xexpr), you do not need to do any additional modification.


That's it for now - folks! Would love to hear back on any thoughts, etc. Happy SHP'ing.

2 comments: