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.
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.
(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.
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.
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.
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.
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.
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.
INVOKE-NEXT-ACTION
:
NEXT-ACTION
is a random string.INVOKE-NEXT-ACTION
looks up a special hash table *K*
. If
(gethash next-action *k*)
retuns a function, the function is
invoked.NEXT-ACTION
is NIL
. We can not find any function associated
with NIL
. By default, the main
function is invoked. In this
case, the function invokes (counter-main 0)
.DYNAMIC-URL
(defmacro dynamic-url (&body body) ...)
BODY
ID
, and associates ID
with the
function, i.e. (setf (gethash id *k*) #'the-function)
ID
.COUNTER-MAIN
is invoked with argument COUNTER
binding to 0HTTP-COUNTER
is invoked with NEXT-ACTION
binds to "4329973BC1559D54C5CCC74C9C20200D"INVOKE-NEXT-ACTION
must find the function, with body "(counter-main (1+ counter))"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".
with-call-back
An example is as below.
(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")))
with-call-back
is a macro, return a random string as an id of a
query parameter.(var)
of with-call-back
is bound to the value of
the query parameter.name
.name
is within the lexical scope which is shared by the body
of with-call-back
and the body of dynamic-url
, so the assignment of
name
is also visible in the body of dynamic-url
dynamic-url
, all call back functions
created by with-call-back
are invoked if the associated query
string exits.dynamic-url
invokes the next action, i.e. how-are-you-1
As the matter of style, the below example has better style. It is very helpful, especially when your page becomes more complicate.
(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
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
.
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 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.
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")) ))))
(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"))))