Skip to content

encalmo/xmlwriter

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

59 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

GitHub Maven Central Version Scaladoc

xmlwriter

Macro-powered fast and easy XML serialization library for Scala 3.

Table of contents

Example usage

import org.encalmo.writer.xml.XmlWriter

case class Address(
    street: String,
    city: String,
    postcode: String
)

case class Employee(
    name: String,
    age: Int,
    email: Option[String],
    addresses: List[Address],
    active: Boolean
)

val entity = Employee(
  name = "John Doe",
  age = 30,
  email = Some("john.doe@example.com"),
  addresses = List(
    Address(street = "123 Main St", city = "Anytown", postcode = "12345"),
    Address(street = "456 Back St", city = "Downtown", postcode = "78901")
  ),
  active = true
)

val xml = XmlWriter.writeIndented(entity)
println(xml)

Output:

<?xml version='1.0' encoding='UTF-8'?>
<Employee>
    <name>John Doe</name>
    <age>30</age>
    <email>john.doe@example.com</email>
    <addresses>
        <Address>
            <street>123 Main St</street>
            <city>Anytown</city>
            <postcode>12345</postcode>
        </Address>
        <Address>
            <street>456 Back St</street>
            <city>Downtown</city>
            <postcode>78901</postcode>
        </Address>
    </addresses>
    <active>true</active>
</Employee>

The example above produces the following code after macro expansion:

{
  val builder: org.encalmo.writer.xml.XmlOutputBuilder = ...
  builder.appendElementStart("Employee", immutable.Nil)

  def writeCaseClassToXml_Address(address: Address): scala.Unit = {
    builder.appendElementStart("street")
    builder.appendText(address.street)
    builder.appendElementEnd("street")
    builder.appendElementStart("city")
    builder.appendText(address.city)
    builder.appendElementEnd("city")
    builder.appendElementStart("postcode")
    builder.appendText(address.postcode)
    builder.appendElementEnd("postcode")
  }

  def writeCaseClassToXml_Employee(employee: Employee): scala.Unit = {
    builder.appendElementStart("name")
    builder.appendText(employee.name)
    builder.appendElementEnd("name")
    builder.appendElementStart("age")
    builder.appendText(employee.age.toString())
    builder.appendElementEnd("age")

    employee.email match {
      case string: scala.Some[scala.Predef.String] =>
        builder.appendElementStart("email")
        builder.appendText(string.value)
        builder.appendElementEnd("email")
      case scala.None =>
        ()
    }

    builder.appendElementStart("addresses")
    val addressesIterator: scala.collection.Iterator[Address] = (employee.addresses: scala.collection.Iterable[Address]).iterator
    while (addressesIterator.hasNext) {
      val addressItem: Address = addressesIterator.next()
      builder.appendElementStart("Address", immutable.Nil)
      writeCaseClassToXml_Address(addressItem)
      builder.appendElementEnd("Address")
      ()
    }
    builder.appendElementEnd("addresses")

    builder.appendElementStart("active")
    builder.appendText(employee.active.toString())
    builder.appendElementEnd("active")
  }
  
  writeCaseClassToXml_Employee(entity)
  builder.appendElementEnd("Employee")
}

Outstanding features

  • Generates highly performant low-level code
  • Supports field, value, case, and type annotations enabling fine-tuning of the resulting XML,
  • Supports custom tag and attribute name transformation (e.g., snake_case, kebab-case, upper/lower case, etc),
  • Indented or compact XML output with pluggable output builders (including streaming),
  • Automatic escaping of text (element and attribute content) to produce well-formed XML.
  • Extensible to custom types via typeclass instances,
  • Can automatically derive XmlWriter typeclass if requested,
  • Invokes toString() as a fallback strategy when type is not supported directly or does not have an XmlWriter instance in scope.
  • Decouples data structure traversal (XmlWriter) from output assembly (XmlOutputBuilder)

