August 11, 2019
By: 马海强

clojure luminus开发之常用spec

  1. 一般定义:
  2. 加namespace
  3. 用coll-of定义出一个list结构
  4. 指定长度的字符串:
  5. 使用函数验证参数合法性
  6. 定义数组
  7. 另类的使用

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库入门学习

Tags: clojure web