Skip to content

Commit 1e41d02

Browse files
committed
New defmojo macro. Syntactic sugar for defining mojos in clojure
1 parent 4544f42 commit 1e41d02

File tree

3 files changed

+303
-0
lines changed

3 files changed

+303
-0
lines changed

README.md

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,41 @@ Annotations are used to get values from the pom.
6969
(SimpleMojo. nil nil nil nil nil (atom nil)))
7070
```
7171

72+
For convenience, you may instead use the `defmojo` macro:
73+
74+
```clojure
75+
(ns example.simple
76+
"Simple Mojo"
77+
(:use clojure.maven.mojo.defmojo
78+
clojure.maven.mojo.log))
79+
80+
(defmojo SimpleMojo
81+
82+
{:goal "simple"
83+
:requires-dependency-resolution "test" }
84+
85+
;; parameters
86+
[base-directory {:expression "${basedir}" :required true :readonly true}
87+
88+
classpath-elements {:required true :readonly true
89+
:description "Compile classpath"
90+
:defaultValue "${project.compileClasspathElements}" }
91+
92+
test-classpath-elements {:required true :readonly true
93+
:defaultValue "${project.testClasspathElements}" } ]
94+
95+
;; Body that is executed when Mojo is run
96+
(do
97+
(info "Hi Maven World!")
98+
(info (str "basedir is: " base-directory))))
99+
```
100+
101+
Note1: `defmojo` implicitly defines params 'log' and 'plugin-context' for you.
102+
103+
Note2: `clojure.maven.mojo.log` defines convenience functions `debug`, `info`, etc.
104+
for you to send messages to Maven's log stream.
105+
106+
72107
### pom.xml
73108

74109
To write a plugin in clojure, your pom packaging should be set to `maven-plugin`.
Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
1+
(ns clojure.maven.mojo.defmojo
2+
(:require
3+
[clojure.string :as str]
4+
[clojure.maven.mojo.log :as log])
5+
(:import
6+
org.apache.maven.plugin.ContextEnabled
7+
org.apache.maven.plugin.Mojo
8+
org.apache.maven.plugin.MojoExecutionException))
9+
10+
(defn assert-arg [b msg]
11+
(if-not b (throw (IllegalArgumentException. msg))
12+
b))
13+
14+
(defn validate-param [[param opts]]
15+
(assert-arg (and (symbol? param) (map? opts))
16+
"Each Mojo parameter must be a symbol followed by a map of options")
17+
[param opts])
18+
19+
(fn [[param options]]
20+
(and (symbol? param) (map? options)))
21+
(defn key->annotation
22+
"Convert a keyword name (ie: :requires-dependency) to the
23+
corresponding camelcase symbol (ie: RequiresDependency) and
24+
validate it's a Java annotation"
25+
[k]
26+
(let [name (str "clojure.maven.annotations."
27+
(-> (name k)
28+
str/capitalize
29+
(str/replace #"-([a-zA-Z])"
30+
#(str/upper-case (second
31+
%)))))
32+
fail-msg (str "Cannot find corresponding Mojo annotation for "
33+
k)]
34+
(try (assert-arg (.isAnnotation (Class/forName name)) fail-msg)
35+
(catch ClassNotFoundException ex
36+
(assert-arg false fail-msg)))
37+
38+
(symbol name)))
39+
40+
41+
(defn validate-body [body]
42+
(assert-arg (seq body) "Body of your Mojo definition is missing.")
43+
(assert-arg (= (count body) 1)
44+
(str "Body of your Mojo definition must be a single sexp. "
45+
"You may use form (do ...) to enclose multiple sexps."))
46+
body)
47+
48+
49+
(defmacro defmojo
50+
"Define a Mojo with the given name. This defines some common fields and
51+
leaves you to just specify a body for the execute function.
52+
Example:
53+
54+
(defmojo MyClojureMojo
55+
56+
{:goal \"simple\"
57+
:requires-dependency-resolution \"test\"
58+
:phase \"validate\" }
59+
60+
;; Mojo parameters
61+
[base-dir {:expression \"${basedir}\" :required true :readonly true}
62+
project {:expression \"${project}\" :required true :readonly true}
63+
output-dir {:defaultValue \"${project.build.outputDirectory}\"
64+
:required true}]
65+
66+
(do
67+
(println \"Hello Maven World!\")
68+
(println \"This is project \" (.getName project))))
69+
"
70+
71+
[mojoType annotations-map parameters & body]
72+
73+
(assert-arg (map? annotations-map)
74+
"First arg must be a map of Mojo annotations")
75+
(assert-arg (vector? parameters)
76+
(str "Second arg must be a vector of parameters "
77+
"(a name and options map for each param"))
78+
(let [mojo-annotations (into {} (map (fn [[k v]] [(key->annotation k) v])
79+
annotations-map))
80+
params (map validate-param (partition-all 2 parameters))
81+
;body (mapcat identity rest)
82+
body (validate-body body)]
83+
84+
`(do
85+
(deftype
86+
;; Mojo annotations
87+
~(vary-meta mojoType merge mojo-annotations)
88+
89+
;; Mojo parameters
90+
~(vec
91+
(concat
92+
(map (fn [[param options]]
93+
(vary-meta param merge
94+
{'clojure.maven.annotations.Parameter options}))
95+
params)
96+
;; pre-defined parameters
97+
`( ~(with-meta 'log {:volatile-mutable true})
98+
~'plugin-context
99+
)))
100+
101+
;; Mojo predefined methods
102+
Mojo
103+
~'(setLog [_ logger] (set! log logger))
104+
~'(getLog [_] log)
105+
106+
;; Mojo's suplied methods
107+
(~'execute [~'this]
108+
(log/with-log ~'log
109+
~@body))
110+
111+
;; Plugin-Context handling
112+
ContextEnabled
113+
~'(setPluginContext [_ context] (reset! plugin-context context))
114+
~'(getPluginContext [_] @plugin-context)
115+
)
116+
117+
118+
(defn ~(symbol (str "make-" mojoType))
119+
"Function to provide a no argument constructor"
120+
[]
121+
(~(symbol (str mojoType "."))
122+
~@(repeat (count params) nil) nil (atom nil)))
123+
124+
)
125+
))
Lines changed: 143 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,143 @@
1+
(ns clojure.maven.mojo.defmojo-test
2+
(:use
3+
clojure.test
4+
clojure.maven.mojo.defmojo
5+
clojure.walk)
6+
(:require
7+
[clojure.string :as str]
8+
[clojure.maven.mojo.log :as log])
9+
(:import
10+
org.apache.maven.plugin.ContextEnabled
11+
org.apache.maven.plugin.Mojo
12+
org.apache.maven.plugin.MojoExecutionException))
13+
14+
15+
16+
(deftest defmacro-minimal
17+
18+
(is (=
19+
(macroexpand-1
20+
'(defmojo
21+
MyFirstMojo {} [] (log/info "Hello World!")))
22+
23+
'(do
24+
(clojure.core/deftype MyFirstMojo
25+
[ ^{:volatile-mutable true} log
26+
plugin-context ]
27+
28+
org.apache.maven.plugin.Mojo
29+
(setLog [_ logger] (set! log logger))
30+
(getLog [_] log)
31+
(execute [this] (clojure.maven.mojo.log/with-log log
32+
(log/info "Hello World!")))
33+
34+
org.apache.maven.plugin.ContextEnabled
35+
(setPluginContext [_ context] (reset! plugin-context context))
36+
(getPluginContext [_] (clojure.core/deref plugin-context)))
37+
38+
(clojure.core/defn make-MyFirstMojo
39+
"Function to provide a no argument constructor" []
40+
(MyFirstMojo. nil (clojure.core/atom nil)))))))
41+
42+
43+
(deftest defmacro-basic
44+
45+
;; Note: By definition '=' doesn't compare metadata.
46+
;; Since defmojo generates quite a bit of metadata, we are not really testing
47+
;; all there is to it here :-(
48+
(is (=
49+
(macroexpand-1
50+
'(defmojo
51+
MyFirstMojo {:goal "hello-world"}
52+
[
53+
project {:expression "${project}"}
54+
basedir {:expression "${basedir}"}
55+
]
56+
(log/info "Hello World!")))
57+
58+
'(do
59+
(clojure.core/deftype ^{clojure.maven.annotations.Goal "hello-world"}
60+
MyFirstMojo
61+
[^{clojure.maven.annotations.Parameter {:expression "${project}"}}
62+
project
63+
^{clojure.maven.annotations.Parameter {:expression "${basedir}"}}
64+
basedir
65+
^{:volatile-mutable true}
66+
log
67+
plugin-context]
68+
69+
org.apache.maven.plugin.Mojo
70+
(setLog [_ logger] (set! log logger))
71+
(getLog [_] log)
72+
73+
(execute [this] (clojure.maven.mojo.log/with-log log
74+
(log/info "Hello World!")))
75+
76+
org.apache.maven.plugin.ContextEnabled
77+
(setPluginContext [_ context] (reset! plugin-context context))
78+
(getPluginContext [_] (clojure.core/deref plugin-context)))
79+
80+
(clojure.core/defn make-MyFirstMojo
81+
"Function to provide a no argument constructor" []
82+
(MyFirstMojo. nil nil nil (clojure.core/atom nil)))))))
83+
84+
85+
86+
(deftest body-missing
87+
(is (thrown-with-msg? IllegalArgumentException #"Body.*missing"
88+
(macroexpand-1 '(defmojo Test {} [])))))
89+
90+
(deftest body-multiple-sexps
91+
(is (thrown-with-msg? IllegalArgumentException #"Body.*do"
92+
(macroexpand-1 '(defmojo Test {} []
93+
(log/info "first")
94+
(log/info "second"))))))
95+
96+
(deftest invalid-mojo-annotations
97+
(is (thrown-with-msg? IllegalArgumentException #"annotation"
98+
(macroexpand-1 '(defmojo Test
99+
non-map
100+
[]
101+
(log/info "hello world!"))))))
102+
103+
104+
(deftest invalid-mojo-annotation
105+
(is (thrown-with-msg? IllegalArgumentException #"annotation"
106+
(macroexpand-1 '(defmojo Test
107+
{:goalie "test"}
108+
[]
109+
(log/info "hello world!"))))))
110+
111+
(deftest invalid-params-vector
112+
(is (thrown-with-msg? IllegalArgumentException #"vector.*param"
113+
(macroexpand-1 '(defmojo
114+
Test {}
115+
non-vector
116+
(log/info "hello world!"))))))
117+
118+
(deftest invalid-param-pair
119+
(is (thrown-with-msg? IllegalArgumentException #"parameter"
120+
(macroexpand-1 '(defmojo
121+
Test {}
122+
[param-name]
123+
(log/info "hello world!")))))
124+
125+
(is (thrown-with-msg? IllegalArgumentException #"parameter"
126+
(macroexpand-1 '(defmojo
127+
Test {}
128+
[param-name [:required "true"]]
129+
(log/info "hello world!")))))
130+
131+
(is (thrown-with-msg? IllegalArgumentException #"parameter"
132+
(macroexpand-1 '(defmojo
133+
Test {}
134+
[param-name no-map]
135+
(log/info "hello world!"))))))
136+
137+
(deftest invalid-param-pair
138+
(is (thrown-with-msg? IllegalArgumentException #"parameter"
139+
(macroexpand-1 '(defmojo
140+
Test
141+
{:goal "test"}
142+
[param-name no-map]
143+
(log/info "hello world!"))))))

0 commit comments

Comments
 (0)