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 и оставаться в курсе последних событий.