Search code examples
javaclojuremacrosinterop

Clojure reify - automate implementation of Java interface with another macro?


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,


Solution

  • 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})))
    
          ))
      )