• Stars
    star
    236
  • Rank 170,480 (Top 4 %)
  • Language
    Clojure
  • License
    Eclipse Public Li...
  • Created about 5 years ago
  • Updated 6 months ago

Reviews

There are no reviews yet. Be the first to send feedback to the community and the maintainers!

Repository Details

Efficient, hassle-free function call validation with a concise inline syntax for clojure.spec and Malli

Guardrails

guardrails CircleCI

Guardrails is intended to make it possible (and easy) to use specs 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, without affecting production build size or performance.

NEW in 1.1.0: Asynchronous Checking!

Guardrails can now have a much smaller impact on your dev-time performance!

I realized that most of the time in dev mode you’re just running bits of code here and there in fits and starts. Most people rarely run super high numbers of iterations without some kind of pause. Even then: if you’re running such a computation there is still a high probability that you’ll see problems even if some of the checks are silently dropped.

As a result, guardrails now has an asynchronous checking mode (which mainly benefits Clojure)!

When this mode is enabled it pushes spec checking into a core.async channel with a dropping buffer (size 10,000). The overhead for the put is just a few microseconds. This allows an alternate thread to run the checks, and as long as you don’t have large sustained computations this can give you nearly full-production performance of your code, while an alternate core in your computer handles the checks.

Benefits:

  • Much faster dev performance (Clojure only. The option works in CLJS, but there’s not a second thread so there is no benefit).

  • High performance algorithms can use guardrails with a tolerable cost.

Costs:

  • Checking results are queued. If a lot of slow checks get in the queue you might have to wait some time before you see the problems. This could cause confusion (you might be running your next expression in the REPL and see an error from the prior one).

  • Not all checks will run in a CPU-intensive task that queues checks rapidly.

  • Async mode is incompatible with the :throw? true option.

To enable the async mode, just add :async? true in your guardrails.edn file.

Quick Start

  1. Add this library to your dependencies

  2. Create a guardrails.edn with {} in it in your project root.

  3. 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. 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)))

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.

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:

{: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.

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 noop.cljc.

Why?

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’s goals are:

  • 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 support dead-code elimination 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.

The Gspec Syntax

[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.

Single/Multiple Arities

Write the function as normal, and put a gspec after the argument list:

(>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:

(>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 fn-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).

(>defn f
  [i]
  [int? | #(< 0 i 10) => int? | #(pos-int? %)]
  ...)
Warning
Return value such-that clauses are syntactically supported, but are not currently checked.

Nilable

The ? macro can be used as a shorthand for s/nilable:

(>fdef clojure.core/empty?
  [coll]
  [(? seqable?) => boolean?])

Nested Specs

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:

  • It’s much more concise and easier to write and read.

  • It’s inline, so you can see at a glance what kind of data a function expects and returns right under the docstring and arg list, for example when previewing the function definition in your editor.

  • It can be elided to have zero impact on build by an external control (config file/JVM parameter).

  • Renaming/refactoring parameters is a breeze – just use your IDE’s symbol rename functionality and all references in the predicate functions will be handled correctly.

  • You can reliably bypass Guardrails temporarily by simply changing >defn to defn - the minimal performance impact of evaluating the gspec vector as the first body form aside, nothing will break because >defn syntax is valid defn syntax.

Credit: The above documentation was largely taken from Ghostwheel’s documentation.

Enabling

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).

Configuration

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}

 ;; Check specs in parallel (CLJ only)
 :async? true

 ;; should a spec failure on args or ret throw an execption?
 ;; (always logs an informative message)
 :throw?     false

 ;; should a spec failure be forwarded through tap> ?
 :tap>?      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.

More Repositories

1

fulcro

A library for development of single-page full-stack web applications in clj/cljs
Clojure
1,543
star
2

fulcro-rad

Fulcro Rapid Application Development
Clojure
200
star
3

statecharts

A Statechart library for CLJ(S)
Clojure
93
star
4

fulcro-template

A sample project that could be used as a starting point for a Fulcro 3 project.
Clojure
93
star
5

fulcro-rad-demo

A demo for Fulcro RAD using either SQL or Datomic databases.
Clojure
67
star
6

fulcro-inspect

A tool for inspecting and debugging Fulcro applications during development.
Clojure
66
star
7

fulcro-spec

A library that wraps clojure.test for a better BDD testing experience.
Clojure
43
star
8

fulcro-native

A small library of helpers for using React Native with Fulcro
Clojure
27
star
9

fulcro-developer-guide

Fulcro 3 Developer's Guide. (See root of older fulcro repository for version 2)
HTML
24
star
10

fulcro-rad-semantic-ui

Semantic UI Rendering Plugin for RAD
Clojure
23
star
11

datomic-cloud-backup

A library for creating and restoring backups of databases in Datomic Cloud
Clojure
22
star
12

semantic-ui-wrapper

Fulcro 3 wrappers of React Semantic UI Controls
Clojure
22
star
13

fulcro-native-template

A sample template with a server, web, and native app.
Clojure
20
star
14

video-series

Repository with files from the YouTube Video series
Clojure
20
star
15

fulcro-garden-css

Garden CSS co-location on Fulcro (3+) Components
Clojure
16
star
16

fulcro-rad-datomic

Datomic database support plugin for Fulcro RAD
Clojure
14
star
17

fulcro-websockets

A Pluggable Remote for Fulcro 3 that uses Websockets
Clojure
12
star
18

fulcro-i18n

Internationalization Support For Fulcro
Clojure
7
star
19

grokking-fulcro

A repository containing projects that were used in making the videos for Grokking Fulcro.
Clojure
7
star
20

fulcro-rad-template

A template for starting new Fulcro projects, that includes RAD.
Clojure
7
star
21

fulcro-rad-kvstore

A RAD database adapter that is generalized to work with key-value databases
Clojure
6
star
22

fulcro-rad-tutorial

Files that go with YouTube videos for RAD
Clojure
5
star
23

fulcro-rad-sql

SQL Plugin for Fulcro RAD
Clojure
4
star
24

copilot

Discussions and Issue Tracking for Copilot
2
star
25

aws-logs

A bare-bones library for watching AWS logs (like saw, but Clojure)
Clojure
1
star
26

deep-tree-optimization-demo

A demo of using various tricks to optimize a very large tree of data/components
Clojure
1
star