2 Commits

Author SHA1 Message Date
ba56af66bd revert 2018-07-04 17:28:05 +03:00
0caafa60b7 add protobuf codecs, change version 2018-07-04 17:25:47 +03:00
8 changed files with 23 additions and 222 deletions

View File

@ -44,7 +44,7 @@ In general, functional types conversion could be lazy or eager, be performed in
```scala ```scala
import cats.Id import cats.Id
val resEagerSync: Either[CodecError, Array[Byte]] = intToBytes.direct.runF[Id](33) val resEagerSync: Either[CodecError, Array[Byte]] = intToBytes.runF[Id](33)
``` ```
@ -121,8 +121,7 @@ libraryDependencies ++= Seq(
// eyJpZCI6MjM0LCJuYW1lIjoiSGV5IEJvYiJ9 // eyJpZCI6MjM0LCJuYW1lIjoiSGV5IEJvYiJ9
``` ```
For synthetic examples refer to the [examples directory](examples/). For more real-world examples, see [Fluence](https://github.com/fluencelabs/fluence).
For the real-world examples checkout [Fluence](https://github.com/fluencelabs/fluence) main repo.
## Roadmap ## Roadmap

View File

@ -14,7 +14,7 @@ val scalaV = scalaVersion := "2.12.5"
val commons = Seq( val commons = Seq(
scalaV, scalaV,
version := "0.0.3", version := "0.0.4",
fork in Test := true, fork in Test := true,
parallelExecution in Test := false, parallelExecution in Test := false,
organization := "one.fluence", organization := "one.fluence",
@ -148,14 +148,3 @@ lazy val `codec-protobuf` = crossProject(JVMPlatform, JSPlatform)
lazy val `codec-protobuf-jvm` = `codec-protobuf`.jvm lazy val `codec-protobuf-jvm` = `codec-protobuf`.jvm
lazy val `codec-protobuf-js` = `codec-protobuf`.js lazy val `codec-protobuf-js` = `codec-protobuf`.js
lazy val `codec-examples` = project
.in(file("examples"))
.settings(
commons,
libraryDependencies ++= Seq(
"io.monix" %%% "monix" % "3.0.0-RC1"
)
)
.dependsOn(`codec-core-jvm`)
.dependsOn(`codec-kryo`)

View File

@ -208,7 +208,7 @@ abstract class MonadicalEitherArrow[E <: Throwable] {
* @param f Function to lift * @param f Function to lift
*/ */
def liftFuncPoint[A, B](f: A Point[B]): Func[A,B] = def liftFuncPoint[A, B](f: A Point[B]): Func[A,B] =
new Func[A, B]{ new Func[A,B]{
override def apply[F[_] : Monad](input: A): EitherT[F, E, B] = override def apply[F[_] : Monad](input: A): EitherT[F, E, B] =
f(input).apply[F](()) f(input).apply[F](())
} }

View File

@ -1,65 +0,0 @@
package fluence.codec.examples
import cats.Id
import fluence.codec.kryo.KryoCodecs
import fluence.codec.{CodecError, PureCodec}
import monix.eval.Task
import shapeless.{::, HNil}
object KryoCodecExample {
case class Aircraft(manufacturer: String, model: String, tailNumber: String)
case class Fuel(amount: Double) extends AnyVal
case class UnknownClass(x: String)
def main(args: Array[String]): Unit = {
// This way we can define a typed collection of codecs using kryo for the underlying serialization.
//
// These codecs can be used only to transform the corresponding type: i.e. it won't be possible to
// use an aircraft codec to serialize fuel (which is essentially a typed wrapper over double value).
//
// It won't be possible to obtain from this collection a codec for previously not registered class.
// Type safety FTW!
//
// Note that different methods are used to register Aircraft and Fuel that's because one is a reference,
// and another is a value type.
val codecs: KryoCodecs[Task, ::[Fuel, ::[Aircraft, ::[Array[Byte], ::[Long, ::[String, HNil]]]]]] = KryoCodecs()
.addCase(classOf[Aircraft])
.add[Fuel]
.build[Task]()
val skyhawk61942 = Aircraft("Cessna", "172S G1000", "N61942")
val tabsFuel = Fuel(53)
val aircraftCodec: PureCodec[Aircraft, Array[Byte]] = codecs.pureCodec[Aircraft]
val fuelCodec: PureCodec[Fuel, Array[Byte]] = codecs.pureCodec[Fuel]
// This will cause a compilation error, because the class was never registered with the codecs.
// "You requested an element of type (...).UnknownClass, but there is none in the HList"
//
// val unknownCodec = codecs.pureCodec[UnknownClass]
// Here all the standard machinery of codecs applies (for more examples, consider checking out PureCodecExample.
// We can serialize and deserialize the object and unsurprisingly the original and restored values match.
//
// Let's serialize an aircraft instance.
{
val ser: Id[Either[CodecError, Array[Byte]]] = aircraftCodec.direct[Id](skyhawk61942).value
val deser: Id[Either[CodecError, Aircraft]] = aircraftCodec.inverse[Id](ser.right.get).value
println(ser.right.map(x => s"$skyhawk61942 => serialized size: ${x.length}"))
assert(deser == Right(skyhawk61942))
}
// Same thing for the fuel instance (which is AnyVal fwiw).
{
val ser: Id[Either[CodecError, Array[Byte]]] = fuelCodec.direct[Id](tabsFuel).value
val deser: Id[Either[CodecError, Fuel]] = fuelCodec.inverse[Id](ser.right.get).value
println(ser.right.map(x => s"$tabsFuel => serialized size: ${x.length}"))
assert(deser == Right(tabsFuel))
}
}
}

View File

@ -1,138 +0,0 @@
package fluence.codec.examples
import cats.Id
import cats.data.EitherT
import cats.implicits._
import fluence.codec.PureCodec.{Bijection, Point}
import fluence.codec.{CodecError, PureCodec}
import scala.util.Try
object PureCodecExample {
def main(args: Array[String]): Unit = {
// Here we are defining a simple codec transforming a string to integer and back.
//
// It's not really a bijection: even not taking into account unparseable strings like "test", there are
// different string values (e.g., "+20" and "20") producing the same integer value. It's good enough for
// demonstration purposes though, so we keep using it.
val str2intCodec: Bijection[String, Int] = PureCodec.build[String, Int](
(x: String) => x.toInt,
(x: Int) => x.toString
)
// Using an identity monad, we can parse a valid string into integer (which produces EitherT) and then map
// the result. Now, we can use EitherT[F, E, B] or convert it into F[Either[E, B]] representation.
{
val res: EitherT[Id, CodecError, Int] = str2intCodec.direct[Id]("31330").map(_ + 7)
val resMonad: Id[Either[CodecError, Int]] = res.value
assert(res.toString == "EitherT(Right(31337))")
assert(resMonad.toString == "Right(31337)")
}
// We can also supply a different type class (Monad[F[_]]) in this case the result will be wrapped into
// the corresponding type F[_] using the `F.pure(_)` method.
{
val res = str2intCodec.direct[Option]("42")
val resMonad = res.value
assert(res.toString == "EitherT(Some(Right(42)))")
assert(resMonad.toString == "Some(Right(42))")
}
// Here we attempt to pass an unparseable string value. Note that PureCodec won't catch a thrown exception
// automatically despite that return type is EitherT (this might be a bit confusing). Instead, the exception
// will come all the way up to the caller, which will have to handle it manually.
{
val resWrapped = Try {
val res: EitherT[Id, CodecError, Int] = str2intCodec.direct[Id]("foo")
res
}
assert(resWrapped.toString == "Failure(java.lang.NumberFormatException: For input string: \"foo\")")
}
// To handle exceptions automatically, we can use Try monad. Note that we get `EitherT(Failure(...))`, not
// `EitherT(Failure(Right(...)))` as one might expect by analogy with previous examples. It's not
// `EitherT(Left(...))` too which could have been more convenient potentially.
{
val res = str2intCodec.direct[Try]("foo")
val resMonad: Try[Either[CodecError, Int]] = res.value
assert(res.toString == "EitherT(Failure(java.lang.NumberFormatException: For input string: \"foo\"))")
assert(resMonad.toString == "Failure(java.lang.NumberFormatException: For input string: \"foo\")")
}
// If we really want to receive Left with the exception info when the string argument can't be parsed, a little
// more effort is needed. The problem we had before was that the supplied function `(x: String) => x.toInt`
// could throw parse exceptions and therefore was not really pure.
//
// However, we can catch exceptions in this function and return an Either, which will make it pure. Now, all we
// need to do is to lift this function into the Func context.
val str2intEitherCodec: Bijection[String, Int] = PureCodec.build(
PureCodec.liftFuncEither((x: String) => Either.catchNonFatal(x.toInt).left.map(e => CodecError(e.getMessage))),
PureCodec.liftFuncEither((x: Int) => Either.catchNonFatal(x.toString).left.map(e => CodecError(e.getMessage)))
)
// For lawful strings those which can be parsed into an integer the behavior hasn't really changed.
// Note that we receive Right(...) wrapped in the supplied monadic type.
{
val res: EitherT[Option, CodecError, Int] = str2intEitherCodec.direct[Option]("1024")
val resMonad = res.value
assert(res.toString == "EitherT(Some(Right(1024)))")
assert(resMonad.toString == "Some(Right(1024))")
}
// However, for strings that can't be parsed, we will receive Left(...) which is a desired behavior!
{
val res: EitherT[Option, CodecError, Int] = str2intEitherCodec.direct[Option]("bar")
val resMonad = res.value
assert(res.toString == "EitherT(Some(Left(fluence.codec.CodecError: For input string: \"bar\")))")
assert(resMonad.toString == "Some(Left(fluence.codec.CodecError: For input string: \"bar\"))")
}
// It's also totally possible to perform an inverse transformation: after all, a codec is a bijection.
{
val res: EitherT[Id, CodecError, String] = str2intCodec.inverse[Id](720)
val resMonad: Id[Either[CodecError, String]] = res.value
assert(res.toString == "EitherT(Right(720))")
assert(resMonad.toString == "Right(720)")
}
// It's also possible to pass the to-be-converted value first, but perform the actual transformation only
// later on (using different enclosing monads if desired). To achieve this, `pointAt` method which returns a
// lazily evaluated function can be used.
{
val point: Point[Int] = str2intCodec.direct.pointAt("333")
val resId: EitherT[Id, CodecError, Int] = point[Id]()
val resOption: EitherT[Option, CodecError, Int] = point[Option]()
assert(resId.toString == "EitherT(Right(333))")
assert(resOption.toString == "EitherT(Some(Right(333)))")
}
// Sometimes, we might want to be able to compose two codecs together. Here we define an integer to boolean
// codec and compose it with one of the previously defined codecs. Yes, the int-to-bool codec is not really
// a bijection but we can put up with that for the sake of example.
val int2boolCodec: Bijection[Int, Boolean] = PureCodec.build[Int, Boolean](
(x: Int) => x != 0,
(x: Boolean) => if (x) 1 else 0
)
val str2boolCodec: Bijection[String, Boolean] = str2intCodec andThen int2boolCodec
{
val resA: EitherT[Id, CodecError, Boolean] = str2boolCodec.direct[Id]("100")
val resB = str2boolCodec.inverse[Option](true)
assert(resA.toString == "EitherT(Right(true))")
assert(resB.toString == "EitherT(Some(Right(1)))")
}
// TODO: describe `runF` and `toKleisli`
}
}

View File

@ -21,8 +21,8 @@ import com.twitter.chill.{AllScalaRegistrar, KryoBase, KryoInstantiator}
import org.objenesis.strategy.StdInstantiatorStrategy import org.objenesis.strategy.StdInstantiatorStrategy
/** /**
* This Instantiator enables compulsory class registration and registers all java and scala main classes. * This Instantiator enable compulsory class registration, registers all java and scala main classes.
* This class is required for [[com.twitter.chill.KryoPool]]. * This class required for [[com.twitter.chill.KryoPool]].
* @param classesToReg additional classes for registration * @param classesToReg additional classes for registration
* @param registrationRequired if true, an exception is thrown when an unregistered class is encountered. * @param registrationRequired if true, an exception is thrown when an unregistered class is encountered.
*/ */

View File

@ -18,4 +18,4 @@ addSbtPlugin("com.lihaoyi" % "workbench" % "0.4.0")
libraryDependencies += "com.thesamet.scalapb" %% "compilerplugin" % "0.7.1" libraryDependencies += "com.thesamet.scalapb" %% "compilerplugin" % "0.7.1"
addSbtPlugin("org.foundweekends" % "sbt-bintray" % "0.5.4") addSbtPlugin("org.foundweekends" % "sbt-bintray" % "0.5.4")

View File

@ -19,6 +19,7 @@ package fluence.codec.pb
import com.google.protobuf.ByteString import com.google.protobuf.ByteString
import fluence.codec.PureCodec import fluence.codec.PureCodec
import scalapb.{GeneratedMessage, GeneratedMessageCompanion, Message}
import scala.language.higherKinds import scala.language.higherKinds
@ -30,4 +31,19 @@ object ProtobufCodecs {
arr ByteString.copyFrom(arr) arr ByteString.copyFrom(arr)
) )
/**
* Codec for converting byte array to protobuf class.
*
* @param gen Protobuf class's companion.
* @return New codec for converting byte array to a specific protobuf class.
*/
def protobufDynamicCodec[A <: GeneratedMessage with Message[A]](
gen: GeneratedMessageCompanion[A]
): PureCodec.Func[Array[Byte], A] = PureCodec.liftFunc[Array[Byte], A](gen.parseFrom)
/**
* Codec for converting protobuf class to a byte array.
*/
val generatedMessageCodec: PureCodec.Func[GeneratedMessage, Array[Byte]] =
PureCodec.liftFunc[GeneratedMessage, Array[Byte]](_.toByteArray)
} }