Closed piotr-yuxuan closed 3 years ago
Simplest working example, here with string. The multimethod is invoked only when the transformer is built, so the cost is not paid on encoding time. The best thing is that when entering the encoding function, malli
gives us the pleasant certainty that the type is correct. This is for free.
(defmulti ->avro-fn :avro/type)
(defmethod ->avro-fn :default
[_]
identity)
(defmethod ->avro-fn :avro/string
[opts]
(case (:avro/string-type opts)
:utf8 #(Utf8. ^String %)
identity))
(defmethod ->avro-fn :avro/field
[opts]
(get opts :avro/ident-fn identity))
(defmethod ->avro-fn :avro/map
[opts]
(let [->avro (->avro-fn (assoc opts :avro/type :avro/field))]
(fn [m]
;; Return a HashMap, so guarantee to be exactly what is expected
;; by Avro. Subtlety: a vector of tuples, like the result
;; of `(map f {})` will be cast into a HashMap.
(let [^HashMap hm (HashMap.)]
(doseq [[k v] m]
(.put hm
(->avro k)
v))
hm))))
(defmethod ->avro-fn :avro/array
[_]
(fn [s]
;; Return a List, so guarantee to be exactly what is expected by
;; Avro. Subtlety: a map, like {:a 1, :b 2} will be cast into the
;; List<MapEntry> [[:a 1], [:b 2]], provided the schema is valid.
(let [^List l (ArrayList.)]
(doseq [i s] (.add l i))
l)))
(defn avro-encoders
[opts]
{'keyword? {:enter #(doto %)
:leave #(doto %)}
'int? {:enter #(doto %)
:leave #(doto %)}
'string? (->avro-fn (assoc opts :avro/type :avro/string))
:vector {:leave (->avro-fn (assoc opts :avro/type :avro/array))}
:map {:leave (->avro-fn (assoc opts :avro/type :avro/map))}})
(defn avro-transformer
[opts]
(mt/transformer
{:name :avro
:encoders (avro-encoders opts)}))
(def schema
(malli/schema
;; Recursive, because I can.
[:schema {:registry {::cons [:maybe [:map [:v-field [:vector [:or string? [:ref ::cons]]]]]]}}
::cons]))
(def value
{:v-field [{:v-field [{:v-field [{:v-field ["lol"]}]}]}
{:v-field []}]})
(assert (malli/validate schema value))
(let [opts {:avro/string-type :str
:avro/ident-fn csk/->SCREAMING_SNAKE_CASE_KEYWORD}]
(malli/encode schema value (avro-transformer opts)))
=> {:V_FIELD [{:V_FIELD [{:V_FIELD [{:V_FIELD ["lol"]}]}]} {:V_FIELD []}]}
(let [opts {:avro/string-type :utf8
:avro/ident-fn csk/->camelCaseString}]
(malli/encode schema value (avro-transformer opts)))
=> {"vField" [{"vField" [{"vField" [{"vField" [#object[org.apache.avro.util.Utf8 0x53367723 "lol"]]}]}]} {"vField" []}]}