Component, meet Schema

How to validate Clojure component systems

author picture
Malcolm Sparks
CTO & Co-founder
image

At JUXT, most of our projects are structured using Stuart Sierra’s component library.

Ideally, we should validate that the configuration and dependencies of our components are fully satisfied, and that they meet a component’s expectations.

Validation isn’t always necessary for smaller systems, but is useful when systems grow beyond a certain size.

A great candidate for providing the validation logic is Prismatic’s Schema library.

But what is the best way to integrate Schema?

Option A: Validate in the record’s constructor function

It is common to define a function that creates an instance of a component record. This affords us the option to declare any dependency expectations the component may have.

We could also perform validation in the function, at least for constructor arguments. This is the approach I’ve been taking for the past year or so.

(require '[schema.core :as s])

(defn new-my-component [& {:as opts}]
  (s/validate {:some-parameter s/Int} opts)
  (using (map->Website opts) [:some-dependency]))

However, when components are constructed, dependencies are not present. Dependencies are only injected (or rather, the component record is reconstructed with them) when the system is started.

Therefore, we can’t check our dependencies are correct in the constructor. They will always be nil.

Option B: Validate in the start phase

We could validate our component in the Lifecycle start phase.

(defrecord MyComponent [some-parameter some-dependency]
  Lifecycle
  (start [component]
    (s/validate {:some-parameter s/Int
                 :some-dependency (s/proto SomeProtocol)}
     component)))

Incidentally, we can also use schema’s defrecord for this.

(s/defrecord MyComponent
  [some-parameter :- s/Int
   some-dependency (s/proto SomeProtocol)]

  Lifecycle
  (start [component]
    (s/validate MyComponent component)
    component)
  (stop [component] component)

However, there are a few downsides with this approach.

  • we need to add Lifecycle to all our components, even those that wouldn’t otherwise need it

  • if we throw errors in the system start phase we can leave the system in a half-started state which often requires a JVM reboot

Option C: Validate independently, after the system has started

Alternatively, we can add a check function to our user namespace, alongside the start, stop and reset functions.

(defn check
  "Check for component validation errors"
  []
  (let [errors
        (->> system
             (reduce-kv
              (fn [acc k v]
                (assoc acc k (s/check (type v) v)))
              {})
             (filter (comp some? second)))]

    (when (seq errors) (into {} errors))))

We can call this check any time we like.

user> (check)
{:my-component {:some-parameter (not (instance? java.lang.Integer nil))}}

We can even add the following at the end of our usual start function

(when-let [errors (check)] (println "Warning, component integrity violated!" errors))

so that we get a warning on the REPL on any reset where any component in the system has validation errors.

user> (reset)
:reloading ()
Warning, component integrity violated!: {:my-component {:some-parameter (not (instance? java.lang.Integer nil))}}
:ok

There are some key advantages to this approach. We avoid adding validation logic to our component code, we only have to use schema declarations when we care about validation. In other words, we only validate components that have added these declarations, components without them pass the validation check as usual.

I’ve been trying this approach for a few weeks and it’s been holding up well.

Recommended Resources
Head Office
Norfolk House, Silbury Blvd.
Milton Keynes, MK9 2AH
United Kingdom
Company registration: 08457399
Copyright © JUXT LTD. 2012-2024
Privacy Policy Terms of Use Contact Us
Get industry news, insights, research, updates and events directly to your inbox

Sign up for our newsletter