Compare commits

..

No commits in common. "master" and "0.0.1" have entirely different histories.

15 changed files with 188 additions and 408 deletions

View File

@ -2,7 +2,7 @@ sudo: required
language: scala
scala:
- 2.12.8
- 2.12.5
jdk:
- oraclejdk8

137
README.md
View File

@ -1,138 +1,3 @@
[![Build Status](https://travis-ci.org/fluencelabs/codec.svg?branch=master)](https://travis-ci.org/fluencelabs/codec)
# Codec
`Codec` is an opinionated library for `cats`-friendly pure (reversible) conversions between types, with possible errors represented with `Either`.
It's cross-built for Scala and Scala.js, and can also be used for other free-of-effect conversions to reach functional composability.
## Motivation
### Use partial functions, track the errors
Often it's useful to do some validation alongside conversion, like in `String => Int`.
However, this function throws when malformed input is given. Hence `String => Either[Throwable, Int]`, being a total function, should fit better.
In this case, error is reflected in type system. It keeps things pure. We go further, forcing the error to be of type `CodecError`, so that later it's easy to track where it comes from, especially in asynchronous environment.
This uni-direction type is called `fluence.codec.PureCodec.Func` for a fixed `CodecError` error type. Any other error type could be used by extending `fluence.codec.MonadicalEitherArrow[Error]`.
Bidirection type `A <=> B` is composed from two `Func`s and is called `Bijection`.
### Lawful composition
A type `Func[A, B]`, being something like `A => Either[E, B]`, is not very composable on it's own, so we implemented `cats.arrow.ArrowChoice[Func]` for it. You may use `cats.syntax.compose._` or anything like that to receive `andThen` and other lawful functions.
`Bijection[A, B]` is more complex type, so it has only `Compose[Bijection]` typeclass. Finally you can do something like this:
```scala
import cats.syntax.compose._
import fluence.codec.PureCodec
val intToBytes: PureCodec[Int, Array[Byte]] = PureCodec[Int, String] andThen PureCodec[String, Array[Byte]]
```
Errors are handled in monad-like "fail-fast" fashion.
### Benefit from different Monads
In general, functional types conversion could be lazy or eager, be performed in current thread or another. This choice should not affect the logic of conversion, as it's pure.
`PureCodec` may use any monad to preform execution upon, retaining its nature. The most simple case is strict eager evaluation:
```scala
import cats.Id
val resEagerSync: Either[CodecError, Array[Byte]] = intToBytes.runF[Id](33)
```
You may use any monad, like `Task`, `Coeval`, `Eval`, `IO`...
### Minimal dependencies
`codec-core` depends only on `cats`. Each particular codec set is moved into separate module.
### Cross compile
In case of complex algorithms, it's worthy to share codebase between platforms. We cross-compile all the codecs possible both to Scala and Scala.js.
## Installation
```scala
// Bintray repo is used so far. Migration to Maven Central is planned
resolvers += Resolver.bintrayRepo("fluencelabs", "releases")
val codecV = "0.0.4"
libraryDependencies ++= Seq(
"one.fluence" %%% "codec-core" % codecV, // basic types
"one.fluence" %%% "codec-bits" % codecV, // scodec-bits conversions for ByteVector
"one.fluence" %%% "codec-circe" % codecV, // json handling with circe
"one.fluence" %%% "codec-protobuf" % codecV, // ByteString conversions for both scala and scala.js
"one.fluence" %% "codec-kryo" % codecV // typesafe kryo codecs, only for scala
)
```
## Example
```scala
import cats.syntax.compose._
import fluence.codec.PureCodec
import fluence.codec.circe.CirceCodecs._
import io.circe.{Decoder, Encoder, Json}
import scodec.bits.ByteVector
import fluence.codec.bits.BitsCodecs._
// Simple class
case class User(id: Int, name: String)
// Encode and decode with circe
implicit val encoder: Encoder[User] =
user ⇒ Json.obj("id" → Encoder.encodeInt(user.id), "name" → Encoder.encodeString(user.name))
implicit val decoder: Decoder[User] = cursor ⇒
for {
id ← cursor.downField("id").as[Int]
name ← cursor.downField("name").as[String]
} yield User(id, name)
// Get codec for encoder/decoder
implicit val userJson: PureCodec[User, Json] = circeJsonCodec(encoder, decoder)
// A trivial string to bytes codec; never use it in production!
implicit val stringCodec: PureCodec[String, Array[Byte]] =
PureCodec.liftB(_.getBytes, bs ⇒ new String(bs))
// Convert user to byte vector and vice versa
implicit val userJsonVec: PureCodec[User, ByteVector] =
PureCodec[User, Json] andThen
PureCodec[Json, String] andThen
PureCodec[String, Array[Byte]] andThen
PureCodec[Array[Byte], ByteVector]
// Try it with an instance
val user = User(234, "Hey Bob")
// unsafe() is to be used in tests only; it throws!
println(userJsonVec.direct.unsafe(user).toBase64)
// eyJpZCI6MjM0LCJuYW1lIjoiSGV5IEJvYiJ9
```
For more real-world examples, see [Fluence](https://github.com/fluencelabs/fluence).
## Roadmap
- `connect[A, B, C]` to compose several Funcs or Bijections
- `sbt-tut` for docs
- Implement more codecs
- Enhance `Func` api with shortcuts to `EitherT` methods
- Consider improving performance: `EitherT` [is not so fast](https://twitter.com/alexelcu/status/988031831357485056) (at least yet)
## License
Fluence is free software; you can redistribute it and/or modify it under the terms of the GNU Affero General Public License v3 (AGPLv3) as published by the Free Software Foundation.
Fluence includes some [external modules](https://github.com/fluencelabs/codec/blob/master/build.sbt) that carry their own licensing.
Like bijections, but pure and suitable for `cats`.

View File

@ -29,36 +29,18 @@ object BitsCodecs {
implicit val byteArrayToVector: PureCodec[Array[Byte], ByteVector] =
liftB(ByteVector.apply, _.toArray)
object Base64 {
// Notice the use of default Base64 alphabet
implicit val base64ToVector: PureCodec[String, ByteVector] =
alphabetToVector(Bases.Alphabets.Base64)
// Notice the use of default Base64 alphabet
implicit val base64ToVector: PureCodec[String, ByteVector] =
base64AlphabetToVector(Bases.Alphabets.Base64)
def alphabetToVector(alphabet: Bases.Base64Alphabet): PureCodec[String, ByteVector] =
liftEitherB(
str
ByteVector
.fromBase64Descriptive(str, alphabet)
.left
.map(CodecError(_)),
vec Right(vec.toBase64(alphabet))
)
}
object Base58 {
// Notice the use of default Base64 alphabet
implicit val base58ToVector: PureCodec[String, ByteVector] =
alphabetToVector(Bases.Alphabets.Base58)
def alphabetToVector(alphabet: Bases.Alphabet): PureCodec[String, ByteVector] =
liftEitherB(
str
ByteVector
.fromBase58Descriptive(str, alphabet)
.left
.map(CodecError(_)),
vec Right(vec.toBase58(alphabet))
)
}
def base64AlphabetToVector(alphabet: Bases.Base64Alphabet): PureCodec[String, ByteVector] =
liftEitherB(
str
ByteVector
.fromBase64Descriptive(str, alphabet)
.left
.map(CodecError(_)),
vec Right(vec.toBase64(alphabet))
)
}

View File

@ -22,7 +22,6 @@ import cats.syntax.compose._
import org.scalatest.prop.Checkers
import org.scalatest.{Matchers, WordSpec}
import scodec.bits.ByteVector
import BitsCodecs.Base64._
import BitsCodecs._
import fluence.codec.PureCodec
@ -33,7 +32,7 @@ class BitsCodecsSpec extends WordSpec with Matchers with Checkers {
val arrCodec = implicitly[PureCodec[Array[Byte], ByteVector]]
val b64Codec = implicitly[PureCodec[ByteVector, String]]
check { bytes: List[Byte]
check { (bytes: List[Byte])
(arrCodec andThen arrCodec.swap).direct.apply[Id](bytes.toArray).value.map(_.toList).contains(bytes) &&
(arrCodec andThen b64Codec andThen b64Codec.swap andThen arrCodec.swap).direct
.apply[Id](bytes.toArray)

View File

@ -1,6 +1,6 @@
import de.heikoseeberger.sbtheader.License
import org.scalajs.sbtplugin.ScalaJSPlugin.autoImport._
import sbtcrossproject.CrossPlugin.autoImport.crossProject
import sbtcrossproject.crossProject
name := "codec"
@ -10,12 +10,11 @@ javaOptions in Test ++= Seq("-ea")
skip in publish := true // Skip root project
val scalaV = scalaVersion := "2.12.8"
val scalaV = scalaVersion := "2.12.5"
val commons = Seq(
scalaV,
//crossScalaVersions := Seq(scalaVersion.value, "2.13.0-RC1"),
version := "0.0.5",
version := "0.0.1",
fork in Test := true,
parallelExecution in Test := false,
organization := "one.fluence",
@ -23,27 +22,25 @@ val commons = Seq(
organizationHomepage := Some(new URL("https://fluence.one")),
startYear := Some(2017),
licenses += ("AGPL-V3", new URL("http://www.gnu.org/licenses/agpl-3.0.en.html")),
headerLicense := Some(License.AGPLv3("2017", organizationName.value)),
headerLicense := Some(License.AGPLv3("2017", organizationName.value)),
bintrayOrganization := Some("fluencelabs"),
publishMavenStyle := true,
bintrayRepository := "releases"
publishMavenStyle := true,
bintrayRepository := "releases"
)
commons
val kindProjector = addCompilerPlugin("org.typelevel" % "kind-projector" % "0.10.0" cross CrossVersion.binary)
val kindProjector = addCompilerPlugin("org.spire-math" %% "kind-projector" % "0.9.6")
val Cats1V = "1.6.0"
val ScodecBitsV = "1.1.10"
val CirceV = "0.11.1"
val Cats1V = "1.1.0"
val ScodecBitsV = "1.1.5"
val CirceV = "0.9.3"
val ShapelessV = "2.3.+"
val chill = "com.twitter" %% "chill" % "0.9.3"
val chill = "com.twitter" %% "chill" % "0.9.2"
val ScalatestV = "3.0.5"
// Note that cats-laws 1.5 are compiled against scalacheck 1.13, and scalacheck-shapeless should also not introduce the upgrade
val ScalacheckV = "1.13.5"
val ScalatestV = "3.0.+"
val ScalacheckV = "1.13.4"
val protobuf = Seq(
PB.targets in Compile := Seq(
@ -64,11 +61,12 @@ lazy val `codec-core` = crossProject(JVMPlatform, JSPlatform)
commons,
kindProjector,
libraryDependencies ++= Seq(
"org.typelevel" %%% "cats-core" % Cats1V,
"org.typelevel" %%% "cats-testkit" % Cats1V % Test,
"org.typelevel" %%% "cats-core" % Cats1V,
"org.typelevel" %%% "cats-laws" % Cats1V % Test,
"org.typelevel" %%% "cats-testkit" % Cats1V % Test,
"com.github.alexarchambault" %%% "scalacheck-shapeless_1.13" % "1.1.8" % Test,
"org.scalacheck" %%% "scalacheck" % ScalacheckV % Test,
"org.scalatest" %%% "scalatest" % ScalatestV % Test
"org.scalacheck" %%% "scalacheck" % ScalacheckV % Test,
"org.scalatest" %%% "scalatest" % ScalatestV % Test
)
)
.jsSettings(
@ -86,9 +84,9 @@ lazy val `codec-bits` = crossProject(JVMPlatform, JSPlatform)
.settings(
commons,
libraryDependencies ++= Seq(
"org.scodec" %%% "scodec-bits" % ScodecBitsV,
"org.scalacheck" %%% "scalacheck" % ScalacheckV % Test,
"org.scalatest" %%% "scalatest" % ScalatestV % Test
"org.scodec" %%% "scodec-bits" % ScodecBitsV,
"org.scalacheck" %%% "scalacheck" % ScalacheckV % Test,
"org.scalatest" %%% "scalatest" % ScalatestV % Test
)
)
.jsSettings(
@ -127,8 +125,8 @@ lazy val `codec-kryo` = project
commons,
libraryDependencies ++= Seq(
chill,
"com.chuusai" %% "shapeless" % ShapelessV,
"org.scalatest" %% "scalatest" % ScalatestV % Test
"com.chuusai" %% "shapeless" % ShapelessV,
"org.scalatest" %% "scalatest" % ScalatestV % Test
)
)
.dependsOn(`codec-core-jvm`)
@ -150,3 +148,4 @@ lazy val `codec-protobuf` = crossProject(JVMPlatform, JSPlatform)
lazy val `codec-protobuf-jvm` = `codec-protobuf`.jvm
lazy val `codec-protobuf-js` = `codec-protobuf`.js

View File

@ -22,7 +22,7 @@ import io.circe._
object CirceCodecs {
implicit def circeJsonCodec[T](implicit encoder: Encoder[T], decoder: Decoder[T]): PureCodec[T, Json] =
implicit def circeJsonCodec[T](encoder: Encoder[T], decoder: Decoder[T]): PureCodec[T, Json] =
PureCodec.liftEitherB[T, Json](
t Right(encoder.apply(t)),
json decoder.decodeJson(json).left.map(f CodecError("Cannot decode json value", causedBy = Some(f)))

View File

@ -0,0 +1,97 @@
/*
* Copyright (C) 2017 Fluence Labs Limited
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package fluence.codec
import cats.data.Kleisli
import cats.{Applicative, FlatMap, Traverse}
import cats.syntax.applicative._
import scala.language.{higherKinds, implicitConversions}
/**
* Base trait for serialize/deserialize objects.
*
* @tparam A The type of plain object representation
* @tparam B The type of binary representation
* @tparam F Encoding/decoding effect
*/
@deprecated(
"Codec is planned for removing soon, as it's impure and not properly tested. Use PureCodec instead.",
"6.4.2018"
)
final case class Codec[F[_], A, B](encode: A F[B], decode: B F[A]) {
self
implicit val direct: Kleisli[F, A, B] = Kleisli(encode)
implicit val inverse: Kleisli[F, B, A] = Kleisli(decode)
def andThen[C](other: Codec[F, B, C])(implicit F: FlatMap[F]): Codec[F, A, C] =
Codec((self.direct andThen other.direct).run, (other.inverse andThen self.inverse).run)
def compose[C](other: Codec[F, C, A])(implicit F: FlatMap[F]): Codec[F, C, B] =
Codec((other.direct andThen self.direct).run, (self.inverse andThen other.inverse).run)
def swap: Codec[F, B, A] = Codec(decode, encode)
}
@deprecated(
"Codec is planned for removing soon, as it's impure and not properly tested. Use PureCodec instead.",
"6.4.2018"
)
object Codec {
implicit def identityCodec[F[_]: Applicative, T]: Codec[F, T, T] =
Codec(_.pure[F], _.pure[F])
implicit def traverseCodec[F[_]: Applicative, G[_]: Traverse, O, B](
implicit codec: Codec[F, O, B]
): Codec[F, G[O], G[B]] =
Codec[F, G[O], G[B]](Traverse[G].traverse[F, O, B](_)(codec.encode), Traverse[G].traverse[F, B, O](_)(codec.decode))
implicit def toDirect[F[_], A, B](implicit cod: Codec[F, A, B]): Kleisli[F, A, B] =
cod.direct
implicit def toInverse[F[_], A, B](implicit cod: Codec[F, A, B]): Kleisli[F, B, A] =
cod.inverse
implicit def swap[F[_], A, B](implicit cod: Codec[F, A, B]): Codec[F, B, A] =
Codec[F, B, A](cod.decode, cod.encode)
@deprecated(
"Codec is planned for removing soon, as it's impure and not properly tested. Use PureCodec instead.",
"6.4.2018"
)
def codec[F[_], O, B](implicit codec: Codec[F, O, B]): Codec[F, O, B] = codec
/**
* Constructs a Codec from pure encode/decode functions and an Applicative
*
* @param encodeFn Encode function that never fail
* @param decodeFn Decode function that never fail
* @tparam F Applicative effect
* @tparam O Raw type
* @tparam B Encoded type
* @return New codec for O and B
*/
@deprecated(
"Codec is planned for removing soon, as it's impure and not properly tested. Use PureCodec instead.",
"6.4.2018"
)
def pure[F[_]: Applicative, O, B](encodeFn: O B, decodeFn: B O): Codec[F, O, B] =
Codec(encodeFn(_).pure[F], decodeFn(_).pure[F])
}

View File

@ -18,13 +18,12 @@
package fluence.codec
import cats.{Monad, MonadError, Traverse}
import cats.arrow.{ArrowChoice, Category}
import cats.syntax.arrow._
import cats.arrow.{ArrowChoice, Compose}
import cats.data.{EitherT, Kleisli}
import cats.syntax.flatMap._
import cats.syntax.compose._
import scala.language.{existentials, higherKinds}
import scala.language.higherKinds
import scala.util.Try
/**
@ -33,7 +32,6 @@ import scala.util.Try
* @tparam E Error type
*/
abstract class MonadicalEitherArrow[E <: Throwable] {
mea
/**
* Alias for error type
@ -49,7 +47,6 @@ abstract class MonadicalEitherArrow[E <: Throwable] {
* @tparam B Successful result type
*/
abstract class Func[A, B] {
f
/**
* Run the func on input, using the given monad.
@ -63,10 +60,9 @@ abstract class MonadicalEitherArrow[E <: Throwable] {
* Run the func on input, lifting the error into MonadError effect.
*
* @param input Input
* @param F All internal maps and composes, as well as errors, are to be executed with this MonadError.
* Error type should be a supertype for this arrow's error E.
* @param F All internal maps and composes, as well as errors, are to be executed with this Monad
*/
def runF[F[_]](input: A)(implicit F: MonadError[F, EE] forSome { type EE >: E }): F[B] =
def runF[F[_]](input: A)(implicit F: MonadError[F, Throwable]): F[B] =
runEither(input).flatMap(F.fromEither)
/**
@ -77,37 +73,12 @@ abstract class MonadicalEitherArrow[E <: Throwable] {
*/
def apply[F[_]: Monad](input: A): EitherT[F, E, B]
/**
* Shortcut for function composition
*
* @param other Other function to run after
* @tparam C Resulting input type
* @return Composed function
*/
def on[C](other: Func[C, A]): Func[C, B] =
catsMonadicalEitherArrowChoice.compose(this, other)
/**
* Convert this Func into another one, lifting the error
*
* @param m Another instance of MonadicalEitherArrow
* @param convertE Convert error
* @tparam EE Error type
* @return Converted function
*/
def to[EE <: Throwable](m: MonadicalEitherArrow[EE])(implicit convertE: E EE): m.Func[A, B] =
new m.Func[A, B] {
override def apply[F[_]: Monad](input: A): EitherT[F, EE, B] =
f[F](input).leftMap(convertE)
}
/**
* Converts this Func to Kleisli, using MonadError to execute upon and to lift errors into.
*
* @param F All internal maps and composes, as well as errors, are to be executed with this MonadError.
* Error type should be a supertype for this arrow's error E.
* @param F All internal maps and composes, as well as errors, are to be executed with this Monad
*/
def toKleisli[F[_]](implicit F: MonadError[F, EE] forSome { type EE >: E }): Kleisli[F, A, B] =
def toKleisli[F[_]](implicit F: MonadError[F, Throwable]): Kleisli[F, A, B] =
Kleisli(input runF[F](input))
/**
@ -120,15 +91,6 @@ abstract class MonadicalEitherArrow[E <: Throwable] {
import cats.instances.try_._
runF[Try](input).get
}
/**
* Picks a point from the arrow, using the initial element (Unit) on the left.
*
* @param input Point to pick
* @return Picked point
*/
def pointAt(input: A): Point[B] =
catsMonadicalEitherArrowChoice.lmap(this)(_ input)
}
/**
@ -146,12 +108,12 @@ abstract class MonadicalEitherArrow[E <: Throwable] {
*/
lazy val swap: Bijection[B, A] = Bijection(inverse, direct)
/**
* Splits the input and puts it to either bijection, then merges output.
* It could have been achieved with `Strong` typeclass in case it doesn't extend `Profunctor`; but it does.
*/
def split[A1, B1](bj: Bijection[A1, B1]): Bijection[(A, A1), (B, B1)] =
mea.split(this, bj)
@deprecated(
"You should keep codec Pure until running direct or inverse on it: there's no reason to bind effect into Codec",
"6.4.2018"
)
def toCodec[F[_]](implicit F: MonadError[F, Throwable]): Codec[F, A, B] =
Codec(direct.runF[F], inverse.runF[F])
}
/**
@ -211,26 +173,6 @@ abstract class MonadicalEitherArrow[E <: Throwable] {
EitherT.rightT[F, E](input).subflatMap(f)
}
/**
* Check a condition, lifted with a Func.
*
* @param error Error to produce when condition is not met
* @return Func that takes boolean, checks it, and returns Unit or fails with given error
*/
def cond(error: E): Func[Boolean, Unit] =
liftFuncEither(Either.cond(_, (), error))
/**
* Lift a function which returns a Func arrow with Unit on the left side.
*
* @param f Function to lift
*/
def liftFuncPoint[A, B](f: A Point[B]): Func[A, B] =
new Func[A, B] {
override def apply[F[_]: Monad](input: A): EitherT[F, E, B] =
f(input).apply[F](())
}
/**
* Func that does nothing with input.
*/
@ -264,42 +206,6 @@ abstract class MonadicalEitherArrow[E <: Throwable] {
}
}
/**
* Point type maps from Unit to a particular value of A, so it's just a lazy Func.
*
* @tparam A Output value type
*/
type Point[A] = Func[Unit, A]
/**
* Point must obey MonadErrorLaws
*/
implicit object catsMonadicalEitherPointMonad extends MonadError[Point, E] {
override def flatMap[A, B](fa: Point[A])(f: A Point[B]): Point[B] =
new Func[Unit, B] {
override def apply[F[_]: Monad](input: Unit): EitherT[F, E, B] =
fa[F](()).flatMap(f(_).apply[F](()))
}
override def tailRecM[A, B](a: A)(f: A Point[Either[A, B]]): Point[B] =
new Func[Unit, B] {
override def apply[F[_]: Monad](input: Unit): EitherT[F, E, B] =
Monad[EitherT[F, E, ?]].tailRecM(a)(f(_).apply[F](()))
}
override def raiseError[A](e: E): Point[A] =
liftFuncEither(_ Left(e))
override def handleErrorWith[A](fa: Point[A])(f: E Point[A]): Point[A] =
new Func[Unit, A] {
override def apply[F[_]: Monad](input: Unit): EitherT[F, E, A] =
fa[F](()).leftFlatMap(e f(e).apply[F](()))
}
override def pure[A](x: A): Point[A] =
liftFunc(_ x)
}
/**
* Lifts pure direct and inverse functions into Bijection.
*
@ -322,12 +228,6 @@ abstract class MonadicalEitherArrow[E <: Throwable] {
def liftEitherB[A, B](direct: A Either[E, B], inverse: B Either[E, A]): Bijection[A, B] =
Bijection(liftFuncEither(direct), liftFuncEither(inverse))
/**
* Lifts point functions into Bijection.
*/
def liftPointB[A, B](direct: A Point[B], inverse: B Point[A]): Bijection[A, B] =
Bijection(liftFuncPoint(direct), liftFuncPoint(inverse))
/**
* Bijection that does no transformation.
*/
@ -350,20 +250,10 @@ abstract class MonadicalEitherArrow[E <: Throwable] {
implicit def swap[A, B](implicit bijection: Bijection[A, B]): Bijection[B, A] = bijection.swap
/**
* Bijection should obey CategoryLaws
* Bijection should obey ComposeLaws
*/
implicit object catsMonadicalBijectionCategory extends Category[Bijection] {
implicit object catsMonadicalBijectionCompose extends Compose[Bijection] {
override def compose[A, B, C](f: Bijection[B, C], g: Bijection[A, B]): Bijection[A, C] =
Bijection(f.direct compose g.direct, g.inverse compose f.inverse)
override def id[A]: Bijection[A, A] = identityBijection
}
/**
* Splits the input and puts it to either bijection, then merges output.
* It could be achieved with `Strong` typeclass in case it doesn't extend `Profunctor`; but it does.
*/
def split[A1, B1, A2, B2](f1: Bijection[A1, B1], f2: Bijection[A2, B2]): Bijection[(A1, A2), (B1, B2)] =
Bijection(f1.direct *** f2.direct, f1.inverse *** f2.inverse)
}

View File

@ -17,7 +17,7 @@
package fluence.codec
import cats.laws.discipline.CategoryTests
import cats.laws.discipline.ComposeTests
import cats.tests.CatsSuite
import cats.Eq
import org.scalacheck.ScalacheckShapeless._
@ -32,7 +32,7 @@ class PureCodecBijectionLawsSpec extends CatsSuite {
Eq.instance((x, y) directEq.eqv(x.direct, y.direct) && inverseEq.eqv(x.inverse, y.inverse))
checkAll(
"PureCodec.Bijection.CategoryLaws",
CategoryTests[PureCodec].category[Int, String, Double, BigDecimal]
"PureCodec.Bijection.ComposeLaws",
ComposeTests[PureCodec].compose[Int, String, Double, BigDecimal]
)
}

View File

@ -23,13 +23,6 @@ import org.scalacheck.{Arbitrary, Cogen, Gen}
import org.scalacheck.ScalacheckShapeless._
object PureCodecFuncTestInstances {
implicit def arbCodecError: Arbitrary[CodecError] =
Arbitrary(Gen.alphaLowerStr.map(CodecError(_)))
implicit def eqCodecError: Eq[CodecError] = Eq.fromUniversalEquals
implicit def cogenE: Cogen[CodecError] = Cogen.cogenString.contramap[CodecError](_.message)
implicit def arbFunc[A: Arbitrary: Cogen, B: Arbitrary]: Arbitrary[PureCodec.Func[A, B]] =
Arbitrary(
Gen

View File

@ -1,52 +0,0 @@
/*
* Copyright (C) 2017 Fluence Labs Limited
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package fluence.codec
import cats.{Applicative, Eq, Invariant}
import cats.data.EitherT
import cats.syntax.functor._
import cats.laws.discipline.{MonadErrorTests, SemigroupalTests}
import cats.tests.CatsSuite
import fluence.codec
import org.scalacheck.Arbitrary
import org.scalacheck.Arbitrary._
import org.scalacheck.ScalacheckShapeless._
class PureCodecPointLawsSpec extends CatsSuite {
import PureCodecFuncTestInstances._
implicit def eqEitherTFEA: Eq[EitherT[PureCodec.Point, CodecError, Int]] =
Eq.instance{
case (aa,bb)
aa.value.unsafe(()) == bb.value.unsafe(())
}
implicit val iso = SemigroupalTests.Isomorphisms.invariant[PureCodec.Point](
new Invariant[PureCodec.Point]{
override def imap[A, B](fa: codec.PureCodec.Point[A])(f: A B)(g: B A): codec.PureCodec.Point[B] =
fa.map(f)
}
)
checkAll(
"PureCodec.Point.MonadErrorLaws",
MonadErrorTests[PureCodec.Point, CodecError]
.monadError[Int, String, Double]
)
}

View File

@ -17,29 +17,36 @@
package fluence.codec.kryo
import cats.MonadError
import com.twitter.chill.KryoPool
import fluence.codec.{CodecError, PureCodec}
import fluence.codec.{Codec, CodecError, PureCodec}
import shapeless._
import scala.language.higherKinds
import scala.reflect.ClassTag
import scala.util.Try
import scala.util.control.NonFatal
/**
* Wrapper for a KryoPool with a list of registered classes
*
* @param pool Pre-configured KryoPool
* @param F Applicative error
* @tparam L List of classes registered with kryo
* @tparam F Effect
*/
class KryoCodecs[L <: HList] private (pool: KryoPool) {
class KryoCodecs[F[_], L <: HList] private (pool: KryoPool)(implicit F: MonadError[F, Throwable]) {
/**
* Returns a codec for any registered type
*
* @param sel Shows the presence of type T within list L
* @tparam T Object type
* @return Freshly created PureCodec with Kryo inside
* @return Freshly created Codec with Kryo inside
*/
implicit def codec[T](implicit sel: ops.hlist.Selector[L, T]): Codec[F, T, Array[Byte]] =
pureCodec[T].toCodec[F]
implicit def pureCodec[T](implicit sel: ops.hlist.Selector[L, T]): PureCodec[T, Array[Byte]] =
PureCodec.Bijection(
PureCodec.liftFuncEither { input
@ -105,10 +112,10 @@ object KryoCodecs {
* @tparam F Effect type
* @return Configured instance of KryoCodecs
*/
def build(
def build[F[_]](
poolSize: Int = Runtime.getRuntime.availableProcessors
): KryoCodecs[L] =
new KryoCodecs[L](
)(implicit F: MonadError[F, Throwable]): KryoCodecs[F, L] =
new KryoCodecs[F, L](
KryoPool.withByteArrayOutputStream(
poolSize,
KryoFactory(klasses, registrationRequired = true) // registrationRequired should never be needed, as codec derivation is typesafe

View File

@ -18,7 +18,6 @@
package fluence.codec.kryo
import cats.instances.try_._
import cats.syntax.profunctor._
import org.scalatest.{Matchers, WordSpec}
import scala.util.Try
@ -32,15 +31,15 @@ class KryoCodecsSpec extends WordSpec with Matchers {
KryoCodecs()
.add[Array[Array[Byte]]]
.addCase(classOf[TestClass])
.build()
.build[Try]()
"encode and decode" should {
"be inverse functions" when {
"object defined" in {
val codec = testCodecs.pureCodec[TestClass]
val codec = testCodecs.codec[TestClass]
val result = codec.inverse.unsafe(codec.direct.unsafe(testClass))
val result = codec.encode(testClass).flatMap(codec.decode).get
result.str shouldBe "one"
result.num shouldBe 2
@ -48,8 +47,8 @@ class KryoCodecsSpec extends WordSpec with Matchers {
}
"object is null" in {
val codec = testCodecs.pureCodec[TestClass]
val result = codec.direct.runF[Try](null).flatMap(codec.inverse.runF[Try])
val codec = testCodecs.codec[TestClass]
val result = codec.encode(null).flatMap(codec.decode)
result.isFailure shouldBe true
}
}
@ -58,8 +57,9 @@ class KryoCodecsSpec extends WordSpec with Matchers {
"encode" should {
"not write full class name to binary representation" when {
"class registered" in {
val codec = testCodecs.pureCodec[TestClass]
val encoded = codec.direct.rmap(new String(_)).unsafe(testClass)
//val codec = KryoCodec[TestClass](Seq(classOf[TestClass], classOf[Array[Byte]], classOf[Array[Array[Byte]]]), registerRequired = true)
val codec = testCodecs.codec[TestClass]
val encoded = codec.encode(testClass).map(new String(_)).get
val reasonableMaxSize = 20 // bytes
encoded should not contain "TestClass"
encoded.length should be < reasonableMaxSize

View File

@ -1 +1 @@
sbt.version = 1.2.8
sbt.version = 1.1.2

View File

@ -1,21 +1,21 @@
addSbtPlugin("org.scalameta" % "sbt-scalafmt" % "2.0.0")
addSbtPlugin("com.geirsson" % "sbt-scalafmt" % "1.4.0")
addSbtPlugin("com.typesafe.sbt" % "sbt-native-packager" % "1.3.20")
addSbtPlugin("com.typesafe.sbt" % "sbt-native-packager" % "1.3.2")
addSbtPlugin("de.heikoseeberger" % "sbt-header" % "5.2.0")
addSbtPlugin("de.heikoseeberger" % "sbt-header" % "4.1.0")
addSbtPlugin("com.thesamet" % "sbt-protoc" % "0.99.20")
addSbtPlugin("com.thesamet" % "sbt-protoc" % "0.99.18")
addSbtPlugin("org.scala-js" % "sbt-scalajs" % "0.6.27")
addSbtPlugin("org.portable-scala" % "sbt-crossproject" % "0.6.0")
addSbtPlugin("org.portable-scala" % "sbt-scalajs-crossproject" % "0.6.0")
addSbtPlugin("org.scala-js" % "sbt-scalajs" % "0.6.22")
addSbtPlugin("org.portable-scala" % "sbt-crossproject" % "0.3.1")
addSbtPlugin("org.portable-scala" % "sbt-scalajs-crossproject" % "0.3.1")
addSbtPlugin("ch.epfl.scala" % "sbt-scalajs-bundler" % "0.14.0")
addSbtPlugin("ch.epfl.scala" % "sbt-scalajs-bundler" % "0.10.0")
addSbtPlugin("net.virtual-void" % "sbt-dependency-graph" % "0.9.2")
addSbtPlugin("net.virtual-void" % "sbt-dependency-graph" % "0.9.0")
addSbtPlugin("com.lihaoyi" % "workbench" % "0.4.1")
addSbtPlugin("com.lihaoyi" % "workbench" % "0.4.0")
libraryDependencies += "com.thesamet.scalapb" %% "compilerplugin" % "0.8.4"
libraryDependencies += "com.thesamet.scalapb" %% "compilerplugin" % "0.7.1"
addSbtPlugin("org.foundweekends" % "sbt-bintray" % "0.5.4")
addSbtPlugin("org.foundweekends" % "sbt-bintray" % "0.5.4")