git.fiddlerwoaroof.com
Raw Blame History
#+TITLE: TODO backend implementation using CL and fukamachi/ningle

* todo API

  We use a fairly simple structure for our "database": a fset map (a
  clojure-inspired persistent data structure) and a handful of
  interface functions that wrap it. In this code, this fset map is
  referenced as =*todo*=, but this is a detail hidden behind the API.
  
** List-level APIS
   
   These are functions for getting the todo list and clearing it.

   #+NAME: todolist-manipulation
   #+BEGIN_SRC lisp :tangle no
     (defun todos ()
       (gmap:gmap :seq
                  (lambda (_ b)
                    (declare (ignore _))
                    b)
                  (:map *todos*)))

     (defun clear-todos ()
       (setf *todos*
             (fset:empty-map)))
   #+END_SRC
  
** Getting/Replacing a todo
   
   This uses lisp's [[http://www.lispworks.com/documentation/HyperSpec/Body/05_a.htm][generalized references]] to abstract away the
   storage details of the todos. We also provide a =delete-todo=
   function for removing a todo from the list.
   
   #+NAME: todo-accessor
   #+BEGIN_SRC lisp :tangle no
     (defun todo (id)
       (let ((todo (fset:@ *todos* id)))
         todo))

     (defun (setf todo) (new-value id)
       (setf (fset:@ *todos* id)
             new-value))

     (defun delete-todo (id)
       (setf *todos*
             (fset:less *todos* id)))
   #+END_SRC
   
