如何在编译时以编程方式创建验证合同?

时间:2017-03-02 13:45:21

标签: json scala validation json-deserialization play-json

如果这是一个XY问题,我提前道歉。

TL; DR:

我想要一个类型为[Request.type, Response.type]的编译时映射,所以我可以有效地说如果我发送消息Request,CLI应该在编译时知道如何反序列化它预期Response,不管它在运行时都不知道发送什么类型的请求。

太久了;仍然读:

我有一个与HTTP服务器通信的CLI,根据发送到HTTP服务器的消息类型,我想根据案例验证JSON响应。

例如,如果我向HTTP服务器发送AddFoo消息,我可能想验证JSON响应是否可以反序列化为AddedFoo等。

我目前的解决方案非常黑客。使用play-json,我试图使用从config.mode(即发出到CLI的命令)到预期响应'隐式Reads的映射来解析JSON响应。

我的代码看起来像这样:

val modeToResponseReads: Map[String, Reads[_]] = Map(
  Modes.ADD_FOO -> AddedFoo.addedFooReads,
  Modes.ADD_BOO -> AddedBoo.addedBooReads,
  Modes.GET_WOO -> GetWooResponse.getWooReads,
)

parser.parse(args, MyConfig()) match {

  case Some(config) => try {
    val exec = new MyHttpExecutor(remoteUri, config)
    val res = Await.result(exec.getResponse, 100.seconds)

    // passing `Reads` to `as` because JsValue#as[T] cannot be
    // applied at runtime -- only compile-time.
    val _ = Json.parse(res.json.toString)
                .as(modeToResponseReads(config.mode))

    exec.actorSystem.terminate()
    exec.wsClient.close()
  } catch {
    case t: Throwable => logger.error(t.getMessage)
  }

  case None => {
    logger.error("Bad arguments.")
    sys.exit(1)
  }
}

虽然这种方法有效,但随着消息数量的增加,它变得越来越难以维护。此外,我发现需要在需要进行某种类型的验证或转换的任何地方复制此模式(例如,Future[Any]转换为Future[AddedFoo])。

当然,我的方法不是正确的方法......传统上这是怎么做的?如果是正确的方法(请不要),是否可以进行优化?

1 个答案:

答案 0 :(得分:0)

我设法通过将合同直接编码到子Request类中来实现这一目标。也就是说,子Request类将保持ResponseType类型,基类强制执行协变类型。

所以我可以这样做:

abstract class Response
abstract class Request[+A <: Response]

case class Foo(id: String)

object Foo {
  implicit val fooReads = Json.reads[Foo]
  implicit val fooFormat = Json.format[Foo]
}

case class FooResponse(foo: Foo) extends Response {
  def greet = println("woo hoo!")
}

object FooResponse {
  implicit val fooRespReads = Json.reads[FooResponse]
  implicit val fooRespFormat = Json.format[FooResponse]
}

case class FooRequest() extends Request[FooResponse] {
  type ResponseType = FooResponse
}

object Main extends App {
  val req: FooRequest = new FooRequest()
  val foo = Foo("12345")
  val resp = new FooResponse(foo)

  val respJsonString = Json.toJson(resp).toString
  println(Json.parse(respJsonString).as[req.ResponseType])
}