0
Fork 0
mirror of https://github.com/penpot/penpot.git synced 2025-01-23 23:18:48 -05:00
penpot/common/app/common/spec.cljc
2021-01-25 11:51:15 +01:00

203 lines
5 KiB
Clojure

;; This Source Code Form is subject to the terms of the Mozilla Public
;; License, v. 2.0. If a copy of the MPL was not distributed with this
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
;;
;; This Source Code Form is "Incompatible With Secondary Licenses", as
;; defined by the Mozilla Public License, v. 2.0.
;;
;; Copyright (c) 2020 UXBOX Labs SL
(ns app.common.spec
"Data manipulation and query helper functions."
(:refer-clojure :exclude [assert])
#?(:cljs (:require-macros [app.common.spec :refer [assert]]))
(:require
#?(:clj [clojure.spec.alpha :as s]
:cljs [cljs.spec.alpha :as s])
[expound.alpha :as expound]
[app.common.uuid :as uuid]
[app.common.exceptions :as ex]
[app.common.geom.point :as gpt]
[cuerdas.core :as str]))
(s/check-asserts true)
;; --- Constants
(def email-rx
#"^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\.[a-zA-Z0-9-.]+$")
(def uuid-rx
#"^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$")
(def max-safe-int (int 1e6))
(def min-safe-int (int -1e6))
;; --- Conformers
(defn uuid-conformer
[v]
(if (uuid? v)
v
(if (string? v)
(if (re-matches uuid-rx v)
(uuid/uuid v)
::s/invalid)
::s/invalid)))
(defn boolean-conformer
[v]
(if (boolean? v)
v
(if (string? v)
(if (re-matches #"^(?:t|true|false|f|0|1)$" v)
(contains? #{"t" "true" "1"} v)
::s/invalid)
::s/invalid)))
(defn boolean-unformer
[v]
(if v "true" "false"))
(defn- number-conformer
[v]
(cond
(number? v) v
(str/numeric? v)
#?(:clj (Double/parseDouble v)
:cljs (js/parseFloat v))
:else ::s/invalid))
(defn- integer-conformer
[v]
(cond
(integer? v) v
(string? v)
(if (re-matches #"^[-+]?\d+$" v)
#?(:clj (Long/parseLong v)
:cljs (js/parseInt v 10))
::s/invalid)
:else ::s/invalid))
(defn- color-conformer
[v]
(if (and (string? v) (re-matches #"^#(?:[0-9a-fA-F]{3}){1,2}$" v))
v
::s/invalid))
(defn- email-conformer
[v]
(if (and (string? v) (re-matches email-rx v))
v
::s/invalid))
(defn keyword-conformer
[v]
(cond
(keyword? v)
v
(string? v)
(keyword v)
:else
::s/invalid))
;; --- Default Specs
(s/def ::keyword (s/conformer keyword-conformer name))
(s/def ::inst inst?)
(s/def ::string string?)
(s/def ::email (s/conformer email-conformer str))
(s/def ::color (s/conformer color-conformer str))
(s/def ::uuid (s/conformer uuid-conformer str))
(s/def ::boolean (s/conformer boolean-conformer boolean-unformer))
(s/def ::number (s/conformer number-conformer str))
(s/def ::integer (s/conformer integer-conformer str))
(s/def ::not-empty-string (s/and string? #(not (str/empty? %))))
(s/def ::url string?)
(s/def ::fn fn?)
(s/def ::point gpt/point?)
(s/def ::id ::uuid)
(s/def ::safe-integer
#(and
(int? %)
(>= % min-safe-int)
(<= % max-safe-int)))
(s/def ::safe-number
#(and
(or (int? %)
(float? %))
(>= % min-safe-int)
(<= % max-safe-int)))
;; --- Macros
(defn spec-assert*
[spec x message context]
(if (s/valid? spec x)
x
(let [data (s/explain-data spec x)
explain (with-out-str (s/explain-out data))]
(ex/raise :type :assertion
:code :spec-validation
:hint message
:data data
:explain explain
:context context
#?@(:cljs [:stack (.-stack (ex-info message {}))])))))
(defmacro assert
"Development only assertion macro."
[spec x]
(when *assert*
(let [nsdata (:ns &env)
context (when nsdata
{:ns (str (:name nsdata))
:name (pr-str spec)
:line (:line &env)
:file (:file (:meta nsdata))})
message (str "Spec Assertion: '" (pr-str spec) "'")]
`(spec-assert* ~spec ~x ~message ~context))))
(defmacro verify
"Always active assertion macro (does not obey to :elide-asserts)"
[spec x]
(let [nsdata (:ns &env)
context (when nsdata
{:ns (str (:name nsdata))
:name (pr-str spec)
:line (:line &env)
:file (:file (:meta nsdata))})
message (str "Spec Assertion: '" (pr-str spec) "'")]
`(spec-assert* ~spec ~x ~message ~context)))
;; --- Public Api
(defn conform
[spec data]
(let [result (s/conform spec data)]
(when (= result ::s/invalid)
(let [data (s/explain-data spec data)
explain (with-out-str
(s/explain-out data))]
(throw (ex/error :type :validation
:code :spec-validation
:explain explain
:data data))))
result))
(defmacro instrument!
[& {:keys [sym spec]}]
(when *assert*
(let [message (str "Spec failed on: " sym)]
`(let [origf# ~sym
mdata# (meta (var ~sym))]
(set! ~sym (fn [& params#]
(spec-assert* ~spec params# ~message mdata#)
(apply origf# params#)))))))