C4SAハンズオン資料 (Ruby)

ンテキストの作成

C4SA へアクセスし、ログイン後にRubyコンテクストを選択してください。

発環境の準備

今回はSinatraを使ってウェブアプリを開発します。 そのためにまずSinatraをC4SA上にインストールする必要があります。

gemファイルの編集

gem "sinatra"

ンドルインストール

バンドルインストールの「ウインドウ」を開き「実行ボタン」からインストールできます。

足: Sinatraとは?

SinatraとはRubyでウェブアプリを開発するためのフレームワークの1つです。 Rubyで有名なフレームワークといえばRuby on Railsがありますが、 SinatraはRuby on Railsに比べて軽量で、少ない量のコードでウェブアプリを実装できるという特徴があります。 使い分けの例としてはデータベースを使うある程度かっちりとしたアプリではRails、 それ以外の軽めのアプリではSinatraといった感じです。

Hello, world

まずはウェブアプリの基礎としてhello worldを作成します。手順は以下の通りです。

app.rbを作成

# -*- encoding: utf-8 -*-
require "rubygems"
require "sinatra"

# トップページ向けに / でアクセスされた場合のルートを定義
get "/" do
    "hello world"
end

config.ruの編集

作成したapp.rbを実行するための設定。元々書かれているのは削除して構いません。

require "./app"

run Sinatra::Application

ーバの再起動

ここまでできたらサーバをリスタートして動作確認をしましょう。

動作確認は上部にあるリンクから行えます。

ラメータの取得

次にユーザからの入力を受け取るようにしましょう。Sinatraでは params という変数にパラメータは格納されているのでそこから取得します。例えば以下のように/resultに対して付与された name=foo パラメータを受け取るようにしましょう。

http://localhost/result?name=foo

get "/" do
    "hello world"
end

# nameパラメータを受け取るルートを追加
get "/result" do
    "hello " + params[:name]
end

編集したらサーバを再起動して確認してみましょう。

ンプレートエンジンの導入

ここまでは単純に文字列を表示するだけでしたが、ウェブアプリなのでHTMLを表示するようにしましょう。 ただHTMLを生で書くのは効率的ではないのでテンプレートエンジンを使ってHTML出力させます。
今回はerbというrubyに標準添付されているテンプレートエンジンを使います。

足: テンプレートエンジンとは?

テンプレートエンジンとは雛形ファイル(テンプレート)とそれに埋め込みたい実データを元に ドキュメントを生成するためのライブラリで、HTML内にプログラムのコードを埋め込むものや、 HTMLに変換するためのDSL(Domain-Specific Language)を用いて より簡潔な記述でHTMLを表現するものなどいろいろな種類があります。

テンプレートエンジンを導入するための手順は以下の通りです。

viewsフォルダーの作成

ンプレートの作成

<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8" />
    <title><%= TITLE %></title>
</head>
<body>
    <h1><%= TITLE %></h1>
    <form method="GET" action="/result">
        <label>アカウント名: <input type="text" name="name" /></label>
        <input type="submit" value="診断する" />
    </form>
</body>
</html>
<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8" />
    <title><%= TITLE %></title>
</head>
<body>
    <h1><%= @name %>さんの診断結果</h1>
    <p><%= @result %></p>

    <p><a href="/">トップへもどる</a></p>
</body>
</html>

app.rbからの呼び出し

# -*- encoding: utf-8 -*-
require "rubygems"
require "sinatra"
require "erb"      # erb の require を追加

# 共通で使うタイトルの定義
TITLE = "foo診断"

get "/" do
    # テンプレートを呼び出し
    # :index が views/index.rb に対応する
    erb :index
end

get "/result" do
    @name   = params[:name]
    @result = "hello " + @name

    erb :result
end

ここまで出来たらサーバを再起動して、トップページで入力フォームが表示されること、 「診断する」ボタンで結果ページが表示されることを確認しましょう。

キュリティ補足: クロスサイトスクリプティング

