Data Validation

Most of the entities that get manipulated and stored on chains are represented by structured radicle data. To maintain the integrety of the chain it can be important to refuse malformed/invalid data. To detect this, use validator functions. These are functions which take a piece of data as input and either throw an exception (thus cancelling the current transaction), or return the data unchanged. The radicle prelude contains functions for creating and combining validators.

For example let’s assume that we want to validate the :priority field on an issue entity, with the requirement that it be an integer \(p\) such that \(0 \leq p \leq 10\). To do this we can define a validator:

(def validator/priority
  (validator/and
    [(validator/type :number)
     (validator/pred "is integer"
                     integral?)
     (validator/pred "0 <= p <= 10"
                     (fn [p] (and (< -1 p) (< p 11))))]))

Here we have used validator/and to combine several validators together: the priority is valid only if it passes all three validators:

  • The first one checks that the value is a number.
  • The second uses validator/pred, which takes a description and a predicate,

and checks that the predicate holds. In this case we ensure that the priority is a whole number.

  • The third validator ensures the priority is in the required range.

Very often entities are represented using dicts with specific required keys. Validators for these entities can be created using the validator/keys function. For example to validate an issue we could use:

(def validator/issue
  (validator/keys
    {:id     (validator/and
               [(validator/type :string)
                (validator/pred "valid UUID"
                                 uuid?)])
     :author (validator/pred "valid public key"
                             public-key?)
     :title  (validator/and
               [(validator/type :string)
                (validator/pred "< 60 chars"
                                (fn [s] (< (string-length s) 60)))])
     :body   (validator/and
               [(validator/type :string)
                (validator/pred "valid markdown"
                                markdown?)
                (validator/pred "< 4000 chars"
                                (fn [s] (< (string-length s) 4000)))])
     :tags   (validator/every
               (validator/and
                 [(validator/type :string)
                  (validator/pred "< 40 chars"
                                  (fn [s] (< (string-length s) 40)))]))
     :priority validator/priority}))

This checks that an issue is a dict, with required keys:

  • :id that’s a valid UUID string,
  • :author that’s a valid public key,
  • :title that’s a string shorter than 60 characters,
  • :body that’s a markdown string less that 4000 characters,
  • :tag that’s a vector of strings, each shorted than 40 characters.
  • :priority that’s a integers \(p\) such that \(0 \leq p \leq 10\).

Because validators return their input if it is valid, we can just wrap any value which should be validated with the appropriate validator:

(fn [issue]
  (modify-ref
    issues
    (fn [is]
      (add-right (validator/issue issue) is))))