Browse code
(init)
Ed Langley authored on 29/08/2019 00:43:13
Showing 8 changed files
Showing 8 changed files
- .gitignore
- lack-cors-middleware.lisp
- lack-json-middleware.lisp
- main.lisp
- model.lisp
- package.lisp
- routing.lisp
- todo-backend.asd
0 | 3 |
new file mode 100644 |
... | ... |
@@ -0,0 +1,23 @@ |
1 |
+(in-package :fwoar.lack.cors.middleware) |
|
2 |
+;; cors middleware |
|
3 |
+(defparameter *acao-lens* |
|
4 |
+ (data-lens:<>1 (data-lens.lenses:make-list-lens 1) |
|
5 |
+ (data-lens.lenses:make-plist-lens :Access-Control-Allow-Origin))) |
|
6 |
+(defparameter *acah-lens* |
|
7 |
+ (data-lens:<>1 (data-lens.lenses:make-list-lens 1) |
|
8 |
+ (data-lens.lenses:make-plist-lens :Access-Control-Allow-Headers))) |
|
9 |
+(defparameter *acam-lens* |
|
10 |
+ (data-lens:<>1 (data-lens.lenses:make-list-lens 1) |
|
11 |
+ (data-lens.lenses:make-plist-lens :Access-Control-Allow-Methods))) |
|
12 |
+ |
|
13 |
+(defun cors-middleware (app) |
|
14 |
+ (lambda (env) |
|
15 |
+ (rutilsx.threading:->> |
|
16 |
+ (if (eq :options |
|
17 |
+ (getf env :request-method)) |
|
18 |
+ '(200 nil nil) |
|
19 |
+ (let ((res (funcall app env))) |
|
20 |
+ res)) |
|
21 |
+ (data-lens.lenses:set *acao-lens* "*") |
|
22 |
+ (data-lens.lenses:set *acah-lens* "Content-Type") |
|
23 |
+ (data-lens.lenses:set *acam-lens* "GET,POST,DELETE,PATCH")))) |
0 | 24 |
new file mode 100644 |
... | ... |
@@ -0,0 +1,36 @@ |
1 |
+(in-package :fwoar.lack.json.middleware) |
|
2 |
+ |
|
3 |
+;; json middleware |
|
4 |
+(defparameter *result-lens* |
|
5 |
+ (data-lens.lenses:make-list-lens 2)) |
|
6 |
+ |
|
7 |
+(defun json-middleware (app) |
|
8 |
+ (lambda (env) |
|
9 |
+ (let ((res (funcall app env))) |
|
10 |
+ (data-lens.lenses:over *result-lens* 'encode-result |
|
11 |
+ res)))) |
|
12 |
+ |
|
13 |
+(defun encode-json-to-string (v) |
|
14 |
+ (yason:with-output-to-string* (:indent t) |
|
15 |
+ (yason:encode v |
|
16 |
+ yason::*json-output*))) |
|
17 |
+ |
|
18 |
+(defclass json-result () |
|
19 |
+ ((%v :initarg :v :reader json-value))) |
|
20 |
+ |
|
21 |
+(defun wrap-result (v) |
|
22 |
+ (make-instance 'json-result :v v)) |
|
23 |
+ |
|
24 |
+(defgeneric encode-result (v) |
|
25 |
+ (:method (v) |
|
26 |
+ v) |
|
27 |
+ (:method ((v json-result)) |
|
28 |
+ (list (encode-json-to-string (json-value v))))) |
|
29 |
+ |
|
30 |
+(defmethod yason:encode ((o fset:seq) &optional s) |
|
31 |
+ (yason:encode (coerce (fset:convert 'list o) 'vector) |
|
32 |
+ s)) |
|
33 |
+ |
|
34 |
+(defmethod yason:encode ((o fset:map) &optional s) |
|
35 |
+ (yason:encode (fset:convert 'hash-table o) |
|
36 |
+ s)) |
0 | 37 |
new file mode 100644 |
... | ... |
@@ -0,0 +1,35 @@ |
1 |
+(in-package :fwoar.todo) |
|
2 |
+ |
|
3 |
+;;; entrypoint |
|
4 |
+(defun setup () |
|
5 |
+ (let ((app (make-instance 'ningle:<app>))) |
|
6 |
+ (prog1 app (setup-routes app)))) |
|
7 |
+ |
|
8 |
+(defvar *handler*) |
|
9 |
+ |
|
10 |
+(defun is-running () |
|
11 |
+ (and (boundp '*handler*) |
|
12 |
+ *handler*)) |
|
13 |
+ |
|
14 |
+(defun ensure-started (&rest r &key port) |
|
15 |
+ (declare (ignore port)) |
|
16 |
+ (let ((app (setup))) |
|
17 |
+ (values app |
|
18 |
+ (setf *handler* |
|
19 |
+ (if (not (is-running)) |
|
20 |
+ (apply 'clack:clackup |
|
21 |
+ (lack.builder:builder |
|
22 |
+ :accesslog |
|
23 |
+ 'fwoar.lack.cors.middleware:cors-middleware |
|
24 |
+ 'fwoar.lack.json.middleware:json-middleware |
|
25 |
+ app) |
|
26 |
+ r) |
|
27 |
+ *handler*))))) |
|
28 |
+ |
|
29 |
+(defun stop () |
|
30 |
+ (if (is-running) |
|
31 |
+ (progn |
|
32 |
+ (clack:stop *handler*) |
|
33 |
+ (makunbound '*handler*) |
|
34 |
+ nil) |
|
35 |
+ nil)) |
0 | 36 |
new file mode 100644 |
... | ... |
@@ -0,0 +1,50 @@ |
1 |
+(in-package :fwoar.todo) |
|
2 |
+ |
|
3 |
+;; todo "database" api |
|
4 |
+(defvar *todos* (fset:empty-map)) |
|
5 |
+ |
|
6 |
+(defun todos () |
|
7 |
+ (gmap:gmap :seq |
|
8 |
+ (lambda (_ b) |
|
9 |
+ (declare (ignore _)) |
|
10 |
+ b) |
|
11 |
+ (:map *todos*))) |
|
12 |
+ |
|
13 |
+(defun todo (id) |
|
14 |
+ (let ((todo (fset:@ *todos* id))) |
|
15 |
+ todo)) |
|
16 |
+ |
|
17 |
+(defun (setf todo) (new-value id) |
|
18 |
+ (setf *todos* |
|
19 |
+ (fset:with *todos* id new-value)) |
|
20 |
+ new-value) |
|
21 |
+ |
|
22 |
+(defun delete-todo (id) |
|
23 |
+ (setf *todos* |
|
24 |
+ (fset:less *todos* id))) |
|
25 |
+ |
|
26 |
+(defparameter *cur-id* 0) |
|
27 |
+(defun next-id () |
|
28 |
+ (incf *cur-id*)) |
|
29 |
+ |
|
30 |
+(defun new-todo (value) |
|
31 |
+ (let ((id (next-id))) |
|
32 |
+ (setf (todo id) |
|
33 |
+ (alexandria:alist-hash-table |
|
34 |
+ (rutilsx.threading:->> |
|
35 |
+ value |
|
36 |
+ (acons "completed" 'yason:false) |
|
37 |
+ (acons "url" (format nil "http://localhost:5000/todo/~d" id))) |
|
38 |
+ :test 'equal)))) |
|
39 |
+ |
|
40 |
+(defun clear-todos () |
|
41 |
+ (setf *todos* |
|
42 |
+ (fset:empty-map))) |
|
43 |
+ |
|
44 |
+(defun update-todo (id v) |
|
45 |
+ (setf (todo id) |
|
46 |
+ (serapeum:merge-tables (or (todo id) |
|
47 |
+ (make-hash-table :test 'equal)) |
|
48 |
+ (alexandria:alist-hash-table |
|
49 |
+ v |
|
50 |
+ :test 'equal)))) |
0 | 51 |
new file mode 100644 |
... | ... |
@@ -0,0 +1,11 @@ |
1 |
+(defpackage :fwoar.lack.json.middleware |
|
2 |
+ (:use :cl) |
|
3 |
+ (:export #:json-middleware)) |
|
4 |
+ |
|
5 |
+(defpackage :fwoar.lack.cors.middleware |
|
6 |
+ (:use :cl) |
|
7 |
+ (:export #:cors-middleware)) |
|
8 |
+ |
|
9 |
+(defpackage :fwoar.todo |
|
10 |
+ (:use :cl) |
|
11 |
+ (:export )) |
0 | 12 |
new file mode 100644 |
... | ... |
@@ -0,0 +1,38 @@ |
1 |
+(in-package :fwoar.todo) |
|
2 |
+ |
|
3 |
+(defmacro defroutes (app &body routes) |
|
4 |
+ "Define a set of routes for given paths. the ROUTES parameter expects this format: |
|
5 |
+ ((\"/path/to/{route}\" :method :POST) route-callback) the AS-ROUTE macro helps one |
|
6 |
+ avoid binding function values to the route for flexibility." |
|
7 |
+ (alexandria:once-only (app) |
|
8 |
+ `(progn |
|
9 |
+ ,@(loop for ((target &key method) callback) in routes |
|
10 |
+ collect `(setf (ningle:route ,app ,target :method ,(or method :GET)) ,callback))))) |
|
11 |
+ |
|
12 |
+ |
|
13 |
+;; routing |
|
14 |
+(defun success (value) |
|
15 |
+ (list 200 nil value)) |
|
16 |
+ |
|
17 |
+(defmacro handler ((&optional (sym (gensym "PARAMS"))) &body body) |
|
18 |
+ `(lambda (,sym) |
|
19 |
+ (declare (ignorable ,sym)) |
|
20 |
+ (success |
|
21 |
+ (fwoar.lack.json.middleware::wrap-result |
|
22 |
+ (progn ,@body))))) |
|
23 |
+ |
|
24 |
+(defun get-id (params) |
|
25 |
+ (parse-integer (serapeum:assocdr :id params))) |
|
26 |
+ |
|
27 |
+(defun setup-routes (app) |
|
28 |
+ (defroutes app |
|
29 |
+ (("/" :method :GET) (handler () (todos))) |
|
30 |
+ (("/" :method :POST) (handler (v) (new-todo v))) |
|
31 |
+ (("/" :method :DELETE) (handler () (clear-todos))) |
|
32 |
+ (("/todo/:id" :method :GET) (handler (v) (todo (get-id v)))) |
|
33 |
+ (("/todo/:id" :method :DELETE) (handler (v) |
|
34 |
+ (delete-todo (get-id v)) |
|
35 |
+ nil)) |
|
36 |
+ (("/todo/:id" :method :PATCH) (handler (v) |
|
37 |
+ (update-todo (get-id v) |
|
38 |
+ (remove :id v :key #'car)))))) |
0 | 39 |
new file mode 100644 |
... | ... |
@@ -0,0 +1,25 @@ |
1 |
+;;; -*- Mode:Lisp; Syntax:ANSI-Common-Lisp; Package: ASDF-USER -*- |
|
2 |
+(in-package :asdf-user) |
|
3 |
+ |
|
4 |
+(defsystem :todo-backend |
|
5 |
+ :description "" |
|
6 |
+ :author "Ed L <edward@elangley.org>" |
|
7 |
+ :license "MIT" |
|
8 |
+ :depends-on (#:alexandria |
|
9 |
+ #:uiop |
|
10 |
+ #:serapeum |
|
11 |
+ #:yason |
|
12 |
+ #:fset |
|
13 |
+ #:ningle |
|
14 |
+ #:alexandria |
|
15 |
+ #:clack) |
|
16 |
+ :components ((:file "package") |
|
17 |
+ (:file "lack-cors-middleware" :depends-on ("package")) |
|
18 |
+ (:file "lack-json-middleware" :depends-on ("package")) |
|
19 |
+ (:file "model" :depends-on ("package")) |
|
20 |
+ (:file "routing" :depends-on ("package" |
|
21 |
+ "model")) |
|
22 |
+ (:file "main" :depends-on ("package" |
|
23 |
+ "routing" |
|
24 |
+ "lack-cors-middleware" |
|
25 |
+ "lack-json-middleware")))) |