Make JSON Parsing & Error Handling More Functional in Scala - scala

I have the following piece of code that I use to read the incoming JSON which looks like this:
{
"messageTypeId": 2,
"messageId": "19223201",
"BootNotification" :
{
"reason": "PowerUp",
"chargingStation": {
"serialNumber" : "12345",
"model" : "",
"vendorName" : "",
"firmwareVersion" : "",
"modem": {
"iccid": "",
"imsi": ""
}
}
}
}
I have the following reads using play-json that would process this JSON:
implicit val ocppCallRequestReads: Reads[OCPPCallRequest] = Reads { jsValue =>
val messageTypeId = (jsValue \ 0).toOption
val messageId = (jsValue \ 1).toOption
val actionName = (jsValue \ 2).toOption
val payload = (jsValue \ 3).toOption
(messageTypeId.zip(messageId.zip(actionName.zip(payload)))) match {
case Some(_) => JsSuccess(
OCPPCallRequest( // Here I know all 4 exists, so safe to call head
messageTypeId.head.as[Int],
messageId.head.as[String],
actionName.head.as[String],
payload.head
)
)
case None => JsError( // Here, I know that I have to send a CallError back!
"ERROR OCCURRED" // TODO: Work on this!
)
}
}
It is not playing nicely when it comes to delivering the exact error message for the case None. It is all in or nothing, but what I want to avoid is that in my case None block, I would like to avoid looking into each of the Option and populate the corresponding error message. Any ideas on how I could make it more functional?

Related

Akka Http route test with formFields causing UnsupportedRequestContentTypeRejection

I have a GET request with parameters and a formField.
It works when I use a client like Insomnia/Postman to send the req.
But the route test below fails with the error:
UnsupportedRequestContentTypeRejection(Set(application/x-www-form-urlencoded, multipart/form-data))
(Rejection created by unmarshallers. Signals that the request was rejected because the requests content-type is unsupported.)
I have tried everything I can think of to fix it but it still returns the same error.
It is the formField that causes the problem, for some reason when called by the test it doesnt like the headers.
Is it something to do with withEntity ?
Code:
path("myurl" ) {
get {
extractRequest { request =>
parameters('var1.as[String], 'var2.as[String], 'var3.as[String], 'var4.as[String]) { (var1, var2, var3, var4) =>
formField('sessionid.as[String]) { (sessionid) =>
complete {
request.headers.foreach(a => println("h=" + a))
}
}
}
}
}
}
Test:
// TESTED WITH THIS - Fails with UnsupportedRequestContentTypeRejection(Set(application/x-www-form-urlencoded, multipart/form-data))
class GETTest extends FreeSpec with Matchers with ScalatestRouteTest {
val get = HttpRequest(HttpMethods.GET, uri = "/myurl?var1=456&var2=123&var3=789&var4=987")
.withEntity("sessionid:1234567890")
.withHeaders(
scala.collection.immutable.Seq(
RawHeader("Content-Type", "application/x-www-form-urlencoded"), // same problem if I comment out these 2 Content-Type lines
RawHeader("Content-Type", "multipart/form-data"),
RawHeader("Accept", "Application/JSON")
)
)
get ~> route ~> check {
status should equal(StatusCodes.OK)
}
The exception is thrown before the formField line.
Full exception:
ScalaTestFailureLocation: akka.http.scaladsl.testkit.RouteTest$$anonfun$check$1 at (RouteTest.scala:57)
org.scalatest.exceptions.TestFailedException: Request was rejected with rejection UnsupportedRequestContentTypeRejection(Set(application/x-www-form-urlencoded, multipart/form-data))
at akka.http.scaladsl.testkit.TestFrameworkInterface$Scalatest$class.failTest(TestFrameworkInterface.scala:24)
}
You could either use:
val get = HttpRequest(HttpMethods.GET, uri = "/myurl?var1=456&var2=123&var3=789&var4=987", entity = FormData("sessionid" -> "1234567.890").toEntity)
or
val get = Get("/myurl?var1=456&var2=123&var3=789&var4=987", FormData("sessionid" -> "1234567.890"))

