Skip to content

Commit f8a7308

Browse files
authored
Merge pull request #1138 from frenchy64/humanize-negation
`:not` humanizer
2 parents dcfc4ba + 7a44cf6 commit f8a7308

File tree

3 files changed

+251
-33
lines changed

3 files changed

+251
-33
lines changed

README.md

+44-1
Original file line numberDiff line numberDiff line change
@@ -817,7 +817,50 @@ Or if you already have a malli validation exception (e.g. in a catch form):
817817

818818
## Custom error messages
819819

820-
Error messages can be customized with `:error/message` and `:error/fn` properties:
820+
Error messages can be customized with `:error/message` and `:error/fn` properties.
821+
822+
If `:error/message` is of a predictable structure, it will automatically support custom `[:not schema]` failures for the following locales:
823+
- `:en` if message starts with `should` or `should not` then they will be swapped automatically. Otherwise, message is ignored.
824+
```clojure
825+
;; e.g.,
826+
(me/humanize
827+
(m/explain
828+
[:not
829+
[:fn {:error/message {:en "should be a multiple of 3"}}
830+
#(= 0 (mod % 3))]]
831+
3))
832+
; => ["should not be a multiple of 3"]
833+
```
834+
835+
The first argument to `:error/fn` is a map with keys:
836+
- `:schema`, the schema to explain
837+
- `:value` (optional), the value to explain
838+
- `:negated` (optional), a function returning the explanation of `(m/explain [:not schema] value)`.
839+
If provided, then we are explaining the failure of negating this schema via `(m/explain [:not schema] value)`.
840+
Note in this scenario, `(m/validate schema value)` is true.
841+
If returning a string,
842+
the resulting error message will be negated by the `:error/fn` caller in the same way as `:error/message`.
843+
Returning `(negated string)` disables this behavior and `string` is used as the negated error message.
844+
```clojure
845+
;; automatic negation
846+
(me/humanize
847+
(m/explain
848+
[:not [:fn {:error/fn {:en (fn [_ _] "should not be a multiple of 3")}}
849+
#(not= 0 (mod % 3))]]
850+
1))
851+
; => ["should be a multiple of 3"]
852+
853+
;; manual negation
854+
(me/humanize
855+
(m/explain [:not [:fn {:error/fn {:en (fn [{:keys [negated]} _]
856+
(if negated
857+
(negated "should not avoid being a multiple of 3")
858+
"should not be a multiple of 3"))}}
859+
#(not= 0 (mod % 3))]] 1))
860+
; => ["should not avoid being a multiple of 3"]
861+
```
862+
863+
Here are some basic examples of `:error/message` and `:error/fn`:
821864

