SDynamic
is a small utility to write untyped object literals in Scala
and then treat dynamic results as if they were regular Scala objects.
// Look ma: no intervening case classes!
val naftaCountries = dyaml"""
|- { name: USA, currency: USD, population: 313.9,
| motto: In God We Trust, languages: [ English ] }
|- { name: Canada, currency: CAD, population: 34.9,
| motto: A Mari Usque ad Mare, languages: [ English, French ] }
|- { name: Mexico, currency: MXN, population: 116.1,
| motto: 'Patria, Libertad, Trabajo y Cultura', languages: [ Spanish ] }
""".toList
assert(naftaCountries.length == 3)
assert(naftaCountries(0).name == "USA")
assert(naftaCountries(1).population == 34.9)
assert(naftaCountries(2).motto == "Patria, Libertad, Trabajo y Cultura")
assert(naftaCountries(1).languages.toList == Seq("English", "French"))
The serialization language used to enunciate object graphs is Yaml.
Object-like property manipulation is based on Scala's
Dynamic
trait.
The dyaml
and syaml
string interpolators provide a convenient notation while ensuring Yaml well-formedness
at compile-time via a simple macro. (syaml
uses the SnakeYAML parser).
Intellij Idea users get the added bonus of Yaml literal syntax highlighting and edit-time validation:
In a small way, SDynamic relates to an old request first formulated in 2008: SI-993: Add support for YAML (like XML).
This utility is further described at http://blog.xrrocha.net/.
Yeah, why? And what about type-safety? ;-)
Like many such small utilities, SDynamic
was born of a personal itch to scratch: I've needed to write numerous unit
tests requiring lots of structured (but otherwise volatile) data.
Creating case classes nesting other case classes and then writing looong object literal expressions for them quickly grows tedious and cumbersome:
case class Country(name: String, currency: String, population: Double,
motto: String, languages: Seq[String])
// Wrappers, parens, quotes, commas. Oh my!
val naftaCountries = Seq(
Country(
name = "USA",
currency = "UDS",
population = 313.9,
motto = "In God We Trust",
languages = Seq("English")),
Country(
name = "Canada",
currency = "CAD",
population = 34.9,
motto = "A Mari Usque ad Mare",
languages = Seq("English", "French")),
Country(
name = "Mexico",
currency = "MXN",
population = 116.1,
motto = "Patria, Libertad, Trabajo y Cultura",
languages = Seq("Spanish"))
)
☝️ The astute reader will notice the above could be written sàns named parameters. For nested structures with more than just a few fields, however, positional parameters in object literals quickly become a liability as they obscure value-to-field attribution.
When dealing with one-off object literals we want:
- Minimal verbosity
- Maximal readability
Yeah! Why not JSON? Or XML?
Let's see:
Language | Example |
---|---|
Yaml (Mkay) |
|
JSON (Uff!) |
|
XML (Ugh!) |
|
Yaml minimizes punctuation while enhancing readability:
- No need to enclose property values or (the horror!) property names in quotation marks
- No need to separate list elements with commas or enclosing lists in brackets when using multi-line mode
- No need to verbosely mark the beginning and end of each property
The example below builds the following HTML content:
object Example extends App {
import DYaml._
val countries = dyaml"""
|- name: USA
| currency: USD
| population: 313.9
| motto: In God We Trust
| languages:
| - { name: English, comment: Unofficially official }
| - { name: Spanish, comment: Widely spoken all over }
| flag: http://upload.wikimedia.org/wikipedia/en/thumb/a/a4/Flag_of_the_United_States.svg/30px-Flag_of_the_United_States.svg.png
|- name: Canada
| currency: CAD
| population: 34.9
| motto: |
| A Mari Usque ad Mare<br>
| (<i>From sea to sea, D'un océan à l'autre</i>)
| languages:
| - { name: English, comment: 'Official, yes' }
| - { name: French, comment: 'Officiel, oui' }
| flag: http://upload.wikimedia.org/wikipedia/en/thumb/c/cf/Flag_of_Canada.svg/30px-Flag_of_Canada.svg.png
|- name: Mexico
| currency: MXN
| population: 116.1
| motto: |
| Patria, Libertad, Trabajo y Cultura<br>
| (<i>Homeland, Freedom, Work and Culture</i>)
| languages:
| - { name: Spanish, comment: 'Oficial, sí' }
| - { name: Zapoteco, comment: Dxandi' anja }
| flag: http://upload.wikimedia.org/wikipedia/commons/thumb/f/fc/Flag_of_Mexico.svg/30px-Flag_of_Mexico.svg.png
""".toList
import Html._
def country2Html(country: SDynamic) = html"""
|<tr>
| <td><img src="${country.flag}"></td>
| <td>${country.name}</td>
| <td>${country.motto}</td>
| <td>
| <ul>
| ${
country.languages.toList.map { lang =>
s"<li>${lang.name}: ${lang.comment}</li>"
}.
mkString("\n")
}
| </ul>
| </td>
|</tr>
"""
val pageHtml = html"""
|<html>
|<head><title>NAFTA Countries</title><meta charset="UTF-8"></head>
|<body>
|<table border='1'>
|<tr>
| <th>Flag</th>
| <th>Name</th>
| <th>Motto</th>
| <th>Languages</th>
|</tr>
|<tr>${(countries map country2Html).mkString}</tr>
|</table>
|</body>
|</html>
"""
val out = new java.io.FileOutputStream("src/test/resources/countries.html")
out.write(pageHtml.getBytes("UTF-8"))
out.flush()
out.close()
}
object Html {
implicit class HtmlString(val sc: StringContext) extends AnyVal {
def html(args: Any*) = sc.s(args: _*).stripMargin.trim
}
}