Skip to content
Merged
45 changes: 31 additions & 14 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,22 +30,18 @@ libraryDependencies += "com.47deg" %% "pbdirect" % "0.3.1"
## Dependencies

PBDirect depends on:
- [protobuf-java](https://developers.google.com/protocol-buffers/docs/javatutorial) the Protobuf java library (maintained by Google)
- [protobuf-java](https://developers.google.com/protocol-buffers/docs/javatutorial) the Protobuf java library (maintained by Google)
- [shapeless](https://github.com/milessabin/shapeless) for the generation of type-class instances
- [cats](https://github.com/typelevel/cats) to deal with optional and repeated fields

## Usage

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

```scala
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
Expand All @@ -54,8 +50,29 @@ PBDirect serialises case classes into protobuf and there is no need for a .proto

```scala
case class MyMessage(
id: Option[Int],
text: Option[String],
@pbIndex(1) id: Option[Int],
@pbIndex(3) text: Option[String],
@pbIndex(5) numbers: List[Int]
)
```

is equivalent to the following protobuf definition:

```protobuf
message MyMessage {
optional int32 id = 1;
optional string text = 3;
repeated int32 numbers = 5;
}
```

Note that the `@pbIndex` annotation is optional. If it is not present, the field's position in the case class is used
as its index. For example, an unannotated case class like:

```scala
case class MyMessage(
id: Option[Int],
text: Option[String],
numbers: List[Int]
)
```
Expand All @@ -70,8 +87,6 @@ message MyMessage {
}
```

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._`.
Expand All @@ -83,6 +98,7 @@ val message = MyMessage(
numbers = List(1, 2, 3, 4)
)
val bytes = message.toPB
// bytes: Array(8, 123, 26, 5, 72, 101, 108, 108, 111, 40, 1, 40, 2, 40, 3, 40, 4)
```

### Deserialization
Expand All @@ -91,8 +107,9 @@ Deserializing bytes into a case class is also straight forward. You only need to
This method is added implicitly on all `Array[Byte]` by importing `pbdirect._`.

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

## Extension
Expand Down Expand Up @@ -124,8 +141,8 @@ 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)
implicit val instantWriter: PBFieldWriter[Instant] =
PBFieldWriter[Long].contramap(_.toEpochMilli)
)
```

Expand All @@ -136,4 +153,4 @@ pbdirect is designed and developed by 47 Degrees

Copyright (C) 2019 47 Degrees. <http://47deg.com>

[comment]: # (End Copyright)
[comment]: # (End Copyright)
28 changes: 16 additions & 12 deletions project/ProjectPlugin.scala
Original file line number Diff line number Diff line change
Expand Up @@ -18,13 +18,15 @@ object ProjectPlugin extends AutoPlugin {
object autoImport {

lazy val V = new {
val cats: String = "2.0.0"
val protobuf: String = "3.11.1"
val scala211: String = "2.11.12"
val scala212: String = "2.12.10"
val scalaTest: String = "3.1.0"
val shapeless: String = "2.3.3"
val enumeratum: String = "1.5.14"
val cats: String = "2.0.0"
val protobuf: String = "3.11.1"
val scala211: String = "2.11.12"
val scala212: String = "2.12.10"
val shapeless: String = "2.3.3"
val enumeratum: String = "1.5.14"
val scalaTest: String = "3.1.0"
val scalatestScalacheck: String = "3.1.0.0"
val scalacheckShapeless: String = "1.2.3"
}
}

Expand Down Expand Up @@ -58,11 +60,13 @@ object ProjectPlugin extends AutoPlugin {
scalaVersion := V.scala212,
crossScalaVersions := Seq(scalaVersion.value, V.scala211),
libraryDependencies ++= Seq(
"com.chuusai" %% "shapeless" % V.shapeless,
"org.typelevel" %% "cats-core" % V.cats,
"com.google.protobuf" % "protobuf-java" % V.protobuf,
"com.beachape" %% "enumeratum" % V.enumeratum,
"org.scalatest" %% "scalatest" % V.scalaTest % Test
"com.chuusai" %% "shapeless" % V.shapeless,
"org.typelevel" %% "cats-core" % V.cats,
"com.google.protobuf" % "protobuf-java" % V.protobuf,
"com.beachape" %% "enumeratum" % V.enumeratum,
"org.scalatest" %% "scalatest" % V.scalaTest % Test,
"org.scalatestplus" %% "scalacheck-1-14" % V.scalatestScalacheck % Test,
"com.github.alexarchambault" %% "scalacheck-shapeless_1.14" % V.scalacheckShapeless % Test
),
orgScriptTaskListSetting := List(
(clean in Global).asRunnableItemFull,
Expand Down
29 changes: 29 additions & 0 deletions src/main/scala/pbdirect/FieldIndex.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
/*
* Copyright (c) 2019 Beyond the lines
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@juanpedromoreno What do you think we should do about the copyright headers in this project? I am not a lawyer but it seems strange to add this copyright to newly added files.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, that sounds reasonable to me.

We can tweak the sbt-headers accordingly to make the copyright the more accurate possible.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've been looking into the MIT licence a little.

  • Apparently there is no need for a copyright header in every file. The Apache Foundation recommend it if you are using Apache 2.0, but there is no need for MIT.
  • There is no need to list changes. The example @fedefernandez linked above is from a project using Apache 2.0, which has a requirement to list changes.
  • The only requirement is a LICENSE file containing a copyright notice and the MIT licence text.

So the simplest solution could be:

  • remove the source file headers
  • add a note to the LICENSE file saying 47 Degrees forked the project in 2019 and we claim copyright for any code added or changed since the fork.

What do you think? @juanpedromoreno @fedefernandez

References:

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sounds good to me

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sounds good to me too 👍

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cool, I'll do that in a separate PR after I've merged this.

*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
* the Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/

package pbdirect

/**
* A protobuf index, also known as the field number.
* It holds a list of indices in order to support 'oneof' fields,
* which are encoded as shapeless Coproducts and have a different index for each branch.
*/
private[pbdirect] final case class FieldIndex(values: List[Int])
Original file line number Diff line number Diff line change
Expand Up @@ -25,140 +25,133 @@ import java.io.ByteArrayOutputStream

import cats.{Contravariant, Functor}
import com.google.protobuf.CodedOutputStream
import shapeless.{:+:, ::, CNil, Coproduct, Generic, HList, HNil, Inl, Inr, Lazy}
import enumeratum.values.IntEnumEntry

trait PBWriter[A] {
trait PBFieldWriter[A] {
def writeTo(index: Int, value: A, out: CodedOutputStream): Unit
}

trait LowPriorityPBWriterImplicits {
def instance[A](f: (Int, A, CodedOutputStream) => Unit): PBWriter[A] =
new PBWriter[A] {
trait LowPriorityPBFieldWriterImplicits {
def instance[A](f: (Int, A, CodedOutputStream) => Unit): PBFieldWriter[A] =
new PBFieldWriter[A] {
override def writeTo(index: Int, value: A, out: CodedOutputStream): Unit =
f(index, value, out)
}
implicit val hnilWriter: PBWriter[HNil] = instance { (_: Int, _: HNil, _: CodedOutputStream) =>
()
}
implicit def consWriter[H, T <: HList](
implicit head: PBWriter[H],
tail: Lazy[PBWriter[T]]): PBWriter[H :: T] =
instance { (index: Int, value: H :: T, out: CodedOutputStream) =>
head.writeTo(index, value.head, out)
tail.value.writeTo(index + 1, value.tail, out)
}
implicit def prodWriter[A, R <: HList](
implicit gen: Generic.Aux[A, R],
writer: Lazy[PBWriter[R]]): PBWriter[A] =
instance { (index: Int, value: A, out: CodedOutputStream) =>
val buffer = new ByteArrayOutputStream()
val pbOut = CodedOutputStream.newInstance(buffer)
writer.value.writeTo(1, gen.to(value), pbOut)
pbOut.flush()
out.writeByteArray(index, buffer.toByteArray)
}

implicit val cnilWriter: PBWriter[CNil] = instance { (_: Int, _: CNil, _: CodedOutputStream) =>
throw new Exception("Can't write CNil")
}
implicit def cconsWriter[H, T <: Coproduct](
implicit head: PBWriter[H],
tail: PBWriter[T]): PBWriter[H :+: T] =
instance { (index: Int, value: H :+: T, out: CodedOutputStream) =>
value match {
case Inl(h) => head.writeTo(index, h, out)
case Inr(t) => tail.writeTo(index, t, out)
implicit def embeddedMessageFieldWriter[A](
implicit messageWriter: PBMessageWriter[A]): PBFieldWriter[A] =
instance { (index, message, out) =>
{
val buffer = new ByteArrayOutputStream()
Comment thread
tzimisce012 marked this conversation as resolved.
val bufferOut = CodedOutputStream.newInstance(buffer)
messageWriter.writeTo(message, bufferOut)
bufferOut.flush()
out.writeByteArray(index, buffer.toByteArray)
}
}
implicit def coprodWriter[A, R <: Coproduct](
implicit gen: Generic.Aux[A, R],
writer: PBWriter[R]): PBWriter[A] =
instance { (index: Int, value: A, out: CodedOutputStream) =>
writer.writeTo(index, gen.to(value), out)
implicit def functorWriter[F[_], A](
implicit functor: Functor[F],
writer: PBFieldWriter[A]): PBFieldWriter[F[A]] =
instance { (index: Int, value: F[A], out: CodedOutputStream) =>
functor.map(value) { v =>
writer.writeTo(index, v, out)
}
()
}
}

trait PBWriterImplicits extends LowPriorityPBWriterImplicits {
implicit object BooleanWriter extends PBWriter[Boolean] {
trait PBFieldWriterImplicits extends LowPriorityPBFieldWriterImplicits {
implicit object BooleanWriter extends PBFieldWriter[Boolean] {
override def writeTo(index: Int, value: Boolean, out: CodedOutputStream): Unit =
out.writeBool(index, value)
}
implicit object ByteWriter extends PBWriter[Byte] {
implicit object ByteWriter extends PBFieldWriter[Byte] {
override def writeTo(index: Int, value: Byte, out: CodedOutputStream): Unit =
out.writeInt32(index, value)
out.writeInt32(index, value.toInt)
}
implicit object ShortWriter extends PBWriter[Short] {
implicit object ShortWriter extends PBFieldWriter[Short] {
override def writeTo(index: Int, value: Short, out: CodedOutputStream): Unit =
out.writeInt32(index, value)
out.writeInt32(index, value.toInt)
}
implicit object IntWriter extends PBWriter[Int] {
implicit object IntWriter extends PBFieldWriter[Int] {
override def writeTo(index: Int, value: Int, out: CodedOutputStream): Unit =
out.writeInt32(index, value)
}
implicit object LongWriter extends PBWriter[Long] {
implicit object LongWriter extends PBFieldWriter[Long] {
override def writeTo(index: Int, value: Long, out: CodedOutputStream): Unit =
out.writeInt64(index, value)
}
implicit object FloatWriter extends PBWriter[Float] {
implicit object FloatWriter extends PBFieldWriter[Float] {
override def writeTo(index: Int, value: Float, out: CodedOutputStream): Unit =
out.writeFloat(index, value)
}
implicit object DoubleWriter extends PBWriter[Double] {
implicit object DoubleWriter extends PBFieldWriter[Double] {
override def writeTo(index: Int, value: Double, out: CodedOutputStream): Unit =
out.writeDouble(index, value)
}
implicit object StringWriter extends PBWriter[String] {
implicit object StringWriter extends PBFieldWriter[String] {
override def writeTo(index: Int, value: String, out: CodedOutputStream): Unit =
out.writeString(index, value)
}
implicit object BytesWriter extends PBWriter[Array[Byte]] {
implicit object BytesWriter extends PBFieldWriter[Array[Byte]] {
override def writeTo(index: Int, value: Array[Byte], out: CodedOutputStream): Unit =
out.writeByteArray(index, value)
}
implicit def functorWriter[F[_], A](
implicit functor: Functor[F],
writer: PBWriter[A]): PBWriter[F[A]] =
instance { (index: Int, value: F[A], out: CodedOutputStream) =>
functor.map(value) { v =>
writer.writeTo(index, v, out)
}
()
implicit def optionWriter[A](implicit writer: PBFieldWriter[A]): PBFieldWriter[Option[A]] =
instance { (index: Int, option: Option[A], out: CodedOutputStream) =>
option.foreach(v => writer.writeTo(index, v, out))
}
implicit def listWriter[A](implicit writer: PBFieldWriter[A]): PBFieldWriter[List[A]] =
instance { (index: Int, list: List[A], out: CodedOutputStream) =>
list.foreach(v => writer.writeTo(index, v, out))
}
implicit def keyValuePairWriter[K, V](
implicit keyWriter: PBFieldWriter[K],
valueWriter: PBFieldWriter[V]): PBFieldWriter[(K, V)] =
instance { (index: Int, pair: (K, V), out: CodedOutputStream) =>
val buffer = new ByteArrayOutputStream()
val bufferOut = CodedOutputStream.newInstance(buffer)
keyWriter.writeTo(1, pair._1, bufferOut)
valueWriter.writeTo(2, pair._2, bufferOut)
bufferOut.flush()
out.writeByteArray(index, buffer.toByteArray)
}
implicit def mapWriter[K, V](implicit writer: PBWriter[List[(K, V)]]): PBWriter[Map[K, V]] =
implicit def mapWriter[K, V](
implicit writer: PBFieldWriter[List[(K, V)]]): PBFieldWriter[Map[K, V]] =
instance { (index: Int, value: Map[K, V], out: CodedOutputStream) =>
writer.writeTo(index, value.toList, out)
}
implicit def collectionMapWriter[K, V](
implicit writer: PBWriter[List[(K, V)]]): PBWriter[collection.Map[K, V]] =
implicit writer: PBFieldWriter[List[(K, V)]]): PBFieldWriter[collection.Map[K, V]] =
instance { (index: Int, value: collection.Map[K, V], out: CodedOutputStream) =>
writer.writeTo(index, value.toList, out)
}
implicit def seqWriter[A](implicit writer: PBWriter[List[A]]): PBWriter[Seq[A]] =
implicit def seqWriter[A](implicit writer: PBFieldWriter[List[A]]): PBFieldWriter[Seq[A]] =
instance { (index: Int, value: Seq[A], out: CodedOutputStream) =>
writer.writeTo(index, value.toList, out)
}
implicit def enumWriter[E](implicit values: Enum.Values[E], ordering: Ordering[E]): PBWriter[E] =
implicit def enumWriter[E](
implicit values: Enum.Values[E],
ordering: Ordering[E]): PBFieldWriter[E] =
instance { (index: Int, value: E, out: CodedOutputStream) =>
out.writeInt32(index, Enum.toInt(value))
}
implicit def enumerationWriter[E <: Enumeration#Value]: PBWriter[E] =
implicit def enumerationWriter[E <: Enumeration#Value]: PBFieldWriter[E] =
instance { (index: Int, value: E, out: CodedOutputStream) =>
out.writeInt32(index, value.id)
}
implicit def enumeratumIntEnumEntryWriter[E <: IntEnumEntry]: PBWriter[E] =
implicit def enumeratumIntEnumEntryWriter[E <: IntEnumEntry]: PBFieldWriter[E] =
instance { (index: Int, entry: E, out: CodedOutputStream) =>
out.writeInt32(index, entry.value)
}

implicit object ContravariantWriter extends Contravariant[PBWriter] {
override def contramap[A, B](writer: PBWriter[A])(f: B => A) =
implicit object ContravariantWriter extends Contravariant[PBFieldWriter] {
override def contramap[A, B](writer: PBFieldWriter[A])(f: B => A) =
instance { (index: Int, b: B, out: CodedOutputStream) =>
writer.writeTo(index, f(b), out)
}
}
}

object PBWriter extends PBWriterImplicits {
def apply[A: PBWriter]: PBWriter[A] = implicitly
object PBFieldWriter extends PBFieldWriterImplicits {
def apply[A: PBFieldWriter]: PBFieldWriter[A] = implicitly
}
Loading