Clojure Spec - это новая функция, добавленная в Clojure 1.9, которая позволяет вам определять структуру ваших данных, проверять ваши данные и генерировать ваши данные. Clojure с динамической типизацией часто обнаруживает, что те, кто знаком с системой типов, с трудом понимают, чего ожидает каждая функция. Спецификация помогает преодолеть этот пробел, описывая набор допустимых значений. Поиграв со спецификацией Clojure, я пришел к выводу, что система типов по-прежнему оставляет много работы по утверждению вводимых значений.

Одна из лучших функций - это возможность генерировать данные на основе спецификации. Поскольку моя повседневная работа заключается в написании программного обеспечения и разработке инструментов для области ГИС (географических информационных систем), я расскажу, как вы можете использовать спецификации для создания и проверки данных выборки geojson. Ниже приведен пример точки, представленной в geojson.

{
  "type": "Feature",
  "geometry": {
    "type": "Point",
    "coordinates": [125.6, 10.1]
  },"properties": {
    "name": "Dinagat Islands"
  }
}

Начнем с изнанки. Запустите REPL. Для начала нам нужно два двойных значения широты и долготы.

(require '[clojure.spec :as s])
(s/def ::longitude double?)
(s/def ::latitude double?)

Теперь нам нужно объединить эти два значения ключа координат.

(s/def :wgs/coordinates (s/tuple ::longitude ::latitude))

(s/valid? :wgs/coordinates [1.0 2.0])
=> true 
(s/explain :wgs/coordinates [1.8 "d"])
In: [1] val: "d" fails spec: :clojure-spec-meetup-talk.core/latitude at: [1] predicate: double?
=> nil

Давайте сначала попробуем сгенерировать данные.

(gen/generate (s/gen :wgs/coordinates))
=> [-134.5 -1.0]

Давайте посмотрим, какие типы данных он может генерировать.

(type (clojure.spec.gen/generate (s/gen :wgs/coordinates))) 
=> clojure.lang.PersistentVector
(type (clojure.spec.gen/generate (s/gen :cart/coordinates)))
=> clojure.lang.LazySeq

Теперь, когда у нас есть возможность генерировать данные, давайте приступим к созданию спецификации для GeoJSON.

(s/def ::type #{"Point"})
(s/def ::geometry (s/keys :req-un [::type :wgs/coordinates]))

и сгенерируйте данные

(gen/generate (s/gen ::geometry))
=> {:type "Point", :coordinates [-0.01104752100945916 -0.017578125]}

или пробуйте спецификацию несколько раз

(gen/sample (s/gen ::geometry))
=>
({:type "Point", :coordinates [-2.0 2.0]}
 {:type "Point", :coordinates [-0.5 -2.0]}
 {:type "Point", :coordinates [-2.0 1.0]}
 {:type "Point", :coordinates [0.0 1.0]}
 {:type "Point", :coordinates [0.5 0.5]}
 {:type "Point", :coordinates [-2.0 -1.75]}
 {:type "Point", :coordinates [2.75 0.0]}
 {:type "Point", :coordinates [1.0 -1.75]}
 {:type "Point", :coordinates [-3.0 -1.0]}
 {:type "Point", :coordinates [2.53125 6.375]})

Теперь мы знаем, что есть разумные диапазоны широты и долготы. Здесь вы можете интегрировать бизнес-логику вашего домена в свои спецификации.

(s/def ::longitude (s/double-in :min -180.0 :max 180 :NaN false :infinite? false))
(s/def ::latitude (s/double-in :min -90.0 :max 90 :NaN false :infinite? false))
(s/def ::coordinates (s/tuple ::longitude ::latitude))
(s/def ::geometry (s/keys :req-un [::type ::coordinates]))

Теперь, когда координаты определены, нам нужно добавить другие части спецификации.

(s/def :feature/type #{"Feature"})
(s/def ::properties map?)
(s/def ::feature (s/keys :req-un [:feature/type ::geometry ::properties]))
(gen/sample (s/gen ::feature))
=> {:id "zOWGcd4B9",
 :type "Feature",
 :geometry {:id "77", :type "Point", :geometry {:type "Point", :coordinates [0.5 3.9921875]}, :properties {\B 4/3}},
 :properties {\y #uuid"6af3de71-717b-475d-b818-55a9a6dbc332",
              :bHlh_k .4,
              z?7+4!3oQ 0.75,
              -0.8974609375 -1.375,
              :JHb?.*65U96!71*.m2.+_86x_F/?TA1fFL!I1 3/8,
              -4 :raW6_*_.u._+50h.WY89+.bI2cs!/SR}}

Вот почему clojure.spec и генеративное тестирование - это круто. Посмотрев на properties, мы увидим, что JSON недопустим. Мы можем переопределить его так:

(s/def ::properties (s/map-of string? (s/or :s string? :n number? :m map? :c coll?)))
=> 
{:id "Kg3K2id2",
 :type "Feature",
 :properties {"6y" {:wQ6b!--.L+P127J._1-.lP3!-4.k_1LrI3jNf.M/*6r21*u2m A*.!5-L,
                    *3Q_-.-K._2kxd!OO?*._-*a1*.U.og1-W*+9r6-.DI3p/i_- #uuid"029e4e58-3613-4667-ae18-5027798121aa",
                    false true,
                    :V9C-!7 q-3.r.*C13IS-+!-/_**FbC-,
                    #uuid"ff256ba0-a0b6-4d46-b69f-e5cc55881edd" \V,
                    \X zh-D89+7A.ir24*l.o?.*H_-!4.F2**kx/Zu.Bq,
                    -1 :g_-8.BA.+ifII.Mz_Hd.k2_!nvMD*.hP8t!OcnW.R7.Ir-!/?XnA!}}}

properties ключ на карте - это строка, но мы не требовали, чтобы значение также было допустимым GeoJSON. Давайте попробуем дополнительно определить, что такое действительная карта JSON.

(s/def ::jsonobj (s/map-of string? (s/or :s string? :n number? :m ::jsonobj :c (s/coll-of ::jsonobj))))

Здесь мы можем использовать спецификацию, чтобы определить спецификацию. Единственная проблема заключается в том, что случайность этого будет привязать ваш процессор, потому что это создаст случайную глубину для этого вложения.

(s/def ::properties ::jsonobj) 
(s/def ::id (s/or :p pos-int? :s (s/and string? #(not (str/blank? %)))))
(s/def ::feature (s/keys :req-un [::id :feature/type ::geometry ::properties]))
(gen/sample (s/gen ::feature)) ; pegging the CPU time

Поэтому нам нужно подумать о недостатках генерации случайных данных. На данный момент я не знаю способа контролировать глубину вложенных карт JSON, поэтому вам придется прибегнуть к map? на данный момент.

Теперь давайте определим некоторые типы геометрии в GeoJSON со спецификациями.

; namespace point
(s/def :pt/type #{"Point"})
(s/def :pt/geometry (s/keys :req-un [:pt/type ::coordinates]))
(s/def :pt/feature (s/keys :req-un [::id :pt/type :pt/geometry ::properties]))
; namespace poly
(s/def :poly/type #{"Polygon"})
(defn circle-gen [x y]
  (let [vertices (+ (rand-int 8) 4)
        radius (rand 3) ;2 dec degrees radius length
        rads (/ (* 2.0 Math/PI) vertices)
        pts (map (fn [r]
                   [(+ x (* radius (Math/cos (* r rads))))
                    (+ y (* radius (Math/sin (* rads r))))])
                 (range vertices))]
    (conj pts (last pts))))
(s/def :poly/coordinates (s/with-gen
                           coll?
                           #(gen/fmap (fn [[lon lat]] (list (circle-gen lon lat)))
                                      (gen/tuple (s/gen ::longitude) (s/gen ::latitude)))))
(s/def :poly/geometry (s/keys :req [:poly/type :poly/coordinates]))
(s/def :poly/feature (s/keys :req-un [::id :poly/geometry :poly/type ::properties]))
(s/def :feat/geometry (s/or :poly/feature :pt/feature))
(s/def ::feature (s/keys :req-un [::id :feature/type :feat/geometry ::properties]))
(gen/sample (s/gen :poly/feature))
(s/def :gfeature/type (s/or :pt/type :poly/type))

circle-gen - это простой способ для нашего генератора генерировать полигоны. Тезисы не годятся для сложной ГИС-системы, потому что это простые выпуклые многоугольники, которые в реальном мире вам не очень помогут. Есть две стороны использования спецификации: проверка данных и генерация данных.

И в завершение, вот как мы определяем это в функции.

(s/def ::feature-spec (s/keys :req-un
                                 [::id :gfeature/type
                                  :feat/geometry ::properties]))
(s/def :gj/features (s/coll-of ::feature-spec))
(s/def :fc/type #{"FeatureCollection"})
(s/def ::featurecollection-spec (s/keys :req-un [:fc/type :gj/features]))
(gen/sample (s/gen ::featurecollection-spec))

Я начал делать библиотеку геометрии для Clojure здесь. Цель состоит в том, чтобы заставить работать с JTS и JSTS для Clojure и Clojurescript и оставаться в курсе последних событий.