Popularity
1.4
Growing
Activity
0.0
Stable
21
2
1

Programming language: Scala
License: MIT License

Poppet alternatives and similar packages

Based on the "Distributed Systems" category.
Alternatively, view Poppet alternatives based on common mentions on social networks and blogs.

Do you think we are missing an alternative of Poppet or a related project?

Add another 'Distributed Systems' Package

README

Poppet

Maven Central Sonatype Nexus (Snapshots) Build Status License: MIT

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

Essential differences from autowire:

  • no explicit macro application .call, result of a consumer is an instance of original trait
  • no restricted kind Future, you can specify any monad (has cats.Monad typeclass) as a processor kind, and an arbitrary kind for trait methods
  • no forced codec dependencies uPickle, you can choose from predefined codecs or simply implement your own
  • robust failure handling mechanism
  • supports Scala 3 (method/class generation with macros is still an experimental feature)

Table of contents

  1. Quick start
  2. Customizations
    1. Logging
    2. Failure handling
  3. Manual calls
  4. Examples
  5. Changelog

Quick start

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

val version = new {
    cats = "2.6.1"
    poppet = "0.3.0"
}

libraryDependencies ++= Seq(
    "org.typelevel" %% "cats-core" % version.cats,
    "com.github.yakivy" %% "poppet-circe" % version.poppet, //to use circe
    //"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:

  • [F[_]] - is your service data kind, can be any monad (has cats.Monad typeclass);
  • [I] - is an intermediate data type that your coding framework works with, can be any serialization format, but it would be easier to choose from existed codec modules as they come with a bunch of predefined codecs;
  • poppet.consumer.Transport - used to transfer the data between consumer and provider apps, technically it is just a function from [I] to [F[I]], so you can use anything as long as it can receive/pass the chosen data type;
  • poppet.Codec - used to convert [I] to domain models and vice versa. Poppet comes with a bunch of modules, where you will hopefully find a favourite codec. If it is not there, you can always try to write your own by providing 2 basic implicits like here;
  • poppet.CodecK - used to convert method return kind to [F] and vice versa. It's needed only if return kind differs from your service kind, compilation errors will hint you what codecs are absent;
  • poppet.FailureHandler[F[_]] - used to handle internal failures, more info you can find here;
  • poppet.Peek[F[_], I] - used to decorate a function from Request[I] to F[Response[I]]. Good fit for logging, more info you can find here.

Logging

Both provider and consumer take Peek[F, I] as an argument, that allows to inject logging logic around the Request[I] => F[Response[I]] function. Let's define simple logging peek:

val peek: Peek[Id, Json] = f => request => {
    println("Request: " + request)
    val response = f(request)
    println("Response: " + response)
    response
}

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, *] kind, 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, *] kind.

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: ```shell script 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 } }'


### Examples
- run desired example:
    - Http4s with Circe: https://github.com/yakivy/poppet/tree/master/example/http4s
        - run provider: `./mill example.http4s.provider.run`
        - run consumer: `./mill example.http4s.consumer.run`
    - Play Framework with Play Json: https://github.com/yakivy/poppet/tree/master/example/play
        - run provider: `./mill example.play.provider.run`
        - run consumer: `./mill example.play.consumer.run`
        - remove `RUNNING_PID` file manually if services are conflicting with each other
    - And even Spring Framework with Jackson ๐Ÿ˜ฒ: https://github.com/yakivy/poppet/tree/master/example/spring
        - run provider: `./mill example.spring.provider.run`
        - run consumer: `./mill example.spring.consumer.run`
- put `http://localhost:9002/api/user/1` in the address bar
### Changelog

#### 0.3.0:
- add Scala 3 support

#### 0.2.2:
- fix compilation error message for ambiguous implicits

#### 0.2.1:
- fix processor compilation for complex types

#### 0.2.0:
- migrate to mill build tool
- add Scala JS and Scala Native support
- add more details to `Can't find processor` exception
- make `FailureHandler` explicit
- rename `poppet.coder` package to `poppet.codec`
- various refactorings and cleanups


*Note that all licence references and agreements mentioned in the Poppet README section above are relevant to that project's source code only.