2015-12-09 49 views
3

我有包含表示兩種不同類型的實體JsValue對象JsArray - 它們中的一些代表節點,另一部分表示邊緣播放的Json API:轉換一個JsArray到JsResult [序號[元件]]

在斯卡拉方面,已經有案例類NodeEdge超類型Element。目標是將JsArray(或Seq[JsValue])轉換爲包含Scala類型的集合,例如Seq[Element](=>包含NodeEdge類型的對象)。

我已經定義Read爲的情況下的類:

implicit val nodeReads: Reads[Node] = // ... 

implicit val edgeReads: Reads[Edge] = // ... 

除此之外,還有一個ReadJsArray本身的第一步:

implicit val elementSeqReads = Reads[Seq[Element]](json => json match { 
    case JsArray(elements) => ??? 
    case _ => JsError("Invalid JSON data (not a json array)") 
}) 

與問號的部分如果JsArray的所有元素都是有效的節點和邊,則負責創建JsSuccess(Seq(node1, edge1, ...);如果不是,則創建JsError

但是,我不知道如何以優雅的方式做到這一點。

的邏輯節點區分和邊緣可以這個樣子:

def hasType(item: JsValue, elemType: String) = 
    (item \ "elemType").asOpt[String] == Some(elemType) 

val result = elements.map { 
    case n if hasType(n, "node") => // use nodeReads 
    case e if hasType(e, "edge") => // use edgeReads 
    case _ => JsError("Invalid element type") 
} 

的事情是,我不知道如何在這一點上處理nodeReads/edgeReads。當然,我可以直接調用他們的validate方法,但result的類型爲Seq[JsResult[Element]]。所以最終我將不得不檢查是否有任何JsError對象並將它們以某種方式委託給頂端(請記住:一個無效的數組元素應該總體上導致JsError)。如果沒有錯誤,我仍然必須根據result生成JsSuccess[Seq[Element]]

也許最好避免調用validate,而是暫時使用Read實例。但我不知道如何在最後「合併」所有的Read實例(例如,在簡單的例子類映射中,您有一堆調用JsPath.read(返回Read),最後validate產生一個單一結果基於使用關鍵字and連接的所有這些Read實例)。

編輯:有一點點的信息。

首先,我應該提到案件類別NodeEdge基本上具有相同的結構,至少現在是這樣。目前,單獨分類的唯一原因是爲了獲得更多的類型安全。

一個元件的JsValue具有以下JSON-表示:

{ 
    "id" : "aet864t884srtv87ae", 
    "type" : "node", // <-- type can be 'node' or 'edge' 
    "name" : "rectangle", 
    "attributes": [], 
    ... 
} 

相應的情況下,類看起來像這樣(注意,我們上面已經看到的類型屬性是類的屬性 - 相反,它由類的類型代表 - >Node)。

case class Node(
    id: String, 
    name: String, 
    attributes: Seq[Attribute], 
    ...) extends Element 

Read如下:

implicit val nodeReads: Reads[Node] = (
     (__ \ "id").read[String] and 
     (__ \ "name").read[String] and 
     (__ \ "attributes").read[Seq[Attribute]] and 
     .... 
    ) (Node.apply _) 

一切看起來爲Edge相同,至少目前是這樣。

回答

2

嘗試定義elementReads作爲

implicit val elementReads = new Reads[Element]{ 
    override def reads(json: JsValue): JsResult[Element] = 
     json.validate(
     Node.nodeReads.map(_.asInstanceOf[Element]) orElse 
     Edge.edgeReads.map(_.asInstanceOf[Element]) 
    ) 
} 

和導入的範圍,那麼你應該能夠編寫

json.validate[Seq[Element]] 

如果你的JSON的結構是不夠的NodeEdge區分,你可以在每種類型的讀取中強制執行它。

基於簡化NodeEdge情況下類(只以避免任何不相關的代碼迷惑的答案)

case class Edge(name: String) extends Element 
case class Node(name: String) extends Element 

默認讀取用於這些情況下的類將被分別

Json.reads[Edge] 
Json.reads[Node] 

導出。不幸的是,由於兩個案例類具有相同的結構,這些讀取將忽略json中的type屬性,並愉快地將節點json轉換爲Edge實例或相反。

讓我們來看看我們如何能夠表達對type約束所有的本身:

def typeRead(`type`: String): Reads[String] = { 
    val isNotOfType = ValidationError(s"is not of expected type ${`type`}") 
    (__ \ "type").read[String].filter(isNotOfType)(_ == `type`) 
    } 

此方法構建一個讀取[字符串]實例,它會試圖找到在所提供的JSON一個type字符串屬性。如果從json解析出來的字符串與作爲方法參數傳遞的預期type不匹配,它將使用自定義驗證錯誤isNotOfType來篩選JsResult。當然,如果type屬性不是json中的一個字符串,Reads [String]將返回一個錯誤,表明它期望一個String。

現在,我們有一個讀,可以強制執行的JSON的type屬性的值,我們所要做的就是建立一個我們期望類型的每個值讀出並與相關的情況下,類讀取組成這個。我們可以使用Reads#flatMap來忽略輸入,因爲解析後的字符串對我們的案例類沒有用處。

object Edge { 
    val edgeReads: Reads[Edge] = 
    Element.typeRead("edge").flatMap(_ => Json.reads[Edge]) 
} 
object Node { 
    val nodeReads: Reads[Node] = 
    Element.typeRead("node").flatMap(_ => Json.reads[Node]) 
} 

注意,如果在type約束失敗flatMap通話將被忽略。

的問題仍然是放在哪裏的方法typeRead,在這個答案我最初把它的同伴Element對象與elementReads實例一起如下面的代碼。

import play.api.libs.json._ 

trait Element 
object Element { 
    implicit val elementReads = new Reads[Element] { 
    override def reads(json: JsValue): JsResult[Element] = 
     json.validate(
     Node.nodeReads.map(_.asInstanceOf[Element]) orElse 
     Edge.edgeReads.map(_.asInstanceOf[Element]) 
    ) 
    } 
    def typeRead(`type`: String): Reads[String] = { 
    val isNotOfType = ValidationError(s"is not of expected type ${`type`}") 
    (__ \ "type").read[String].filter(isNotOfType)(_ == `type`) 
    } 
} 

這其實是一個相當不好的地方來定義typeRead: - 它沒有任何具體到Element - 介紹了Element同伴對象,既NodeEdge同伴之間的循環依賴對象

我我會讓你想起正確的位置:)

