如何在 Play 2.7 for Scala 中编写一个通用 JSON 解析器来验证入站请求?

Posted

技术标签:

【中文标题】如何在 Play 2.7 for Scala 中编写一个通用 JSON 解析器来验证入站请求?【英文标题】:How do I write a generic JSON parser in Play 2.7 for Scala that validates inbound requests? 【发布时间】:2019-09-11 23:12:58 【问题描述】:

我在 Scala 中有一个 Play 2.7 控制器,它根据案例类模式验证入站 JSON 请求,并报告入站请求负载错误(请注意,我从更大的代码库中提取了这个示例,试图保留其正确的可编译性和功能,不过可能有小错误):

import scala.concurrent.ExecutionContext, Future
import scala.util.Failure, Success, Try

import com.google.inject.Inject
import play.api.libs.json.JsError, JsPath, JsSuccess, JsValue, Json, JsonValidationError
import play.api.mvc.AbstractController, Action, ControllerComponents, Request, Result

class Controller @Inject() (playEC: ExecutionContext, cc: ControllerComponents) extends AbstractController(cc) 

  case class RequestBody(id: String)
  implicit val requestBodyFormat = Json.format[RequestBody]

  private val tryJsonParser = parse.tolerantText.map(text => Try(Json.parse(text)))(playEC)

  private def stringify(path: JsPath, errors: Seq[JsonValidationError]): String = 
    s"$path: [$errors.map(x => x.messages.mkString(",") + (if (x.args.size > 0) (": " + x.args.mkString(",")) else "")).mkString(";")]"
  

  private def runWithRequest(rawRequest: Request[Try[JsValue]], method: (RequestBody) => Future[Result]): Future[Result] = 
    rawRequest.body match 
      case Success(validBody) =>
        Json.fromJson[RequestBody](validBody) match 
          case JsSuccess(r, _) => method(r)
          case JsError(e) => Future.successful(BadRequest(Json.toJson(e.map(x => stringify(x._1, x._2)).head)))
        
      case Failure(e) => Future.successful(BadRequest(Json.toJson(e.getMessage.replaceAll("\n", ""))))
    
  

  // POST request processor
  def handleRequest: Action[Try[JsValue]] = Action(tryJsonParser).async  request: Request[Try[JsValue]] =>
    runWithRequest(request, r => 
      Future.successful(Ok(r.id))
    )
  

向“handleRequest”端点发送 POST 请求时,验证的工作方式如下:

使用有效负载 malformed,,,我会收到带有 Unexpected character ('m' (code 109)): was expecting double-quote to start field name at [Source: (String)"malformed,,"; line: 1, column: 3] 的 400 响应。 使用有效负载,我收到了带有/id: [error.path.missing] 的400 响应

我想做的是使解析和验证通用化, 将该逻辑移动到实用程序类中,以便在handleRequest 方法中进行最干净的重用。例如,像这样:

import scala.concurrent.ExecutionContext, Future
import scala.util.Failure, Success, Try

import com.google.inject.Inject, Singleton
import play.api.Configuration, Logging
import play.api.libs.json.JsError, JsPath, JsSuccess, JsValue, Json, JsonValidationError
import play.api.mvc.AbstractController, Action, ActionBuilderImpl, AnyContent, BodyParsers, ControllerComponents, Request, Result

object ParseAction 
  // TODO: how do I work this in?
  val tryJsonParser = parse.tolerantText.map(text => Try(Json.parse(text)))(playEC)


class ParseAction @Inject()(parser: BodyParsers.Default)(implicit ec: ExecutionContext) extends ActionBuilderImpl(parser) 
  private def stringify(path: JsPath, errors: Seq[JsonValidationError]): String = 
    s"$path: [$errors.map(x => x.messages.mkString(",") + (if (x.args.size > 0) (": " + x.args.mkString(",")) else "")).mkString(";")]"
  

  // TODO: how do I make this method generic?
  override def invokeBlock[A](rawRequest: Request[A], block: (A) => Future[Result]) = 
    rawRequest.body match 
      case Success(validBody) =>
        Json.fromJson[A](validBody) match 
          case JsSuccess(r, _) => block(r).getFuture
          case JsError(e) => Future.successful(BadRequest(Json.toJson(e.map(x => stringify(x._1, x._2)).head)))
        
      case Failure(e) => Future.successful(BadRequest(Json.toJson(e.getMessage.replaceAll("\n", ""))))
    
  


class Controller @Inject() (cc: ControllerComponents) extends AbstractController(cc) 

  case class RequestBody(id: String)
  implicit val requestBodyFormat = Json.format[RequestBody]

  // route processor
  def handleRequest = ParseAction.async  request: RequestBody =>
    Future.successful(Ok(r.id))
  

我知道,由于公然的 Scala 和 Play API 滥用,而不仅仅是小的编码错误,这段代码无法按原样编译。我尝试从 Play's own documentation about Action composition 中提取信息,但我没有成功地把事情做好,所以我留下了所有的碎片,希望有人能帮助我把它们一起工作,做成有用的东西。

如何更改第二个代码示例,使其编译和功能与第一个代码示例相同?

【问题讨论】:

将问题限制在有问题的代码/详细遇到的错误 这正是playsonify 的目标之一,您可能会选择AbstractJsonController 并根据您的需要进行调整。 【参考方案1】:

我使用 ActionBuilder 的隐式类归档了类似的目标:

trait ActionBuilderImplicits 

  implicit class ExActionBuilder[P](actionBuilder: ActionBuilder[Request, P])(implicit cc: ControllerComponents) 

    def validateJson[A](implicit executionContext: ExecutionContext, reads: Reads[A]): ActionBuilder[Request, A] = 
      actionBuilder(cc.parsers.tolerantJson.validate(jsValue => 
        jsValue.validate.asEither.left
          .map(errors => BadRequest(JsError.toJson(errors)))
      ))
    
  



object ActionBuilderImplicits extends ActionBuilderImplicits

然后在控制器中你可以导入ActionBuilderImplicits并将其用作

Action.validateJson[A].async  request =>
   processingService.process(request.body)

这里是 request.body 已经是 A 的类型

【讨论】:

以上是关于如何在 Play 2.7 for Scala 中编写一个通用 JSON 解析器来验证入站请求?的主要内容,如果未能解决你的问题,请参考以下文章

play for scala 实现SessionFilter 过滤未登录用户跳转到登录页面

如何在 Play 中的所有响应中设置标题!框架 2.7 (Java)

为啥我的 Play Framework for Scala 应用程序的 Docker 映像没有以 AccessDeniedException 开头?

如何搭建scala的play框架

如何在 Play Framework v1.2.7 中配置带有动态 id 属性的 POST 路由,该属性将加载 JPA 实体

如何将 IntelliJ 与 Play Framework 和 Scala 一起使用