Intallation
Getting started with GINSENG
start and stop an HTTP server
Hello World
Handling function can take arguments.
Dynamic URL
Call back functions
with-call-back
bindf
the first argument of with-call-back is optional.
the "checkbox" problem.
We can define the type of the first argument of with-call-back.
An example of adding two numbers.
TO BE CONTINUE.

Intallation

GINSENG depends following libraries.

I assume that you already install hunchentoot and run it's demos. It is not easy. I suggest to use asdf-install to install everything.

I use SBCL on Linux environment and CCL on Win32 environment.

You can download GINSENG (righ now, only CVS checkout is available) and create a symbolic link to ginseng.asd at your ASDF register. For example.

cvs -z3 -d ext:anonymous@common-lisp.net:/project/ginseng/cvsroot checkout ginseng
ln -s /<path>/<to>/<ginseng>/ginseng.asd ~/.asdf/systems/

then start SLIME, then load "GINSENG".

TO DO: To my understanding, it is not so easy for new comers to setup a development environment for web applications. This introduction is not informative enough for novices and redundant for experienced users. It is necessary to have a dedicate page to introduce the installation.

Getting started with GINSENG

I guess everyone likes getting started with a "hello world" example, so do I. And everyone loves videos rather than texts, so I create a short video for it.

start and stop an HTTP server

(asdf:oos 'asdf:load-op 'ginseng)  ;; load GINSENG
(ginseng:start-hunchentoot) ;; optional, not necessary if you already
                            ;; start a hunchentoot server instance.
(ginseng:start-ginseng)

;; stop
(ginseng:stop-ginseng)
(ginseng:start-hunchentoot)

Only one server instance is supported, it is not so difficult to make it support many.

Hello World

hellow world 1

source code in following examples is in ./examples/examples.lisp

(defpackage :ginseng-examples
  (:use :cl :yaclml :ginseng))
