我有一个接收JSON主体的端点。我有这种JSON格式的隐式读写。在端点中,我对JSON进行了验证并对结果进行了折叠!这是:
def updatePowerPlant(id: Int) = Action.async(parse.tolerantJson) { request =>
request.body.validate[PowerPlantConfig].fold(
errors => {
Future.successful(
BadRequest(
Json.obj("message" -> s"invalid PowerPlantConfig ${errors.mkString(",")}")
).enableCors
)
},
success => {
dbService.insertOrUpdatePowerPlant(success).runAsync.materialize.map {
case Failure(ex) =>
InternalServerError(s"Error updating PowerPlant " +
s"Reason => ${ex.getMessage}").enableCors
case Success(result) =>
result match {
case Left(errorMessage) =>
BadRequest(Json.obj("message" -> s"invalid PowerPlantConfig $errorMessage")).enableCors
case Right(updatedConfig) =>
Ok(Json.prettyPrint(Json.toJson(updatedConfig))).enableCors
}
}
}
)
}
因为我可以看到我折叠错误并返回BadRequest。但是当我尝试编写单元测试时,我没有像我期望的那样将HTTP状态作为BadRequest,但是测试崩溃时出现如下异常:
JsResultException(errors:List((,List(ValidationError(List(error.expected.jsnumber),WrappedArray())))))
play.api.libs.json.JsResultException: JsResultException(errors:List((,List(ValidationError(List(error.expected.jsnumber),WrappedArray())))))
at play.api.libs.json.JsReadable$$anonfun$2.apply(JsReadable.scala:23)
at play.api.libs.json.JsReadable$$anonfun$2.apply(JsReadable.scala:23)
at play.api.libs.json.JsResult$class.fold(JsResult.scala:73)
at play.api.libs.json.JsError.fold(JsResult.scala:13)
at play.api.libs.json.JsReadable$class.as(JsReadable.scala:21)
at play.api.libs.json.JsDefined.as(JsLookup.scala:132)
at com.inland24.plantsim.models.package$$anon$1.reads(package.scala:61)
at play.api.libs.json.JsValue$class.validate(JsValue.scala:18)
at play.api.libs.json.JsObject.validate(JsValue.scala:76)
at com.inland24.plantsim.controllers.PowerPlantController$$anonfun$updatePowerPlant1$1.apply(PowerPlantController.scala:64)
at com.inland24.plantsim.controllers.PowerPlantController$$anonfun$updatePowerPlant1$1.apply(PowerPlantController.scala:63)
at play.api.mvc.Action$.invokeBlock(Action.scala:498)
at play.api.mvc.Action$.invokeBlock(Action.scala:495)
at play.api.mvc.ActionBuilder$$anon$2.apply(Action.scala:458)
at com.inland24.plantsim.controllers.PowerPlantControllerTest$$anonfun$4$$anonfun$apply$mcV$sp$11.apply(PowerPlantControllerTest.scala:313)
at com.inland24.plantsim.controllers.PowerPlantControllerTest$$anonfun$4$$anonfun$apply$mcV$sp$11.apply(PowerPlantControllerTest.scala:296)
at org.scalatest.OutcomeOf$class.outcomeOf(OutcomeOf.scala:85)
at org.scalatest.OutcomeOf$.outcomeOf(OutcomeOf.scala:104)
at org.scalatest.Transformer.apply(Transformer.scala:22)
at org.scalatest.Transformer.apply(Transformer.scala:20)
at org.scalatest.WordSpecLike$$anon$1.apply(WordSpecLike.scala:1078)
at org.scalatest.TestSuite$class.withFixture(TestSuite.scala:196)
at com.inland24.plantsim.controllers.PowerPlantControllerTest.withFixture(PowerPlantControllerTest.scala:40)
at org.scalatest.WordSpecLike$class.invokeWithFixture$1(WordSpecLike.scala:1075)
at org.scalatest.WordSpecLike$$anonfun$runTest$1.apply(WordSpecLike.scala:1088)
at org.scalatest.WordSpecLike$$anonfun$runTest$1.apply(WordSpecLike.scala:1088)
at org.scalatest.SuperEngine.runTestImpl(Engine.scala:289)
at org.scalatest.WordSpecLike$class.runTest(WordSpecLike.scala:1088)
at com.inland24.plantsim.controllers.PowerPlantControllerTest.runTest(PowerPlantControllerTest.scala:40)
at org.scalatest.WordSpecLike$$anonfun$runTests$1.apply(WordSpecLike.scala:1147)
at org.scalatest.WordSpecLike$$anonfun$runTests$1.apply(WordSpecLike.scala:1147)
at org.scalatest.SuperEngine$$anonfun$traverseSubNodes$1$1.apply(Engine.scala:396)
at org.scalatest.SuperEngine$$anonfun$traverseSubNodes$1$1.apply(Engine.scala:384)
at scala.collection.immutable.List.foreach(List.scala:392)
at org.scalatest.SuperEngine.traverseSubNodes$1(Engine.scala:384)
at org.scalatest.SuperEngine.org$scalatest$SuperEngine$$runTestsInBranch(Engine.scala:373)
at org.scalatest.SuperEngine$$anonfun$traverseSubNodes$1$1.apply(Engine.scala:410)
at org.scalatest.SuperEngine$$anonfun$traverseSubNodes$1$1.apply(Engine.scala:384)
at scala.collection.immutable.List.foreach(List.scala:392)
at org.scalatest.SuperEngine.traverseSubNodes$1(Engine.scala:384)
at org.scalatest.SuperEngine.org$scalatest$SuperEngine$$runTestsInBranch(Engine.scala:379)
at org.scalatest.SuperEngine.runTestsImpl(Engine.scala:461)
at org.scalatest.WordSpecLike$class.runTests(WordSpecLike.scala:1147)
at com.inland24.plantsim.controllers.PowerPlantControllerTest.runTests(PowerPlantControllerTest.scala:40)
at org.scalatest.Suite$class.run(Suite.scala:1147)
at com.inland24.plantsim.controllers.PowerPlantControllerTest.org$scalatest$WordSpecLike$$super$run(PowerPlantControllerTest.scala:40)
at org.scalatest.WordSpecLike$$anonfun$run$1.apply(WordSpecLike.scala:1192)
at org.scalatest.WordSpecLike$$anonfun$run$1.apply(WordSpecLike.scala:1192)
at org.scalatest.SuperEngine.runImpl(Engine.scala:521)
at org.scalatest.WordSpecLike$class.run(WordSpecLike.scala:1192)
at com.inland24.plantsim.controllers.PowerPlantControllerTest.org$scalatest$BeforeAndAfterAll$$super$run(PowerPlantControllerTest.scala:40)
at org.scalatest.BeforeAndAfterAll$class.liftedTree1$1(BeforeAndAfterAll.scala:213)
at org.scalatest.BeforeAndAfterAll$class.run(BeforeAndAfterAll.scala:210)
at com.inland24.plantsim.controllers.PowerPlantControllerTest.run(PowerPlantControllerTest.scala:40)
at org.scalatest.tools.SuiteRunner.run(SuiteRunner.scala:45)
at org.scalatest.tools.Runner$$anonfun$doRunRunRunDaDoRunRun$1.apply(Runner.scala:1340)
at org.scalatest.tools.Runner$$anonfun$doRunRunRunDaDoRunRun$1.apply(Runner.scala:1334)
at scala.collection.immutable.List.foreach(List.scala:392)
at org.scalatest.tools.Runner$.doRunRunRunDaDoRunRun(Runner.scala:1334)
at org.scalatest.tools.Runner$$anonfun$runOptionallyWithPassFailReporter$2.apply(Runner.scala:1011)
at org.scalatest.tools.Runner$$anonfun$runOptionallyWithPassFailReporter$2.apply(Runner.scala:1010)
at org.scalatest.tools.Runner$.withClassLoaderAndDispatchReporter(Runner.scala:1500)
at org.scalatest.tools.Runner$.runOptionallyWithPassFailReporter(Runner.scala:1010)
at org.scalatest.tools.Runner$.run(Runner.scala:850)
at org.scalatest.tools.Runner.run(Runner.scala)
at org.jetbrains.plugins.scala.testingSupport.scalaTest.ScalaTestRunner.runScalaTest2(ScalaTestRunner.java:138)
at org.jetbrains.plugins.scala.testingSupport.scalaTest.ScalaTestRunner.main(ScalaTestRunner.java:28)
这是我的单元测试:
"not update for an invalid PowerPlantConfig JSON" in {
// We are updating the PowerPlant with id = 101, Notice that the powerPlantId is invalid
val jsBody =
"""
|{
| "powerPlantId":"invalidId",
| "powerPlantName":"joesan 1",
| "minPower":100,
| "maxPower":800,
| "rampPowerRate":20.0,
| "rampRateInSeconds":"2 seconds",
| "powerPlantType":"RampUpType"
|}
""".stripMargin
val result: Future[Result] =
controller.updatePowerPlant(101)
.apply(
FakeRequest().withBody(Json.parse(jsBody))
)
result.materialize.map {
case Success(succ) =>
assert(succ.header.status === BAD_REQUEST)
case Failure(_) =>
fail("Unexpected test failure when Updating a PowerPlant! Please Analyze!")
}
}
知道为什么我没有得到预期的行为?我期待我收到HTTP BadRequest!
编辑:为了摆脱意外的异常,我不得不将我的代码包装到Try块中,我不希望这样。所以这段代码摆脱了错误:
def updatePowerPlant(id: Int) = Action.async(parse.tolerantJson) { request =>
scala.util.Try(request.body.validate[PowerPlantConfig]) match {
case Failure(fail) =>
Future.successful(InternalServerError(s"Error updating PowerPlant " +
s"Reason => ${fail.getMessage}").enableCors)
case Success(succ) =>
succ.fold(
errors => {
Future.successful(
BadRequest(
Json.obj("message" -> s"invalid PowerPlantConfig ${errors.mkString(",")}")
).enableCors
)
},
success => {
dbService.insertOrUpdatePowerPlant(success).runAsync.materialize.map {
case Failure(ex) =>
InternalServerError(s"Error updating PowerPlant " +
s"Reason => ${ex.getMessage}").enableCors
case Success(result) =>
result match {
case Left(errorMessage) =>
BadRequest(Json.obj("message" -> s"invalid PowerPlantConfig $errorMessage")).enableCors
case Right(updatedConfig) =>
Ok(Json.prettyPrint(Json.toJson(updatedConfig))).enableCors
}
}
}
)
}
}
但是可以看出还有这个额外的Try(....)块,我不想要这个!
以下是我对PowerPlantConfig的定义:
sealed trait PowerPlantConfig {
def id: Int
def name: String
def minPower: Double
def maxPower: Double
def powerPlantType: PowerPlantType
}
object PowerPlantConfig {
case class OnOffTypeConfig(
id: Int,
name: String,
minPower: Double,
maxPower: Double,
powerPlantType: PowerPlantType
) extends PowerPlantConfig
case class RampUpTypeConfig(
id: Int,
name: String,
minPower: Double,
maxPower: Double,
rampPowerRate: Double,
rampRateInSeconds: FiniteDuration,
powerPlantType: PowerPlantType
) extends PowerPlantConfig
case class UnknownConfig(
id: Int = -1,
name: String,
minPower: Double,
maxPower: Double,
powerPlantType: PowerPlantType
) extends PowerPlantConfig
// represents all the PowerPlant's from the database
case class PowerPlantsConfig(
snapshotDateTime: DateTime,
powerPlantConfigSeq: Seq[PowerPlantConfig]
)
}
这是我的JSON读写:
implicit val powerPlantCfgFormat: Format[PowerPlantConfig] = new Format[PowerPlantConfig] {
def reads(json: JsValue): JsResult[PowerPlantConfig] = {
val powerPlantTyp = PowerPlantType.fromString((json \ "powerPlantType").as[String])
powerPlantTyp match {
case PowerPlantType.OnOffType =>
JsSuccess(OnOffTypeConfig(
id = (json \ "powerPlantId").as[Int],
name = (json \ "powerPlantName").as[String],
minPower = (json \ "minPower").as[Double],
maxPower = (json \ "maxPower").as[Double],
powerPlantType = powerPlantTyp
))
case PowerPlantType.RampUpType =>
JsSuccess(RampUpTypeConfig(
id = (json \ "powerPlantId").as[Int],
name = (json \ "powerPlantName").as[String],
minPower = (json \ "minPower").as[Double],
rampPowerRate = (json \ "rampPowerRate").as[Double],
rampRateInSeconds = Duration.apply((json \ "rampRateInSeconds").as[String]).asInstanceOf[FiniteDuration],
maxPower = (json \ "maxPower").as[Double],
powerPlantType = powerPlantTyp
))
case _ =>
JsSuccess(UnknownConfig(
id = (json \ "powerPlantId").as[Int],
name = (json \ "powerPlantName").as[String],
minPower = (json \ "minPower").as[Double],
maxPower = (json \ "maxPower").as[Double],
powerPlantType = powerPlantTyp
))
}
}
def writes(o: PowerPlantConfig): JsValue = {
if (o.powerPlantType == RampUpType) {
Json.obj(
"powerPlantId" -> o.id,
"powerPlantName" -> o.name,
"minPower" -> o.minPower,
"maxPower" -> o.maxPower,
"rampPowerRate" -> o.asInstanceOf[RampUpTypeConfig].rampPowerRate,
"rampRateInSeconds" -> o.asInstanceOf[RampUpTypeConfig].rampRateInSeconds.toString(),
"powerPlantType" -> PowerPlantType.toString(o.powerPlantType)
)
}
else {
Json.obj(
"powerPlantId" -> o.id,
"powerPlantName" -> o.name,
"minPower" -> o.minPower,
"maxPower" -> o.maxPower,
"powerPlantType" -> PowerPlantType.toString(o.powerPlantType)
)
}
}
}
答案 0 :(得分:2)
根据您的堆栈跟踪(我标记的行)
JsResultException(errors:List((,List(ValidationError(List(error.expected.jsnumber),WrappedArray())))))
play.api.libs.json.JsResultException: JsResultException(errors:List((,List(ValidationError(List(error.expected.jsnumber),WrappedArray())))))
at play.api.libs.json.JsReadable$$anonfun$2.apply(JsReadable.scala:23)
at play.api.libs.json.JsReadable$$anonfun$2.apply(JsReadable.scala:23)
at play.api.libs.json.JsResult$class.fold(JsResult.scala:73)
at play.api.libs.json.JsError.fold(JsResult.scala:13)
--> at play.api.libs.json.JsReadable$class.as(JsReadable.scala:21)
at play.api.libs.json.JsDefined.as(JsLookup.scala:132)
at com.inland24.plantsim.models.package$$anon$1.reads(package.scala:61)
at play.api.libs.json.JsValue$class.validate(JsValue.scala:18)
at play.api.libs.json.JsObject.validate(JsValue.scala:76)
at com.inland24.plantsim.controllers.PowerPlantController$$anonfun$updatePowerPlant1$1.apply(PowerPlantController.scala:64)
at com.inland24.plantsim.controllers.PowerPlantController$$anonfun$updatePowerPlant1$1.apply(PowerPlantController.scala:63)
您使用as[Int]
格式的Read
id
PowerPlantConfig
字段as[Int]
。
当您调用Int
时,您试图强制给定的json路径键入as
。如果它不能(如在你的测试中),它会引发异常。您可以阅读asOpt
,validate
和as
here之间的差异,例如
<强>更新强>
如果你研究asOpt
,validate
和validate
的实现,你会看到所有这三个一开始都做同样的事情,但在某种程度上有所不同:
reads
- 我确实需要结果或失败的信息包装(只需在json上调用隐式arg的asOpt
)
as
- 我需要结果或者没有结果,如果用于解决方案的读取返回解析错误,它将被忽略,因为根本没有设置
as
- 我需要结果或异常。换句话说,&#34;我确定,如果没有,这总是这种类型,而不是一般错误&#34;
asOpt
和as
都是&#34;扩展验证&#34;解释结果。
示例强>
示例如何在层次结构中从validate
移动到as
(两种格式 - 一种用validate
作为你的sealed trait PowerPlantConfig {
def id: Int
}
case class RampUpTypeConfig(id: Int) extends PowerPlantConfig
implicit val powerPlantCfgFormat: Format[PowerPlantConfig] = new Format[PowerPlantConfig] {
def reads(json: JsValue): JsResult[PowerPlantConfig] = {
JsSuccess(RampUpTypeConfig(
id = (json \ "powerPlantId").as[Int]
))
}
def writes(o: PowerPlantConfig): JsValue = {
Json.obj(
"powerPlantId" -> o.id)
}
}
val powerPlantCfgFormatFixed: Format[PowerPlantConfig] = new Format[PowerPlantConfig] {
def reads(json: JsValue): JsResult[PowerPlantConfig] = {
for {
id <- (json \ "powerPlantId").validate[Int]
} yield {
RampUpTypeConfig(
id = id
)
}
}
def writes(o: PowerPlantConfig): JsValue = {
Json.obj(
"id" -> o.id)
}
}
Json.parse("""{"powerPlantId":"123"}""").validate[PowerPlantConfig](powerPlantCfgFormatFixed)
将抛出异常,另一种用res1: play.api.libs.json.JsResult[PowerPlantConfig] = JsError(List((,List(ValidationError(error.expected.jsnumber,WrappedArray())))))
不会抛出异常):
SELECT id,
DATE_FORMAT(starttime, '%h:%i %p') AS starttime,
DATE_FORMAT(endtime, '%h:%i %p') AS endtime
FROM TableName
输出不会是例外,但JsFailure是预期的
DATETIME