Prezantimi i spray-json

Starmind, ne përdorim bibliotekën spray-json për të analizuar dhe gjeneruar JSON. Përcaktimi se si krijohet JSON për klasat tona mund të duket kështu:

import spray.json._

case class Person(firstName: String, lastName: String)

implicit val personJsonWriter = new JsonWriter[Person] {
  def write(person: Person): JsValue = {
    JsObject(
      "first_name" -> JsString(person.firstName),
      "last_name" -> JsString(person.lastName)
    )
  }
}

Person(firstName = "David", lastName = "Bowie").toJson

"Ekzekutoni këtë kod në Scastie."

JsonWriter përdoret si një parametër i nënkuptuar për metodën .toJson. Kjo prodhon një objekt JsValue (nga DSL që spray-json përdor për të përfaqësuar JSON në Scala) në të cilin për shembull mund të thërrasim .prettyPrint për ta kthyer atë në një varg të njohur JSON:

{
  "first_name": "David",
  "last_name": "Bowie"
}

Ka edhe dy gjëra të tjera që duhet të kuptojmë në lidhje me spray-json për këtë artikull.

Së pari, përveç tiparit JsonWriter[T] me metodën write për të kthyer diçka të tipit TJsValue, ekziston edhe një tipar JsonReader[T] me metodën read për të bërë konvertimin e kundërt (nga JsValue në llojin T). Kur duam të mbështesim leximin dhe shkrimin, ne mund të zbatojmë një JsonFormat[T] i cili thjesht "përcaktohet si"

trait JsonFormat[T] extends JsonReader[T] with JsonWriter[T]

Së dyti, ka tipare RootJsonWriter, RootJsonReader dhe RootJsonFormat, të cilat shtrihen nga JsonWriter, JsonReader dhe JsonFormat respektivisht, pa shtuar ndonjë funksionalitet shtesë. "Dokumentacioni spray-json" shpjegon:

Sipas specifikimit JSON, jo të gjitha llojet e vlerave të përcaktuara JSON lejohen në nivelin rrënjësor të një dokumenti JSON. Një varg JSON për shembull (si "foo") nuk përbën një dokument ligjor JSON në vetvete. Vetëm objektet JSON ose vargjet JSON lejohen si rrënjë dokumentesh JSON.

Për të dalluar, në nivelin e tipit, JsonFormats "të rregullt" nga ato që prodhojnë objekte ose vargje JSON të nivelit rrënjë spray-json përcakton llojin "RootJsonFormat", i cili nuk është gjë tjetër veçse një specializim shënuesi prej JsonFormat. Bibliotekat që mbështesin spray-json si një mjet për serializimin e dokumenteve mund të zgjedhin të varen nga një RootJsonFormat[T] për një lloj të personalizuar T (në vend se një "i thjeshtë" JsonFormat[T]), në mënyrë që të mos lejojnë paraqitjen e dokumentit të paligjshëm rrënjët.

Në fakt, ajo që është ose nuk është e vlefshme JSON e nivelit të lartë nuk duket të jetë aq e thjeshtë, me "specifikime të ndryshme që përdorin përkufizime të ndryshme". Megjithatë, kjo është një histori krejtësisht e ndryshme…

JsonWriters që mungojnë

Tani spray.json.DefaultJsonProtocol (i disponueshëm si trait dhe si object) përmban një numër shkrimtarësh, lexuesish dhe formatesh të paracaktuara, si dhe metoda të nënkuptuara që mund p.sh. jep automatikisht një JsonFormat[List[T]] për çdo lloj T që ka një JsonFormat[T] të nënkuptuar. (Blogu i programimit të Haoyi ka një shpjegim të mirë se si funksionojnë këto të nënkuptuara.) Kjo na mundëson të bëjmë diçka të tillë:

import spray.json._
import spray.json.DefaultJsonProtocol._

case class Person(firstName: String, lastName: String)

implicit val personJsonFormat = new RootJsonFormat[Person] {
  def write(person: Person): JsValue = {
    JsObject(
      "first_name" -> person.firstName.toJson,
      "last_name" -> person.lastName.toJson
    )
  }
  
  def read(value: JsValue): Person = ???
}

