Skip to content

Instantly share code, notes, and snippets.

@simon-brooke
Created May 26, 2020 16:12
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save simon-brooke/47f44b6589222710bf817c0a23043514 to your computer and use it in GitHub Desktop.
Save simon-brooke/47f44b6589222710bf817c0a23043514 to your computer and use it in GitHub Desktop.
Tagging in Clojure
(ns walkmap.tag
"Code for tagging, untagging, and finding tags on objects. Note the use of
the namespaced keyword, `:walkmap.tag/tags`, denoted in this file `::tags`.
This is in an attempt to avoid name clashes with other uses of this key."
(:require [clojure.set :refer [difference union]]))
(defn tagged?
"True if this `object` is tagged with each of these `tags`."
[object & tags]
(if
(map? object)
(if
(every? keyword? tags)
(let [ot (::tags object)]
(and
(set? ot)
(every? ot tags)
true))
(throw (IllegalArgumentException.
(str "Must be keyword(s): " (map type tags)))))
(throw (IllegalArgumentException.
(str "Must be a map: " (type object))))))
(defn tag
"Return an object like this `object` but with these `tags` added to its tags,
if they are not already present."
[object & tags]
(if
(map? object)
(if
(every? keyword? tags)
(assoc object ::tags (union (set tags) (::tags object)))
(throw (IllegalArgumentException.
(str "Must be keyword(s): " (map type tags)))))
(throw (IllegalArgumentException.
(str "Must be a map: " (type object))))))
(defn untag
"Return an object like this `object` but with these `tags` removed from its
tags, if present."
[object & tags]
(if
(map? object)
(if
(every? keyword? tags)
(assoc object ::tags (difference (::tags object) (set tags)))
(throw (IllegalArgumentException.
(str "Must be keywords: " (map type tags)))))
(throw (IllegalArgumentException.
(str "Must be a map: " (type object))))))
(ns walkmap.tag-test
(:require [clojure.test :refer :all]
[walkmap.tag :refer :all]))
(deftest tag-tests
(testing "Tagging"
(is (set? (:walkmap.tag/tags (tag {} :foo :bar :ban :froboz)))
"The value of `:walkmap.tag/tags should be a set.")
(is (= (count (:walkmap.tag/tags (tag {} :foo :bar :ban :froboz))) 4)
"All the tags passed should be added.")
(is (:walkmap.tag/tags (tag {} :foo :bar :ban :froboz) :ban)
"`:ban` should be present in the set, and, as it is a set, it
should be valid to apply it to a keyword.")
(is (not ((:walkmap.tag/tags (tag {} :foo :bar :ban :froboz)) :cornflakes))
"`:cornflakes should not be present.")
(is (true? (tagged? (tag {} :foo :bar :ban :froboz) :bar))
"`tagged?` should return an explicit `true`, not any other value.")
(is (tagged? (tag {} :foo :bar :ban :froboz) :bar :froboz)
"We should be able to test for the presence of more than one tag")
(is (= (tagged? (tag {} :foo :bar :ban :froboz) :bar :cornflakes) false)
"If any of the queried tags is missing, false should be returned")
(is (tagged? (tag (tag {} :foo) :bar) :foo :bar)
"We should be able to add tags to an already tagged object")
(is (false? (tagged? (tag {} :foo :bar) :cornflakes))
"`tagged?` should return an explicit `false` if a queried tag is missing.")
(let [object (tag {} :foo :bar :ban :froboz)]
(is (= (untag object :cornflakes) object)
"Removing a missing tag should have no effect.")
(is (tagged? (untag object :foo) :bar :ban :froboz)
"All tags not explicitly removed should still be present.")
(is (false? (tagged? (untag object :bar) :bar))
"But the tag which has been removed should be removed."))
(is (thrown? IllegalArgumentException (tag [] :foo))
"An exception should be thrown if `object` is not a map: `tag`.")
(is (thrown? IllegalArgumentException (tagged? [] :foo))
"An exception should be thrown if `object` is not a map: `tagged?`.")
(is (thrown? IllegalArgumentException (untag [] :foo))
"An exception should be thrown if `object` is not a map: `untag`.")
(is (thrown? IllegalArgumentException (tag {} :foo "bar" :ban))
"An exception should be thrown if any of `tags` is not a keyword: `tag`.")
(is (thrown? IllegalArgumentException (tagged? {} :foo "bar" :ban))
"An exception should be thrown if any of `tags` is not a keyword: `tagged?`.")
(is (thrown? IllegalArgumentException (untag {} :foo "bar" :ban))
"An exception should be thrown if any of `tags` is not a keywordp: `untag`.")))
@simon-brooke
Copy link
Author

There are lots of ways to tag things in Clojure, but I wanted to ensure that tags didn't collide with other keys. I'm rather pleased with this solution.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment