Generic REST API with Akka HTTP? - scala

Let's say you have 100 models, and for all of them you want the client to be able to do GET/POST/PUT/DELETE.
I read this article and a few others, but even with the promising title it seems that a lot of code would have to be duplicated for each model. Boilerplate, as some would call it.
Ideally I want to be able to say something like
case class AwesomeCaseClass(primeNumber : Long)
case class EvenMoreAwesomeCaseClass(favoritePrimeNumber : Long)
and then specify either all commands that should be accepted for each class (or maybe things like UPSERT).
I cannot find anyone who has done this, can anyone provide a link or a pointer in the right direction?
When Googling I also came across this, but for Akka HTTP in particular there seemed to only be support for API documentation.
Here is my attempt so far:
package route
import akka.actor.ActorSystem
import akka.http.scaladsl.Http
import akka.http.scaladsl.model.HttpResponse
import akka.http.scaladsl.server.Route
import akka.stream.ActorMaterializer
import scala.reflect._
import akka.http.scaladsl.server.Directives._
import akka.http.scaladsl.unmarshalling.FromRequestUnmarshaller
/**
* Created by flyrev on 30.05.16.
*/
case class Test(name: String)
class REST[T](implicit um: FromRequestUnmarshaller[T]) {
def generateRoute(klazz: Class[_]) : Route = {
path("test" / klazz.getSimpleName) {
get {
complete(HttpResponse(entity = "OK", status = 200))
} ~ post {
entity(as[T]) {
// Insert into DB here
(zomg) => complete(HttpResponse(entity = "Would have POSTed that stuff", status = 200))
}
}
} ~ get {
complete(HttpResponse(entity="Not found", status=404))
}
}
}
object GenerateRouteFromClass extends App {
def runtimeClass[T: ClassTag] = classTag[T].runtimeClass
implicit val system = ActorSystem()
implicit val materializer = ActorMaterializer()
import argonaut._
import CodecJson.derive
implicit val um = derive[Test]
Http().bindAndHandle(new REST[Test].generateRoute(runtimeClass[Test]), "localhost", 8080)
}
However, this gives:
Error:(45, 24) could not find implicit value for parameter um: akka.http.scaladsl.unmarshalling.FromRequestUnmarshaller[route.Test]
Http().bindAndHandle(new REST[Test].generateRoute(runtimeClass[Test]), "localhost", 8080)
^
Error:(45, 24) not enough arguments for constructor REST: (implicit um: akka.http.scaladsl.unmarshalling.FromRequestUnmarshaller[route.Test])route.REST[route.Test].
Unspecified value parameter um.
Http().bindAndHandle(new REST[Test].generateRoute(runtimeClass[Test]), "localhost", 8080)
^

Related

Scala compiler can't find the unmarshalling implicits in route declaration

