(ns metabase.util.malli
  (:refer-clojure :exclude [fn defn defn- defmethod])
  (:require
   #?@(:clj
       ([metabase.util.malli.defn :as mu.defn]
        [metabase.util.malli.fn :as mu.fn]
        [net.cgrand.macrovich :as macros]
        [potemkin :as p]))
   [clojure.core :as core]
   [malli.core :as mc]
   [malli.destructure]
   [malli.error :as me]
   [malli.util :as mut]
   [metabase.util.i18n :as i18n])
  #?(:cljs (:require-macros [metabase.util.malli])))
#?(:clj
   (p/import-vars
    [mu.fn fn instrument-ns?]
    [mu.defn defn defn-]))

Pass into mu/humanize to include the value received in the error message.

(core/defn humanize-include-value
  [{:keys [value message]}]
  ;; TODO Should this be translated with more complete context? (tru "{0}, received: {1}" message (pr-str value))
  (str message ", " (i18n/tru "received") ": " (pr-str value)))

Explains a schema failure, and returns the offending value.

(core/defn explain
  [schema value]
  (-> (mc/explain schema value)
      (me/humanize {:wrap humanize-include-value})))
(def ^:private Schema
  [:and any?
   [:fn {:description "a malli schema"} mc/schema]])

Schema for localized string.

(def localized-string-schema
  #?(:clj  [:fn {:error/message "must be a localized string"}
            i18n/localized-string?]
     ;; TODO Is there a way to check if a string is being localized in CLJS, by the `ttag`?
     ;; The compiler seems to just inline the translated strings with no annotation or wrapping.
     :cljs :string))

Update a malli schema with an arbitrary map of properties

(metabase.util.malli/defn with
  {:style/indent [:form]}
  [mschema props]
  (mut/update-properties (mc/schema mschema) merge props))

Update a malli schema to have a :description (used by umd/describe, which is used by api docs), and a :error/fn (used by me/humanize, which is used by defendpoint). They don't have to be the same, but usually are.

(with-api-error-message [:string {:min 1}] (deferred-tru "Must be a string with at least 1 character representing a User ID."))

Kondo gets confused by :refer [defn] on this, so it's referenced fully qualified.

(metabase.util.malli/defn with-api-error-message
  {:style/indent [:form]}
  ([mschema :- Schema error-message :- localized-string-schema]
   (with-api-error-message mschema error-message error-message))
  ([mschema                :- :any
    description-message    :- localized-string-schema
    specific-error-message :- localized-string-schema]
   (mut/update-properties (mc/schema mschema) assoc
                          ;; override generic description in api docs and :errors key in API's response
                          :description description-message
                          ;; override generic description in :specific-errors key in API's response
                          :error/fn    (constantly specific-error-message))))

Convenience for disabling [[defn]] and [[metabase.util.malli.fn/fn]] input/output schema validation. Since input/output validation is currently disabled for ClojureScript, this is a no-op.

#?(:clj
   (defmacro disable-enforcement
     {:style/indent 0}
     [& body]
     (macros/case
      :clj
       `(binding [mu.fn/*enforce* false]
          ~@body)
       :cljs
       `(do ~@body))))

Impl for [[defmethod]] for regular Clojure.

Impl for [[defmethod]] for ClojureScript.

#?(:clj
   (defmacro -defmethod-clj
     [multifn dispatch-value & fn-tail]
     (let [dispatch-value-symb (gensym "dispatch-value-")
           error-context-symb  (gensym "error-context-")]
       `(let [~dispatch-value-symb ~dispatch-value
              ~error-context-symb  {:fn-name        '~(or (some-> (resolve multifn) symbol)
                                                          (symbol multifn))
                                    :dispatch-value ~dispatch-value-symb}
              f#                   ~(mu.fn/instrumented-fn-form error-context-symb (mu.fn/parse-fn-tail fn-tail))]
          (.addMethod ~(vary-meta multifn assoc :tag 'clojure.lang.MultiFn)
                      ~dispatch-value-symb
                      f#)))))
#?(:clj
   (defmacro -defmethod-cljs
     [multifn dispatch-value & fn-tail]
     `(core/defmethod ~multifn ~dispatch-value
        ~@(mu.fn/deparameterized-fn-tail (mu.fn/parse-fn-tail fn-tail)))))

Like [[schema.core/defmethod]], but for Malli.

#?(:clj
   (defmacro defmethod
     [multifn dispatch-value & fn-tail]
     (macros/case
      :clj  `(-defmethod-clj ~multifn ~dispatch-value ~@fn-tail)
      :cljs `(-defmethod-cljs ~multifn ~dispatch-value ~@fn-tail))))

Returns the value if it matches the schema, else throw an exception.

#?(:clj
   (defn validate-throw
     [schema-or-validator value]
     (let [is-validator? (fn? schema-or-validator)]
       (if-not ((if is-validator?
                  schema-or-validator
                  (mc/validator schema-or-validator))
                value)
         (throw (ex-info "Value does not match schema" (when-not is-validator?
                                                         {:error (explain schema-or-validator value)})))
         value))))

Returns a new schema that is the same as map-schema, but with the key k associated with the value v. If kvs are provided, they are also associated with the schema.

(core/defn map-schema-assoc
  [map-schema & kvs]
  (if kvs
    (if (next kvs)
      (let [key (first kvs)
            val (first (next kvs))
            ret (mut/assoc map-schema key val)]
        (recur ret (nnext kvs)))
      (throw (ex-info "map-schema-assoc expects even number of arguments after schema-map, found odd number" {})))
    map-schema))