上記のサンプルでは受け取ったパラメータをそのまま画面に表示しているが、 これだとパラメータに悪意のあるコードを含まれた場合に クロスサイトスクリプティングの攻撃を受けてしまいます。

例えばFirefoxで以下のようなパラメータを付けることで任意のJavaScriptを実行できてしまう(XSS攻撃)ことを確認しましょう。

http://localhost/result?name=%3Cscript%3Ealert%28%27test%27%29%3B%3C%2Fscript%3E

XSS攻撃へ対処するには入力値を画面表示する前にエスケープ処理をしてあげる必要があります。

require "cgi" # CGIライブラリを追加

get "/result" do
    # HTMLに使われる文字をエスケープすることでscriptタグなどを無効にする
    @name   = CGI.escapeHTML params[:name]
    @result = "hello " + @name

    erb :result
end

イアウトの導入

index.erbとresult.erbでは共通する部分がいくつかあります。 今回はこの2ファイルのみですが、テンプレートが増えた場合には 同じものを何度も書くのは非効率的です。 そこでレイアウトを利用しましょう

layout.erbの作成

<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8" />
    <title><%= TITLE %></title>
</head>
<body>
    <!-- yield部分に子テンプレートの内容が挿入される -->
    <%= yield %>
</body>
</html>

index.erb, result.erbの修正

<h1><%= TITLE %></h1>
<form method="GET" action="/result">
    <label>アカウント名: <input type="text" name="name" /></label>
    <input type="submit" value="診断する" />
</form>
<h1><%= @name %>さんの診断結果</h1>
<p><%= @result %></p>

<p><a href="/">トップへもどる</a></p>

テンプレートの修正はサーバの再起動なしに反映されるので ここまで出来たら再起動はせずにブラウザで表示に変わりないか、 layout.erbを編集するとトップページ/結果ページ共に変更が反映されているか確認してみましょう。

断処理

Sinatraでのルート定義、パラメータの受取り方、erbを用いた画面出力までできるようになりました。 ここからはいよいよ診断アプリのコアロジックの実装に入ります。
手順は以下の通りです。

字列を数字に変換

アカウント名から診断結果を導くために第一段階としてアカウント名を整数に変換します。
ここでは単純にアスキーコードの合計値を用いることとします。

hello
104101108108111
#
# アスキーコードの合計値を返す
# ex)
#   puts get_bytes_total "hello"
#   #=> 532
#
def get_bytes_total(s)
    total = 0
    s.each_byte do |i|
        total += i
    end

    total
end

字に対応する診断結果を取得

第二段階として整数から、用意しておいた診断結果の配列の要素を取得します。

#
# 診断結果の配列と整数から1つの診断結果を返す
# ex)
#   # 診断結果
#   result = ["hello", "world", "neko", "inu"]
#   puts get_message(result, 0)
#   #=> "hello"
#
def get_message(arr, num)
    arr[num % arr.size]
end

ここまで出来るとget_bytes_totalget_messageを組み合わせて 例えば以下のようにしてアカウント名から診断結果を取得できます。

###################################
#   C4SA上には書かないでください
###################################
result = ["hello", "world", "neko", "inu"]

puts get_message(result, get_bytes_total("foo"))
# => "hello"
puts get_message(result, get_bytes_total("bar"))
# => "world"

毎に結果を変更する

ただし今の処理だとアカウント名が同じ場合にはいつも同じ結果しか表示されません。 そこで日毎に結果が変わるように修正しましょう。

name   = params[:name]
# 今日の日付を文字列で取得
today  = Time.now.strftime("%Y%m%d")
# 毎秒変えて確認する場合はこちら
#today  = Time.now.strftime("%Y%m%d%H%M%S")

# アスキーコードの合計値を取得する際に(アカウント名+今日の日付)の合計値を取得することで
# 日毎の合計値に違いを付ける
num    = get_bytes_total(name + today)
result = get_message(RESULT, num)

果を表示

