clojure-spec

About: http://clojure.org/about/spec Guide: http://clojure.org/guides/spec API: https://clojure.github.io/spec.alpha/clojure.spec.alpha-api.html
Ricardo Cabral 2020-12-09T08:26:34.469200Z

Hello all, I hoe you are doing good and safe. I am a newbie in Clojure and I am trying to learn about spec. I am using spec-alpha2 and I am facing an issue trying to generate a contact as you can see in the code below. The error is Couldn’t satisfy such-that predicate after 100 tries. when I try to generate a sample of an email using regex. Is it possible to do like this?

(def email-regex #"^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,63}$")
(s/def ::firstname string?)
(s/def ::lastname string?)

(s/def ::email  (s/and string? #(re-matches email-regex %)))

(s/def ::contact (s/schema [::firstname, ::lastname, ::email]))
(def contact-test  {:firstname "Ricardo" :lastname "Cabral" :email "<mailto:ricardo@ricardo.com|ricardo@ricardo.com>" :extra 1})

(s/valid? ::contact contact-test)
(gen/generate (s/gen ::contact)). ;;error here
(gen/sample (s/gen ::contact)) ;; error here as well

lassemaatta 2020-12-09T08:34:53.469300Z

I'm no spec expert, but my understanding is that s/and uses the first spec (in this case string?) to generate values and then checks that the value matches the rest of the conditions (in this case the regex). Because string? generates random strings, it is unlikely that it matches the very specific email regex. After generating several random strings, spec gives up as it cannot create a random string which satisfies the email regex.

lassemaatta 2020-12-09T08:39:01.469500Z

possible solutions: a) use a constant set of sample email addresses and use s/with-gen to pick one of the samples, b) read about test.check and the tools it provides to build custom generators and try to build an email address generator by hand, c) check out libraries like lambdaisland/regal, which provide facilities to create generators for regex

Ricardo Cabral 2020-12-09T09:01:01.469800Z

Thank you @lasse.maatta for your quick answer. I will check that

alexmiller 2020-12-09T14:38:36.473800Z

You may have some trouble with this as spec2 is a work in progress and with-gen in particular may have some issues. I would recommend using spec 1 for now

Aleh Atsman 2020-12-09T15:20:10.474Z

explain data approach - maybe but clojure.spec/form - doesn't work for merged spec thanks for links

Aleh Atsman 2020-12-09T15:20:55.474200Z

both ways feels more hacky than just abstracting clojure.spec away and managing things by hands

misha 2020-12-09T15:33:09.477Z

For merged specs probably need to walk it doing s/form and s/registry. Walking datastructures ain’t hacky in a Lisp :op:

misha 2020-12-09T15:35:12.477900Z

They say spec2 is gonna be easier in this regard, fwiw

Ricardo Cabral 2020-12-09T17:49:45.478100Z

Thank you @alexmiller so far I am not facing any big issue. I am just trying some small experiments to understand how spec2 works and also learn about the idea behind it, and so far it is working as expected

Ricardo Cabral 2020-12-09T17:49:51.478300Z

(def email-regex #"^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,63}$")

(s/def ::email-type (s/and string? #(re-matches email-regex %) ))

(defn- email-generator
   "Generates sample emails" []
	 (gen/fmap #(str % "@" % ".com") (gen/string-alphanumeric))

		(s/def ::sample-email (s/with-gen ::email-type email-generator))
        
     (defn generate-contact-sample
		"Generate sample contact to test"
        []
		  {:firstname (gen/generate (gen/string))
		   :lastname (gen/generate (gen/string))
			:email (gen/generate (email-generator))}
		)

(generate-contact-sample)

(s/def ::firstname string?)
(s/def ::lastname string?)
(s/def ::email ::email-type )

(s/def ::contact (s/schema [::firstname ::lastname ::email]))

(s/valid? ::contact (generate-contact-sample))