** Adding and modifying todos

   =new-todo= is fairly trivial. It's main feature is that it has to
   make sure the =completed= and =url= keys are set to the appropriate
   values. Completed isn't a lisp boolean, so it serializes to JSON
   properly.

   #+NAME: new-todo
   #+BEGIN_SRC lisp :tangle no
     (defun new-todo (value)
       (let ((id (next-id)))
         (setf (todo id)
               (alexandria:alist-hash-table
                (rutilsx.threading:->>
                 value
                 (acons "completed" 'yason:false)
                 (acons "url" (format nil "http://localhost:5000/todo/~d" id)))
                :test 'equal))))
   #+END_SRC

   =update-todo= just merges the input from the frontend into the
   relevant todo and then makes sure that the =completed= key is a
   yason-compatible boolean.

   #+NAME: update-todo
   #+BEGIN_SRC lisp :tangle no
     (defun update-todo (id v)
       (setf (todo id)
             (serapeum:merge-tables (or (todo id)
                                        (make-hash-table :test 'equal))
                                    (data-lens.lenses:over *completed-lens*
                                                           'bool-to-yason
                                                           (alexandria:alist-hash-table
                                                            v
                                                            :test 'equal)))))
   #+END_SRC

** Examples
  
   #+BEGIN_SRC lisp :tangle no :noweb yes :exports both :results verbatim
     <<example-setup>>
     (with-fresh-todos ()
       (new-todo '(("title" . "get groceries")))
       (new-todo '(("title" . "write-better-documentation")))
       (fset:convert 'list (todos)))
   #+END_SRC
  
   #+RESULTS:
   : (#<hash-table "url": "http://localhost:5000/todo/22",
   :               "title": "get groceries",
   :               "completed": YASON:FALSE>
   :  #<hash-table "url": "http://localhost:5000/todo/23",
   :               "title": "write-better-documentation",
   :               "completed": YASON:FALSE>)
  
* Source
** model.lisp source code

   #+BEGIN_SRC lisp :tangle model.lisp :noweb yes :comments noweb
     <<package-include>>
     <<model-utils>>

     (defvar *todos* (fset:empty-map))

     <<todolist-manipulation>>

     <<todo-accessor>>

     <<new-todo>>

     <<update-todo>>

     (defmacro with-fresh-todos (() &body body)
       `(let ((*todos* (fset:empty-map)))
          ,@body))
   #+END_SRC

** routing.lisp source 

   #+BEGIN_SRC lisp :tangle routing.lisp :noweb yes
   <<package-include>>
   (defmacro defroutes (app &body routes)
     "Define a set of routes for given paths. the ROUTES parameter expects this format:
      ((\"/path/to/{route}\" :method :POST) route-callback) the AS-ROUTE macro helps one
      avoid binding function values to the route for flexibility."
     (alexandria:once-only (app)
       `(progn
          ,@(loop for ((target &key method) callback) in routes
                  collect `(setf (ningle:route ,app ,target :method ,(or method :GET)) ,callback)))))


   ;; routing
   (defun success (value)
     (list 200 nil value))

   (defmacro handler ((&optional (sym (gensym "PARAMS"))) &body body)
     `(lambda (,sym)
        (declare (ignorable ,sym))
        (success
         (fwoar.lack.json.middleware:wrap-result
          (progn ,@body)))))

   (defun get-id (params)
     (parse-integer (serapeum:assocdr :id params)))

   (defun setup-routes (app)
     (defroutes app
       (("/" :method :GET)            (handler () (todos)))
       (("/" :method :POST)           (handler (v) (new-todo v)))
       (("/" :method :DELETE)         (handler () (clear-todos)))
       (("/todo/:id" :method :GET)    (handler (v) (todo (get-id v))))
       (("/todo/:id" :method :DELETE) (handler (v)
                                        (delete-todo (get-id v))
                                        nil))
       (("/todo/:id" :method :PATCH)  (handler (v)
                                        (update-todo (get-id v) 
                                                     (remove :id v :key #'car))))))
   #+END_SRC

** main.lisp source 

   #+BEGIN_SRC lisp :tangle main.lisp :noweb yes
   <<package-include>>
   ;;; entrypoint
   (defun setup ()
     (let ((app (make-instance 'ningle:<app>)))
       (prog1 app (setup-routes app))))

   (defvar *handler*)

   (defun is-running ()
     (and (boundp '*handler*)
          ,*handler*))

   (defun ensure-started (&rest r &key port)
     (declare (ignore port))
     (let ((app (setup)))
       (values app
               (setf *handler*
                     (if (not (is-running))
                         (apply 'clack:clackup
                                (lack.builder:builder
                                 :accesslog
                                 'fwoar.lack.cors.middleware:cors-middleware
                                 'fwoar.lack.json.middleware:json-middleware
                                 app)
                                r)
                         ,*handler*)))))

   (defun stop ()
     (if (is-running)
         (progn
           (clack:stop *handler*)
           (makunbound '*handler*)
           nil)
         nil))
   #+END_SRC

   #+NAME: package-include
   #+BEGIN_SRC lisp :tangle no :exports none
   (in-package :fwoar.todo)

   #+END_SRC

   #+NAME: model-utils
   #+BEGIN_SRC lisp :tangle no :exports none
   (defparameter *cur-id* 0)
   (defun next-id ()
     (incf *cur-id*))

   (defparameter *completed-lens*
     (data-lens.lenses:make-hash-table-lens "completed"))

   (defun bool-to-yason (bool)
     (if bool
         'yason:true
         'yason:false))
   #+END_SRC

   #+NAME: example-setup
   #+BEGIN_SRC lisp :tangle no :noweb yes :exports none
   <<package-include>>
   (load "pprint-setup")

   #+END_SRC

#+HTML_HEAD: <style>
#+HTML_HEAD: :root {
#+HTML_HEAD:     --zenburn-fg-plus-2: #ffffef;
#+HTML_HEAD:     --zenburn-fg-plus-1: #f5f5d6;
#+HTML_HEAD:     --zenburn-fg: #dcdccc;
#+HTML_HEAD:     --zenburn-bg: #3f3f3f;
#+HTML_HEAD:     --zenburn-bg-plus-1: #4f4f4f;
#+HTML_HEAD:     --zenburn-bg-plus-2: #5f5f5f;
#+HTML_HEAD:     --zenburn-blue: #8cd0d3;
#+HTML_HEAD: }
#+HTML_HEAD: 
#+HTML_HEAD: * {box-sizing: border-box;}
#+HTML_HEAD: 
#+HTML_HEAD: body {
#+HTML_HEAD:     font-size: 1.2rem;
#+HTML_HEAD:     width: 75rem;
#+HTML_HEAD:     margin: 0 0 0 25rem;
#+HTML_HEAD:     background: var(--zenburn-bg);
#+HTML_HEAD:     color: var(--zenburn-fg);
#+HTML_HEAD: }
#+HTML_HEAD: 
#+HTML_HEAD: a {color: var(--zenburn-blue);}
#+HTML_HEAD: 
#+HTML_HEAD: h1, h2, h3, h4, h5, h6 {margin: 0;}
#+HTML_HEAD: 
#+HTML_HEAD: pre {margin: 0; box-shadow: none; border-width: 0.5em;}
#+HTML_HEAD: 
#+HTML_HEAD: pre.example {
#+HTML_HEAD:     background-color: var(--zenburn-bg-plus-2);
#+HTML_HEAD:     color: var(--zenburn-fg-plus-2);
#+HTML_HEAD:     border: none;
#+HTML_HEAD:     padding-left: 4em;
#+HTML_HEAD: }
#+HTML_HEAD: 
#+HTML_HEAD: pre.src {
#+HTML_HEAD:     background-color: var(--zenburn-bg-plus-1);
#+HTML_HEAD:     border-color: var(--zenburn-bg-plus-2);
#+HTML_HEAD:     color: var(--zenburn-fg-plus-1);
#+HTML_HEAD: }
#+HTML_HEAD: 
#+HTML_HEAD: pre.src::before {
#+HTML_HEAD:     background-color: var(--zenburn-bg-plus-1);
#+HTML_HEAD:     border-color: var(--zenburn-bg-plus-2);
#+HTML_HEAD:     color: var(--zenburn-fg-plus-1);
#+HTML_HEAD: }
#+HTML_HEAD: </style>

# Local Variables:
# after-save-hook: (lambda nil (when (org-html-export-to-html) (rename-file "README.html" "docs/index.html" t)))
# End: