Rovanion
Rovanion

Reputation: 4582

Why is my or-spec only valid for one of the given specs?

Consider the following spec for a text or a link layer port number:

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

(spec/def ::text (spec/and string? not-empty))
(spec/valid? ::text "a")                ; => true
(spec/valid? ::text "")                 ; => false
(spec/def ::port (spec/and pos-int? (partial > 65535)))
(spec/valid? ::port 4)                  ; => true
(spec/valid? ::port 0)                  ; => false
(spec/def ::text-or-port (spec/or ::text ::port))
(spec/valid? ::text-or-port 5)          ; => true
(spec/valid? ::text-or-port "hi")       ; => false

For some reason it only accepts port-numbers and not text, why would that be?

Upvotes: 1

Views: 111

Answers (1)

Rovanion
Rovanion

Reputation: 4582

The key to understanding this problem can be found in in the documentation and using spec/conform.

(spec/conform ::text-or-port 5)
; => [:user/text 5]

The problem is that clojure.spec.alpha/or has an API which is dissimmilar to clojure.core/or which given two arguments returns the first truthy one:

(#(or (string? %) (integer? %)) 5)      ; => true
(#(or (string? %) (integer? %)) "")     ; => true
(#(or (string? %) (integer? %)) :a)     ; => false

Rather it takes pairs of labels and specs/predicates. And since even namespaced keywords are accepted as labels the ::text-or-port spec given in the OP matched only that which passed the requirements for ::port and gave it the label ::text. Below is a correct spec for that which we want to match:

(spec/def ::text-or-port (spec/or :text ::text
                                  :port ::port))
(spec/valid? ::text-or-port "hi")       ; => true
(spec/valid? ::text-or-port 10)         ; => true

Upvotes: 2

Related Questions