Skip to content

Instantly share code, notes, and snippets.

@daneko
Last active October 13, 2016 11:10
Show Gist options
  • Save daneko/4657681 to your computer and use it in GitHub Desktop.
Save daneko/4657681 to your computer and use it in GitHub Desktop.
play2.x系Json周りメモ AngularJs + coffee + Play のためのメモ
// play-jsonをフツーのプロジェクトでも使いたいサンプル
name := "hello"
version := "1.0"
scalaVersion := "2.10.3"
resolvers += "Typesafe repository" at "http://repo.typesafe.com/typesafe/releases/"
libraryDependencies ++=Seq(
"com.typesafe.play" %% "play-json" % "2.2.1"
)

ドキュメントよりも、もとのヤツのほうが参考になる? http://mandubian.com/2012/10/29/unveiling-play-2-dot-1-json-api-part3-json-transformers/

あと元ソースのコメント play.api.libs.json.JsPath.scala

特定のキーのみで再構成してみる

import play.api.libs.json._
import play.api.libs.json.Reads._ //of[Type]を使うのなら忘れない

val json = Json.obj(
  "key1" -> "value1",
  "key2" -> Json.obj(
    "key21" -> 123,
    "key22" -> true,
    "key23" -> Json.arr("alpha", "beta", "gamma"),
    "key24" -> Json.obj(
      "key241" -> 234.123,
      "key242" -> "value242"
    )
  ),
  "key3" -> 234
)

val jsonTransformer = (__ \ 'key2).json.pickBranch(
  of[JsObject].map{ case obj:JsObject =>
    JsObject( Seq(("key21", obj \ "key21") , ("key24", obj \ "key24")))
  }
)

scala> json.validate(jsonTransformer)
res: play.api.libs.json.JsResult[play.api.libs.json.JsObject] = JsSuccess({"key2":{"key21":123,"key24":{"key241":234.123,"key242":"value242"}}},/key2)

scala> Json.prettyPrint(json.validate(jsonTransformer).get)
res2: String =
{
  "key2" : {
    "key21" : 123,
    "key24" : {
      "key241" : 234.123,
      "key242" : "value242"
    }
  }
}

簡単な例 特定キーを置き換える系

import play.api.libs.json._
import play.api.libs.json.Reads._ //of[Type]を使うのなら忘れない

val json = Json.obj(
  "key1" -> "value1",
  "key2" -> Json.obj(
    "key21" -> 123,
    "key22" -> true,
    "key23" -> Json.arr("alpha", "beta", "gamma"),
    "key24" -> Json.obj(
      "key241" -> 234.123,
      "key242" -> "value242"
    )
  ),
  "key3" -> 234
)

// key2をただのbooleanにする

