Fork me on GitHub
#clojure-spec
<
2022-04-20
>
onetom03:04:02

i wanted to generate some datomic lookup-refs with spec, but s/cat always returns a seq, not a vector. is there some built-in / idiomatic / recommended way to spec vectors? i saw there are :kind and :into options for s/coll-of, but i couldn't combine it with s/cat; i always got Couldn't satisfy such-that predicate after 100 tries. error. i found a recommendation here: https://gist.github.com/mhuebert/8fdeedae57bf797778054dcf8f33ab8b but my gut feeling tells me, that there might be a simpler way.

Alex Miller (Clojure team)03:04:52

unfortunately, there is not a simpler way - it is very challenging (in spec 1) to spec vectors and get all of conform, unform, gen etc to work. we've fixed this in spec 2 but I don't have a good answer in spec 1.

🙏 1
Kelvin13:04:55

Would s/tuple work?

Alex Miller (Clojure team)13:04:11

for some limited cases, yes (it's fixed size and can only be positionally described, not with the regex ops)

onetom20:04:31

I just started looking into spec2. Found this page, which specifically mentions how to solve my issue with non-flowing s/and-: https://github.com/clojure/spec-alpha2/wiki/Differences-from-spec.alpha (s/def ::vcat (s/and- (s/cat :i int?) vector?)) Thanks @U064X3EF3 for the great docs!

Alex Miller (Clojure team)21:04:57

not mentioned there, but that's actually reified for you in s/catv (vector-only) and s/cats (sequence-only) in spec 2

Alex Miller (Clojure team)21:04:46

I don't think that ever made it back to those docs, but they're there

Alex Miller (Clojure team)21:04:05

and before you ask, no I would not recommend using spec 2 at this time, it has a number of bugs :)

🙂 1
👍 1
Cam Saul22:04:22

Is there a way to pass along/access/use the overrides from a top-level call to s/gen inside a custom generator defined with s/with-gen or (s/spec ... :gen ...)? Example. Supposed I want to generate some sort of query with the keys :database and :table. I'd like to have a custom generator for :table so its values are based on the value of :database... however if I use an overriden generator for :database That generator is invisible from inside my custom generator.

(s/def ::database string?)

(s/def ::table
  (s/with-gen
    string?
    (fn []
      (gen/bind
       (s/gen ::database)
       (fn [db]
         (gen/fmap (fn [table]
                     (str db \. table))
                   gen/string))))))

(s/def ::query
  (s/keys :req-un [::database ::table]))

(gen/sample (s/gen ::query {::database (fn [] (gen/return "my_database"))}))
;; =>
[{:database "my_database", :table "."}
 {:database "my_database", :table "7.$"}
 {:database "my_database", :table "T2."}
 {:database "my_database", :table "xv.H¨"}
 {:database "my_database", :table "aB.Ín"}
 {:database "my_database", :table "Q3MCh.Çá"}
 {:database "my_database", :table "F68.·%:"}
 {:database "my_database", :table ".>ì"}
 {:database "my_database", :table "jBk.ÇP µrfL"}
 {:database "my_database", :table "d.¥¶«â\b"}]
Is there some way to do this? I know I could do the whole thing with test.check.generators/let or bind but I'd be missing out on all the niceness from spec maps since AFAIK there's nothing like s/keys in test.check

Cam Saul23:04:36

I guess I could get it working by redefining the entire spec with s/def instead of trying to pass an override for the generator, or maybe just with-redefs -ing clojure.spec.alpha/registry-ref itself... both feel icky but since this just for generative testing it's probably not the end of the world.

Cam Saul23:04:38

Another option might be to replace all my specs with functions that return specs based on previous state, but for map specs I guess I'd have to use Spec Alpha 2 so I could programmatically create them with schema or the like. Maybe that's a bit better. But creating a bunch of ad-hoc specs programatically actually seems like more work than if I forgot about spec altogether and just used test.check.generators directly

colinkahn15:04:10

I don't think there's any way provided in spec to make your custom generator see the override (you could get creative and bind a dynamic var, but that's outside of specs functionality and probably a bit unexpected). test.check does have hash-map, which will generate a map of keys to values like (gen/hash-map :database db-gen :table table-gen). I would probably use fmap though here and do something like:

(gen/fmap (fn [[db-str table-str]] {:table (str db-str \. table-str) : database db-str})
           (gen/tuple (gen/return "my_database") gen/string))
Most things can be solved using fmap and then reconciling the generated parts in the fmap fn somehow. This gives better shrinking as well than using bind/let.

Cam Saul21:04:35

I tried binding dynamic vars at every possible place but couldn't figure out how to get it to work consistently. Thanks, I think I'll just stick to using test.check stuff without spec for now. I managed to put together a generator that works sort of like s/keys with :opt or :opt-un , not sure how well it's going to handle the shrinking. I'll have to wrap my head around that a bit more