How do you post form data using pytest?

I'm trying to write a unit test that posts form data. The actual line in question is:
def test_create_request():
with app.test_client() as test_client:
app_url = '/requests/'
with app.app_context():
new_request = get_new_request()
form_data = json.dumps(new_request, default=str)
print('FORM DATA: ', form_data)
resp = test_client.post(app_url, data=form_data, headers={'Content-Type': 'application/json'})
assert resp.status_code == 200
s = json.loads(resp.data)
assert s['success'] == True
Where new_request is a dict representation of an object. The print statement yields (I've formatted it a bit):
FORM DATA: {
"request_id": "6",
"state_id": 1,
"orig_project_id": "1",
"orig_project_code": "QQQ",
"orig_allocated_funding": "123.45",
"orig_ytd_spend": "123.45",
"orig_ytd_commit": "123.45",
"orig_ytd_ocnr": "123.45",
"new_project_id": 2,
"new_funding_amount": 123.45,
"new_ytd_spend": 123.45,
"new_ytd_commit": 123.45,
"new_ytd_ocnr": 123.45,
"plan": "this is the plan",
"reason": "this is the reason",
"sm_director": "sm.dir#example.com",
"submitted_by": "xfgbn#vexample.com",
"created_on": "2021-09-14 16:32:55",
"meets_approval_guidelines": null
}
In the flask form, most fields are required. When I try to post the data, the form.validate_on_submit() function in the view's route returns False, and the 2nd assert fails. WTForms claims that none of the required orig_allocated_funding, orig_ytd_spend, orig_ytd_commit, orig_ytd_ocnr, new_project_id, new_funding_amount, new_ytd_spend, new_ytd_commit, new_ytd_ocnr, reason, plan, sm_director, or submitted_by fields are supplied.
I've read several tutorials, and I can't see what I'm doing wrong. Can anyone help?
What I was able to make work was scraping form_data altogether:
def test_create_request():
with app.test_client() as test_client:
app_url = '/requests/'
with app.app_context():
new_request = get_new_request()
resp = test_client.post(app_url, data=new_request)
assert resp.status_code == 200
s = json.loads(resp.data)
print(s['html'])
assert s['success'] == True

akka-http HttpEntity.toStrict timed out while still waiting for outstanding data

I encounter a problem randomly without SSL(sometimes it works without issue), and each time with SSL, and I don't understand why.
It's a time out during HttpEntity.toStrict.
extractRequest{payload =>
val futureRequestEntity = payload.entity.toStrict(5.seconds).map(_.data.utf8String)
val requestEntity = Await.result(futureRequestEntity, 10.seconds)
... }
I tried to set longer waiting times, but doesn't solve the issue.
java.util.concurrent.TimeoutException: HttpEntity.toStrict timed out after 5 seconds while still waiting for outstanding data
at akka.http.impl.util.ToStrict$$anon$1.onTimer(package.scala:138)
I need to use extractRequest for my path because I extract the entity and the header further in the code.
Route code :
options {
corsHandler.corsHandler(complete(StatusCodes.OK))
} ~ post {
path("recommandation" / Segment / Segment / "suggestion") {(docType,docId) =>
extractRequest{payload =>
writeLog("info",s"/recommandation : Post request for $docType suggestion : $docId ")
val coBranding = payload.headers.filter(x => x.is("cobrandingcontext")).map(x => x.value()).head
val futureRequestEntity = payload.entity.toStrict(5.seconds).map(_.data.utf8String)
val requestEntity = Await.result(futureRequestEntity, 10.seconds)
val parsedPayload = suggestionEngine.payloadParser.suggestionEnginePayloadParser(requestEntity,
coBranding,docType, docId)
if (parsedPayload.isDefined){
val suggestionResult = suggestionEngine.suggestionWorker.suggestionPipeline(parsedPayload.get)
val suggestionResponse = suggestionEngine.responseHandler.responseBuilder(suggestionResult).get
complete(200,List(`Content-Type`(`application/json`)),suggestionResponse)
} else {
writeLog("error","/recommandation : undefined payload")
complete(StatusCodes.BadRequest,List(`Content-Type`(`application/json`)),"Undefined payload")
}
}
}
}
Request example :
curl -i https://mydns.com:443/recommandation/products/59ad73be20a35d3fa47c80c8/suggestion -H
'cobrandingcontext: branding' -H 'Content-Type: application/json;charset=UTF-8' -X POST -d '{"collection":"products",
"query":{"category_parent":"category-ex","category_child":"category-child-ex","dimensions.width":{"$lt":70,"$gt":0},
"dimensions":{"$lt":40,"$gt":-40},"status":true,"structured":true,"visibilities":"architects-3d-btoc"},"configuration":{"type":"nearest-neighbors","metric":"cosine","features":["styles"]}}'
Thank's in advance
I think the explanation is that the use of Await will block a dispatcher thread which can lead to thread starvation.
The solution I found is to nest directives :
options {
corsHandler.corsHandler(complete(StatusCodes.OK))
} ~ post {
path("/"){
entity(as[String]){payload =>
headerValueByName("context"){ contextValue => {
...// work with payload and contextValue
}
}
}
}
}

