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 (update-in object [:walkmap.tag/tags] difference (set tags'))))