This is a Scala 3 configuration library similar to Config used by rocket-chip with a Jsonnet-like DSL.
The user API of this library revolves around Cde objects. These objects may
contain mappings of string field names to arbitrary values.
import cde._
import cde.syntax._
val boxConfig = Cde {
"width" := 100
"height" := 200
}They may also contain logic to compute field values by performing recursive lookups of other field values.
val boxConfig = Cde {
"width" := 100
"height" := 200
// lazily computes the area based on the final values of height and width
"area" :+= Site.width[Int] * Site.height[Int]
}Cde objects may also mix-in other Cde
objects to override values.
val baseBoxConfig = Cde {
"width" := 100
"height" := 200
"area" :+= Site.width[Int] * Site.height[Int]
}
val boxConfig = baseBoxConfig + Cde {
// overrides baseBoxConfigs's width to 300
"width" := 300
// doubles the existing height value in baseBoxConfig
"height" :+= Up.height[Int] * 2
}This library provides a small DSL for contructing Cdes that uses the builder
pattern.
Operator extension methods can be made available by importing cde.syntax._.
Cdes are constructed using the Cde { ... } method which builds a Cde
according to the builder methods executed within the { ... } block. These
builder methods require a given instance of CdeBuilder (provided by the
Cde.apply method). These are the builder methods (and their operator syntax):
bind(:=): binds a static value to a fieldbindHidden(::=): likebindbut also makes it so that the field will not appear in the elaborated JSONupdate(:+=): updates a field with a value that may be computed from recursive field lookupsupdateHidden(::+=): likeupdatebut also makes it so that the field will not appear in the elaborated JSON These methods may only be called once for each field name within the sameCdeblock.
Cde objects may be mixed-in using the mixin method or the + operator.
This will return a new Cde object with the field values of the RHS overriding
the those in the LHS.
The update and updateHidden methods can accept values computed from field
lookups. These lookups can be performed using the Up and Site objects.
These are named after the up and site variable names used by rocket-chip
for Config views, because they provide the same functionality. The methods in
the Up and Site objects require a given instance of CdeUpdateContext
(provided by update and updateHidden).
Up: Looks up a field value in the parent of the currentCdeobject. This is equivalent tosuperin Jsonnet. Lookups can be performed calling theapplymethod with the field name and its expected value type e.g.Up[Int]("width")or by using method syntaxUp.width[Int]. If no name is provided to theapplymethod e.g.Up[Int](), the current field name of the enclosingupdate/updateHiddenis used. Will cause elaboration to fail if the field does not exist in the parent or if it does not have the expected typit does not have the expected typeSite: Looks up a field in the top-levelCde. i.e. the field lookup is performed from the view of the finalCdeafter all otherCdes have been mixed-in. This is equivalent toselfin Jsonnet. Lookups can be performed calling theapplymethod with the field name and its expected value type e.g.Site[Int]("width")or by using method syntaxSite.width[Int]. If no name is provided to theapplymethod e.g.Site[Int](), the current field name of the enclosingupdate/updateHiddenis used. Will cause elaboration to fail if the field was never set or if it does not have the expected typit does not have the expected type
Cde objects aren't very useful on their own. They need to be elaborated to be
converted into useful formats like JSON objects. Elaboration is done using the
Cde.elaborate method. This method requires a given instance of the
CdeElaborator type class. This library includes a simple JSON AST with an
associated CdeElaborator to produce JSON from Cdes. The elaborate method
returns type Either[Seq[CdeError], T]. Elaboration will return
Seq[CdeError] if any Up/Site look ups fail or if there are validation
errors. CdeErrors contain a source: CdeSource method locating the site of
the error and a message: String method explainin the cause of the error.
import cde.json.JValue.JObject
val box = Cde {
bind("width", 100)
bind("height", 200)
}
Cde.elaborate[JObject](box)
.foreach(o => println(o.prettyPrint()))
// {
// "width": 100,
// "height": 200
// }Here is a simple example that uses the features of this library to create box configurations that dynamiclly updates box coordinates based on the values of user-specified fields.
import cde.syntax._ // operator extension methods
enum Location:
case Center
case BottomRight
case BottomLeft
case TopRight
case TopLeft
val baseBoxConfig = Cde {
import Location._
"origin_x_y" ::= (0, 0)
// lazily calculates the coordinates of the top-left corner based on the
// values of other fields
"top_left" ::+= {
val (x: Int, y: Int) = Site.origin_x_y[Tuple2[Int, Int]]
val height = Site.height[Int]
val width = Site.width[Int]
Site.origin_location[Location] match
case Center => (x + width / 2, y + height / 2)
case BottomRight => (x - width, y + height)
case BottomLeft => (x, y + height)
case TopRight => (x - width, y)
case TopLeft => (x, y)
}
"top" :+= Site.top_left[Tuple2[Int, Int]]._2
"left" :+= Site.top_left[Tuple2[Int, Int]]._1
}
// will fail elaboration because "height" and "width" are not set
Cde.elaborate[JObject](
baseBoxConfig
).swap.foreach(_.foreach(println))
// REPL:9:33
// no field named "height" defined
val smallBoxConfig = Cde {
"width" := 10
"height" := 20
"origin_location" ::= Location.Center
}
Cde.elaborate[JObject](
baseBoxConfig +
smallBoxConfig
).foreach(o => println(o.prettyPrint()))
// {
// "top": 10,
// "left": 5,
// "width": 10,
// "height": 20
// }
val bottomLeftConfig = Cde {
"origin_location" ::= Location.Center
}
Cde.elaborate[JObject](
baseBoxConfig +
smallBoxConfig +
bottomLeftConfig
).foreach(o => println(o.prettyPrint()))
// {
// "top": 10,
// "left": 5,
// "width": 10,
// "height": 20
// }
// creates a new box config with its origin translated
def translate(dx: Int, dy: Int)(cde: Cde): Cde =
cde + Cde {
"origin_x_y" ::+= {
val (x: Int, y: Int) = Up.origin_x_y[Tuple2[Int, Int]]
(x + dx, y + dy)
}
}
Cde.elaborate[JObject](
translate(5, -5)(
baseBoxConfig +
smallBoxConfig +
bottomLeftConfig
)
).foreach(o => println(o.prettyPrint()))
// {
// "top": 5,
// "left": 10,
// "width": 10,
// "height": 20
// }