clojure.spec custom generator for Java objects
Asked Answered
A

2

7

I just saw one of Rich's talks on clojure.spec, and really want to give it a try on my project. I'm writing a series of tools for parsing C code using the eclipse CDT library, and I would like to spec that my functions accept and emit AST objects.

I think a very basic spec could be written for a function that takes the root of an AST and emits all the tree's leaves like this:

(import '(org.eclipse.cdt.core.dom.ast IASTNode))
(require '[clojure.spec :as s])

(defn ast-node? [node] (instance? IASTNode node))
(s/def ::ast-node ast-node?)
(s/fdef leaves :args ::ast-node :ret (s/coll-of ::ast-node))

However when I try to exercise the code (s/exercise leaves) I get an error:

Unable to construct gen at: [] for:  
xxx.x$leaves@xxx  
#:clojure.spec{:path [], :form #function[xxx.xxx/leaves], :failure :no-gen}

How can I write a custom generator for Java objects to fully spec and exercise my code?

Aftermost answered 16/12, 2016 at 3:10 Comment(0)
W
14

You can attach a custom generator to a spec using s/with-gen. You'll need to write a generator that produces all the node variants that you need. You might find it easier to write one generator per node type and then combine them, either with s/or or possibly by using something like s/multi-spec instead (which would make this open to extension).

An example of writing a generator that produces a Java object would be something like this:

(s/def ::date 
  (s/with-gen #(instance? java.util.Date %)
    (fn [] (gen/fmap #(java.util.Date. %) (s/gen pos-int?)))))

fmap takes a function and applies that to each result from the generator you give it. If you have a Java object with a constructor that takes multiple values, you can use a source generator like (s/gen (s/tuple int? string? int?)).

Wychelm answered 16/12, 2016 at 7:0 Comment(0)
A
2

For completeness, here's my code after applying Alex's answer to spec a "LiteralExpression" AST node:

(ns atom-finder.ast-spec
  (:import [org.eclipse.cdt.internal.core.dom.parser.cpp CPPASTLiteralExpression])
  (:require [clojure.spec :as s]
            [clojure.spec.gen :as gen]))

(def gen-literal-expression-args
 (gen/one-of
  [
   (gen/tuple (s/gen #{CPPASTLiteralExpression/lk_char_constant})
              (gen/char-ascii))
   (gen/tuple (s/gen #{CPPASTLiteralExpression/lk_float_constant})
              (gen/double))
   (gen/tuple (s/gen #{CPPASTLiteralExpression/lk_integer_constant})
              (s/gen (s/int-in -2147483648 2147483647)))
   (gen/tuple (s/gen #{CPPASTLiteralExpression/lk_string_literal})
              (gen/string))]))

(def gen-literal-expression
  (gen/fmap
   (fn [[type val]]
     (CPPASTLiteralExpression. type (.toCharArray (str val))))
   gen-literal-expression-args))

(s/def ::literal-expression
  (s/with-gen
    (partial instance? CPPASTLiteralExpression)
    (fn [] gen-literal-expression)))

(s/exercise :atom-finder.ast-spec/literal-expression 10
Aftermost answered 22/12, 2016 at 2:37 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.