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.

*   *   *

środa, 3 września 2014

Neurologia Pair Programming

"Programowanie w parze" jeszcze kilka lat temu szło (a być może wciąż tu i tam idzie) wbrew intuicyjnej wiedzy managerskiej zapożyczonej wprost z fabryki pralek. Bo gdzie jest profit jak dwa risorsy robią pracę jednego risorsa?

Z jednej strony są relacje programistów, którzy mówią iż czasem "co dwie głowy to nie jedna" a z drugiej jest jakiś irracjonalny wzór na efektywność mierzoną ilością wciśniętych klawiszy w jednostce czasu. Kiedyś nawet na JUGu w Łodzi był koleś z prezentacją jakiegoś chorego pluginu do eclipse zliczającego akcje programisty w przeciągu godziny.(Zastanawiałem się czy o tym w ogóle wspominać by jakiś managerski "mlody wilku" nie poczuł inspiracji).

Być może poniższy tekst doda trochę siły naukowej do faktu, iż czasem siedzenie we dwóch przy jednym kompie może być wnieść coś więcej niż tylko "programista razy 2"

Sieć zadaniowa - co się dzieje gdy siedzimy sami nad kompem

Gdy sami siedzimy zagłębieni w problemie aktywowana jest sieć : http://en.wikipedia.org/wiki/Task-positive_network

  • Zalety : skupiamy się na jednej konkretnej rzeczy i rozwiązujemy konkretny problem.
  • Wady : jesteśmy zamknięci na nowe pomysły i sposoby podejścia do danego problemu - a to dlatego, że w danej chwili aktywna jest tylko jedna sieć.
  • Komentarz : oczywiście mogą pojawić się głosy ludzi twierdzące, że to bez znaczenia ponieważ programista ma tylko przepisać diagram narysowany przez kogoś z bardziej egzotyczną nazwą stanowiska. Owe osoby zapytane "skoro to tylko kwestia przepisania diagramu to dlaczego nie mogą tego robić automaty" - odpowiadają pomidor

Sieć społeczna - co się dzieje gdy siedzimy w parze

Teraz powinna działać cześć sieci : http://en.wikipedia.org/wiki/Task-negative A w celu uzupełnienia informacji można też zerknąć na :
  1. http://en.wikipedia.org/wiki/Default_mode_network
  2. http://www.ncbi.nlm.nih.gov/pmc/articles/PMC3380415/ - tu jest dużo i naukowo.
  • Zalety : otwieramy się na nowe pomysły i możliwości rozwiązania danego problemu.
  • Wady : trudniej jest nam się skupić na jednej rzeczy i zanalizować konkretny problem dokładniej i dogłębniej.
  • Komentarz : oczywiście mogą pojawić się głosy ludzi twierdzące, że to bez znaczenia ponieważ programista ma tylko przepisać diagram narysowany przez kogoś z bardziej egzotyczną nazwą stanowiska. Owe osoby zapytane "skoro to tylko kwestia przepisania diagramu to dlaczego nie mogą tego robić automaty" - odpowiadają pomidor
Poniżej default mode network to zdaje się zielone i niebieskie.

Podsumowanie

Rzecz jasna nie było bezpośrednich badań nurkujących wprost do głów programistów w sytuacjach niezwykle fascynującej codzienności życia IT. Powyższy opis jest jedynie interpretacją obecnych faktów naukowych ale być może przyda się jako argument do dyskusji (jeśli w jakichś firmach takie dyskusje wciąż trwają) odnośnie produktywności programowania w parach.

Należy zerwać z błędną wizją "dwóch risorsów" przy jednym komputerze - a zaprosić nowe przekonanie, że programiści czy to pracując sami w słuchawkach czy też żywo dyskutując w N osób - pracują tak naprawdę w różnych trybach przydatnych do rozwiązywania różnych problemów.

oczywiście mogą pojawić się głosy ludzi twierdzące, że to bez znaczenia ponieważ programista ma tylko przepisać diagram narysowany przez kogoś z bardziej egzotyczną nazwą stanowiska. Owe osoby zapytane "skoro to tylko kwestia przepisania diagramu to dlaczego nie mogą tego robić automaty" - odpowiadają pomidor

Informacje do tego artykułu napotkałem w nowym kursie na courserze - Inspiring Leadership through Emotional Intelligence. Na początku myślałem, że to będzie standardowy badziew managerski w stylu "klep ludzi po ramieniu, mów good job, pamiętaj o urodzinach, jak trzeba bądź surowy i stanowczy i kłam, że release jest miesiąc wcześniej niż w rzeczywistości". A tu się okazało, że koleś ciekawie nawija o neurologi co jest o tyle interesujące, że trudno filozofować o neurologii bo to jest tak jak z kompilacją kodu - jak działa to działa a jak nie to nie.

*   *   *