Scala types supported directly without the need for typeclass derivation

  • Case classes and nested case classes (including recursive, deeply nested types)
  • Enums and sealed trait hierarchies
  • Tuples: e.g. (A, B), (A, B, C) etc.
  • Named tuples: (a: A, b: B)
  • Instances of Selectable with a Fields type: serialization for structural types and objects extending Selectable with a Fields member type
  • Opaque types with an upper bound
  • Iterable[T] collections and Array[T]
  • Option[T]: (properly serializes presence or absence)
  • Either[T]
  • All standard Scala primitive types: Int, Long, Double, Float, Boolean, Char, Short, Byte and String
  • Big number types: BigInt, BigDecimal

Supported Java types

  • Java boxed primitives: java.lang.Integer, java.lang.Long, java.lang.Double, etc.
  • Java records
  • Java enums
  • Java iterables: support for java.util.List, java.util.Set, and other iterables
  • Java maps: support for java.util.Map and subclasses

Supported annotations

  • All annotations are defined in org.encalmo.writer.xml.annotation.
  • Annotations can be placed on types, fields, values and enum cases, on case class fields or sealed trait members.
  • Custom tag and attribute names are only required when you want to override defaults.
Annotation Description
@xmlAttribute Marks the target to be serialized as an XML attribute of the enclosing element rather than as a child.
@xmlContent Marks target as the content (text value) of the XML element instead of a tag or attribute.
@xmlTag Sets a custom XML tag or attribute name for this target (overrides the target name in serialization).
@xmlAdditionalTag Annotation to wrap value in and additional XML element
@xmlTagLabelAndType Annotation to mandate nested tag elements for a field: <field><type> ... </type></field>
@xmlItemTag Annotation to define the name of the XML element wrapping each item in an array or collection. This will override custom names of the items in the collection.
@xmlAdditionalItemTag Annotation to define the name of the XML element additionally wrapping each item in an array or collection. This will NOT override custom names of the items in the collection.
@xmlNoItemTags Prevents wrapping each collection element in an extra XML tag; all items are added directly.
@xmlValue Defines a static value for an element, useful for enum cases
@xmlValueSelector Selects which member/field/property from a nested type is used as the value/text for this element.
@xmlEnumCaseValuePlain Annotation to force writing the enum case value as plain text, without wrapping it in a tag.

Key abstractions

  • object XmlWriter provides the main user-facing API, a host of methods to serialize data types to XML,
  • trait XmlWriter[T] defines typeclass interface,
  • trait XmlOutputBuilder defines low-level API for constructing XML output,
  • object XmlOutputBuilder provides a set of default implementations of XmlOutputBuilder trait producing indented or compact format, building a String or writing directly to the java.io.OutputStream

How do we tag elements?

Root element tag

Root tag can be either provided by the user or derived from the type name.

case class Foo(bar: String)
val entity = Foo("HELLO")

// <Foo><bar>HELLO</bar></Foo>
val xml1 = XmlWriter.writeIndented(entity) 

// <Example><bar>HELLO</bar></Example>
val xml2 = XmlWriter.writeIndentedUsingRootTagName("Example", entity, addXmlDeclaration = false)

Nested elements

Nested elements borrow tag name either from:

  • field name of case classes, selectables or records
  • enum case name or value
  • declared type name (including type aliases and opaque types)
  • keys of the map
  • @xmlTag and @xmlItemTag annotations
case class Tool(name: String, weight: Double)
case class ToolBox(hammer: Tool, screwdriver: Tool)
val entity =
  ToolBox(
    hammer = Tool(name = "Hammer", weight = 10.0),
    screwdriver = Tool(name = "Screwdriver", weight = 2.0)
  )
val xml = XmlWriter.writeIndented(entity)
println(xml)
<?xml version='1.0' encoding='UTF-8'?>
<ToolBox>
    <hammer>
        <name>Hammer</name>
        <weight>10.0</weight>
    </hammer>
    <screwdriver>
        <name>Screwdriver</name>
        <weight>2.0</weight>
    </screwdriver>
</ToolBox>

Dependencies

Usage

Use with SBT

libraryDependencies += "org.encalmo" %% "xmlwriter" % "0.16.0"

or with SCALA-CLI

//> using dep org.encalmo::xmlwriter:0.16.0

More examples

Example with nested case classes and optional fields:

import org.encalmo.writer.xml.XmlWriter

case class Address(
  street: String,
  city: String,
  postcode: String,
  country: Option[String] = None
)

