clojure-russia

Работа и релокейт: #jobs-rus | #clojure-russia-offtop Телеграм-чат https://t.me/clojure_ru
kuzmin_m 2018-12-27T16:32:14.010600Z

Привет! Можете объяснить концепцию/философию 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?

kuzmin_m 2018-12-27T16:44:26.011Z

(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))))))

kuzmin_m 2018-12-27T16:44:30.011300Z

вот так?

dottedmag 2018-12-27T16:47:14.011700Z

Да, так.

dottedmag 2018-12-27T16:47:35.012100Z

> А что, если данные были “подходящие“, а стали “неподходящие“? Это называется "сломать свой софт", и так делать нельзя.

dottedmag 2018-12-27T16:48:15.012400Z

См. https://www.youtube.com/watch?v=oyLBGkS5ICk

prepor 2018-12-27T16:53:35.013600Z

@kuzmin_m не, ну выразить-то такое можно довольно легко

prepor 2018-12-27T16:53:40.014100Z

(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?))

prepor 2018-12-27T16:53:44.014300Z

типа

prepor 2018-12-27T16:53:50.014700Z

но это так конечно себе спека )

kuzmin_m 2018-12-27T16:54:11.015100Z

а если я захочу расширить ::translation?

(s/def ::foo string?)
(s/def ::ext (s/merge ::translation (s/keys :req-un [::foo])))

kuzmin_m 2018-12-27T16:54:26.015600Z

оно же через s/and и будет ошибка

prepor 2018-12-27T16:55:26.015800Z

ну или более строго

prepor 2018-12-27T16:55:32.016200Z

(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])))

prepor 2018-12-27T16:56:07.017200Z

> а если я захочу расширить ::translation? в смысле добавить еще один вариант в or?

kuzmin_m 2018-12-27T16:56:29.017800Z

> данные были “подходящие“, а стали “неподходящие“? и теперь у меня не req-un, а req. и фейковые неймспейсы не работают

kuzmin_m 2018-12-27T16:57:48.019800Z

> в смысле добавить еще один вариант в or? есть у меня где-то :some.ns/translation в другом неймспейсе я хочу сделать версию перевода: (s/def :another.ns/translation (s/merge :some.ns/translation ....)

kuzmin_m 2018-12-27T16:58:03.020800Z

Т.е. к тем полям еще добавить полей

dottedmag 2018-12-27T16:58:52.022100Z

@kuzmin_m Да вроде норм. Добавлять поля можно, это ничего не ломает.

dottedmag 2018-12-27T16:59:05.022600Z

s/keys требует указанные поля, но не запрещает другие.

kuzmin_m 2018-12-27T16:59:05.022700Z

по идее все это можно свести к вопросу как на s/keys наложить ограничения с помощью своей функцией и потом к этому еще что-то смержить через s/merge

dottedmag 2018-12-27T16:59:48.023400Z

@kuzmin_m Да уже и так работает. Функция выбирает нужные поля и их валидирует. Остальное её не волнует.

kuzmin_m 2018-12-27T17:02:09.024200Z

хм, видимо у меня в первый раз что-то не завелось валидатор работает а с генератором нужно разобраться

kuzmin_m 2018-12-27T17:02:29.024600Z

т.е. я по спеке еще данные хочу генерировать

dottedmag 2018-12-27T17:03:04.025400Z

Угу. Если ограничитель серьёзный, то спеке надо подсказать, иначе она будет генерить варианты миллиардами, и миллиардами же их откидывать.

kuzmin_m 2018-12-27T17:03:33.025700Z

не, там именно ошибка была

kuzmin_m 2018-12-27T17:03:43.026Z

а не то, что он не смог сгенерировать

kuzmin_m 2018-12-27T17:09:35.026300Z

(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)

kuzmin_m 2018-12-27T17:09:42.026600Z

работает

kuzmin_m 2018-12-27T17:10:42.027700Z

т.е. логика тут такая для каждого ключа добавляем спеку на все возможные варианты а уже для “сущности” вешаем ограничения

dottedmag 2018-12-27T17:12:35.028Z

Да, это правильно.

kuzmin_m 2018-12-27T17:12:44.028400Z

ок, спасибо)

dottedmag 2018-12-27T17:13:00.028700Z

Хотя nilable не нужно. И так ведь opt-un?

dottedmag 2018-12-27T17:13:38.029500Z

Или у тебя поле с явным значением nil?

kuzmin_m 2018-12-27T17:13:50.029700Z

с явным nil

dottedmag 2018-12-27T17:13:54.030Z

Тогда ок.

kuzmin_m 2018-12-27T17:14:14.030600Z

это уже детали, и может быть я где-то и ошибся, но главное, что концепция понятна

