/src/view-server/view-server.lisp

http://github.com/sykopomp/chillax · Lisp · 209 lines · 151 code · 27 blank · 31 comment · 8 complexity · 4edd966aa9bff48a7da68a73041abd21 MD5 · raw file

  1. (defpackage #:chillax-server
  2. (:use :cl :chillax.utils)
  3. (:export :mkhash :strcat :fun :emit :hashget :log-message :validation-failure :forbidden
  4. :*user-package* :*encode-json* :*decode-json*))
  5. (defpackage #:chillax-server-user
  6. (:use :cl :chillax-server))
  7. (in-package :chillax-server)
  8. ;;;
  9. ;;; Configuration
  10. ;;;
  11. (defparameter *user-package* (find-package :chillax-server-user)
  12. "Package that user view functions will be compiled and executed in.")
  13. (defparameter *encode-json* #'yason:encode
  14. "Function to use when encoding Lisp->JSON. Must return a string.")
  15. (defparameter *decode-json* #'yason:parse
  16. "Function to use to decode JSON->Lisp. Must accept a string.")
  17. ;;;
  18. ;;; Utils
  19. ;;;
  20. (defmacro with-user-package (&body body)
  21. "Evaluates BODY in the :chillax-server-user package."
  22. `(let ((*package* *user-package*))
  23. ,@body))
  24. (defmacro fun (&body body)
  25. "This macro puts the FUN back in FUNCTION."
  26. `(lambda (&optional _) (declare (ignorable _)) ,@body))
  27. ;;;
  28. ;;; Conditions
  29. ;;;
  30. (define-condition chillax-server-error (error) ())
  31. (define-condition function-compilation-error (chillax-server-error)
  32. ((function-string :initarg :string :reader function-string))
  33. (:report (lambda (c s) (format s "Function compilation failed: ~A" (function-string c)))))
  34. (define-condition validation-failure (chillax-server-error)
  35. ((message :initarg :message :reader failure-message))
  36. (:report (lambda (c s) (format s "Validation failed: ~A" (failure-message c)))))
  37. (define-condition forbidden (validation-failure)
  38. ()
  39. (:report (lambda (c s) (format s "Operation Forbidden: ~A" (failure-message c)))))
  40. ;;;
  41. ;;; User functions
  42. ;;;
  43. (defvar *config* nil)
  44. (defvar *functions*) ; holds a list of functions CouchDB is currently dealing with.
  45. (defvar *function-cache* (make-hash-table :test #'equal)
  46. "Cache of compiled user functions. This is cleared whenever the reset command is run.")
  47. (defun ensure-view-function (maybe-function)
  48. "Returns a compiled lisp function. MAYBE-FUNCTION can be a function object or a string
  49. with the source code to compile a function from."
  50. (or (when (functionp maybe-function) maybe-function)
  51. (gethash maybe-function *function-cache*)
  52. (compile-view-function maybe-function)
  53. (error "Can't return a function based on ~S" maybe-function)))
  54. (defun compile-view-function (string)
  55. "Compiles an anonymous function from STRING."
  56. (multiple-value-bind (function warningsp failurep)
  57. (with-user-package
  58. (compile nil (read-from-string string)))
  59. (when warningsp
  60. (log-message "A view function did not compile cleanly."))
  61. (if failurep
  62. (error 'function-compilation-error :string string)
  63. (setf (gethash string *function-cache*) function))))
  64. (defun call-user-function (function &rest args)
  65. (with-user-package (apply (ensure-view-function function) args)))
  66. (defun respond (response)
  67. (handler-case
  68. (funcall *encode-json* response)
  69. (error () (log-message "Error encoding response: ~A." response)))
  70. (terpri)
  71. (finish-output))
  72. ;;;
  73. ;;; User-accessible functions
  74. ;;;
  75. (defvar *map-results*)
  76. (defun emit (key value)
  77. "Adds an entry to the current map function results."
  78. (push (list key value) *map-results*))
  79. (defun log-message (format-string &rest format-args)
  80. "Like FORMAT, but the resulting string is written to CouchDB's log."
  81. (format t "~&[\"log\", \"Chillax View Server: ~A\"]~%"
  82. (apply #'format nil format-string format-args))
  83. (finish-output))
  84. ;;;
  85. ;;; CouchDB Commands
  86. ;;;
  87. (defun add-fun (string)
  88. "Compiles and adds a function whose source code is in STRING to the current list of
  89. active CouchDB functions."
  90. (push (ensure-view-function string) *functions*)
  91. (respond t))
  92. (defun reset (&optional config)
  93. "Resets the view server. Any caches should be emptied at this point, and any stored
  94. map functions should be cleared out."
  95. (setf *config* config)
  96. (when (boundp '*functions*)
  97. (setf *functions* nil))
  98. (clrhash *function-cache*)
  99. (respond t))
  100. (defun call-map-function (function doc &aux *map-results*)
  101. "Calls a stored compile function on a document. *MAP-RESULTS* is where EMIT will send k/v pairs."
  102. (call-user-function function doc)
  103. (or *map-results* '(#())))
  104. (defun map-doc (doc)
  105. "Responds to CouchDB with the results of calling all the currently-active map functions on DOC."
  106. (respond (or (mapcar (fun (call-map-function _ doc)) (reverse *functions*)) '((#())))))
  107. (defun reduce-results (fun-strings keys-and-values)
  108. "Responds to CouchDb with the results of calling the functions in FUN-STRINGS on KEYS-AND-VALUES."
  109. (loop for result in keys-and-values
  110. collect (caar result) into keys
  111. collect (cadr result) into values
  112. finally (respond (list t (mapcar (fun (call-user-function _ keys values nil))
  113. fun-strings)))))
  114. (defun rereduce (fun-strings values)
  115. "Responds to CouchDB with the results of rereducing FUN-STRINGS on VALUES."
  116. (respond (list t (mapcar (fun (call-user-function _ nil values t)) fun-strings))))
  117. (defun filter (docs req user-context)
  118. "Responds to CouchDB with the result of filtering DOCS using the current filter function."
  119. ;; Yes. I know it only uses the first function only. The JS view server does the same thing.
  120. (respond (list t (mapcar (fun (call-user-function (car (last *functions*)) _ req user-context)) docs))))
  121. (defun validate (fun-string new-doc old-doc user-context)
  122. "Executes a view function that validates NEW-DOC as a new version of OLD-DOC.
  123. Validation passes when the function returns normally. Validation will fail if the function errors in
  124. any way, and the condition's name and printout will be send to CouchDB as the exception."
  125. (handler-case (progn
  126. (call-user-function fun-string new-doc old-doc user-context)
  127. (respond "1")) ; the JS server does this. Cargo cult culture dictates
  128. ; that I should copy behavior regardless of understanding.
  129. (error (e)
  130. (respond (mkhash (string-downcase (princ-to-string (type-of e)))
  131. (remove #\Newline (princ-to-string e)))))))
  132. (defun ensure-response (maybe-response)
  133. (if (hash-table-p maybe-response)
  134. maybe-response
  135. (mkhash "body" (princ-to-string maybe-response))))
  136. (defun show (fun-string doc request)
  137. "Show functions are used to render documents. See CouchDB documentation for more details."
  138. (respond (list "resp" (ensure-response (call-user-function fun-string doc request)))))
  139. (defun update (fun-string doc request)
  140. "See CouchDB documentation for how to use _update. Functions written for the Chillax view server
  141. should return (values document response)."
  142. (respond (multiple-value-call #'list "up" (call-user-function fun-string doc request))))
  143. ;;;
  144. ;;; View server
  145. ;;;
  146. (defparameter *dispatch*
  147. `(("reset" . reset)
  148. ("add_fun" . add-fun)
  149. ("map_doc" . map-doc)
  150. ("reduce" . reduce-results)
  151. ("rereduce" . rereduce)
  152. ;; TODO - everything below here has changed.
  153. ;; CouchDB now uses some sort of 'DDoc'
  154. ;; thing, so this needs updating.
  155. ;; ("filter" . ,#'filter)
  156. ;; ("validate" . ,#'validate)
  157. ;; ("show" . ,#'show)
  158. ;; ("update" . ,#'update)
  159. ;; ("list" . couch-list)
  160. )
  161. "Dispatch table holding Couch command -> Chillax function associations.")
  162. (defun run-server (&aux *functions*
  163. (black-hole (make-broadcast-stream))
  164. (*error-output* black-hole)
  165. (*debug-io* black-hole)
  166. (*trace-output* black-hole))
  167. "Toplevel function that parses view requests from CouchDB and sends responses back."
  168. (loop for line = (read-line *standard-input* nil nil)
  169. while (< 0 (length line))
  170. for (name . args) = (funcall *decode-json* line) do
  171. (handler-case
  172. (let ((dispatch-result (assoc name *dispatch* :test #'string=)))
  173. (if dispatch-result
  174. (apply (cdr dispatch-result) args)
  175. (progn
  176. (log-message "Unknown message: ~A" name)
  177. (respond (mkhash "error" "unknown_message"
  178. "reason" "Received an unknown message from CouchDB")))))
  179. (end-of-file () (return-from run-server nil))
  180. (error (e)
  181. (respond (mkhash "error" (princ-to-string (type-of e))
  182. "reason" (remove #\Newline (princ-to-string e))))))))