val transformer1 = __.json.update( (__ \ 'key2).json.put(JsBoolean(true)))
val transformer2 = (__ \ 'key2).json.update( of[JsObject].map(x => JsBoolean(true)))

scala> json.validate(transformer1)
res4: play.api.libs.json.JsResult[play.api.libs.json.JsObject] = JsSuccess({"key1":"value1","key2":true,"key3":234},)

scala> json.validate(transformer2)
res6: play.api.libs.json.JsResult[play.api.libs.json.JsObject] = JsSuccess({"key1":"value1","key2":true,"key3":234},/key2)

ありがちな例をJsonからObjectに変換してみる

{
  "name" : "hoge",
  "sex" : "female",
  "id" : 123456
}
{
  "name" : "fuga",
  "sex" : "male",
  "id" : "123457"
}
  • sexってのはどうやらEnum的な(ここではmale/female以外の文字列は飛んでこないとします)
  • id は文字列だったり数値だったり…(ここでは数値は文字列に直せばOkとします)
  case class User(name: String, sex: Sex, id: String)

  object Sex {
    case object Male extends Sex("male")
    case object Female extends Sex("female")

    def apply(sex: String) = sex match {
      case Male.sex   => Male
      case Female.sex => Female
    }
  }

  sealed abstract class Sex(val sex: String)
  import play.api.libs.json._
  import play.api.libs.functional.syntax._
  import play.api.data.validation.ValidationError

  implicit val sexReader = new Reads[Sex] {
    def reads(json: JsValue): JsResult[Sex] = json match {
      case JsString(x) => Try(JsSuccess(Sex(x))).getOrElse(JsError())
      case _           => JsError()
    }
  }

  implicit val userReader = (
    (__ \ 'name).read[String] ~
    (__ \ 'sex).read[Sex] ~
    (__ \ 'id).read[JsValue].collect(ValidationError("validate.error")) {
      case JsString(x) => x
      case JsNumber(x) => x.toString}
    )(User)

もうちょっとあっさりしてみる

  import play.api.libs.json.Reads._

  implicit val userReader = (
    (__ \ 'name).read[String] ~
    (__ \ 'sex).read[Sex] ~
    (__ \ 'id).read(of[String] or of[Int].map(_.toString))
  )(User)

sample の zentask がcoffee + backboneで参考になるかと


Json関連

http://www.playframework.org/documentation/2.0.4/ScalaJsonRequests

jerkson使ってJSONでかえす

import com.codahale.jerkson.JsonSnakeCase

@JsonSnakeCase
case class Hoge(msg:String, sampleCount:Int)

import com.codahale.jerkson.Json

// return [{"msg":"hoge","sample_count":1},{"msg":"fuga","sample_count":2}] 
def index = Action{
  Ok(generate(List(Hoge("hoge", 1), Hoge("fuga", 2))))
}
hoge = ->
  $.ajax
    type: 'GET' # routes設定による
    url: '/index' # routes設定による
    data:
      msg: "test"
    success: (data) ->
      console.log(data)
    error: (response) ->
      console.log(response)

javascriptRouter 定義しないとならないかとおもいきや、別に普通に呼べる

てけとーにモデルのところにcase class を作っておいて、それをJson.generateに適当に渡せばOK


controllerを書く際の注意

http://stackoverflow.com/questions/11927131/angularjs-breaks-with-coffeescript-function-expression

ダメ
HogeCtrl = ($scope) ->
  @scope.hoge = [1,2,3]
  
OK
@HogeCtrl = ($scope) ->
  @scope.hoge = [1,2,3]
  

controller @scope.list を サーバからJsonを取得して初期化したい

$http api reference

# model
class Hoge
  constructor: (@name, @id) ->

# controller
@HogeCtrl = ($scope, $http) ->
  $http.get("/json_list").success (data) ->
      $scope.hoge_list = data.map (obj) -> new Hoge(obj.name, obj.id)

# with error handring
@HogeCtrl = ($scope, $http) ->
  $http(
    method: "GET"
    url: "/json_list"
  ).success((data, status, headers, config) ->
    $scope.hoge_list = data.map (obj) -> new Hoge(obj.name, obj.id)
  ).error (data, status, headers, config) ->
    console.log("error")

# use jQuery ajax
@HogeCtrl = ($scope) ->
  $.ajax ->
    async: false
    type: 'GET'
    dataType: 'json'
    url: '/json_list'
    success: (data) ->
      $scope.hoge_list = data.map (obj) -> new Hoge(obj.name, obj.id)
    error: (data) ->
      console.log("error")


残念な対応メモ

query:filter で絞った結果で使用しているクラスに対してのみ操作する

無理矢理感否めない

対象となるDOMに同じクラスと同じデータ属性を与えて、データ属性から中身を判断するとかそんなので逃げた

一応checkボタンを押した時、表示されているやつだけ、flagが変わる

<button class="btn" type="button" ng-click="check()">check</button>
  
<li ng-repeat="hoge in hoge_list | filter:query">
  <input type="checkbox" ng-model="hoge.hoge_flag" class="hoge-class" data-hogeid="{{hoge.id}}">
</li>
#model
class Hoge
  constructor: (@id) ->
    @hoge_flag = false

@HogeCtrl = (@scope) ->
  $scope.hoge_list = [
    new Hoge(..)
    new Hoge(..)
    ..
  ]
  
  $scope.check = () ->
    idlist = $(".hoge-class").map (i, dom) -> $(dom).data("hogeid")

    for hoge in $scope.hoge_list
      if hoge.id in idlist
        hoge.hoge_flag = true

    $(".hoge-box").attr("checked", true )

ちょっとましになった


ajax から playのコントローラーに配列をパラメータとして渡して、且つFormで判定する

case class Hoge(id:List(Int))

val form = Form(
  mapping(
    "hoge" -> list(number)
  )
)(Hoge.apply)(Hoge.unapply)

def index = Action{ implicit request =>
  searchForm.bindFromRequest.fold(
    e => BadRequest,
    v => Ok("Ok")
  )
}
# NG

$.ajax ->
  url: "/path/to/index
  data: 
    hoge: [1,2,3,4]
  success: ...

# OK

array2hash = (keyname, arry) ->
  key = (index) -> "#{keyname}[#{index}]"
  data = {}
  for el, i in arry
    data[key(i)] = el
  data

$.ajax ->
  url: "/path/to/index
  data: array2hash("hoge", [1,2,3,4])
  success: ...

FormにListでバインドするときは hoge[0] = 1, hoge[1] = 2 ... という形でないとダメ

なので {hoge: [1,2,3,4]} という形は理解されない

そのため {hoge[0]: 1, hoge[1]: 2 ...} という形に置き換えてリクエストしている

ただこの形の時は下記のような形の時にこまる。

$.ajax ->
  data:
    hoge: 1
    fuga: 2

その時は jQueryのメソッドを使ってハッシュを足し合わせることで回避する

query = $.extend({}, {hoge[0]: 1, hoge[1]: 2}, {fuga: 2})

http://www.playframework-ja.org/documentation/2.0.4/ScalaForms 値の繰り返し参照

play2.1を超部分的に触ってみたよー

jsonまわり

  • jerksonがplayのcoreから消えたので注意
  • とりあえずドキュメント読む 5ページあるから気をつけろ

case class を jsonに変換

import play.api.libs.json._
import play.api.libs.functional.syntax._

case class Hoge(hoge:String, fuga:Int, piyo:List[Int])

val convert = Json.format[Hoge] // 下記の場合Json.writesで十分だけど…

val a1 = Hoge("a1", 1, List(1,2,3))
val a2 = Hoge("a2", 2, List(3,2,1))

Json.toJson(convert.writes(a1))
Json.toJson(a1)(convert)
// 2つとも一緒 play.api.libs.json.JsValue = {"hoge":"a1","fuga":1,"piyo":[1,2,3]}


Json.toJson(Seq(a1,a2).map{a => convert.writes(a)})
// play.api.libs.json.JsValue = [{"hoge":"a1","fuga":1,"piyo":[1,2,3]},{"hoge":"a2","fuga":2,"piyo":[3,2,1]}]

もうちょっと複雑な形にTry

  import play.api.libs.json._
  import play.api.libs.functional.syntax._

  case class Hoge(hoge: String, fuga: Fuga, piyo: List[Int])

  case class Fuga(fuga1: String, fuga2: String)

  implicit val fugaFormat = (
    (__ \ "fuga1").format[String] ~
    (__ \ "fuga2").format[String])(Fuga.apply, unlift(Fuga.unapply))

  implicit val hogeFormat = (
    (__ \ "hoge").format[String] ~
    (__ \ "fuga").format[Fuga](fugaFormat) ~
    (__ \ "piyo").format[List[Int]])(Hoge.apply, unlift(Hoge.unapply))

  val a1 = Hoge("a1", Fuga("aa", "bb"), List(1, 2, 3))
  val a2 = Hoge("a2", Fuga("ee", "ff"), List(3, 2, 1))

  Json.toJson(a1)(hogeFormat)
  Json.toJson(a2)(hogeFormat)

jsonによるリクエストを簡易チェック

  • fieldがOptionならNull許容となる
  • 下記のケースだと型が正しいことのみがチェック対象となる
  • より細かく、たとえばEmail形式だとか、値の範囲を指定するだとかの場合はここの Writing Reads[T] combinators を読むとよい
case class Hoge(hoge:String, fuga:List[Int], piyo:Option[String])

val jsonValidate = Json.format[Hoge] // 下記のようにチェックだけならJson.readsでOK

def putUrl = Action(parse.json){ request =>
  request.body.validate[Hoge](jsonValidate).map {
    case x:Hoge => Ok(...
  }.recoverTotal {
    e => BadRequest("Detected error:" + JsError.toFlatJson(e))
  }
}
  • Action(parse.json) で受けているのでcontentTypeを指定しないとエラーとなる
successJsonData1 = {hoge: "hoge", fuga: [1,2,3]}
successJsonData2 = {hoge: "hoge", piyo: "hoge", fuga: [1,2,3]}
successJsonData3 = {hoge: "hoge", hogee: "hoge", fuga: [1,2,3]}
errorJsonData1 = {hogee: "hoge", fuga: [1,2,3]}
errorJsonData2 = {hoge: "hoge", fuga: ["hoge","fuga"]}
test = (jsonData) ->
  $.ajax
    contentType: 'text/json'
    type: 'PUT'
    url: '/uri'
    data: JSON.stringify(jsonData)
    success: (data) ->
      // todo
    error: (data) ->
      // todo

case class Hoge(List[Int]) で Json.format[Hoge] がエラーに?

case class Hoge(List[Int]) みたいにfieldがひとつでそれがCollectionだと Json.format[A]で

error: No apply function found matching unapply parameters

となる

回答

This is not really a bug but a limitation of current API. らしい

下記(リンク先まんまパクっているだけだが)の様に自分で対応ればOK。

case class A(value: List[Int])

val areads = (__ \ 'value).read[List[Int]].map{ l => A(l) } // covariant map

val awrites = (__ \ 'value).write[List[Int]].contramap{ a: A => a.value } // contravariant map

val aformat = (__ \ 'value).format[List[Int]].inmap(  (l: List[Int]) => A(l), (a: A) => a.value ) // invariant = both covariant and contravariant


case class A(value: Option[List[Int]]) なら readNullable とかにすればOk

2.0からの移行

// Build.scala

import play.Project._ //←パッケージ名が変わっている

  // anorm使うなら追記する
  val appDependencies = Seq(
    // Add your project dependencies here,
    jdbc,
    anorm
  )
  
  val main = play.Project(appname .... // ← これも変わっている

// build.properties
sbt.version=0.12.2

// plugin.sbt
addSbtPlugin("play" % "sbt-plugin" % "2.1.0")

いちどplay new hoge で空のプロジェクト作って差分をみるのがいいかも

sub project を持った場合の publicの扱い

以下の様な設定だとする

#conf/routes

-> /hoge hoge.Routes

GET     /*file               controllers.Assets.at(path="/public", file)

#hoge/conf/routes

GET     /*file               controllers.hoge.Assets.at(path="/public", file)

#大体のツリー構造
.
├── app
├── conf
├── hoge
│   ├── app
│   ├── conf
│   └── public
│       └── fuga.html ※1
└── public
    └── fuga.html ※2

このとき /hoge/fuga.html にアクセスに行くと ※1 にアクセスしてほしい!!!

が、残念ながら ※2 にいく

出来上がるリソースファイルが被るんだろうと想像(確認してないw)

現状妥協したやつ

#hoge/conf/routes

GET     /*file               controllers.hoge.Assets.at(path="/public/hoge", file)

#大体のツリー構造
.
├── app
├── conf
├── hoge
│   ├── app
│   └── conf
└── public
    ├── fuga.html
    └── hoge
        └── fuga.html


public は mainプロジェクトに集約したほうがいいかもね

ってことはassetsもか…な?

何れにしても PlayそのものはApiサーバとして、フロント側を分離することを考えると分離箇所が一箇所になるしそういうものと割り切る。

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment