Full code example:
(ns api.foo
(:require [clojure.spec.alpha :as s]))
(s/def ::common-email (s/and string? (partial re-matches #"^[a-z0-9._%+-]+@[a-z0-9.-]+\.[a-z]{2,63}$")))
;(s/def :foo/email (s/and ::common-email))
(s/def :foo/email ::common-email)
(s/def :db/foo (s/keys :req [:foo/email]))
(comment
(->> (s/explain-data :db/foo {:foo/email "bar"})
::s/problems))
*Issue:*
({:path [:foo/email],
:pred (clojure.core/partial clojure.core/re-matches #"^[a-z0-9._%+-]+@[a-z0-9.-]+\.[a-z]{2,63}$"),
:val "bar",
:via [:db/foo :api.foo/common-email],
:in [:foo/email]})
*Dirty hack*
But if I use `(s/def :foo/email (s/and ::common-email))` instead it return
({:path [:foo/email],
:pred (clojure.core/partial clojure.core/re-matches #"^[a-z0-9._%+-]+@[a-z0-9.-]+\.[a-z]{2,63}$"),
:val "bar",
:via [:db/foo :foo/email :api.foo/common-email],
:in [:foo/email]})
*expected behaviour*
{{(s/def :foo/email ::common-email)}}
return
{{:via [:db/foo :foo/email :api.foo/common-email]}}
*What happen here?*
{{[:db/foo :api.foo/common-email]}} vs {{[:db/foo :foo/email :api.foo/common-email]}}
So {{(s/def :foo/email ::common-email)}} is totally omit here. In my opinion it is a bug, not a feature ;)
*Why is it important to fix?*
It is important to keep it full tracked to turn this errors to right communication in User Interface.
So `[:db/foo :foo/email :api.foo/common-email]`, I am looking if I have proper message for UI. First `:api.foo/common-email`. No message, then check `:foo/email`. I have message for it so I can return "E-mail is not valid".
In practice it can be `:user/password` with split spec for length, special characters etc. validation or `:company/vat-id` based on country. But always I don't want to keep final validation at that moment, because things like street, phone number, vat-id, email etc. are common and I want to have one definition in one place.
On top of it I can do for example `(s/def :user/email (s/and (s/conformer clojure.string/lower-case) ::s-common/email))`. Here is the point. But not all e-mail validations will do lower-case. So today I have to use dirty hack or have redundant e-mail validation in all places which is harder to maintenance.
I don't want to base on `::common-email`, because this part is considered to change in any moment. It can be bookkeeping library with common definition for European Union vat-id. I don't want to base messages for UI for this library, I want to base on my definitions in my code, but `(s/def :company/vat-id ::bookkeeping-library/vat-id)` lose in `:via`.
We can imagine it is deeper and more complex structure. At that moment figuring out what is the issue of fail is rocket science, that is why I use dirty hack `(s/def :foo/email (s/and ::common-email))` to read it simple from `:via`.