ccQpein's Blog

In building, I become

#lisp-rpc #lisp

Lisp-RPC example: Lisp version special

I made the lisp-rpc example accountant app. The accountant development process mostly focused on the Rust lib and how to make a backend app. This post is a bit different from that. In the Lisp version, lisp-rpc has some special features, thanks to Lisp's homoiconic magic.

Homoiconic

Since lisp-rpc is valid Common Lisp syntax from day 1, lisp-rpc automatically gets Lisp's homoiconic magic.

For example, assume I have data like (hello-world :from "USA"). This is a valid s-expression, so I can do (eval (read-from-string data)).

;;; I define the function `hello-world`
(defun hello-world (&rest args &key my-name from)
  (declare (ignore args))
  (format nil "Hello~@[ to ~A~]!~@[ Friend from ~A!~]" my-name from))


;; Then run code below
(let ((pure-data-0 "(hello-world)")
      (pure-data-1 "(hello-world :from \"USA\")")
      (pure-data-2 "(hello-world :from \"Japan\" :my-name \"Mikasa\")"))
  (pprint (eval (read-from-string pure-data-0)))
  (pprint (eval (read-from-string pure-data-1)))
  (pprint (eval (read-from-string pure-data-2))))
;; The results are:
;; "Hello!"
;; "Hello! Friend from USA!"
;; "Hello to Mikasa! Friend from Japan!"

And this feature doesn't need any lisp-rpc lib code at all. It just needs a Common Lisp implementation.

Other general features are still here in Lisp

Other general features in the Rust lib are still here too.

Raw data

After installing lisp-rpc (I installed it in Quicklisp's local projects path), (ql:quickload "lisp-rpc") is all I need to do in the REPL.

I'll still use the lisp-rpc data example from above:

(defun run ()
  (let ((data '("(hello-world)"
                "(hello-world :from \"USA\")"
                "(hello-world :from \"Japan\" 
:my-name \"Mikasa\")"
                "(hello-world :from '(\"Japan\" \"Tokyo\") 
:my-name \"Mikasa\")"))
        (data1 "(nest-hello :from (hello-world :from \"USA\"))"))

    (loop for d in data
          do (let ((rd (lisp-rpc-raw-data:parse-data d)))
               (format t "name: ~a, from: ~a, name: ~a~%"
                       (lisp-rpc-raw-data:get-name rd)
                       (lisp-rpc-raw-data:data-get rd :from)
                       (lisp-rpc-raw-data:data-get rd :name))))

    (let* ((rd (lisp-rpc-raw-data:parse-data data1))
           (nest-rd (lisp-rpc-raw-data:data-get rd :from))) ;; nesting data
      (format t "name: ~a~%" (lisp-rpc-raw-data:get-name rd))
      (format t "in nest data's from, name: ~a, from: ~a"
              (lisp-rpc-raw-data:get-name nest-rd)
              (lisp-rpc-raw-data:data-get nest-rd :from)))
    ))

Run this function in the REPL:

CL-USER> (run)
name: HELLO-WORLD, from: NIL, name: NIL
name: HELLO-WORLD, from: USA, name: NIL
name: HELLO-WORLD, from: Japan, name: NIL
name: HELLO-WORLD, from: #S(RAW-DATA-LIST :L (Japan Tokyo)), name: NIL
name: NEST-HELLO
in nest data's from, name: HELLO-WORLD, from: USA

Spec lib generator

Like the Rust repo, the Lisp version also has a binary executable generator. After running ./make.sh, there will be a lisp-rpc-gen executable in the current path. Then I can run it with ./lisp-rpc-gen gen spec.lisp like:

(def-rpc-package demo)

(def-msg language-preference :lang 'string)

(def-msg book-info
  :lang 'language-preference
  :title 'string
  :version 'string
  :id 'string)

(def-msg authors :names (list 'string))

(def-rpc get-book
    '(:title 'string :version 'string
      :lang '(:lang 'string :encoding 'number)
      :authors 'authors)
  'book-info)

(def-rpc ping-no-pong
    '(:nothing 'string))

In the target folder output:

┌─ demo.asd
├─ lib.lisp
output

In demo.asd:

;;;; -*- Mode: Lisp; Syntax: ANSI-Common-Lisp; Base: 10 -*-

(defpackage :demo-sys
  (:use :cl :asdf))

(in-package :demo-sys)

(defsystem :demo
  :description "Generated Lisp RPC System for demo"
  :version "0.0.1"
  :depends-on ("lisp-rpc")
  :components ((:file "lib")))

And lib.lisp contains everything we need. Then we can start a web server and use this lib:

(load "./output/demo.asd")

(ql:quickload '("lisp-rpc" "demo"))

(use-package :lisp-rpc-server)
(use-package :demo)

;; 1. Initialize server
(defparameter *server* (make-rpc-server))

;; 2. Register handler (validated against rpc-endpoint-p)
(register-rpc-handler *server*
                      'demo:get-book
                      (lambda (req)
                        ;; req is automatically deserialized into a
                        ;; #S(DEMO:GET-BOOK ...) struct
                        (demo:make-book-info
                         :lang (make-language-preference :lang "en")
                         :title (demo:get-book-title req)
                         :id "12345")))

;; 3. Start server
(start-server *server*)

Then I can call the API with this:

curl -X POST http://localhost:5432 \
     -H "Content-Type: text/plain" \
     -d '(GET-BOOK :TITLE "Land of Lisp" :VERSION "1.0" :LANG (:LANG "en" :ENCODING 1))'

Response:

(BOOK-INFO :LANG (LANGUAGE-PREFERENCE :LANG "en") :TITLE "Land of Lisp" :VERSION "" :ID "12345")