Home

Awesome

Poppet

Maven Central Sonatype Nexus (Snapshots) License: MIT <a href="https://typelevel.org/cats/"><img src="https://typelevel.org/cats/img/cats-badge.svg" height="40px" align="right" alt="Cats friendly" /></a>

Poppet is a minimal, type-safe RPC Scala library.

Essential differences from autowire:

Table of contents

  1. Quick start
  2. Customizations
    1. Failure handling
  3. Manual calls
  4. Limitations
  5. API versioning
  6. Examples
  7. Changelog

Quick start

Put cats and poppet dependencies in the build file, let's assume you are using SBT:

val version = new {
    val cats = "2.10.0"
    val circe = "0.14.6"
    val poppet = "0.4.0"
}

libraryDependencies ++= Seq(
    "org.typelevel" %% "cats-core" % version.cats,

    //to use circe
    "io.circe" %% "circe-core" % version.circe,
    "com.github.yakivy" %% "poppet-circe" % version.poppet,

    //"com.github.yakivy" %% "poppet-upickle" % version.poppet, //to use upickle
    //"com.github.yakivy" %% "poppet-play-json" % version.poppet, //to use play json
    //"com.github.yakivy" %% "poppet-jackson" % version.poppet, //to use jackson
    //"com.github.yakivy" %% "poppet-core" % version.poppet, //to build custom codec
)

Define service trait and share it between provider and consumer apps:

case class User(email: String, firstName: String)
trait UserService {
    def findById(id: String): Future[User]
}

Implement service trait with actual logic:

class UserInternalService extends UserService {
    override def findById(id: String): Future[User] = {
        //emulation of business logic
        if (id == "1") Future.successful(User(id, "Antony"))
        else Future.failed(new IllegalArgumentException("User is not found"))
    }
}

Create service provider (can be created once and shared for all incoming calls), keep in mind that only abstract methods of the service type will be exposed, so you need to explicitly specify a trait type:

import cats.implicits._
import io.circe._
import io.circe.generic.auto._
import poppet.codec.circe.all._
import poppet.provider.all._

//replace with serious pool
implicit val ec: ExecutionContext = ExecutionContext.global

val provider = Provider[Future, Json]()
    .service[UserService](new UserInternalService)
    //.service[OtherService](otherService)

Create service consumer (can be created once and shared everywhere):

import cats.implicits._
import io.circe._
import io.circe.generic.auto._
import poppet.codec.circe.all._
import poppet.consumer.all._
import scala.concurrent.ExecutionContext

//replace with serious pool
implicit val ec: ExecutionContext = ExecutionContext.global
//replace with actual transport call
val transport: Transport[Future, Json] = request => provider(request)

val userService = Consumer[Future, Json](transport)
    .service[UserService]

Enjoy 👌

userService.findById("1")

Customizations

The library is build on following abstractions:

Failure handling

All meaningful failures that can appear in the library are being transformed into poppet.Failure, after what, handled with poppet.FailureHandler. Failure handler is a simple polymorphic function from failure to lifted result:

trait FailureHandler[F[_]] {
    def apply[A](f: Failure): F[A]
}

by default, throwing failure handler is being used:

def throwing[F[_]]: FailureHandler[F] = new FailureHandler[F] {
    override def apply[A](f: Failure): F[A] = throw f
}

so if your don't want to deal with JVM exceptions, you can provide your own instance of failure handler. Let's assume you want to pack a failure with EitherT[Future, String, *] HKT, then failure handler can look like:

type SR[A] = EitherT[Future, String, A]
val SRFailureHandler = new FailureHandler[SR] {
    override def apply[A](f: Failure): SR[A] = EitherT.leftT(f.getMessage)
}

For more info you can check Http4s with Circe example project, it is built around EitherT[IO, String, *] HKT.

Manual calls

If your codec has a human-readable format (JSON for example), you can use a provider without consumer (mostly for debug purposes) by generating requests manually. Here is an example of curl call:

curl --location --request POST '${providerUrl}' \
--data-raw '{
    "service": "poppet.UserService", #full class name of the service
    "method": "findById", #method name
    "arguments": {
        "id": "1" #argument name: encoded value
    }
}'

Limitations

You can generate consumer/provider almost from any Scala trait (or Java interface 😲). It can have non-abstract members, methods with default arguments, methods with multiple argument lists, varargs, etc... But there are several limitations:

//compiles
def apply(a: String): Boolean = ???
def apply(b: Int): Boolean = ???

// doesn't compile
def apply(a: String): Boolean = ???
def apply(a: Int): Boolean = ???
//compiles
trait A[T] {
    def apply(t: T): Boolean
}

//doesn't compile
trait A {
    def apply[T](t: T): Boolean
}
trait A {
    type T
    def apply(t: T): Boolean
}

API versioning

The goal of the library is to closely resemble typical Scala traits, so same binary compatibility approaches can also be applied for API versioning, for example:

@deprecared def apply(a: String): Boolean = ???
def apply(b: Int): Boolean = ???
def apply(a: String): Boolean = ???
//if Email is serialized as a String, method can be updated to
def apply(a: Email): Boolean = ???
@deprecated trait A
trait B extends A

Provider[..., ...]()
    .service[A](bImpl)
    .service[B](bImpl)

Examples

Roadmap

Changelog

0.4.x:

0.3.x:

0.2.x: