1
votes

So I'm trying to make a Clojure macro that makes it easy to interop with Java classes utilizing the Builder pattern.

Here's what I've tried so far.

(defmacro test-macro
  []
  (list
   (symbol ".queryParam")
   (-> (ClientBuilder/newClient)
       (.target "https://www.test.com"))
   "key1"   
   (object-array ["val1"])))

Which expands to the below

(. 
  #object[org.glassfish.jersey.client.JerseyWebTarget 0x107a5073 "org.glassfish.jersey.client.JerseyWebTarget@107a5073"] 
  queryParam 
  "key1" 
  #object["[Ljava.lang.Object;" 0x16751ba2 "[Ljava.lang.Object;@16751ba2"])

The desired result is:

 (.queryParam
  #object[org.glassfish.jersey.client.JerseyWebTarget 0x107a5073 "org.glassfish.jersey.client.JerseyWebTarget@107a5073"]       
  "key1" 
  #object["[Ljava.lang.Object;" 0x16751ba2 "[Ljava.lang.Object;@16751ba2"]) 

I guess the . is causing something to get evaluated and moved around? In which case the solution would to be to quote it. But how can I quote the results of an evaluated expression?

My goal is to convert maps into code that build the object by have the maps keys be the functions to be called and the values be the arguments passed into the Java functions.


I understand how to use the threading and do-to macros but am trying to make request building function data driven. I want to be able take in a map with the key as "queryParam" and the values as the arguments. By having this I can leverage the entirety on the java classes functions only having to write one function myself and there is enough of a 1 to 1 mapping I don't believe others will find it magical.

(def test-map {"target" ["https://www.test.com"]
               "path" ["qa" "rest/service"] 
               "queryParam" [["key1" (object-array ["val1"])]
                              ["key2" (object-array ["val21" "val22" "val23"])]] })

 (-> (ClientBuilder/newClient)
     (.target "https://www.test.com")
     (.path "qa")
     (.path "rest/service")
     (.queryParam "key1" (object-array ["val1"]))
     (.queryParam "key2" (object-array ["val21" "val22" "val23"])))
1

1 Answers

2
votes

From your question it's not clear if you have to use map as your builder data structure. I would recommend using the threading macro for working directly with Java classes implementing the builder pattern:

(-> (ClientBuilder.)
      (.forEndpoint "http://example.com")
      (.withQueryParam "key1" "value1")
      (.build))

For classes that don't implement builder pattern and their methods return void (e.g. setter methods) you can use doto macro:

(doto (Client.)
  (.setEndpoint "http://example.com")
  (.setQueryParam "key1" "value1"))

Implementing a macro using a map for encoding Java method calls is possible but awkward. You would have to keep each method arguments inside a sequence (in map values) to be a able to call methods with multiple parameters or have some convention for storing arguments for single parameter methods, handling varargs, using map to specify method calls doesn't guarantee the order they will be invoked etc. It will add much complexity and magic to your code.

This is how you could implement it:

(defmacro builder [b m]
  (let [method-calls
        (map (fn [[k v]] `(. (~(symbol k) ~@v))) m)]
    `(-> ~b
         ~@method-calls))) 

(macroexpand-1
  '(builder (StringBuilder.) {"append" ["a"]})) 
;; => (clojure.core/-> (StringBuilder.) (. (append "a"))) 

(str
  (builder (StringBuilder.) {"append" ["a"] })) 
;; => "a"