At Bright IT, we often extend Content Management Systems for our customers. Predictably, this means that we need to interact with CMS services allowing remote access to content and data. Tasks for which this interaction is necessary are ofter either repetitive - like uploading development content - or formulaic - like migrating content to a new format. Naturally, this makes them prime candidates for automation.
As CMS services often have Java bindings available and we're a Scala company, we've begun considering using Scala to solve the issue. After all, Scala is both known for its strong static typing - which lends itself well to ensuring that, for instance, client's content is migrated safely - and its remarkable terseness which sometimes makes people confuse it with a dynamic language. This is where Ammonite, a modernized Scala REPL and script runner, comes in. Ammonite can run Scala files as though they were scripts. "Hello, world" is as simple as:
Notice that there's zero fuss necessary - we just write the code that we want to be executed with no ceremony. Of course, this is not where Ammonite's usefulness ends - the nice thing about it is that it not only makes writing Scala scripts possible, it also makes it easy. Let's see a script that will output the status of a specific issue on GitHub:
Example interaction with this script looks like this:
This time, we did need to write a
main method - but this was because we needed to parse arguments. Notice that there's no argument parsing code in the script - we simply annotated a method with
@main and Ammonite took care of the rest. We also needed to include a dependency, which again was done with an absolute minimum of fuss - all that was necessary was a "magic"
$ivy import. In Python, the library would need to be downloaded before the script can be run, but Ammonite takes care of that on its own - if the library is not available locally, it will be downloaded when the script is executed for the first time.
Ammonite also allows scripts to include other scripts:
greet.sc behaves like this:
This feature is very much necessary when scripting remote services, as we definitely do not want to duplicate service-related code inside each script. More interestingly, it plays very nicely with
@main methods - a script can call other scripts in a simple way and remain type-safe since, after all, the entrypoints to other scripts are just methods.
Last but not least, Ammonite also helps with developing scripts. Since Scala is statically typed, we can expect a type error or ten when writing our scripts. Manually re-running them can get tedious, so Ammonite can instead watch script files and re-run them on each change if
--watch flag is set. If combined with
--predef flag, a REPL will instead be opened with the script's top-level definitions available. Ammonite allows opening source code for methods and objects and additionally has great tab-completion and multi-line editing - all of these features allow using Ammonite as a quasi-IDE for progressively writing the script.
To illustrate the points so far, we've prepared an example repository that allows interacting with a Northwind-like database.
Scripts inside the
cmd directory can be used to set up and tear down the local environment, download remote data and update it. Some of the scripts are lower-level than others - for example,
docker.sc is responsible for Docker and is run by
setup.sc when setting up the environment. If necessary,
cmd/repl can be used to open up a REPL with all the scripts loaded and ready to run. This is very helpful if you're not entirely familiar with the scripts since inside the REPL we have tab-completion available. Since argument parsing is based on methods, running the scripts from the REPL is very similar to running them normally:
On a higher level, it's worth noticing that there are two kinds of Scala files in the repository - "scripts", which are directly in
cmd directory, and "modules" contained inside
cmd/modules. "Modules" are code common to most, if not all scripts - in this specific case they are essentially bindings for the Northwind database. When interacting with a service for which only Java bindings are available, modules would mostly contain Scala adapters instead. A very good reason for organizing code like this, besides avoiding code duplication, is that it's very easy to create a library out of such modules - each file can be directly converted to an object. This, in turn, is very handy if it turns out that another project needs similar scripts - common parts can simply be lifted to a library, ready to be downloaded from the company repository.
This brings us to the last, but not least, problem which our example repository demonstrates how to solve - custom repositories and authorization. While Ammonite by default has support for the former, it doesn't really support the latter. We've prepared a minimal wrapper around Ammonite which solves this problem, aptly named
bin/try-auth.sh will start a Bash session where
amm is based on
As we can see,
is-prime.sc uses a specific version of Apache
commons-math3, only available from the Red Hat Maven repository. You can try running it without using
try-auth.sh - it won't work!
bin to your
$PATH, which contains
amm script using
amm+auth internally. Hypothetically, if the Red Hat required authorization for its Maven repository, storing the credentials in your home directory so
amm+auth can use them would be as simple as:
If you're interested in seeing how
amm+auth can help you, head over to its repo at https://github.com/BrightIT/coursier-plus-auth.
To sum up: we've found the approach we've presented so far very useful for developing scripts that can interact with remote services. Ammonite not only allows writing such scripts, but it also lends itself naturally to building simple command-line interfaces out of them and, if necessary, creating libraries out of CLIs developed this way. Additionally, Ammonite's features are helpful enough during development that often no IDE is necessary. If you're too often manually dealing with services for which Java bindings are available, then scripting those interactions with Ammonite might be just what you need.