Last active
March 4, 2019 12:22
-
-
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
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
(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)))) |
One more alternative. Checking for "typos" in the entire registry: https://gist.github.com/stuarthalloway/f4c4297d344651c99827769e1c3d34e9
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
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
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