Fork me on GitHub
#clojure-spec
<
2021-02-19
>
Zak Singh03:02:55

I have a bit of a messy spec/generators question which I’ve written up here - https://stackoverflow.com/questions/66271188/spec-for-map-with-interdependent-values-between-nested-levels - is such a thing possible?

Alex Miller (Clojure team)04:02:07

Doing this kind of thing is inherently challenging. The general approach to take is: first generate a model by building up a core and extending it with gen/bind, then at the end generate the actual data structure with gen/fmap

Alex Miller (Clojure team)04:02:39

As a simple example, don’t try to generate a square by generating random points. Instead, generate the right and left x, the top and bottom y (that’s your model), then generate the points of the square using fmap at the end

Alex Miller (Clojure team)04:02:02

So I’m yours, maybe first generate a pool of terminals (collection of more constrained nodes, then randomly pick subsets to combine, and then maybe do some massage at the end

Alex Miller (Clojure team)04:02:21

How far you go with this depends how much of the space you want to cover

Zak Singh04:02:17

Reformulating the problem like that makes a lot of sense - I essentially need to build up layers from the terminal case. This is really an incredibly powerful tool

Alex Miller (Clojure team)04:02:19

Make sure to lean on s/gen of specs that may not match your public specs - easiest way to make new sub generators

Alex Miller (Clojure team)04:02:53

Like (s/gen (s/tuple ...))

Alex Miller (Clojure team)04:02:51

Or (s/gen #{:magic :values})

Zak Singh04:02:15

(def terminal-gen
  (gen/bind
    (spec/gen (spec/tuple ::terminal-name ::terminal-kind))
    (fn [[name kind]]
      (gen/hash-map
        :name (spec/gen #{name})
        :kind (spec/gen #{kind})))))

Zak Singh05:02:53

Managed to get it built! That was some fun code:

(spec/def ::kind #{"NON_NULL" "LIST" "SCALAR" "OBJECT"})
(spec/def ::name (spec/nilable string?))
(spec/def ::ofType (spec/or :terminal nil?
                            :type ::type))

(spec/def ::terminal-kind #{"SCALAR" "OBJECT"})
(spec/def ::terminal-name string?)

(spec/def ::wrapper-kind #{"NON_NULL" "LIST"})

(def terminal-gen
  (gen/bind
    (spec/gen (spec/tuple ::terminal-name ::terminal-kind))
    (fn [[name kind]]
      (gen/hash-map
        :name (spec/gen #{name})
        :kind (spec/gen #{kind})
        :ofType (gen/return nil)))))

(defn build-type
  ([max-depth] (if (= max-depth 1) terminal-gen
                                   (build-type max-depth 0 terminal-gen)))
  ([max-depth curr-depth inner-gen]
   (if (< curr-depth max-depth)
     (recur max-depth
            (inc curr-depth)
            (gen/bind inner-gen
                      (fn [inner-gen]
                        (if (= "NON_NULL" (:kind inner-gen))
                          (gen/hash-map
                            :name (gen/return nil)
                            :kind (spec/gen #{"LIST"}) ; two NON_NULLs cannot be child-parent
                            :ofType (spec/gen #{inner-gen}))
                          (gen/hash-map
                            :name (gen/return nil)
                            :kind (spec/gen ::wrapper-kind)
                            :ofType (spec/gen #{inner-gen}))))))
     inner-gen)))

(def type-gen
  (gen/bind
    (spec/gen (spec/int-in 1 5))
    build-type))