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.
validdoesn't throw. It returns a Boolean value, which you're not doing anything with.