travels.lisp 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232
  1. (in-package :travels)
  2. (defun update-indexes (storage old new)
  3. (let ((id (or (getf old :|id|) (getf new :|id|)))
  4. (old-user (getf old :|user|))
  5. (new-user (getf new :|user|))
  6. (old-loc (getf old :|location|))
  7. (new-loc (getf new :|location|)))
  8. (labels ((update (s o n)
  9. (when o (setf (gethash o s) (remove id (gethash o s))))
  10. (when n (push id (gethash n s)))))
  11. (when (or (null new)
  12. (null old)
  13. (and new-user (not (= old-user new-user))))
  14. (update (or (gethash :user-visits storage)
  15. (setf (gethash :user-visits storage) (make-hash-table)))
  16. old-user new-user))
  17. (when (or (null new) (null old)
  18. (and new-loc (not (= old-loc new-loc))))
  19. (update (or (gethash :location-visits storage)
  20. (setf (gethash :location-visits storage) (make-hash-table)))
  21. old-loc new-loc)))))
  22. (defun load-data (pathname)
  23. (let ((storage (make-hash-table)))
  24. (zip:with-zipfile (zip pathname)
  25. (zip:do-zipfile-entries (name entry zip)
  26. (let ((data (jojo:parse
  27. (flex:octets-to-string (zip:zipfile-entry-contents entry) :external-format :utf-8)
  28. :as :plist)))
  29. (destructuring-bind (entity elements) data
  30. (let ((entity-storage
  31. (or (gethash entity storage)
  32. (setf (gethash entity storage)
  33. (make-hash-table :size (length elements))))))
  34. (loop for item in elements
  35. do (setf (gethash (getf item :|id|) entity-storage) item)
  36. when (eq entity :|visits|)
  37. do (update-indexes storage nil item)))))))
  38. storage))
  39. (defvar *storage* nil "data store")
  40. (let ((keyword-package (find-package :keyword)))
  41. (defun getkey (place indicator)
  42. (declare
  43. (optimize (speed 3) (safety 0))
  44. (type list place)
  45. (type symbol indicator))
  46. (intern (getf place indicator) keyword-package)))
  47. (defparameter +400+ '(400 nil nil))
  48. (defparameter +404+ '(404 nil nil))
  49. (defparameter +200-empty+ '(200 (:content-type "application/json") ("{}")))
  50. (defun get-entity (params)
  51. (let ((id (parse-integer (getf params :id) :junk-allowed t))
  52. (entity (getkey params :entity)))
  53. (if id
  54. (case entity
  55. ((:|users| :|visits| :|locations|)
  56. (let ((entity (gethash id (gethash entity *storage*))))
  57. (if entity
  58. `(200 (:content-type "application/json") (,(jojo:to-json entity)))
  59. +404+)))
  60. (otherwise +404+))
  61. +404+)))
  62. (defun post-entity (params)
  63. (let ((id (parse-integer (getf params :id) :junk-allowed t))
  64. (entity (getkey params :entity)))
  65. (if (or id (string= (getf params :id) "new"))
  66. (case entity
  67. ((:|users| :|visits| :|locations|)
  68. (handler-case
  69. (let ((body (make-string (getf myway:*env* :content-length))))
  70. (read-sequence body (flex:make-flexi-stream
  71. (getf myway:*env* :raw-body)
  72. :external-format :utf8))
  73. (let ((item (jojo:parse body))
  74. (entity-storage (gethash entity *storage*)))
  75. (if id
  76. (let ((existing (gethash id entity-storage)))
  77. (if existing
  78. (progn
  79. (when (eq entity :|visits|)
  80. (update-indexes *storage* existing item))
  81. (loop for (k v) on item by #'cddr
  82. unless (eq k :|id|)
  83. do (setf (getf existing k) v))
  84. +200-EMPTY+)
  85. +404+))
  86. (progn
  87. (when (eq entity :|visits|)
  88. (update-indexes *storage* nil item))
  89. (setf (gethash (getf item :|id|) entity-storage) item)
  90. +200-EMPTY+))))
  91. (error () +400+)))
  92. (otherwise +404+))
  93. +400+)))
  94. (defun aget (place indicator &key (test #'equal))
  95. (declare (type list place)
  96. (type (or string symbol) indicator)
  97. (type function test)
  98. (optimize (speed 3) (safety 0)))
  99. (cdr (assoc indicator place :test test)))
  100. (defun may-integer (string)
  101. (declare (type (or null string) string))
  102. (when string
  103. (parse-integer string)))
  104. (defun matching-location (location country to-distance)
  105. (and (or (not country) (equal (getf location :|country|) country))
  106. (or (not to-distance) (< (getf location :|distance|) to-distance))))
  107. (defun matching-visit (visit from-date to-date)
  108. (and (or (not from-date) (> (getf visit :|visited_at|) from-date))
  109. (or (not to-date) (< (getf visit :|visited_at|) to-date))))
  110. (defun user-visits (params)
  111. (let ((id (parse-integer (getf params :id) :junk-allowed t)))
  112. (if id
  113. (let ((user (gethash id (gethash :|users| *storage*))))
  114. (if user
  115. (handler-case
  116. (let* ((user-visit-ids (gethash id (gethash :user-visits *storage*)))
  117. (locations (gethash :|locations| *storage*))
  118. (visits (gethash :|visits| *storage*))
  119. (query-string (getf myway:*env* :query-string))
  120. (query-params (and query-string (quri:url-decode-params query-string))))
  121. (let ((from-date (may-integer (aget query-params "fromDate")))
  122. (to-date (may-integer (aget query-params "toDate")))
  123. (to-distance (may-integer (aget query-params "toDistance")))
  124. (country (aget query-params "country")))
  125. ;; TODO: Smarter indexes
  126. (let ((user-visits (loop for v-id in user-visit-ids
  127. for visit = (gethash v-id visits)
  128. for loc = (gethash (getf visit :|location|) locations)
  129. when (and visit (matching-visit visit from-date to-date))
  130. when (and loc (matching-location loc country to-distance))
  131. collect (list :|mark| (getf visit :|mark|)
  132. :|visited_at| (getf visit :|visited_at|)
  133. :|place| (getf loc :|place|)))))
  134. (sort user-visits #'< :key (lambda (v) (getf v :|visited_at|)))
  135. `(200 (:content-type "application/json") (,(jojo:to-json (list :|visits| user-visits)))))))
  136. (error () +400+))
  137. +404+))
  138. +404+)))
  139. (defun smart-f (arg &optional digits)
  140. (with-output-to-string (s)
  141. (prin1 (cond ((= (round arg) arg) (round arg))
  142. (digits (float (/ (round (* arg (expt 10 digits)))
  143. (expt 10 digits))))
  144. (t arg))
  145. s)))
  146. (defvar *unix-epoch-difference*
  147. (encode-universal-time 0 0 0 1 1 1970 0))
  148. (defvar *year*
  149. (round (* 60 60 24 365.25)))
  150. (defun universal-to-unix-time (universal-time)
  151. (- universal-time *unix-epoch-difference*))
  152. (defun unix-to-universal-time (unix-time)
  153. (+ unix-time *unix-epoch-difference*))
  154. (defun get-unix-time ()
  155. (universal-to-unix-time (get-universal-time)))
  156. (defmethod jojo::%to-json ((ratio ratio))
  157. (jojo:%write-string (smart-f ratio 5)))
  158. (defun matching-user (user from-age to-age gender now)
  159. (let ((age (/ (- now (getf user :|birth_date|)) *year*)))
  160. (and (or (not gender) (equal (getf user :|gender|) gender))
  161. (or (not from-age) (> age from-age))
  162. (or (not to-age) (< age to-age)))))
  163. (defun location-avg-mark (params)
  164. (let ((id (parse-integer (getf params :id) :junk-allowed t)))
  165. (if id
  166. (let ((location (gethash id (gethash :|locations| *storage*))))
  167. (if location
  168. (handler-case
  169. (let* ((location-visit-ids (gethash id (gethash :location-visits *storage*)))
  170. (users (gethash :|users| *storage*))
  171. (visits (gethash :|visits| *storage*))
  172. (query-string (getf myway:*env* :query-string))
  173. (query-params (and query-string (quri:url-decode-params query-string))))
  174. (let ((from-date (may-integer (aget query-params "fromDate")))
  175. (to-date (may-integer (aget query-params "toDate")))
  176. (now (get-unix-time))
  177. (from-age (may-integer (aget query-params "fromAge")))
  178. (to-age (may-integer (aget query-params "toAge")))
  179. (gender (aget query-params "gender")))
  180. (let ((marks (loop for v-id in location-visit-ids
  181. for visit = (gethash v-id visits)
  182. for user = (gethash (getf visit :|user|) users)
  183. when (and visit (matching-visit visit from-date to-date))
  184. when (and user (matching-user user from-age to-age gender now))
  185. collect (getf visit :|mark|))))
  186. `(200 (:content-type "application/json")
  187. (,(jojo:to-json (list :|avg|
  188. (if marks
  189. (/ (apply #'+ marks) (length marks))
  190. 0.0))))))))
  191. (error () +400+))
  192. +404+))
  193. +404+)))
  194. (defvar *mapper* (myway:make-mapper))
  195. (myway:connect *mapper* "/:entity/:id" 'post-entity :method :post)
  196. (myway:connect *mapper* "/:entity/:id" 'get-entity)
  197. (myway:connect *mapper* "/users/:id/visits" 'user-visits)
  198. (myway:connect *mapper* "/locations/:id/avg" 'location-avg-mark)
  199. (myway:connect *mapper* "*" (lambda (p) (declare (ignore p)) '(404 nil nil)))
  200. (defun main (&key (port 5000) (data "data.zip") (address "0.0.0.0"))
  201. (setf *storage* (load-data data))
  202. (format t "Loaded ~A~%" data)
  203. (clack:clackup (myway:to-app *mapper*)
  204. :server :woo
  205. :address address
  206. :port port
  207. :debug nil
  208. :use-default-middlewares nil
  209. :use-thread nil))