Fork me on GitHub
Zak Singh03:02:55

I have a bit of a messy spec/generators question which I’ve written up here - - is such a thing possible?


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


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


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


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


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


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


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

Zak Singh04:02:15

(def terminal-gen
    (spec/gen (spec/tuple ::terminal-name ::terminal-kind))
    (fn [[name kind]]
        :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
    (spec/gen (spec/tuple ::terminal-name ::terminal-kind))
    (fn [[name kind]]
        :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))
                            :name (gen/return nil)
                            :kind (spec/gen #{"LIST"}) ; two NON_NULLs cannot be child-parent
                            :ofType (spec/gen #{inner-gen}))
                            :name (gen/return nil)
                            :kind (spec/gen ::wrapper-kind)
                            :ofType (spec/gen #{inner-gen}))))))

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