mirror of
https://github.com/fluencelabs/codec
synced 2025-07-01 23:41:36 +00:00
Compare commits
2 Commits
examples
...
protobuf-c
Author | SHA1 | Date | |
---|---|---|---|
ba56af66bd | |||
0caafa60b7 |
@ -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
|
||||||
|
|
||||||
|
13
build.sbt
13
build.sbt
@ -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`)
|
|
||||||
|
@ -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](())
|
||||||
}
|
}
|
||||||
|
@ -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))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -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`
|
|
||||||
}
|
|
||||||
}
|
|
@ -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.
|
||||||
*/
|
*/
|
||||||
|
@ -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")
|
||||||
|
@ -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)
|
||||||
}
|
}
|
||||||
|
Reference in New Issue
Block a user