Scala/Spray/Akka unable to leverage mapRequest

I am new to Scala/Spray/AKKA so please excuse this dumb requests.
I have the following Directive and it is being called as the first
logger line ("inside") is showing up in logs.
However, anything inside mapRequest{} is skipped over. The logging line ("headers:") isn't showing up
private def directiveToGetHeaders(input: String) : Directive0 = {
logger.info("inside")
mapRequest { request =>
val headList: List[HttpHeader] = request.headers
logger.info("headers: " + headList.size)
request
}
}
I am not sure what I did wrong. My goal is to pull out all the HTTP headers. Any tip/pointer much appreciated. Thanks
-v
You can use extractRequest directive for getting headers.
private def directiveToGetHeaders(input: String) : Directive0 = {
logger.info("inside")
extractRequest { request =>
val headList: Seq[HttpHeader] = request.headers
logger.info("headers: " + headList.size)
complete(HttpResponse())
}
}

How to check for 200 OK response status using Scala and Play Framework

I have a following Actor class that is responsible for sending a JSON message to a URL using POST.
import play.api.libs.ws._
class Worker extends Actor {
val logger: Logger = Logger("superman")
val supermanURL = "http://localhost:9000/superman/send"
def receive = {
case message: JsValue => {
val transactionID = (message \ "transactionID").get
println("Received JSON Object =>" + message)
val responseFromSuperman = WS.url(supermanURL).withHeaders("Content-Type" -> "application/json").post(message)
responseFromSuperman.map(
result => {
//TODO: Make sure to only log if response status is 200 OK
logger.info("""Message="ACK received from Superman" for transactionID=""" + transactionID)}
).recover { case error: Throwable =>
logger.error("""Message="NACK received from Superman" for transactionID=""" + transactionID) + " errorMessage:" + error.getLocalizedMessage()
}
}
}
}
So, if you look into my TODO above, I would like to add a check for a response type 200 OK. The current implementation is not doing that and it logs the message even if I manually send in a BadRequest. I tried checking for result.allHeaders which returns:
Map(Date -> Buffer(Wed, 27 Jan 2016 21:45:31 GMT), Content-Type -> Buffer(text/plain; charset=utf-8), Content-Length -> Buffer(7))
but no information about response status 200 OK
Simply:
import play.api.http.Status
if(result.status == Status.OK) {
// ...
}
Maybe I am missing here something but you have "status" on the response.
So you can do:
WS.url(url).withHeaders("Content-Type" -> "application/json").post(message).map{
case response if ( response.status == OK) => //DO SOMETHING?
}