/moultingyaml

Scala wrapper for SnakeYAML

Primary LanguageScalaMIT LicenseMIT

MoultingYAML

Join the chat at https://gitter.im/jcazevedo/moultingyaml Build Status Coverage Status License

Maven Central Maven Central Maven Central

MoultingYAML is a Scala wrapper for SnakeYAML based on spray-json.

Its basic idea is to provide a simple immutable model of the YAML language, built on top of SnakeYAML models, as well as a type-class based serialization and deserialization of custom objects.

Installation

MoultingYAML's latest release is 0.4.0 and is built against Scala 2.12.0, 2.11.8 and 2.10.6.

To use it in an existing SBT project, add the following dependency to your build.sbt:

libraryDependencies += "net.jcazevedo" %% "moultingyaml" % "0.4.0"

Usage

In order to use MoultingYAML, bring all relevant elements into scope with:

import net.jcazevedo.moultingyaml._
import net.jcazevedo.moultingyaml.DefaultYamlProtocol._ // if you don't supply your own protocol

You can then parse a YAML string into its Abstract Syntax Tree (AST) representation with:

val source = """- Mark McGwire
               |- Sammy Sosa
               |- Ken Griffey""".stripMargin
val yamlAst = source.parseYaml

It is also possible to print a YAML AST back to a String using the prettyPrint method:

val yaml = yamlAst.prettyPrint

If more fine-grained control over the printed yaml is needed, it is possible to use the configurable print method. For example, to enclose everything in double quotes:

val yaml = yamlAst.print(scalarStyle = DoubleQuoted)

YAML provide different scalar styles to choose from, controlled by the argument scalarStyle of the print method. The possible values for scalarStyle are Plain, SingleQuoted, DoubleQuoted, Literal and Folded. Refer to the YAML specification for details on each representation.

In addition, YAML also has flow styles, in order to be able to use explicit indicators instead of indentation to denote scope. The flow style is controlled by the flowStyle argument of the print method. The possible values for flowStyle are Flow, Block and Auto. Block style uses indentation, whereas Flow style relies on explicit indicators to denote scope. The Auto flow style attempts to combine both the Block and Flow style within the same document.

Scala objects can be converted to a YAML AST using the pimped toYaml method:

val yamlAst = List(1, 2, 3).toYaml

Convert a YAML AST to a Scala object with the convertTo method:

val myList = yamlAst.convertTo[List[Int]]

In order to support calling the toYaml and convertTo methods for an object of type T, you need to have implicit values in scope that provide YamlFormat[T] instances for T and all types used by T (directly or indirectly). You normally do that through a YamlProtocol.

YamlProtocol

YamlProtocols follow the same design as spray-json's JsonProtocols, which in turn are based on SJSON's. It's a type-class based approach that connects an existing type T with the logic of how to (de)serialize its instances to and from YAML.

A YamlProtocol is a bunch of implicit values of type YamlFormat[T], where each YamlFormat[T] contains the logic of how to convert instances of T to and from YAML.

MoultingYAML comes with a DefaultYamlProtocol, which already covers all of Scala's value types as well as the most important reference and collection types. The following are types already taken care of by the DefaultYamlProtocol:

  • Byte, Short, Int, Long, Float, Double, Char, Unit, Boolean
  • String, Symbol
  • BigInt, BigDecimal
  • Option, Either, Tuple1 - Tuple7
  • List, Array
  • immutable.{Map, Iterable, Seq, IndexedSeq, LinearSeq, Set, Vector}
  • collection.{Iterable, Seq, IndexedSeq, LinearSeq, Set}

When you want to convert types not covered by the DefaultYamlProtocol, you need to provide a YamlFormat[T] for your custom types.

Prodiving YamlFormats for Case Classes

If your custom type T is a case class then augmenting the DefaultYamlProtocol with a YamlFormat[T] can be done using the yamlFormatX helpers, where X stands for the number of fields in the case class:

case class Color(name: String, red: Int, green: Int, blue: Int)

object MyYamlProtocol extends DefaultYamlProtocol {
  implicit val colorFormat = yamlFormat4(Color)
}

import MyYamlProtocol._
import net.jcazevedo.moultingyaml._

val yaml = Color("CadetBlue", 95, 158, 160).toYaml
val color = yaml.convertTo[Color]

