metosin / malli

High-performance data-driven data specification library for Clojure/Script.
Eclipse Public License 2.0
1.44k stars 204 forks source link

Multiple error messages and error paths from :fn validation #965

Open irigarae opened 8 months ago

irigarae commented 8 months ago

Hi,

I wonder if it's possible to output multiple errors from a single :fn and if not, how do you usually handle it. Here's an example in which I have a function to validate the uniqueness of names in an array:

(require '[malli.util :as mu]
         '[malli.core :as m])

(me/humanize
 (m/explain
  [:and
   [:map
    [:scheme :string]
    [:colours [:vector
               [:map
                [:colour :string]
                [:hex #"^#[a-f0-9]{6}$"]]]]]
   [:fn {:error/path [:colours]
         :error/message "colour names must be unique"}
    (fn [scheme] (->> (:colours scheme) (map :colour) (apply distinct?)))]]
  {:scheme "foo"
   :colours [{:colour "duplicated" :hex "#000000"}
             {:colour "secondary" :hex "#111111"}
             {:colour "highlight" :hex "#222222"}
             {:colour "duplicated" :hex "#333333"}]}))

;; it gives this
#_=> {:colours ["colour names must be unique"]}

;; I would like to have this
#_=> {:colours [{:colour ["colour names must be unique"]} 
                nil
                nil
                {:colour ["colour names must be unique"]}]}

;; the closest I can get is by redoing the validation in the :error/fn replacing :error/message
{:error/fn (fn [{:keys [value]} _]
             (let [fs (frequencies (map :color (:colors value)))]
               ["color names must be unique, bad indexes:"
                (->> (:colors value)
                     (map-indexed (fn [idx c] (when-not (= 1 (fs (:color c))) idx)))
                     (filterv some?))]))}

;; which gives this
#_=> {:colours [["colour names must be unique, bad indexes:" [0 3]]]}

How do you usually handle these kind of validations? How do you report the individual places that fail validation?

I think what I would expect is an :fn that behaves the opposite way, instead of returning truthy for valid, would return nil, otherwise return a bunch of [{:error/message "" :error/path []}], but maybe that's far from the way schemas are walked and errors reported in malli.

Anyway interested to see what's the recommended approach for these kind of validations.

Thank you

fgasperino commented 7 months ago

I've had to do something similar in the past for accumulating state between :error fns.

  (defn mk-stateful-schema
    []
    (let [state (atom {})]
      [:and
       [:map
        [:scheme :string]
        [:colours [:vector
                   [:map
                    [:colour :string]
                    [:hex #"^#[a-z0-9]{6}$"]]]]]
       [:fn
        {:error/fn
         (fn [& _]
           (format "The following schemes have duplicate colors: %s" (:dupes @state)))}
        (fn [{:keys [scheme colours]}]
          (->> colours
               (map :colour)
               (reduce
                (fn [{:keys [seen] :as acc} m]
                  (if (contains? seen m)
                    (update acc :dupes conj {:scheme scheme :colour m})
                    (update acc :seen conj m)))
                {:seen #{} :dupes #{}})
               (reset! state)
               (#(empty? (:dupes %)))))]]))

  (malli-error/humanize
   (malli/explain (mk-stateful-schema)
                  {:scheme "foo"
                   :colours [{:colour "duplicated" :hex "#000000"}
                             {:colour "secondary" :hex "#111111"}
                             {:colour "highlight" :hex "#222222"}
                             {:colour "duplicated" :hex "#333333"}]}))
irigarae commented 6 months ago

This should be obsolete once https://github.com/metosin/malli/issues/975 is done.