Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Clojure macro for calling Java setters based on a map?

I'm writing a Clojure wrapper for the Braintree Java library to provide a more concise and idiomatic interface. I'd like to provide functions to instantiate the Java objects quickly and concisely, like:

(transaction-request :amount 10.00 :order-id "user42")

I know I can do this explicitly, as shown in this question:

(defn transaction-request [& {:keys [amount order-id]}]
  (doto (TransactionRequest.)
    (.amount amount)
    (.orderId order-id)))

But this is repetitive for many classes and becomes more complex when parameters are optional. Using reflection, it's possible to define these functions much more concisely:

(defn set-obj-from-map [obj m]
  (doseq [[k v] m]
    (clojure.lang.Reflector/invokeInstanceMethod
      obj (name k) (into-array Object [v])))
  obj)

(defn transaction-request [& {:as m}]
  (set-obj-from-map (TransactionRequest.) m))

(defn transaction-options-request [tr & {:as m}]
  (set-obj-from-map (TransactionOptionsRequest. tr) m))

Obviously, I'd like to avoid reflection if at all possible. I tried defining a macro version of set-obj-from-map but my macro-fu isn't strong enough. It probably requires eval as explained here.

Is there a way to call a Java method specified at runtime, without using reflection?

Thanks in advance!

Updated solution:

Following the advice from Joost, I was able to solve the problem using a similar technique. A macro uses reflection at compile-time to identify which setter methods the class has and then spits out forms to check for the param in a map and call the method with it's value.

Here's the macro and an example use:

; Find only setter methods that we care about
(defn find-methods [class-sym]
  (let [cls (eval class-sym)
        methods (.getMethods cls)
        to-sym #(symbol (.getName %))
        setter? #(and (= cls (.getReturnType %))
                      (= 1 (count (.getParameterTypes %))))]
    (map to-sym (filter setter? methods))))

; Convert a Java camelCase method name into a Clojure :key-word
(defn meth-to-kw [method-sym]
  (-> (str method-sym)
      (str/replace #"([A-Z])"
                   #(str "-" (.toLowerCase (second %))))
      (keyword)))

; Returns a function taking an instance of klass and a map of params
(defmacro builder [klass]
  (let [obj (gensym "obj-")
        m (gensym "map-")
        methods (find-methods klass)]
    `(fn [~obj ~m]
       ~@(map (fn [meth]
               `(if-let [v# (get ~m ~(meth-to-kw meth))] (. ~obj ~meth v#)))
              methods)
       ~obj)))

; Example usage
(defn transaction-request [& {:as params}]
  (-> (TransactionRequest.)
    ((builder TransactionRequest) params)
    ; some further use of the object
  ))
like image 783
bkirkbri Avatar asked Feb 28 '12 19:02

bkirkbri


2 Answers

You can use reflection at compile time ~ as long as you know the class you're dealing with by then ~ to figure out the field names, and generate "static" setters from that. I wrote some code that does pretty much this for getters a while ago that you might find interesting. See https://github.com/joodie/clj-java-fields (especially, the def-fields macro in https://github.com/joodie/clj-java-fields/blob/master/src/nl/zeekat/java/fields.clj).

like image 134
Joost Diepenmaat Avatar answered Sep 28 '22 05:09

Joost Diepenmaat


The macro could be as simple as:

(defmacro set-obj-map [a & r] `(doto (~a) ~@(partition 2 r)))

But this would make your code look like:

(set-obj-map TransactionRequest. .amount 10.00 .orderId "user42")

Which I guess is not what you would prefer :)

like image 33
Ankur Avatar answered Sep 28 '22 05:09

Ankur