How to create a RESTful API to the PicoLisp Database

Search for a command to run...

No comments yet. Be the first to comment.
In our previous post, we introduced how to utilize Discrete Event Simulation (DES) in PicoLisp. Now, let's explore another application: a railroad model simulation that includes tracks and trains. The visualization is ASCII-based, so feel free to unl...

The Dining Philosophers

In this post, we will introduce the concept of co-routines and how they are handled in PicoLisp. This will be the foundation for the subsequent posts on Discrete Event Simulation and finally a couple of examples, including an ASCII model railway simu...

One of the many nice things of PicoLisp is that it comes with a built-in database. This makes it easy to collect and store our own data, for example via a mobile app. But not all data is best represented in tables and lists: sometimes a colorful litt...

In the last two posts, we have built a little app that shows the current location and displays the nearest ice cream shops: https://picolisp-explored.com/an-openstreetmap-app-written-in-picolisp-pt-1 https://picolisp-explored.com/an-openstreetmap-ap...

In the previous posts we have created a database and generated content that can be displayed in the browser. The format we chose was human-readable but not so suitable for automatic processing.
Therefore we will now create a REST-API that enables other services to access the data in JSON format.

REST stands for Representational State Transfer, which means that it fulfills the following properties:
In simple words, what we want is to create an interface to our PicoLisp database that can be accessed from outside. The client doesn't have to speak PicoLisp, and the output format should be a standard machine-readable format such as JSON.
Let's create an API that any client can access via a simple GET-request. Similarly to the previous example, the record is specified via the URL.
JSON (JavaScript Object Notation) is a very popular, language-independent data format that can be used to exchange data between different services.
It supports a number of data types:
true or false)null as empty value.PicoLisp comes with a library to create json-output. It can be found in the library files under json.l. There are four functions:
checkJson (X Item),parseJson (Str),readJson (),printJson (Item).For our purpose, we will only need the printJson (Item) function. Its functionality is very simple: it takes a list of cons-pairs and prints them in the JSON format.
Let's open the REPL ($ pil +) and try it at an example:
: (setq A
'( ( name . "Smith" )
( age . 25)
( address
( street . "21 2nd Street")
( city . "New York")
( state . "NY" )
( zip . 10021 ) ) ) )
We have a nested list of cons-pairs, i. e. a list of cells where the "key" is found in the CAR and the "value" in the CDR. The keys are internal symbols (that's why it's name and not "name").
We can execute the printJson function on this:
: (printJson A)
{"name": "Smith", "age": 25, "address": {"street": "21 2nd Street", "city": "New York", "state": "NY", "zip": 10021}}
The function prints out a valid JSON string.
In order to be able to use printJson, we need to convert the database output into a structured cons-pair list. This will require some manual formatting. Up to now, we only queried specific attributes for a record, like ( : nm ). However, we can also get a list of items using the getl function.
Let's test it in the REPL. We can start the database without the server using any of the previous scripts, for example this one:
$ pil family.l -family~main +
family:
It opens us a prompt directly in the family namespace. Let's get the item {A1} in list format:
family: (getl '{A1})
-> ((({A2} {A4}) . kids) ("Margaret Rose" . nm) ({A3} . mate) ({A11} . ma) ({A33} . pa) (705091 . dat) ("Countess of Snowdon" . job))
As you can see, it's a cons-pair list, but with some differences to our desired format:
( "Margaret Rose" . nm ) we need ( nm . "Margaret Rose" ).mate, ma, pa and so on should be replaced by the person's names.705091 should be formatted.Let's go through it step by step.
How can we convert ( "Margaret Rose" . nm ) to ( nm . "Margaret Rose" )? Well, this is quite easy: We can build a new cons-pair using the cons function, which takes two arguments: CAR and CDR.
: (cons 1 2)
-> (1 . 2)
We can apply this to all items in the list using mapcar and an anonymous function:
(mapcar
'((X)
(cons (cdr X) (car X)) )
(getl This )
Let's test it in the REPL:
family: (mapcar '((X) (cons (cdr X) (car X))) (getl '{A1}))
-> ((kids {A2} {A4}) (nm . "Margaret Rose") (mate . {A3}) (ma . {A11}) (pa . {A33}) (dat . 705091) (job . "Countess of Snowdon"))
To get a better overview, let's write down each cons-pair of the getl output:
(({A2} {A4}) . kids)("Margaret Rose" . nm)({A3} . mate)({A11} . ma)({A33} . pa)(705091 . dat)("Countess of Snowdon" . job)Looking at the car, we have four cases: It is either a list of +Person objects, a number representing a date, a Person object, or a string. Let's treat either of these cases separately. We can switch between different "cases" using the cond function:
(cond ('any1 . prg1) ('any2 . prg2) ..) -> any
Multi-way conditional: Ifanyof theanyNconditions evaluates to non-NIL,prgNis executed and the result returned. Otherwise (all conditions evaluate toNIL),NILis returned.
Case 1: The CAR is a number.
If the value (let's call it V) is a number, return the formatted value. We can test whether it's a number with the num? function.
(cond
((num? V) (datStr V))
Case 2: The CAR is an +Person object.
To check this, we can use the isa function, which takes a class and an object. If yes, we want to get the name property of this object. We can get it with the ; function:
(cond
((num? V) (datStr V))
((isa '+Person V) (; V nm))
Case 3: The CAR is a list of +Person objects.
Now this one is a little bit tricky. Let's say the CAR is a list. In this case, we want to loop over every list item and return the name. We can do this with mapcar and an anonymous function which takes an object and returns it name property:
(mapcar '((This) (: nm)) V)
But how can we check if V is a list? For this purpose, we can use the pair function which checks if the argument is a cons pair and returns it if true. Technically, a "normal" list is also a cons pair, while numbers, strings and and objects (i. e. all the other cases) are not. So we can expand our cond condition list with the following line:
(cond
((num? V) (datStr V))
((isa '+Person V) (; V nm))
((pair V) (mapcar '((This) (: nm)) V) )
Case 4: The CAR is a string.
Lastly, if the CAR is a string or any other case we didn't consider, we do nothing with it:
(cond
((num? V) (datStr V))
((isa '+Person V) (; V nm))
((pair V) (mapcar '((This) (: nm)) V) )
(T V) )
Now we combine everything in one function:
(getl This)mapcar to build a new cons paircar, we modify it depending on the result of cond.(mapcar
'((X)
(cons (cdr X)
(let V (car X)
(cond
((pair V)
(mapcar '((This) (: dat)) V) )
((num? V) (datStr V)) # Can only be date
((isa '+Person V) (; V nm))
(T V) ) ) ) )
(getl This) )
Now that we have our list, all we have to do is converting it and modify our HTTP-Header so that it returns Content-Type: application/json instead of text/html. Instead of our standard html function, we call the function httpHead with the arguments "application/json" for the content-type and 0 for the cache-control. Let's test it in the REPL:
family: (httpHead "application/json" 0)
HTTP/1.0 200 OK
Server: PicoLisp
Date: Sun, 17 Oct 2021 14:44:31 GMT
Cache-Control: max-age=0
Cache-Control: private, no-store, no-cache
Content-Type: application/json
The function returns a complete header which informs the browser that the received data format is JSON.
Now we have to send it. In the HTTP protocol, there are two possibilities to upload data: Either the content-length is communicated in the header (which is difficult, i. e. expensive in a dynamically created page), or the upload is sent in chunks. We can create chunked upload and send the content via the ht:Out function from the htlibrary.
(httpHead "application/json" 0)
(ht:Out *Chunked
(printJson
...
As final step, we wrap it in a function called person.json. The .json is not a requirement, but it makes clear that the output of this format is a json-format. Accordingly, we modify the server and the allowed-function.
(allowed NIL
"@lib.css" "!person.json" )
...
(de person.json (This)
(httpHead "application/json" 0)
(ht:Out *Chunked
(printJson
...
...
Then we can start the program with
$ pil family-rest.l -family~main -go +
If we now point the server towards http://localhost:8080/?-A67, we see the following JSON-formatted output:

This output can be read by any application that calls a GET request to the URL.
That's it! The complete source code to this example can be found here.
https://software-lab.de/doc/index.html
https://software-lab.de/doc/app.html