822865
```clojure
823866
(-> [:map

src/malli/error.cljc

+67-29
Original file line numberDiff line numberDiff line change
@@ -3,16 +3,43 @@
33
[malli.core :as m]
44
[malli.util :as mu]))
55

6+
(declare default-errors error-message)
7+
68
(defn -pr-str [v] #?(:clj (pr-str v), :cljs (str v)))
79

810
(defn -pred-min-max-error-fn [{:keys [pred message]}]
9-
(fn [{:keys [schema value]} _]
11+
(fn [{:keys [schema value negated]} _]
1012
(let [{:keys [min max]} (m/properties schema)]
1113
(cond
1214
(not (pred value)) message
1315
(and min (= min max)) (str "should be " min)
14-
(and min (< value min)) (str "should be at least " min)
15-
max (str "should be at most " max)))))
16+
(and min ((if negated >= <) value min)) (str "should be at least " min)
17+
max (str "should be at most " max)
18+
negated message))))
19+
20+
(let [prefix (str "-en-humanize-negation-" (random-uuid))]
21+
(defn- -en-humanize-negation [{:keys [schema negated] :as error} options]
22+
(if negated
23+
(negated (error-message (dissoc error :negated) options))
24+
(let [remove-prefix #(str/replace-first % prefix "")
25+
negated? #(str/starts-with? % prefix)]
26+
(loop [schema schema]
27+
(or (when-some [s (error-message (assoc error :negated #(some->> % (str prefix))) options)]
28+
(if (negated? s)
29+
(remove-prefix s)
30+
(or (when (and (string? s)
31+
(str/starts-with? s "should not "))
32+
(str/replace-first s "should not" "should"))
33+
(when (and (string? s)
34+
(str/starts-with? s "should "))
35+
(str/replace-first s "should" "should not")))))
36+
(let [dschema (m/deref schema)]
37+
(when-not (identical? schema dschema)
38+
(recur dschema)))))))))
39+
40+
(defn- -forward-negation [?schema {:keys [negated] :as error} options]
41+
(let [schema (m/schema ?schema options)]
42+
(negated (error-message (-> error (dissoc :negated) (assoc :schema schema)) options))))
1643

1744
(def default-errors
1845
{::unknown {:error/message {:en "unknown error"}}
@@ -64,8 +91,8 @@
6491
'uri? {:error/message {:en "should be a uri"}}
6592
#?@(:clj ['decimal? {:error/message {:en "should be a decimal"}}])
6693
'inst? {:error/message {:en "should be an inst"}}
67-
'seqable? {:error/message {:en "should be a seqable"}}
68-
'indexed? {:error/message {:en "should be an indexed"}}
94+
'seqable? {:error/message {:en "should be seqable"}}
95+
'indexed? {:error/message {:en "should be indexed"}}
6996
'map? {:error/message {:en "should be a map"}}
7097
'vector? {:error/message {:en "should be a vector"}}
7198
'list? {:error/message {:en "should be a list"}}
@@ -79,30 +106,33 @@
79106
#?@(:clj ['rational? {:error/message {:en "should be a rational"}}])
80107
'coll? {:error/message {:en "should be a coll"}}
81108
'empty? {:error/message {:en "should be empty"}}
82-
'associative? {:error/message {:en "should be an associative"}}
83-
'sequential? {:error/message {:en "should be a sequential"}}
109+
'associative? {:error/message {:en "should be associative"}}
110+
'sequential? {:error/message {:en "should be sequential"}}
84111
#?@(:clj ['ratio? {:error/message {:en "should be a ratio"}}])
85112
#?@(:clj ['bytes? {:error/message {:en "should be bytes"}}])
86113
:re {:error/message {:en "should match regex"}}
87-
:=> {:error/message {:en "invalid function"}}
114+
:=> {:error/message {:en "should be a valid function"}}
88115
'ifn? {:error/message {:en "should be an ifn"}}
89-
'fn? {:error/message {:en "should be an fn"}}
116+
'fn? {:error/message {:en "should be a fn"}}
90117
:enum {:error/fn {:en (fn [{:keys [schema]} _]
91118
(str "should be "
92119
(if (= 1 (count (m/children schema)))
93120
(-pr-str (first (m/children schema)))
94121
(str "either " (->> (m/children schema) butlast (map -pr-str) (str/join ", "))
95122
" or " (-pr-str (last (m/children schema)))))))}}
123+
:not {:error/fn {:en (fn [{:keys [schema] :as error} options]
124+
(-en-humanize-negation (assoc error :schema (-> schema m/children first)) options))}}
96125
:any {:error/message {:en "should be any"}}
97126
:nil {:error/message {:en "should be nil"}}
98-
:string {:error/fn {:en (fn [{:keys [schema value]} _]
127+
:string {:error/fn {:en (fn [{:keys [schema value negated]} _]
99128
(let [{:keys [min max]} (m/properties schema)]
100129
(cond
101130
(not (string? value)) "should be a string"
102131
(and min (= min max)) (str "should be " min " character" (when (not= 1 min) "s"))
103-
(and min (< (count value) min)) (str "should be at least " min " character"
104-
(when (not= 1 min) "s"))
105-
max (str "should be at most " max " character" (when (not= 1 max) "s")))))}}
132+
(and min ((if negated >= <) (count value) min)) (str "should be at least " min " character"
133+
(when (not= 1 min) "s"))
134+
max (str "should be at most " max " character" (when (not= 1 max) "s"))
135+
negated "should be a string")))}}
106136
:int {:error/fn {:en (-pred-min-max-error-fn {:pred int?, :message "should be an integer"})}}
107137
:double {:error/fn {:en (-pred-min-max-error-fn {:pred double?, :message "should be a double"})}}
108138
:boolean {:error/message {:en "should be a boolean"}}
@@ -111,22 +141,30 @@
111141
:qualified-keyword {:error/message {:en "should be a qualified keyword"}}
112142
:qualified-symbol {:error/message {:en "should be a qualified symbol"}}
113143
:uuid {:error/message {:en "should be a uuid"}}
114-
:> {:error/fn {:en (fn [{:keys [schema value]} _]
115-
(if (number? value)
116-
(str "should be larger than " (first (m/children schema)))
117-
"should be a number"))}}
118-
:>= {:error/fn {:en (fn [{:keys [schema value]} _]
119-
(if (number? value)
120-
(str "should be at least " (first (m/children schema)))
121-
"should be a number"))}}
122-
:< {:error/fn {:en (fn [{:keys [schema value]} _]
123-
(if (number? value)
124-
(str "should be smaller than " (first (m/children schema)))
125-
"should be a number"))}}
126-
:<= {:error/fn {:en (fn [{:keys [schema value]} _]
127-
(if (number? value)
128-
(str "should be at most " (first (m/children schema)))
129-
"should be a number"))}}
144+
:> {:error/fn {:en (fn [{:keys [schema value negated] :as error} options]
145+
(if negated
146+
(-forward-negation [:<= (first (m/children schema))] error options)
147+
(if (number? value)
148+
(str "should be larger than " (first (m/children schema)))
149+
"should be a number")))}}
150+
:>= {:error/fn {:en (fn [{:keys [schema value negated] :as error} options]
151+
(if negated
152+
(-forward-negation [:< (first (m/children schema))] error options)
153+
(if (number? value)
154+
(str "should be at least " (first (m/children schema)))
155+
"should be a number")))}}
156+
:< {:error/fn {:en (fn [{:keys [schema value negated] :as error} options]
157+
(if negated
158+
(-forward-negation [:>= (first (m/children schema))] error options)
159+
(if (number? value)
160+
(str "should be smaller than " (first (m/children schema)))
161+
"should be a number")))}}
162+
:<= {:error/fn {:en (fn [{:keys [schema value negated] :as error} options]
163+
(if negated
164+
(-forward-negation [:> (first (m/children schema))] error options)
165+
(if (number? value)
166+
(str "should be at most " (first (m/children schema)))
167+
"should be a number")))}}
130168
:= {:error/fn {:en (fn [{:keys [schema]} _]
131169
(str "should be " (-pr-str (first (m/children schema)))))}}
132170
:not= {:error/fn {:en (fn [{:keys [schema]} _]

0 commit comments

Comments
 (0)