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] = None
Jak 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@50b98ef4
I 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