工具当中需要检测数据格式, 试着用了一下 Clojure Spec.
如果英文好, 直接看文档就行了, 也不用这篇笔记, 太琐碎了, 也缺失例子...
https://clojure.org/guides/sp...
例子我整理在了 spec-examples 仓库, 可以用 Lumo 直接跑.
首先添加依赖, 因为我在 ClojureScript 当中用, 所以用了 cljs.spec
这个代码.
expound 是一个用于美化 Spec 输出的类库, 直接引用进来.
[cljs.spec.alpha :as s]
[expound.alpha :refer [expound]]
首先是一个很简单觉得例子, 有 s/valid?
判断数据是否符合格式.
首先用 s/def
定义好一个校验的规则, 其中 ::example
会按照命名空间展开.
(s/def ::example boolean?)
(println (s/valid? ::example 1)) ; false
(println (s/valid? ::example true)) ; true
基础的校验用的是函数, 也可以是 string?
.
s/conform
表示返回输出的值.. 当然这个是正确的情况, 返回了匹配到的字符串,
(s/def ::example string?)
(println (s/conform ::example "DEMO")) ; DEMO
如果不匹配, 返回值就是 invalid,
(println (s/conform number? ""))
:cljs.spec.alpha/invalid
可以通过 s/explain
来打印失败的原因,
(s/def ::example string?)
(println (s/explain ::example 1))
; 1 - failed: string? spec: :app.main/example
可以看到这个原因比较精确, 但是可读性不怎么样, 就可以用 expound
替换了, 可读性会好很多,
(s/def ::example string?)
(println (expound ::example 1))
-- Spec failed --------------------
1
should satisfy
string?
-- Relevant specs -------
:app.main/example:
cljs.core/string?
-------------------------
Detected 1 error
既然校验规则是函数, 也可以写成,
(s/def ::example #(and (> % 6) (< % 20)))
(println (s/valid? ::example 1)) ; false
(println (s/valid? ::example 10)) ; true
(println (s/valid? ::example 20)) ; false
校验规则也可以组合使用, 最简单就是 s/or
, 注意参数中奇数位置都用的 keyword,
(s/def ::example (s/or :as-number number? :as-boolean boolean?))
(let [data 0]
(if (s/valid? ::example data)
(println (s/conform ::example data))
(println (expound ::example data)))))
打印的结果是,
[:as-number 0]
s/or
里直接用函数式简写了, 可以专门定义两个规则出来, 然后再使用,
(s/def ::boolean boolean?)
(s/def ::number number?)
(s/def ::example (s/or :as-number ::number :as-boolean ::boolean))
(if (s/valid? ::example 20)
(println (s/conform ::example 20))
(println (expound ::example 20)))
返回依然得到数据,
[:as-number 20]
对于数组的结构的数据, 用 s/coll-of
来判别,
(s/def ::number number?)
(s/def ::example (s/coll-of ::number))
(let [data [1]]
(if (s/valid? ::example data)
(println (s/conform ::example data))
(println (expound ::example data))))
得到,
[1]
s/coll-of
还支持比如 :count
这样的校验, 具体可以再看文档,
(s/def ::example (s/coll-of number? :count 2))
(defn task! []
(let [data [1]]
(if (s/valid? ::example data)
(println (s/conform ::example data))
(println (expound ::example data)))))
-- Spec failed --------------------
[1]
should satisfy
(= 2 (count %))
-- Relevant specs -------
:app.main/example:
(cljs.spec.alpha/coll-of cljs.core/number? :count 2)
-------------------------
Detected 1 error
对于 Map, 用 s/keys
来判断, :req-un
表示必选项, opt-un
是可选项,
(s/def ::age number?)
(s/def ::name string?)
(s/def ::example (s/keys :req-un [::age] :opt-un [::name]))
(let [data {:age 1, :name "a"}]
(if (s/valid? ::example data)
(println (s/conform ::example data))
(println (expound ::example data))))
得到,
{:age 1, :name a}
如果不满足校验规则, 会准确提示出来, 比如可选项的规则不满足,
(s/def ::age number?)
(s/def ::name string?)
(s/def ::example (s/keys :req-un [::age] :opt-un [::name]))
(let [data {:age 1, :name 1}]
(if (s/valid? ::example data)
(println (s/conform ::example data))
(println (expound ::example data))))
-- Spec failed --------------------
{:age ..., :name 1}
^
should satisfy
string?
-- Relevant specs -------
:app.main/name:
cljs.core/string?
:app.main/example:
(cljs.spec.alpha/keys :req-un [:app.main/age] :opt-un [:app.main/name])
-------------------------
Detected 1 error
上面用到的 -un
的后缀表示 "unqualified", 如果没有后缀, 意味着 keyword 要根据命名空间展开,
(s/def ::age number?)
(s/def ::name string?)
(s/def ::example (s/keys :req [::age] :opt [::name]))
(let [data {:age 1, :name 1}]
(if (s/valid? ::example data)
(println (s/conform ::example data))
(println (expound ::example data))))
于是就不满足了,
-- Spec failed --------------------
{:age 1, :name 1}
should contain key: :app.main/age
| key | spec |
|---------------+---------|
| :app.main/age | number? |
-- Relevant specs -------
:app.main/example:
(cljs.spec.alpha/keys :req [:app.main/age] :opt [:app.main/name])
-------------------------
Detected 1 error
就需要改写一下 key, 也用 ::x
的语法带上命名空间,
(s/def ::age number?)
(s/def ::name string?)
(s/def ::example (s/keys :req [::age] :opt [::name]))
(let [data {::age 1, ::name "a"}]
(if (s/valid? ::example data)
(println (s/conform ::example data))
(println (expound ::example data))))
得到,
{:app.main/age 1, :app.main/name a}
Spec 也可以对字符串进行校验, 同时也可以解析得到数据,
其中需要用到 (s/conformer seq)
来对字符串进行转化...
这个写法目前我也不够清晰, 参考了一下例子,
https://gist.github.com/thege...
(s/def ::left-paren #{"("})
(s/def ::right-paren #{")"})
(s/def ::space (s/and string? (s/conformer seq) (s/+ #{" "})))
(s/def ::token (s/and string? (s/conformer seq) (s/+ #{"a" "b" "c"})))
(s/def
::example
(s/cat
:left-paren
::left-paren
:expr
(s/+ (s/or :token ::token :space ::space))
:right-paren
::right-paren))
(let [data (seq "(a b)")]
(if (s/valid? ::example data)
(println (pr-str (s/conform ::example data)))
(println (s/explain ::example data))))
最终得到,
{:left-paren "(", :expr [[:token ["a"]] [:space [" "]] [:token ["b"]]], :right-paren ")"}
更多
另外关于 multi-spec 的例子, 还有生成代码的例子, 我在 GitHub 上整理了,
https://github.com/jiyinyiyon...
代码比较成就不复制了.
另外细节的功能没有记录, 具体要看官方文档. https://clojure.org/guides/sp...