Blog
util-evalで遊んでみました
こんにちは、GameTailor大城です。
GameTailorにはユーザー1人1人に応じたクリエイティブを生成する機能があります。
例えばユーザーが選択しているキャラクターを背景にする、などです。
この時、そのユーザーに応じたキャラクター画像やキャラの名前等をデータベースから取得する必要がありますが、ゲームタイトルによってユーザーデータの構造や必要な情報が異なる事があります。
この違いを設定ファイルで吸収し、実装から切り離すことが出来ないかな・・?と思いTwitterのutil-evalを調べてみました。
util-evalとは?
Twitter製のConfigライブラリです。ConfigをJSONやYAMLの代わりにScalaで書き、型チェックやコンパイラチェックを効かせるという思想を持っています。
https://github.com/twitter/util#eval
動作を確認したいと思います。
1.build.sbt を用意します
適当なディレクトリを作り、build.sbtを作ります。
1 2 3 4 5 |
// build.sbt scalaVersion := "2.11.4" libraryDependencies += "com.twitter" %% "util-eval" % "6.24.0" |
2.Confgのクラスを用意します。
1 2 3 4 5 6 7 8 9 |
// src/main/scala/config/MyConfig.scala package config trait MyConfig { val firstName: String val lastName: String val company: String val nickname: String } |
3.Configを用意します
1 2 3 4 5 6 7 8 9 |
// config/OrenoConfig.scala import config.MyConfig new MyConfig { val firstName = "kazuma" val lastName = "oshiro" val company = "GameTailor" val nickname = "big" } |
4.読み込みます
1 2 3 4 5 6 7 8 9 |
// src/main/scala/Main.scala import config.MyConfig import com.twitter.util.Eval object Main extends App { val config = Eval[MyConfig](new java.io.File("config/OrenoConfig.scala")) println(s"Hi, ${config.compnay} ${config.firstName} ${lastName}.") } |
5.実行します
1 2 |
$ sbt run Hi, GameTailor Big. |
無事、OrenoConfig.scalaが読み込まれ挨拶することが出来ました。読み込まれた後はScalaのクラス同様に扱い、IDEの補完を効かせることも出来ます。
読み込むコンフィグファイル(上記ではconfig/OrenoConfig.scala)は .scalaファイルではありますが、実行時にコンパイル・評価される為ビルド後でも設定を変更することが可能です。プロジェクト全体を再ビルドする必要はありません。
6.他にも・・・
ファイルからでなく直接文字列を評価することも出来ます。また、関数オブジェクトを評価して変数に格納することも出来ます。
1 2 3 4 5 6 7 8 9 10 11 |
scala> import com.twitter.util.Eval import com.twitter.util.Eval scala> Eval[Int](" 100 * 4 ") res0: Int = 400 scala> val greeting = Eval[(String) => Unit](""" (name: String) => println(s"hello $name") """) greeting: String => Unit = <function1> scala> greeting("zu-ma-") hello zu-ma- |
※ ユーザーの入力値をEvalするのは、意図しないコードを実行してしまうという危険をはらんでいるのでやめましょう。
もっとEvalであることを活かしたい
冒頭で触れたユーザー応じた情報を返す、という部分でEvalを活用できないか考えます。
以下の様な要件があったとします。
前提
- ユーザーのデータはJSONで格納されている
- JSONの構造はゲームタイトルによって異なる
- マスタの解決が必要である
- {“character_id”: 12} といったようなデータがあった場合、12を解決しキャラクターの名前や画像URLを取得しなければいけない
インプット
- ユーザーデータ
- マスタデータ
求めるアウトプット
- クリエイティブを作るために必要な情報
- 例: {“character_name”: “ビッグ・キャッスル”, “character_image_url”: “http://xxxx”}
素直に実装すると以下のようになると思います
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
// jsonライブラリにplay-jsonを使用します import play.api.libs.json._ // DBから取得 val characterMaster = Map( "1" -> Json.parse("""{"name": "ビッグ・キャッスル", "image_url": "http://xxx", "attribute_id": "2"}"""), "2" -> Json.parse("""{"name": "スモール・キャッスル", "image_url": "http://xxx", "attribute_id": "1"}""") ) // DBから取得 val user = Json.parse("""{"user_id": 1, "user_name": "大城", "character_id": "1"}""") // fixme: ここがゲームタイトル固有コードになってしまう val output = Map[String, JsValue]( "character_name" -> characterMaster((user \ "character_id").as[String]) \ "name", "character_image_url" -> characterMaster((user \ "character_id").as[String]) \ "image_url" ) Json.toJson(output) // play.api.libs.json.JsValue = {"character_name":"ビッグ・キャッスル","character_image_url":"http://xxx"} |
キャラクターの名前とイメージURLを取得することが出来ました。
しかし、character_name、character_image_urlを解決するためにcharacter_idをどのマスタデータと突き合わせればよいのかはゲームタイトルによって異なりますし、character_idがprofile_idやcard_idだったりした場合はどう対応しましょう?
うむむ・・・個別の対応が必要そうです。
この箇所は抜き出し、外から動作を変更できるようにすれば、ソースコードを変更せずに違いを吸収することが出来そうです。
マスタデータとユーザーデータをInputにして「ほげほげした結果」をOutputをする
ほげほげの部分を設定値で表現したいと思います。
コード
1.play-jsonの依存を追加します(build.sbtの編集)
jsonを扱うために、play-jsonの依存を追加します。
1 2 3 4 5 6 7 8 9 10 |
// build.sbt scalaVersion := "2.11.4" resolvers += "Typesafe Repo" at "http://repo.typesafe.com/typesafe/releases/" libraryDependencies ++= Seq( "com.typesafe.play" %% "play-json" % "2.3.0", "com.twitter" %% "util-eval" % "6.24.0" ) |
2.Configクラスを書き換えます
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
// src/main/scala/config/MyConfig.scala package config import com.twitter.util.Eval import play.api.libs.json.JsValue trait MyConfig { case class Param(key: String, code: String) { type T = ((String) => (JsValue) => JsValue, JsValue) => JsValue val method: T = { Eval(s"""(master: (String) => (play.api.libs.json.JsValue) => play.api.libs.json.JsValue, user: play.api.libs.json.JsValue) => $code""") } } val paramSettings: Map[Int, Seq[Param]] } |
3.Configファイルに設定を書きます
Paramクラスのcodeへ記述した設定を使用して、ユーザーデータを解決します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
// config/OrenoConfig.scala import config.MyConfig new MyConfig { val paramSettings = Map( 1 -> Seq( Param( key = "character_name", code = """ master("character")(user \ "character_id") \ "name" """), Param( key = "character_image_url", code = """ master("character")(user \ "character_id") \ "image_url" """), Param( key = "character_attribute", code = """ master("attribute")( (master("character")(user \ "character_id")) \ "attribute_id" ) \ "name" """)), 2 -> Seq(/* todo */) ) } |
4.Mainクラスを修正
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 |
// src/main/scala/Main.scala import config.MyConfig import com.twitter.util.Eval import play.api.libs.json.{Json, JsValue} object Main extends App { // config を読み込む val config = Eval[MyConfig](new java.io.File("config/OrenoConfig.scala")) // ユーザーデータを解決する def userDataResolve(settingId: Int, user: JsValue): JsValue = { import DataExample.masterResolver Json.toJson((config.paramSettings(settingId) map (p => p.key -> p.method(masterResolver, user))).toMap) } println(userDataResolve(1, DataExample.user)) } // サンプルデータです object DataExample { val master = Map( "character" -> Map( "1" -> Json.parse("""{"name": "ビッグ・キャッスル", "image_url": "http://xxx", "attribute_id": "2"}"""), "2" -> Json.parse("""{"name": "スモール・キャッスル", "image_url": "http://xxx", "attribute_id": "1"}""") ), "attribute" -> Map( "1" -> Json.parse("""{"name": "火", "image_url": "http://xxx"}"""), "2" -> Json.parse("""{"name": "水", "image_url": "http://xxx"}"""), "3" -> Json.parse("""{"name": "風", "image_url": "http://xxx"}"""), "4" -> Json.parse("""{"name": "地", "image_url": "http://xxx"}""") ) ) val masterResolver = (name: String) => (id: JsValue) => master.get(name).get(id.as[String]) val user = Json.parse("""{"user_id": 1, "user_name": "大城", "character_id": "1"}""") } |
5.実行します
1 2 |
$ sbt run {"character_name":"ビッグ・キャッスル","character_image_url":"http://xxx","character_attribute":"水"} |
少しトリッキーな感じになっている部分もありますが、ゲームタイトル固有な記述をソースコードから切り離すことが出来ました。マスタデータが増えた、別のゲームタイトルも導入する、となった場合にはコンフィグに記述を増やすことで対応することが可能です。
Evalは実行にコストが掛かりますし、ユーザーの入力値を評価してしまうような作りにすると意図しないコードを実行してしまうという危険性を持っていますが、用途を間違えなければ強力な事ができそうです。
今回のサンプルコードは以下にあります。
https://github.com/oshiro-kazuma/util-eval-example
sys.exit //それではまた!
Copyright about logo
Copyright (c) 2002-2015 EPFL
Author