Malli 高级使用技巧与实践指南
Malli 是一个强大的 Clojure/Script 数据验证和转换库,提供了丰富的功能和灵活的扩展方式。本文将深入探讨 Malli 的一些高级使用技巧,帮助开发者更好地利用这个工具解决实际问题。
在转换中同时访问 Schema 和值
在实际开发中,我们经常需要在数据转换过程中同时访问 Schema 定义和实际值。Malli 提供了这种能力:
(m/decode
Address
lillan
(mt/transformer
{:default-decoder
{:compile (fn [schema _]
(fn [value]
(prn [value (m/form schema)])
value)}}))
这种技术特别适用于需要根据 Schema 定义对值进行特殊处理的场景,比如日志记录、调试或复杂的业务逻辑转换。
基于属性过滤 Schema
Malli 允许我们递归遍历 Schema 并根据属性进行过滤:
(m/walk
Schema
(fn [schema _ children options]
(when-not (:deleteMe (m/properties schema))
(let [children (if (m/entries schema) (filterv last children) children)]
(try (m/into-schema (m/type schema) (m/properties schema) children options)
(catch Exception _)))))
这个技巧可用于:
- 根据环境过滤敏感字段
- 动态调整 Schema 结构
- 实现条件性验证规则
字符串处理技巧
Malli 提供了灵活的字符串处理能力,例如自动修剪字符串:
(defn string-trimmer []
(mt/transformer
{:decoders
{:string
{:compile (fn [schema _]
(let [{:string/keys [trim]} (m/properties schema)]
(when trim #(cond-> % (string? %) str/trim))))}}}))
(m/decode [:string {:string/trim true, :min 1}] " kikka " string-trimmer)
;; => "kikka"
这种技术可以确保用户输入的数据符合格式要求,同时保持业务逻辑的简洁性。
集合类型解码
Malli 可以轻松处理集合类型的转换,例如将逗号分隔的字符串转换为整数向量:
(m/decode
[:vector {:decode/string #(str/split % #",")} int?]
"1,2,3,4"
(mt/string-transformer))
;; => [1 2 3 4]
对于更复杂的需求,我们可以创建自定义转换器:
(defn query-decoder [schema]
(m/decoder
schema
(mt/transformer
(mt/transformer
{:name "vectorize strings"
:decoders
{:vector
{:compile (fn [schema _]
(let [separator (-> schema m/properties :query/separator (or ","))]
(fn [x]
(cond
(not (string? x)) x
(str/includes? x separator) (into [] (.split ^String x separator))
:else [x]))))}}})
(mt/string-transformer))))
默认值的高级用法
Malli 不仅支持静态默认值,还可以通过函数动态计算默认值:
(defn default-fn-value-transformer
([]
(default-fn-value-transformer nil))
([{:keys [key] :or {key :default-fn}}]
(let [add-defaults
{:compile
(fn [schema _]
(let [->k-default (fn [[k {default key :keys [optional]} v]]
(when-not optional
(when-some [default (or default (some-> v m/properties key))]
[k default])))
defaults (into {} (keep ->k-default) (m/children schema))
exercise (fn [x defaults]
(reduce-kv (fn [acc k v]
(if-not (contains? x k)
(-> (assoc acc k ((m/eval v) x))
acc))
x defaults))]
(when (seq defaults)
(fn [x] (if (map? x) (exercise x defaults) x)))))}]
(mt/transformer
{:decoders {:map add-defaults}
:encoders {:map add-defaults}}))))
这种技术特别适用于:
- 计算派生字段
- 实现字段间的依赖关系
- 提供上下文相关的默认值
Schema 转换与映射
Malli 提供了强大的 Schema 转换能力,可以递归地修改 Schema 结构:
(defn schema-mapper [m]
(fn [s] ((or (get m (m/type s))
(get m ::default)
(constantly s))
s)))
(m/walk
[:map
[:id :keyword]
[:size :int]
[:tags [:set :keyword]]]
(m/schema-walker
(schema-mapper
{:keyword (constantly :string)
:int #(m/-set-properties % {:gen/elements [1 2]})
::default #(m/-set-properties % {::type (m/type %)})})))
这种技术可用于:
- 不同环境间的 Schema 转换
- 版本兼容性处理
- 生成测试数据
错误处理与人性化展示
Malli 提供了丰富的错误信息,可以将其转换为更友好的格式:
(-> [:map
[:x :int]
[:y [:set :keyword]]
(m/explain {:x "1" :y #{:a "b"}})
(me/humanize {:wrap #(select-keys % [:value :message])}))
;; => {:x [{:value "1" :message "should be an integer"}],
;; :y #{[{:value "b" :message "should be a keyword"}]}}
这种技术可以显著提升用户体验,特别是在表单验证等场景中。
总结
Malli 提供了丰富而灵活的功能,通过本文介绍的高级技巧,开发者可以:
- 实现复杂的数据转换逻辑
- 动态调整 Schema 结构
- 处理各种边界条件和特殊需求
- 提供更好的用户体验
这些技术可以组合使用,解决实际开发中遇到的各种数据验证和转换挑战。Malli 的设计理念强调组合性和可扩展性,使得开发者可以根据具体需求定制解决方案。
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考