Example combining custom YamlFormats:

case class Color(name: String, red: Int, green: Int, blue: Int)
case class Palette(name: String, colors: Option[List[Color]] = None)

object PaletteYamlProtocol extends DefaultYamlProtocol {
  implicit val colorFormat = yamlFormat4(Color)
  implicit val paletteFormat = yamlFormat2(Palette)
}

import PaletteYamlProtocol._
import net.jcazevedo.moultingyaml._

val yaml = """name: My Palette
             |colors:
             |- name: color 1
             |  red: 1
             |  green: 1
             |  blue: 1
             |- name: color 2
             |  red: 2
             |  green: 2
             |  blue: 2
             |""".stripMargin.parseYaml
val palette = yaml.convertTo[Palette]

If you explicitly declare the companion object for your case class the notation above will stop working. You'll have to explicitly refer to the companion object's apply method to fix this:

case class Color(name: String, red: Int, green: Int, blue: Int)
object Color

object MyYamlProtocol extends DefaultYamlProtocol {
  implicit val colorFormat = yamlFormat4(Color.apply)
}

If your case class has a type parameter the yamlFormat methods can also help you. However, there is a little more boilerplate required as you need to add context bounds for all type parameters and explicitly refer to the case classes apply method as in this example:

case class NamedList[A](name: String, items: List[A])

object MyYamlProtocol extends DefaultYamlProtocol {
  implicit def namedListFormat[A: YamlFormat] = yamlFormat2(NamedList.apply[A])
}

NullOptions

As in spray-json, the NullOptions trait supplies an alternative rendering mode for optional case class members. Normally optional members that are undefined (None) are not rendered at all. By mixing in this trait into your custom YamlProtocol you can enforce the rendering of undefined members as null. (Note that this only affects YAML writing, MoultingYAML will always read missing optional members as well as null optional members as None)

Providing YamlFormats for other Types

To provide (de)serialization logic for types that aren't case classes, one has to define the write and read methods of YamlFormat. Here is one example:

class Color(val name: String, val red: Int, val green: Int, val blue: Int)

object MyYamlProtocol extends DefaultYamlProtocol {
  implicit object ColorYamlFormat extends YamlFormat[Color] {
    def write(c: Color) =
      YamlArray(
        YamlString(c.name),
        YamlNumber(c.red),
        YamlNumber(c.green),
        YamlNumber(c.blue))

    def read(value: YamlValue) = value match {
      case YamlArray(
        Vector(
          YamlString(name),
          YamlNumber(red: Int),
          YamlNumber(green: Int),
          YamlNumber(blue: Int))) =>
        new Color(name, red, green, blue)
      case _ => deserializationError("Color expected")
    }
  }
}

import MyYamlProtocol._

val yaml = new Color("CadetBlue", 95, 158, 160).toYaml
val color = yaml.convertTo[Color]

This serializes Color instances as a YAML array. Another way would be to serialize Colors as YAML mappings, which are called YAML objects in MoultingYAML:

object MyYamlProtocol extends DefaultYamlProtocol {
  implicit object ColorYamlFormat extends YamlFormat[Color] {
    def write(c: Color) = YamlObject(
      YamlString("name") -> YamlString(c.name),
      YamlString("red") -> YamlNumber(c.red),
      YamlString("green") -> YamlNumber(c.green),
      YamlString("blue") -> YamlNumber(c.blue)
    )
    def read(value: YamlValue) = {
      value.asYamlObject.getFields(
        YamlString("name"),
        YamlString("red"),
        YamlString("green"),
        YamlString("blue")) match {
        case Seq(
          YamlString(name),
          YamlNumber(red: Int),
          YamlNumber(green: Int),
          YamlNumber(blue: Int)) =>
          new Color(name, red, green, blue)
        case _ => deserializationError("Color expected")
      }
    }
  }
}

Credits

Most of MoultingYAML's type-class (de)serialization code was inspired by spray-json, by Mathias Doenitz. spray-json was, in turn, inspired by the SJSON library by Debasish Ghosh. Both deserve credits here.

License

MoultingYAML is licensed under the MIT license. See LICENSE.md for details.

Contributions

Feedback and contributions to the project are very welcome. Use GitHub's issue tracker to report any issues you might have when using the project. Submit code contributions via GitHub's pull requests.