-
Add this library to your dependencies
-
Create a
guardrails.edn
with{}
in it in your project root. -
When you run a REPL or CLJS compiler, include the JVM option
-Dguardrails.enabled
.-
Optionally: If you’re using CLJS, set your compiler options to include
{:external-config {:guardrails {}}}
-
And code as follows:
(ns com.domain.app-ns
(:require
[com.fulcrologic.guardrails.core :refer [>defn >def | ? =>]]))
;; >def (and >fdef) can be used to remove specs from production builds. Use them to define
;; specs that you only need in development, and they will not hurt CLJS production build size.
(>def ::thing (s/or :i int? :s string?))
;; When guardrails is disabled this will just be a normal `defn`, and no fspec overhead will
;; appear in cljs builds. When enabled it will check the inputs/outputs and *always* log
;; an error using `expound`, and then *optionally* throw an exception,
(>defn f [i]
[::thing => int?]
(if (string? i)
0
(inc i)))
then when the function is misused you’ll at least get a log error:
user=> (f 3.2)
ERROR /Users/user/project/src/com/domain/app_ns.clj:12 f's argument list
-- Spec failed --------------------
[3.2]
^^^
should satisfy
int?
or
string?
-- Relevant specs -------
:user/thing:
(clojure.spec.alpha/or :i clojure.core/int? :s clojure.core/string?)
You can control if spec failures are advisory or fatal by editing guardrails.edn
and setting the :throw?
option. See
Configuration for more details.
I use shadow-cljs
as the build tool for all of my projects, and highly recommend it. Version 0.0.11 of Guardrails
checks the compiler optimizations and refuses to output guardrails checks except in development mode (no optimizations). This
prevents you from accidentally releasing a CLJS project with big runtime performance penalties due to spec checking
at every function call.
The recommended approach for using guardrails in your project is to make a separate :dev
and :release
section of your
shadow-cljs config, like so:
:builds {:main {:target :browser
...
:dev {:compiler-options
{:closure-defines {'goog.DEBUG true}
:external-config {:guardrails {}}}}
:release {}}
Doing so will prevent you from accidentally generating a release build with guardrails enabled in case you had a shadow-cljs server running in dev mode (which would cache that guardrails was enabled) and built a release target:
# in one terminal:
$ shadow-cljs server
# later, in a different terminal
$ shadow-cljs release main
In this scenario Guardrails will detect that you have accidentally enabled it on a production build and will throw an exception. The only way to get guardrails to build into a CLJS release build is to explicitly set the JVM property "guardrails.enabled" to "production" (NOTE: any truthy value will enable it in CLJ).
You can set JVM options in shadow-cljs using the :jvm-opts
key:
:jvm-opts ["-Dguardrails.enabled=production"]
but this is highly discouraged.
Clojure spec’s instrument (and Orchestra’s outstrument) have a number of disadvantages when trying to use them for this purpose. Specifically, they are side-effecting after-calls that do not play particularly well with hot code reload, and always throw when there is a failed spec. Furthermore, management of the accidental inclusion of specs in your cljs builds (which increase build size) is a constant pain when writing separate specs for functions (the specs end up in a whole other file, inclusion needs to be via a development ns, and things easily get out of date).
This library is a middle ground between the features of raw Clojure spec and George Lipov’s Ghostwheel. Much of the source code in this library is directly from Ghostwheel.
This library is interested in providing:
-
The ability to use a simple DSL to declare the spec with a function (taken from Ghostwheel). See that library’s docs for syntax of
>defn
,>defn
, etc. -
The ability to control when specs are actually emitted separate from checking (to control build size in cljs).
-
No reliance on generative testing facilities/checkers. No orchestra/instrument stuff.
-
Good output when a function receives or emits an incorrect value.
-
The ability to control if a spec failure causes a throw (instrument always throws), because a lot of the time during development your spec is just wrong, and crashing your program is very inconvenient. You just want a log message to make you aware.
without the extra overhead of Ghostwheel’s support for:
-
Automatic generative testing stuff.
-
Tracing.
-
Side-effect detection/warning.
[arg-specs* (| arg-preds+)? => ret-spec (| fn-preds+)? (<- generator-fn)?]
|
= :st
– such that
=>
= :ret
– return value, same as in fspec
Note
|
Throughout this guide the symbolic gspec operators => and | will be used instead of the equivalent
keyword-based :ret and :st . The two sets are perfectly interchangeable and can even be freely mixed within the same gspec.
|
The number of arg-specs
must match the number of function arguments, including a possible variadic argument – Guardrails will shout at you if it doesn’t.
Write the function as normal, and put a gspec after the argument list:
(>defn myf
([x]
[int? => number?]
...)
([x y]
[int? int? => int?]
...))
arg-specs
for variadic arguments are defined as one would expect from standard fspec:
(>fdef clojure.core/max
[x & more]
[number? (s/* number?) => number?])
Note
|
The The |
To add an additional condition add |
after either the argument specs (just before ⇒
) or return value spec
and supply a lambda that uses the symbol names from the argument list (and %
for return value).
(>defn f
[i]
[int? | #(< 0 i 10) => int? | #(pos-int? %)]
...)
Warning
|
Return value such-that clauses are syntactically supported, but are not currently checked. |
The ?
macro can be used as a shorthand for s/nilable
:
(>fdef clojure.core/empty?
[coll]
[(? seqable?) => boolean?])
Nested gspecs are defined using the exact same syntax:
(>fdef clojure.core/map-indexed
([f]
[[nat-int? any? => any?] => fn?])
([f coll]
[[nat-int? any? => any?] (? seqable?) => seq?]))
In the rare cases when a nilable gspec is needed ?
is put in a vector rather than a list:
(>fdef clojure.core/set-validator!
[a f]
[atom? [? [any? => any?]] => any?])
Tip
|
For nested gspecs there’s no way to reference the args in the arg-preds or fn-preds by symbol. The recommended
approach here is to register the required gspec separately by using >fdef with a keyword.
|
Note
|
Nested gspecs with one or more any? argspecs desugar to ifn? , so as not to mess up generative testing. This
can be overridden by passing a generator – even an empty one, that is simply adding <- or :gen to the gspec – in which case the gspec will desugar exactly as specified.
The assumption here is that any? does not imply that the function can in fact handle any type of argument.
You should still write out nested gspecs, even if they are as simple as [any? => any?] – this is useful as succinct
documentation that this particular function receives exactly one argument.
|
Note
|
The gspec syntax has a number of advantages:
|
Credit: The above documentation was largely taken from Ghostwheel’s documentation.
The JVM option -Dguardrails.enabled=true
should be used to turn on
guardrails. When not defined >defn
will emit exactly what defn
would.
You may also enable it in cljs in your shadow-cljs config (see Configuration…adding even an empty config map will enable it).
The default config goes in top of project as guardrails.edn
:
{
; what to emit instead of defn, if you have another defn macro
:defn-macro nil
;; Nilable map of Expound configuration options.
:expound {:show-valid-values? true
:print-specs? true}
;; should an fspec be emitted for functions
:emit-spec? true
;; should a spec failure on args or ret throw an execption?
;; (always logs an informative message)
:throw? false}
You can override the config file name using JVM option
-Dguardrails.config=filename
.
In your shadow-cljs config file you can override settings via the [:compiler-options :external-config :guardrails]
config path of a build:
...
:app {:target :browser
:dev {:compiler-options
{:external-config {:guardrails {:throw? false}}
:closure-defines {'goog.DEBUG true}}}
...
The code and documentation taken from Ghostwheel is by George Lipov and follows the ownership/copyright of that library. The modifications in this library are copyrighted by Fulcrologic, LLC.
This library follows Ghostwheel’s original license: Eclipse public license version 2.0.