診断結果を取得する処理ができたので、これをアプリに反映させましょう。
なお以下の例では1つの診断結果ではなく、3つの診断結果を用意しておいて それを1つの文字列にまとめるようにしています。

# -*- encoding: utf-8 -*-
require "rubygems"
require "sinatra"
require "erb"
require "cgi"

# タイトルを変更
TITLE  = "リア充診断"

# 診断結果を定義
MSG_A  = [0, 1, 50, 100, 1000, 10000, 1000000]
MSG_B  = ["3秒", "10分", "1週間", "半年", "1年", "50年"]
MSG_C  = [-1000, -100, -50, 0, 1, 50, 100, 1000, 10000, 1000000]
FORMAT = "あなたのリア充レベルは%dで、%s後にはレベル%dになるでしょう!"

# アスキーコードの合計値を返す
def get_bytes_total(s)
    total = 0
    s.each_byte do |i|
        total += i
    end

    total
end

# 診断結果の配列と整数から1つの診断結果を返す
def get_message(arr, num)
    arr[num % arr.size]
end

# トップページの処理は変更なし
get "/" do
    erb :index
end

# 結果ページで診断結果を表示するよう修正
get "/result" do
    name = params[:name]
    num = get_bytes_total(name)

    @name   = CGI.escapeHTML(name)
    @result = sprintf(FORMAT,
                get_message(MSG_A, num),
                get_message(MSG_B, num),
                get_message(MSG_C, num))

    erb :result
end

編集が完了したらサーバを再起動して確認してみましょう。

Twitterへの投稿リンクを追加する

結果の表示まで出来たら次はTwitterで共有できるようにしましょう。
ここではOAuth認証連携までは時間がなくてできないので、 単純にリンクでの連携までにします。

足: Twitter投稿リンク

http://twitter.com/?status=... でTwitterへの投稿用リンクが作れます。
例) http://twitter.com/?status=投稿リンクテスト

足: 環境変数

request.envで環境変数にアクセスでき、request.env['HTTP_HOST']で アプリが動いているホスト名を取得することができます。 自アプリへのリンクを貼る場合にはハードコーディングでも良いですが、 環境変数を使って生成するという方法もあります。

<!-- Twitter共有リンク -->
<p><a target="_blank" href="http://twitter.com/?status=<%= TITLE %>: <%= @result %>
                            http://<%= request.env['HTTP_HOST'] %>">
    &raquo; Twitterで共有する
</a></p>

テンプレートの修正のみなので再起動は不要です。ブラウザで確認してみましょう。

Twitter bootstrapの導入

とりあえず結果が表示できるところまではできました。 が、HTMLは最低限のものなので見た目がかなり貧相です。
デザインが出来る人であればデザインしてもいいのですが、 デザインができない/自身がない場合にはフレームワークを導入すると早いです。
今回はTwitter Bootstrapでデザインを整えてみます。 手順は以下の通りです。

Bootstrapのダウンロード

Bootstrapのページへ行き、 Download Bootstrapボタンからダウンロードしてください。

C4SAへの展開

C4SAではアップロードするファイルがZIPの場合には自動で展開してくれる機能があります。 解凍してからアップロードでは面倒なので、今回はその機能を使います。

足: publicフォルダ

publicフォルダは静的ファイルを置くためのフォルダで、 public配下に置かれたファイル、例えば public/foo.png は http://.../foo.png でアクセスできます。(publicという文字はURLに含まれません)

ンプレートの修正

Bootstrapが有効になるようテンプレートを編集しましょう。
今回ここで編集する内容はBootstrapのドキュメントにも書いてあることなので より細かくBootstrapを使いたい場合にはドキュメントを参照して挑戦してみてください。
手順は以下の通りです。

layout.erbの編集

共通のレイアウトファイルではCSS、Javascripを主に読み込みます。

<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8" />
    <title><%= TITLE %></title>
    <!-- Bootstrapのスタイルシートを追加 ここから -->
    <link rel="stylesheet" type="text/css" href="/bootstrap/css/bootstrap.min.css" />
    <!-- /Bootstrapのスタイルシートを追加 ここまで -->
