I’ve been experimenting with clojure.spec on a new web project, as a way of validating data coming in from the client. We’re using JSON API as the lingua franca between the server and client, which adds a not-insignificant amount of ceremony and overhead to the JSON payload. So once I got one namespace’s endpoints spec’ed correctly, the obvious next step was to generalize it with a function that looked like this (edited for brevity):
(defn request-spec
[type]
(let [api-ns (str "my-project.api." type)
db-ns (str "my-project.db." type)]
(s/def (keyword api-ns "attributes") (keyword db-ns "attributes"))
(s/def (keyword api-ns "data") (s/keys :req-un [(keyword api-ns "attributes")]))))
What we’re accomplishing here with (keyword api-ns "attributes")
is building a keyword that looks like :my-project.api.users/attributes
. Or at least, that’s what I thought we were doing; here’s the error I ran into:
CompilerException java.lang.AssertionError: Assert failed: k must be namespaced keyword or resolvable symbol
That’s weird — (keyword api-ns "attributes")
on the REPL is building a clojure.lang.Keyword
like I expected it to, so why is an assert that it’s a keyword failing?
The answer lies in how the macro behind clojure.spec/def
is implemented. Macroexpanding (s/def (keyword "spec-test-ns" "name") string?)
gives you:
(clojure.spec/def-impl (quote (keyword "spec-test-ns" "name")) (quote clojure.core/string?) string?)
So since it’s quote
ing our input, it’s not resolving to the keyword before the assert. The solution I came to is to build a macro that unquotes my keyword building before s/def
is called:
(defmacro request-spec
[type]
(let [api-ns (str "my-project.api." type)
db-ns (str "my-project.db." type)
attr-k (keyword api-ns "attributes")
attr-v (keyword db-ns type)
data-k (keyword api-ns "data")]
`(do (s/def ~attr-k ~attr-v)
(s/def ~data-k (s/keys :req-un [~attr-k])))))
I generally prefer non-macro solutions when available, but as of now, this seems like the only way to DRY up this particular pattern.