2 ;;; Inspiration: http://svn.automattic.com/wordpress/trunk/wp-includes/taxonomy.php
4 ;;;; Copyright (c) 2008, Maciej Pasternacki <maciej@pasternacki.net>
5 ;;;; All rights reserved. This file is available on the terms
6 ;;;; detailed in COPYING file included with it.
8 (defpackage #:trane-taxonomy
9 (:use
#:common-lisp
#:trane-common
#:iterate
#:postmodern
)
10 (:export
#:taxonomy
#:taxonomy-name
#:taxonomy-slug
#:site-dao-class
11 #:item-dao-class
#:taxonomy-value-mixin
12 #:taxonomy-cl-store-value-mixin
#:valued-taxonomy
13 #:cl-store-valued-taxonomy
#:deftaxonomy
#:ensure-taxonomy
14 #:ensure-term
#:term
#:term-text
15 #:slug
#:term-site-id
#:term-taxonomy-name
16 #:description
#:parent-id
#:term-taxonomy
#:term-site
17 #:id
#:find-terms
#:new-term
#:apply-term
18 #:term-value
#:item-terms
19 #:unbind-term
:term-item-ids
#:term-items
))
21 (in-package #:trane-taxonomy
)
23 (defvar *taxonomies
* nil
24 "A list of defined taxonomies."
25 ;; It is supposed to be just a few of them, hash table overhead
30 ((name :initarg
:name
:reader taxonomy-name
)
31 (site-dao-class :initarg
:site-dao-class
:reader site-dao-class
)
32 (item-dao-class :initarg
:item-dao-class
:reader item-dao-class
))
33 (:documentation
"Base taxonomy class.
35 Taxonomy has a name (symbol or string, which is transformed to a
36 keyword by DEFTAXONOMY), and refers to DAO classes of taxonomy's
37 SITE and ITEM. DAO classes are required to have an integer
38 primary key, accessible by reader named ID."))
40 (defmethod print-object ((taxonomy taxonomy
) stream
)
41 (print-unreadable-object (taxonomy stream
:type t
:identity t
)
42 (princ (taxonomy-name taxonomy
) stream
)))
44 (defun taxonomy-slug (taxonomy)
45 "Return name of taxonomy for database or URL (lowercased string).
47 Applicable to taxonomy objects, symbols and strings."
48 (string-downcase (string (if (or (stringp taxonomy
)
51 (taxonomy-name taxonomy
)))))
53 (defclass valued-taxonomy
(taxonomy)
54 ((value-encoder-function :initarg
:encoder
:reader value-encoder-function
:initform
#'identity
)
55 (value-decoder-function :initarg
:decoder
:reader value-decoder-function
:initform
#'identity
))
56 (:documentation
"Taxonomy where items applied to terms may have values.
58 Defines encoder and decoder function slots, which are responsible
59 for translating value from a Lisp value to DB-safe string. By
60 default it is identity function, which means values need to be
63 #| FIXME
:base64
/cl-store
/flex
64 (defclass cl-store-valued-taxonomy
(valued-taxonomy)
66 (:default-initargs
:encoder
#'store-to-base64
:decoder
#'restore-from-base64
)
67 (:documentation
"Valued taxonomy that by default encodes/decodes almost any Lisp object with CL-STORE as BASE64 string."))
70 (defun encode-value (taxonomy value
)
71 "Encode VALUE for database, as TAXONOMY specifies.
73 TAXONOMY should be a VALUED-TAXONOMY instance, and VALUE should
74 be any value supported by the taxonomy. When VALUE is NIL, it is
75 encoded as database NULL."
77 (funcall (value-encoder-function taxonomy
)
81 (defun decode-value (taxonomy value
)
82 "Decode VALUE from database, as TAXONOMY specifies.
84 TAXONOMY should be a VALUED-TAXONOMY instance."
85 (unless (eq :null value
)
86 (funcall (value-decoder-function taxonomy
) value
)))
88 (defmacro deftaxonomy
(name (&optional
(class 'taxonomy
)) &rest args
)
89 "Defines taxonomy named NAME, with class CLASS and initargs ARGS, and remembers it in *TAXONOMIES*.
91 NAME is symbol or string, which will be transformed to a keyword anyway."
93 (cons (make-instance ',class
94 :name
,(make-keyword name
)
96 (delete ,(make-keyword name
) *taxonomies
*
97 :key
#'taxonomy-name
))))
99 (defun ensure-taxonomy (taxonomy)
100 "If TAXONOMY is a taxonomy object, return it, otherwise find and return taxonomy named TAXONOMY."
101 (if (subtypep (class-of taxonomy
) (find-class 'taxonomy
))
103 (find (make-keyword taxonomy
) *taxonomies
*
104 :key
#'taxonomy-name
)))
107 ((term-id :col-type integer
:reader id
)
108 (term-text :col-type varchar
:initarg
:text
:accessor term-text
:documentation
"Full text of term")
109 (term-slug :col-type varchar
:accessor slug
:documentation
"URL-friendly version of term text, initially chosen by database")
110 (site-id :col-type integer
:reader term-site-id
:initarg
:site-id
:documentation
"ID of term's site")
111 (term-taxonomy :col-type
(varchar 32) :reader term-taxonomy-name
:initarg
:taxonomy-name
:documentation
"Name of term's taxonomy")
112 (term-description :col-type text
:accessor description
:initarg
:description
:documentation
"Textual description of a term"))
113 (:metaclass dao-class
)
115 (:documentation
"Class for a term associated with taxonomy and a site."))
117 (defmethod print-object ((term term
) stream
)
118 (print-unreadable-object (term stream
:type t
:identity t
)
119 (princ (slug term
) stream
)))
121 (defun taxonomy (term)
122 "TERM's taxonomy object"
123 (ensure-taxonomy (term-taxonomy-name term
)))
125 (defun term-site (term)
126 "TERM's site object, if TERM is associated with a SITE."
127 (let ((id (term-site-id term
)))
129 (get-dao (site-dao-class (taxonomy term
)) id
))))
131 (defun ensure-term (taxonomy site
&key text slug create-p
)
132 "Find or create term in taxonomy TAXONOMY for site SITE.
134 TEXT is a full text of term; if TEXT is given, CREATE-P is
135 non-NIL and term is not found, new term is inserted into
138 If SLUG is given instead of TEXT, only search is possible, not
140 #+b0rken
(assert (and (or text slug
)
141 (not (and text slug
)))
143 "Either TEXT or SLUG should be given, but not both.")
147 (query-dao 'term
(:select
'* :from
(:ensure-term text
(or slug
(slugify text
)) (id site
) (taxonomy-slug taxonomy
))))
148 (select-dao 'term
(:and
(:= 'term-text text
)
149 (:= 'site-id
(id site
))
150 (:= 'term-taxonomy
(taxonomy-slug taxonomy
)))))
153 (warn "CREATE-P is meaningful only with TEXT defined."))
154 (select-dao 'term
(:and
(:= 'term-slug slug
)
155 (:= 'site-id
(id site
))
156 (:= 'term-taxonomy
(taxonomy-slug taxonomy
))))))))
158 (defun new-term (taxonomy site text
&optional parent
)
159 "Create new term in SITE for TAXONOMY, with full name TEXT."
160 (first (query-dao 'term
162 (:new-term text
(id site
) (taxonomy-slug taxonomy
) (if parent
(slug parent
) :null
))))))
164 (defun find-terms-where-clause (&key taxonomy site text slug
167 (push (list := 'term-taxonomy
(taxonomy-slug taxonomy
))
171 (push (list := 'site-id
(id site
))
175 (push (list := 'term-text text
) query
))
178 (push (list := 'term-slug slug
) query
))
180 (if (> (length query
) 1)
182 (setf query
(first query
)))
186 (defun find-terms (&key taxonomy site text slug
)
187 "Find list of terms satisfying given keywords."
189 (sql-compile `(:order-by
(select '* :from
'term
190 :where
,(find-terms-where-clause :taxonomy taxonomy
196 (defun apply-term (item term
&optional value
)
197 "Apply TERM to ITEM, optionally setting its value to VALUE."
198 (query (:insert-into
'item-term
:set
201 'item-term-value
(encode-value (taxonomy term
) value
))))
203 (defun term-value (item term
204 &aux
(encoded (query (:select
'item-term-value
:from
'item-term
205 :where
(:and
(:= 'item-id
(id item
))
206 (:= 'term-id
(id term
))))
208 "Returns value that association of ITEM and TERM is set to.
210 As a second value returns T if an association was found at all,
211 NIL otherwise. This makes it possible to tell between an
212 association with a NIL value and no association at all."
214 (values (decode-value (taxonomy term
) encoded
) t
)
217 (defun (setf term-value
) (new-value item term
)
218 "Set new value for association of ITEM and TERM.
220 New association between ITEM and TERM is established if it was
222 (query (:select
(:set-item-term-value
(id item
) (id term
)
223 (encode-value (taxonomy term
) new-value
)))
226 (defun unbind-term (item term
)
227 "Deletes association between ITEM and TERM."
228 (query (:delete-from
'item-term
:where
229 (:and
(:= 'item-id
(id item
))
230 (:= 'term-id
(id term
))))))
232 (defun item-terms (item &optional taxonomy
)
233 "List TERMs associated with ITEM in given TAXONOMY.
235 If TAXONOMY is not given, returns terms in all taxonomies."
238 `(:select
'term.
* :from
'term
'item-term
:where
239 (:and
,@(when taxonomy
242 (taxonomy-slug taxonomy
))))
243 (:= 'item-term.item-id
,(id item
))
244 (:= 'term.term-id
'item-term.term-id
))))))
246 (defun term-item-ids (term)
247 "IDs of items associated with given TERM."
248 (query (:select
'item-id
:from
'item-term
:where
(:= 'term-id
(id term
)))
251 (defun term-items (term &aux
(class (item-dao-class (taxonomy (if (integerp term
) ; FIXME
254 "Items associated with given TERM."
255 (mapcar #'(lambda (id)
257 (term-item-ids term
)))
259 #+cl-trane.taxonomy.use-cl-store
261 (defun store-to-base64 (obj)
262 (base64:usb8-array-to-base64-string
263 (flex:with-output-to-sequence
(s)
264 (cl-store:store obj s
))))
266 (defun restore-from-base64 (b64)
267 (flex:with-input-from-sequence
(s (base64:base64-string-to-usb8-array b64
))
268 (cl-store:restore s
)))
270 (defclass cl-store-valued-taxonomy
(valued-taxonomy)
272 (:default-initargs
:encoder
#'store-to-base64
:decoder
#'restore-from-base64
)
273 (:documentation
"Valued taxonomy that by default encodes/decodes almost any Lisp object with CL-STORE as BASE64 string.")))