#lang scheme/base
(require (file "util.scm")
(file "record.scm")
(file "settings.scm")
"log.scm")
(provide store-rec!
delete-rec!
add-child-and-save!
remove-child-and-save!
load-children
load-descendants
contains-child?
load-rec
record-id-stored?
refresh
rec-rec-prop
load-where
load-one-where
fresh-rec-from-data
only-rec-of-type
is-descendant?
find-parent
find-ancestor
find-highest-ancestor
find-incoming-record
find-incoming-records
sort-recs-by
define-cache
define-type-cache
populate-caches
)
(define-syntax only-rec-of-type
(syntax-rules ()
((_ rec-id type (rec-iden) body ...)
(let ((rec-iden (load-rec rec-id)))
(if (and rec-iden (rec-type-is? rec-iden 'type))
(begin body ...)
(e "Unauthorized access."))))))
(define (store-rec! r)
(cache-store r)
(write-record! r)
r)
(define (delete-rec! r
#:children-props-to-delete (child-props '())
#:recur (recur #f))
(define (delete-chillins item)
(for-each (lambda (child-prop)
(for-each (lambda (child-id)
(let ((child (load-rec child-id)))
(cache-delete child)
(delete-file (abs-path-to-record child-id))
(when recur (delete-chillins child))))
(rec-child-prop item child-prop)))
child-props))
(cache-delete r)
(delete-file (abs-path-to-record (rec-id r)))
(delete-chillins r))
(define (load-rec id #:ensure (ensure '()))
(or (and-let* ((data (read-record-data id))
(result (rec-filter-where (list (make-rec data id)) ensure)))
(if (empty? result)
#f
(first result)))
(e "Record with id ~A does not exist in repository." id)))
(define (refresh rec)
(load-rec (rec-id rec)))
(define (add-child-and-save! parent prop child #:to-end (to-end #f))
(rec-add-child! parent prop child #:to-end to-end)
(store-rec! parent))
(define (remove-child-and-save! parent prop child)
(rec-remove-child! parent prop child)
(store-rec! parent))
(define (load-children parent prop)
(map load-rec (rec-child-prop parent prop)))
(define (load-descendants parent recursive-prop)
(let ((one-level (load-children parent recursive-prop)))
(append one-level (append-map (cut load-descendants <> recursive-prop) one-level))))
(define (rec-rec-prop rec prop)
(aand (rec-prop rec prop) (load-rec it)))
(define (contains-child? parent prop putative-child)
(any (cute string=? <> (rec-id putative-child)) (rec-child-prop parent prop)))
(define (fresh-rec-from-data data
#:stamp-time (stamp-time #f)
#:id-length (id-length #f)
#:id (id #f))
(let* ((id (or (assoc-val 'id data)
(cond (id id)
(id-length (fresh-id #:id-length id-length))
(else (fresh-id)))))
(rec (make-rec data id)))
(when stamp-time (rec-set-prop! rec 'created-at (current-seconds)))
rec))
(define (fresh-id #:id-length (id-length 5))
(let ((try (random-key-string id-length)))
(if (file-exists? (abs-path-to-record try))
(fresh-id #:id-length id-length)
try)))
(define (ignore-filename? filename-path)
(let ((ignore-regexps
(list
".svn"
"~$")))
(and (any (lambda (re) (pregexp-match re filename-path)) ignore-regexps) #t)))
(define (load-all-recs #:type (type-or-types #f))
(or (and type-or-types
(if (list? type-or-types)
(let ((lookups (map type-cache-get-records type-or-types)))
(if (any not lookups)
#f
lookups))
(type-cache-get-records type-or-types)))
(filter-map (lambda (filename-path)
(let ((filename (path->string filename-path)))
(and (not (ignore-filename? filename))
(and-let* ((rec (load-rec filename)))
(cond ((not type-or-types) rec)
((symbol? type-or-types)
(and (rec-type-is? rec type-or-types) rec))
((list? type-or-types)
(and (memq (rec-prop rec 'type) type-or-types) rec))
(else (e "Can't restrict where type(s) is '~A'."
type-or-types)))))))
(directory-list (setting *PATH_TO_DATA*)))))
(define (load-where (pairs '())
#:restricted-to (restricted-to #f)
#:type (type-or-types #f)
#:sort-by (sort-by #f)
#:compare (compare <)
#:equal-fn (equal-fn equal?) #:exactly-one (exactly-one #f)
#:limit (limit #f))
(let* ((result
(rec-filter-where (cond ((and restricted-to (list? restricted-to))
(map load-rec restricted-to))
(restricted-to
(filter restricted-to
(load-all-recs #:type type-or-types)))
(else (load-all-recs #:type type-or-types)))
pairs
#:equal-fn equal-fn))
(sorted (sort-recs-by result sort-by #:compare compare)))
(cond (exactly-one (if (empty? sorted) #f (first sorted)))
(limit (take-up-to sorted limit))
(else sorted))))
(define (sort-recs-by recs sort-by #:compare (compare-fn <))
(if sort-by
(sort recs (lambda (a b)
(if (procedure? sort-by)
(compare-fn (sort-by a) (sort-by b))
(compare-fn (rec-prop a sort-by)
(rec-prop b sort-by)))))
recs))
(define (load-one-where pairs #:restricted-to (restricted-to #f))
(let ((results (load-where pairs)))
(if (empty? results)
#f
(first results))))
(declare-setting *PATH_TO_DATA* (build-path (current-directory) "data"))
(define (write-record! r)
(let ((id (rec-id r)))
(call-with-output-file (abs-path-to-record id)
(lambda (port)
(write (rec-data r) port))
#:exists 'replace)))
(define (read-record-data id)
(let ((p (abs-path-to-record id)))
(and (file-exists? p)
(call-with-input-file p
(lambda (port)
(read port))))))
(define (record-stored? rec)
(record-id-stored? (rec-id rec)))
(define (record-id-stored? rec-id)
(file-exists? (abs-path-to-record rec-id)))
(define (abs-path-to-record id)
(unless (directory-exists? (setting *PATH_TO_DATA*))
(e "Can't find data directory '~A'. Current directory is ~A."
(setting *PATH_TO_DATA*) (current-directory)))
(build-path (setting *PATH_TO_DATA*) id))
(define (find-parent child-rec child-prop #:parent-type (parent-type #f))
(find-incoming-records child-rec child-prop
#:find-type parent-type
#:is-ptr-to-list #t
#:return-just-one-record #t))
(define (is-descendant? low-rec high-rec child-ptr-prop)
(any (cut same-rec? <> low-rec) (load-descendants high-rec child-ptr-prop)))
(define (find-incoming-record starting-rec ptr-prop
#:find-type (find-type #f))
(find-incoming-records starting-rec ptr-prop
#:find-type find-type
#:return-just-one-record #t))
(define (find-incoming-records starting-rec ptr-prop
#:find-type (find-type #f)
#:is-ptr-to-list (is-ptr-to-list #f)
#:return-just-one-record (just-one #f))
(let ((clookup (cache-lookup starting-rec find-type ptr-prop)))
(if clookup
(cond ((and just-one (not (empty? clookup))) (first clookup))
(just-one #f)
(else clookup))
(let ((start-rec-id (rec-id starting-rec))
(find-fn (if just-one find filter)))
(find-fn (lambda (outgoing-rec)
(if is-ptr-to-list
(member start-rec-id (rec-child-prop outgoing-rec ptr-prop))
(aand (rec-prop outgoing-rec ptr-prop)
(string=? it start-rec-id))))
(load-where #:type find-type))))))
(define (find-ancestor descendant-rec pred recursive-child-prop
#:ancestor-type (anc-type #f))
(if (pred descendant-rec)
descendant-rec
(let ((p (find-parent descendant-rec recursive-child-prop #:parent-type anc-type)))
(and p
(find-ancestor p pred recursive-child-prop #:ancestor-type anc-type)))))
(define (find-highest-ancestor descendant-rec recursive-child-prop
#:ancestor-type (anc-type #f))
(let ((p (find-parent descendant-rec recursive-child-prop #:parent-type anc-type)))
(if p
(find-highest-ancestor p recursive-child-prop #:ancestor-type anc-type)
descendant-rec)))
(define (robust-path->string str-or-path)
(if (string? str-or-path)
str-or-path
(path->string str-or-path)))
(define-struct cache-spec (load-fn store-fn delete-fn might-answer-fn lookup-fn ht))
(define-struct type-cache (type-name records) #:mutable)
(define *CACHE_SPECS* (list))
(define *TYPE_CACHES* (list))
(define (add-to-cache-specs! cs)
(set! *CACHE_SPECS* (cons cs *CACHE_SPECS*)))
(define (add-to-type-caches! tc)
(set! *TYPE_CACHES* (cons tc *TYPE_CACHES*)))
(define-syntax define-cache
(syntax-rules (<=)
((_ name prop-ptr <= (incoming-type))
(begin (define name (make-hash))
(add-to-cache-specs! (create-cache-spec name 'prop-ptr 'incoming-type
#:has-many-incoming #t))))
((_ name prop-ptr <= incoming-type)
(begin (define name (make-hash))
(add-to-cache-specs! (create-cache-spec name 'prop-ptr 'incoming-type))))))
(define-syntax define-type-cache
(syntax-rules ()
((_ type)
(add-to-type-caches! (make-type-cache 'type (list))))))
(define (create-cache-spec ht prop-ptr incoming-type
#:has-many-incoming (has-many-incoming #f))
(define (add! incoming-rec points-to-id)
(hash-set! ht points-to-id (cons (rec-id incoming-rec)
(hash-ref ht points-to-id '()))))
(define (remove! incoming-rec formerly-pointed-to-id)
(hash-set! ht formerly-pointed-to-id (removef (cute string=? <> (rec-id incoming-rec))
(hash-ref ht formerly-pointed-to-id))))
(define i-care? (cut rec-type-is? <> incoming-type))
(define (load-fn incoming-rec)
(when (i-care? incoming-rec)
(awhen (rec-prop incoming-rec prop-ptr)
(for-each (cut add! incoming-rec <>) (listify it)))))
(define (store-fn incoming-rec)
(when (i-care? incoming-rec)
(let ((old-ptrs (and (record-stored? incoming-rec)
(rec-prop (load-rec (rec-id incoming-rec)) prop-ptr)))
(new-ptrs (rec-prop incoming-rec prop-ptr)))
(unless (equal? old-ptrs new-ptrs)
(when old-ptrs
(for-each (cut remove! incoming-rec <>) (listify old-ptrs)))
(load-fn incoming-rec)))))
(define (delete-fn incoming-rec)
(when (i-care? incoming-rec)
(awhen (rec-prop incoming-rec prop-ptr)
(for-each (cut remove! incoming-rec <>) (listify it)))))
(define (might-answer-fn a-incoming-type a-prop-ptr)
(and (eq? incoming-type a-incoming-type)
(eq? prop-ptr a-prop-ptr)))
(define (lookup-fn a-rec a-incoming-type a-prop-ptr)
(hash-ref ht (rec-id a-rec) '()))
(make-cache-spec load-fn store-fn delete-fn might-answer-fn lookup-fn ht))
(define (cache-load rec)
(for-each (lambda (spec) ((cache-spec-load-fn spec) rec))
*CACHE_SPECS*)
(add-record-to-type-cache! rec))
(define (cache-store rec)
(for-each (lambda (spec) ((cache-spec-store-fn spec) rec))
*CACHE_SPECS*)
(add-record-to-type-cache! rec))
(define (cache-delete rec)
(for-each (lambda (spec) ((cache-spec-delete-fn spec) rec))
*CACHE_SPECS*)
(delete-record-from-type-cache! rec))
(define (cache-lookup starting-rec incoming-type prop-ptr)
(let ((appro-spec (find (lambda (spec)
((cache-spec-might-answer-fn spec) incoming-type prop-ptr))
*CACHE_SPECS*)))
(and appro-spec
(map load-rec
((cache-spec-lookup-fn appro-spec) starting-rec incoming-type prop-ptr)))))
(define (type-cache-get-records type-name)
(aand (find-type-cache-with-name type-name)
(type-cache-records it)))
(define (find-type-cache rec)
(find-type-cache-with-name (rec-type rec)))
(define (find-type-cache-with-name type-name)
(find (lambda (type-cache) (eq? type-name (type-cache-type-name type-cache)))
*TYPE_CACHES*))
(define (add-record-to-type-cache! rec)
(let ((id (rec-id rec)))
(awhen (find-type-cache rec)
(let ((recs-in-cache (type-cache-records it)))
(set-type-cache-records! it (cons rec
(if (record-id-stored? id)
(removef (cut id-is? <> id) recs-in-cache)
recs-in-cache)))))))
(define (delete-record-from-type-cache! rec)
(awhen (find-type-cache rec)
(let ((its-id (rec-id rec)))
(set-type-cache-records! it (removef (lambda (r) (string=? (rec-id r) its-id))
(type-cache-records it))))))
(define (populate-caches)
(server-log "Populating caches...")
(for-each cache-load (load-all-recs))
(server-log "Done populating cache."))