val davidBowie = Person(firstName = "David", lastName = "Bowie")
val iggyPop = Person(firstName = "Iggy", lastName = "Pop")
List(davidBowie, iggyPop).toJson

"Ekzekutoni këtë kod në Scastie."

Metoda .toJson kërkon një JsonWriter[List[Person]] të nënkuptuar, e cila ofrohet nga "metoda"

implicit def listFormat[T :JsonFormat]: RootJsonFormat[List[T]]

nga DefaultJsonProtocol me parametrin e tipit T të vendosur në Person (dhe lidhja e kontekstit :JsonFormat është e kënaqur nga JsonFormat[Person] tonë të nënkuptuar). Me fjalë të tjera, duke shkruar më qartë

List(davidBowie, iggyPop).toJson(listFormat(personJsonFormat))

është e barabartë me List(davidBowie, iggyPop).toJson.

Unë përdora një JsonFormat këtu në vend të një JsonWriter. Nëse ne jemi të interesuar vetëm të shkruajmë, do të ishte më e pastër të zbatonim vetëm një JsonWriter, në vend që ta linim metodën read të pazbatuar, ose ta bënim atë të bënte një përjashtim. Në mënyrë të bezdisshme, spray-json duket se inkurajon pikërisht këtë të fundit, duke provuar një "metodë"

def lift[T](writer :JsonWriter[T]) = new JsonFormat[T] {
  def write(obj: T): JsValue = writer.write(obj)
  def read(value: JsValue) =
    throw new UnsupportedOperationException("JsonReader implementation missing")
}

DefaultJsonProtocol, ndërkohë që neglizhohet të ofrojë ndonjë JsonWriters të nënkuptuar për listat ose koleksionet e tjera.

Fatmirësisht, ky lëshim në bibliotekën spray-json na ofron mundësinë të mësojmë disa mësime të rëndësishme rreth zgjidhjes së nënkuptuar, duke u përpjekur të zbatojmë vetë JsonWriters që mungojnë…

Zgjerimi i Protokollit DefaultJson

Ne në thelb mund të kopjojmë-ngjitim disa rreshta nga kodi burimor spray-json për të krijuar JsonWriters të nënkuptuara për listat.

import spray.json._

object ExtendedJsonProtocol extends DefaultJsonProtocol {
  implicit def listJsonWriter[T : JsonWriter]: RootJsonWriter[List[T]] = new  RootJsonWriter[List[T]] {
   def write(list: List[T]): JsArray = JsArray(list.map(_.toJson).toVector)
  }
}
import ExtendedJsonProtocol._

case class Person(firstName: String, lastName: String)

implicit val personJsonWriter = new RootJsonWriter[Person] {
  def write(person: Person): JsValue = {
    JsObject(
      "first_name" -> person.firstName.toJson,
      "last_name" -> person.lastName.toJson
    )
  }
}

val davidBowie = Person(firstName = "David", lastName = "Bowie")
val iggyPop = Person(firstName = "Iggy", lastName = "Pop")
List(davidBowie, iggyPop).toJson

"Ekzekutoni këtë kod në Scastie."

Lehtë.

Por, çka nëse ne, pasi kemi zgjatur kështu protokollin e paracaktuar JSON, vendosim që duam të mbështesim leximin dhe shkrimin për klasën tonë të rastit Person? Ne mund ta zëvendësojmë JsonWriter[Person] tonë të nënkuptuar me njëJsonFormat[Person] të nënkuptuar. Le të provojmë me formatin që spray-json mund të gjenerojë automatikisht për klasat e rasteve:

import spray.json._

object ExtendedJsonProtocol extends DefaultJsonProtocol {
  implicit def listJsonWriter[T : JsonWriter]: RootJsonWriter[List[T]] = new  RootJsonWriter[List[T]] {
   def write(list: List[T]): JsArray = JsArray(list.map(_.toJson).toVector)
  }
}
import ExtendedJsonProtocol._

case class Person(firstName: String, lastName: String)