fmnoise 2018-12-27T20:11:57.031100Z

там еще есть мультиспека

fmnoise 2018-12-27T20:12:11.031500Z

но она честно говоря не особо читаемая

fmnoise 2018-12-27T20:13:15.032100Z

вообще у меня возникло такое ощущение, что спека скорее о шейпе данных чем о валидации

fmnoise 2018-12-27T20:14:51.032800Z

типа защита от вызова .toLowerCase на Integer

fmnoise 2018-12-27T20:15:47.033700Z

потому что если валидацию доводить до совершенства и пользоваться спекой, то нечитаемый код получается

fmnoise 2018-12-27T20:17:03.034700Z

ребята из Attendify вот такое сделали https://github.com/KitApps/schema-refined

fmnoise 2018-12-27T20:18:37.036100Z

там есть удобная штука dispatch-on которая в спеке решается через мультики

fmnoise 2018-12-27T20:18:57.036600Z

но поскольку оно на схеме, то оно более менее читаемое

fmnoise 2018-12-27T20:19:21.037200Z

мультики со спекой это спагетти 88 уровня

kuzmin_m 2018-12-27T20:21:03.038400Z

Где провести грань между спекой и валидацией?

(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 2018-12-27T20:21:16.038800Z

и эту валидацю как-то отдельно делать?

kuzmin_m 2018-12-27T20:23:45.039400Z

и да, в моем варианте explain гранулированный не получить

kuzmin_m 2018-12-27T20:24:13.040Z

плюс, нужно на схему посмотреть, спасибо за ссылку

fmnoise 2018-12-27T20:27:46.040700Z

@kuzmin_m посмотри на ту штуку что я кинул, там реально очень хорошая идея по валидации именно данных

fmnoise 2018-12-27T20:29:36.041200Z

мне кажется спека больше о том, что можно а что нельзя делать над данными

fmnoise 2018-12-27T20:30:04.042100Z

в то время как реальная валидация скорее о бизнес-контексте

fmnoise 2018-12-27T20:30:48.042900Z

например если у тебя в ticket :type = :free то :price должно быть nil

fmnoise 2018-12-27T20:30:57.043200Z

ну или 0

fmnoise 2018-12-27T20:31:16.043800Z

потому что не может быть :free ticket с ценой > 0

kuzmin_m 2018-12-27T20:31:36.044200Z

т.е. это про валидацию

kuzmin_m 2018-12-27T20:31:50.044800Z

schema-refined поможет с этим?

fmnoise 2018-12-27T20:32:07.045200Z

да, schema refined как раз про fine grained validation

fmnoise 2018-12-27T20:32:56.046200Z

типа int? ничего не говорит, а вот int в границах от 0 до 1000 это уже нормальный бизнес-кейс

fmnoise 2018-12-27T20:34:32.047100Z

там еще ссылка на слайды есть https://speakerdeck.com/kachayev/keep-your-data-safe-with-refined-types

fmnoise 2018-12-27T20:35:02.047900Z

рекомендую тоже взглянуть

👌 1
fmnoise 2018-12-27T20:37:31.049200Z

еще там есть фишка с неизвестными ключами

fmnoise 2018-12-27T20:37:44.049700Z

спека на них кладет болт, а схема нет

fmnoise 2018-12-27T20:38:02.050100Z

по умолчанию в смысле

fmnoise 2018-12-27T20:41:15.051100Z

я вот такое рисовал чтобы обойтись

(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?)))

kuzmin_m 2018-12-27T20:44:40.051600Z

;; <https://groups.google.com/forum/#!topic/clojure/fti0eJdPQJ8>
(defmacro only-keys [&amp; {: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))))
с генератором

fmnoise 2018-12-27T20:56:34.052Z

ну это красивее

fmnoise 2018-12-27T20:56:45.052600Z

наверное

fmnoise 2018-12-27T20:56:48.052900Z

то я давно писал

fmnoise 2018-12-27T20:57:01.053200Z

explain дает уродский

fmnoise 2018-12-27T20:57:57.053700Z

да, у этой штуки explain красивый

fmnoise 2018-12-27T21:00:35.054400Z

вообще не знаю, кто-то заметил или нет, в кложуре 1.10 добавили в спеку возможность спеки удалять из регистра

fmnoise 2018-12-27T21:01:10.055Z

то есть можно schema-style по идее запилить

fmnoise 2018-12-27T21:01:37.055500Z

не знаю насколько оно thread-safe правда

fmnoise 2018-12-27T21:01:50.055900Z

подозреваю что не thread-safe at all

kuzmin_m 2018-12-27T21:09:36.056500Z

