Warsztaty
Spotkanie odbędzie się 23 września we wtorek - a tutaj są wszystkie szczegóły -
I klika zdjęć z ostatnich zajęć na dowód, że te warsztaty się naprawdę odbywają :) (Bo nigdy nic nie wiadomo - mogę w tej chwili siedzieć gdzieś zakuty w fartuch w zakładzie a to wszystko może dziać się w mojej wyobraźni ale dla potrzeb marketingowych załóżmy, że na 99,999% się jednak odbywają)
Plan na zajęcia
W tej partii materiału skupiamy się na wysyłaniu JSONa z serwera do klienta. Do tego aby nie bawić się w Javascript - klientem będzie inna aplikacja Playowa, która to umożliwi nam naukę używania klienta WebSerwisów(ale o tym w drugiej części).
Cechą charakterystyczną tych zajęć będzie więcej zabawy z bibliotekami Playa samymi w sobie aniżeli z Playem samym w sobie ale oczywiście na Play samego w sobie też się znajdzie czas sam w sobie.
A tutaj są linki do poprzednich części :
Coś na start
Aby dać przedsmak tego jak wygodne jest użycie w playu JSONa - szybki przykład na start. Taki mały kodzik a jakże czytelny i ileż on robi! :
def dajJsona = Action { import play.api.libs.json.Json._ val json = obj( "temat" -> "play i json", "plan"->arr("krótki przykład","ćwiczenia z biblioteką","dłuższy przykład","webserwisy") ) Ok(obj("message"->json)) }I w odpowiedzi dostaniemy
{"message": {"temat":"play i json", "plan":["krótki przykład","ćwiczenia z biblioteką","dłuższy przykład","webserwisy"] } }I wiadomo typ "application/json"
Zabawa z JSONem
A zabawa będzie polegała na ćwiczeniach z Jsonem w worksheet. Otwieramy sobie nowy pliczek, kopiujemy to co pod spodem i wio.
//wazny imporcik import play.api.libs.json._ val mealJson: JsValue = Json.parse(""" { "meal": { "name" : "hamburger", "calories" : 3000, "description" : "bardzo tuczące jedzenie", "isJunk" : true, "ingredient" : { "name" : "imitacja mięsa", "calories" : 2000 } } } """)Obiekt do ćwiczeń mamy przygotowany więc teraz poćwiczymy jak go parsować i jak po nim nawigować
// wyszukiwanie pojedynczej wartosc val name= mealJson \ "meal" \ "name" //> name : play.api.libs.json.JsValue = "hamburger" //wyszukiwanie listy wszystkich wartosci val calories= mealJson \\ "calories" //> calories : Seq[play.api.libs.json.JsValue] = List(3000, 2000)Następnie małe ćwiczonka na konwersję do typów Scali:
//zwykła konwersja do scali val nameScala= (mealJson \ "meal" \ "name").as[String] //> nameScala : String = hamburger //jakby się miało wywalić to prosimy nie o zwykły typ ale o Option (mealJson \ "meal" \ "nieMaMnie").asOpt[String] //> res0: Option[String] = NoneJak już czytamy to i można od razy walidować
//można od razu walidować val zle=(mealJson \ "meal" \ "nieMaMnie").validate[String] //> zle : play.api.libs.json.JsResult[String] = JsError(List((,List(ValidationE //| rror(error.expected.jsstring,WrappedArray()))))) zle.map(name=>"jem :"+name) //> res1: play.api.libs.json.JsResult[String] = JsError(List((,List(ValidationEr //| ror(error.expected.jsstring,WrappedArray()))))) val dobre=(mealJson \ "meal" \ "name").validate[String] //> dobre : play.api.libs.json.JsResult[String] = JsSuccess(hamburger,) dobre.map(name=>"jem :"+name) //> res2: play.api.libs.json.JsResult[String] = JsSuccess(jem :hamburger,)
Sami tworzymy JSONa
Co gdy chcemy sami stworzyć obiekt JSONa z istniejących danych? Nie trzeba oczywiście sklejać stringa gdyż istnieje bardzo wygodny i wyrazisty mechanizm do wypełnienia właśnie tego oto zadania. Także uczestnicy kursu są tutaj poproszeni o zastąpienie stringa poniższą konstrukcją i wszystko hulać powinno.
//poniższy import działa dlatego, że wcześniej mamy import "play.api.libs.json._" - fajny patent import Json._ val mealJson=obj( "meal"->obj( "name"->"hamburger", "calories"->3000, "description"->"bardzo tuczące jedzenie", "isJunk" -> true, "ingredient"->obj("name"->"imitacja mięsa","calories"->2000) ) )
Automatyczne konwersje
Mamy do dyspozycji metodę "Json.toJson(...)", która konwertuje nam dany typ w JSona. Działa z automatu dla podstawowych typów Scali - aby działało dla naszych typów musimy dostarczyć przepis na konwersję. Potem robi się "implicit" dzięki czemu nie trzeba tego cały czas przekazywać do metod.
//> json : play.api.libs.json.JsValue = {"name":"mięso","calories":3000} //| } = controllers.writers$$anonfun$main$1$$anon$1@50b98ef4I teraz różne implementacje "ingredientWrites" można sobie importować z różnych bibliotek (ale nie na raz bo się pogryzą) i w ten sposób można konfigurować, który sposób konwersji ma być użyty. Zerknijmy na bardziej rozbudowany przykład z pełną klasą "Meal"
case class Ingredient(name:String,calories:Int) case class Meal(name:String,calories:Int,description:String,isJunk:Boolean,i:Ingredient) implicit val ingredientWrites=new Writes[Ingredient]{ def writes(i:Ingredient)=obj( "name"->i.name, "calories"->i.calories ) } //> ingredientWrites : play.api.libs.json.Writes[controllers.writers.Ingredient //| ]{def writes(i: controllers.writers.Ingredient): play.api.libs.json.JsObject //| } = controllers.writers$$anonfun$main$1$$anon$1@24ef2645 val ingredient=Ingredient("mięso",3000) //> ingredient : controllers.writers.Ingredient = Ingredient(mięso,3000) implicit val mealWrites=new Writes[Meal]{ def writes(meal:Meal)=obj( "name"->meal.name, "calories"->meal.calories, "description"->meal.description, "isJunk"->meal.isJunk, //I tutaj łądnie wykorzystujemy wcześniej zadeklarowany "Writer" dla typu Ingredient "ingredient"->meal.i ) } //> mealWrites : play.api.libs.json.Writes[controllers.writers.Meal]{def writes //| (meal: controllers.writers.Meal): play.api.libs.json.JsObject} = controllers //| .writers$$anonfun$main$1$$anon$2@347db2f9 val meal=Meal("Hamburger",3000,"złe jedzenie",true,ingredient) //> meal : controllers.writers.Meal = Meal(Hamburger,3000,złe jedzenie,true,In //| gredient(mięso,3000)) val json=toJson(meal) //> json : play.api.libs.json.JsValue = {"name":"Hamburger","calories":3000,"de //| scription":"złe jedzenie","isJunk":true,"ingredient":{"name":"mięso","calo //| ries":3000}}A żeby było ładniej to można :
prettyPrint(json) //> res0: String = { //| "name" : "Hamburger", //| "calories" : 3000, //| "description" : "złe jedzenie", //| "isJunk" : true, //| "ingredient" : { //| "name" : "mięso", //| "calories" : 3000 //| } //| }
I w drugą stronę
Jak do zamiany obiektu w JSONa jest "Writes" tak do zamiany na odwyrtkę jest "Reads". Tutaj będzie trochę inaczej bo korzystamy z lekko innego api. "Writes" też można tworzyć tym innym sposobem co być może dałoby lepsze wrażenie spójności ale to mapowanie przez strzałki, którego użyliśmy powyżej jest prostsze.
//najpierw taki dziwny import bo będziemy korzystać z innego api import play.api.libs.functional.syntax._ //i tera implicit val ingredientReads: Reads[Ingredient] = ( (JsPath \ "name").read[String] and (JsPath \ "calories").read[Int] )(Ingredient.apply _) //> ingredientReads : play.api.libs.json.Reads[controllers.writers.Ingredient] //| = play.api.libs.json.Reads$$anon$8@503dbd9a //i wyłuskiwać naszą klasę można (json \ "ingredient").as[Ingredient] //> res0: controllers.writers.Ingredient = Ingredient(mięso,3000) //i walidować (json \ "ingredient").validate[Ingredient] //> res1: play.api.libs.json.JsResult[controllers.writers.Ingredient] = JsSucce //| ss(Ingredient(mięso,3000),)Tutaj walidacja była poprawna ale gdyby coś się popsuło to można tak zadziałać :
val zlyJson=Json.obj{"zly"->"json"} //> zlyJson : play.api.libs.json.JsObject = {"zly":"json"} (zlyJson \ "ingredient").validate[Ingredient] match { case s:JsSuccess[Ingredient] => s"dobre ${s.get}" case e:JsError => s"złe ${JsError.toFlatJson(e)}" } //> res2: String = złe {"obj.name":[{"msg":"error.path.missing","args":[]}],"o //| bj.calories":[{"msg":"error.path.missing","args":[]}]}
Jeszcze dokładniejsza walidacja
implicit val ingredientReads: Reads[Ingredient] = ( (JsPath \ "name").read[String](minLength[String](10)) and (JsPath \ "calories").read[Int](min(1000) keepAnd max(8000)) )(Ingredient.apply _) //I niestety "hamburger się nie załapał" (json \ "ingredient").as[Ingredient] //> play.api.libs.json.JsResultException: JsResultException(errors:List((/name, //| List(ValidationError(error.minLength,WrappedArray(10))))))
Makra
I tutaj te wszystkie napisane Writery i Readery można zastąpić takim oto zestawem jednolinijkowców.
object JsonImplicits{ case class Ingredient(name:String,calories:Int) case class Meal(name:String,calories:Int,description:String,isJunk:Boolean,i:Ingredient) implicit val macroIngredientFormat=Json.format[Ingredient] implicit val macroMealFormat=Json.format[Meal] } import JsonImplicits._To automatycznie generuje Writes i Reads makrami na podstawie Definicji Klas. Kiedy to testowałem to aby działało w Worksheet musiałem owe makra zamknąć w zewnętrznym obiekcie, zaimportować i wtedy trybi. Może tak to musi działać aby makra zaskoczyły a być może muszę się po prostu wyspać.
No to jakaś aplikacyjka w Playu
object Application extends Controller { def index = Action { Ok(views.html.index("Your new application is ready.")) } import play.api.libs.json.Json implicit val mealFormat=Json.format[Meal] def list = Action { Ok(Json.toJson(MealDatabase.data)) } def oneByName(n:String)=Action{ MealDatabase.data.find(_.name.toLowerCase==n).map{foundMeal=> Ok(Json.toJson(foundMeal)) }.getOrElse(BadRequest(s"no meal with name ${n}")) } def add=Action(parse.json){request=> val meal=request.body.as[Meal] MealDatabase.add(meal) Created(s"added ${meal.name}") } } case class Meal(name:String,calories:Int,description:Option[String]) object MealDatabase{ var data=List(Meal("HotDog",2500,Some("Parówa w bule")),Meal("Pizza",4000,None)) def add(m:Meal)=data=m::data }No i Testy
"Application" should { "return meals list" in new WithApplication{ val jsonResult = route(FakeRequest(GET, "/list")).get contentType(jsonResult) must beSome.which(_ == "application/json") contentAsJson(jsonResult) \\ "name" must containAllOf(Seq(JsString("HotDog"),JsString("Pizza"))) } "return one meal by name" in new WithApplication{ val jsonResult = route(FakeRequest(GET, "/meal/pizza")).get contentType(jsonResult) must beSome.which(_ == "application/json") contentAsJson(jsonResult) \ "name" must beEqualTo(JsString("Pizza")) } "add new meal to list" in new WithApplication{ val inputJson=Json.obj("name"->"pomidorowa","calories"->5000) val addingResult = route(FakeRequest(POST, "/add").withJsonBody(inputJson)).get status(addingResult) must beEqualTo(CREATED) contentAsString(addingResult) must beEqualTo("added pomidorowa") } }
I teraz w planach jest, iż zostanie trochę czasu aby przełączyć się na naukę wywołania webserwisów... co opiszę w następnym odcinku. W sumie jak ludzie będą chcieli to można jechać JSONa przez całe spotkanie - grunt by było ciekawie bo tylko wtedy nauka ma sens. Z drugiej strony wywołania asynchroniczne i działanie na abstrakcjach, które symbolizują coś co się jeszcze nie wydarzyło i zmaterializuje się w przyszłości jest magiczne samo w sobie... i znowu wielokropek.
* * *
Brak komentarzy:
Prześlij komentarz