LoginSignup
7

More than 5 years have passed since last update.

Crystal 0.7.4の時代に作ったbotを0.9.1に対応させてStreamingを使う

Posted at

この投稿は、Crystal Advent Calendar 2015の20日目のもの。

ひとまず 0.9.1で動くようにする

Crystalはまだまだ開発中のため破壊的変更が随所で行われている。その影響で昔作ったこのbotがいろいろと動かなかったため、動くところまで変更に追従した。
新しいバージョンへの移行例としてちょうど良い題材だったから残しておく。

実行結果

Crystal側の変更点

修正diff

というスタイルで記述していく。

CGIモジュールが無くなっていた

Error in ./src/main.cr:1: while requiring "./crybot"

require "./crybot"
^

in ./src/crybot.cr:2: while requiring "cgi": can't find file 'cgi' relative to '/Users/mihyaeru/pj/crystal/crybot/src'

require "cgi"
^

0.9.0で変わったらしい。

(breaking change) The CGI module's funcionality has been moved to URI and HTTP::Params

これでOK。

diff --git a/src/crybot.cr b/src/crybot.cr
index 0fca99d..05487ae 100644
--- a/src/crybot.cr
+++ b/src/crybot.cr
@@ -1,5 +1,5 @@
 require "./crybot/*"
-require "cgi"
+require "uri"

 class Crybot
     def initialize
@@ -26,7 +26,7 @@ class Crybot
                         name = user["screen_name"]?
                         text = (text as String).gsub("@crybot21 ", "")
                         tweet_test = "@#{name} #{text}"
-                        p self.tweet(CGI.escape(tweet_test))
+                        p self.tweet(URI.escape(tweet_test))
                         p tweet_test
                     end
                 end

Timeのインタフェースが変わっていた

Error in ./src/main.cr:6: instantiating 'Crybot#user_stream()'

bot.user_stream
    ^~~~~~~~~~~

in ./src/crybot.cr:18: instantiating 'Client#start_stream(String, String)'

        @client.start_stream "GET", "/user.json", do |body|
                ^~~~~~~~~~~~

in ./src/crybot/client.cr:32: instantiating 'make_request(String, String, String)'

        request = make_request(method, STREAM_HOST, path)
                  ^~~~~~~~~~~~

instantiating 'make_request(String, String, String, Nil)'

in ./src/crybot/client.cr:68: undefined method 'to_i' for Time (did you mean 'to_s'?)

        request.headers["Authorization"] = @signature.authorization_header(request, true, Time.utc_now.to_i.to_s, SecureRandom.hex(32))
                                                                                                       ^~~~

================================================================================

Time trace:

  /Users/mihyaeru/.anyenv/envs/crenv/versions/0.9.1/src/time/time.cr:224

      def self.utc_now
               ^~~~~~~

  /Users/mihyaeru/.anyenv/envs/crenv/versions/0.9.1/src/time/time.cr:225

        new utc_ticks, Kind::Utc
        ^~~

0.7.6で変わったらしい。

(breaking change) Renamed Time.at to Time.epoch, and Time#to_i and Time#to_f to Time#epoch and Time#epoch_f

これでOK。

diff --git a/src/crybot/client.cr b/src/crybot/client.cr
index 07a7799..c6d7868 100644
--- a/src/crybot/client.cr
+++ b/src/crybot/client.cr
@@ -65,7 +65,7 @@ class Client
         request = HTTP::Request.new(method, API_VERSION + path, body: body)
         request.headers["Host"]          = host
         request.headers["Content-type"]  = "application/x-www-form-urlencoded" if method == "POST"
-        request.headers["Authorization"] = @signature.authorization_header(request, true, Time.utc_now.to_i.to_s, SecureRandom.hex(32))
+        request.headers["Authorization"] = @signature.authorization_header(request, true, Time.utc_now.epoch.to_s, SecureRandom.hex(32))
         return request
     end

IOのインタフェースが変わっていた

Error in ./src/main.cr:6: instantiating 'Crybot#user_stream()'

bot.user_stream
    ^~~~~~~~~~~

in ./src/crybot.cr:18: instantiating 'Client#start_stream(String, String)'

        @client.start_stream "GET", "/user.json", do |body|
                ^~~~~~~~~~~~

in ./src/crybot/client.cr:47: no overload matches 'OpenSSL::SSL::Socket#read' with types Int32
Overloads are:
 - OpenSSL::SSL::Socket#read(slice : Slice(UInt8))
 - OpenSSL::SSL::Socket#read(slice : Slice(UInt8))

            line += io.read(chunk_size)
                       ^~~~

0.9.0で変わったらしい。

