完整的代码示例
(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))
*问题:*
({: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]})
*脏诡计(Hack)*
但如果我使用`(s/def :foo/email (s/and ::common-email))`,它将返回
({: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 Behavior)*
{{(s/def :foo/email ::common-email)}}
返回
{{:via [:db/foo :foo/email :api.foo/common-email]}}
*这里发生了什么事(What Happened Here)?*
{{[:db/foo :api.foo/common-email]}} vs {{[:db/foo :foo/email :api.foo/common-email]}}
所以这里{{(s/def :foo/email ::common-email)}}被完全忽略了。在我看来,这是一个错误,而不是一个特性(feature);)
*为什么修复它很重要(Why This Is Important to Fix)?*
保持它们完全跟踪,以便将错误转换为用户界面中的正确通信非常重要。
所以 `[:db/foo :foo/email :api.foo/common-email]`,我在寻找是否有恰当的消息给UI。首先 `:api.foo/common-email`。没有消息,然后检查`:foo/email`。我对它有消息,因此可以返回“电子邮件无效”。
在实践中,这可以是`:user/password`,具有针对长度、特殊字符等的拆分规范验证,或者是根据国家而定的`:company/vat-id`。但无论如何,我都不想在这一刻保留最终验证,因为地址、电话号码、vat-id、电子邮件等都是常用的,我希望有一个定义在一个地方。
除此之外,我还可以做例如`(s/def :user/email (s/and (s/conformer clojure.string/lower-case) ::s-common/email))`。这里是重点。但并不是所有电子邮件验证都会进行小写处理。因此,今天我必须使用脏诡计,或者在所有地方有冗余的电子邮件验证,这很困难维护。
我不想基于 `::common-email`,因为这部分可以在任何时刻更改。这可以是一个记账库,具有对欧洲联盟vat-id的通用定义。我不想为这个库基于UI的消息,我想基于我代码中的定义,但 `(s/def :company/vat-id ::bookkeeping-library/vat-id)` 在 `:via` 中丢失。
我们可以想象它要深得多而且更复杂。这时,确定失败的问题就是天文学(rocket science),这就是为什么我使用脏诡计 `(s/def :foo/email (s/and ::common-email))` 来简单地从 `:via` 中读取。