-
Notifications
You must be signed in to change notification settings - Fork 21
Basic guide
Phobos is a data-binding library that allows to transform case classes or sealed traits to XML documents and vice-verca.
A case class represents an XML element. Case class parameters correspond to contents of the element. There are three types of parameters:
- parameters with
@attr
annotation are attributes; - parameter with
@text
annotation is the text inside the element (only one is allowed per case class); - parameters without these two annotations are children elements.
Attributes' and children elements' labels are defined by parameter names. Label of the element represented by the case class is provided externally, so that a case class could describe several elements with same structure, but different names.
Case class parameters are processed with several special macros.
For encoding some case class or sealed trait A
to XML document encoding.XmlEncoder[A]
(ru.tinkoff.phobos.
prefix is omitted) instance is required. This instance is created with derivation.semiauto.deriveXmlEncoder
macro. The macro takes single param - the name of the root element to encode. For case classes it traverses parameters and tries to infer implicit
-
encoding.AttributeEncoder
for attributes (@attr
), -
encoding.TextEncoder
for text (@text
) and -
encoding.ElementEncoder
for elements (everything else).
Instances of these typeclasses are defined for simple types. Instances of encoding.ElementEncoder
can be also derived for case classes or sealed traits with derivation.semiauto.deriveElementEncoder
macro. It works much like derivation.semiauto.deriveXmlEncoder
, but does not require element label. Instance of encoding.XmlEncoder
can also be created from encoding.ElementEncoder
by helper functions from encoding.XmlEncoder
object.
Decoding works the same way, just replace "encoding" to "decoding" in typeclass names.
Scala classes below represent journey information, which can be encoded to (and decoded from) XML.
import ru.tinkoff.phobos.decoding._
import ru.tinkoff.phobos.encoding._
import ru.tinkoff.phobos.syntax._
import ru.tinkoff.phobos.derivation.semiauto._
case class TravelPoint(country: String, city: String)
object TravelPoint {
implicit val travelPointElementEncoder: ElementEncoder[TravelPoint] =
deriveElementEncoder
implicit val travelPointElementDecoder: ElementDecoder[TravelPoint] =
deriveElementDecoder
}
case class Price(@attr currency: String, @text value: Double)
object Price {
implicit val priceElementEncoder: ElementEncoder[Price] = deriveElementEncoder
implicit val priceElementDecoder: ElementDecoder[Price] = deriveElementDecoder
}
case class Journey(price: Price, departure: TravelPoint, arrival: TravelPoint)
object Journey {
implicit val journeyXmlEncoder: XmlEncoder[Journey] = deriveXmlEncoder("journey")
implicit val journeyXmlDecoder: XmlDecoder[Journey] = deriveXmlDecoder("journey")
}
Journey
class can be encoded to XML documents like this one:
<journey>
<departure>
<country>France</country>
<city>Marcelle</city>
</departure>
<arrival>
<country>Germany</country>
<city>Munich</city>
</arrival>
<price currency="EUR">1000</price>
</journey>
This code encodes journey class to XML and then decodes it back, checking, that initial and decoded values are equal.
val journey =
Journey(
price = Price("EUR", 1000.0),
departure = TravelPoint("France", "Marcelle"),
arrival = TravelPoint("Germany", "Munich")
)
val xml: String = XmlEncoder[Journey].encode(journey)
println(xml)
val decodedJourney = XmlDecoder[Journey].decode(xml)
println(decodedJourney)
assert(Right(journey) == decodedJourney)
Some documents may contain several elements with same label and structure, like this one:
<shoppingList>
<item count="2">"Sausage"</item>
<item count="4">"Egg"</item>
<item count="1">"Tomato"</item>
</shoppingList>
It can be processed with following code:
import ru.tinkoff.phobos.decoding._
import ru.tinkoff.phobos.derivation.semiauto._
import ru.tinkoff.phobos.syntax._
val xml = """
|<shoppingList>
| <item count="2">"Sausage"</item>
| <item count="4">"Egg"</item>
| <item count="1">"Tomato"</item>
|</shoppingList>
|""".stripMargin
case class Item(@attr count: Int, @text value: String)
object Item {
implicit val itemElementDecoder: ElementDecoder[Item] = deriveElementDecoder
}
case class ShoppingList(item: List[Item])
object ShoppingList {
implicit val shoppingListXmlDecoder: XmlDecoder[ShoppingList] = deriveXmlDecoder("shoppingList")
}
println(XmlDecoder[ShoppingList].decode(xml))
// Right(ShoppingList(List(Item(2,"Sausage"), Item(4,"Egg"), Item(1,"Tomato"))))
Hence, it works very similar to regular elements.
Namespaces are usually represented with case objects, which have Namespace
typeclass instance:
import ru.tinkoff.phobos.Namespace
case object foo {
implicit val fooNamespace: Namespace[foo.type] = Namespace.mkInstance("example.com")
}
Namespace of case class parameter can be defined with @xmlns
annotation:
import ru.tinkoff.phobos.syntax._
case class Bar(@xmlns(foo) baz: Int)
This design looks much like namespace prefixes in XML documents, however name of case object is ignored and name space prefix value will differ from it.
Macros derivation.semiauto.deriveXmlEncoder
and derivation.semiauto.deriveXmlDecoder
have overloaded versions which allow to provide namespaces for elements along with labels:
import ru.tinkoff.phobos.encoding._
import ru.tinkoff.phobos.derivation.semiauto._
implicit val barXmlEncoder: XmlEncoder[Bar] = deriveXmlEncoder("bar", foo)
println(XmlEncoder[Bar].encode(Bar(10)))
// <?xml version='1.0' encoding='UTF-8'?>
// <ans1:bar xmlns:ans1="example.com">
// <ans1:baz>10</ans1:baz>
// </ans1:bar>
To activate macro annotations add -Ymacro-annotations
flag to scalac options (Scala 2.13):
scalacOptions += "-Ymacro-annotations"
For Scala 2.12 enable scala macro paradise:
libaryDependencies ++= compilerPlugin("org.scalamacros" % "paradise" % "X.X.X" cross CrossVersion.patch)
All annotations are contained in ru.tinkoff.phobos.annotations
package.
-
ElementCodec
derivesElementEncoder
andElementDecoder
for case class:@ElementCodec case class Foo()
-
XmlCodec
derivesElementEncoder
,ElementDecoder
,XmlEncoder
andXmlDecoder
for provided name:@XmlCodec("foo") case class Foo()
-
XmlnsDef
providesNamespace
instance for an object:@XmlnsDef("example.com") case object xpl
-
XmlCodecNs
works likeXmlCodec
, but it also requires namespace:@XmlCodecNs("foo", xpl) case class Foo()
Decoders and encoders can be derived separately with phobos-derevo module.
Case class parameter names can be transformed with special configured codec:
import ru.tinkoff.phobos.annotations.XmlCodec
import ru.tinkoff.phobos.configured.ElementCodecConfig
import ru.tinkoff.phobos.configured.naming._
import ru.tinkoff.phobos.decoding._
import ru.tinkoff.phobos.encoding._
import ru.tinkoff.phobos.syntax._
val config = ElementCodecConfig.default
.withElementsRenamed(camelCase)
.withAttributesRenamed(snakeCase)
// For element codecs @XmlCodec(config) or
// implicit val colorEncoder = deriveElementEncoderConfigured[Color](config)
@XmlCodec("Color", config)
case class Color(@attr htmlName: String, hexCode: String)
val color = Color("tomato", "#FF6347")
val encoded = XmlEncoder[Color].encode(color)
println(encoded)
// <?xml version='1.0' encoding='UTF-8'?>
// <Color html_name="tomato">
// <HexCode>#FF6347</HexCode>
// </Color>
assert(XmlDecoder[Color].decode(encoded) == Right(color))
Parameters also can be specifically renamed with @renamed
annotation:
import ru.tinkoff.phobos.annotations.XmlCodec
import ru.tinkoff.phobos.configured.ElementCodecConfig
import ru.tinkoff.phobos.configured.naming._
import ru.tinkoff.phobos.decoding._
import ru.tinkoff.phobos.encoding._
import ru.tinkoff.phobos.syntax._
val config = ElementCodecConfig.default
.withElementsRenamed(camelCase)
.withAttributesRenamed(snakeCase)
@XmlCodec("Address", config)
case class Address(
@attr @renamed("postCode") code: String, // will be renamed to "postCode", but not "PostCode"
city: String, // will be transformed to "City"
@renamed("state") region: String // will be renamed to "state", but not "State"
)
val address = Address("124365", "New York", "NY")
val encoded = XmlEncoder[Address].encode(address)
println(encoded)
// <?xml version='1.0' encoding='UTF-8'?>
// <Address postCode="124365">
// <City>New York</City>
// <state>NY</state>
// </Address>
assert(XmlDecoder[Address].decode(encoded) == Right(address))
Sealed trait support was implemented in 0.5.0. Sealed trait instances are differentiated with special discriminator-attribute. By default it is http://www.w3.org/2001/XMLSchema-instance:type
as in XSD. There are some examples below.
import ru.tinkoff.phobos.annotations.XmlCodec
import ru.tinkoff.phobos.annotations.ElementCodec
import ru.tinkoff.phobos.decoding._
import ru.tinkoff.phobos.encoding._
@XmlCodec("mammal")
sealed trait Mammal
@ElementCodec
case class Lion(name: String, isLeader: Boolean) extends Mammal
@ElementCodec
case class Wolf(name: String, age: Int) extends Mammal
val lion = Lion("Mickey", true)
val encodedLion = XmlEncoder[Mammal].encode(lion)
println(encodedLion)
// <?xml version='1.0' encoding='UTF-8'?>
// <mammal xmlns:ans1="http://www.w3.org/2001/XMLSchema-instance" ans1:type="Lion">
// <name>Mickey</name>
// <isLeader>true</isLeader>
// </mammal>
val wolf = Wolf("Bill", 33)
val encodedWolf = XmlEncoder[Mammal].encode(wolf)
println(encodedWolf)
// <?xml version='1.0' encoding='UTF-8'?>
// <mammal xmlns:ans1="http://www.w3.org/2001/XMLSchema-instance" ans1:type="Wolf">
// <name>Bill</name>
// <age>33</age>
// </mammal>
assert(XmlDecoder[Mammal].decode(encodedLion) == Right(lion))
assert(XmlDecoder[Mammal].decode(encodedWolf) == Right(wolf))
Discriminator-attribute can be renamed with configured codec. Discriminator values can be transformed with configured codec or overwritten with @discriminator
annotation.
import ru.tinkoff.phobos.annotations.XmlCodec
import ru.tinkoff.phobos.annotations.ElementCodec
import ru.tinkoff.phobos.decoding._
import ru.tinkoff.phobos.encoding._
import ru.tinkoff.phobos.configured.ElementCodecConfig
import ru.tinkoff.phobos.configured.naming._
import ru.tinkoff.phobos.syntax.discriminator
val config = ElementCodecConfig.default
.withConstructorsRenamed(snakeCase)
.withDiscriminator("species", None)
// For element codecs @XmlCodec(config) or
// implicit val fishEncoder = deriveElementEncoderConfigured[Fish](config)
@XmlCodec("fish", config)
sealed trait Fish
@ElementCodec
@discriminator("ClownFish") // will not be transformed to "clown_fish"
case class ClownFish(name: String, finNumber: Int) extends Fish
@ElementCodec
case class WhiteShark(name: String, teethNumber: Long) extends Fish
val clownFish = ClownFish("Nemo", 1)
val encodedClownFish = XmlEncoder[Fish].encode(clownFish)
println(encodedClownFish)
// <?xml version='1.0' encoding='UTF-8'?>
// <fish species="ClownFish">
// <name>Nemo</name>
// <finNumber>1</finNumber>
// </fish>
val whiteShark = WhiteShark("John", 200)
val encodedWhiteShark = XmlEncoder[Fish].encode(whiteShark)
println(encodedWhiteShark)
// <?xml version='1.0' encoding='UTF-8'?>
// <fish species="white_shark">
// <name>John</name>
// <teethNumber>200</teethNumber>
// </fish>
assert(XmlDecoder[Fish].decode(encodedClownFish) == Right(clownFish))
assert(XmlDecoder[Fish].decode(encodedWhiteShark) == Right(whiteShark))