implicit val personJsonFormat = jsonFormat2(Person)

val davidBowie = Person(firstName = "David", lastName = "Bowie")
val iggyPop = Person(firstName = "Iggy", lastName = "Pop")
List(davidBowie, iggyPop).toJson

"Ekzekutoni këtë kod në Scastie."

Papritur përpiluesi nuk do ta pranojë më kodin…

Error:(17, 46) ambiguous implicit values:
 both method listFormat in trait CollectionFormats of type [T](implicit evidence$1: spray.json.JsonFormat[T])spray.json.RootJsonFormat[List[T]]{def write(list: List[T]): spray.json.JsArray}
 and method listJsonWriter in object ExtendedJsonProtocol of type [T](implicit evidence$1: spray.json.JsonWriter[T])spray.json.RootJsonWriter[List[T]]
 match expected type spray.json.JsonWriter[List[A$A10.this.Person]]
lazy val result = List(davidBowie, iggyPop).toJson
                                            ^

Nga mesazhi i gabimit "vlera të paqarta të nënkuptuara", mund të kuptojmë se përpiluesi ka dy mënyra për të gjeneruar në mënyrë implicite një JsonWriter[List]: duke përdorur DefaultJsonProtocol për të krijuar një RootJsonFormat[List], d.m.th.

List(davidBowie, iggyPop).toJson(listFormat(personJsonFormat))

ose duke përdorur metodën tonë listJsonWriter për të kthyer JsonFormat[Person]RootJsonWriter[List]:

List(davidBowie, iggyPop).toJson(listJsonWriter(personJsonFormat))

Scala do të ishte goxha e gjymtuar nëse do të bënte gjithmonë një gabim sa herë që zbatohet më shumë se një konvertim i nënkuptuar. Sigurisht që Scala ka disa mekanizma për të renditur konvertimet e nënkuptuara të disponueshme dhe për të zgjedhur atë më të përshtatshëm. Por si funksionon saktësisht kjo dhe pse dështoi në shembullin e mësipërm? Koha për t'u zhytur në "Specifikimet e Gjuhës Scala" të frikshme!

Kuptimi i zgjidhjes së nënkuptuar

Specifikimi i gjuhës Scala ka një kapitull i cili i kushtohet impliciteve, kështu që le të fillojmë të kërkojmë atje. Në seksionin "7.2 Parametrat e nënkuptuar", gjejmë një përkufizim të sferës së nënkuptuar (i cili mund të jetë subjekt i disa eseve në vetvete; "Pyetjet e shpeshta të Scala" është një pikënisje e mirë) si dhe fjali:

Nëse ka disa argumente të pranueshëm që përputhen me llojin e parametrit të nënkuptuar, do të zgjidhet një më specifik duke përdorur rregullat e "rezolucionit të mbingarkesës" statike.

Kjo na çon në seksionin "6.26.3 Rezolucioni i mbingarkesës"", i cili është mjaft i frikshëm për t'u lexuar. Megjithatë, nëse heqim të gjitha detajet teknike, idetë kryesore nuk janë aq komplekse. Scala kërkon alternativën "më specifike". Këtu, të qenit "më specifik" është një marrëdhënie e pjesshme që merr parasysh dy aspekte të ndryshme. Për të vendosur nëse një alternativë X është më specifike se një alternativë Y, mund të imagjinojmë një garë në dy raunde (një raund për secilin nga dy aspektet), ku rezultati përfundimtar është shuma e rezultateve të raundeve individuale.

  • Rundi 1: Ku përcaktohet alternativa?
    X shënon një pikë mbi Y kur X përcaktohet në një klasë ose objekt që rrjedh nga (për shembull zgjerohet nga) klasa ose objekti ku përcaktohet Y (dhe anasjelltas).
  • Rundi 2: Cili është lloji i alternativës?
    X shënon një pikë mbi Y kur lloji i X "përputhet" me (d.m.th. është një nënlloj i) llojit të Y (dhe anasjelltas).
    (Nuk është e qartë për ta nxjerrë këtë nga përkufizimi teknik i dhënë në Specifikimet e Gjuhës Scala, i cili në mënyrë konfuze përdor gjithashtu fjalët "aq specifike sa". Megjithatë, shumica e detajeve teknike kanë të bëjnë me aplikimin e funksionit dhe nuk ndikojnë në rezolucionin e nënkuptuar.)

