Real World HTTP 第2版 ―歴史とコードに学ぶインターネットとウェブ技術
- 作者:渋川 よしき
- 発売日: 2020/04/21
- メディア: 単行本(ソフトカバー)
初版も持っていたけど、コレ1冊あればおおよそ俯瞰できるので、2版も購入。
ちょっと分厚いけど、まずは前半だけでも読めばOKだし、読み切るより、リファレンスとして知りたいことが有る時に、入り口として使った方がいいかも。
あと、Go以外で実装してみるのも良いかも。
Real World HTTP 第2版 ―歴史とコードに学ぶインターネットとウェブ技術
初版も持っていたけど、コレ1冊あればおおよそ俯瞰できるので、2版も購入。
ちょっと分厚いけど、まずは前半だけでも読めばOKだし、読み切るより、リファレンスとして知りたいことが有る時に、入り口として使った方がいいかも。
あと、Go以外で実装してみるのも良いかも。
import A.{given _}
と書くと、パッケージAの中でgiven tc as TC
と指定した内容がインポートされます。
PerlのExporterモジュールで@EXPORT
にエキスポートしたい関数を指定するみたいな感じですね。
このPR、単純な依存ライブラリのアップデートのはずがScala 2.11だけテストが失敗している。
エラーメッセージを見ると、サーバ側がstatus code500を返して、テストが失敗していることが分かる。
[info] [info] MyScalatraServletTests: [info] [info] - GET / on MyScalatraServlet should return status 200 *** FAILED *** [info] [info] 500 did not equal 200 (MyScalatraServletTests.scala:11) [info] [info] Run completed in 2 seconds, 18 milliseconds.
しかし、テスト結果からはアプリケーション内で何が失敗して500になっているのか分からないので、同じコードをデバッガで実行しながら確認することにした。そうすると下記の例外が発生していることを突き止めた。
Failure(java.lang.AbstractMethodError: Receiver class org.scalatra.SinatraPathPatternParser does not define or inherit an implementation of the resolved method 'abstract scala.util.DynamicVariable scala$util$parsing$combinator$Parsers$$lastNoSuccessVar()' of interface scala.util.parsing.combinator.Parsers.)
scala-parser-combinatorsのlastNoSuccessVar
というメソッドが無くなっているそうだ。確かに、以下のコミットで削除されている。
このメソッドが削除されたのはscala-parser-combinatorsの1.1.2というバージョンからだ。そしてこの変更によりScala 2.11での非互換が発生していることも既に分かっている。
そのため、scala-parser-combinatorsを使うときは、ターゲットとするScalaのバージョンに合わせて以下のように、バージョンを切り替える方法が取られる(面倒くさい…)。
private val parserCombinatorVersion = Def.setting( CrossVersion.partialVersion(scalaVersion.value) match { case Some((2, 11)) => // https://github.com/scala/scala-parser-combinators/issues/197 "1.1.1" case _ => "1.1.2" } )
確かにこうやって互換性の有るバージョンが指定されているはずなのに…何故か非互換のある1.1.2が使われているのと同じエラーが出ている。
ほかにscala-parser-combinatorsを使っているライブラリが無いか調べてみると、テンプレートエンジンであるTwirlが使っていることが分かった。
しかも一律1.1.2を指定している。こっちがクラスパスに存在するためだ。
しかし、Twirlはscala-parser-combinatorsの中でも非互換が発生するパッケージ(scala.util.parsing.combinator
)は使用しておらず(scala.util.parsing.input
のみ利用)、Scala 2.11をサポートしているにも関わらず非互換によるエラーが発生しないのだ。
原因は分かった…あとは解決策
Twirl側に読み込むライブラリを分岐させるロジックを入れる方法も有るが、それほど前向きな話でもない…そもそもScalatra.g8は新規のプロジェクトを作るときのテンプレートライブラリだ。これからScalatraの新規プロジェクトが2.11で始められる可能性は低い(アップデートは有ると思っている)。
というわけで、ここはScalatra.g8からScala 2.11のテストを落とす、というのが妥当である(サポートしない)、という結論に至った。
ScalatraのTwirlサポートは後から入った機能なので、Scala 2.11ベースで動かしている人たちは、今すぐ問題になることは無いと思われるのでScalatra本体のScala 2.11サポートを今すぐ落とす必要は無いけど、やはり古いバージョンはどこかで切る必要が出てくると思う。
このようにアドバイスも頂いた。
はい。そもそも、(一部さっき言ったことの繰り返しになるけど)それなりな数のライブラリがもうScala 2.11切り捨て始めていて、どうせいつかは切り捨てるものなので、そこ頑張るくらいならそろそろdotty対応とか、他のことを頑張った方がいいというか
— Kenji Yoshida (@xuwei_k) April 29, 2020
ライブラリの非互換性の問題が分かりづらい形で出てくるときも有るね、という話でした。
調べたことを雑に残すシリーズです。
Spray-Jsonの基本的な使い方は以下の通りです。
case class Color(name: String, red: Int, green: Int, blue: Int) object MyJsonProtocol extends DefaultJsonProtocol { implicit val colorFormat = jsonFormat4(Color) } import MyJsonProtocol._ import spray.json._ val json = Color("CadetBlue", 95, 158, 160).toJson val color = json.convertTo[Color]
jsonFormatN
というメソッドで指定するフィールドの個数だけは指定させているのはなぜでしょう?
jsonFormatNメソッドは以下のソースコードで定義されています。
ただし、これはテンプレートであり、実際のコードはsbt-boilerplate
というpluginで生成されるようになっています。Scalaでおなじみの引数が1〜22個までの同じ挙動のメソッドを自動で生成するための仕組みになっています。
一度コンパイルすると、以下の場所にソースが生成されます。
spray-json/target/scala-2.12/src_managed/main/spray/json/ProductFormatsInstances.scala
例えば引数を二つ取るjsonFormat2
は以下のようなコードに展開されます。
// Case classes with 2 parameters def jsonFormat2[P1 :JF, P2 :JF, T <: Product :ClassTag](construct: (P1, P2) => T): RootJsonFormat[T] = { val Array(p1, p2) = extractFieldNames(classTag[T]) jsonFormat(construct, p1, p2) } def jsonFormat[P1 :JF, P2 :JF, T <: Product](construct: (P1, P2) => T, fieldName1: String, fieldName2: String): RootJsonFormat[T] = new RootJsonFormat[T]{ def write(p: T) = { val fields = new collection.mutable.ListBuffer[(String, JsValue)] fields.sizeHint(2 * 3) fields ++= productElement2Field[P1](fieldName1, p, 0) fields ++= productElement2Field[P2](fieldName2, p, 1) JsObject(fields.toSeq: _*) } def read(value: JsValue) = { val p1V = fromField[P1](value, fieldName1) val p2V = fromField[P2](value, fieldName2) construct(p1V, p2V) } }
先ほどの implicit val colorFormat = jsonFormat4(Color)
は、オブジェクトを渡していますが、以下の仕組みにより、実際にはColor.applyが渡されているのと同等です。
つまり、そのcase classのファクトリメソッドが渡されているわけです。
write
はJSON astを生成するメソッドなので、ここで必要になってくるのはread
の方です。
先ほどのreadメソッドの中を一つ一つ見ていきましょう。
二つのフィールドをJSON astが格納されているvalue
からfromField
メソッドを使って取り出しているようですね。続いてfromField
メソッドの中身を見ていきましょう。
fromField
メソッドは、src/main/scala/spray/json/ProductFormats.scala
で以下のように定義されています。
protected def fromField[T](value: JsValue, fieldName: String) (implicit reader: JsonReader[T]) = value match { case x: JsObject if (reader.isInstanceOf[OptionFormat[_]] & !x.fields.contains(fieldName)) => None.asInstanceOf[T] case x: JsObject => try reader.read(x.fields(fieldName)) catch { case e: NoSuchElementException => deserializationError("Object is missing required member '" + fieldName + "'", e, fieldName :: Nil) case DeserializationException(msg, cause, fieldNames) => deserializationError(msg, cause, fieldName :: fieldNames) } case _ => deserializationError("Object expected in field '" + fieldName + "'", fieldNames = fieldName :: Nil) }
ちょっと複雑で分かりづらいですが…
元々、case classの引数を取得しているはずなので、JsObjectとして値が取れないと例外なのは妥当ですね。
read
は取り出す値の型ごとに用意されていて、例えばStringとして値を取得したい場合は以下のメソッドが呼ばれます。
implicit object StringJsonFormat extends JsonFormat[String] { def write(x: String) = { require(x ne null) JsString(x) } def read(value: JsValue) = value match { case JsString(x) => x case x => deserializationError("Expected String as JsString, but got " + x) } }
JsStringであれば、値が取り出され、それ以外は例外になることが分かります。
基本的な型については、src/main/scala/spray/json/BasicFormats.scala
で定義されています。
こうやって値を一つ一つ生成していって、最終的にはScalaのcase classからのオブジェクトがapplyメソッド(相当)で生成されます。
ランタイムリフレクションを使って、メソッドの引数と型を一つ一つ特定する方法もありますが、こちらの方がシンプルですね。
ということで、Spray-JSONが引数の個数を指定させるのは、case classのオブジェクトが型情報を引き回してくれることを利用して型を特定する方法を使うため、ということが分かりました。
タイトルで全部言い切っているんだけど、先日Meraki Goを導入したらApple Musicにつながらなくなったので、その解決までの道のり。
普通のウェブサイトへの接続とか、特に動作がおかしいとかもなく普通に使っていたんだけど、よく見るとApple Musicが反応しなくなっている。
別のアクセスポイント経由では普通につながる。
Meraki GoではブリッジモードとNATモードが有り、最初NATモードになっていたのでリブートしてブリッジモードに変更したことが原因かな?と思ったけど、ちょっと違う。
[https://documentation.meraki.com/Go/Meraki_Go-When_Bridge_Mode_is_not_Available_(Auto_NAT)/jp:embed:cite]
macOSのネットワーク設定で変えられるところと言えばIP v6くらい。試しにLocal-Link mode(LANの外にIP v6を使わない)に変更してみると…繋がる!
しかし、iOSには同様の設定は無い…
一瞬Meraki GoがIPv6を上手く扱えないのかと疑ったけど(Meraki GoのアプリにはIPv4のアドレスしか表示されないので)、さすがにドキュメントを読むとフルサポートと書かれている。
普段全然アクセスしないフレッツのルーターにログインしてみると、IPv6のPPoEが切断状態になっている!というか、IPv6のPPoEの設定ができたんだ…
これを有効にしたところ、Apple Musicにつながるようになった。
しかし、以前のWi-Fiルータもブリッジモードでしか使っていなかったのに、この差はいったいなんだったんだろう…
コンストラクタのフィールドを得る方法について
import scala.reflect.runtime.{universe => ru} import ru._ class Person(val name: String, age: Int, blah: List[Int]) object ReflectionTest { def main(args: Array[String]): Unit = { def generate[T](implicit tag: TypeTag[T]): Unit = { val typeSymbol = typeOf[T].typeSymbol // 型のSymbolを得る val constructorSymbol = typeSymbol.asClass.typeSignature.decl(termNames.CONSTRUCTOR).asMethod // コンストラクタ val list = constructorSymbol.paramLists(0) // Listが空の時は、def test = {}みたいな定義が行われているとき // https://stackoverflow.com/questions/27473440/scala-get-constructor-parameters-at-runtime val params = list.reverse.foldRight(Map(): Map[String, ru.Type])((p, a) => { a + (p.name.decodedName.toString -> p.typeSignature) }) params.foreach { p => println(p) } } generate[Person] } }
一つ前のエントリの続き
次はcase classではなく、普通のクラスを生成する
import scala.reflect.runtime.{universe => ru} import ru._ class Person(val name: String) object ReflectionTest { def main(args: Array[String]): Unit = { def generate[T](implicit tag: TypeTag[T]): T = { val typeSymbol = typeOf[T].typeSymbol // 型のSymbolを得る val constructorSymbol = typeSymbol.asClass.typeSignature.decl(termNames.CONSTRUCTOR).asMethod // コンストラクタ val mirror = scala.reflect.runtime.currentMirror // 現在のミラーを取得する val cm = mirror.reflectClass(typeSymbol.asClass) // Class Mirrorを得る val constrcutorMirror = cm.reflectConstructor(constructorSymbol) constrcutorMirror("John").asInstanceOf[T] // 実行する } println(generate[Person].name) // 生成したオブジェクトが得られる } }