(ns metabase.util.malli.defn
  (:refer-clojure :exclude [defn defn-])
  (:require
   [clojure.core :as core]
   [clojure.string :as str]
   [malli.destructure]
   [metabase.util :as u]
   [metabase.util.malli.fn :as mu.fn]
   [net.cgrand.macrovich :as macros]))
(set! *warn-on-reflection* true)

TODO -- this should generate type hints from the schemas and from the return type as well.

(core/defn- deparameterized-arglist [{:keys [args]}]
  (-> (malli.destructure/parse args)
      :arglist
      (with-meta (macros/case
                   :cljs
                   (meta args)
                   ;; make sure we resolve classnames e.g. `java.sql.Connection` intstead of `Connection`, otherwise the
                   ;; tags won't work if you use them in another namespace that doesn't import that class. (Clj only)
                   :clj
                   (let [args-meta    (meta args)
                         tag          (:tag args-meta)
                         resolved-tag (when (symbol? tag)
                                        (let [resolved (ns-resolve *ns* tag)]
                                          (when (class? resolved)
                                            (symbol (.getName ^Class resolved)))))]
                     (cond-> args-meta
                       resolved-tag (assoc :tag resolved-tag)))))))
(core/defn- deparameterized-arglists [{:keys [arities], :as _parsed}]
  (let [[arities-type arities-value] arities]
    (case arities-type
      :single   (list (deparameterized-arglist arities-value))
      :multiple (map deparameterized-arglist (:arities arities-value)))))

Generate a docstring with additional information about inputs and return type using a parsed fn tail (as parsed by [[mx/SchematizedParams]]).

(core/defn- annotated-docstring
  [{original-docstring           :doc
    [arities-type arities-value] :arities
    :keys                        [return]
    :as                          _parsed}]
  (str/trim
   (str "Inputs: " (case arities-type
                     :single   (pr-str (:args arities-value))
                     :multiple (str "("
                                    (str/join "\n           "
                                              (map (comp pr-str :args)
                                                   (:arities arities-value)))
                                    ")"))
        "\n  Return: " (str/replace (u/pprint-to-str (:schema return :any))
                                    "\n"
                                    "\n          ")
        (when (not-empty original-docstring)
          (str "\n\n  " original-docstring)))))

Implementation of [[metabase.util.malli/defn]]. Like [[schema.core/defn]], but for Malli.

Doesn't Malli already have a version of this in [[malli.experimental]]? It does, but it tends to eat memory; see https://metaboat.slack.com/archives/CKZEMT1MJ/p1690496060299339 and #32843 for more information. This new implementation solves most of our memory consumption problems.

Unless it's in a skipped namespace during prod, (see: [[mu.fn/instrument-ns?]]) this macro emits clojure code to validate its inputs and outputs based on its malli schema annotations.

Example macroexpansion:

(mu/defn f :- :int [x :- :int] (inc x))

;; =>

(def f (let [&f (fn [x] (inc x))] (fn ([a] (metabase.util.malli.fn/validate-input :int a) (->> (&f a) (metabase.util.malli.fn/validate-output :int))))))

Known issue: does not currently generate automatic type hints the way [[schema.core/defn]] does, nor does it attempt to preserve them if you specify them manually. We can fix this in the future.

(defmacro defn
  [& [fn-name :as fn-tail]]
  (let [parsed           (mu.fn/parse-fn-tail fn-tail)
        cosmetic-name    (gensym (munge (str fn-name)))
        {attr-map :meta} parsed
        attr-map         (merge
                          {:arglists (list 'quote (deparameterized-arglists parsed))
                           :schema   (mu.fn/fn-schema parsed {:target :target/metadata})}
                          attr-map)
        docstring        (annotated-docstring parsed)
        instrument?      (mu.fn/instrument-ns? *ns*)]
    (if-not instrument?
      `(def ~(vary-meta fn-name merge attr-map)
         ~docstring
         ~(mu.fn/deparameterized-fn-form parsed cosmetic-name))
      `(def ~(vary-meta fn-name merge attr-map)
         ~docstring
         ~(macros/case
            :clj  (let [error-context {:fn-name (list 'quote fn-name)}]
                    (mu.fn/instrumented-fn-form error-context parsed cosmetic-name))
            :cljs (mu.fn/deparameterized-fn-form parsed cosmetic-name))))))

Same as defn, but creates a private def.

(defmacro defn-
  [fn-name & fn-tail]
  `(defn
     ~(with-meta fn-name (assoc (meta fn-name) :private true))
     ~@fn-tail))