001  (ns walkmap.tag
002    "Code for tagging, untagging, and finding tags on objects. Note the use of
003    the namespaced keyword, `:walkmap.tag/tags`, denoted in this file `::tags`.
004    This is in an attempt to avoid name clashes with other uses of this key."
005    (:require [clojure.set :refer [difference union]]
006              [taoensso.timbre :as l]
007              [walkmap.utils :refer [kind-type]]))
008  
009  (defn tagged?
010    "True if this `object` is tagged with each of these `tags`. It is an error
011    (and an exception will be thrown) if
012  
013    1. `object` is not a map;
014    2. any of `tags` is not a keyword."
015    [object & tags]
016    (when-not (map? object)
017      (throw (IllegalArgumentException.
018               (str "Must be a map: " (kind-type object)))))
019    (let [tags' (flatten tags)]
020      (when-not (every? keyword? tags')
021        (throw (IllegalArgumentException.
022                 (str "Must be keywords: " (map kind-type tags')))))
023        (let [ot (::tags object)]
024          (and
025            (set? ot)
026            (every? ot tags')))))
027  
028  (defn tag
029    "Return an object like this `object` but with these `tags` added to its tags,
030    if they are not already present. It is an error (and an exception will be
031    thrown) if
032  
033    1. `object` is not a map;
034    2. any of `tags` is not a keyword or sequence of keywords.
035  
036    It's legal to include sequences of keywords in `tags`, so that users can do
037    useful things like `(tag obj (map keyword some-strings))`."
038    [object & tags]
039    (l/debug "Tagging" (kind-type object) "with" tags)
040    (when-not (map? object)
041      (throw (IllegalArgumentException.
042               (str "Must be a map: " (kind-type object)))))
043    (let [tags' (flatten tags)]
044      (when-not (every? keyword? tags')
045        (throw (IllegalArgumentException.
046                 (str "Must be keywords: " (map kind-type tags')))))
047          (assoc object ::tags (union (set tags') (::tags object)))))
048  
049  (defmacro tags
050    "Return the tags of this object, if any."
051    [object]
052    `(::tags ~object))
053  
054  (defn untag
055    "Return an object like this `object` but with these `tags` removed from its
056    tags, if present. It is an error (and an exception will be thrown) if
057  
058    1. `object` is not a map;
059    2. any of `tags` is not a keyword or sequence of keywords."
060    [object & tags]
061    (when-not (map? object)
062      (throw (IllegalArgumentException.
063               (str "Must be a map: " (kind-type object)))))
064    (let [tags' (flatten tags)]
065      (when-not (every? keyword? tags')
066        (throw (IllegalArgumentException.
067                 (str "Must be keywords: " (map kind-type tags')))))
068      (assoc object ::tags (difference (::tags object) (set tags')))))