Skip to content

Latest commit

 

History

History
265 lines (230 loc) · 8.27 KB

README.md

File metadata and controls

265 lines (230 loc) · 8.27 KB

Fun with Scala Dynamic, macros and Yaml

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:

dyaml

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/.

Why on Earth?

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

Why Yaml?

Yeah! Why not JSON? Or XML?

Let's see:

Language Example
Yaml
(Mkay)
- 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 ]
JSON
(Uff!)
[{"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" ] }
]
XML
(Ugh!)
<countries>
  <country>
    <name>USA</name>
    <currency>USD</currency>
    <population>313.9</population>
    <motto>In God We Trust</motto>
    <languages>
      <language>English</language>
    </languages>
  </country>
  <country>
    <name>Canada</name>
    <currency>CAD</currency>
    <population>34.9</population>
    <motto>A Mari Usque ad Mare</motto>
    <languages>
      <language>English</language>
      <language>French</language>
    </languages>
  </country>
  <country>
    <name>Mexico</name>
    <currency>MXN</currency>
    <population>116.1</population>
    <motto>Patria, Libertad, Trabajo y Cultura</motto>
    <languages>
      <language>Spanish</language>
    </languages>
  </country>
</countries>

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

Example

The example below builds the following HTML content:

countries

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
  }
}