niedziela, 14 września 2014

PlayFramework warsztaty - usługi REST - tworzenie i konsumpcja

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