Skip to content

Instantly share code, notes, and snippets.

@metametadata
Last active March 4, 2019 12:22
Show Gist options
  • Star 2 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save metametadata/5f600e20e0e9b0ce6bce146c6db429e2 to your computer and use it in GitHub Desktop.
Save metametadata/5f600e20e0e9b0ce6bce146c6db429e2 to your computer and use it in GitHub Desktop.
Helpers for core.spec. Also see the related discussion: https://groups.google.com/forum/#!topic/clojure/i8Rz-AnCoa8
(ns libs.spec-plus
"Helpers for core.spec. Only tested in Lumo at the moment."
(:require [clojure.spec.alpha :as s]
[clojure.spec.test.alpha :as st]
[clojure.set :as set]
[cljs.analyzer :as ana])
#?(:cljs (:require-macros [libs.spec-plus])))
(defmacro speced-keys
"The same as s/keys but asserts that all keys have specs already registered.
Does not support recursive spec definitions, i.e. this will fail: (s/def ::m (libs.spec-plus/map :opt [::m]))"
[& form]
(let [map-keys (into #{} (filter qualified-keyword? (flatten form)))]
`(let [speced-keys# (set (keys (s/registry)))
unspeced-keys# (set/difference ~map-keys speced-keys#)]
(when (seq unspeced-keys#)
(throw (ex-info (str "these map keys have no specs registered: " (pr-str unspeced-keys#)) {})))
(s/keys ~@form))))
(defmacro fdef+instrument
"The same as clojure.spec/fdef but also immediately instruments the function after spec definition
(instrumentation only adds validation of input arguments ignoring :ret and :fn specs).
Both qualified and unqualified function symbols are supported.
Future: make instrumentation depend on the global config flag (atom) so that app can be compiled without instrumenting."
[fn-sym & specs]
(let [qualified-fn-sym (if (qualified-symbol? fn-sym)
fn-sym
(symbol (str ana/*cljs-ns*) (str fn-sym)))]
`(do
(when (not (ifn? ~fn-sym))
(throw (ex-info "function must be already defined" {})))
(s/fdef ~fn-sym ~@specs)
(let [result# (st/instrument '~qualified-fn-sym)]
(when (empty? result#)
(throw (ex-info (str "oops, couldn't instrument " (pr-str '~qualified-fn-sym)) {})))
result#))))
(defmacro defn+
"The same as defn but also calls fdef+instrument after function is defined.
Options for s/fdef will be taken from function var's meta. Example:
(libs.spec-plus/defn+ foo
\"Optional docstring here.\"
{:ret int? :args (s/cat :x int?)}
[x]
(+ x 100))
The main reason function meta is used is because it's easy to implement the macro (no new syntax has to be parsed).
Plus it's possible to make Cursive IDE treat this macro as a regular defn."
[fn-sym & fdecl]
(let [fdecl-after-docstring (if (string? (first fdecl))
(next fdecl)
fdecl)
fn-meta (when (map? (first fdecl-after-docstring))
(first fdecl-after-docstring))
fdef-specs (as-> fn-meta $
(select-keys $ [:args :f :ret])
(apply concat $))]
; Notify user if args are not speced.
(assert (contains? fn-meta :args)
(str "at least :args spec should be specified on using defn+, actual specs: " (pr-str fdef-specs)))
`(do
(defn ~fn-sym ~@fdecl)
(fdef+instrument ~fn-sym ~@fdef-specs))))
@metametadata
Copy link
Author

Approach to checking that all keys have specs at validation site instead of s/def site: https://groups.google.com/d/msg/clojure/i8Rz-AnCoa8/RShM-qdhBwAJ

@metametadata
Copy link
Author

One more alternative. Checking for "typos" in the entire registry: https://gist.github.com/stuarthalloway/f4c4297d344651c99827769e1c3d34e9

@metametadata
Copy link
Author

metametadata commented Nov 10, 2017

Similar approach using known-keys macro instead of s/keys, fails at macro expansion time in case of unknown key specs: https://groups.google.com/d/msg/clojure/i8Rz-AnCoa8/NkRmyUW2BAAJ

@metametadata
Copy link
Author

@metametadata
Copy link
Author

Newer version with support for closed maps and merging: https://gist.github.com/metametadata/53a847cd3b02056e8e4c124e63d9ae5a.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment