clojure luminus开发之常用spec
spec库的使用需要引入[clojure.spec.alpha :as s]和[spec-tools.core :as st]。
spec是定义api的request和response结构并进行格式校验的非常好的工具,比如request里如果格式校验不通过直接返回400。
配合swagger-ui使用,能很大程度提高接口联调的效率,同时 有利于api的管理。
一般定义:
(s/def ::page
(st/spec
{:spec int?
:description "页码,从0开始"
:swagger/default "0"
:reason "页码参数不能为空"}))
(s/def ::size
(st/spec
{:spec int?
:description "每页条数"
:swagger/default 10
:reason "条数参数不能为空"}))
使用:
["/page"
{:get {:summary "分页获取字典数据"
:parameters {:query (s/keys :req-un [::page ::size])
:handler (s/keys :req-un [page]
:opt-un [size])}
:handler (fn [{{{:keys [page, size]} :query} :parameters :as request}]
{:status 200
:body {:code 10
:data {:total-elements (->> (db/get-dicts-page {:count true})
(map :total-elements)
(first))
:content (db/get-dicts-page {:page page
:size size})}}})}}]
加namespace
spec的参数也可以定义在其他namespace里,使用时加上namespace的名字即可,比如可以定义在一个叫base的namespace里 常用来定义通用的参数,比如:
(s/def :base/role
(st/spec
{:spec #{"PATIENT", "DOCTOR"}
:description "角色"
:reason "角色不能为空"}))
这个枚举类型的spec在另一个namespace里使用时不需要在require里引入这个base,而直接在spec里加namespace的名字,是这样的:
:parameters {:header (s/keys :req-un [:base/role])}
用coll-of定义出一个list结构
(s/def ::head-id id?)
(s/def ::url string?)
(s/def ::unmatched-head
(s/keys :req [::head-id ::url]))
(s/def ::unmatched-head-result
(st/spec
{:spec (s/coll-of ::unmatched-head)}))
再比定义一个下面的post的body体:
{
"patient-id": "string",
"patient-ext-list": [
{
"dict-id": 0,
"dict-type": "string",
"dict-value": "string",
"other-value": "string"
}
]
}
spec定义
(s/def ::dict-id int?)
(s/def ::dict-value string?)
(s/def ::dict-type string?)
(s/def ::other-value string?)
(s/def ::patient-ext-list (s/coll-of (s/keys :req-un [::dict-id ::dict-type ::dict-value ::other-value])))
(s/def ::ext-body (s/keys :req-un [:base/patient-id ::patient-ext-list]))
coll-of函数还接收可选的参数,用来对数组中的元素进行限制,可选参数有如下:
(1):kind- - - -可以指定数组的类型,vector,set,list等;
(2):count- - - -可以限定数组中元素的个数;
(3):min-count- - - -限定数组中元素个数的最小值
(4):max-count- - - -限定数组中元素个数的最大值
(5):distinct- - - -数组没有重复的元素
(6):into- - - -可以将数组的元素插入到[],(),{},#{}这些其中之一,主要是为了改变conform函数的返回结果
指定长度的字符串:
(s/def ::id
(st/spec
{:spec (s/and string? #(= (count %) 6))
:description "一个长度为6字符串"
:swagger/default "666666"
:reason "必须是长度为6的字符串"}))
使用函数验证参数合法性
(s/def ::head-body-id
(st/spec
{:spec (s/and string? (fn [s]
(let [[head-id body-id] (clojure.string/split s #"-")]
(and (s/valid? ::head-id head-id)
(s/valid? ::body-id body-id)))))
:description "一个长度为13字符串, head-id 和 body-id 用‘-’ 连起来"
:swagger/default "666666-999999"
:reason "必须是长度为13的字符串,用-把body-id和head-id连起来"}))
定义数组
(s/def ::dict-id [string?]) ;Good
(s/def ::dict-id vector?) ;Bad
另类的使用
有时候可能你的一个post有很多参数,有必须的和非必须的,如果根据上面的示例,你可能要先定义出这一堆字段,再指定一个名称将其包进来,这样难免有些啰嗦。
这时候可以使用spec的spec-tools.data-spec库,使用如下:
(require '[spec-tools.data-spec :as ds])
(def LungFuncBody
{:patient-id string?
:date string?
:pef float?
:pef-a float? ,
:fev1 float?,
:fev1-a float?,
:fev1-fvc float?,
:fev1-fvc_a float?,
:fef25 float?,
:fef25-a float?,
:fef50 float?,
:fef50-a float?,
:fef75 float?,
:fef75-a float?,
:fvc float?,
:fvc-a float?,
(ds/opt :img1) string?,
:img2 string?,
:img3 string?})
:post {:summary "添加肺功能"
:parameters {:body LungFuncBody}
:handler (fn [_])}
跟java定义一个对象一样,这样避免了较多的繁琐。
又有时候,你虽然定义字段是string,但是json里传个null也会400,因为spec不认为这是个string,这很好的避免了某些空指针的出现,但是如果想跟java里一样,让string也能接受null,请这么用:
(require '[clojure.spec.alpha :as s])
(s/nilable string?)
又有个一时候,你定义了一个map,然后用一个字段是这个map的集合,同时还要支持为null的情况,比如商品详情图,那就这么用:
(require '[clojure.spec.alpha :as s])
(require '[spec-tools.data-spec :as ds])
(def image {:type int?
:img string?})
(def detail-images
{:image (s/nilable [image])
(ds/opt :item) string?})
更多使用参考: clojure.spec库入门学习