I'd like to use clojure spec to build up a set of type constraints that can be aliased or further constrained by other specs.
For example, I might have many fields that all need to be valid sanitized markdown.
The following example works for validation (s/valid?) but not for generation (gen/generate)
(s/def ::sanitized-markdown string?)
(s/def ::instruction-list #(s/valid? ::sanitized-markdown %)) ;; works
(gen/generate (s/gen ::instruction-list)) ;; fails
However (gen/generate (s/gen ::sanitized-markdown)) does work.
Is there a way to extend ::instruction-list from ::sanitized-markdown so that it preserves all behavior?
You can alias another spec by providing it directly to s/def:
(s/def ::instruction-list ::sanitized-markdown)
You can use s/merge when merging map specs and s/and in other cases.
(s/def ::sanitized-markdown string?)
(s/def ::instruction-list (s/and ::sanitized-markdown #(> (count %) 10)))
(s/valid? ::instruction-list "abcd")
;; false
(s/valid? ::instruction-list "abcdefghijkl")
;; true
(gen/generate (s/gen ::instruction-list))
;; "178wzJW3W3zx2G0GJ1931eEeO"
An example with maps
(s/def ::a string?)
(s/def ::b string?)
(s/def ::c string?)
(s/def ::d string?)
(s/def ::first-map (s/keys :opt [::a ::b]))
(s/def ::second-map (s/keys :opt [::c ::d]))
(s/def ::third-map (s/merge ::first-map ::second-map))
(s/valid? ::third-map {:a "1" :d "2"})
;; true
(gen/generate (s/gen ::third-map))
;; {::b "gvQ7DI1kQ9DxG7C4poeWhk553", ::d "9KIp77974TEqs9HCq", ::c "qeSZA8NcYr7UVpJDsA17K"}
Related
I am trying to use Clojure Spec to define a data structure containing a java.time.LocalDate element:
(s/def :ex/first-name string?)
(s/def :ex/last-name string?)
(s/def :ex/birth-date (s/valid? inst? (java.time.LocalDate/now)))
(s/def :ex/person
(s/keys :req [:ex/first-name
:ex/last-name
:ex/birth-date]))
(def p1 #:ex{:first-name "Jenny"
:last-name "Barnes"
:birth-date (java.time.LocalDate/parse "1910-03-15")})
(println p1)
produces the following output
#:ex{:first-name Jenny, :last-name Barnes, :birth-date #object[java.time.LocalDate 0x4ed4f9db 1910-03-15]}
However, when I test to see if p1 conforms to the :ex/person spec, it fails:
(s/valid? :ex/person p1)
ClassCastException java.lang.Boolean cannot be cast to clojure.lang.IFn clojure.spec.alpha/spec-impl/reify--1987 (alpha.clj:875)
Looking closer at the Clojure examples for inst?, I see:
(inst? (java.time.Instant/now))
;;=> true
(inst? (java.time.LocalDateTime/now))
;;=> false
However, I don't see an obvious reason as to why that returns false. This seems to be the root of my issue, but I have not found a solution and would like some help.
You're probably looking for instance?- and your example fails, because in:
(s/def :ex/birth-date (s/valid? inst? (java.time.LocalDate/now)))
this part (s/valid? inst? (java.time.LocalDate/now)) should be a function (predicate), not boolean. The full code:
(s/def :ex/first-name string?)
(s/def :ex/last-name string?)
(s/def :ex/birth-date #(instance? java.time.LocalDate %))
(s/def :ex/person
(s/keys :req [:ex/first-name
:ex/last-name
:ex/birth-date]))
(def p1 #:ex{:first-name "Jenny"
:last-name "Barnes"
:birth-date (java.time.LocalDate/parse "1910-03-15")})
(s/valid? :ex/person p1)
=> true
inst? won't work here, because Inst is a protocol, used to extend java.util.Date and java.time.Instant:
(defprotocol Inst
(inst-ms* [inst]))
(extend-protocol Inst
java.util.Date
(inst-ms* [inst] (.getTime ^java.util.Date inst)))
(defn inst?
"Return true if x satisfies Inst"
{:added "1.9"}
[x]
(satisfies? Inst x))
(extend-protocol clojure.core/Inst
java.time.Instant
(inst-ms* [inst] (.toEpochMilli ^java.time.Instant inst)))
And you can use satisfies? to check whether some object satisfies given protocol:
(satisfies? Inst (java.time.LocalDate/parse "1910-03-15"))
=> false
I'm relatively new to clojure and I'm looking for a way to use the value of one required key in the validation of another. I can do it by creating another map with the two values and passing that, but I was hoping there was a simpler way. Thanks
(s/def ::country string?)
(s/def ::postal-code
;sudo-code
;(if (= ::country "Canda")
;(re-matches #"^[A-Z0-9]{5}$")
;(re-matches #"^[0-9]{5}$"))
)
(s/def ::address
(s/keys :req-un [
::country
::postal-code
::street
::state
]))
Here's a way to do it with multi-spec:
(defmulti country :country)
(defmethod country "Canada" [_]
(s/spec #(re-matches #"^[A-Z0-9]{5}$" (:postal-code %))))
(defmethod country :default [_]
(s/spec #(re-matches #"^[0-9]{5}$" (:postal-code %))))
(s/def ::country string?)
(s/def ::postal-code string?)
(s/def ::address
(s/merge
(s/keys :req-un [::country ::postal-code])
(s/multi-spec country :country)))
(s/explain ::address {:country "USA" :postal-code "A2345"})
;; val: {:country "USA", :postal-code "A2345"} fails spec: :sandbox.so/address at: ["USA"] predicate: (re-matches #"^[0-9]{5}$" (:postal-code %))
(s/explain ::address {:country "Canada" :postal-code "A2345"})
;; Success!
Another option is and-ing another predicate on your keys spec:
(s/def ::address
(s/and
(s/keys :req-un [::country ::postal-code])
#(case (:country %)
"Canada" (re-matches #"^[A-Z0-9]{5}$" (:postal-code %))
(re-matches #"^[0-9]{5}$" (:postal-code %)))))
You might prefer the multi-spec approach because it's open for extension i.e. you can define more defmethods for country later as opposed to keeping all the logic in the and predicate.
I have a simple problem.
Given a spec def, I want to use the same spec but only as a nilable variant.
E.g.
(s/def ::uuid uuid?)
(s/def ::problem-spec (s/keys :req-un [::uuid]))
(s/def ::nilable-problem-spec (s/keys :req-un [::uuid])) ; <- (or nil ::uuid)
; what I expect
(s/valid? ::uuid #uuid "9494a3e0-7ef0-4b3f-b539-bd7f7f4f0181") ; true
(s/valid? ::uuid nil) ; false
(s/valid? ::problem-spec {:uuid #uuid "9494a3e0-7ef0-4b3f-b539-bd7f7f4f0181"}) ; true
(s/valid? ::problem-spec {:uuid nil}) ; false
(s/valid? ::nilable-problem-spec {:uuid #uuid "9494a3e0-7ef0-4b3f-b539-bd7f7f4f0181"}) ; true
(s/valid? ::nilable-problem-spec {:uuid nil}) ; true
Since you've defined ::uuid as non-nillable you cannot then use the ::uuid key for a nilable value in a map/keyset. This is intentional - qualified keys in spec are globally defined.
What you can do in this case is to spec ::uuid as nillable and then restrict the non-nillable version of the key set:
(s/def ::uuid (s/nilable uuid?))
(s/def ::problem-spec (s/and (s/keys :req-un [::uuid])
#(some? (:uuid %))))
(s/def ::nilable-problem-spec (s/keys :req-un [::uuid])) ;; as previously
Alternatively, since you're dealing with unqualified keys in the maps you can also define two different qualified uuid keywords with different semantics:
(s/def :nillable/uuid (s/nilable uuid?))
(s/def :non.nil/uuid uuid?)
(s/def ::problem-spec (s/keys :req-un [:non.nil/uuid])))
(s/def ::nilable-problem-spec (s/keys :req-un [:nillable/uuid]))
Maybe my question has already been answered but I am stuck with a submap specification.
Imagine I have two possibilities like that
{:type :a
:spec {:name "a"}}
{:type :b
:spec {:id "b"}}
In short: the :spec keys depends on the type. For the type :a, the :spec must contain the field :name and for type :b the spec must contain the field :id.
I tried this:
(s/def ::type keyword?)
(defmulti input-type ::type)
(defmethod input-type :a
[_]
(s/keys :req-un [::name]))
(defmethod input-type :b
[_]
(s/keys :req-un [::id]))
(s/def ::spec (s/multi input-type ::type))
(s/def ::input (s/keys :req-un [::type ::spec]))
This tells me: no method ([:spec nil]).
I think I see why: maybe type is not acccessible.
So I thought to make a multi-spec of a higher level (based on the whole map).
Problem: I do not know how to define :spec based on :type because they have the same name. Do you know how to perform this?
Thanks
(s/def ::type keyword?)
(s/def ::id string?)
(s/def ::name string?)
(s/def :id/spec (s/keys :req-un [::id]))
(s/def :name/spec (s/keys :req-un [::name]))
To accommodate the two different meanings for your :spec map, we can define those in different namespaces: :id/spec and :name/spec. Note that the non-namespace suffix of these keywords are both spec and our keys specs are using un-namespaced keywords. These are "fake" namespaces here, but you could also define these in other, "real" namespaces in your project.
(defmulti input-type :type)
(defmethod input-type :a [_]
(s/keys :req-un [::type :name/spec]))
(defmethod input-type :b [_]
(s/keys :req-un [::type :id/spec]))
(s/def ::input (s/multi-spec input-type :type))
(s/valid? ::input {:type :a, :spec {:name "a"}})
=> true
You can also get samples of this spec:
(gen/sample (s/gen ::input))
=>
({:type :a, :spec {:name ""}}
{:type :b, :spec {:id "aI"}} ...
After writing this answer, I was inspired to try to specify Clojure's destructuring language using spec:
(require '[clojure.spec :as s])
(s/def ::binding (s/or :sym ::sym :assoc ::assoc :seq ::seq))
(s/def ::sym (s/and simple-symbol? (complement #{'&})))
The sequential destructuring part is easy to spec with a regex (so I'm ignoring it here), but I got stuck at associative destructuring. The most basic case is a map from binding forms to key expressions:
(s/def ::mappings (s/map-of ::binding ::s/any :conform-keys true))
But Clojure provides several special keys as well:
(s/def ::as ::sym)
(s/def ::or ::mappings)
(s/def ::ident-vec (s/coll-of ident? :kind vector?))
(s/def ::keys ::ident-vec)
(s/def ::strs ::ident-vec)
(s/def ::syms ::ident-vec)
(s/def ::opts (s/keys :opt-un [::as ::or ::keys ::strs ::syms]))
How can I create an ::assoc spec for maps that could be created by merging together a map that conforms to ::mappings and a map that conforms to ::opts? I know that there's merge:
(s/def ::assoc (s/merge ::opts ::mappings))
But this doesn't work, because merge is basically an analogue of and. I'm looking for something that's analogous to or, but for maps.
You can spec hybrid maps using an s/merge of s/keys and s/every of the map as tuples. Here's a simpler example:
(s/def ::a keyword?)
(s/def ::b string?)
(s/def ::m
(s/merge (s/keys :opt-un [::a ::b])
(s/every (s/or :int (s/tuple int? int?)
:option (s/tuple keyword? any?))
:into {})))
(s/valid? ::m {1 2, 3 4, :a :foo, :b "abc"}) ;; true
This simpler formulation has several benefits over a conformer approach. Most importantly, it states the truth. Additionally, it should generate, conform, and unform without further effort.
You can use s/conformer as an intermediate step in s/and to transform your map to the form that’s easy to validate:
(s/def ::assoc
(s/and
map?
(s/conformer #(array-map
::mappings (dissoc % :as :or :keys :strs :syms)
::opts (select-keys % [:as :or :keys :strs :syms])))
(s/keys :opt [::mappings ::opts])))
That will get you from e.g.
{ key :key
:as name }
to
{ ::mappings { key :key }
::opts { :as name } }