I'm trying to build a REST server using this tutorial:
https://spindance.com/reactive-rest-services-akka-http/
However, having reached the "Responding with JSON" section, I've noticed that my code doesn't compile, and I can't make POST requests. This is the error that I'm getting:
Error:(59, 18) could not find implicit value for parameter um: akka.http.scaladsl.unmarshalling.FromRequestUnmarshaller[Health]
entity(as[Health]) { statusReport =>
^
Having looked at other tutorials, I've found out that you need to include an object containing an implicit variable for the class that I'm trying to unmarshall. I did that, and I even imported the httpx library, but I'm still getting this error. My code is given below.
import akka.actor.ActorSystem
import akka.http.scaladsl.Http
import akka.http.scaladsl.model.StatusCodes
import akka.http.scaladsl.server.Directives._
import akka.http.scaladsl.server.Route
import akka.stream.ActorMaterializer
import akka.pattern.ask
import akka.util.Timeout
import spray.json._
import DefaultJsonProtocol._
import spray.httpx.SprayJsonSupport.sprayJsonUnmarshaller
import scala.concurrent.duration._
import scala.io.StdIn
object JsonImplicits extends DefaultJsonProtocol {
implicit val healthFormat = jsonFormat2(Health)
}
object MyApplication {
val host = "localhost"
val port = 8080
def main(args: Array[String]): Unit = {
implicit val system = ActorSystem("simple-rest-system")
// Something to do with flows
implicit val materializer = ActorMaterializer()
// A reference to a specific thread pool
// You can configure thread pool options through it
// It is the engine that executes the actors
implicit val executionContext = system.dispatcher
val requestHandler = system.actorOf(RequestHandler.props(), "requestHandler")
//Define the route
val route : Route = {
implicit val timeout = Timeout(20 seconds)
import JsonImplicits._
import spray.httpx.SprayJsonSupport._
path("health") {
get {
onSuccess(requestHandler ? GetHealthRequest) {
case response: HealthResponse =>
complete(StatusCodes.OK, s"Everything is ${response.health.status}!")
case _ =>
complete(StatusCodes.InternalServerError)
}
}
} ~ post {
// Entity extracts the body of the POST request and then converts it into a
// Health object
entity(as[Health]) { statusReport =>
onSuccess(requestHandler ? SetStatusRequest(statusReport)) {
case response: HealthResponse =>
complete(StatusCodes.OK,s"Posted health as ${response.health.status}!")
case _ =>
complete(StatusCodes.InternalServerError)
}
}
}
}
//Start up and listen for requests
val bindingFuture = Http().bindAndHandle(route, host, port)
println(s"Waiting for requests at http://$host:$port/...\nHit RETURN to terminate")
StdIn.readLine()
//Shutdown
bindingFuture.flatMap(_.unbind())
system.terminate()
}
}

play-json - Could not find implicit value for parameter um

I know this question has been asked many times before, however all of them use spray-json. I wanted to use play-json. This might be why the proposed solutions don't solve the problem.
The trait with the route is in a separate file named RestApi.scala. The Http.bindAndHandle which makes use of it is in the file named Main.scala. Both files without irrelevant element removed are presented below. If you want to see the whole file please click at the links above.
RestApi.scala
package restApi
import akka.actor.{ActorSystem, Props}
import akka.http.scaladsl.model.StatusCodes
import akka.http.scaladsl.server.Directives._
import akka.pattern.ask
import akka.stream.ActorMaterializer
import akka.util.Timeout
import scala.concurrent.ExecutionContext
import scala.concurrent.duration._
trait RestApi {
import models._
import cassandraDB.{WriterActor, ReaderActor}
implicit val system: ActorSystem
implicit val materializer: ActorMaterializer
implicit val ec: ExecutionContext
implicit val timeout = Timeout(20 seconds)
val cassandraWriterWorker = system.actorOf(Props[WriterActor], "cassandra-writer-actor")
val cassandraReaderWorker = system.actorOf(Props[ReaderActor], "cassandra-reader-actor")
val route =
pathPrefix("api") {
pathSuffix("contact") {
// the line below has the error
(post & entity(as[Contact])) { contact =>
complete {
cassandraWriterWorker ! contact
StatusCodes.OK
}
}
} ~
pathSuffix("gps"/ "log") {
// an analogous error message is shown in the line below
(post & entity(as[GpsLog])) { gpsLog =>
complete {
cassandraWriterWorker ! gpsLog
StatusCodes.OK
}
}
}
}
}
Main.scala
package initialization
import akka.actor.{ActorSystem, Props}
import akka.http.scaladsl.Http
import akka.stream.ActorMaterializer
import cassandraDB.{ConfigCassandraCluster, ReaderActor, WriterActor}
import gcm.GcmServer
import restApi.RestApi
object Main extends App with ConfigCassandraCluster with RestApi {
override implicit val system = ActorSystem()
override implicit val materializer = ActorMaterializer()
override implicit val ec = system.dispatcher
val write = system.actorOf(Props(new WriterActor(cluster)))
val read = system.actorOf(Props(new ReaderActor(cluster)))
val gcmServer = system.actorOf(Props(new GcmServer(11054...,"AIzaSyCOnVK...")), "gcm-server")
val bindingFuture = Http().bindAndHandle(route, "localhost", 8080)
}
I point out the line where the error occurs in RestApi.scala
Error Message:
Error:(31, 26) could not find implicit value for parameter um: akka.http.scaladsl.unmarshalling.FromRequestUnmarshaller[models.Contact]
(post & entity(as[Contact])) { contact =>
^
The Contact is a data model defined in Contact.scala.
I hope this isn't a duplicate, but I really don't think so. Any help is much appreciated.
It turns out there is an easy solution. Simply add resolvers += Resolver.bintrayRepo("hseeberger", "maven") to your build file and then "de.heikoseeberger" %% "akka-http-play-json" % "1.5.3" to your libraryDependencies and import import de.heikoseeberger.akkahttpplayjson.PlayJsonSupport._ into your code. You can now use play-json like you would use spray-json.

could not find implicit ...: akka.http.server.RoutingSetup

While playing with, akka-http experimental 1.0-M2 I am trying to create a simple Hello world example.
import akka.actor.ActorSystem
import akka.http.Http
import akka.http.model.HttpResponse
import akka.http.server.Route
import akka.stream.FlowMaterializer
import akka.http.server.Directives._
object Server extends App {
val host = "127.0.0.1"
val port = "8080"
implicit val system = ActorSystem("my-testing-system")
implicit val fm = FlowMaterializer()
val serverBinding = Http(system).bind(interface = host, port = port)
serverBinding.connections.foreach { connection ⇒
println("Accepted new connection from: " + connection.remoteAddress)
connection handleWith Route.handlerFlow {
path("") {
get {
complete(HttpResponse(entity = "Hello world?"))
}
}
}
}
}
Compilation fails with could not find implicit value for parameter setup: akka.http.server.RoutingSetup
Also, if I change
complete(HttpResponse(entity = "Hello world?"))
with
complete("Hello world?")
I get another error: type mismatch; found : String("Hello world?") required: akka.http.marshalling.ToResponseMarshallable
With research I was able to understand the issue to be lack of Execution Context. To solve both the issue I needed to include this:
implicit val executionContext = system.dispatcher
Looking into akka/http/marshalling/ToResponseMarshallable.scala I see ToResponseMarshallable.apply requires it which returns a Future[HttpResponse].
Also, in akka/http/server/RoutingSetup.scala, RoutingSetup.apply needs it.
May be akka team just needs to add some more #implicitNotFounds. I was able to find not exact but related answer at: direct use of Futures in Akka and spray Marshaller for futures not in implicit scope after upgrading to spray 1.2
Well found - this problem still exists with Akka HTTP 1.0-RC2, so the code for that now must look like this (given their API changes):
import akka.actor.ActorSystem
import akka.http.scaladsl.server._
import akka.http.scaladsl._
import akka.stream.ActorFlowMaterializer
import akka.stream.scaladsl.{Sink, Source}
import akka.http.scaladsl.model.HttpResponse
import Directives._
import scala.concurrent.Future
object BootWithRouting extends App {
val host = "127.0.0.1"
val port = 8080
implicit val system = ActorSystem("my-testing-system")
implicit val fm = ActorFlowMaterializer()
implicit val executionContext = system.dispatcher
val serverSource: Source[Http.IncomingConnection, Future[Http.ServerBinding]] =
Http(system).bind(interface = host, port = port)
serverSource.to(Sink.foreach {
connection =>
println("Accepted new connection from: " + connection.remoteAddress)
connection handleWith Route.handlerFlow {
path("") {
get {
complete(HttpResponse(entity = "Hello world?"))
}
}
}
}).run()
}

Implicits getting out of scope in Spray example code: what's happening here?

I have copied Spray Client's example code into my own project, to have it easily available. I use IntelliJ 13.
Here is the code I have:
package mypackage
import scala.util.Success
import scala.concurrent.duration._
import akka.actor.ActorSystem
import akka.pattern.ask
import akka.event.Logging
import akka.io.IO
import spray.json.{JsonFormat, DefaultJsonProtocol}
import spray.can.Http
import spray.util._
import spray.client.pipelining._
import scala.util.Success
import scala.util.Failure
case class Elevation(location: Location, elevation: Double)
case class Location(lat: Double, lng: Double)
case class GoogleApiResult[T](status: String, results: List[T])
object ElevationJsonProtocol extends DefaultJsonProtocol {
implicit val locationFormat = jsonFormat2(Location)
implicit val elevationFormat = jsonFormat2(Elevation)
implicit def googleApiResultFormat[T :JsonFormat] = jsonFormat2(GoogleApiResult.apply[T])
}
object SprayExample extends App {
// we need an ActorSystem to host our application in
implicit val system = ActorSystem("simple-spray-client")
import system.dispatcher // execution context for futures below
val log = Logging(system, getClass)
log.info("Requesting the elevation of Mt. Everest from Googles Elevation API...")
val pipeline = sendReceive ~> unmarshal[GoogleApiResult[Elevation]]
val responseFuture = pipeline {
Get("http://maps.googleapis.com/maps/api/elevation/json?locations=27.988056,86.925278&sensor=false")
}
responseFuture onComplete {
case Success(GoogleApiResult(_, Elevation(_, elevation) :: _)) =>
log.info("The elevation of Mt. Everest is: {} m", elevation)
shutdown()
case Success(somethingUnexpected) =>
log.warning("The Google API call was successful but returned something unexpected: '{}'.", somethingUnexpected)
shutdown()
case Failure(error) =>
log.error(error, "Couldn't get elevation")
shutdown()
}
def shutdown(): Unit = {
IO(Http).ask(Http.CloseAll)(1.second).await
system.shutdown()
}
}
As it stands, this works perfectly and it prints the height of Mt.Everest as expected.
The strange thing happens if I move the file down one level in the package structure, that is I create a mypackage.myinnerpackage and move the file inside it.
IDEA changes my first line of code into package mypackage.myinnerpackage and that's it.
Then I try to run the app and the compilation will fail with the following message:
could not find implicit value for evidence parameter of type spray.httpx.unmarshalling.FromResponseUnmarshaller[courserahelper.sprayexamples.GoogleApiResult[courserahelper.sprayexamples.Elevation]]
val pipeline = sendReceive ~> unmarshal[GoogleApiResult[Elevation]]
^
I did not change anything in the code, I effectively just changed the package! Additionally, this code is self contained, it does not rely on any other implicit I declared in any other part of my code....
What am I missing?
Thanks!
(Replaced the comment by this answer which supports proper formatting.)
The code you posted is missing these two imports before the usage of unmarshal:
import ElevationJsonProtocol._
import SprayJsonSupport._
val pipeline = sendReceive ~> unmarshal[GoogleApiResult[Elevation]]
which exist in the original code. IntelliJ is sometimes messing with imports so that may be the reason they got lost in the transition.
You need to provide a Json Formatter for your case class.
case class Foo(whatever: Option[String])
object FooProtocol extends DefaultJsonProtocol {
implicit val fooJsonFormat = jsonFormat1(Foo)
}
Then include the following near the implementation...
import SprayJsonSupport._
import co.xxx.FooProtocol._

spray-json cannot marshal Map[String,String]

I have the following route setup, but when my map is returned in the first complete block I get an error:
could not find implicit value for evidence parameter of type spray.httpx.marshalling.Marshaller[scala.collection.immutable.Map[String,String]]
import spray.routing.HttpService
import akka.actor.Actor
import spray.http.HttpRequest
import spray.routing.RequestContext
import spray.json.DefaultJsonProtocol._
class UserServiceActor extends Actor with RestUserService {
def actorRefFactory = context
def receive = runRoute(linkRoute)
}
trait RestUserService extends HttpService {
val userService = new LinkUserService
def linkRoute =
pathPrefix("user" / Segment) {
userId =>
path("link") {
parameters('service ! "YT") {
complete {
Map("status"-> "OK", "auth_url" -> "http://mydomain.com/auth")
}
}
}
}
}
According to this test I should be able to convert a Map to json when DefaultJsonProtocol._ is imported but even that's failing:
val map:Map[String, String] = Map("hi"->"bye")
map.toJson
Cannot find JsonWriter or JsonFormat type class for scala.collection.mutable.Map[String,String]
Not sure what's wrong :(
Someone on the spray mailing list pointed out that the Map being created was a mutable one, spray-json won't marshal that. I changed it to be scala.collection.immutable.Map and also added the following imports:
import spray.httpx.SprayJsonSupport._
import spray.json.DefaultJsonProtocol._
And now everything works great.