Paul Dorman
Paul Dorman

Reputation: 603

Resolving symbols in :require'd namespace

I'm writing a small application that includes a password change function with validators for password quality. Currently the validators are specified in a map like so:

(def validations
  {:min-length 6
   :max-length 32})

The validations map is defined in the validations namespace, but I plan to move it to a configuration namespace later on. My decision to use a map in this way was to make configuration straight-forward for non-programmers.

There are a number of validation functions in the validations namespace that usually take the form:

(defn min-length [n s]
  {:req (str "be at least " n " characters long")
   :pass? (>= (.length (or s "")) n)})

So with the function above (min-length 3 "clojure") would return {:req "be at least 3 characters long", :pass? true}.

I can validate a password with this function in the validation namespace with this function:

(defn validate-new-password [password]
  (into {} (for [[k v] validations]
             [k (eval (list (-> k name symbol) v password))])))

The result being something like:

>(validate-new-password "clojure")
{:min-length {:req "be at least 6 characters long", :pass? true}, 
 :max-length {:req "be no longer than 32 characters long", :pass? true}, 
 :min-digits {:req "have at least 1 digit", :pass? false},  
 :allow-whitespace {:req "not contain spaces or tabs", :pass? true}, 
 :allow-dict-words {:req "not be a dictionary word", :pass? false}}

What is the most practical way to resolve the validation functions when the validate-new-password function is called from outside the validation namespace?

I've tried a number of approaches over the past weeks but I've never been happy with the resulting form (and none of them have worked!).

Generally I guess the question is "how are symbols in a :require'd namespace resolved when called by functions within that namespace?"

I'm also interested on any general comments about my implementation.

Upvotes: 3

Views: 192

Answers (2)

Michał Marczyk
Michał Marczyk

Reputation: 84341

To answer the general question (from the penultimate paragraph of the question), symbols are normally resolved at compile time -- you have to go out of your way (that is, use resolve1) to postpone resolution to runtime.

That said, you probably don't need to postpone resolution in this case, you simply need to :require the configuration namespace in the validations namespace and refer to config/validations in the relevant spot.

A minor complication would arise if you wanted your config namespace to depend on the validations namespace (i.e. :require or :use it itself, or else another namespace which depends on it). In that case you could provide a handful of configuration setters in the validations namespace and use those from config. E.g.:

(def validations (atom default-validations))
(defn set-validations! [vs]   (reset! validations vs))
(defn add-validation!  [k v]  (swap! validations assoc k v))

Then put @validations in place of validations in validate-new-password.

Alternatively, you could put your default validations in a Var and provide functions to rebind the Var (presumably this will only need to happen once, or else very rarely, so using a Var shouldn't be a problem):

(def validations default-validations)
(defn set-validations! [vs]  (.bindRoot validations vs))
(defn add-validation!  [k v] (alter-var-root validations assoc k v))

Now if you actually cannot predict, at the time of writing the code, which namespace you'll need to resolve your symbol in, then kotarak's answer (with its use of resolve) is the way to go.


1 You could say eval also allows you to do it, although technically it performs compilation at runtime, so symbol resultion in eval'd forms still occurs at compilation time. The more important thing to remember about it is that it's hardly ever needed.

Upvotes: 1

kotarak
kotarak

Reputation: 17299

There is no need for eval, as in 98% of the cases.

(defn validate-new-password
  [password]
  (into {} (for [[k v] validations]
             [k ((->> k name (symbol "your.name.space") resolve) v password)])))

Upvotes: 2

Related Questions