case class Company(
  name: String,
  address: Address
)

case class Employee(
  name: String,
  age: Int,
  email: Option[String],
  address: Option[Address],
  company: Option[Company]
)

val employee = Employee(
  name = "Alice Smith",
  age = 29,
  email = Some("alice.smith@company.com"),
  address = Some(
    Address(
      street = "456 Market Ave",
      city = "Metropolis",
      postcode = "90210",
      country = None
    )
  ),
  company = Some(
    Company(
      name = "Acme Widgets Inc.",
      address = Address(
        street = "123 Corporate Plaza",
        city = "Metropolis",
        postcode = "90211",
        country = Some("USA")
      )
    )
  )
)

// Serialize as indented XML (with XML declaration)
val xml: String = XmlWriter.writeIndented(employee)
println(xml)

Output:

<?xml version='1.0' encoding='UTF-8'?>
<Employee>
    <name>Alice Smith</name>
    <age>29</age>
    <email>alice.smith@company.com</email>
    <address>
        <street>456 Market Ave</street>
        <city>Metropolis</city>
        <postcode>90210</postcode>
    </address>
    <company>
        <name>Acme Widgets Inc.</name>
        <address>
            <street>123 Corporate Plaza</street>
            <city>Metropolis</city>
            <postcode>90211</postcode>
            <country>USA</country>
        </address>
    </company>
</Employee>
// Example: Serialize a case class with collections and XML annotations

import org.encalmo.writer.xml.XmlWriter
import org.encalmo.writer.xml.annotation.{xmlAttribute, xmlItemTag, xmlTag}

case class Tag(
  @xmlAttribute name: String,
  value: String
)

@xmlTag("Bookshelf")
case class Library(
  @xmlAttribute libraryId: String,
  name: String,
  @xmlItemTag("Book") books: List[Book]
)

case class Book(
  @xmlAttribute isbn: String,
  title: String,
  author: String,
  tags: List[Tag]
)

val library = Library(
  libraryId = "lib123",
  name = "City Library",
  books = List(
    Book(
      isbn = "978-3-16-148410-0",
      title = "Programming Scala",
      author = "Dean Wampler",
      tags = List(
        Tag(name = "Scala", value = "Functional"),
        Tag(name = "Programming", value = "JVM")
      )
    ),
    Book(
      isbn = "978-1-61729-065-7",
      title = "Functional Programming in Scala",
      author = "Paul Chiusano",
      tags = List(
        Tag(name = "Scala", value = "FP"),
        Tag(name = "Education", value = "Advanced")
      )
    )
  )
)

val xml: String = XmlWriter.writeIndented(library)
println(xml)

Output:

<?xml version='1.0' encoding='UTF-8'?>
<Bookshelf libraryId="lib123">
    <name>City Library</name>
    <books>
        <Book isbn="978-3-16-148410-0">
            <title>Programming Scala</title>
            <author>Dean Wampler</author>
            <tags>
                <Tag name="Scala">Functional</Tag>
                <Tag name="Programming">JVM</Tag>
            </tags>
        </Book>
        <Book isbn="978-1-61729-065-7">
            <title>Functional Programming in Scala</title>
            <author>Paul Chiusano</author>
            <tags>
                <Tag name="Scala">FP</Tag>
                <Tag name="Education">Advanced</Tag>
            </tags>
        </Book>
    </books>
</Bookshelf>

Project content

├── .github
│   └── workflows
│       ├── pages.yaml
│       ├── release.yaml
│       └── test.yaml
│
├── .gitignore
├── .scalafmt.conf
├── annotation.scala
├── ExampleModel.test.scala
├── ExampleModelSpec.test.scala
├── LICENSE
├── Order.java
├── project.scala
├── README.md
├── Status.java
├── test.sh
├── TestData.test.scala
├── TestModel.test.scala
├── XmlOutputBuilder.scala
├── XmlWriter.scala
├── XmlWriterMacro.scala
├── XmlWriterMacroVisitor.scala
└── XmlWriterSpec.test.scala

About

Macro-powered fast XML serialization library for Scala 3.

Topics

Resources

License

Stars

Watchers

Forks

Contributors

Languages