Skip to content

Commit

Permalink
Flesh out Reader and Writer APIs (#56)
Browse files Browse the repository at this point in the history
* Flesh out Reader and Writer APIs

We are missing many combinators for those type classes.

* Add Reader and Writer tests
  • Loading branch information
Georgi Krastev authored Sep 21, 2020
1 parent edb296c commit 1bfeffe
Show file tree
Hide file tree
Showing 6 changed files with 336 additions and 154 deletions.
2 changes: 1 addition & 1 deletion build.sbt
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ lazy val `teleproto` =
.settings(Project.inConfig(Test)(sbtprotoc.ProtocPlugin.protobufConfigSettings): _*)
.settings(
name := "teleproto",
version := "1.7.0",
version := "1.8.0",
libraryDependencies ++= Seq(
library.scalaPB % "protobuf",
library.scalaPBJson % Compile,
Expand Down
53 changes: 49 additions & 4 deletions src/main/scala/io/moia/protos/teleproto/Reader.scala
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ import scala.util.Try
* Provides reading of a generated Protocol Buffers model into a business model.
*/
@implicitNotFound("No Protocol Buffers mapper from type ${P} to ${M} was found. Try to implement an implicit Reader for this type.")
trait Reader[-P, +M] { self =>
trait Reader[-P, +M] {

/**
* Returns the read business model or an error message.
Expand All @@ -43,18 +43,63 @@ trait Reader[-P, +M] { self =>
* Transforms successfully read results.
*/
def map[N](f: M => N): Reader[P, N] =
(protobuf: P) => self.read(protobuf).map(f)
read(_).map(f)

/**
* Transforms the protobuf before reading.
*/
final def contramap[Q](f: Q => P): Reader[Q, M] =
protobuf => read(f(protobuf))

/**
* Transforms successfully read results with the option to fail.
*/
def flatMap[N](f: M => PbSuccess[N]): Reader[P, N] =
(protobuf: P) => self.read(protobuf).flatMap(f)
final def pbmap[N](f: M => PbResult[N]): Reader[P, N] =
read(_).flatMap(f)

@deprecated("Use a function that returns a Reader with flatMap or one that returns a PbResult with emap", "1.8.0")
protected def flatMap[N](f: M => PbSuccess[N]): Reader[P, N] =
read(_).flatMap(f)

/**
* Transforms successfully read results by stacking another reader on top of the original protobuf.
*/
final def flatMap[Q <: P, N](f: M => Reader[Q, N])(implicit dummy: DummyImplicit): Reader[Q, N] =
protobuf => read(protobuf).flatMap(f(_).read(protobuf))

/**
* Combines two readers with a specified function.
*/
final def zipWith[Q <: P, N, O](that: Reader[Q, N])(f: (M, N) => O): Reader[Q, O] =
protobuf =>
for {
m <- this.read(protobuf)
n <- that.read(protobuf)
} yield f(m, n)

/**
* Combines two readers into a reader of a tuple.
*/
final def zip[Q <: P, N](that: Reader[Q, N]): Reader[Q, (M, N)] =
zipWith(that)((_, _))

/**
* Chain `that` reader after `this` one.
*/
final def andThen[N](that: Reader[M, N]): Reader[P, N] =
read(_).flatMap(that.read)

/**
* Chain `this` reader after `that` one.
*/
final def compose[Q](that: Reader[Q, P]): Reader[Q, M] =
that.andThen(this)
}

object Reader extends LowPriorityReads {

def apply[P, M](implicit reader: Reader[P, M]): Reader[P, M] = reader

/* Combinators */

def transform[PV, MV](protobuf: PV, path: String)(implicit valueReader: Reader[PV, MV]): PbResult[MV] =
Expand Down
56 changes: 56 additions & 0 deletions src/main/scala/io/moia/protos/teleproto/TestData.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
package io.moia.protos.teleproto

import java.time.Instant

import com.google.protobuf.duration.{Duration => PBDuration}
import com.google.protobuf.timestamp.Timestamp

import scala.concurrent.duration.Duration

object TestData {
final case class Protobuf(
id: Option[String],
price: Option[String],
time: Option[Timestamp],
duration: Option[PBDuration],
pickupId: Option[String],
prices: Seq[String],
discounts: Map[String, String]
)

final case class Model(
id: String,
price: BigDecimal,
time: Instant,
duration: Duration,
pickupId: Option[String],
prices: List[BigDecimal],
discounts: Map[String, BigDecimal]
)

final case class ProtobufLight(
id: Option[String],
price: Option[String],
time: Option[Timestamp],
duration: Option[PBDuration]
) {
def complete(
pickupId: Option[String] = None,
prices: Seq[String] = Nil,
discounts: Map[String, String] = Map.empty
): Protobuf = Protobuf(id, price, time, duration, pickupId, prices, discounts)
}

final case class ModelLight(
id: String,
price: BigDecimal,
time: Instant,
duration: Duration
) {
def complete(
pickupId: Option[String] = None,
prices: List[BigDecimal] = Nil,
discounts: Map[String, BigDecimal] = Map.empty
): Model = Model(id, price, time, duration, pickupId, prices, discounts)
}
}
43 changes: 41 additions & 2 deletions src/main/scala/io/moia/protos/teleproto/Writer.scala
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ import scala.concurrent.duration.{Deadline, Duration}
@implicitNotFound(
"No mapper from business model type ${M} to Protocol Buffers type ${P} was found. Try to implement an implicit Writer for this type."
)
trait Writer[-M, +P] { self =>
trait Writer[-M, +P] {

/**
* Returns the written Protocol Buffer object.
Expand All @@ -42,11 +42,50 @@ trait Writer[-M, +P] { self =>
/**
* Transforms each written result.
*/
def map[Q](f: P => Q): Writer[M, Q] = new Writer.Mapped(this, f)
def map[Q](f: P => Q): Writer[M, Q] =
model => f(write(model))

/**
* Transforms the model before writing.
*/
final def contramap[N](f: N => M): Writer[N, P] =
model => write(f(model))

/**
* Transforms written results by stacking another writer on top of the original model.
*/
final def flatMap[N <: M, Q](f: P => Writer[N, Q]): Writer[N, Q] =
model => f(write(model)).write(model)

/**
* Combines two writers with a specified function.
*/
final def zipWith[N <: M, Q, R](that: Writer[N, Q])(f: (P, Q) => R): Writer[N, R] =
model => f(this.write(model), that.write(model))

/**
* Combines two writers into a writer of a tuple.
*/
final def zip[N <: M, Q](that: Writer[N, Q]): Writer[N, (P, Q)] =
zipWith(that)((_, _))

/**
* Chain `that` writer after `this` one.
*/
final def andThen[Q](that: Writer[P, Q]): Writer[M, Q] =
model => that.write(this.write(model))

/**
* Chain `this` writer after `that` one.
*/
final def compose[N](that: Writer[N, M]): Writer[N, P] =
that.andThen(this)
}

object Writer extends LowPriorityWrites {

def apply[M, P](implicit writer: Writer[M, P]): Writer[M, P] = writer

/* Combinators */

def transform[MV, PV](model: MV)(implicit valueWriter: Writer[MV, PV]): PV =
Expand Down
Loading

0 comments on commit 1bfeffe

Please sign in to comment.