Build status codecov License Download

PBDirect

Read/Write Scala objects directly to Protobuf with no .proto file definitions

Context

Protobuf is a fast and efficient way to serialize data. While .proto files are great to share schema definitions between components, it is sometimes much simpler and straightforward to directly encode Scala object without using a .proto schema definition file.

PBDirect aims just that: Make it easier to serialize/deserialize into Protobuf.

Setup

In order to use PBDirect you need to add the following lines to your build.sbt:

resolvers += Resolver.bintrayRepo("beyondthelines", "maven")

libraryDependencies += "beyondthelines" %% "pbdirect" % "0.1.0"

Dependencies

PBDirect depends on:

Usage

In order to use PBDirect you need to import the following:

import cats.instances.list._
import cats.instances.option._
import pbdirect._

Note: It's not recommended to use import cats.instances.all._ as it may cause issues with implicit resolution.

Example

Schema definition

PBDirect serialises case classes into protobuf and there is no need for a .proto schema definition file.

case class MyMessage(
  id: Option[Int],
  text: Option[String],
  numbers: List[Int]
)

is equivalent to the following protobuf definition:

message MyMessage {
   optional int32  id      = 1;
   optional string text    = 2;
   repeated int32  numbers = 3;
}

The field numbers correspond to the order of the fields inside the case class.

Serialization

You only need to call the toPB method on your case class. This method is implicitly added with import pbdirect._.

val message = MyMessage(
  id = Some(123),
  text = Some("Hello"),
  numbers = List(1, 2, 3, 4)
)
val bytes = message.toPB

Deserialization

Deserializing bytes into a case class is also straight forward. You only need to call the pbTo[A] method on the byte array containing the protobuf encoded data. This method is added implicitly on all Array[Byte] by importing pbdirect._.

val bytes: Array[Byte] = Array[Byte](8, 123, 18, 5, 72, 101, 108, 108, 111, 24, 1, 32, 2, 40, 3, 48, 4)
val message = bytes.pbTo[MyMessage]

Indexing

The protobuf indexes reflects directly the order of the fields declaration. E.g.

case class MyMessage(
  id: Option[Int],
  text: Option[String],
  numbers: List[Int]
)

is equivalent to the following protobuf definition:

message MyMessage {
   optional int32  id      = 1;
   optional string text    = 2;
   repeated int32  numbers = 3;
}

It's possible to specify the protobuf index by using the @Index annotation.

case class MyMessage(
  @Index(10) id: Option[Int],
  @Index(20) text: Option[String],
  @Index(30) numbers: List[Int]
)

is equivalent to the following protobuf definition:

message MyMessage {
   optional int32  id      = 10;
   optional string text    = 20;
   repeated int32  numbers = 30;
}

This is particularly useful to model an ADT where several members have the same generic type (i.e the same HList)

Extension

You might want to define your own formats for unsupported types. E.g. to add a format to write java.time.Instant you can do:

import java.time.Instant
import cats.syntax.invariant._

implicit val instantFormat: PBFormat[Instant] =
  PBFormat[Long].imap(Instant.ofEpochMilli)(_.toEpochMilli)

If you only need a reader you can map over an existing PBReader

import java.time.Instant
import cats.syntax.functor._

implicit val instantReader: PBReader[Instant] =
  PBReader[Long].map(Instant.ofEpochMilli)

And for a writer you simply contramap over it:

import java.time.Instant
import cats.syntax.contravariant._

implicit val instantWriter: PBWriter[Instant] =
  PBWriter[Long].contramap(_.toEpochMilli)
  )

More information

Finally you can find more implementation details over here