This page is not created by, affiliated with, or supported by Slack Technologies, Inc.
2018-12-27
Channels
- # aleph (8)
- # announcements (14)
- # beginners (25)
- # cider (20)
- # cljdoc (5)
- # clojure (70)
- # clojure-europe (2)
- # clojure-germany (6)
- # clojure-italy (8)
- # clojure-nl (3)
- # clojure-russia (107)
- # clojure-spec (22)
- # clojure-uk (40)
- # clojurescript (18)
- # core-async (3)
- # cursive (8)
- # data-science (11)
- # datomic (20)
- # editors (1)
- # emacs (5)
- # figwheel-main (19)
- # fulcro (25)
- # graphql (1)
- # hoplon (2)
- # hyperfiddle (2)
- # jobs (1)
- # leiningen (3)
- # lumo (4)
- # off-topic (40)
- # pedestal (1)
- # quil (4)
- # re-frame (5)
- # shadow-cljs (105)
- # sql (4)
- # uncomplicate (1)
Привет! Можете объяснить концепцию/философию clojure.spec? Почему нет из коробки поддержки спецификаций для мапов? Вменяемой сторонней библиотеки тоже нет. Допустим, у меня есть следующие данные:
{:title "some title"
:content "some content"
:published-at (java.time.Instant/now)}
или
{:title "-"
:content nil
:published-at nil}
Т.е. если published-at - inst?
, то title и content - должы присутствовать в мапе, быть строками, а title быть длинее 3х символов.
Если published-at nil?
, то ключи могут отсутствовать, быть nil или любой строкой.
В дргих случаях мапа невалидна.
Получается, что в зависимости от внешних условий :titile
и :content
имеют разные спецификации.
Судя по тому, что сделали s/keys
и спецификации на отдельные ключи это by design нельзя делать.
А что делать, если хочется? Spec не для таких данных? А что, если данные были “подходящие“, а стали “неподходящие“?
Страдать, пока оно в alpha?(s/def ::title (s/nilable (s/and string?)))
(s/def ::content (s/nilable string?))
(s/def ::published-at (s/nilable inst?))
(s/def ::translation (s/and (s/keys :opt-un [::title ::content ::published-at])
(fn [{:keys [title content published-at]}]
(or (nil? published-at)
(and (> (count title) 3)
(not-empty content))))))
> А что, если данные были “подходящие“, а стали “неподходящие“? Это называется "сломать свой софт", и так делать нельзя.
(s/def ::title (s/and string? #(< 3 (count %))))
(s/def ::content string?)
(s/def ::published-at inst?)
(s/def ::translation (s/or
(s/keys :req-un [::title ::content ::published-at])
map?))
а если я захочу расширить ::translation
?
(s/def ::foo string?)
(s/def ::ext (s/merge ::translation (s/keys :req-un [::foo])))
(s/def ::title (s/and string? #(< 3 (count %))))
(s/def ::content string?)
(s/def ::published-at inst?)
(s/def :other.title (s/nilable string?))
(s/def :other.content (s/nilable string?))
(s/def :other.published-at nil?)
(s/def ::translation (s/or
(s/keys :req-un [::title ::content ::published-at])
(s/keys :req-un [:other.published-at]
:opt-un [:other.title :other.content])))
> данные были “подходящие“, а стали “неподходящие“? и теперь у меня не req-un, а req. и фейковые неймспейсы не работают
> в смысле добавить еще один вариант в or?
есть у меня где-то :some.ns/translation
в другом неймспейсе я хочу сделать версию перевода: (s/def :another.ns/translation (s/merge :some.ns/translation ....)
по идее все это можно свести к вопросу как на s/keys наложить ограничения с помощью своей функцией и потом к этому еще что-то смержить через s/merge
@kuzmin_m Да уже и так работает. Функция выбирает нужные поля и их валидирует. Остальное её не волнует.
хм, видимо у меня в первый раз что-то не завелось валидатор работает а с генератором нужно разобраться
Угу. Если ограничитель серьёзный, то спеке надо подсказать, иначе она будет генерить варианты миллиардами, и миллиардами же их откидывать.
(s/def ::title (s/nilable (s/and string?)))
(s/def ::content (s/nilable string?))
(s/def ::published-at (s/nilable inst?))
(s/def ::translation (s/and (s/keys :opt-un [::title ::content ::published-at])
(fn [{:keys [title content published-at]}]
(or (nil? published-at)
(and (>= (count title) 3)
(not-empty content))))))
(assert (s/valid? ::translation {}))
(assert (not (s/valid? ::translation {:title 1})))
(assert (s/valid? ::translation {:title nil}))
(assert (s/valid? ::translation {:published-at (java.time.Instant/now)
:title "123"
:content "some"}))
(gen ::translation)
(s/def ::foo string?)
(s/def ::ext (s/merge ::translation (s/keys :req-un [::foo])))
(gen ::ext)
т.е. логика тут такая для каждого ключа добавляем спеку на все возможные варианты а уже для “сущности” вешаем ограничения
вообще у меня возникло такое ощущение, что спека скорее о шейпе данных чем о валидации
потому что если валидацию доводить до совершенства и пользоваться спекой, то нечитаемый код получается
ребята из Attendify вот такое сделали https://github.com/KitApps/schema-refined
Где провести грань между спекой и валидацией?
(s/def ::title (s/nilable string?))
(s/def ::summary (s/nilable string?))
(s/def ::tags (s/coll-of string? :kind vector? :distinct true))
(s/def ::published-at (s/nilable inst?))
(s/def ::translation (s/and (s/keys :req-un [::title ::summary ::tags ::published-at])
(fn [{:keys [title summary published-at]}]
(or (nil? published-at)
(and (not-empty title)
(not-empty summary))))))
все что кроме (fn …) - спека, а (fn …) - валидация?@kuzmin_m посмотри на ту штуку что я кинул, там реально очень хорошая идея по валидации именно данных
типа int? ничего не говорит, а вот int в границах от 0 до 1000 это уже нормальный бизнес-кейс
там еще ссылка на слайды есть https://speakerdeck.com/kachayev/keep-your-data-safe-with-refined-types
я вот такое рисовал чтобы обойтись
(defmacro strict-keys
"The same as `clojure.spec.alpha/keys` but limits keyset to given keys"
[& {:keys [req opt req-un opt-un] :as keyspec}]
`(s/merge (s/keys :req ~req :opt ~opt :req-un ~req-un :opt-un ~opt-un)
(s/map-of (->> ~keyspec vals (reduce #(into %1 %2) #{})) any?)))
;;
(defmacro only-keys [& {:keys [req req-un opt opt-un] :as args}]
(let [keys-spec `(s/keys ~@(apply concat (vec args)))]
`(s/with-gen
(s/merge ~keys-spec
(s/map-of ~(set (concat req
(map (comp keyword name) req-un)
opt
(map (comp keyword name) opt-un)))
any?))
#(s/gen ~keys-spec))))
с генераторомвообще не знаю, кто-то заметил или нет, в кложуре 1.10 добавили в спеку возможность спеки удалять из регистра
(s/def ::translation (s/and (s/keys :req-un [::title ::summary ::tags ::published-at])
(s/or :not-published #(-> % :published-at nil?)
:published (s/and #(-> % :title not-empty)
#(-> % :summary not-empty)))))
вот мой вариант получше, если кому-то интересно
он более точный explain дает(s/def ::title (s/nilable string?))
(s/def ::summary (s/nilable string?))
(s/def ::tags (s/coll-of string? :kind vector? :distinct true))
(s/def ::published-at (s/nilable inst?))
(s/def ::translation (s/and (s/keys :req-un [::title ::summary ::tags ::published-at])
(s/or :not-published #(-> % :published-at nil?)
:published (s/and #(-> % :title not-empty)
#(-> % :summary not-empty)))))
можно еще вот так:
(s/def ::translation (s/and (s/keys :opt-un [::title ::summary ::published-at]
:req-un [::tags])
(s/or :not-published #(-> % :published-at nil?)
:published (s/and #(-> % :title not-empty)
#(-> % :summary not-empty)))))
(s/def ::translation (s/and (s/keys :req-un [::title ::summary ::tags] :opt-un [::published-at])
(s/or :not-published #(-> % :published-at nil?)
:published (s/and #(-> % :title not-empty)
#(-> % :summary not-empty)))))
(s/explain-data ::translation {:title "" :summary "" :tags "" :published-at ""})
;; => #:clojure.spec.alpha{:problems ({:path [:not-published], :pred (clojure.core/fn [%] (clojure.core/-> % :published-at clojure.core/nil?)), :val {:title "", :summary "", :tags "", :published-at ""}, :via [:eventum.fiken/translation], :in []} {:path [:published], :pred (clojure.core/fn [%] (clojure.core/-> % :title clojure.core/not-empty)), :val {:title "", :summary "", :tags "", :published-at ""}, :via [:eventum.fiken/translation], :in []}), :spec :eventum.fiken/translation, :value {:title "", :summary "", :tags "", :published-at ""}}
если выносить в отдельные спеки, то в ::published нужно добавить проверку #(-> % :published-at inst?)
(s/def ::published (s/and #(-> % :published-at some?)
#(-> % :title not-empty)
#(-> % :summary not-empty)))
(s/def ::not-published #(-> % :published-at nil?))
(s/def ::translation (s/and (s/keys :req-un [::title ::summary ::tags ::published-at])
(s/or :not-published ::not-published
:published ::published)))