</head>
<body>
    <!-- コンテナを追加 ここから -->
    <div class="container">
        <%= yield %>
    </div>
    <!-- /コンテナを追加 ここまで -->

    <!-- BootstrapのJavascriptを追加 ここから -->
    <script type="text/javascript" src="/bootstrap/js/bootstrap.min.js"></script>
    <!-- /BootstrapのJavascriptを追加 ここまで -->
</body>
</html>

index.erbの編集

トップページではHero unitを使います。

<!-- ドキュメントに従って記述 ここから -->
<div class="hero-unit">
    <h1><%= TITLE %></h1>
    <p>Twitterのアカウント名から~を診断します。</p>

    <!-- form内は今まで通り -->
    <form method="GET" action="/result" class="well form-inline">
        <label>アカウント名: <input type="text" name="name" /></label>
        <input type="submit" class="btn btn-primary" value="診断する" />
    </form>
</div>
<!-- /ドキュメントに従って記述 ここまで -->

result.erbの編集

結果ページではPage headerWells、 またIcon glyphsを利用します。

<!-- Page headerを適用 ここから -->
<div class="page-header">
    <h1><%= @name %>さんの診断結果</h1>
</div>
<!-- /Page headerを適用 ここまで -->

<!-- Wellsを適用 ここから -->
<div class="well well-large">
    <p><%= @result %></p>
    <p><a target="_blank" href="http://twitter.com/?status=<%= TITLE %>: <%= @result %> http://<%= request.env['HTTP_HOST'] %>">
        <!-- Iconglyphsを利用 -->
        <i class="icon-share"></i>Twitterで共有する
    </a></p>
</div>
<!-- /Wellsを適用 ここまで -->

<!-- Iconglyphsを利用 -->
<p><a href="/"><i class="icon-home"></i>トップへ戻る</a></p>

今回もテンプレートの修正だけだったので、サーバは再起動せずにブラウザで確認してみましょう。 どうでしょう?少しは見れるページになったでしょうか?

足: Bootstrapのデザイン

Bootstrapを使うと綺麗なページを簡単に作ることができますが、 Bootstrapを使ったサイトが溢れてくるとどれも同じページに見えてしまうようになります。
Bootstrapを使いつつ他サイトとの差別化を図りたい場合には Bootswatchで配布されているBootstrap向けのテーマファイルを利用したり、 WrapBootstrapで販売しているテーマファイルを利用すると テンプレートに手を加えることなくデザインを変えることができます。

+α

これ以降はハンズオンでは扱わない内容になります。 時間が余ってしまった人は是非挑戦してみてください!

モリさんとモリタさんが同じ結果になってしまうのを防ぐ

アカウント名が abc, cba といったアナグラムになっている場合、 get_bytes_totalの計算結果が同じ値になってしまうため 違うアカウント名なのに必ず同じ結果が返ってしまいます。
今回はこれをハッシュ値を使って解決してみましょう。

# 他ライブラリのrequireは省略
# ...

# SHA1のライブラリを読み込み
require "digest/sha1"

# 他処理のコードは省略
# ...

get "/result" do
    name  = params[:name]
    today = Time.now.strftime("%Y%m%d")
    # (name + today) をハッシュ化してユニークな文字列にする
    num   = get_bytes_total(Digest::SHA1.hexdigest(name + today))

    @name   = CGI.escapeHTML(name)
    @result = sprintf(FORMAT,
                get_message(MSG_A, num),
                get_message(MSG_B, num),
                get_message(MSG_C, num))

    erb :result
end

題: アナグラムへの対応

今回はハッシュ化の例を出しましたが他の解決策がないか考えてみましょう!

断結果メッセージの外部ファイル化

現状、診断結果のメッセージはプログラムの中に埋め込んでいますが それだと変更がある度にサーバの再起動が必要になり面倒です。
そこで外部にメッセージファイルを作成してそれを読み込むようにしてみましょう。 手順は以下の通りです。

部ファイルの作成

