How to get human output from validation function using spec?

I’m developing a tool for validating sequences of items.
The tool itself needs a map of rules, which are related to keywords extracted from the data-stream sequence. It is a limited subset of linear temporal logic.

I’m trying to express the requirements of the rules in a spec
The current valid rules that I’m able to parse are:

(def valid-rules #{:not-eventually :is-after :relax :next})

My valid demo rule set is:

(def demorules
  {:header {:not-eventually :header} ; :header is not allowed to follow :header
   :beta {:is-after :header} ; :beta must follow :header
   :trailer {:is-after :header ; :trailer must follow :header
             :relax [:header] ; :trailer releases constraints from :header
             :next :header}}) ; :header must be the next immediate value

I want to make a spec that encodes this information, so that I can validate the consistency of the rules…
What I have for verifying the basic structure is:

(require '[clojure.spec.alpha :as s])
(s/def ::rule.entry (s/or :a keyword? :b (s/+ keyword?)))
(s/def ::rule valid-rules)
(s/def ::rules (s/map-of keyword? (s/map-of ::rule ::rule.entry)))

I would like to express that
if any key X is the subject of a rule, X must also has a top-level entry X’

So these rules are illegal:

(def badrules
    [{:X {:not-eventually :Y}}
     {:X {:relax [:Y]}}])

I can already do the check for this with

  (defn get-rule-entries [rules]
    (set (keys rules)))

  (defn get-subjects [rules]
    (->> (vals rules)
         (map vals)
         (flatten)
         (set)))

  (defn subjects-in-rules? [rules]
    (every? (get-rule-entries rules) (get-subjects rules)))

which introduces the new rule:

(s/def ::rule-consistent subjects-in-rules?)

But the problem is that there is no good output from (s/explain ::rule-consistent {:X {:not-eventually :Y}}) … just {:X {:not-eventually :Y}} - failed: subjects-in-rules? spec: ::rule-consistent

So I need some help in translating that to a proper spec. I welcome any suggestions

Thanks :slight_smile:

Checkout https://github.com/alexanderkiel/phrase

1 Like

Yeah, per @didibus, Phrase is good for getting nicer messages based on your predicates. You could get more info about the specific missing subject, and gen a message based on that if the test fails.

You might also check out malli as an alternative to spec, which has support for custom messages built in, and addresses some of the difficulties with getting at the error data of interest.

Another +1 for Phrase. When we started with using Spec heavily (as soon as it appeared in prerelease form back in the Clojure 1.9 days!), we rolled our own machinery for turning Spec failures into human-readable messages but I recently started a new piece of work heavily based on Spec for validation and decided to try Phrase and, so far, I’m very happy with that choice and it’s a lot less work than the old machinery we created :slight_smile:

Thanks everyone for the link to the Phrase library. It is good to know about.

I made spec-forms a while back, as I prefer this approach over pattern-matching:

(defn max-length [n]
  (sf/validator
    #(>= n (count %))
    (str "Must be " n " characters or less.")))

(defn min-length [n]
  (sf/validator
    #(<= n (count %))
    (str "Must be at least " n " characters long.")))

(def non-blank
  (sf/validator
    #(and % (not (str/blank? %)))
    "Must not be blank."))
    
(s/def ::login (s/and (min-length 2) (max-length 4) non-blank))
(s/def ::password (s/and (min-length 2) (max-length 4) non-blank))

(s/def ::login-form (s/keys :req [::login ::password]))

I think I was worried that there could be weird or complicated edge-cases with the pattern matching and I wanted a very straightforward approach to be available. It was designed for use with reforms, but it works fine when calling Spec directly. It is easily combined with Phrase, so you can try it out in existing code or use it just where it makes things simpler.