Clojure macro for calling Java setter based on map?
I wrote a clojure wrapper for the Braintree Java library to provide a more concise and idiomatic interface I want to provide functions to instantiate Java objects quickly and concisely, such as:
(transaction-request :amount 10.00 :order-id "user42")
I know I can do this clearly, as shown in this question:
(defn transaction-request [& {:keys [amount order-id]}] (doto (TransactionRequest.) (.amount amount) (.orderId order-id)))
However, this is repeated for many classes and becomes more complex when parameters are optional With reflection, you can define these functions 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, if possible, I want to avoid reflection I tried to define a macro map of set obj from the map, but my macro is not strong enough This may require Eval, as described here
Is there a way to call a Java method specified at run time without using reflection?
Thank you in advance!
Update solution:
According to Joost's suggestion, I can use similar technology to solve the problem A macro uses reflection at compile time to identify which setter methods the class has, then looks up param in the table and calls the method with this value
Here is a macro and an example:
; 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 ))
Solution
You can use reflection at compile time ~ as long as you know the class you are working on, then ~ find the field name and generate a "static" setter from it I wrote some code that you might find interesting for getters See https://github.com/joodie/clj-java-fields (especially https://github.com/joodie/clj-java-fields/blob/master/src/nl/zeekat/java/fields.clj Def fields macro in)