Loam is a dialect of Scala, a general-purpose programming language. You can write anything you want (mostly; there some esoteric limits, but the basic point stands). Of course, just because one can do something does not mean that one should. To help with this, here are some recommendations from the LoamStream team.
Fundamentally, we encourage a separation between config files, containing just data, and Loam files, which declare stores and commands, and optionally declare pipeline topologies that depend on data in config files. Loam code can read from any type of file, but LoamStream includes built-in support for HOCON files. For example, given a config file like:
numFoos = 42fooLabel = "some text"xEnabled = falsefoo { bar { baz = "xyz" } labels = ["a", "b", "c"] counts = [42, 43, 44] bips = [ { bip = 99 bop = "42" }, { bip = 42 bop = "hello" } ]}it could be read in a Loam file like
val myConfigFile: Path = path("path/to/myDatasetConfig.conf")val config: DataConfig = loadConfig(myConfigFile)//Check for the presence of a value at a keyval fooIsDefined: Boolean = config.isDefined("foo")//get values stored at keys:val numFoos: Int = config.getInt("numFoos")val label: String = config.getStr("fooLabel")val isXEnabled: Boolean = config.getBool("xEnabled")val foo: DataConfig = config.getObj("foo")//nesting is possible:val baz: String = config.getObj("foo").getObj("bar").getStr("baz")//as are lists/arrays:val labels: Seq[String] = foo.getStrList("labels")val counts: Seq[Int] = foo.getIntList("counts")val bips: Seq[DataConfig] = foo.getObjList("bips")Use .loam files to declare commands and stores. .loam files should be as declarative as possible; try to minimize logic in them, and make any logic expression-oriented instead of imperative.
for(arrayName <- myConfig.getStrList("arrays")) { val input = store.at("in/$arrayName.vcf") val output = store.at("out/$arrayName.txt") cmd"foo --array $arrayName -i $input -o $output"}val dir = parentDir.listFiles.filter(_.getName.endsWith("foo"))val arrayNameFile = parentDir.listFiles.filter(_.getName.endsWith("arrays")).headval rawArrayNames = Files.readContents(arrayNameFile).split("\\s+")val arrayNames = rawArrayNames.map(_.toUpperCase) for(arrayName <- arrayNames) { val input = store.at("in/$arrayName.vcf") val output = store.at("out/$arrayName.txt") cmd"foo --array $arrayName -i $input -o $output"}Conditionals are fine, as is building the pipeline topology from config data:
/* * With config data like * { * fooEnabled = true * } * wire up the pipeline one way if fooEnabled is true, and another way otherwise. */val shouldFoo = myConfig.getBool("fooEnabled")if(shouldFoo) { //N cmd"..."s} else { //M slightly different cmd"..."s}or given config data like
blah { arrays = [ { name = "bar" fooLabels = ["x", "y", "z"] }, ... { name = "baz" fooLabels = ["a", "b"] }, ]}do
for(arrayConfig <- myConfig.getObjList("arrays")) { val arrayName = arrayConfig.getStr("name") val fooLabels = arrayConfig.getStrList("fooLabels") for(fooLabel <- fooLabels) { cmd"some-command -a $arrayName -l $fooLabel" }}