From 222f0256c11f9bc6db8f257e24c877f646a4c86e Mon Sep 17 00:00:00 2001 From: Tommi Reiman Date: Wed, 2 Oct 2019 22:58:25 +0300 Subject: [PATCH] User-defined errors --- README.md | 46 +++++++++++++++++++++++++++++++++++++++ deps.edn | 4 ++-- src/malli/core.cljc | 16 ++++++++++++-- test/malli/core_test.cljc | 31 ++++++++++++++++++++++++++ 4 files changed, 93 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 481331fb9..89a95c366 100644 --- a/README.md +++ b/README.md @@ -149,6 +149,52 @@ Detailed errors with `m/explain`: ; {:path [3 1 4 1 2], :in [:address :lonlat 1], :schema double?, :value nil})} ``` +## Custom Error Messages + +Schema properties `:error/message` and `:error/fn` can be used for human-readable errors: + +```clj +(-> [int? {:error/message "should be an int"}] + (m/explain "kikka") + :errors + (first) + (m/error-message)) +; "should be an int" + +(-> [int? {:error/fn '(fn [schema value opts] (str "should be a int, was " (type value)))}] + (m/explain "kikka") + :errors + (first) + (m/error-message)) +; "should be a int, was class java.lang.String" +``` + +Error property values can be wrapped into localication maps (default-locale `:en`): + +```clj +(-> [int? {:error/message {:en "should be an int" + :fi "pitäisi olla numero"}}] + (m/explain "kikka") + :errors + (first) + (m/error-message {:locale :fi})) +; "pitäisi olla numero" +``` + +Schema-based defaults can be used: + +```clj +(-> int? + (m/explain "kikka") + :errors + (first) + (m/error-message + {:locale :fi + :errors {'int? {:error/message {:en "should be an int" + :fi "pitäisi olla numero"}}}})) +; "pitäisi olla numero" +``` + ## Value Transformation Schema-driven value transformations with `m/transform`: diff --git a/deps.edn b/deps.edn index e5ca4a05a..18139800f 100644 --- a/deps.edn +++ b/deps.edn @@ -27,8 +27,8 @@ "-Dclojure.compiler.direct-linking=true"]}} :deps {org.clojure/clojure {:mvn/version "1.10.1"} borkdude/sci {:git/url "https://github.com/borkdude/sci" - :sha "e5cc4e422e2712fe25876ee601f782c2b0d02e7d"} + :sha "1463f84738120275bc58351232ecd1acdaf0fcb1"} borkdude/edamame {:git/url "https://github.com/borkdude/edamame" - :sha "739bce6ad55f1ea563ee1ddceb8d0a7f41d0f85b"} + :sha "b577e565b136d3dd51945fe874049d4297946f57"} org.clojure/test.check {:mvn/version "0.9.0"} com.gfredericks/test.chuck {:mvn/version "0.2.10"}}} diff --git a/src/malli/core.cljc b/src/malli/core.cljc index 8e94d5337..e4613866e 100644 --- a/src/malli/core.cljc +++ b/src/malli/core.cljc @@ -45,8 +45,8 @@ (clojure.core/name x)) x)) -(defn eval [code] - (sci/eval-string (str code) {:preset :termination-safe})) +(defn eval [?code] + (if (fn? ?code) ?code (sci/eval-string (str ?code) {:preset :termination-safe}))) (defn fail! ([type] @@ -663,6 +663,18 @@ ([?schema value opts] ((explainer ?schema opts) value [] []))) +(defn error-message + ([error] + (error-message error nil)) + ([{:keys [value schema]} {:keys [errors locale] :or {errors {}} :as opts}] + (let [maybe-localized (fn [x] (if (map? x) (get x (or locale :en)) x)) + schema-properties (properties schema) + default-properties (errors (name schema))] + (or (if-let [fn (maybe-localized (:error/fn schema-properties))] ((eval fn) schema value opts)) + (maybe-localized (:error/message schema-properties)) + (if-let [fn (maybe-localized (:error/fn default-properties))] ((eval fn) schema value opts)) + (maybe-localized (:error/message default-properties)))))) + (defn transformer "Creates a value transformer given a transformer and a schema." ([?schema t] diff --git a/test/malli/core_test.cljc b/test/malli/core_test.cljc index 0fb22fb54..a80643246 100644 --- a/test/malli/core_test.cljc +++ b/test/malli/core_test.cljc @@ -34,6 +34,14 @@ (defn visitor [schema childs _] (into [(m/name schema)] (seq childs))) +(deftest eval-test + (is (= 2 ((m/eval inc) 1))) + (is (= 2 ((m/eval 'inc) 1))) + (is (= 2 ((m/eval '#(inc %)) 1))) + (is (= 2 ((m/eval '#(inc %1)) 1))) + (is (= 2 ((m/eval '(fn [x] (inc x))) 1))) + (is (= 2 ((m/eval "(fn [x] (inc x))") 1)))) + (deftest validation-test (testing "coercion" @@ -497,6 +505,29 @@ (is (= [2 1] (?path (m/explain [:map {:name int?} [:x int?]] {:x "1"})))) (is (= [2 2] (?path (m/explain [:map {:name int?} [:x {:optional false} int?]] {:x "1"})))))) +(deftest error-message-test + (let [msg "should be an int" + fn1 (fn [_ value _] (str "should be an int, was " value)) + fn2 '(fn [_ value _] (str "should be an int, was " value))] + (doseq [[schema value message opts] + [;; via schema + [[int? {:error/message msg}] "kikka" "should be an int"] + [[int? {:error/fn fn1}] "kikka" "should be an int, was kikka"] + [[int? {:error/fn fn2}] "kikka" "should be an int, was kikka"] + [[int? {:error/message msg, :error/fn fn2}] "kikka" "should be an int, was kikka"] + ;; via defaults + [[int?] "kikka" "should be an int" {:errors {'int? {:error/message msg}}}] + [[int?] "kikka" "should be an int, was kikka" {:errors {'int? {:error/fn fn1}}}] + [[int?] "kikka" "should be an int, was kikka" {:errors {'int? {:error/fn fn2}}}] + [[int?] "kikka" "should be an int, was kikka" {:errors {'int? {:error/message msg, :error/fn fn2}}}] + ;; both + [[int? + {:error/message msg, :error/fn fn2}] + "kikka" "should be an int, was kikka" + {:errors {'int? {:error/message "fail1", :error/fn (constantly "fail2")}}}]]] + (is (= message (-> (m/explain schema value) :errors first (m/error-message opts))))))) + + (deftest properties-test (testing "properties can be set and retrieved" (let [properties {:title "kikka"}]