Jessie Richardson
Jessie Richardson

Reputation: 958

Clojure Spec Not Validating Data Properly

I am super confused by Clojure Spec. When I run in the repl by entering:

(require '[clojure.spec.alpha :as s])

And then add:

(s/valid? even? 10)

I get //true. And when I run:

(s/valid? even? 11)

//False. Ok so that works. Then when I require spec in my core.clj as:

(ns spam-problem.core
    (:require [clojure.spec.alpha :as s]
              [clojure.spec.gen.alpha :as gen]))

And try a simple validation to get it to throw an error, nothing happens:

(defn -main
  "I don't do a whole lot ... yet."
  [& args]
  (s/valid? even? 11))

I have no idea what I'm doing wrong here and am very confused about how spec is supposed to work. I am running this by using command lein run. Is there another way you're supposed to run it?

Upvotes: 0

Views: 1199

Answers (2)

Ivan Grishaev
Ivan Grishaev

Reputation: 1681

I understand what you are feeling because once I got into Spec it caused me the same thoughts. What really helped me to solve the problem in my mind is to considering Spec being not a final library but rather a framework. In my projects, usually I've got a special module with high-level wrappers above basic spec capabilities. I believe, you might do the same: define a function that takes data, spec and raises those error message you desire to have in terms of your business-logic. Here is a small example of my code:

(ns project.spec
  (:require [clojure.spec.alpha :as s]))

;; it's better to define that value is a constant
(def invalid :clojure.spec.alpha/invalid)

(defn validate
  "Either returns coerced data or nil in case of error."
  [spec value]
  (let [result (s/conform spec value)]
    (if (= result invalid)
      nil
      result)))

(defn spec-error
  "Returns an error map for data structure that does not fit spec." 
  [spec data]
  (s/explain-data spec data))

Now, let's prepare some specs:

(defn x-integer? [x]
  (if (integer? x)
    x
    (if (string? x)
      (try
        (Integer/parseInt x)
        (catch Exception e
          invalid))
      invalid)))

(def ->int (s/conformer x-integer?))

(s/def :opt.visits/fromDate ->int)
(s/def :opt.visits/toDate ->int)
(s/def :opt.visits/country string?)
(s/def :opt.visits/toDistance ->int)

(s/def :opt.visits/params
  (s/keys :opt-un [:opt.visits/fromDate
                   :opt.visits/toDate
                   :opt.visits/country
                   :opt.visits/toDistance]))

And here are some usage examples:

(let [spec :opt.visits/params
      data {:some :map :goes :here}]
  (if-let [cleaned-data (validate spec data)]
    ;; cleaned-data has values coerced from strings to integers,
    ;; quite useful for POST parameters
    (positive-logic cleaned-data)
    ;; error values holds a map that describes an error
    (let [error (spec-error spec data)]
      (error-logic-goes-here error))))

What might be improved here is to have a combo-function with both validate and error functionality. Such a function could return a vector of two values: success flag and either result or error data structure as follows:

[true {:foo 42}] ;; good result
[false {:error :map}] ;; bad result

The Spec library does not dictate a single way of processing data; that's why it's really good and flexible.

Upvotes: 1

Taylor Wood
Taylor Wood

Reputation: 16194

valid? is a predicate that returns true or false. Your program isn’t doing anything with the return value. Try printing it to console or using s/assert if you want to throw an exception:

If (check-asserts?) is false at runtime, always returns x. Defaults to value of 'clojure.spec.check-asserts' system property, or false if not set. You can toggle check-asserts? with (check-asserts bool).

So you may need to set (s/check-asserts true) to have s/assert throw exceptions:

(clojure.spec.alpha/assert even? 3)
=> 3
(clojure.spec.alpha/check-asserts?)
=> false
(clojure.spec.alpha/check-asserts true)
=> true
(clojure.spec.alpha/assert even? 3)
ExceptionInfo Spec assertion failed
val: 3 fails predicate: :clojure.spec.alpha/unknown
clojure.core/ex-info (core.clj:4739)

Upvotes: 0

Related Questions