(breaking change) IO#read() is now IO#gets_to_end. Removed IO#read(count), added IO#skip(count)

これでOK...

diff --git a/src/crybot/client.cr b/src/crybot/client.cr
index c6d7868..94ae2a6 100644
--- a/src/crybot/client.cr
+++ b/src/crybot/client.cr
@@ -44,7 +44,7 @@ class Client
         # read body
         line = ""
         while (chunk_size = io.gets.not_nil!.to_i(16)) > 0
-            line += io.read(chunk_size)
+            line += io.gets(chunk_size) as String
             io.gets # Read \r\n

             if line =~ /^\s+$/

と思いきや、実行はできたけども実行時エラーに。

{"friends" => [198177363]}
invalid Int32:
 (ArgumentError)
[4347956178] *CallStack::unwind:Array(Pointer(Void)) +82
[4347956081] *CallStack#initialize<CallStack>:Array(Pointer(Void)) +17
[4347956040] *CallStack::new:CallStack +40
[4347955297] *Exception@Exception#initialize<ArgumentError, String>:CallStack +33
[4347955229] *ArgumentError#initialize<ArgumentError, String>:CallStack +29
[4347955169] *ArgumentError::new<String>:ArgumentError +97
[4347945122] *String#to_i32<String, Int32, Bool, Bool, Bool, Bool>:Int32 +226
[4347944885] *String#to_i<String, Int32, Bool, Bool, Bool, Bool>:Int32 +37
[4347950319] *String#to_i<String, Int32>:Int32 +79
[4348087281] *Crybot#user_stream<Crybot>:Nil +625
[4347913599] __crystal_main +24159
[4347923200] main +32

どうやらread(count)gets(count)では読み取り時の挙動が若干違うらしく、io.gets"\r\n"を読み飛ばす辺りで読み飛ばせず、次のループのio.gets.not_nil!.to_i(16)にて"\r\n".to_i(16)になるように変わったようだ。

これで今度こそ動いた。

diff --git a/src/crybot/client.cr b/src/crybot/client.cr
index 94ae2a6..1fa6282 100644
--- a/src/crybot/client.cr
+++ b/src/crybot/client.cr
@@ -43,9 +43,12 @@ class Client

         # read body
         line = ""
-        while (chunk_size = io.gets.not_nil!.to_i(16)) > 0
+        while true
+            chunk_size_str = io.gets as String
+            next if chunk_size_str == "\r\n"
+            chunk_size = chunk_size_str.to_i(16)
+
             line += io.gets(chunk_size) as String
-            io.gets # Read \r\n

             if line =~ /^\s+$/
                 line = ""

Streamingを使う

このbotを書いた2015年7月29日時点ではHTTP::ClientにStreaming機能が存在しておらずSocket周りを頑張って自前で実装した。東京 Crystal 勉強会 #1でこのbotの制作をLTした同日、7月30日に公開された0.7.5でStreamingがサポートされるというタイミングの悪さを発揮した。
発表以降、とくに何も手を加えていなかったが、Advent Calendarを機にStreamingの機能を使った版に書き換えてみた。

長々と自前で頑張っていた辛さあふれるこのメソッドが、

def start_stream(method, path)
    request = make_request(method, STREAM_HOST, path)
    io = make_socket(request)
    request.to_io(io)

    # read header
    headers = HTTP::Headers.new
    while line = io.gets
        break if line == "\r\n" || line == "\n"
        name, value = HTTP.parse_header(line)
        headers.add(name, value)
    end

    # read body
    line = ""
    while true
        chunk_size_str = io.gets as String
        next if chunk_size_str == "\r\n"
        chunk_size = chunk_size_str.to_i(16)

        line += io.gets(chunk_size) as String

        if line =~ /^\s+$/
            line = ""
            next
        end

        begin
            yield JSON.parse(line) as Hash
            line = ""
        rescue e
            p e # FIXME: !!!
        end
    end
end

private def make_socket(request)
    socket = TCPSocket.new(request.headers["Host"], 443)
    socket.sync = false
    return OpenSSL::SSL::Socket.new(socket)
end

こんな感じですっきりと書けた。すごい!

def start_stream(method, path)
    request = make_request(method, STREAM_HOST, path)
    @stream_client.exec(request) do |response|
        while true
            line = response.body_io.gets as String
            next if line =~ /^\s+$/
            yield JSON.parse(line) as Hash
        end
    end
end

終わりに

当初の予定ではStreamingの機能を使う変更だけで記事を書こうと思っていたけども、Crystalのバージョンアップで動かなくなった部分が多々あり併記する形にした。新旧バージョンの比較ができて良い感じに収まったと個人的に思っている。

明日の担当は@5t111111さんの予定。

参考

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
7