DEV Community

PNS11
PNS11

Posted on

Let's build a picolisp guestbook

Introduction

This is a short write-up on a guestbook web app I wrote for practice some time ago.

It renders a form for entering a message and posting it to a guestbook object.

To run it you install picolisp, save the source code below as 'gub.l', then you do

$ pil gub.l +
: (new! '(+Gbk) 'nm "Your guestbook name here")
-> {2}
:(go)(wait)

or replace the last line with

:(bye)
$ pil gub.l -go -wait

and now you can point a web browser to localhost:8080.

How-to

A good place to start developing a web app is to design your data model. For a guestbook just about anything from loose message objects to a complex schema with users, roles and sub-guestbooks could be thought up in a short time.

For this example I chose to go about halfway between the simplest possible, loose messages, and a model with an object holding messages in a list in one of its fields, naming them and making it easy to replace a guestbook without deleting all of the messages at once.

(class +Msg +Entity)
(rel hdr (+String))
(rel bdy (+String))
(rel sdr (+String))
(rel dor (+String))
(rel book (+Joint) msgs (+Gbk))

(class +Gbk +Entity)
(rel nm (+Ref +String))
(rel msgs (+List +Joint) book (+Msg))

With a model like this one we can expose a certain '+Gbk object in the GUI by adjusting this line:

*Gbk (car (collect 'nm '+Gbk))

# for example, (db 'nm '+Gbk "Certain Name")

A more production ready way to do it would be to replace this with a function for browsing and selecting among available guestbooks.

Fetching data from the GUI form is done by holding values in three global variables and then passing these as method parameters to the object held in '*Gbk.

The method looks like this:

(dm addM> (Hdr Bdy Sdr) 
    (let  
        D (new! '(+Msg) 
            'hdr Hdr # header
            'bdy Bdy # message body
            'sdr Sdr # sender
            'dor (stamp)
            'book This )
        (put!> This 'msgs D)
    (commit 'upd) ))

It is possible to nest 'put!> and 'new! but it would be harder to read and extend, for example, if the newly created object should be added to more places in the database it is usually more convenient to have a 'let scoped symbol for doing so instead of relying on a nest of implicit symbol passing.

Typically one doesn't use 'ht:Prin directly, it is a fairly low level function used by GUI components, but it is quite useful for experimentation and learning (and, of course, construction of new components). It is more idiomatic and a better practice to use '+Chart with a pilog expression, but it also requires a little more and harder to read code.

            (for X (get *Gbk 'msgs) # one can use 'get 
                (with X # as well as 'with and the shortcuts 
                    (<h3> NIL (ht:Prin (: hdr)) )
                    (<br>)
                    (<p> NIL (ht:Prin (: bdy)) )
                    (<br>)
                    (<p> NIL (ht:Prin (pack (: sdr) " - " (: dor))) )

For the next steps in refining this application it could be a good idea to replace this presentation code with 'url> or chart constructs, or perhaps prepend the 'form with another one that lets the user choose among available guestbooks, perhaps giving the choice to create a new one.

The entire source code

(load "@lib/http.l" "@lib/xhtml.l" "@lib/form.l")

# in a real application you would use 'allowed to restrict access to 
# certain directories and functions. 
# you would also put httpGate, nginx or somesuch between the Internet 
# and the port where your program is listening.

(class +Msg +Entity)
(rel hdr (+String))
(rel bdy (+String))
(rel sdr (+String))
(rel dor (+String))
(rel book (+Joint) msgs (+Gbk))

# here a TODO could be to write a method that outputs message contents

(class +Gbk +Entity)
(rel nm (+Ref +String))
(rel msgs (+List +Joint) book (+Msg))

(dm addM> (Hdr Bdy Sdr) 
    (let D (new! '(+Msg) 
        'hdr Hdr 
        'bdy Bdy
        'sdr Sdr 
        'dor (stamp)
        'book This )
        (put!> This 'msgs D)
    (commit 'upd) ))

(pool "gb.db")

(setq 
    *Gbk (car (collect 'nm '+Gbk)) # decide which guestbook we'll serve
     )

(de work ()
    (app) # sets up session handling
    (action # says there will be action in some web forms
        (html 0 "Guestbook" "@lib.css" NIL 
            (<h2> NIL "Picolisp guestbook")
            (<hr>)
            (form NIL 
        (<p> NIL "Message header:")
        (gui 'hdr '(+Var +TextField) '*MsgHdr 30)
                (do 2 (<br>))
        (<p> NIL "Message text:")
                (gui 'bdy '(+Var +TextField) '*MsgBdy 30 5)
                (do 2 (<br>))
        (<p> NIL "Your name:")
                (gui 'sdr '(+Var +TextField) '*MsgSdr 30)
                (do 2 (<br>))
                (gui '(+Button) "Send" 
                    '(ask "Post this message?" 
                        (addM> *Gbk *MsgHdr *MsgBdy *MsgSdr) )) )
            (<br>)
        (<hr>)
            (for X (get *Gbk 'msgs) # instead of '+Chart, '+QueryChart or 'url>
                (with X
                    (<h3> NIL (ht:Prin (: hdr)) )
                    (<br>)
                    (<p> NIL (ht:Prin (: bdy)) )
                    (<br>)
                    (<p> NIL (ht:Prin (pack (: sdr) " - " (: dor))) )
            (<br>) )) )))

(de go ()
    (server 8080 "!work") )

The end

If you thought this was worthwhile reading, consider donating a coffee or two, Stagling@Patreon.

Top comments (0)