規範證明它一起工作:

import org.specs2.mutable.Specification 
import play.api.libs.json._ 
import play.api.data.validation.ValidationError 

class ElementSpec extends Specification { 

    "Element reads" should { 
    "read an edge json as an edge" in { 
     val result: JsResult[Element] = edgeJson.validate[Element] 
     result.isSuccess should beTrue 
     result.get should beEqualTo(Edge("myEdge")) 
    } 
    "read a node json as an node" in { 
     val result: JsResult[Element] = nodeJson.validate[Element] 
     result.isSuccess should beTrue 
     result.get should beEqualTo(Node("myNode")) 
    } 
    } 
    "Node reads" should { 
    "read a node json as an node" in { 
     val result: JsResult[Node] = nodeJson.validate[Node](Node.nodeReads) 
     result.isSuccess should beTrue 
     result.get should beEqualTo(Node("myNode")) 
    } 
    "fail to read an edge json as a node" in { 
     val result: JsResult[Node] = edgeJson.validate[Node](Node.nodeReads) 
     result.isError should beTrue 
     val JsError(errors) = result 
     val invalidNode = JsError.toJson(Seq(
     (__ \ "type") -> Seq(ValidationError("is not of expected type node")) 
    )) 
     JsError.toJson(errors) should beEqualTo(invalidNode) 
    } 
    } 

    "Edge reads" should { 
    "read a edge json as an edge" in { 
     val result: JsResult[Edge] = edgeJson.validate[Edge](Edge.edgeReads) 
     result.isSuccess should beTrue 
     result.get should beEqualTo(Edge("myEdge")) 
    } 
    "fail to read a node json as an edge" in { 
     val result: JsResult[Edge] = nodeJson.validate[Edge](Edge.edgeReads) 
     result.isError should beTrue 
     val JsError(errors) = result 
     val invalidEdge = JsError.toJson(Seq(
     (__ \ "type") -> Seq(ValidationError("is not of expected type edge")) 
    )) 
     JsError.toJson(errors) should beEqualTo(invalidEdge) 
    } 
    } 

    val edgeJson = Json.parse(
    """ 
     |{ 
     | "type":"edge", 
     | "name":"myEdge" 
     |} 
    """.stripMargin) 

    val nodeJson = Json.parse(
    """ 
     |{ 
     | "type":"node", 
     | "name":"myNode" 
     |} 
    """.stripMargin) 
} 

,如果你不希望使用asInstanceOf爲鑄造您可以編寫 elementReads情況下,像這樣:

implicit val elementReads = new Reads[Element] { 
    override def reads(json: JsValue): JsResult[Element] = 
    json.validate(
     Node.nodeReads.map(e => e: Element) orElse 
     Edge.edgeReads.map(e => e: Element) 
    ) 
} 

不幸的是,你不能在這種情況下使用_

+0

感謝您的快速回答。如果我沒有錯,JsValues之間沒有明確的映射(基於它們的「elemType」屬性,它可以是節點或邊緣)和案例類,而是通過反覆試驗做出決定,對嗎?我猜這是一個問題,因爲這兩個類(邊緣和節點)目前具有相同的結構(id,name,attributes,...),唯一的區別是類型本身。所以不會將邊緣和節點都轉換爲'Node'的實例? – alapeno

+0

是的,但由於您沒有提供案例類或相關閱讀的結構,因此很難進一步幫助您。對於每個案例類的讀取可以只有在'elemType'具有正確的值 – Jean

+0

我添加了一些信息,希望它有幫助。對'Read'中的elemType值的檢查也跨越了我的想法,但是這個值本身不應該是結果的一部分(參見上面的澄清)。 – alapeno