An open API service indexing awesome lists of open source software.

https://github.com/encalmo/xmlwriter

Macro-powered fast XML serialization library for Scala 3.
https://github.com/encalmo/xmlwriter

macros scala3 serialization xml

Last synced: 4 months ago
JSON representation

Macro-powered fast XML serialization library for Scala 3.

Awesome Lists containing this project

README

          

![GitHub](https://img.shields.io/badge/github-%23121011.svg?style=for-the-badge&logo=github&logoColor=white) ![Maven Central Version](https://img.shields.io/maven-central/v/org.encalmo/xmlwriter_3?style=for-the-badge) Scaladoc

# xmlwriter

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

## Table of contents

- [Example usage](#example-usage)
- [Outstanding features](#outstanding-features)
- [Scala types supported directly without the need for typeclass derivation](#scala-types-supported-directly-without-the-need-for-typeclass-derivation)
- [Supported Java types](#supported-java-types)
- [Supported annotations](#supported-annotations)
- [Key abstractions](#key-abstractions)
- [How do we tag elements?](#how-do-we-tag-elements?)
- [Root element tag](#root-element-tag)
- [Nested elements](#nested-elements)
- [Dependencies](#dependencies)
- [Usage](#usage)
- [More examples](#more-examples)
- [Project content](#project-content)

## Example usage

```scala
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

John Doe
30
john.doe@example.com


123 Main St
Anytown
12345


456 Back St
Downtown
78901


true

```

The example above produces the following code after macro expansion:
```scala
{
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`](XmlWriter.scala) provides the main user-facing API, a host of methods to serialize data types to XML,
- trait `XmlWriter[T]` defines typeclass interface,
- trait [`XmlOutputBuilder`](XmlOutputBuilder.scala) 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.
```scala
case class Foo(bar: String)
val entity = Foo("HELLO")

// HELLO
val xml1 = XmlWriter.writeIndented(entity)

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

```scala
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


Hammer
10.0


Screwdriver
2.0

```

## Dependencies

- [Scala](https://www.scala-lang.org) >= 3.7.4
- org.encalmo [**macro-utils** 0.11.0](https://central.sonatype.com/artifact/org.encalmo/macro-utils_3)

## Usage

Use with SBT

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

or with SCALA-CLI

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

## More examples

Example with nested case classes and optional fields:

```scala
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

Alice Smith
29
alice.smith@company.com


456 Market Ave
Metropolis
90210


Acme Widgets Inc.

123 Corporate Plaza
Metropolis
90211
USA

```

```scala
// 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

City Library


Programming Scala
Dean Wampler

Functional
JVM



Functional Programming in Scala
Paul Chiusano

FP
Advanced


```

## 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
├── SimpleTypeTreeVisitor.scala
├── Status.java
├── TagName.scala
├── test.sh
├── TestData.test.scala
├── TestModel.test.scala
├── TypeTreeIterator.scala
├── TypeTreeVisitor.scala
├── XmlOutputBuilder.scala
├── XmlWriter.scala
├── XmlWriterMacro.scala
├── XmlWriterMacroVisitor.scala
└── XmlWriterSpec.test.scala
```