(s/def ::translation (s/and (s/keys :req-un [::title ::summary ::tags ::published-at])
                            (s/or :not-published #(-&gt; % :published-at nil?)
                                  :published (s/and #(-&gt; % :title not-empty)
                                                    #(-&gt; % :summary not-empty)))))
вот мой вариант получше, если кому-то интересно он более точный explain дает

kuzmin_m 2018-12-27T21:09:59.056700Z

да и понятнее

fmnoise 2018-12-27T21:10:24.057200Z

а что первый ключ в or означает?

kuzmin_m 2018-12-27T21:10:38.057400Z

это имя ветки

fmnoise 2018-12-27T21:10:47.057600Z

ааа, хм

fmnoise 2018-12-27T21:10:54.057900Z

действительно симпатично

fmnoise 2018-12-27T21:14:13.058600Z

но у тебя ключ published-at должен присутствовать и быть nil

kuzmin_m 2018-12-27T21:14:28.058800Z

(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 #(-&gt; % :published-at nil?)
                                  :published (s/and #(-&gt; % :title not-empty)
                                                    #(-&gt; % :summary not-empty)))))

kuzmin_m 2018-12-27T21:15:16.059400Z

можно еще вот так:

(s/def ::translation (s/and (s/keys :opt-un [::title ::summary ::published-at]
                                    :req-un [::tags])
                            (s/or :not-published #(-&gt; % :published-at nil?)
                                  :published (s/and #(-&gt; % :title not-empty)
                                                    #(-&gt; % :summary not-empty)))))

fmnoise 2018-12-27T21:15:54.060300Z

ну по опыту ключ скорее не будет чем будет nil

fmnoise 2018-12-27T21:16:06.060900Z

когда он opt то норм

fmnoise 2018-12-27T21:16:14.061200Z

(s/def ::translation (s/and (s/keys :req-un [::title ::summary ::tags] :opt-un [::published-at])
                            (s/or :not-published #(-&gt; % :published-at nil?)
                                  :published (s/and #(-&gt; % :title not-empty)
                                                    #(-&gt; % :summary not-empty)))))

kuzmin_m 2018-12-27T21:16:25.061700Z

тут пока спорно, я пока еще не решил

fmnoise 2018-12-27T21:16:37.062100Z

как-то только он присутствует, сразу требуются тайтл и саммари

kuzmin_m 2018-12-27T21:16:51.062600Z

не, title и summary s/nilable

fmnoise 2018-12-27T21:17:12.063300Z

ну у меня нет изначальных спек

fmnoise 2018-12-27T21:17:14.063600Z

только вот эта

kuzmin_m 2018-12-27T21:17:17.063900Z

хотя это тоже может поменяется

fmnoise 2018-12-27T21:17:22.064200Z

и там not-empty

fmnoise 2018-12-27T21:17:35.064600Z

(s/explain-data ::translation {:title "" :summary "" :tags ""});; =&gt; nil

kuzmin_m 2018-12-27T21:18:01.065200Z

так правильно

kuzmin_m 2018-12-27T21:18:08.065700Z

у тебя published-at опущен

fmnoise 2018-12-27T21:18:11.065900Z

(s/explain-data ::translation {:title "" :summary "" :tags "" :published-at ""})
;; =&gt; #:clojure.spec.alpha{:problems ({:path [:not-published], :pred (clojure.core/fn [%] (clojure.core/-&gt; % :published-at clojure.core/nil?)), :val {:title "", :summary "", :tags "", :published-at ""}, :via [:eventum.fiken/translation], :in []} {:path [:published], :pred (clojure.core/fn [%] (clojure.core/-&gt; % :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 ""}}

kuzmin_m 2018-12-27T21:18:12.066Z

и это ок

fmnoise 2018-12-27T21:18:46.066300Z

а вот если добавляю его

kuzmin_m 2018-12-27T21:19:36.066800Z

в общем у меня вопросы закончились) дальше уже бантики

fmnoise 2018-12-27T21:28:19.067300Z

да, если разнести еще ветки or по спекам то вообще красиво выходит

kuzmin_m 2018-12-27T21:30:34.068400Z

если выносить в отдельные спеки, то в ::published нужно добавить проверку #(-&gt; % :published-at inst?)

kuzmin_m 2018-12-27T21:30:57.068900Z

т.к. пока все в одном or можно не делать, а если разносить, то нужно явно указать

kuzmin_m 2018-12-27T21:32:42.069200Z

(s/def ::published (s/and #(-&gt; % :published-at some?)
                          #(-&gt; % :title not-empty)
                          #(-&gt; % :summary not-empty)))
(s/def ::not-published #(-&gt; % :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)))

fmnoise 2018-12-27T21:52:57.069600Z

та вроде работает и без этого