まず外部ファイルのフォーマットの仕様を決めましょう。 診断結果の個数, 個数分の診断結果メッセージ, 出力フォーマット の3つの情報がわかれば良いので それが判別できるよう記述されていれば問題ありません。
ただファイル形式を独自のものにしてしまうと汎用性がなくなってしまうので ここではYAML(YAML Ain't Markup Language)を使います。

format: "あなたのリア充レベルは%dで、%s後にはレベル%dになるでしょう!"
messages:
 - [0, 1, 50, 100, 1000, 10000, 1000000]
 - ["3秒", "10分", "1週間", "半年", "1年", "50年"]
 - [-1000, -100, -50, 0, 1, 50, 100, 1000, 10000, 1000000]

これはrubyのコードでいうと以下と同じになります。

{"format"   => "あなたのリア充レベルは%dで、%s後にはレベル%dになるでしょう!"
 "messages" => [[0, 1, 50, 100, 1000, 10000, 1000000],
                ["3秒", "10分", "1週間", "半年", "1年", "50年"],
                [-1000, -100, -50, 0, 1, 50, 100, 1000, 10000, 1000000]]}

app.rb の編集

では実際にYAMLからデータを読み込んで結果表示させる処理を書きましょう。

# 他ライブラリのrequireは省略
# ...

# YAMLライブラリを読み込み
require "yaml"

# 他処理のコードは省略
# ...

get "/result" do
    name  = params[:name]
    today = Time.now.strftime("%Y%m%d")
    num   = get_bytes_total(Digest::SHA1.hexdigest(name + today))

    # messages.yamlをrubyの形式で読み込む
    data = YAML.load_file("messages.yaml")
    # messagesには診断結果リストの配列が入っているので
    # それぞれの配列に対して診断結果を求める
    # この時点で msgs は以下のような値を持つ
    # => ["1つ目の診断の結果", "2つ目の診断の結果", "3つ目の診断の結果"]
    msgs = data["messages"].map do |msg_list|
        get_message(msg_list, num)
    end
    @name   = CGI.escapeHTML(name)
    # sprintf(data["format"], msgs[0], msgs[1], msgs[2]) と同意
    @result = sprintf(data["format"], *msgs)
    erb :result
end

アプリのコードを変更したのでサーバを再起動して確認してみましょう。 またmessages.yamlを変更してすぐに結果に反映されることも確認してみましょう。

数の診断に対応

今のところ「~診断」という1つの診断をアプリ内で作って来ました。 ただ今後診断内容を増やしたいと思った時に毎回別アプリで構築するのは面倒です。
そこで1つのアプリ内で複数の診断ができるよう修正しましょう。 手順は以下の通りです。

数の診断向けにmessages.yamlを修正

前回のmessages.yamlを一段回階層を下げて、その上に診断名のキーとなる文字列を定義します。
また今までは定数として持っていたタイトルも設定ファイル内で定義するように変更します。

default:
  title: "リア充診断"
  format: "あなたのリア充レベルは%dで、%s後にはレベル%dになるでしょう!"
  messages:
   - [0, 1, 50, 100, 1000, 10000, 1000000]
   - ["3秒", "10分", "1週間", "半年", "1年", "50年"]
   - [-1000, -100, -50, 0, 1, 50, 100, 1000, 10000, 1000000]

programming:
  title: "プログラミング言語診断"
  format: "あなたが使うべき言語は%sで、PaaSは%sがいいでしょう!"
  messages:
   - ["Ruby", "Java", "Clojure", "JavaScript", "Perl", "PHP", "Scala", "Python"]
   - ["Nifty C4SA", "DotCloud", "Windows Azure", "Google Appengine", "Heroku"]

app.rbを編集

次にapp.rbを新しい設定ファイルに対応させます。
今回は診断毎のタイトルを設定ファイルで持っているのでトップページを表示する際にも 設定ファイルを読み込む必要があり、結果ページでも勿論設定ファイルを読み込みます。 このように複数のルートで共通の処理がある場合にはbefore, afterルートが便利です。
その名の通りbeforeは各ルートが実行される前に、afterは実行された後に共通して実行されるルートです。

# -*- encoding: utf-8 -*-
require "rubygems"
require "sinatra"
require "erb"
require "cgi"
require "digest/sha1"
require "yaml"

def get_bytes_total(s)
    total = 0
    s.each_byte do |i|
        total += i
    end

    total
end

def get_message(arr, num)
    arr[num % arr.size]
end

# ここまで変更なし

before do
    # kindパラメータ: default以外の診断をする場合に指定
    # 未指定の場合には "default" が使われる
    kind  = params[:kind] || "default"
    # 今回は英字のみに限定して不正な文字列が入りこまないようにする
    halt 500, 'error' unless /^[a-zA-z]+$/ =~ kind

    # 設定ファイルを読み込み
    data = YAML.load_file("messages.yaml")
    # 設定ファイル内に kind に対応する定義が存在すればそれを使う
    # 存在しなければ "default" を使う
    @kind = (data.include? kind) ? kind : "default"
    @def  = data[@kind]
end

get "/" do
    # 表示するタイトルを設定
    @title = @def["title"]
    erb :index
end

get "/result" do
    name  = params[:name]
    today = Time.now.strftime("%Y%m%d")
    num   = get_bytes_total(Digest::SHA1.hexdigest(name + today))

    # 診断結果は @def 内のmessagesから取得するよう修正
    msgs = @def["messages"].map do |msg_list|
        get_message(msg_list, num)
    end

    # タイトルを設定
    @title  = @def["title"]
    @name   = CGI.escapeHTML(name)
    # formatも @def から取得
    @result = sprintf(@def["format"], *msgs)
    erb :result
end

ンプレートを編集

最後にテンプレートを修正しましょう。主に追加になった kind パラメータの扱いに関する修正です。

<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8" />
    <link rel="stylesheet" type="text/css" href="/bootstrap/css/bootstrap.min.css" />
    <!-- 定数ではなく @title を出力するよう修正 -->
    <title><%= @title %></title>
</head>
<body>
    <div class="container">
        <%= yield %>
    </div>

    <script type="text/javascript" src="/bootstrap/js/bootstrap.min.js"></script>
</body>
</html>
<div class="hero-unit">
    <!-- 定数ではなく @title を出力するよう修正 -->
    <h1><%= @title %></h1>
    <p>Twitterのアカウント名から~を診断します。</p>

    <form method="GET" action="/result" class="well form-inline">
        <!-- 結果ページへ kind パラメータを引き継ぐよう修正 -->
        <input type="hidden" name="kind" value="<%= @kind %>" />
        <label>アカウント名 <input type="text" name="name" /></label>
        <button type="submit" class="btn btn-primary">診断する</button>
    </form>
</div>
<div class="page-header">
    <h1><%= @name %>さんの診断結果</h1>
</div>

<div class="well well-large">
    <p><%= @result %></p>
    <!-- サイトへのURLにkindパラメータを追加 -->
    <p><a target="_blank" href="http://twitter.com/?status=<%= @title %>: <%= @result %> http://<%= request.env['HTTP_HOST'] %>/?kind=<%= @kind %>">
        <i class="icon-share"></i>Twitterで共有する
    </a></p>
</div>

<!-- トップページのURLにkindパラメータを追加 -->
<p><a href="/?kind=<%= @kind %>"><i class="icon-home"></i>トップへ戻る</a></p>

ではサーバを再起動して確認してみましょう。
http://.../ でデフォルトの診断が、http://.../?kind=programming でプログラミング診断が 表示されることが確認できるでしょうか?

後に

今回はハンズオンの時間が90分しかないということで簡単なアプリの開発だけでしたが、 C4SA + Ruby + Sinatraでの開発フローはわかっていただけたと思います。
プログラムの実行環境をすぐに利用できるのがPaaSの強みなので これを機にウェブアプリ開発に興味を持っていただけたら幸いです!