| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295 |
- (in-package :travels)
- (defun update-indexes (storage old new)
- (declare (optimize (speed 3) (safety 0))
- (type hash-table storage)
- (type (or null list) old)
- (type (or null list) new))
- (let ((id (or (getf old :|id|) (getf new :|id|)))
- (old-user (the (or null fixnum) (getf old :|user|)))
- (new-user (the (or null fixnum) (getf new :|user|)))
- (old-loc (the (or null fixnum) (getf old :|location|)))
- (new-loc (the (or null fixnum) (getf new :|location|))))
- (labels ((update (s o n)
- (when o (setf (gethash o s) (remove id (gethash o s))))
- (when n (push id (gethash n s)))))
- (when (or (null new)
- (null old)
- (and new-user (not (= old-user new-user))))
- (update (or (gethash :user-visits storage)
- (setf (gethash :user-visits storage) (make-hash-table)))
- old-user new-user))
- (when (or (null new) (null old)
- (and new-loc (not (= old-loc new-loc))))
- (update (or (gethash :location-visits storage)
- (setf (gethash :location-visits storage) (make-hash-table)))
- old-loc new-loc)))))
- (defun loads (bytes)
- (declare (optimize (speed 3) (safety 0))
- (type (simple-array (unsigned-byte 8) (*)) bytes))
- (jojo:parse (trivial-utf-8:utf-8-bytes-to-string bytes)))
- (defun dumps (data)
- (declare (optimize (speed 3) (safety 0))
- (type (or null list) data))
- (trivial-utf-8:string-to-utf-8-bytes (jojo:to-json data)))
- (defun load-data (pathname)
- (declare (optimize (speed 3) (safety 0)))
- (let ((storage (make-hash-table)))
- (zip:with-zipfile (zip pathname)
- (zip:do-zipfile-entries (name entry zip)
- (let ((data (loads (zip:zipfile-entry-contents entry))))
- (destructuring-bind (entity elements) data
- (let ((entity-storage
- (or (gethash entity storage)
- (setf (gethash entity storage)
- (make-hash-table :size (length (the list elements)))))))
- (loop for item in elements
- do (setf (gethash (getf item :|id|) entity-storage)
- (dumps item))
- when (eq entity :|visits|)
- do (update-indexes storage nil item)))))))
- storage))
- (defvar *storage* nil "data store")
- (defvar *storage-users* nil "users store")
- (defvar *storage-locations* nil "locations store")
- (defvar *storage-visits* nil "visits store")
- (let ((keyword-package (find-package :keyword)))
- (defun getkey (place indicator)
- (declare
- (optimize (speed 3) (safety 0))
- (type list place)
- (type symbol indicator))
- (intern (getf place indicator) keyword-package)))
- (defparameter +400+ '(400 nil #.(trivial-utf-8:string-to-utf-8-bytes "400")))
- (defparameter +404+ '(404 nil #.(trivial-utf-8:string-to-utf-8-bytes "404")))
- (defparameter +200-empty+ '(200 (:content-type "application/json")
- #.(trivial-utf-8:string-to-utf-8-bytes "{}")))
- (defvar *200* '(200 (:content-type "application/json") content))
- (defun 200-json (data) (setf (caddr *200*) (dumps data)) *200*)
- (defun get-entity (params)
- (let ((id (parse-integer (getf params :id) :junk-allowed t))
- (entity (getkey params :entity)))
- (if id
- (case entity
- ((:|users| :|visits| :|locations|)
- (let ((entity (gethash id (gethash entity *storage*))))
- (if entity
- `(200 (:content-type "application/json") ,entity)
- +404+)))
- (otherwise +404+))
- +404+)))
- (defun validate-item (item)
- (declare (type (or null list) item)
- (optimize (speed 3) (safety 0)))
- (when item
- (loop for (k v) on item by #'cddr
- unless v return nil
- ; when (and (eq k :|id|) (not (integerp v))) return nil
- ; when (and (eq k :|distance|) (not (integerp v))) return nil
- ; when (and (eq k :|visited_at|) (not (integerp v))) return nil
- ; when (and (eq k :|country|) (or (not (stringp v)) (> (length v) 50))) return nil
- ; when (and (eq k :|city|) (or (not (stringp v)) (> (length v) 50))) return nil
- ; when (and (eq k :|mark|) (or (not (integerp v)) (not (<= 0 (the fixnum v) 5)))) return nil
- ; when (and (eq k :|user|) (or (not (integerp v)) (not (gethash v *storage-users*)))) return nil
- ; when (and (eq k :|location|) (or (not (integerp v)) (not (gethash v *storage-locations*)))) return nil
- finally (return t))))
- (defun post-entity (params)
- (declare (optimize (speed 3) (safety 0)))
- (let ((id (parse-integer (getf params :id) :junk-allowed t))
- (entity (getkey params :entity)))
- (if (or id (string= (the simple-string (getf params :id)) "new"))
- (case entity
- ((:|users| :|visits| :|locations|)
- (handler-case
- (let* ((body (trivial-utf-8:read-utf-8-string
- (getf myway:*env* :raw-body)
- :stop-at-eof t))
- (item (jojo:parse (coerce body 'simple-string)))
- (entity-storage (gethash entity *storage*)))
- (if (validate-item item)
- (let ((item-id (getf item :|id|)))
- (if id
- (if item-id +400+
- (let ((existing-raw (gethash id entity-storage)))
- (if existing-raw
- (let ((existing (loads existing-raw)))
- (when (eq entity :|visits|)
- (update-indexes *storage* existing item))
- (loop for (k v) on item by #'cddr
- do (setf (getf existing k) v))
- (setf (gethash id entity-storage)
- (dumps existing))
- +200-EMPTY+)
- +404+)))
- (let ((e (gethash item-id entity-storage)))
- (if e +400+
- (progn
- (when (eq entity :|visits|)
- (update-indexes *storage* nil item))
- (setf (gethash item-id entity-storage)
- (dumps item))
- +200-EMPTY+)))))
- +400+))
- ))
- (othewise +404+))
- +400+)))
- (defun aget (place indicator &key (test #'equal))
- (declare (type list place)
- (type (or string symbol) indicator)
- (type function test)
- (optimize (speed 3) (safety 0)))
- (cdr (assoc indicator place :test test)))
- (defun may-integer (string)
- (declare (type (or null string) string))
- (when string
- (parse-integer string)))
- (defun matching-location (location country to-distance)
- (and (or (not country) (equal (getf location :|country|) country))
- (or (not to-distance) (< (getf location :|distance|) to-distance))))
- (defun matching-visit (visit from-date to-date)
- (and (or (not from-date) (> (getf visit :|visited_at|) from-date))
- (or (not to-date) (< (getf visit :|visited_at|) to-date))))
- (defun user-visits (params)
- (declare (optimize (speed 3) (safety 0))
- (type list params))
- (let ((id (parse-integer (getf params :id) :junk-allowed t)))
- (if id
- (let ((user (gethash id *storage-users*)))
- (if user
- (handler-case
- (let* ((user-visit-ids (gethash id (gethash :user-visits *storage*)))
- (query-string (getf myway:*env* :query-string))
- (query-params (and query-string (quri:url-decode-params query-string))))
- (let ((from-date (may-integer (aget query-params "fromDate")))
- (to-date (may-integer (aget query-params "toDate")))
- (to-distance (may-integer (aget query-params "toDistance")))
- (country (the (or null string) (aget query-params "country"))))
- ;; (assert (or (null country) (<= (length country) 50)))
- ;; TODO: Smarter indexes
- (let ((user-visits (loop for v-id in user-visit-ids
- for visit = (loads (gethash v-id *storage-visits*))
- for loc = (loads (gethash (getf visit :|location|) *storage-locations*))
- when (and visit (matching-visit visit from-date to-date))
- when (and loc (matching-location loc country to-distance))
- collect (list :|mark| (getf visit :|mark|)
- :|visited_at| (getf visit :|visited_at|)
- :|place| (getf loc :|place|)))))
- (200-json (list :|visits|
- (sort (the list user-visits)
- #'< :key (lambda (v) (getf v :|visited_at|))))))))
- (error () +400+))
- +404+))
- +404+)))
- (defun smart-f (arg &optional digits)
- (with-output-to-string (s)
- (prin1 (cond ((= (round arg) arg) (round arg))
- (digits (float (/ (round (* arg (expt 10 digits)))
- (expt 10 digits))))
- (t arg))
- s)))
- (defvar *unix-epoch-difference*
- (encode-universal-time 0 0 0 1 1 1970 0))
- (defvar *year*
- (round (* 60 60 24 365.25)))
- (defun universal-to-unix-time (universal-time)
- (- universal-time *unix-epoch-difference*))
- (defun unix-to-universal-time (unix-time)
- (+ unix-time *unix-epoch-difference*))
- (defun get-unix-time ()
- (universal-to-unix-time (get-universal-time)))
- (defmethod jojo::%to-json ((ratio ratio))
- (jojo:%write-string (smart-f ratio 5)))
- (defun matching-user (user from-age to-age gender now)
- (let ((age (/ (- now (getf user :|birth_date|)) *year*)))
- (and (or (not gender) (equal (getf user :|gender|) gender))
- (or (not from-age) (> age from-age))
- (or (not to-age) (< age to-age)))))
- (defun location-avg-mark (params)
- (declare (optimize (speed 3) (safety 0))
- (type list params))
- (let ((id (parse-integer (getf params :id) :junk-allowed t)))
- (if id
- (let ((location (gethash id (gethash :|locations| *storage*))))
- (if location
- (handler-case
- (let* ((location-visit-ids (gethash id (gethash :location-visits *storage*)))
- (query-string (getf myway:*env* :query-string))
- (query-params (and query-string (quri:url-decode-params query-string))))
- (let ((from-date (may-integer (aget query-params "fromDate")))
- (to-date (may-integer (aget query-params "toDate")))
- (now (get-unix-time))
- (from-age (may-integer (aget query-params "fromAge")))
- (to-age (may-integer (aget query-params "toAge")))
- (gender (the (or null string) (aget query-params "gender"))))
- (assert (or (null gender) (string= gender "m") (string= gender "f")))
- (multiple-value-bind (s c)
- (loop
- with sum-marks of-type fixnum = 0
- with count-marks of-type fixnum = 0
- for v-id in location-visit-ids
- for visit = (loads (gethash v-id *storage-visits*))
- for user = (loads (gethash (getf visit :|user|) *storage-users*))
- when (and visit (matching-visit visit from-date to-date))
- when (and user (matching-user user from-age to-age gender now))
- do (setf sum-marks (+ sum-marks (the fixnum (getf visit :|mark|)))
- count-marks (1+ count-marks))
- finally (return (values sum-marks count-marks)))
- (200-json (list :|avg| (if (zerop c) 0.0 (/ s c)))))))
- (error () +400+))
- +404+))
- +404+)))
- (defvar *mapper* (myway:make-mapper))
- (myway:connect *mapper* "/:entity/:id" 'post-entity :method :post)
- (myway:connect *mapper* "/:entity/:id" 'get-entity)
- (myway:connect *mapper* "/users/:id/visits" 'user-visits)
- (myway:connect *mapper* "/locations/:id/avg" 'location-avg-mark)
- (myway:connect *mapper* "*" (lambda (p) (declare (ignore p)) +404+))
- (defun main (&rest args
- &key
- (data "data.zip")
- (address "0.0.0.0")
- (port 5000)
- (debug nil)
- (use-thread nil) &allow-other-keys)
- (setf *storage* (load-data data))
- (setf *storage-users* (gethash :|users| *storage*)
- *storage-locations* (gethash :|locations| *storage*)
- *storage-visits* (gethash :|visits| *storage*))
- #+sbcl (sb-ext:gc)
- (room)
- (format t "Loaded ~A~%" data)
- (apply #'clack:clackup
- (myway:to-app *mapper*)
- :server :woo
- :address address
- :port port
- :debug debug
- :use-default-middlewares nil
- :use-thread use-thread
- (alexandria:remove-from-plist args :data :address :port :debug :use-thread)))
|