Malli 中文文档
Table of Contents
- 1. 引言
- 2. 库介绍
- 3. 快速开始
- 4. 语法
- 5. 示例地址模式
- 6. 验证
- 7. 枚举模式
- 8. Map 中的限定键
- 9. 同质化 Maps
- 10. Map 与默认模式
- 11. 可序列化的模式
- 12. 序列模式
- 13. 向量模式
- 14. 集合模式
- 15. 字符串模式
- 16. 可能的模式
- 17. 函数模式
- 18. 错误消息
- 19. 人性化错误消息
- 20. 自定义错误消息
- 21. 拼写检查
- 22. 错误中的值
- 23. 开发模式
- 24. 值转换
- 25. 持久化模式
- 26. 多模式
- 27. 递归模式
- 28. 值生成
- 29. 推断模式
- 30. 描述
- 31. 链接(以及感谢)
- 32. Alpha 版本
- 33. 开发
Malli 是一个强大而灵活的数据驱动模式库, 适用于 Clojure 和 ClojureScript.
1. 引言
Malli 是一个数据驱动的模式库, 专为 Clojure 和 ClojureScript 设计. 它基于现有库的最佳部分, 并结合了我们多年来开发的多个项目特定工具, 旨在满足动态系统开发中所有重要需求.
如果你对别人未能满足你的期望有期待, 这些期待是你自己的责任. 你需要为自己的需求负责. 如果你想要某些东西, 就去创造它们.
- Rich Hickey, 开源不关乎你
2. 库介绍
3. 快速开始
(require '[malli.core :as m]) (def UserId :string) (def Address [:map [:street :string] [:country [:enum "FI" "UA"]]]) (def User [:map [:id #'UserId] [:address #'Address] [:friends [:set {:gen/max 2} [:ref #'User]]]]) (require '[malli.generator :as mg]) (mg/generate User)
{:id "PP3D5Mr7",
:address {:street "4NlqZ", :country "FI"},
:friends
#{{:id "k", :address {:street "", :country "FI"}, :friends #{}}}}
(m/validate User *1)
false
4. 语法
Malli 支持 Vector语法 Map语法 和 简洁语法.
4.1. Vector语法
默认语法使用向量, 灵感来自 hiccup
type [type & children] [type properties & children]
示例:
;; 仅类型(字符串) :string ;; 带属性的类型 [:string {:min 1, :max 10}] ;; 带属性和子项的类型 [:tuple {:title "location"} :double :double] ;; 一个函数模式: 从 :int 到 :int [:=> [:cat :int] :int] [:-> :int :int]
| :string |
| [:string {:min 1, :max 10}] |
| [:tuple {:title "location"} :double :double] |
| [:=> [:cat :int] :int] |
| [:-> :int :int] |
用法:
(require '[malli.core :as m]) (def non-empty-string (m/schema [:string {:min 1}])) (m/schema? non-empty-string) ; => true (m/validate non-empty-string "") ; => false (m/validate non-empty-string "kikka") ; => true (m/form non-empty-string) ; => [:string {:min 1}]
[:string {:min 1}]
4.2. Map 语法
类似于 cljfx 的替代 map 语法:
注意: 目前, Map 语法被视为内部使用, 因此不要将其用作数据库持久化模型.
;; 仅类型(字符串) {:type :string} ;; 带属性的类型 {:type :string :properties {:min 1, :max 10}} ;; 带属性和子项的类型 {:type :tuple :properties {:title "location"} :children [{:type :double} {:type :double}]} ;; 一个函数模式: 从 :int 到 :int {:type :=> :input {:type :cat, :children [{:type :int}]} :output :int} {:type :-> :children [{:type :int} {:type :int}]}
| {:type :string} |
| {:type :string, :properties {:min 1, :max 10}} |
| {:type :tuple, :properties {:title "location"}, :children [{:type :double} {:type :double}]} |
| {:type :=>, :input {:type :cat, :children [{:type :int}]}, :output :int} |
| {:type :->, :children [{:type :int} {:type :int}]} |
用法:
(def non-empty-string (m/from-ast {:type :string :properties {:min 1}})) (m/schema? non-empty-string) ; => true (m/validate non-empty-string "") ; => false (m/validate non-empty-string "kikka") ; => true (m/ast non-empty-string) ; => {:type :string, ; :properties {:min 1}}
| #'user/non-empty-string |
| true |
| false |
| true |
| {:type :string, :properties {:min 1}} |
4.3. 为什么有多种语法?
Malli 最初只有 vector-syntax. 它非常强大且相对易读, 但并不适用于所有用例.
又引入了map-syntax, 因为我们发现解析大量向量语法在像 JavaScript 移动端这样运行缓慢的单线程环境中可能成为瓶颈. Map 语法允许延迟和无需解析的模式创建.
还添加了 lite 作为简化的模式创建, 适用于特殊情况, 例如与 reitit coercion 一起使用, 并且便于从 data-specs 迁移.
5. 示例地址模式
以下示例模式在许多后续示例中假定存在.
(def Address [:map [:id string?] [:tags [:set keyword?]] [:address [:map [:street string?] [:city string?] [:zip int?] [:lonlat [:tuple double? double?]]]]])
#'user/Address
6. 验证
根据模式验证值:
;; 使用模式实例 (m/validate (m/schema :int) 1) ; => true ;; 使用向量语法 (m/validate :int 1) ; => true (m/validate :int "1") ; => false (m/validate [:= 1] 1) ; => true (m/validate [:enum 1 2] 1) ; => true (m/validate [:and :int [:> 6]] 7) ; => true (m/validate [:qualified-keyword {:namespace :aaa}] :aaa/bbb) ; => true ;; 优化的(纯粹的)验证函数, 性能最佳 (def valid? (m/validator [:map [:x :boolean] [:y {:optional true} :int] [:z :string]])) (valid? {:x true, :z "kikka"}) ; => true
| true |
| true |
| false |
| true |
| true |
| true |
| true |
| #'user/valid? |
| true |
模式可以有属性:
(def Age [:and {:title "Age" :description "It's an age" :json-schema/example 20} :int [:> 18]]) (m/properties Age) ; => {:title "Age" ; :description "It's an age" ; :json-schema/example 20}
| #'user/Age |
| {:title "Age", :description "It's an age", :json-schema/example 20} |
默认情况下, Maps 是开放的:
(m/validate [:map [:x :int]] {:x 1, :extra "key"}) ; => true
true
可以使用 :closed 属性关闭 Maps:
(m/validate [:map {:closed true} [:x :int]] {:x 1, :extra "key"}) ; => false
Maps 的键不限于关键词:
(m/validate [:map ["status" [:enum "ok"]] [1 :any] [nil :any] [::a :string]] {"status" "ok" 1 'number nil :yay ::a "properly awesome"}) ; => true
true
大多数核心谓词都映射到模式:
(m/validate string? "kikka") ; => true
true
参见 [默认模式注册表的完整列表](#schema-registry).
7. 枚举模式
:enum 模式 [:enum V1 V2 ...] 表示一个枚举值集合 V1 V2 ....
这大多数情况下如你所愿, 值如果包含在集合中则通过模式, 生成器返回其中一个值, 并缩减到最左边的值.
需要注意一些关于语法的特殊情况. 由于模式属性可以使用 map 或 nil, 枚举以 map 或 nil 开始时必须使用稍微不同的语法.
如果你的 :enum 没有属性, 必须提供 nil 作为属性.
[:enum nil {}] ;; 单例模式, 值为 {} [:enum nil nil] ;; 单例模式, 值为 nil
如果你的 :enum 有属性, 前导 map 会被解释为属性, 而不是枚举值.
[:enum {:foo :bar} {}] ;; 单例模式, 值为 {}, 具有属性 {:foo :bar} [:enum {:foo :bar} nil] ;; 单例模式, 值为 nil, 具有属性 {:foo :bar}
实际上, 这些语法规则适用于所有模式, 但 :enum 是最常见的需要注意的模式, 因此特别提及.
8. Map 中的限定键
你也可以使用 解构的 map 键和值 使用注册表引用. 引用必须是限定关键词或字符串.
(m/validate [:map {:registry {::id int? ::country string?}} ::id [:name string?] [::country {:optional true}]] {::id 1 :name "kikka"}) ; => true
9. 同质化 Maps
有时, 我们使用 map 作为同质索引. 在这种情况下, 所有键值对具有相同的类型. 对于这种用例, 我们可以使用 :map-of 模式.
(m/validate [:map-of :string [:map [:lat number?] [:long number?]]] {"oslo" {:lat 60 :long 11} "helsinki" {:lat 60 :long 24}}) ;; => true
true
10. Map 与默认模式
Map 模式可以定义一个特殊的 :malli.core/default 键来处理额外的键:
(m/validate [:map [:x :int] [:y :int] [::m/default [:map-of :int :int]]] {:x 1, :y 2, 1 1, 2 2}) ; => true
true
默认分支可以任意嵌套:
(m/validate [:map [:x :int] [::m/default [:map [:y :int] [::m/default [:map-of :int :int]]]]] {:x 1, :y 2, 1 1, 2 2}) ; => true
true
11. 可序列化的模式
:seqable 和 :every 模式描述 seqable? 集合. 它们在处理既不是 counted? 也不是 indexed? 的集合以及它们的 [解析器](#parsing-values) 上有所不同:
:seqable解析其元素, 而:every不解析并返回相同的输入.- 有效的未解析
:seqable值会丢失原始集合类型, 而:every返回相同的输入.
:seqable 验证整个集合, 而 :every 仅检查 :min, (inc :max) 和 (::m/coll-check-limit options 101) 的最大值, 或者如果输入是 counted? 或 indexed? 则检查整个集合.
;; :seqable 和 :every 在小型, 已计数或已索引的集合上验证相同 (m/validate [:seqable :int] #{1 2 3}) ;=> true (m/validate [:seqable :int] [1 2 3]) ;=> true (m/validate [:seqable :int] (sorted-set 1 2 3)) ;=> true (m/validate [:seqable :int] (range 1000)) ;=> true (m/validate [:seqable :int] (conj (vec (range 1000)) nil)) ;=> false (m/validate [:every :int] #{1 2 3}) ;=> true (m/validate [:every :int] [1 2 3]) ;=> true (m/validate [:every :int] (sorted-set 1 2 3)) ;=> true (m/validate [:every :int] (vec (range 1000))) ;=> true (m/validate [:every :int] (conj (vec (range 1000)) nil)) ;=> false ;; 对于大型未计数和未索引的集合, :every 仅检查特定长度 (m/validate [:seqable :int] (concat (range 1000) [nil])) ;=> false (m/validate [:every :int] (concat (range 1000) [nil])) ;=> true
12. 序列模式
你可以使用 :sequential 来描述同质的顺序 Clojure 集合.
(m/validate [:sequential any?] (list "this" 'is :number 42)) ;; => true (m/validate [:sequential int?] [42 105]) ;; => true (m/validate [:sequential int?] #{42 105}) ;; => false
| true |
| true |
| false |
Malli 还支持类似 Seqexp 和 Spec 的序列正则表达式(也称为序列表达式). 支持的操作符包括 :cat 和 :catn 用于连接/序列化:
(m/validate [:cat string? int?] ["foo" 0]) ; => true (m/validate [:catn [:s string?] [:n int?]] ["foo" 0]) ; => true
:alt 和 :altn 用于替代:
(m/validate [:alt keyword? string?] ["foo"]) ; => true (m/validate [:altn [:kw keyword?] [:s string?]] ["foo"]) ; => true
以及 :?, :*, :+ 和 :repeat 用于重复:
(m/validate [:? int?] []) ; => true (m/validate [:? int?] [1]) ; => true (m/validate [:? int?] [1 2]) ; => false (m/validate [:* int?] []) ; => true (m/validate [:* int?] [1 2 3]) ; => true (m/validate [:+ int?] []) ; => false (m/validate [:+ int?] [1]) ; => true (m/validate [:+ int?] [1 2 3]) ; => true (m/validate [:repeat {:min 2, :max 4} int?] [1]) ; => false (m/validate [:repeat {:min 2, :max 4} int?] [1 2]) ; => true (m/validate [:repeat {:min 2, :max 4} int?] [1 2 3 4]) ; => true (:max 是包含的, 如同其他地方在 Malli 中) (m/validate [:repeat {:min 2, :max 4} int?] [1 2 3 4 5]) ; => false
:catn 和 :altn 允许命名子序列/替代项:
(m/explain [:* [:catn [:prop string?] [:val [:altn [:s string?] [:b boolean?]]]]] ["-server" "foo" "-verbose" 11 "-user" "joe"]) ;; => {:schema [:* [:map [:prop string?] [:val [:map [:s string?] [:b boolean?]]]]], ;; :value ["-server" "foo" "-verbose" 11 "-user" "joe"], ;; :errors ({:path [0 :val :s], :in [3], :schema string?, :value 11} ;; {:path [0 :val :b], :in [3], :schema boolean?, :value 11})}
而 :cat 和 :alt 则仅使用数字索引作为路径:
(m/explain [:* [:cat string? [:alt string? boolean?]]] ["-server" "foo" "-verbose" 11 "-user" "joe"]) ;; => {:schema [:* [:cat string? [:alt string? boolean?]]], ;; :value ["-server" "foo" "-verbose" 11 "-user" "joe"], ;; :errors ({:path [0 1 0], :in [3], :schema string?, :value 11} ;; {:path [0 1 1], :in [3], :schema boolean?, :value 11})}
所有这些示例显示, 序列表达式(seqex)操作符接受任何非 seqex 子模式, 意味着匹配该模式的单个元素序列. 要强制这种行为, 可以使用 :schema:
(m/validate [:cat [:= :names] [:schema [:* string?]] [:= :nums] [:schema [:* number?]]] [:names ["a" "b"] :nums [1 2 3]]) ; => true ;; 而 (m/validate [:cat [:= :names] [:* string?] [:= :nums] [:* number?]] [:names "a" "b" :nums 1 2 3]) ; => true
尽管在 seqex 实现上投入了大量精力以提高速度:
(require '[clojure.spec.alpha :as s]) (require '[criterium.core :as cc]) (let [valid? (partial s/valid? (s/* int?))] (cc/quick-bench (valid? (range 10)))) ; 执行时间均值: 27µs (let [valid? (m/validator [:* int?])] (cc/quick-bench (valid? (range 10)))) ; 执行时间均值: 2.7µs
但总是最好尽可能使用更少通用的工具:
(let [valid? (partial s/valid? (s/coll-of int?))] (cc/quick-bench (valid? (range 10)))) ; 执行时间均值: 1.8µs (let [valid? (m/validator [:sequential int?])] (cc/quick-bench (valid? (range 10)))) ; 执行时间均值: 0.12µs
13. 向量模式
你可以使用 :vector 来描述同质的 Clojure 向量.
(m/validate [:vector int?] [1 2 3]) ;; => true (m/validate [:vector int?] (list 1 2 3)) ;; => false
:tuple 模式描述固定长度的 Clojure 向量, 包含异质元素:
(m/validate [:tuple keyword? string? number?] [:bing "bang" 42]) ;; => true
要基于 seqex 创建向量模式, 使用 :and
;; 非空向量, 以关键词开头 (m/validate [:and [:cat :keyword [:* :any]] vector?] [:a 1]) ; => true (m/validate [:and [:cat :keyword [:* :any]] vector?] (:a 1)) ; => false
注意: 要从向量 seqex 生成值, 请参见 [:and 生成](#and-generation).
14. 集合模式
你可以使用 :set 来描述同质的 Clojure 集合.
(m/validate [:set int?] #{42 105}) ;; => true (m/validate [:set int?] #{:a :b}) ;; => false
15. 字符串模式
使用谓词:
(m/validate string? "kikka")
使用 :string 模式:
(m/validate :string "kikka") ;; => true (m/validate [:string {:min 1, :max 4}] "") ;; => false
使用正则表达式:
(m/validate #"a+b+c+" "abbccc") ;; => true ;; 使用字符串的 :re (m/validate [:re ".{3,5}"] "abc") ;; => true ;; 使用正则表达式的 :re (m/validate [:re #".{3,5}"] "abc") ;; => true ;; 注意: re-find 语义 (m/validate [:re #"\d{4}"] "1234567") ;; => true ;; 如果你想严格匹配整个字符串, 请使用 ^...$ (m/validate [:re #"^\d{4}$"] "1234567") ;; => false
16. 可能的模式
使用 :maybe 表示元素应该匹配某个模式或为 nil:
(m/validate [:maybe string?] "bingo") ;; => true (m/validate [:maybe string?] nil) ;; => true (m/validate [:maybe string?] :bingo) ;; => false
17. 函数模式
:fn 允许使用任何谓词函数:
(def my-schema [:and [:map [:x int?] [:y int?]] [:fn (fn [{:keys [x y]}] (> x y))]]) (m/validate my-schema {:x 1, :y 0}) ; => true (m/validate my-schema {:x 1, :y 2}) ; => false
18. 错误消息
使用 m/explain 获取详细错误:
(m/explain Address {:id "Lillan" :tags #{:artesan :coffee :hotel} :address {:street "Ahlmanintie 29" :city "Tampere" :zip 33100 :lonlat [61.4858322, 23.7854658]}}) ; => nil (m/explain Address {:id "Lillan" :tags #{:artesan "coffee" :garden} :address {:street "Ahlmanintie 29" :zip 33100 :lonlat [61.4858322, nil]}}) ; => {:schema [:map ; [:id string?] ; [:tags [:set keyword?]] ; [:address [:map ; [:street string?] ; [:city string?] ; [:zip int?] ; [:lonlat [:tuple double? double?]]]]], ; :value {:id "Lillan", ; :tags #{:artesan :garden "coffee"}, ; :address {:street "Ahlmanintie 29" ; :zip 33100 ; :lonlat [61.4858322 nil]}}, ; :errors ({:path [:tags 0] ; :in [:tags 0] ; :schema keyword? ; :value "coffee"} ; {:path [:address :city], ; :in [:address :city], ; :schema [:map ; [:street string?] ; [:city string?] ; [:zip int?] ; [:lonlat [:tuple double? double?]]], ; :type :malli.core/missing-key} ; {:path [:address :lonlat 1] ; :in [:address :lonlat 1] ; :schema double? ; :value nil})}
在 :errors 下, 你会得到一个错误列表, 每个错误包含以下键:
:path, 模式中的错误位置:in, 值中的错误位置:schema, 错误中的模式:value, 错误中的值
(def Schema [:map [:x boolean?] [:y [:maybe [:tuple :string]]]]) (def value {:x 1}) (def error (-> Schema (m/explain value) :errors first)) error ; => {:path [:x 0 0] ; :in [:x 0] ; :schema :string ; :value 1} (get-in value (:in error)) ; => 1 (mu/get-in Schema (:path error)) ; => :string
注意! 如果你需要可以整齐序列化为 EDN/JSON 的错误消息, 请使用 malli.util/explain-data.
19. 人性化错误消息
可以使用 malli.error/humanize 将解释结果人性化:
(require '[malli.error :as me]) (-> Address (m/explain {:id "Lillan" :tags #{:artesan "coffee" :garden} :address {:street "Ahlmanintie 29" :zip 33100 :lonlat [61.4858322, nil]}}) (me/humanize)) ; => {:tags #{["应该是关键词"]} ; :address {:city ["缺少必需的键"] ; :lonlat [nil ["应该是 double"]]}}
或者如果你已经有一个 malli 验证异常(例如在 catch 表达式中):
(require '[malli.error :as me]) (try (m/validate Address {:not "an address"}) (catch Exception e (-> e ex-data :data :explain me/humanize)))
20. 自定义错误消息
错误消息可以通过 :error/message 和 :error/fn 属性自定义:
(-> [:map [:id int?] [:size [:enum {:error/message "应该是: S|M|L"} "S" "M" "L"]] [:age [:fn {:error/fn (fn [{:keys [value]} _] (str value ", 应该 > 18"))} (fn [x] (and (int? x) (> x 18)))]]] (m/explain {:size "XL", :age 10}) (me/humanize {:errors (-> me/default-errors (assoc ::m/missing-key {:error/fn (fn [{:keys [in]} _] (str "缺少键 " (last in)))}))})) ; => {:id ["缺少键 :id"] ; :size ["应该是: S|M|L"] ; :age ["10, 应该 > 18"]}
消息可以本地化:
(-> [:map [:id int?] [:size [:enum {:error/message {:en "should be: S|M|L" :fi "pitäisi olla: S|M|L"}} "S" "M" "L"]] [:age [:fn {:error/fn {:en (fn [{:keys [value]} _] (str value ", 应该 > 18")) :fi (fn [{:keys [value]} _] (str value ", pitäisi olla > 18"))}} (fn [x] (and (int? x) (> x 18)))]]] (m/explain {:size "XL", :age 10}) (me/humanize {:locale :fi :errors (-> me/default-errors (assoc-in ['int? :error-message :fi] "pitäisi olla numero") (assoc ::m/missing-key {:error/fn {:en (fn [{:keys [in]} _] (str "缺少键 " (last in))) :fi (fn [{:keys [in]} _] (str "puuttuu avain " (last in)))}}))})) ; => {:id ["puuttuu avain :id"] ; :size ["pitäisi olla: S|M|L"] ; :age ["10, pitäisi olla > 18"]}
顶级人性化的 map 错误位于 :malli/error:
(-> [:and [:map [:password string?] [:password2 string?]] [:fn {:error/message "密码不匹配"} (fn [{:keys [password password2]}] (= password password2))]] (m/explain {:password "secret" :password2 "faarao"}) (me/humanize)) ; => {:malli/error ["密码不匹配"]}
错误可以通过 :error/path 属性定向:
(-> [:and [:map [:password string?] [:password2 string?]] [:fn {:error/message "密码不匹配" :error/path [:password2]} (fn [{:keys [password password2]}] (= password password2))]] (m/explain {:password "secret" :password2 "faarao"}) (me/humanize)) ; => {:password2 ["密码不匹配"]}
默认情况下, 仅使用直接错误的模式属性:
(-> [:map [:foo {:error/message "条目失败"} :int]] ;; 这里, :int 失败, 没有错误属性 (m/explain {:foo "1"}) (me/humanize)) ; => {:foo ["应该是整数"]}
通过自定义 :resolve 来从父模式查找人性化错误(BETA, 可能会改变):
(-> [:map [:foo {:error/message "条目失败"} :int]] (m/explain {:foo "1"}) (me/humanize {:resolve me/-resolve-root-error})) ; => {:foo ["条目失败"]}
21. 拼写检查
对于封闭的模式, 可以通过以下方式检查键的拼写:
(-> [:map [:address [:map [:street string?]]]] (mu/closed-schema) (m/explain {:name "Lie-mi" :address {:streetz "Hämeenkatu 14"}}) (me/with-spell-checking) (me/humanize)) ; => {:address {:street ["缺少必需的键"] ; :streetz ["应该拼写为 :street"]} ; :name ["不允许的键"]}
22. 错误中的值
仅获取错误中的值部分:
(-> Address (m/explain {:id "Lillan" :tags #{:artesan "coffee" :garden "ground"} :address {:street "Ahlmanintie 29" :zip 33100 :lonlat [61.4858322, "23.7832851,17"]}}) (me/error-value)) ; => {:tags #{"coffee" "ground"} ; :address {:lonlat [nil "23.7832851,17"]}}
屏蔽无关部分:
(-> Address (m/explain {:id "Lillan" :tags #{:artesan "coffee" :garden "ground"} :address {:street "Ahlmanintie 29" :zip 33100 :lonlat [61.4858322, "23.7832851,17"]}}) (me/error-value {::me/mask-valid-values '...})) ; => {:id ... ; :tags #{"coffee" "ground" ...} ; :address {:street ... ; :zip ... ; :lonlat [... "23.7832851,17"]}}
## 美化错误
有两种方式获取美化的错误:
23. 开发模式
启动开发模式:
((requiring-resolve 'malli.dev/start!))
现在, 任何通过 malli.core/-fail! 抛出的异常都会被捕获并在抛出之前进行美化. 美化是可扩展的, 使用 virhe.
美化的 Coercion:
自定义异常(使用默认布局):
美化打印由 malli.dev.virhe/-format 多方法支持, 默认使用 (-> exception (ex-data) :data) 作为调度键. 作为后备, 使用异常类或其子类, 例如以下方法将处理所有 java.sql.SQLException 及其父异常:
(require '[malli.dev.virhe :as v]) (defmethod v/-format java.sql.SQLException [e _ printer] {:title "抛出的异常" :body [:group (v/-block "SQL 异常" (v/-color :string (ex-message e) printer) printer) :break :break (v/-block "更多信息: " (v/-link "https://cljdoc.org/d/metosin/malli/CURRENT" printer) printer)]})
23.1. pretty/explain
对于开发时的美化错误打印, 尝试 malli.dev.pretty/explain
24. 值转换
(require '[malli.transform :as mt])
使用 Transformer 实例, 通过 m/decode 和 m/encode 进行双向模式驱动的值转换.
默认的 Transformer 包括:
| 名称 | 描述 |
|---|---|
mt/string-transformer |
在字符串和 EDN 之间转换 |
mt/json-transformer |
在 JSON 和 EDN 之间转换 |
mt/strip-extra-keys-transformer |
从 maps 中删除额外的键 |
mt/default-value-transformer |
应用来自模式属性的默认值 |
mt/key-transformer |
转换 map 键 |
mt/collection-transformer |
集合之间的转换(例如 set -> vector) |
注意: 包含的 Transformer 是尽力而为的, 即它们不会在坏输入上抛出错误, 而只是将输入值保持不变. 你应该确保你的模式验证捕获这些未转换的值. 自定义 Transformer 应遵循相同的习惯用法.
简单用法:
(m/decode int? "42" mt/string-transformer) ; => 42 (m/encode int? 42 mt/string-transformer) ; => "42"
为了提高性能, 预计算转换使用 m/decoder 和 m/encoder:
(def decode (m/decoder int? mt/string-transformer)) (decode "42") ; => 42 (def encode (m/encoder int? mt/string-transformer)) (encode 42) ; => "42"
24.1. 强制转换
对于同时解码和验证结果(在错误时抛出异常), 使用 m/coerce 和 m/coercer:
(m/coerce :int "42" mt/string-transformer) ; => 42 ((m/coercer :int mt/string-transformer) "42") ; => 42 (m/coerce :int "invalid" mt/string-transformer) ; => 抛出 :malli.core/invalid-input 异常 {:value "invalid", :schema :int, :explain {:schema :int, :value "invalid", :errors ({:path [], :in [], :schema :int, :value "invalid"})}}
转换可以在没有 Transformer 的情况下应用, 仅进行验证:
(m/coerce :int 42) ; => 42 (m/coerce :int "42") ; => 抛出 :malli.core/invalid-input 异常 {:value "42", :schema :int, :explain {:schema :int, :value "42", :errors ({:path [], :in [], :schema :int, :value "42"})}}
使用续接风格进行无异常转换:
(m/coerce :int "fail" nil (partial prn "success:") (partial prn "error:")) ; => 打印 "error:" {:value "fail", :schema :int, :explain ...}
24.2. 高级转换
转换是递归的:
(m/decode Address {:id "Lillan", :tags ["coffee" "artesan" "garden"], :address {:street "Ahlmanintie 29" :city "Tampere" :zip 33100 :lonlat [61.4858322 23.7854658]}} mt/json-transformer) ; => {:id "Lillan", ; :tags #{:coffee :artesan :garden}, ; :address {:street "Ahlmanintie 29" ; :city "Tampere" ; :zip 33100 ; :lonlat [61.4858322 23.7854658]}}
转换 map 键:
(m/encode Address {:id "Lillan", :tags ["coffee" "artesan" "garden"], :address {:street "Ahlmanintie 29" :city "Tampere" :zip 33100 :lonlat [61.4858322 23.7854658]}} (mt/key-transformer {:encode name})) ; => {"id" "Lillan", ; "tags" ["coffee" "artesan" "garden"], ; "address" {"street" "Ahlmanintie 29" ; "city" "Tampere" ; "zip" 33100 ; "lonlat" [61.4858322 23.7854658]}}
转换同质的 :enum 或 :=~(支持自动类型检测 ~:keyword, :symbol, :int 和 :double):
(m/decode [:enum :kikka :kukka] "kukka" mt/string-transformer) ; => :kukka
可以将 Transformers 组合使用 mt/transformer:
(def strict-json-transformer (mt/transformer mt/strip-extra-keys-transformer mt/json-transformer)) (m/decode Address {:id "Lillan", :EVIL "LYN" :tags ["coffee" "artesan" "garden"], :address {:street "Ahlmanintie 29" :DARK "ORKO" :city "Tampere" :zip 33100 :lonlat [61.4858322 23.7854658]}} strict-json-transformer) ; => {:id "Lillan", ; :tags #{:coffee :artesan :garden}, ; :address {:street "Ahlmanintie 29" ; :city "Tampere" ; :zip 33100 ; :lonlat [61.4858322 23.7854658]}}
模式属性可以用于覆盖默认转换:
(m/decode [string? {:decode/string clojure.string/upper-case}] "kerran" mt/string-transformer) ; => "KERRAN"
这也可以这样做:
(m/decode [string? {:decode {:string clojure.string/upper-case}}] "kerran" mt/string-transformer) ; => "KERRAN"
作为拦截器的解码器和编码器(具有 :enter 和 :leave 阶段):
(m/decode [string? {:decode/string {:enter clojure.string/upper-case}}] "kerran" mt/string-transformer) ; => "KERRAN"
(m/decode [string? {:decode/string {:enter #(str "olipa_" %) :leave #(str % "_avaruus")}}] "kerran" mt/string-transformer) ; => "olipa_kerran_avaruus"
要访问模式(以及选项), 使用 :compile:
(m/decode [int? {:math/multiplier 10 :decode/math {:compile (fn [schema _] (let [multiplier (:math/multiplier (m/properties schema))] (fn [x] (* x multiplier))))}}] 12 (mt/transformer {:name :math})) ; => 120
疯狂使用:
(m/decode [:map {:decode/math {:enter #(update % :x inc) :leave #(update % :x (partial * 2))}} [:x [int? {:decode/math {:enter (partial + 2) :leave (partial * 3)}}]]] {:x 1} (mt/transformer {:name :math})) ; => {:x 24}
:and 从左到右累积转换的值.
(m/decode [:and [:map [:name string?] [:description string?]] [:and [:fn (fn [{:keys [name description]}] (not (empty? name)))] [:fn (fn [{:keys [name description]}] (not (empty? description)))]]] {:name "kikka", :description "kukka"} (mt/transformer mt/default-value-transformer)) ; => {:name "kikka", :description "kukka"}
:or 使用第一个成功的模式进行转换, 按顺序.
(m/decode [:or [:map [:name string?]] [:map [:name int?]]] {:name "kikka"} (mt/transformer mt/default-value-transformer)) ; => {:name "kikka"} (m/decode [:or :map [:map [:name string?]]] {:name "kikka"} (mt/transformer mt/default-value-transformer)) ; => {:name "kikka"}
代理模式如 :merge 和 :union 类似于 m/deref.
(m/decode [:merge [:map [:name string?]] [:map [:description string?]]] {:name "kikka", :description "kukka"}) ; => {:name "kikka", :description "kukka"} (m/decode [:union [:map [:name string?]] [:map [:description string?]]] {:name "kikka"}) ; => {:name "kikka"} (m/decode [:union [:map [:name string?]] [:map [:description string?]]] {:description "kukka"}) ; => {:description "kukka"}
25. 持久化模式
将模式写入和读取为 EDN, 无需 eval.
以下示例需要 SCI 或 cherry 作为外部依赖, 因为它包含一个(引用的)函数定义. 参见 [可序列化函数](#serializable-functions).
(require '[malli.edn :as edn]) (-> [:and [:map [:x :int] [:y :int]] [:fn '(fn [{:keys [x y]}] (> x y))]] (edn/write-string) (doto prn) ; => "[:and [:map [:x :int] [:y :int]] [:fn (fn [{:keys [x y]}] (> x y))]]" (edn/read-string) (doto (-> (m/validate {:x 0, :y 1}) prn)) ; => false (doto (-> (m/validate {:x 2, :y 1}) prn))) ; => true ; => [:and ; [:map ; [:x :int] ; [:y :int]] ; [:fn (fn [{:keys [x y]}] (> x y))]]
26. 多模式
使用 :multi 模式和 :dispatch 属性进行封闭调度:
(m/validate [:multi {:dispatch :type} [:sized [:map [:type keyword?] [:size int?]]] [:human [:map [:type keyword?] [:name string?] [:address [:map [:country keyword?]]]]]] {:type :sized, :size 10}) ; => true
使用 ::m/default 分支:
(def valid? (m/validator [:multi {:dispatch :type} ["object" [:map-of :keyword :string]] [::m/default :string]])) (valid? {:type "object", :key "1", :value "100"}) ; => true (valid? "SUCCESS!") ; => true (valid? :failure) ; => false
任何函数都可以用作 :dispatch:
(m/validate [:multi {:dispatch first} [:sized [:tuple keyword? [:map [:size int?]]]] [:human [:tuple keyword? [:map [:name string?] [:address [:map [:country keyword?]]]]]]] [:human {:name "seppo", :address {:country :sweden}}]) ; => true
:dispatch 值应在实际值之前被解码:
(m/decode [:multi {:dispatch :type :decode/string #(update % :type keyword)} [:sized [:map [:type [:= :sized]] [:size int?]]] [:human [:map [:type [:= :human]] [:name string?] [:address [:map [:country keyword?]]]]]] {:type "human" :name "Tiina" :age "98" :address {:country "finland" :street "this is an extra key"}} (mt/transformer mt/strip-extra-keys-transformer mt/string-transformer)) ; => {:type :human ; :name "Tiina" ; :address {:country :finland}}
27. 递归模式
要创建递归模式, 引入 [本地注册表](#local-registry) 并在注册表中使用 :ref 包装所有递归位置. 现在你可以在模式体中引用递归模式.
例如, 这里是一个使用 :schema 的递归模式, 表示单链表的正整数:
(m/validate [:schema {:registry {::cons [:maybe [:tuple pos-int? [:ref ::cons]]]}} ::cons] [16 [64 [26 [1 [13 nil]]]]]) ; => true
没有 :ref 关键字, Malli 会急切地展开模式, 直到堆栈溢出错误:
(m/validate [:schema {:registry {::cons [:maybe [:tuple pos-int? ::cons]]}} ::cons] [16 [64 [26 [1 [13 nil]]]]]) ; 堆栈溢出错误
实际上, 你只需要在递归位置使用 :ref. 然而, 最好在所有递归引用位置使用 :ref 以确保生成器行为良好:
;; 注意: [:schema {:registry {::cons [:maybe [:tuple pos-int? [:ref ::cons]]]}} ::cons] ;; 生成器与 "展开的" 模式相同 [:maybe [:tuple pos-int? [:schema {:registry {::cons [:maybe [:tuple pos-int? [:ref ::cons]]]}} ::cons]]] ;; 而 [:schema {:registry {::cons [:maybe [:tuple pos-int? [:ref ::cons]]]}} [:ref ::cons]] ;; 直接对应于以下生成器: (gen/recursive-gen (fn [rec] (gen/one-of [(gen/return nil) (gen/tuple rec)])) (gen/return nil))
28. 值生成
模式可以用来生成值:
(require '[malli.generator :as mg]) ;; 随机 (mg/generate keyword?) ; => :? ;; 使用种子 (mg/generate [:enum "a" "b" "c"] {:seed 42}) ;; => "a" ;; 使用种子和大小 (mg/generate pos-int? {:seed 10, :size 100}) ;; => 55740 ;; 正则表达式也有效(仅在 clj 和 [com.gfredericks/test.chuck "0.2.10"+] 可用) (mg/generate [:re #"^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,63}$"] {:seed 42, :size 10}) ; => "CaR@MavCk70OHiX.yZ" ;; :gen/return (注意, 不是验证) (mg/generate [:and {:gen/return 42} :int]) ; => 42 ;; :gen/elements (注意, 不是验证) (mg/generate [:and {:gen/elements ["kikka" "kukka" "kakka"]} string?] {:seed 10}) ; => "kikka" ;; :gen/fmap (mg/generate [:and {:gen/fmap (partial str "kikka_")} string?] {:seed 10, :size 10}) ;; => "kikka_WT3K0yax2" ;; 可移植的 :gen/fmap(需要 ~org.babashka/sci~ 依赖才能工作) (mg/generate [:and {:gen/fmap '(partial str "kikka_")} string?] {:seed 10, :size 10}) ;; => "kikka_nWT3K0ya7" ;; :gen/schema (mg/generate [:any {:gen/schema [:int {:min 10, :max 20}]}] {:seed 10}) ; => 19 ;; :gen/min 和 :gen/max 对数字和集合有效 (mg/generate [:vector {:gen/min 4, :gen/max 4} :int] {:seed 1}) ; => [-8522515 -1433 -1 1] ;; :gen/infinite? 和 :gen/NaN? 对 :double 有效 (mg/generate [:double {:gen/infinite? true, :gen/NaN? true}] {:seed 1}) ; => ##Inf
生成的值是有效的:
(mg/generate Address {:seed 123, :size 4}) ; => {:id "H7", ; :tags #{:v?.w.t6!.QJYk-/-?s*4 ; :_7U ; :QdG/Xi8J ; :*Q-.p*8*/n-J9u} ; :address {:street "V9s" ; :city "" ; :zip 3 ; :lonlat [-2.75 -0.625]}}
28.1. :and 生成
:and 模式的生成器通过从第一个子模式生成值, 然后过滤掉不满足整个 :and 模式的值.
为了获得最可靠的结果, 将最有可能生成整个模式有效值的模式放在 :and 模式的第一个子模式.
;; 不好: :string 不太可能生成满足模式的值 (mg/generate [:and :string [:enum "a" "b" "c"]] {:seed 42}) ; => 执行错误 ; 无法在 100 次尝试后满足 such-that 谓词.
;; 好: ~:enum~ 的每个值都是字符串 (mg/generate [:and [:enum "a" "b" "c"] :string] {:seed 42}) ; => "a"
你可能需要自定义第一个 :and 子模式的生成器以提高生成有效值的机会.
例如, 一个非空异质向量的模式可以通过结合 :cat 和 vector? 进行验证, 但由于 :cat 生成序列, 我们需要使用 :gen/fmap 使其生成向量:
;; 生成一个以关键词开头的非空向量 (mg/generate [:and [:cat {:gen/fmap vec} :keyword [:* :any]] vector?] {:size 1 :seed 2}) ; => [:.+ [1]]
29. 推断模式
受 F# 类型提供者 的启发:
(require '[malli.provider :as mp]) (def samples [{:id "Lillan" :tags #{:artesan :coffee :hotel} :address {:street "Ahlmanintie 29" :city "Tampere" :zip 33100 :lonlat [61.4858322, 23.7854658]}} {:id "Huber", :description "Beefy place" :tags #{:beef :wine :beer} :address {:street "Aleksis Kiven katu 13" :city "Tampere" :zip 33200 :lonlat [61.4963599 23.7604916]}}]) (mp/provide samples) ;[:map ; [:id :string] ; [:tags [:set :keyword]] ; [:address ; [:map ; [:street :string] ; [:city :string] ; [:zip :int] ; [:lonlat [:vector :double]]]] ; [:description {:optional true} :string]]
所有样本都符合推断的模式:
(every? (partial m/validate (mp/provide samples)) samples) ; => true
为了提高性能, 使用 mp/provider:
(require '[criterium.core :as p]) ;; 5ms (p/bench (mp/provide samples)) ;; 500µs (10倍) (let [provider (mp/provider)] (p/bench (provider samples)))
29.1. :map-of 推断
默认情况下, :map-of 不被推断:
(mp/provide [{"1" [1]} {"2" [1 2]} {"3" [1 2 3]}]) ; => [:map ; ["1" {:optional true} [:vector :int]] ; ["2" {:optional true} [:vector :int]] ; ["3" {:optional true} [:vector :int]]]
使用 ::mp/map-of-threshold 选项:
(mp/provide [{"1" [1]} {"2" [1 2]} {"3" [1 2 3]}] {:::mp/map-of-threshold 3}) ; => [:map-of :string [:vector :int]]
样本数据可以用 ::mp/hint 类型提示:
(mp/provide [^{::mp/hint :map-of} {:a {:b 1, :c 2} :b {:b 2, :c 1} :c {:b 3} :d nil}]) ; => [:map-of ; :keyword ; [:maybe [:map ; [:b :int] ; [:c :int]]]]
29.2. :tuple 推断
默认情况下, :tuple 不被推断:
(mp/provide [[1 "kikka" true] [2 "kukka" true] [3 "kakka" true]]) ; => [:vector :some]
使用 ::mp/tuple-threshold 选项:
(mp/provide [[1 "kikka" true] [2 "kukka" true] [3 "kakka" false]] {:::mp/tuple-threshold 3}) ; => [:tuple :int :string :boolean]
样本数据可以用 ::mp/hint 类型提示:
(mp/provide [^{::mp/hint :tuple} [1 "kikka" true] ["2" "kukka" true]]) ; => [:tuple :some string? boolean?]
### 值解码在推断中的应用
默认情况下, 不对(叶子)值应用解码:
(mp/provide [{:id "caa71a26-5fe1-11ec-bf63-0242ac130002"} {:id "8aadbf5e-5fe3-11ec-bf63-0242ac130002"}]) ; => [:map [:id string?]]
通过 ::mp/value-decoders 选项添加自定义解码:
(mp/provide [{:id "caa71a26-5fe1-11ec-bf63-0242ac130002" :time "2021-01-01T00:00:00Z"} {:id "8aadbf5e-5fe3-11ec-bf63-0242ac130002" :time "2022-01-01T00:00:00Z"}] {::mp/value-decoders {:string {:uuid mt/-string->uuid 'inst? mt/-string->date}}}) ; => [:map [:id :uuid] [:time inst?]]
30. 描述
你可以调用 describe 获取模式的英文描述:
(require '[malli.experimental.describe :as med]) (med/describe [:map {:closed true} [:x :boolean] [:y :int]]) ;; => "map where {:x -> <boolean>, :y -> <int>} with no other keys"
31. 链接(以及感谢)
- Schema https://github.com/plumatic/schema
- Clojure.spec https://clojure.org/guides/spec
- Spell-spec https://github.com/bhauman/spell-spec
- JSON Schema https://json-schema.org/understanding-json-schema
- Spec-provider: https://github.com/stathissideris/spec-provider
- F# Type Providers: https://docs.microsoft.com/en-us/dotnet/fsharp/tutorials/type-providers/
- Minimallist https://github.com/green-coder/minimallist
- malli-instrument https://github.com/setzer22/malli-instrument
- Core.typed https://github.com/clojure/core.typed
- TypeScript https://www.typescriptlang.org/
- Struct https://funcool.github.io/struct/latest/
- Seqexp https://github.com/cgrand/seqexp
- yup https://github.com/jquense/yup
- JOI https://github.com/hapijs/joi
32. Alpha 版本
Malli 的公共 API 在 [pre-alpha](https://github.com/metosin/malli/issues/207) 和 alpha 版本中已经相当稳定, 我们尽量不破坏现有功能. 然而, 库仍在发展, 像 [值解构](https://github.com/metosin/malli/issues/241) 可能 会影响公共 API, 并 很可能 影响库的扩展者, 例如需要为自定义模式实现新的协议方法.
所有更改(无论是否破坏性)都将在 [CHANGELOG](CHANGELOG.md) 中记录, 并在需要时提供迁移指南和路径.
API 层和稳定性:
- 公共 API: 公共变量, 名称不以
-开头, 例如malli.core/validate. 库中最稳定的部分, alpha 版本中不应有(大)变动. - 扩展 API: 公共变量, 名称以
-开头, 例如malli.core/-collection-schema. 不需要基本用例, 可能在 alpha 期间发展, 详细信息请参见 [CHANGELOG](CHANGELOG.md). - 实验性: 位于
malli.experimental命名空间中的内容, 代码可能会被移动到单独的支持库, 但你总是可以将旧实现复制到项目中, 因此可以放心使用. - 私有 API: 私有变量和
malli.impl命名空间, 一切皆有可能.
33. 开发
Malli 欢迎贡献. 在提交 PR 之前, 请先为其打开一个 issue.
33.1. 添加新模式类型
要添加一个新的模式类型, 例如 :float, 你应添加以下内容:
- 在
malli.core中定义模式 + 测试 - 默认编码器/解码器映射到
malli.transform+ 测试 - JSON Schema 映射到
malli.json-schema+ 测试 - 生成器映射到
malli.generator+ 测试 - 可选: 向
malli.provider添加推断器 + 测试 - 更新
README.md
33.1.1. 运行测试
我们使用 [Kaocha](https://github.com/lambdaisland/kaocha) 和 [cljs-test-runner](https://github.com/Olical/cljs-test-runner) 作为测试运行器. 在运行测试之前, 你需要安装 NPM 依赖.
#+endsrcbash npm install ./bin/kaocha ./bin/node #+endsrc
### 本地安装
#+endsrcbash clj -Mjar clj -Minstall #+endsrc
33.1.2. ClojureScript 的包大小
使用默认注册表(37KB+ Gzipped)
# 没有 sci npx shadow-cljs run shadow.cljs.build-report app /tmp/report.html # 使用 sci npx shadow-cljs run shadow.cljs.build-report app-sci /tmp/report.html # 使用 cherry npx shadow-cljs run shadow.cljs.build-report app-cherry /tmp/report.html
使用最小注册表(2.4KB+ Gzipped)
# 没有 sci npx shadow-cljs run shadow.cljs.build-report app2 /tmp/report.html # 使用 sci npx shadow-cljs run shadow.cljs.build-report app2-sci /tmp/report.html # 使用 cherry npx shadow-cljs run shadow.cljs.build-report app2-cherry /tmp/report.html
33.1.3. 格式化代码
clojure-lsp format clojure-lsp clean-ns
33.1.4. 检查生成的代码
npx shadow-cljs release app --pseudo-names
33.1.5. 在 GraalVM 上测试
没有 sci(11Mb)
./bin/native-image demo ./demo '[:set :keyword]' '["kikka" "kukka"]'
有 sci(18Mb):
./bin/native-image demosci ./demosci '[:fn (fn [x] (and (int? x) (> x 10)))]]' '12'
33.1.6. Babashka
从版本 0.8.9 开始, Malli 与 [babashka](https://babashka.org/) 兼容, 这是一个用于脚本的本地, 快速启动的 Clojure 解释器.
你可以将 malli 添加到 bb.edn:
{:deps {metosin/malli {:mvn/version "0.9.0"}}}
或直接在 babashka 脚本中:
(ns bb-malli (:require [babashka.deps :as deps])) (deps/add-deps '{:deps {metosin/malli {:mvn/version "0.9.0"}}}) (require '[malli.core :as malli]) (prn (malli/validate [:map [:a [:int]]] {:a 1})) (prn (malli/explain [:map [:a [:int]]] {:a "foo"}))
33.1.7. 第三方库
- Aave, Clojure 代码检查工具.
- Gungnir, 用于 Clojure 数据映射的高级数据驱动数据库库.
- Regal, 皇家化的正则表达式.
- Reitit, 一个快速的数据驱动路由器, 适用于 Clojure/Script.
- wasm, 符合规范的 WebAssembly 编译器和反编译器.
- malli-instrument, 模仿 clojure.spec.alpha API 的 Malli 仪器.
- Snoop, 使用 Malli 模式的函数仪器化.
- malli-key-relations, 关于 map 键的关系模式.
- malli-cli, 命令行处理.
- malapropism, 基于 malli 的配置库.
- muotti, 带有 malli 支持的基于图的值转换库.
- malli-select, Malli 的 spec2 选择(当你只需要部分时的羊群 🐑).
33.2. 许可证
版权所有 © 2019-2022 Metosin Oy 及贡献者.
根据 Eclipse 公共许可证 2.0 版本的条款提供, 参见 LICENSE.