2017-03-02 26 views
0

如果這是一個XY問題,我很抱歉。如何在編譯時以編程方式創建驗證合同?

TL;博士:

我想有[Request.type, Response.type]類型的編譯時間圖,所以我可以有效地說,如果我發送消息Request,一個CLI應,在編譯時,知道如何反序列化其預期的Response,而不管它直到運行時才知道發送了什麼類型的請求。

太長;仍然閱讀:

我有一個CLI與HTTP服務器通信,並根據發送到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])。

當然,我的方法是不正確的......這是傳統上的做法嗎?如果方法正確(請不要),是否可以優化?

+2

你的意思是,在*運行時*過去了,發出HTTP請求時,意思? –

+1

我只會嘗試在類型都擴展一些密封特徵或抽象類的時候這樣做,因爲只有類匹配才能幫助您在此之後進行排序。 –

+0

@MichaelZajac所有的請求都會擴展'MyBaseRequest',所有的響應都會擴展'MyBaseResponse'。我發現這是一個相當頑皮的問題......我認爲這將是一個衆所周知的模式。 :) – erip

回答

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]) 
}