(in-package :ginseng-examples)
(defun http-hello-world-1()
  "
<html>
<head>
<title> Hello World </title>
</head>
<body>
<h1> Hello World </h1>
</body>
</html>
")

Then you can visit URL "http://localhost:8080/cgi-bin/ginseng-examples/hello-world-1" to view the page.

hellow world 2

I prefer to YACLML version.

(defun http-hello-world-2(&rest args)
  (declare (ignore args))
  (with-yaclml-output-to-string
    (<:html
     (<:head
      (<:title "Hello World"))
     (<:body
      (<:h1 "Hello World")))))

"http://localhost:8080/cgi-bin/ginseng-examples/hello-world-2" to view this page.

hellow world 3

the macro ginseng:standard-page can save typing, it is defined in ./src/ginseng.lisp

(defmacro standard-page ((&key title) &body body)
  `(with-yaclml-output-to-string
     (<:html :prologue "<!DOCTYPE html
PUBLIC \"-//W3C//DTD XHTML 1.0 Transitional//EN\"
\"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd\">"
             (<:head
              ,(awhen title
                      (<:title (<:as-html it)))
              (<:meta :http-equiv "Content-Type"
                      :content    "text/html;charset=utf-8")
              (<:link :type "text/css"
                       :rel "stylesheet"
                       :href "reset.css")
              (<:link :type "text/css"
                       :rel "stylesheet"
                       :href "style.css"))
             (<:body
              ,@body))))

then, the third version of hello-world-3 is as follows.

(defun http-hello-world-3()
  (standard-page
      (:title "Hello World")
    (<:h1 "Hello World")))

"http://localhost:8080/cgi-bin/ginseng-examples/hello-world-3" to view the page.

Handling function can take arguments.

For example

(defun http-sum-of(&rest args)
  (standard-page ()
    (let ((a-list-of-number (mapcar #'(lambda (x)
                                        (or (parse-integer x :junk-allowed t) 0))
                                    args)))
      (<:p
       (<:as-html (format nil "~{~A~^+~}" a-list-of-number) "="
                  (apply #'+ a-list-of-number))))))

if you go to "http://localhost:8080/cgi-bin/ginseng-examples/sum-of/1/2/3", it shows the page as below.

we can see that the "1", "2" and "3" in the URL request are regarded as the function's arguments.

Dynamic URL

For example

(defun http-counter(&optional (next-action nil))
  (invoke-next-action next-action :main #'(lambda () (counter-main 0))))
(defun counter-main (counter)
  (standard-page  ()
    (<:p (<:as-html counter))
    (<:p(<:a :href (dynamic-url (counter-main (1+ counter))) "++")
        (<:as-html "  ")
        (<:a :href (dynamic-url (counter-main (1- counter))) "--"))))

if you go to "http://localhost:8080/cgi-bin/ginseng-examples/counter", it shows the page as below.

INVOKE-NEXT-ACTION and DYNAMIC-URL are important.

  1. Initially, we requests "http://localhost:8080/cgi-bin/GINSENG-EXAMPLES/counter"
  2. COUNTER-MAIN is invoked with argument COUNTER binding to 0
  3. If we click "++", the browser requests "http://localhost:8080/cgi-bin/GINSENG-EXAMPLES/counter/4329973BC1559D54C5CCC74C9C20200D".
  4. HTTP-COUNTER is invoked with NEXT-ACTION binds to "4329973BC1559D54C5CCC74C9C20200D"
  5. INVOKE-NEXT-ACTION must find the function, with body "(counter-main (1+ counter))"
  6. go to step 2, with COUNTER increased by one.

    If we press "SHIFT" and click "++", we open a new window with the counter increased by one. In this way, we also FORK a window. It is very similar to UNIX system call "fork". We can go back to the previous window and continue to increase or decrease the counter. The two windows do not interfere with each other. Some other web applications which are implemented with the full continuation transformation also have similar behaviour. We do not need worry about a browser's "go back" and "go forward".

Call back functions

with-call-back

An example is as below.

greet-1

(defun http-greet-1 (&optional next-action)
  (invoke-next-action next-action :main #'greet-1))
(defun greet-1 ()
  (let (name)
    (standard-page ()
      (<:form
       :action (dynamic-url (how-are-you name))
       (<:p "What's your name?"
            (<:input :type :input
                     :name (with-call-back (var)
                             (setf name var))))
       (<:p (<:input :type :submit
                     :name "OK"
                     :value "OK"))))))
(defun how-are-you (name)
  (standard-page ()
    (<:p "How are you, " (<:as-html name) "?")
    (<:a :href "." "try again")))

As the matter of style, the below example has better style. It is very helpful, especially when your page becomes more complicate.

greet-2

(defun greet-2 ()
  (let* (name
         (form-id (dynamic-url (how-are-you name)))
         (name-id (with-call-back (var) (setf name var))))
    (standard-page ()
      (<:form
       :action form-id
       (<:p "What's your name?"
            (<:input :type :input
                     :name name-id))
       (<:p (<:input :type :submit
                     :name "OK"
                     :value "OK"))))))

It is better because we separate the data model and view model. In the let*, we only care about what data are exchange between client and server. In the standard-page, we only care about html staff and how to render a page.

bindf

bindf is a very simple macro built on top of with-call-back, it makes source code shorter.

(defun http-greet-3 (&optional next-action)
  (invoke-next-action next-action :main #'greet-1))
(defun greet-3 ()
  (let* (name
         (form-id (dynamic-url (how-are-you name)))
         (name-id (bindf name)))
    (standard-page ()
      (<:form
       :action form-id
       (<:p "What's your name?"
            (<:input :type :input
                     :name name-id))
       (<:p (<:input :type :submit
                     :name "OK"
                     :value "OK"))))))

We only change (with-call-back (var) (setf name var)) to (bindf name). bindf is very handy because it is very common that we only need to bind a query string to a varible, a hashtable, a slot value of an object etc.

Because (bindf <place>) uses setf, we can share benefits brought by setf.

the first argument of with-call-back is optional.

(defun http-greet-4 (&optional next-action)
  (invoke-next-action next-action :main #'greet-4))
(defun greet-4 ()
  (let* (name
         (form-id (dynamic-url (how-are-you name)))
         (bob-id (with-call-back () (setf name "Bob")))
         (john-id (with-call-back () (setf name "John")))
         (tom-id (with-call-back () (setf name "Tom"))))
    (standard-page ()
      (<:form
       :action form-id
       (<:p "What's your name?")
       (<:p (<:input :type :submit :name bob-id :value "Bob")
            (<:input :type :submit :name john-id :value "John")
            (<:input :type :submit :name tom-id :value "Tom"))))))

A call back function created by with-call-back is invoked only if HTTP request contains the corresponding qeury string.

the "checkbox" problem.

The below example won't work as we expected, if the unchecked names are shown as "default-name".

(defun http-greet-5 (&optional next-action)
  (invoke-next-action next-action :main #'greet-5))
(defun greet-5 ()
  (let* ((names   (make-list 3 :initial-element "default-name"))
         (form-id (dynamic-url (how-are-you (format nil "~{~A~^,~}" (remove nil names)))))
         (bob-id  (bindf (nth 0 names)))
         (john-id (bindf (nth 1 names)))
         (tom-id  (bindf (nth 2 names)))
         )
    (standard-page ()
      (<:form
       :action form-id
       (<:p "What's your name?")
       (<:p (<:input :type :checkbox :name bob-id :value "Bob") "Bob")
       (<:p (<:input :type :checkbox :name john-id :value "John") "John")
       (<:p (<:input :type :checkbox :name tom-id :value "Tom")  "Tom")
       (<:p (<:input :type :submit
                     :name "OK"
                     :value "OK"))
       ))))

Because if a checkbox is unchecked, a browse won't regard it as a successful data. The HTTP request has not corresponding query string, then bindf is not evaluated.

It is can be fixed as below.

(defun http-greet-6 (&optional next-action)
  (invoke-next-action next-action :main #'greet-6))
(defun greet-6 ()
  (let* ((names   (make-list 3 :initial-element "default-name"))
         (form-id (dynamic-url (how-are-you (format nil "~{~A~^,~}"
                                                    (remove "0" names :test #'equal)))))
         (bob-id  (bindf (nth 0 names)))
         (john-id (bindf (nth 1 names)))
         (tom-id  (bindf (nth 2 names)))
         )
    (standard-page ()
      (<:form
       :action form-id
       (<:p "What's your name?")
       (<:p (<:input :type :checkbox :name bob-id :value "Bob")
            (<:input :type :hidden :name bob-id :value "0") "Bob")
       (<:p (<:input :type :checkbox :name john-id :value "John")
            (<:input :type :hidden :name john-id :value "0") "John")
       (<:p (<:input :type :checkbox :name tom-id :value "Tom")
            (<:input :type :hidden :name tom-id :value "0")  "Tom")
       (<:p (<:input :type :submit
                     :name "OK"
                     :value "OK"))
       ))))

The hidden elements guarantee that the query data are always submitted to servers, and the corresponding call back functions are invoked.

We can define the type of the first argument of with-call-back.

see below example.

(defun http-greet-7 (&optional next-action)
  (invoke-next-action next-action :main #'greet-7))
(defun greet-7 ()
  (let* ((names   (list "Bob" "John" "Tom"))
         (form-id (dynamic-url (how-are-you
                                (format nil "~{~A~^,~}"
                                        (remove "0" names :test #'equal)))))
         (names-id (with-call-back (var :type 'list) (setf names var)))
         )
    (standard-page ()
      (<:form
       :action form-id
       (<:p "What's your name?")
       (<:input :type :hidden :name names-id :value "0")
       (loop
          for n in names
          do
            (<:p (<:input :type :checkbox :name names-id :value n) (<:as-html n)))
       (<:p (<:input :type :submit
                     :name "OK"
                     :value "OK"))
       ))))

(with-call-back (var :type 'list)...) , we can use :type keyword argument to specify the type of a query string. It is automatically convert it to a lisp object. see hunchentoot:define-easy-handler and hunchentoot::compute-parameter for more detail.

Another keyword :method can be :get, :post or :both.

The hidden element is necessary, because if all checkboxs are unchecked, the query string is missing and the call back function is not invoked.

We can use bindf to simpilify codes as below.

(defun http-greet-8 (&optional next-action)
  (invoke-next-action next-action :main #'greet-8))
(defun greet-8 ()
  (let* ((names   (list "Bob" "John" "Tom"))
         (form-id (dynamic-url (how-are-you
                                (format nil "~{~A~^,~}"
                                        (remove "0" names :test #'equal)))))
         (names-id (bindf names :type 'list))
         )
    (standard-page ()
      (<:form
       :action form-id
       (<:p "What's your name?")
       (<:input :type :hidden :name names-id :value "0")
       (loop
          for n in names
          do
            (<:p (<:input :type :checkbox :name names-id :value n) (<:as-html n)))
       (<:p (<:input :type :submit
                     :name "OK"
                     :value "OK"))
       ))))

An example of adding two numbers.

(defun http-add-two-numbers (&optional (next-action nil))
  (invoke-next-action next-action :main #'add-two-numbers))
(defun add-two-numbers ()
  (let* ((first-number 0)
         (form-id (dynamic-url
                    (input-next-number first-number)))
         (first-number-call-back-id (bindf  first-number :type 'integer))
         )
    (standard-page ()
      (<:form :action form-id
              (<:p "Please input the first number to add:"
                   (<:input :type :text
                            :name first-number-call-back-id
                            ))
              (<:p (<:input :type :submit
                     :name "OK"
                     :value "OK"))))))
(defun input-next-number(first-number)
  (let* ((second-number 0)
         (form-id (dynamic-url
                    (sum-of-the-two-numbers first-number second-number)))
         (second-number-call-back-id (bindf  second-number :type 'integer))
         )
    (standard-page ()
      (<:form :action form-id
              (<:p (<:as-html "Add to " first-number  "."
                              " Please input the second number:")
                   (<:input :type :text
                            :name second-number-call-back-id))
              (<:p (<:input :type :submit
                            :name "OK"
                            :value "OK"))))))
(defun sum-of-the-two-numbers (a b )
  (standard-page ()
      (<:p (<:as-html a "+" b "=" (+ a b)))
      (<:p (<:a :href "." "try again"))))

TO BE CONTINUE.