Clojure reify - автоматизировать реализацию интерфейса Java с помощью другого макроса? - PullRequest
0 голосов
/ 18 апреля 2020

У меня есть java интерфейс, который просто генерирует события, и я пытаюсь реализовать его в Clojure. Интерфейс Java выглядит следующим образом (в действительности множество других методов):

public interface EWrapper {

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

А мой код Clojure выглядит так:

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

))

У меня есть около 75 функций для "реализации" "и все, что делает моя реализация, это отправляет карту, похожую на {:type calling-function-name-kebab-case :parameter-one-kebab-case parameter-one-value :parameter-two-kebab-case parameter-two-value} et c. Кажется, он созрел для другого макроса - который также был бы более безопасным, как если бы базовый интерфейс обновлялся большим количеством функций, так будет ли моя реализация.

Возможно ли это? Как мне вообще начать? Мой идеальный сценарий - читать код. java напрямую, но в качестве альтернативы я могу вручную вставить код Java в структуру карты? Спасибо,

Ответы [ 2 ]

1 голос
/ 18 апреля 2020

Вы можете самостоятельно разобрать данные простого метода (я сам не пробовал API отражения). Вот пример, включающий в себя модульный тест для демонстрации.

Сначала вставьте источник Java в структуры данных Clojure:

(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)
                      ]
          }))

Затем, функцию для разделения метода спецификации, и разбить аргументы на типы и имена. Мы используем библиотеку для преобразования из CamelCase в 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))))

и модульный тест для демонстрации:

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

      ))
  )
1 голос
/ 18 апреля 2020

Пространство имен clojure.reflect содержит методы для получения информации о классе. Я не думаю, что он даст вам имена параметров, хотя. Но вы можете использовать его для реализации чего-то, близкого к тому, что вы запрашиваете:

(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))

Вызов макроса reify-ewrapper расширится до

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

Чтобы получить правильные имена параметров, вам, вероятно, придется проанализировать исходный Java исходный код, я не думаю, что они сохраняются в байтовом коде.

Расширенное решение с именами параметров

Если вам действительно нужны имена параметров, вот небольшой анализатор, который извлечет их. Сначала вам необходимо 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 #"\("))))

Соответствующие биты кода для вывода имен параметров теперь выглядят так:

(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")))))
...