meta-spec
allows you to flace fn spec definitions inside the fn's metadata as:
(defn foo
{:spec {:args (s/cat :x int? :y int?)
:ret int?}}
[x y]
(+ x y))
... instead of defining them with separate fdef
statements as:
(defn foo [x y]
(+ x y))
(s/fdef foo
:args (s/cat :x int? :y int?)
:ret int?)
meta-spec
does not extend clojure.spec.alpha
in any way, it is simply a
helper API that lets you place specs closer to the thing they're spec'ing and
treat specs as any other kind of metadata associated with your fns.
meta-spec
currently only supports Clojure on the JVM and does not support
ClojureScript. It is the intention of the author to implement ClojureScript
support at some point.
Calling (ms/register)
without any arguments will search through the current ns
and register all found specs with s/fdef
.
(ns my.project.core
(:require [clojure.spec.alpha :as s]
[meta-spec.core :as ms]))
(defn foo
{:spec {:args (s/cat :x int? :y int?)
:ret int?}}
[x y]
(+ x y))
(ms/register) ; registers specs for my.project.core/foo
(ns my.project.other
(:require [clojure.spec.alpha :as s]
[meta-spec.core :as ms]))
(defn bar
{:spec {:args (s/cat :x str? :y str?)
:ret str?}}
[x y]
(str x y))
(ms/register) ; registers specs for my.project.other/bar
(ns my.project.other)
(defn baz [x y]
(- x y))
(ms/register) ; does nothing in my.project.other
Since calling (ms/register)
in each ns is tedious and easily forgotten you can
trigger registering your specs in a single place in your project with the
:ns-regex
argument. This must be called after all of your fns have been
defined at run time.
(ns my.project.core
(:require [clojure.spec.alpha :as s]))
(defn foo
{:spec ...
[x y]
...)
(ns my.project.other
(:require [clojure.spec.alpha :as s]))
(defn bar
{:spec ...
[x y]
...)
(ns my.project.setup
(:require [meta-spec.core :as ms]))
;; register specs for all fns in ns matching my.project.*
(ms/register
:ns-regex [#".*my\.project\..*"])
meta-spec
provides the helper fn fns-without-specs
that returns a list of
symbols of fns that do not have any specs. This is intended to be used to assess
spec coverage.
(ns my.project.core
(:require [clojure.spec.alpha :as s]
[meta-spec.core :as ms]))
(defn foo
{:spec {:args (s/cat :x int? :y int?)
:ret int?}}
[x y]
(+ x y))
(defn bar [x y]
(/ x y))
(ms/register)
(ns my.project.coverage
(:require [meta-spec.core :as ms]))
(comment
(ms/fns-without-specs
{:ns-regex [#".*my\.project\..*"]}) ; => (#'my.project.core/bar)
)
(meta-spec.core/register
{
;; A seq of regexes. meta-spec will sequentially search for fn definitions in
;; all namespaces that matches these regexes. Note that the namespaces must
;; already have been loaded since specs are registered at run time.
:ns-regex [#".*my\.lib\..*" #".*another\.project\..*"]
;; A keyword. This is the selector that meta-spec will use to extract spec
;; definitions from fn metadata. Defaults to :spec if the argument is not
;; provided.
:meta-kw :my.ns/spec
;; A seq of namespaces. meta-spec will search for fn definitions in these
;; namespaces and register all specs found in their metadata. Defaults to the
;; current namespace [*ns*] if the argument not provided.
:ns [my.project]
;; A seq of fn symbols. meta-spec will register specs for these fns.
:fn [my.ns/foo another.ns/and-fn]
;; A seq of namespaces. meta-spec will not register specs for fns found in
;; these namespaces.
:exclude-ns [my.lib.excluded]
;; A seq of symbols. meta-spec will not register specs for these fns.
:exclude-fn ['my.ns/excluded-fn]
;; When an fn spec is already found in clojure.spec.alpha's global registry
;; meta-spec will first remove it from the registry before registering the
;; new spec. This is useful when hot reloading code during development. Use
;; with care since this may (s/fdef ...). Defaults to true.
:reset-specs? true
})
(meta-spec.core/fns-without-specs
;; TODO
)
As projects grow in size or age it's natural that its authors can't hold all of
the code's context in their heads. spec
not only a a great tool for run time
validation and instrumentation but also for documenting shapes of values and
intentions of fns. The author can attest to having worked on at least one
project large enough that they wouldn't have been able to be net productive with
their limited mental capacity without heavy reliance on spec
.
meta-spec
arose from the want to bring fn spec definitions "closer to home" by
placing them right there in your fn defnitions as (defn my-fn {:spec ...} [] ...)
, rather than someplace else with a detached (s/fdef my-fn ...)
statement.
It's the belief of the author that a) a code base with fn specs will by and large be easier to grok than one without, b) inlined specs are easier to grok than detached specs and that c) writing inlined specs is more ergonomic and easy to remember than writing detached ones, leading to greater spec coverage.