git.fiddlerwoaroof.com
README.org
4c3588fc
 #+TITLE: TODO backend implementation using CL and fukamachi/ningle
 
19b6e2b4
 * Setup
 
2abb94cb
   - src_sh[:exports code]{git clone https://github.com/fiddlerwoaroof/data-lens.git ~/quicklisp/local-projects/data-lens}
   - src_sh[:exports code]{git clone https://github.com/fukamachi/lack.git ~/quicklisp/local-projects/lack}
 
   #+BEGIN_SRC sh
     sbcl --eval '(asdf:load-asd (truename "todo-backend.asd"))' 
          --eval '(ql:quickload :todo-backend)' 
          --eval '(fwoar.todo::ensure-started)'
   #+END_SRC
   
   After this, all the tests [[http://www.todobackend.com/specs/index.html?http://localhost:5000][here]] should pass and the frontend [[http://www.todobackend.com/client/index.html?http://localhost:5000][here]] should work.
19b6e2b4
 
4c3588fc
 * 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.
   
b7951e2b
 ** List-level APIs
4c3588fc
    
6613ae69
    These are functions for getting the todo list and clearing
    it. These are activated by the root route: =todos= for GET requests
    and =clear-todos= for DELETE requests.
4c3588fc
 
    #+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=
6613ae69
    function for removing a todo from the list. =todo= is what backs
    the GET request for a specific todo by id.
4c3588fc
    
    #+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
6613ae69
    properly. =new-todo= backs POST requests to the root endpoint.
4c3588fc
 
    #+NAME: new-todo
    #+BEGIN_SRC lisp :tangle no
4258e918
      (defvar *external-host*
        "localhost")
      (defvar *external-port*
        5000)
 
4c3588fc
      (defun new-todo (value)
        (let ((id (next-id)))
          (setf (todo id)
                (alexandria:alist-hash-table
                 (rutilsx.threading:->>
                  value
                  (acons "completed" 'yason:false)
5fd28f99
                  (acons "url"
                         (format nil "http://~a:~d/todo/~d"
                                 ,*external-host*
                                 ,*external-port*
                                 id)))
4c3588fc
                 :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
6613ae69
    yason-compatible boolean. =update-todo= backs PATCH requests to the
    todo endpoint for a specific ID.
4c3588fc
 
    #+NAME: update-todo
    #+BEGIN_SRC lisp :tangle no
      (defun update-todo (id v)
5fd28f99
        (let* ((old-todo (or (todo id)
                             (make-hash-table :test 'equal)))
               (in-hash-table (alexandria:alist-hash-table v :test 'equal))
               (update (data-lens.lenses:over *completed-lens*
                                              'bool-to-yason
                                              in-hash-table)))
          (setf (todo id)
                (serapeum:merge-tables old-todo
                                       update))))
4c3588fc
    #+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>)
   
4258e918
 * Routing
 
 ** Routing utilities
 
    The core utility here is the =defroutes= macro.  This takes a
    sequence of endpoint descriptions which contain nested definitions
    for HTTP verbs and expands to ningle's functions for manipulating
    routes.
7762e7b6
 
    #+NAME: defroutes
    #+BEGIN_SRC lisp
      (defmacro defroutes (app &body routes)
        (alexandria:once-only (app)
          `(setf
            ,@(loop for (target . descriptors) in routes
                    append (loop for (method callback) in descriptors
                                 append `((ningle:route ,app ,target
9360d164
                                                        :method ,method)
7762e7b6
                                          ,callback))))))
    #+END_SRC
    
    This macro organizes all the HTTP verbs for a given endpoint under
    the path to that endpoint. A more complete version might allow for
    a list of verbs =(:GET :POST)= in the head of each handler clause.
4258e918
    
    #+BEGIN_SRC lisp :exports both :tangle no :results verbatim
      (macroexpand-1
       '(defroutes app
         ("/"
          (:GET (handler () (todos)))
          (:POST (handler (v) (new-todo v)))
          (:DELETE (handler () (clear-todos))))))
    #+END_SRC
7762e7b6
    
4258e918
 
    #+RESULTS:
    #+begin_example
    (LET ((#:APP1852 APP))
      (SETF (NINGLE/APP:ROUTE #:APP1852 "/" :METHOD METHOD)
              (HANDLER NIL
                (TODOS))
            (NINGLE/APP:ROUTE #:APP1852 "/" :METHOD METHOD)
              (HANDLER (V)
                (NEW-TODO V))
            (NINGLE/APP:ROUTE #:APP1852 "/" :METHOD METHOD)
              (HANDLER NIL
                (CLEAR-TODOS))))
    T
    #+end_example
7762e7b6
    
    Finally, there are some simple helpers to handle some of the
    boilerplate in a clack webserver.  Of particular interest is the
    =handler= macro, which (since this is a json-only API) makes sure
    that all the API results get JSON encoded.
4258e918
 
    #+NAME: routing-helpers
    #+BEGIN_SRC lisp
      (defun success (value)
7762e7b6
        (list 200 '(:conent-type "application/json") value))
4258e918
 
      (defmacro handler ((&optional (sym (gensym "PARAMS"))) &body body)
        `(lambda (,sym)
           (declare (ignorable ,sym))
           (success
            (fwoar.lack.json.middleware:wrap-result
             (progn ,@body)))))
7762e7b6
    #+END_SRC
    
 ** todo routes
 
46d4eb2f
    =setup-routes= binds the endpoints to handlers: ="/"= to handlers
    that handle the todo lists while ="/todo/:id"= to handlers that
7762e7b6
    handle individual todos.  The =:id= indicates that the
    corresponding segment of the path is bound to =:id= in the param
    alist. =get-id= handles this, and extracts an integer for the id
    (since we are using successive integers for the todo ids).
 
    #+NAME: todo-routes
    #+BEGIN_SRC lisp
      ;; routing
      (defun get-id (params)
        (parse-integer (serapeum:assocdr :id params)))
 
      (defun setup-routes (app)
        (defroutes app
          ("/" (:GET (handler () (todos)))
               (:POST (handler (v) (new-todo v)))
               (:DELETE (handler () (clear-todos))))
          ("/todo/:id" (:GET    (handler (v) (todo (get-id v))))
                       (:DELETE (handler (v)
                                  (delete-todo (get-id v))
                                  nil))
                       (:PATCH  (handler (v)
                                  (update-todo (get-id v) 
                                               (remove :id v :key #'car)))))))
    #+END_SRC
4258e918
 
4c3588fc
 * 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 
 
4258e918
    #+BEGIN_SRC lisp :tangle routing.lisp :noweb yes :comments noweb 
      <<package-include>>
 
7762e7b6
      <<defroutes>>
4258e918
 
7762e7b6
      <<routing-helpers>>
4258e918
 
7762e7b6
      <<todo-routes>>
4c3588fc
    #+END_SRC
 
 ** main.lisp source 
 
    #+BEGIN_SRC lisp :tangle main.lisp :noweb yes
9360d164
      <<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 (address "127.0.0.1") (port 5000))
        (declare (ignore address 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))
 
      (defun main (&rest _)
        (declare (ignore _))
        (ensure-started :address "0.0.0.0" :port 5000)
        (loop (sleep 5)))
4c3588fc
    #+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: }
291406c5
 #+HTML_HEAD:
 #+HTML_HEAD: #table-of-contents h2 {
 #+HTML_HEAD:     text-align: center;
 #+HTML_HEAD:     padding-top: 3.5em;
 #+HTML_HEAD: }
 #+HTML_HEAD:
 #+HTML_HEAD: #table-of-contents {
 #+HTML_HEAD:   width: 25rem;
 #+HTML_HEAD:   position: fixed;
 #+HTML_HEAD:   left: 0;
 #+HTML_HEAD:   top: 0;
 #+HTML_HEAD:   height: 100%;
 #+HTML_HEAD:   overflow-y: scroll;
 #+HTML_HEAD:   scrollbar-width: thin;
 #+HTML_HEAD: }
 #+HTML_HEAD:
 #+HTML_HEAD: #table-of-contents::-webkit-scrollbar { width :6px; }
 #+HTML_HEAD:
4c3588fc
 #+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);
e193c726
 #+HTML_HEAD:     font-family: "Alegreya Sans", "Lato", "Roboto", "Open Sans", "Helvetica", sans-serif;
4c3588fc
 #+HTML_HEAD: }
291406c5
 #+HTML_HEAD:
4c3588fc
 #+HTML_HEAD: a {color: var(--zenburn-blue);}
 #+HTML_HEAD: 
e193c726
 #+HTML_HEAD: h1, h2, h3, h4, h5, h6 {margin: 0; margin-top: 1.5em; margin-bottom: 0.5em;}
4c3588fc
 #+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:
f9720d09
 # after-save-hook: (lambda nil (org-babel-tangle) (when (org-html-export-to-html) (rename-file "README.html" "docs/index.html" t)))
4c3588fc
 # End: