[英]How can I programmatically create a validation contract at compile-time?
I apologize in advance if this is an XY problem. 如果这是XY问题,我先向您道歉。
I'd like to have a compile-time map of type [Request.type, Response.type]
so I can effectively say if I send message Request
, a CLI should, at compile-time, know how to deserialize its expected Response
, irrespective of the fact that it won't know what type of request is sent until runtime. 我想要一个类型为
[Request.type, Response.type]
的编译时映射,因此我可以有效地说出如果我发送消息Request
,那么CLI应该在编译时知道如何反序列化其预期的Response
,不管它在运行时之前都不知道发送哪种类型的请求。
I have a CLI which communicates with an HTTP server and depending on the type of message sent to the HTTP server, I'd like to validate the JSON response against a case case. 我有一个与HTTP服务器通信的CLI,并且根据发送到HTTP服务器的消息类型,我想针对一个案例验证JSON响应。
For instance, if I send the HTTP server an AddFoo
message, I might want to validate that the JSON response can be deserialized into an AddedFoo
, etc. 例如,如果我向HTTP服务器发送
AddFoo
消息,则可能需要验证JSON响应是否可以反序列化为AddedFoo
等。
My current solution is quite hacky. 我当前的解决方案很hacky。 Using play-json, I'm attempting to parse the JSON response using a mapping from
config.mode
(ie, command issued to the CLI) to the expected responses' implicit Reads
. 我使用play-json尝试使用从
config.mode
(即,发布给CLI的命令)到预期响应的隐式Reads
的映射来解析JSON响应。
My code looks something like this: 我的代码如下所示:
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)
}
}
While this works, it's an incredible kludge that becomes increasingly unmaintainable with an increasing number of messages. 尽管这有效,但随着消息数量的增加,这种不可思议的麻烦变得越来越难以维护。 Further, I've found that this pattern will need to be replicated anywhere some type of validate or conversion will need to happen (eg,
Future[Any]
being converted to Future[AddedFoo]
). 此外,我发现该模式将需要在需要进行某种类型的验证或转换的任何地方复制(例如,
Future[Any]
被转换为Future[AddedFoo]
)。
Surely my approach isn't the right way... how is this traditionally done? 我的方法肯定不是正确的方法...传统上是如何完成的? If it is the right way (please no), are there optimizations that can be made?
如果这是正确的方法(请否),是否可以进行优化?
I managed to accomplish this by encoding the contract directly into the child Request
classes. 我设法通过将合同直接编码到子
Request
类中来完成此任务。 Namely, the child Request
classes would hold a ResponseType
type with the base class enforcing the covariant type. 即,子
Request
类将包含ResponseType
类型,而基类将强制协变类型。
So I can do something like this: 所以我可以做这样的事情:
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])
}
声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.