I want to print data in xml format when department is "hr"but data in json format when department is "tech" .
Can we use
spray-json Support https://doc.akka.io/docs/akka-http/current/common/json-support.html and XML support https://doc.akka.io/docs/akka-http/current/common/xml-support.html together
private[rest] def route =
(pathPrefix("employee") & get) {
path(Segment) { id =>
parameters('department ? "") { (flag) =>
extractUri { uri =>
complete {
flag match {
case "hr": => {
HttpEntity(MediaTypes.`application/xml`.withCharset(HttpCharsets.`UTF-8`),"hr department")
}
case "tech" =>{
HttpEntity(ContentType(MediaTypes.`application/json`), mapper.writeValueAsString("tech department"))
}
}
}
}
}
}
}
Solution I tried
I tried the below by using by using JsonProtocols and ScalaXmlSupport I get the compile error expected ToResponseMarshallable but found Department
case class department(name:String)
private[rest] def route =
(pathPrefix("employee") & get) {
path(Segment) { id =>
parameters('department ? "") { (flag) =>
extractUri { uri =>
complete {
flag match {
case "hr": => {
complete(department(name =flag))
}
case "tech" =>{
complete(department(name =flag))
}
}
}
}
}
}
}
I think there are several issues you have to overcome to achieve what you want:
You want to customize response type basing on the request parameters. It means standard implicit-based marshaling will not work for you, you'll have to do some explicit steps
You want to marshal into an XML-string some business objects. Unfortunately, ScalaXmlSupport that you referenced does not support this, it can marshal only an XML-tree into a response. So you'll need some library that can do XML serialization. One option would be to use jackson-dataformat-xml with jackson-module-scala. It also means you'll have to write your own custom Marshaller. Luckily it is not that hard.
So here goes some simple code that might work for you:
import akka.http.scaladsl.marshalling.{ToResponseMarshallable, Marshaller}
// json marshalling
import akka.http.scaladsl.marshallers.sprayjson.SprayJsonSupport
import spray.json._
import spray.json.DefaultJsonProtocol._
implicit val departmentFormat = DefaultJsonProtocol.jsonFormat1(department)
val departmentJsonMarshaller = SprayJsonSupport.sprayJsonMarshaller[department]
// xml marshalling, need to write a custom Marshaller
// there are several MediaTypes for XML such as `application/xml` and `text/xml`, you'll have to choose the one you need.
val departmentXmlMarshaller = Marshaller.StringMarshaller.wrap(MediaTypes.`application/xml`)((d: department) => {
import com.fasterxml.jackson.dataformat.xml.XmlMapper
import com.fasterxml.jackson.module.scala.DefaultScalaModule
val mapper = new XmlMapper()
mapper.registerModule(DefaultScalaModule)
mapper.writeValueAsString(d)
})
private val route =
(pathPrefix("employee") & get) {
path(Segment) { id =>
parameters('department ? "") { (flag) =>
extractUri { uri => {
flag match {
case "hr" => {
// explicitly use the XML marshaller
complete(ToResponseMarshallable(department(name = flag))(departmentXmlMarshaller))
}
case "tech" => {
// explicitly use the JSON marshaller
complete(ToResponseMarshallable(department(name = flag))(departmentJsonMarshaller))
}
}
}
}
}
}
}
Note that for Jackson XML serializer to work correctly the department class should be a top level class or you'll get a cryptic error about bad root name.
Akka Http already has built in content type negotiation. Ideally you should just use that by having a marshaller that knows how to turn your department into either xml or json and having the client set the Accept header.
However it sounds like maybe you can't get your client to do that, so here's what you can do, assuming you have already made a ToEntityMarshaller[department] for both xml and json using ScalaXmlSupport and SprayJsonSupport.
val toXmlEntityMarshaller: ToEntityMarshaller[department] = ???
val toJsonEntityMarshaller: ToEntityMarshaller[department] = ???
implicit val departmentMarshaller = Marshaller.oneOf(toJsonEntityMarshaller, toXmlEntityMarshaller)
def route =
parameters("department") { departmentName =>
// capture the Accept header in case the client did request one
optionalHeaderValueByType[Accept] { maybeAcceptHeader =>
mapRequest ( _
.removeHeader(Accept.name)
// set the Accept header based on the department
.addHeader(maybeAcceptHeader.getOrElse(
Accept(departmentName match {
case "hr" ⇒ MediaTypes.`application/xml`
case "tech" ⇒ MediaTypes.`application/json`
})
))
) (
// none of our logic code is concerned with the response type
complete(department(departmentName))
)
}
}
Related
I'm struggling a bit with how to translate an imperative style to a functional style.
In a imperative web request I'm used to saying something like the following psudo code:
public Response controllerAction(Request request) {
val (req, parserErrors) = parser.parseRequest(request);
if (parserErrors.any()) {
return FourHundredError(parserErrors);
}
val businessErrors = model.validate(req);
if (businessErrors.any()){
return FourOhFour(businessErrors);
}
val (response, errorsWithOurStuff) = model.doBusinessLogicStuff(req);
if (errorsWithOurStuff.any()) {
return FiveHundredError(errorsWithOurStuff);
}
return OK(response)
}
I'm trying to translate this into a functional style using http4s.
def businessRoutes[F[_]: Sync](BL: BusinessLogic[F]): HttpRoutes[F] = {
val dsl = new Http4sDsl[F]{}
import dsl._
HttpRoutes.of[F] {
case req # POST -> Root / "sms" =>
for {
request <- req.as[BL.BuisnessRequest]
requestErrors <- BL.validateRequest(request)
response <- if (requestErrors.isEmpty) {
BL.processRequest(request) match {
case Failure(e) => InternalServerError(e)
case Success(response) => Ok(response)
}
} else {
BadRequest(requestErrors)
}
} yield response
}
}
The above code just looks... bad to me and I don't know how to make it better. My goal for this is to keep all the http style abstractions contained here, because I don't want to leak http4s or circe down into a business layer. I feel like I've got a for then an if, then a match and the responses are all jumbled together out of order. I'm writing hard to understand code here and I'm hoping some scala guru could show me how to clean this up and make this readable.
IMHO, the problem is rooted in the way you are modeling the data; mainly with validateRequest
Always remember, parse, don't validate.
Additionally, I would go the untyped errors route with a main handler like this:
import cats.syntax.all._
import io.circe.{Error => CirceError}
object model {
final case class RawRequest(...)
final case class BuisnessRequest(...)
final case class BuisnessResponse(...)
}
object errors {
// Depending on how you end up using those,
// it may be good to use scala.util.control.NoStackTrace with these.
// They may also be case classes to hold some context.
final case object ValidationError extends Throwable
final case object BusinessError extends Throwable
}
trait BusinessLogic {
def validateRequest(rawRequest: RawRequest): IO[BusinessRequets]
def processRequest(request: BusinessRequets): IO[BuisnessResponse]
}
final class HttpLayer(bl: BusinessLogic) extends Http4sDsl[IO] {
private final val errorHanlder: PartialFunction[Throwable, IO[Response[IO]] = {
case circeError: CirceError =>
BadRequest(...)
case ValidationError =>
NotFound(...)
case BusinessError =>
InternalServerError(...)
}
val routes: HttpRoutes[IO] = HttpRoutes[F] {
case req # POST -> Root / "sms" =>
req
.as[RawRequest] // This may fail with CirceError.
.flatMap(bl.validateRequest) // This may fail with ValidationError.
.flatMap(bl.processRequest) // This may fail with BusinessError.
.redeemWith(recover = errorHandler, response => Ok(response))
}
}
I used concrete IO here for simplicity, you may use F[_] if you please.
Let me start by saying that i am very new to akka-http, none of the books i have covered the marsheling topic well. So it is bit of a blackbox for me. I was able to obtain the following (Un)Marsheller which is capable of returning both json and protobuf based on a request header.
This part of the code works fine and i have a get route defined in akka-http and it works fine.
trait PBMarshaller {
private val protobufContentType = ContentType(MediaType.applicationBinary("octet-stream", Compressible, "proto"))
private val applicationJsonContentType = ContentTypes.`application/json`
implicit def PBFromRequestUnmarshaller[T <: GeneratedMessage with Message[T]](companion: GeneratedMessageCompanion[T]): FromEntityUnmarshaller[T] = {
Unmarshaller.withMaterializer[HttpEntity, T](_ => implicit mat => {
case entity#HttpEntity.Strict(`applicationJsonContentType`, data) =>
val charBuffer = Unmarshaller.bestUnmarshallingCharsetFor(entity)
FastFuture.successful(JsonFormat.fromJsonString(data.decodeString(charBuffer.nioCharset().name()))(companion))
case entity#HttpEntity.Strict(`protobufContentType`, data) =>
FastFuture.successful(companion.parseFrom(CodedInputStream.newInstance(data.asByteBuffer)))
case entity =>
Future.failed(UnsupportedContentTypeException(applicationJsonContentType, protobufContentType))
})
}
implicit def PBToEntityMarshaller[T <: GeneratedMessage]: ToEntityMarshaller[T] = {
def jsonMarshaller(): ToEntityMarshaller[T] = {
val contentType = applicationJsonContentType
Marshaller.withFixedContentType(contentType) { value =>
HttpEntity(contentType, JsonFormat.toJsonString(value))
}
}
def protobufMarshaller(): ToEntityMarshaller[T] = {
Marshaller.withFixedContentType(protobufContentType) { value =>
HttpEntity(protobufContentType, value.toByteArray)
}
}
Marshaller.oneOf(protobufMarshaller(), jsonMarshaller())
}
}
the issue i am facing is on the post route.
(post & entity(as[PropertyEntity])) { propertyEntity =>
complete {
saveProperty(propertyEntity)
}
}
During compilation time, i get the following error
Error:(20, 24) could not find implicit value for parameter um: akka.http.scaladsl.unmarshalling.FromRequestUnmarshaller[PropertyEntity]
(post & entity(as[PropertyEntity])) { propertyEntity =>
I am not sure exactly what i am missing. Do i need to define an implicit FromRequestUnmarshaller ? if so what should it have?
i was able to hack something together that works for the moment, but i still don't know how to create a general Unmarshaller that can decode any ScalaPB case class
implicit val um:Unmarshaller[HttpEntity, PropertyEntity] = {
Unmarshaller.byteStringUnmarshaller.mapWithCharset { (data, charset) =>
val charBuffer = Unmarshaller.bestUnmarshallingCharsetFor(data)
JsonFormat.fromJsonString(data.decodeString(charBuffer.nioCharset().name()))(PropertyEntity)
/*PropertyEntity.parseFrom(CodedInputStream.newInstance(data.asByteBuffer))*/
}
}
even this i don't know how to have both decoders enabled at the same time. so i have commented one out.
The following sample code uses Play Framework to parse JSON to an object:
def createEvent(): Action[JsValue] = Action.async(parse.tolerantJson) {
request => {
request.body.validate[SomeEvent] match {
case o:JsSuccess[SomeEvent] => {
//do something
Future(Created)
}
}
}
}
Is it possible to generalise it so it can handle different event types? e.g.
def createEvent(): Action[JsValue] = Action.async(parse.tolerantJson) {
request => {
val eventType = request.contentType match {
case Some("SomeEventType") => SomeEvent
case Some("OtherEventType") => OtherEvent
}
request.body.validate[eventType] match {
case o:JsSuccess[eventType] => {
//do something
Future(Created)
}
}
}
}
Currently, the above code will fail in the line request.body.validate[eventType]
You can extract body.validate[T] into a function and call it from your patten matching construct with a proper type, i.e:
def extract[T: JsonFormat](implicit req: Request[AnyContent]) = req.body.valudate[T]
request.contentType match {
case Some("SomeEventType") => extract[SomeEvent]
case Some("OtherEventType") => extract[OtherEvent]
}
You can read and create class from contentType dynamically. But you can have problem, if will be no implicit Format for extracted type in scope:
error: No Json formatter found for type A. Try to implement an implicit Format for this type.
or
java.lang.ClassNotFoundException: models.Bazz
The available data:
model
package models
case class Bar(name: String)
request
request.contentType: Option[String] = Some("models.Bar")
incoming body
request.body: JsValue = """{"name": "bar name"}"""
format[Bar]
implicit val BarFormat = Json.format[models.Bar]
extractor:
def extract[A <: Any](ct: String, json: JsValue)(implicit format: Format[A]) = {
val manifest:Manifest[A] = Manifest.classType[A](Class.forName(ct))
json.validate[A]
}
request.contentType.map(extract(_, request.body))
res1: Option[play.api.libs.json.JsResult[models.Bar]] = Some(JsSuccess(Bar(bar name),/name))
you can see https://github.com/strongtyped/fun-cqrs/blob/demos/shop-sample/samples/shop/src/main/scala/funcqrs/json/TypedJson.scala for other way of deserializing self-contained json.
Appears I am missing something but here is what I have got (posting only relevant piece). where MyService.save returns Future[Option[MyCompany] ].
def myPost = Action.async(parse.json) { request =>
val mn = Json.fromJson[MyEntity](request.body)
mn.map{
case m : MyEntity => MyService.save(m).map{f=>
f.map(mm=> Ok(mm ) )
}
}.recoverTotal {
e => Future { BadRequest("Detected error:" + JsError.toFlatJson(e)) }
}
}
Although I have defined
implicit val companyWriter: Writes[MyCompany] = (...)
And this implicit is in the scope, it shows compile error
Cannot write an instance of MyCompany to HTTP response. Try to define
a Writeable[MyCompany]
FYI: This writer is used elsewhere where I do Json.toJson(myCompany) and over there it finds and works fine.
Anything in particular to async Ok that it's missing?
EDIT
It appears that Ok() method cannot figure out the MyCompany needs to be transformed to json. following seems to have worked.
Ok(Json.toJson(mm) )
Is this because arguments to Ok can vary? Separately there are too many "map" in the above code. Any recommendation for improvement and making it more concise ?
Your compiler error is about a Writeable, not a Writes. Writeables are used to convert whatever you have to something that can be written to an HTTP response, Writes are used to marshall objects to JSON. The names can be a little confusing.
As for style...
def myPost = Action.async(parse.json) { request =>
request.body.validate[MyEntity] map { myEntity =>
MyService.save(myEntity).map { maybeCompany =>
maybeCompany match {
case Some(company) => Ok(Json.toJson(company))
case None => NoContent // or whatever's appropriate
}
}
} recoverTotal { t =>
Future { BadRequest("Detected error: " + JsError.toFlatJson(e)) }
}
}
I would like to support a couple of different content types submitted to the same URL:
e.g:
application/x-www-form-urlencoded, multipart/form-data, application/json
I would like to do something like:
post {
contentType(`application/x-www-form-urlencoded`) |
contentType(`multipart/form-data`) {
// user POSTed a form
entity(as[MyCaseClass]) { data =>
complete { data.result }
}
} ~ contentType(`application/json`) {
// user POSTed a JSON object
entity(as[MyCaseClass]) { data =>
complete { data.result }
}
}
}
I think there may be some way to do this with custom marshaling and unmarshaling, but I only need it in one or two spots in my service and this seems pretty simple. Can someone help?
There is a really elegant way to achieve this thanks to deep cleverness in the Spray marshalling system. The code (gist) illustrates this:
case class User(name: String, no: Int)
// Json marshaller
object UnMarshalling extends DefaultJsonProtocol {
val jsonUser = jsonFormat2(User)
val textUser = Unmarshaller[User](`text/plain`) {
case HttpEntity.NonEmpty(contentType, data) =>
val res = data.asString.drop(5).dropRight(1).split(',')
User(res(0),res(1).toInt)
}
implicit val userMarshal = Unmarshaller.oneOf(jsonUser, textUser)
}
class UnMarshalTest extends FunSpec with ScalatestRouteTest with Matchers {
import UnMarshalling._
// Marshals response according to the Accept header media type
val putOrder = path("user") {
put {
// Namespace clash with ScalaTestRoutes.entity
MarshallingDirectives.entity(as[User]) {
user =>
complete(s"no=${user.no}")
}
}
}
describe("Our route should") {
val json = """ {"name" : "bender", "no" : 1234} """
it("submit a json") {
Put("/user", HttpEntity(`application/json`,json)) ~> putOrder ~> check {
responseAs[String] should equal("no=1234")
}
}
it("Submit text") {
Put("/user", HttpEntity(`text/plain`,"""User(Zoidberg,322)""")) ~> putOrder ~> check {
responseAs[String] should equal("no=322")
}
}
}
}