How to add a REPL to your Project, using Ammonite and Gradle

Posted on September 28, 2017
Tags: 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 {
  scalaVersion = '2.11.7'
}

dependencies {
  // Delcare all dependencies that should be available in the REPL.
  compile project(':minidns-core')
  compile project(':minidns-iterative-resolver')
  compile project(':minidns-dnssec')
  compile project(':minidns-integration-test')
  compile project(':minidns-hla')

  // Also pull in Ammonite.
  compile "com.lihaoyi:ammonite_$scalaVersion:0.8.0"

  // The dependencies for the -repl tests.
  testCompile project(path: ":minidns-core", configuration: "testRuntime")
  testCompile project(path: ":minidns-core", configuration: "archives")
}

// The printClasspath task is used by the Bash script to kickoff the
// repl with a properly configured classhpath.
task printClasspath(dependsOn: assemble) << {
  println sourceSets.main.runtimeClasspath.asPath
}

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:

de.measite.minidns.minidnsrepl.MiniDnsRepl.init()

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)