Poppet
Poppet is a functional, extensible, type-based Scala library for generating RPC services from pure service traits.
Table of contents
Quick start
Put library version in the build file and add cats dependency, let's assume you are using sbt:
val poppetVersion = "0.0.1"
libraryDependencies += Seq(
"org.typelevel" %% "cats-core" % catsVersion
)
Play framework
API
Define service API and share it between provider and consumer services (link formatter to the model or implement separately on both sides):
case class User(email: String, firstName: String)
object User {
implicit val F = Json.format[User]
}
trait UserService {
def findById(id: String): Future[User]
}
Provider
Implement API on provider side:
class UserInternalService extends UserService {
override def findById(id: String): Future[User] = {
//emulation of business logic
Future.successful(User(id, "Antony"))
}
}
Add play poppet coder and provider dependencies to the build file
libraryDependencies += Seq(
"com.github.yakivy" %% "poppet-coder-play" % poppetVersion,
"com.github.yakivy" %% "poppet-provider-play" % poppetVersion
)
Create a provider for service, keep in mind that only abstract methods of the service type will be exposed, that's why you need to explicitly specify trait type:
import cats.implicits._
import poppet.provider.play.all._
import poppet.coder.play.all._
def provider(cc: ControllerComponents)(implicit ec: ExecutionContext) = Provider(
PlayServer(cc))(
PlayCoder())(
ProviderProcessor[UserService](new UserInternalService).generate()
)
Materialize and register provider:
routes
POST /api/service controller.ProviderController.apply()
ProviderController.scala
@Singleton
class ProviderController @Inject()(
cc: ControllerComponents)(implicit ec: ExecutionContext
) extends AbstractController(cc) {
def apply(): Action[ByteString] = provider(cc).materialize()
}
Consumer
Add play coder and consumer dependencies to the build file, as play consumer is built on play WsClient we will also need it:
libraryDependencies += Seq(
"com.github.yakivy" %% "poppet-coder-play" % poppetVersion,
"com.github.yakivy" %% "poppet-consumer-play" % poppetVersion,
ws
)
Create and materialize consumer for service (can be materialized once and shared everywhere):
import cats.implicits._
import poppet.coder.play.all._
import poppet.consumer.play.all._
def userService(wsClient: WSClient)(implicit ec: ExecutionContext): UserService = Consumer(
PlayClient(s"http://${providerHostName}/api/service")(wsClient))(
PlayCoder())(
ConsumerProcessor[UserService].generate()
).materialize()
Enjoy :)
@Singleton
class UserController @Inject()(
wsClient: WSClient, cc: ControllerComponents)(
implicit ec: ExecutionContext
) extends AbstractController(cc) {
def findById(id: String) = Action.async {
userService(wsClient).findById(id).map(Ok(_))
}
}
Http4s
Development in progress...
Spring framework
If you don't hesitate to put scala classes in your java project then you can freely use this library for java interfaces.
API
Define service API and share it between provider and consumer services:
public class User {
private String email;
private String firstName;
public User() {}
public User(String email, String firstName) {
this.email = email;
this.firstName = firstName;
}
public String getEmail() {
return email;
}
public void setEmail(String email) {
this.email = email;
}
public String getFirstName() {
return firstName;
}
public void setFirstName(String firstName) {
this.firstName = firstName;
}
}
public interface UserService {
public User findById(String id);
}
Provider
Implement API on provider side:
@Service
public class UserInternalService implements UserService {
@Override
public User findById(String id) {
return new User(id, "Antony");
}
}
Add spring coder and provider dependencies to the build file, as jackson coder doesn't work with scala classes out of the box we will also need to include jackson scala module:
libraryDependencies += Seq(
"com.github.yakivy" %% "poppet-coder-jackson" % poppetVersion,
"com.github.yakivy" %% "poppet-provider-spring" % poppetVersion,
"com.fasterxml.jackson.module" %% "jackson-module-scala" % jacksonVersion
)
Create separate scala provider generator, keep in mind that only abstract methods of the service type will be exposed:
import poppet.coder.jackson.all._
import poppet.provider.spring.all._
object ProviderGenerator {
def apply(
userService: UserService
): RequestEntity[Array[Byte]] => ResponseEntity[Array[Byte]] = Provider(
SpringServer())(
JacksonCoder())(
ProviderProcessor(userService).generate()
).materialize()
}
Register provider:
@Controller
public class ProviderController {
private UserService userService;
public ProviderController(UserService userService) {
this.userService = userService;
}
@RequestMapping("/api/service")
public ResponseEntity<byte[]> apply(RequestEntity<byte[]> request) {
return ProviderGenerator.apply(userService).apply(request);
}
}
Consumer
Add spring coder and consumer dependencies to the build file:
libraryDependencies += Seq(
"com.github.yakivy" %% "poppet-coder-jackson" % poppetVersion,
"com.github.yakivy" %% "poppet-consumer-spring" % poppetVersion,
"com.fasterxml.jackson.module" %% "jackson-module-scala" % jacksonVersion
)
Create separate scala consumer generator (can be materialized once and shared everywhere):
import poppet.coder.jackson.all._
import poppet.consumer.spring.all._
object ConsumerGenerator {
def userService(restTemplate: RestTemplate): UserService = Consumer(
SpringClient(s"http://${providerHostName}:9001/api/service")(restTemplate))(
JacksonCoder())(
ConsumerProcessor[UserService].generate()
).materialize()
}
Enjoy :)
@RestController
public class UserController {
private UserService userService;
public UserController(UserService userService) {
this.userService = userService;
}
@RequestMapping("/api/user/{id}")
public User findById(@PathVariable("id") String id) {
return userService.findById(id);
}
}
Decorators
Let's say you want to add simple auth for generated RPC endpoints, you can easily do it with help of decorators. Decorator is an alias for scala function that receives poppet flow as an input and returns decorated flow of the same type.
Firstly we need to define header where we want to pass the secret and the secret by itself:
val authHeader = "auth"
val authSecret = "my-secret"
Then we can create client decorator that will add auth header in all client requests (example is for play-ws
consumer):
val consumerAuthDecorator = new Decorator[WSRequest, WSResponse, Future] {
override def apply(chain: WSRequest => Future[WSResponse]): WSRequest => Future[WSResponse] =
((_: WSRequest).addHttpHeaders(authHeader -> authSecret)).andThen(chain)
}
register it in the consumer:
Consumer(
PlayWsClient(providerUrl)(wsClient), List(authDecorator))(
PlayJsonCoder())(
ConsumerProcessor[UserService].generate()
).materialize()
And finally check if auth header is present in all server requests with server decorator:
val producerAuthDecorator = new Decorator[Request[ByteString], Result, Future] {
override def apply(chain: Request[ByteString] => Future[Result]): Request[ByteString] => Future[Result] =
((rq: Request[ByteString]) => {
if (!rq.headers.get(authHeader).contains(authSecret))
throw new IllegalArgumentException("Wrong secret!")
else rq
}).andThen(chain)
}
Provider(
PlayServer(cc), List(producerAuthDecorator))(
PlayJsonCoder())(
ProviderProcessor(helloService).generate()
).materialize()
Custom kinds
Out of the box library supports only server data kind as a service return kind (Future
for play, Id
for spring and so on). To return custom kind in a service you need to define coders (alias for implicit scala Function1
) from server kind to that kind. For example, to be able to return Id
kind from service that is being provided or consumed by play framework, you need coders from Future
to Id
:
implicit def futureCoderToIdCoder[A, B](
implicit coder: Coder[A, Future[B]]
): Coder[A, B] = a => Await.result(coder(a), Duration.Inf)
implicit def coderToLeftFutureCoder[A, B](
implicit coder: Coder[A, B]
): Coder[Future[A], B] = a => coder(Await.result(a, Duration.Inf))
more examples can be found in *CoderInstances
traits (for instance poppet.coder.play.instances.PlayJsonCoderInstances
)
Error handling
Development in progress...
Examples
- Play Framework: https://github.com/yakivy/poppet/tree/master/example/play
- run provider:
sbt "; project playProviderExample; run 9001"
- run consumer:
sbt "; project playConsumerExample; run 9002"
- put
http://localhost:9002/api/user/1
in the address bar
- run provider:
- Spring Framework: https://github.com/yakivy/poppet/tree/master/example/spring
- run provider:
sbt "; project springProviderExample; run"
- run consumer:
sbt "; project springConsumerExample; run"
- put
http://localhost:9002/api/user/1
in the address bar
- run provider:
Notes
Library is in active development and initial version is not completed yet.