How to add a REPL to your Project, using Ammonite and Gradle
Introduction
A REPL (Read-Eval-Print Loop) is a great way to use, explore and test your software. Imagine “browsing” the API of the code you have just written with tab completion.
After researching the existing REPL implementations, I decided for the Ammonite REPL. It’s an active project with an responsive maintainer, very feature rich and yet easy to use. I can only recommend looking into the other Ammonite projects.
The Ammonite REPL features tab completion, syntax highlighting and more. It can also be used for scripting purposes or as a system shell. Even though I will use it here as REPL, I can only recommend looking at its other features too.
How to add the REPL
The following will show you how to add a REPL to your project by the
example of MiniDNS. The approach consists of a new Gradle subproject,
whose name is post-fixed with -repl
, and a two scripts: One written in
Bash the other in Scala.
Creating the ‘-repl’ Gradle subproject
The
-repl
subproject is
used to collect the classpath and thus, should declare dependencies on
all other subprojects. It also comes with a custom task called
printClasspath
which prints the classpath to stdout for later use in
the Bash script.
The
build.gradle
of
MiniDNS looks as follows:
{
ext = '2.11.7'
scalaVersion }
{
dependencies // Delcare all dependencies that should be available in the REPL.
project(':minidns-core')
compile project(':minidns-iterative-resolver')
compile project(':minidns-dnssec')
compile project(':minidns-integration-test')
compile project(':minidns-hla')
compile
// Also pull in Ammonite.
"com.lihaoyi:ammonite_$scalaVersion:0.8.0"
compile
// The dependencies for the -repl tests.
project(path: ":minidns-core", configuration: "testRuntime")
testCompile project(path: ":minidns-core", configuration: "archives")
testCompile }
// The printClasspath task is used by the Bash script to kickoff the
// repl with a properly configured classhpath.
printClasspath(dependsOn: assemble) << {
task .main.runtimeClasspath.asPath
println sourceSets}
The Bash script to kickoff the REPL
The
repl
Bash script will
kickoff the REPL by using gradle to collect the Maven artifacts of the
required dependencies and to prepare the classpath. After that is
done, the java
binary is used to start the REPL.
#!/usr/bin/env bash
set -e
set -u
set -o pipefail
while getopts d OPTION "$@"; do
case $OPTION in
d)
set -x
;;
esac
done
PROJECT_ROOT=$(dirname "${BASH_SOURCE[0]}")
cd "${PROJECT_ROOT}"
echo "Compiling and computing classpath (May take a while)"
# Sadly even with the --quiet option Gradle (or some component of)
# will print the number of warnings/errors to stdout if there are
# any.
GRADLE_CLASSPATH="$(gradle :minidns-repl:printClasspath --quiet |\
tail -n1)"
echo "Classpath computed, starting REPL"
java \
-ea \
-Dscala.usejavacp=true \
-classpath "${GRADLE_CLASSPATH}" \
\
ammonite.Main -f minidns-repl/scala.repl
You may have noticed the -f minidns-repl/scala.repl
argument given
to Ammonite. The scala.repl
is basically file containing Scala code
which is used to setup the environment of the REPL. It is where you
want to declare often used and important parts of the API, that you
want to make easily accessible in the REPL.
For MiniDNS,
the
scala.repl
file
looks like this:
.measite.minidns.minidnsrepl.MiniDnsRepl.init()
de
import de.measite.minidns._
import de.measite.minidns.record._
import de.measite.minidns.Record.TYPE
import de.measite.minidns.dnssec.DNSSECClient
import de.measite.minidns.minidnsrepl.MiniDnsRepl.clearCache
import de.measite.minidns.minidnsrepl.MiniDNSStats._
import de.measite.minidns.jul.MiniDnsJul._
// Some standard values
Predef.println("Set value 'c' to DNSClient")
val c = de.measite.minidns.minidnsrepl.MiniDnsRepl.DNSCLIENT
Predef.println("Set value 'ic' to IterativeDNSClient")
val ic = de.measite.minidns.minidnsrepl.MiniDnsRepl.ITERATIVEDNSCLIENT
Predef.println("Set value 'dc' to DNSSECClient")
val dc = de.measite.minidns.minidnsrepl.MiniDnsRepl.DNSSECCLIENT
// A normal resolver
Predef.println("Set value 'r' to ResolverApi")
val r = de.measite.minidns.hla.ResolverApi.INSTANCE
// A DNSSEC resolver
Predef.println("Set value 'dr' to DnssecResolverApi")
val dr = de.measite.minidns.hla.DnssecResolverApi.INSTANCE
Predef.println("Enjoy MiniDNS. Go ahead and try a query. For example:")
Predef.println("c query (\"geekplace.eu\", TYPE.A)")
Predef.println("dr resolveDnssecReliable (\"verteiltesysteme.net\", classOf[A])")
Conclusion
I use this technique in multiple FOSS projects I’m involved. Most notably:
Having a such a powerful and nice REPL as provided by Ammonite at hand when developing makes it easy to test and evaluate new features. While using the API via the REPL I often discovered rough edges that made the API unnecessarily hard to use, which I’ve fixed afterwards.
Furthermore the REPL allows new user to explore the API. Feel free to try for yourself. Always wondered what is happening when MiniDNS performs a DNSSEC-enabled lookup? Lets try it out:
$ git clone https://github.com/rtreffer/minidns
$ cd minidns
$ ./repl
Compiling and computing classpath (May take a while)
Classpath computed, starting REPL
Loading...
MiniDNS REPL
Set value 'c' to DNSClient
Set value 'ic' to IterativeDNSClient
Set value 'dc' to DNSSECClient
Set value 'r' to ResolverApi
Set value 'dr' to DnssecResolverApi
Enjoy MiniDNS. Go ahead and try a query. For example:
c query ("geekplace.eu", TYPE.A)
dr resolveDnssecReliable ("verteiltesysteme.net", classOf[A])
Welcome to the Ammonite Repl 0.8.0
(Scala 2.11.8 Java 1.8.0_144)
@ enableMiniDnsTrace
@ dc queryDnssec ("uni-erlangen.de", TYPE.A)