Nëse ka një fitues pas mbledhjes së rezultateve të dy raundeve, atëherë kjo alternativë është më specifike.

Sidoqoftë, rezultati mund të jetë gjithashtu një barazim, i cili shkakton gabimin e "vlerave të nënkuptuara të paqarta" që kemi hasur më parë:

  • Rezultati është barazim në të dyja raundet. Për shembull, të dyja alternativat kanë të njëjtin lloj dhe përcaktohen në të njëjtën klasë/objekt (ose në klasa/objekte të palidhura).
  • Një alternativë fiton një raund dhe alternativa tjetër fiton raundin tjetër. Për shembull, alternativa X përcaktohet në një nënklasë ku përcaktohet alternativa Y, por në të njëjtën kohë lloji i Y është një nëntip i tipit X.

Situata e dytë ndodhi në shembullin tonë. listJsonWriter jonë përkufizohet në një objekt që shtrihet nga DefaultJsonProtocol ku është përcaktuar listFormat e spray-json. Pra, raundi 1 është një fitore 1–0 për listJsonWriter tonë. Megjithatë, JsonFormat[List[T]], që është lloji i kthyer nga metoda listFormat, është një nënlloj i JsonWriter[List[T]], lloji i kthyer nga metoda jonë listJsonWriter. Pra, raundi 2 është një fitore 0–1 për listFormat të spray-json. Në përgjithësi është një barazim, prandaj përpiluesi Scala hedh një gabim "vlera të paqarta të nënkuptuara".

Gjetja e nënkuptimeve të sakta

Me këtë njohuri se si funksionon zgjidhja e nënkuptuar, zgjidhja për problemin tonë të "vlerave të nënkuptuara ambiciozë" nuk është e vështirë të gjendet. Meqenëse nuk mund të bëjmë shumë për llojin e metodës listJsonWriter, duhet ta bëjmë këtë metodë më pak specifike se listFormat e spray-json duke ndryshuar ku është përcaktuar. Nëse ExtendedJsonProtocol-ja jonë nuk shtrihet më nga DefaultJsonProtocol, atëherë duhet të jemi mirë. Një mënyrë për ta arritur këtë në fletën tonë të punës është të ndryshojmë extends DefaultJsonProtocolimport DefaultJsonProtocol._.

import spray.json._
import DefaultJsonProtocol._

object ExtendedJsonProtocol {
  implicit def listJsonWriter[T : JsonWriter]: RootJsonWriter[List[T]] = new RootJsonWriter[List[T]] {
    def write(list: List[T]): JsArray = JsArray(list.map(_.toJson).toVector)
  }
}
import ExtendedJsonProtocol._

case class Person(firstName: String, lastName: String)

implicit val personJsonFormat = jsonFormat2(Person)

val davidBowie = Person(firstName = "David", lastName = "Bowie")
val iggyPop = Person(firstName = "Iggy", lastName = "Pop")
List(davidBowie, iggyPop).toJson

"Ekzekutoni këtë kod në Scastie."

Dhe jo, funksionon përsëri siç pritej! ExtendedJsonProtocol jonë nuk i pengon më DefaultJsonProtocol.

Vini re se në shembullin e mësipërm, ExtendedJsonProtocol nuk përdoret fare. Vetëm nëse një lloj T ka një JsonWriter por jo JsonFormat, atëherë metoda jonë implicitlistJsonWriter do të përdoret kur nevojitet një JsonWriter[List[T]]. Sa herë që disponohet një JsonFormat, në vend të kësaj do të përdoret metoda më specifike listFormat nga spray-json.

Shpresoj ta keni gjetur të dobishëm këtë artikull. Do të isha i lumtur të dëgjoja ndonjë reagim. Shpresoj të publikoj më shumë artikuj si ky mbi programimin e avancuar Scala në të ardhmen.