♻️ Improve the asserts framework

This commit is contained in:
Andrey Antukh 2022-07-07 12:27:31 +02:00
parent c02e8ff883
commit 98190ed92d
2 changed files with 88 additions and 61 deletions

View file

@ -5,7 +5,7 @@
;; Copyright (c) UXBOX Labs SL ;; Copyright (c) UXBOX Labs SL
(ns app.common.spec (ns app.common.spec
"Data manipulation and query helper functions." "Data validation & assertion helpers."
(:refer-clojure :exclude [assert bytes?]) (:refer-clojure :exclude [assert bytes?])
#?(:cljs (:require-macros [app.common.spec :refer [assert]])) #?(:cljs (:require-macros [app.common.spec :refer [assert]]))
(:require (:require
@ -31,8 +31,6 @@
(def max-safe-int (int 1e6)) (def max-safe-int (int 1e6))
(def min-safe-int (int -1e6)) (def min-safe-int (int -1e6))
(def valid? s/valid?)
;; --- Conformers ;; --- Conformers
(defn uuid-conformer (defn uuid-conformer
@ -220,73 +218,102 @@
(fn [s] (fn [s]
(str/join "," s)))) (str/join "," s))))
;; --- Macros ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; MACROS
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(defn spec-assert* (defn explain-data
[spec val hint ctx] [spec value]
(if (s/valid? spec val) (s/explain-data spec value))
val
(let [data (s/explain-data spec val)]
(ex/raise :type :assertion
:code :spec-validation
:hint hint
::ex/data (merge ctx data)))))
(defmacro assert (defn valid?
"Development only assertion macro." [spec value]
[spec x] (s/valid? spec value))
(when *assert*
(let [nsdata (:ns &env)
context (if nsdata
{:ns (str (:name nsdata))
:name (pr-str spec)
:line (:line &env)
:file (:file (:meta nsdata))}
(let [mdata (meta &form)]
{:ns (str (ns-name *ns*))
:name (pr-str spec)
:line (:line mdata)}))
message (str "spec assert: '" (pr-str spec) "'")]
`(spec-assert* ~spec ~x ~message ~context))))
(defmacro verify (defmacro assert-expr*
"Always active assertion macro (does not obey to :elide-asserts)" "Auxiliar macro for expression assertion."
[spec x] [expr hint]
(let [nsdata (:ns &env) `(when-not ~expr
context (when nsdata (ex/raise :type :assertion
:code :expr-validation
:hint ~hint)))
(defmacro assert-spec*
"Auxiliar macro for spec assertion."
[spec value hint]
(let [context (if-let [nsdata (:ns &env)]
{:ns (str (:name nsdata)) {:ns (str (:name nsdata))
:name (pr-str spec) :name (pr-str spec)
:line (:line &env) :line (:line &env)
:file (:file (:meta nsdata))}) :file (:file (:meta nsdata))}
message (str "spec verify: '" (pr-str spec) "'")] {:ns (str (ns-name *ns*))
`(spec-assert* ~spec ~x ~message ~context))) :name (pr-str spec)
:line (:line (meta &form))})
hint (or hint (str "spec assert: " (pr-str spec)))]
`(if (valid? ~spec ~value)
~value
(let [data# (explain-data ~spec ~value)]
(ex/raise :type :assertion
:code :spec-validation
:hint ~hint
::ex/data (merge ~context data#))))))
(defmacro assert
"Is a spec specific assertion macro that only evaluates if *assert*
is true. DEPRECATED: it should be replaced by the new, general
purpose assert! macro."
[spec value]
(when *assert*
`(assert-spec* ~spec ~value nil)))
(defmacro verify
"Is a spec specific assertion macro that evaluates always,
independently of *assert* value. DEPRECATED: should be replaced by
the new, general purpose `verify!` macro."
[spec value]
`(assert-spec* ~spec ~value nil))
(defmacro assert! (defmacro assert!
"General purpose assertion macro." "General purpose assertion macro."
[& {:keys [expr spec always? hint val]}] [& params]
(cond ;; If we only receive two arguments, this means we use the simplified form
(some? spec) (let [pcnt (count params)]
(let [context (if-let [nsdata (:ns &env)] (cond
{:ns (str (:name nsdata)) ;; When we have a single argument, this means a simplified form
:name (pr-str spec) ;; of expr assertion
:line (:line &env) (= 1 pcnt)
:file (:file (:meta nsdata))} (let [expr (first params)
{:ns (str (ns-name *ns*)) hint (str "expr assert failed:" (pr-str expr))]
:name (pr-str spec) (when *assert*
:line (:line (meta &form))}) `(assert-expr* ~expr ~hint)))
message (or hint (str "spec assert: " (pr-str spec)))]
(when (or always? *assert*)
`(spec-assert* ~spec ~val ~message ~context)))
(some? expr) ;; If we have two arguments, this can be spec or expr
(let [message (or hint (str "expr assert: " (pr-str expr)))] ;; assertion. The spec assertion is determined if the first
(when (or always? *assert*) ;; argument is a qualified keyword.
`(when-not ~expr (= 2 pcnt)
(ex/raise :type :assertion (let [[spec-or-expr value-or-msg] params]
:code :expr-validation (if (qualified-keyword? spec-or-expr)
:hint ~message)))) `(assert-spec* ~spec-or-expr ~value-or-msg nil)
`(assert-expr* ~spec-or-expr ~value-or-msg)))
:else nil)) (= 3 pcnt)
(let [[spec value hint] params]
`(assert-spec* ~spec ~value ~hint))
:else
(let [{:keys [spec expr hint always? val]} params]
(when (or always? *assert*)
(if spec
`(assert-spec* ~spec ~val ~hint)
`(assert-expr* ~expr ~hint)))))))
(defmacro verify!
"A variant of `assert!` macro that evaluates always, independently
of the *assert* value."
[& params]
(binding [*assert* true]
`(assert! ~@params)))
;; --- Public Api ;; --- Public Api

View file

@ -111,11 +111,11 @@
;; --- Helper Functions ;; --- Helper Functions
(defn ^boolean check-browser? [candidate] (defn ^boolean check-browser? [candidate]
(us/verify ::browser candidate) (us/verify! ::browser candidate)
(= candidate @browser)) (= candidate @browser))
(defn ^boolean check-platform? [candidate] (defn ^boolean check-platform? [candidate]
(us/verify ::platform candidate) (us/verify! ::platform candidate)
(= candidate @platform)) (= candidate @platform))
(defn resolve-profile-photo-url (defn resolve-profile-photo-url