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?
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
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})
(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})))))
like this?
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))