Guardrails
Efficient, hassle-free function call validation with a concise inline syntax for clojure.spec and Malli
Install / Use
/learn @fulcrologic/GuardrailsREADME
:sectanchors: ifdef::env-github,env-cljdoc[] :tip-caption: :bulb: :note-caption: :information_source: :important-caption: :heavy_exclamation_mark: :caution-caption: :fire: :warning-caption: :warning: endif::[]
= Guardrails
image:https://img.shields.io/clojars/v/com.fulcrologic/guardrails.svg[link=https://clojars.org/com.fulcrologic/guardrails] image:https://circleci.com/gh/fulcrologic/guardrails/tree/main.svg?style=svg["CircleCI", link="https://circleci.com/gh/fulcrologic/guardrails/tree/main"]
Efficient, hassle-free function call validation with a concise inline syntax for clojure.spec and Malli:
[source, clojure]
(>defn ranged-rand
[start end]
[int? int? | #(< start end) ; | = such that
=> int? | #(>= % start) #(< % end)]
(+ start (long (rand (- end start)))))
Guardrails is intended to make it possible (and easy) to use specs/schemas as a loose but informative (even advisory) type system during development so you can better see where you are making mistakes as you make them, with zero impact on production build size or performance, and minimal impact during development.
It's the evolution of https://github.com/gnl/ghostwheel[Ghostwheel's] runtime spec validation functionality, which it streamlines, optimizes and extends, while keeping the original inline syntax. See the <<Why?, "Why?">> section for more details on Guardrails' rationale and origin story.
== NEW in 1.2
Version 1.2 adds some exciting new features and performance improvements (partially funded by https://www.dataico.com[Dataico]):
- Use https://github.com/metosin/malli[Malli] as your <<malli-support,data specification language>>. You can even mix-and-match Clojure Spec and Malli in the same project/namespace.
- <<check-throttling, Runtime check throttling / non-exhaustive checking>>: Limit the number of function call checks per second at the namespace, function, or global level.
- <<static-exclusions, Library exclusions>>: Authors can auto-exclude internal implementation functions from validation when their libraries are used in dependent projects downstream.
- <<dynamic-exclusions, Dynamic exclusions>>: Users can disable and enable active checking for any namespace or function at runtime, as well as override library exclusions.
- <<Such That, Such-that predicates on the return value>>: Full support for checking such-that predicates on a function's output, with the ability to reference any input arguments in both clojure.spec and Malli.
== Quick Start
. 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:
[source, clojure]
(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. See the docstring of
;; com.fulcrologic.guardrails.noop for details.
(>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)))
When the function is misused you'll get an error:
[source, bash]
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.
Make sure to set your editor or IDE to resolve Guardrails' >defn and >fdef as Clojure's defn, and >defn- as defn- respectively – this way you get proper highlighting, formatting, error handling, structural navigation, symbol resolution, and refactoring support. A linting configuration for clj-kondo is included and should work out of the box.
=== Output Options
The configuration can be changed to help the signal to noise ratio on failures. If you have heavily instrumented your code with Guardrails, then turning on these two config options will likely give you a clearer picture on failures:
:guardrails/compact? true- Remove blank/excess lines from the failure output.:guardrails/stack-trace :none- Change what is shown for the stack trace. :none elides it, :prune causes a more pruned stack trace on a single line, and :full is the default of showing the whole thing.:guardrails/trace? true- Shows the GR function call stack on failures (with argument values)
Try the above settings as shown. You can always see the full stack by calling last-failure (which is printed with any failures for convenience). The result is way less noisy, and often sufficient to find the problem. When it's not, the full stack trace is just a copy/paste away.
== Clojurescript Considerations
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:
[source, clojure]
{: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:
[source, bash]
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:
[source, clojure]
:jvm-opts ["-Dguardrails.enabled=production"]
but this is highly discouraged.
=== Dead Code Elimination
There is a a noop namespace that can be used in your build settings to attempt to eliminate all traces of guardrails and dependent code. This will not remove spec dependencies unless you only use spec for guardrails, so do similar tricks for your inclusions of spec namespaces.
See https://github.com/fulcrologic/guardrails/blob/develop/src/main/com/fulcrologic/guardrails/noop.cljc[noop.cljc].
[[gspec-syntax]] == The Gspec Syntax
[arg-specs* (| arg-preds+)? \=> ret-spec (| ret-preds+)? (\<- generator-fn)?]
| : such that
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.
=== Single/Multiple Arities
Write the function as normal, and put a gspec after the argument list:
[source, clojure]
(>defn myf ([x] [int? => number?] ...) ([x y] [int? int? => int?] ...))
=== Variadic Argument Lists
arg-specs for variadic arguments are defined as one would expect from standard fspec:
[source, clojure]
(>fdef clojure.core/max [x & more] [number? (s/* number?) => number?])
[NOTE]
The arg-preds, if defined, are s/and-wrapped together with the arg-specs when desugared.
The ret-preds are equivalent to (and desugar to) spec's :fn predicates, except that the anonymous function parameter
is the ret, and the args are referenced using their symbols. That's because in the gspec syntax spec's :fn is simply
considered a 'such that' clause on the ret.
=== Such That
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).
[source, clojure]
(>defn f [i] [int? | #(< 0 i 10) => int? | #(pos-int? %)] ...)
=== Nilable
The ? macro can be used as a shorthand for s/nilable:
[source, clojure]
(>fdef clojure.core/empty? [coll] [(? seqable?) => boolean?])
=== Nested Specs
Nested gspecs are defined using the exact same syntax:
[source, clojure]
(>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:
[source, clojure]
(>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 ret-preds by symbol. The recommended
approach here is to register the required gspec separately by using >fdef with a keyword.
//You can do it with #(\-> % :arg1) in the arg-preds, but that won't work
