The intent of this library is to allow for a runtime-interpreted script to modify a compiled Scala case class. Imagine you have a business object Shipment, which has Items. Let's say these objects flow in an event stream and you want to be able to mutate certain values in-flight. The challenge is these mutations vary at runtime, for example per client, and the person in your company with the business logic knowledge isn't a software engineer. Their coding skill is at an Excel macro level.
What if there were a way to use a simple scripting language to apply changes to the in-flight objects? That's what dynalens does.
// Scala Business objects
case class Item(number: String, qty: Int, price: Double)
case class Shipment(id: String, items:List[Item], shipMethod: Int)
# My script
shipMethod = 3 # set a top-level field
items[].qty = this * 2 # change all qty fields of all items
items[3].qty = this / 2 # change qty fields only of items[3]
items[3].qty = shipMethod # value set from top-level
The ability to change values can span multiple collections (lists). Let's add another couple layers of business object:
case class Pack(label: String, caseSize: Int, shipments: List[Shipment])
case class Order(id: String, pack: Pack)
If Order is the top-most object we can still change all the qty values of all the items:
pack.shipments[].items[].qty = 1 # set all qty to 1 for all items
In this example we iterated over 2 levels of nesting (shipments and items) with one simple line of assignment code.
In your build.sbt file add the current version of the library to libraryDependences:
libraryDependencies += "co.blocke" %% "dynalens" % CURRENT_VERSION
(CURRENT_VERSION value can be taken from the 'maven central' badge in this github repo.)
dynalens is a Scala 3 macro written to work with ZIO. If you haven't ever worked with ZIO, don't be intimidated. At a high level it's a lot simpler than it appears. The main nugget here is that it provides a pervasive error channel so you don't have to "sew" exception handling through your code. Here's how you set it up:
case class Person(name: String, age: Int, id: String)
val inst = Person("Mike", 45, "abc")
val script = "age = age * 2" // This can come from anywhere in practice--eval'ed at runtime
val lens = dynalens[Person]
for{
compiledScript <- Parser.parseScript(script)
(result,_) <- lens.run(compiledScript, inst)
} yield result // ZIO[Any,DynaLensError,Person]
We really love ZIO around here, but if you just can't get there, there's a non-ZIO flavor that uses Either. Steps 1-3 as above. Step 4 becomes:
val lens = dynalens[Person]
val result: Either[DynaLensError, (Shipment, Map[String, (Any, DynaLens[?])])] =
for {
compiled <- Parser.parseScriptNoZIO(script)
(output,_) <- lens.runNoZIO(compiled, inst)
} yield output
- 1.0.0 - Initial Release