1

I have a java interface that just emits events, and I'm trying to implement it in Clojure. The Java interface is like this (plenty of other methods in reality):

public interface EWrapper {

    void accountSummary(int reqId, String account, String tag, String value, String currency);
    void accountSummaryEnd(int reqId);
}

And my Clojure code looks like:

(defn create
  "Creates a wrapper calling a single function (cb) with maps that all have a :type to indicate
  what type of messages was received, and event parameters
  "
  [cb]
  (reify
    EWrapper

    (accountSummary [this reqId account tag value currency]
      (dispatch-message cb {:type :account-summary :request-id reqId :account account :tag tag :value value :currency currency}))

    (accountSummaryEnd [this reqId]
      (dispatch-message cb {:type :account-summary-end :request-id reqId}))

))

I have about 75 functions to "implement" and all my implementation does is dispatching a map looking like {:type calling-function-name-kebab-case :parameter-one-kebab-case parameter-one-value :parameter-two-kebab-case parameter-two-value} etc. It seems ripe for another macro - which would also be safer as if the underlying interface gets updated with more functions, so will my implementation.

Is that possible? How do I even get started? My ideal scenario would be to read the .java code directly, but alternatively I can manually paste the Java code into a map structure? Thank you,

2 Answers 2

2

You can parse out simple method data yourself (I haven't tried the reflection API myself). Here is a sample, including a unit test to demonstrate.

First, put in the Java source into Clojure data structures:

(ns tst.demo.core
  (:use tupelo.core tupelo.test)
  (:require
    [camel-snake-kebab.core :as csk]
    [schema.core :as s]
    [tupelo.string :as ts]))

(def java-spec
  (quote {:interface EWrapper
          :methods   [; assume have structure of
                      ; <ret-type> <method-name> <arglist>, where <arglist> => (<type1> <name1>, <type2> <name2> ...)
                      void accountSummary (int reqId, String accountName, String tag, String value, String currencyName)
                      void accountSummaryEnd (int reqId)
                      ]
          }))

Then, a function to pull apart the method specs, and deconstruct the args into types & names. We use a library to convert from CamelCase to kabob-case:

(defn reify-gen
  [spec-map]
  (let [methods-data   (partition 3 (grab :methods spec-map))
        ; >>             (spyx-pretty methods-data)
        method-entries (forv [mdata methods-data]
                         (let [[ret-type mname arglist] mdata ; ret-type unused
                               mname-kebab        (csk/->kebab-case mname)
                               arg-pairs          (partition 2 arglist)
                               arg-types          (mapv first arg-pairs) ; unused
                               arg-names          (mapv second arg-pairs)
                               arg-names-kebab    (mapv csk/->kebab-case arg-names)
                               arg-names-kebab-kw (mapv ->kw arg-names-kebab)
                               mresult            (list mname (prepend
                                                                (quote this)
                                                                arg-names)
                                                    (list
                                                      mname-kebab
                                                      (glue {:type (->kw mname-kebab)}
                                                        (zipmap arg-names-kebab-kw arg-names))))]
                           ; (spyx-pretty mresult)
                           mresult ))]
    (->list
      (prepend
        (quote reify)
        (grab :interface spec-map)
        method-entries))))

And a unit test to demonstrate:

(dotest
  (newline)
  (is= (spyx-pretty (reify-gen java-spec))
    (quote
      (reify
        EWrapper
        (accountSummary
          [this reqId accountName tag value currencyName]
          (account-summary
            {:type          :account-summary
             :req-id        reqId,
             :account-name  accountName,
             :tag           tag,
             :value         value,
             :currency-name currencyName}))
        (accountSummaryEnd
          [this reqId]
          (account-summary-end {:type :account-summary-end, :req-id reqId})))

      ))
  )
Sign up to request clarification or add additional context in comments.

2 Comments

thanks for the great answer. Will experiment and mark either this or Rulle's answer as accepted
This worked out beautifully. tupelo looks great although I ended up doing it in plain Clojure to remove dependencies for that specific API. As a side note I had some overloaded methods in the interface I implemented separately to include type hints. Strangely these don't appear through quote even though they're there. Thanks again.
1

The clojure.reflect namespace contains methods to get information about a class. I don't think it will give you the parameter names, though. But you can use it to implement something close to what you are asking for:

(ns playground.reify
  (:require [clojure.reflect :as r])
  (:import EWrapper))

(defn kebab-case [s]
  ;; TODO
  s)

(defn arg-name [index]
  (symbol (str "arg" index)))

(defn generate-method [member this cb]
  (let [arg-names (mapv arg-name (range (count (:parameter-types member))))
        method-name (:name member)]
    `(~method-name [~this ~@arg-names]
      (~cb {:type ~(keyword (kebab-case method-name))
            :args ~arg-names}))))

(defmacro reify-ewrapper [this cb]
  `(reify EWrapper
     ~@(map #(generate-method % this cb) (:members (r/reflect EWrapper)))))

(defn create [cb]
  (reify-ewrapper this cb))

The reify-ewrapper macro call will expand to

(reify*
 [EWrapper]
 (accountSummary
  [this arg0 arg1 arg2 arg3 arg4]
  (cb {:args [arg0 arg1 arg2 arg3 arg4], :type :accountSummary}))
 (accountSummaryEnd
  [this arg0]
  (cb {:args [arg0], :type :accountSummaryEnd})))

To get the parameter names right, you would probably have to parse the original Java source code, I don't think they are preserved in the byte code.

Extended solution with parameter names

If you really do want the parameter names, here is a small parser that will extract them. You need to first require clojure.string :as cljstr:

(defn parse-method [[name-str arg-str]]
  (let [arg-sliced (subs arg-str 0 (cljstr/index-of arg-str ")"))
        param-pairs (for [p (cljstr/split arg-sliced #",")]
                      (into []
                            (comp (map cljstr/trim)
                                  (remove empty?)
                                  (map symbol))
                            (cljstr/split p #" ")))]
    {:name (symbol (subs name-str (inc (cljstr/last-index-of name-str " "))))
     :parameter-types (mapv first param-pairs)
     :parameter-names (mapv second param-pairs)}))

(defn parse-interface [s]
  (map parse-method (partition 2 1 (cljstr/split s #"\("))))

Relevant bits of the code to output the paramater names now look like this:

(defn generate-method [member this cb]
  (let [arg-names (:parameter-names member)
        method-name (:name member)]
    `(~method-name [~this ~@arg-names]
      (~cb ~(merge {:type (keyword (kebab-case method-name))}
                   (zipmap (map (comp keyword kebab-case str) 
                                arg-names)
                           arg-names))))))

(defmacro reify-ewrapper [this cb]
  `(reify EWrapper
     ~@(map #(generate-method % this cb) (parse-interface (slurp "javasrc/EWrapper.java")))))

3 Comments

You shouldn't really parse the parameter names anyway. Java developers don't consider parameter names to be "committed" API, and will feel free to change them at any time. If this breaks you, that's your problem.
@amalloy thank you didn't know that - they're useful in my case as end user will need to refer to Java documentation to understand what methods do
@Rulle thanks for the quick and great answer. Will experiment and mark either this or Alan Thompson's answer as accepted

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.